From 903b3d25a887e89e16b300e1af5fc2f98174b94f Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 2 Mar 2026 15:43:28 +0100 Subject: [PATCH 01/62] localhost proxy doesn't support federation --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bda3823..e5ba037 100644 --- a/README.md +++ b/README.md @@ -276,8 +276,8 @@ proxy to allow using a wildcard domain. ### Setup - Set a DNS record for `*.haze.exmaple.com` and `haze.example.com` pointing to - `127.0.0.1` or your development machine if you need to access it from other - devices. + your development machine. Pointing it to `127.0.0.1` will also work, but comes + with limitations like federation not being supported. - Set the `proxy` configuration with your domain and desired listen endpoint. - Set up a service to run `haze proxy` in the background as your own user. A systemd user service is recommended (see [haze.service](./haze.service) for an From d89c547f2a5b595de60b4a47005b3a7ac7c1520f Mon Sep 17 00:00:00 2001 From: provokateurin Date: Thu, 5 Mar 2026 15:21:47 +0100 Subject: [PATCH 02/62] fix(install): Fix OCI Signed-off-by: provokateurin --- nix/image/scripts/install | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/image/scripts/install b/nix/image/scripts/install index d216118..2a86fcc 100755 --- a/nix/image/scripts/install +++ b/nix/image/scripts/install @@ -10,9 +10,9 @@ fi cd $WEBROOT -if [ "$SQL" = "oci" ]; then +if [ "$SQL" = "oracle" ]; then # oracle is a special snowflake - occ maintenance:install --admin-user=$USER --admin-pass=$PASSWORD --database=$SQL --database-name=xe --database-host=$SQL --database-user=system --database-pass=haze + occ maintenance:install --admin-user=$USER --admin-pass=$PASSWORD --database=oci --database-name=xe --database-host=$SQL --database-user=system --database-pass=haze elif [ "$SQL" = "mariadb" ]; then occ maintenance:install --admin-user=$USER --admin-pass=$PASSWORD --database=mysql --database-name=haze --database-host=$SQL --database-user=haze --database-pass=haze else From 862d33b0174f7c0be24a2dfe20655d05cfb92a81 Mon Sep 17 00:00:00 2001 From: provokateurin Date: Thu, 5 Mar 2026 15:32:11 +0100 Subject: [PATCH 03/62] feat(cloud): Kill containers before removing them for faster destruction Signed-off-by: provokateurin --- src/cloud.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/cloud.rs b/src/cloud.rs index 79d298c..d0b645c 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -499,6 +499,14 @@ impl Cloud { pub async fn destroy(self, docker: &Docker) -> Result<()> { for container in self.containers { + docker + .kill_container( + container.trim_start_matches('/'), + None, + ) + .await + .into_diagnostic() + .wrap_err("Failed to kill container")?; docker .remove_container( container.trim_start_matches('/'), From b7ea4e9760fc05d307bdff33e40efaec197cfa82 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 5 Mar 2026 23:27:54 +0100 Subject: [PATCH 04/62] allow configuring additional app directories and add a writable app directory fixes #15 --- README.md | 1 + nix/image/configs/nc/config.php | 12 +++++- src/args.rs | 1 + src/cloud.rs | 69 ++++++++++++++++++++++++--------- src/config.rs | 16 +++++++- src/mapping.rs | 65 +++++++++++++++++++++++++------ 6 files changed, 130 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index e5ba037..5eea607 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,7 @@ options ```toml sources_root = "/path/to/sources" # path of the nextcloud sources. required +app_directories = ["/path/to/sources/more_app"] # paths to additional app directories. work_dir = "/path/to/temp/dir" # path to temporary directory. optional, defaults to "/tmp/haze" [auto_setup] # optional diff --git a/nix/image/configs/nc/config.php b/nix/image/configs/nc/config.php index e8dc0af..49a135f 100644 --- a/nix/image/configs/nc/config.php +++ b/nix/image/configs/nc/config.php @@ -1,4 +1,12 @@ - true, 'appstoreenabled' => false, 'memcache.local' => '\\OC\\Memcache\\APCu', @@ -9,4 +17,4 @@ 'profiling.secret' => 'haze', 'profiling.path' => '/tmp/profiling', //PLACEHOLDER -]; +]); diff --git a/src/args.rs b/src/args.rs index f5434ca..e4e2b69 100644 --- a/src/args.rs +++ b/src/args.rs @@ -505,6 +505,7 @@ impl SubCommand for HazeCommand { fn test_arg_parse() { let config = HazeConfig { sources_root: Default::default(), + app_directories: Default::default(), work_dir: Default::default(), auto_setup: Default::default(), volume: vec![], diff --git a/src/cloud.rs b/src/cloud.rs index d0b645c..c4de8de 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -1,7 +1,7 @@ use crate::config::{HazeConfig, HazeVolumeConfig}; use crate::database::Database; use crate::exec::{exec, exec_io, exec_tty, ExitCode}; -use crate::mapping::{default_mappings, Mapping}; +use crate::mapping::{for_config, Mapping}; use crate::php::{PhpVersion, PHP_MEMORY_LIMIT}; use crate::service::Service; use crate::service::ServiceTrait; @@ -15,14 +15,14 @@ use flate2::read::GzDecoder; use futures_util::future::try_join_all; use miette::{IntoDiagnostic, Report, Result, WrapErr}; use petname::petname; -use serde_json::Value; +use serde_json::{Map, Value}; use std::borrow::Cow; use std::collections::HashMap; use std::fmt::Display; use std::fs; -use std::fs::read_to_string; +use std::fs::{read_to_string, write}; use std::io::{stdout, Cursor, Read, Stdout, Write}; -use std::iter::Peekable; +use std::iter::{once, Peekable}; use std::net::IpAddr; use std::os::unix::fs::MetadataExt; use std::str::FromStr; @@ -286,11 +286,8 @@ impl Cloud { }) }) .collect::>>()?; - let mappings = config - .volume - .iter() - .map(Mapping::from) - .chain(default_mappings()) + + let mappings = for_config(config) .chain(app_volumes.iter().map(Mapping::from)) .collect::>(); for mapping in &mappings { @@ -300,6 +297,48 @@ impl Cloud { .wrap_err_with(|| format!("Failed to setup work directory {}", mapping.source))?; } + let mut nc_config = Value::Object(Map::new()); + nc_config["apps_paths"] = Value::Array( + once("apps") + .chain( + config + .app_directories + .iter() + .filter_map(|dir| dir.file_name()), + ) + .map(|name| { + [ + ( + String::from("path"), + Value::from(format!("/var/www/html/{}", name)), + ), + (String::from("url"), Value::from(format!("/{}", name))), + (String::from("writable"), Value::from(false)), + ] + .into_iter() + .collect() + }) + .chain(once( + [ + ( + String::from("path"), + Value::from("/var/www/html/store_apps"), + ), + (String::from("url"), Value::from("/store_apps")), + (String::from("writable"), Value::from(true)), + ] + .into_iter() + .collect(), + )) + .collect(), + ); + write( + workdir.join("config/nextcloud.json"), + serde_json::to_string_pretty(&nc_config).unwrap(), + ) + .into_diagnostic() + .wrap_err("Failed to write config json")?; + let network = docker .create_network(NetworkCreateRequest { name: id.clone(), @@ -500,10 +539,7 @@ impl Cloud { pub async fn destroy(self, docker: &Docker) -> Result<()> { for container in self.containers { docker - .kill_container( - container.trim_start_matches('/'), - None, - ) + .kill_container(container.trim_start_matches('/'), None) .await .into_diagnostic() .wrap_err("Failed to kill container")?; @@ -802,12 +838,7 @@ impl Cloud { format!("/var/www/html/{path}").into() }; - let mut mappings = config - .volume - .iter() - .map(Mapping::from) - .chain(default_mappings()) - .collect::>(); + let mut mappings = for_config(config).collect::>(); mappings.sort_by_key(|mapping| usize::MAX - mapping.target.as_str().len()); for mapping in mappings { diff --git a/src/config.rs b/src/config.rs index 07cfcef..fad1d0a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,7 +4,7 @@ use miette::{IntoDiagnostic, Report, Result, WrapErr}; use serde::Deserialize; use std::collections::HashMap; use std::convert::TryFrom; -use std::env::var; +use std::env::home_dir; use std::fs::read_to_string; use std::net::IpAddr; use toml::Value; @@ -13,6 +13,7 @@ use toml::Value; #[serde(from = "RawHazeConfig")] pub struct HazeConfig { pub sources_root: Utf8PathBuf, + pub app_directories: Vec, pub work_dir: Utf8PathBuf, pub auto_setup: HazeAutoSetupConfig, pub volume: Vec, @@ -27,6 +28,8 @@ pub struct RawHazeConfig { #[serde(default = "default_work_dir")] pub work_dir: Utf8PathBuf, #[serde(default)] + pub app_directories: Vec, + #[serde(default)] pub auto_setup: HazeAutoSetupConfig, #[serde(default)] pub volume: Vec, @@ -42,7 +45,11 @@ impl From for HazeConfig { fn from(raw: RawHazeConfig) -> Self { fn normalize_path(path: Utf8PathBuf) -> Utf8PathBuf { if path.starts_with("~") { - let home = var("HOME").expect("HOME not set"); + let home = home_dir().expect("can't detect home directory"); + let home = home + .into_os_string() + .into_string() + .expect("non-utf8 home directory"); format!("{}{}", home, &path.as_str()[1..]).into() } else { path @@ -51,6 +58,11 @@ impl From for HazeConfig { HazeConfig { sources_root: normalize_path(raw.sources_root), + app_directories: raw + .app_directories + .into_iter() + .map(normalize_path) + .collect(), work_dir: normalize_path(raw.work_dir), auto_setup: raw.auto_setup, volume: raw.volume, diff --git a/src/mapping.rs b/src/mapping.rs index 4061100..36587ad 100644 --- a/src/mapping.rs +++ b/src/mapping.rs @@ -1,13 +1,14 @@ use crate::config::{HazeConfig, HazeVolumeConfig}; use camino::{Utf8Path, Utf8PathBuf}; use miette::{IntoDiagnostic, Result}; +use std::borrow::Cow; use tokio::fs::{create_dir_all, write}; #[derive(Debug)] pub struct Mapping<'a> { source_type: MappingSourceType, - pub source: &'a Utf8Path, - pub target: &'a Utf8Path, + pub source: Cow<'a, Utf8Path>, + pub target: Cow<'a, Utf8Path>, mapping_type: MappingType, read_only: bool, map: bool, @@ -23,6 +24,26 @@ impl<'a> Mapping<'a> { where Target: Into<&'a Utf8Path>, Source: Into<&'a Utf8Path>, + { + Mapping { + source_type, + source: Cow::Borrowed(source.into()), + target: Cow::Borrowed(target.into()), + mapping_type: MappingType::Folder, + read_only: false, + map: true, + create: true, + } + } + + pub fn owned( + source_type: MappingSourceType, + source: Source, + target: Target, + ) -> Self + where + Target: Into>, + Source: Into>, { Mapping { source_type, @@ -65,10 +86,10 @@ impl<'a> Mapping<'a> { return Ok(()); } let source = match self.source_type { - MappingSourceType::WorkDir => config.work_dir.join(id).join(self.source), - MappingSourceType::GlobalWorkDir => config.work_dir.join(self.source), + MappingSourceType::WorkDir => config.work_dir.join(id).join(self.source.as_ref()), + MappingSourceType::GlobalWorkDir => config.work_dir.join(self.source.as_ref()), MappingSourceType::Sources => return Ok(()), - MappingSourceType::Absolute => self.source.into(), + MappingSourceType::Absolute => self.source.as_ref().into(), }; match self.mapping_type { MappingType::Folder => create_dir_all(source).await.into_diagnostic()?, @@ -80,10 +101,10 @@ impl<'a> Mapping<'a> { pub fn source(&self, id: &str, config: &HazeConfig, source_root: &Utf8Path) -> Utf8PathBuf { match self.source_type { - MappingSourceType::WorkDir => config.work_dir.join(id).join(self.source), - MappingSourceType::GlobalWorkDir => config.work_dir.join(self.source), - MappingSourceType::Sources => source_root.join(self.source), - MappingSourceType::Absolute => self.source.into(), + MappingSourceType::WorkDir => config.work_dir.join(id).join(self.source.as_ref()), + MappingSourceType::GlobalWorkDir => config.work_dir.join(self.source.as_ref()), + MappingSourceType::Sources => source_root.join(self.source.as_ref()), + MappingSourceType::Absolute => self.source.as_ref().into(), } } @@ -112,6 +133,7 @@ pub fn default_mappings<'a>() -> impl IntoIterator> { Mapping::new(Sources, "", "/var/www/html"), Mapping::new(WorkDir, "data", "/var/www/html/data"), Mapping::new(WorkDir, "config", "/var/www/html/config"), + Mapping::new(WorkDir, "store_app", "/var/www/html/store_app"), Mapping::new(WorkDir, "data-autotest", "/var/www/html/data-autotest"), Mapping::new(WorkDir, "skeleton", "/var/www/html/core/skeleton"), Mapping::new( @@ -168,9 +190,30 @@ pub fn default_mappings<'a>() -> impl IntoIterator> { Mapping::new(WorkDir, "profiling", "/tmp/profiling"), Mapping::new(WorkDir, "php-config", "/config"), ]; + IntoIterator::into_iter(mappings) } +pub fn for_config<'a>(config: &'a HazeConfig) -> impl Iterator> { + let app_dir_mappings = config.app_directories.iter().map(|dir| { + Mapping::owned( + MappingSourceType::Absolute, + dir.as_path(), + Cow::Owned(Utf8PathBuf::from(format!( + "/var/www/html/{}", + dir.file_name().unwrap() + ))), + ) + }); + + config + .volume + .iter() + .map(Mapping::from) + .chain(app_dir_mappings) + .chain(default_mappings()) +} + #[derive(Debug, Copy, Clone)] pub enum MappingSourceType { Sources, @@ -194,8 +237,8 @@ impl<'a> From<&'a HazeVolumeConfig> for Mapping<'a> { }; Mapping { source_type: MappingSourceType::Absolute, - source: config.source.as_path(), - target: config.target.as_path(), + source: Cow::Borrowed(config.source.as_path()), + target: Cow::Borrowed(config.target.as_path()), mapping_type: ty, read_only: config.read_only, map: true, From 9629dea8dfa2d6943e23d1d5d28113690fa1c36c Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 5 Mar 2026 23:36:47 +0100 Subject: [PATCH 05/62] enable appstore --- nix/image/configs/nc/config.php | 1 - src/mapping.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/nix/image/configs/nc/config.php b/nix/image/configs/nc/config.php index 49a135f..b00d47b 100644 --- a/nix/image/configs/nc/config.php +++ b/nix/image/configs/nc/config.php @@ -8,7 +8,6 @@ if (file_exists(__DIR__ . '/nextcloud.json')) { $CONFIG = array_merge_recursive($extra_config, [ 'debug' => true, - 'appstoreenabled' => false, 'memcache.local' => '\\OC\\Memcache\\APCu', 'memcache.distributed' => '\\OC\\Memcache\\APCu', 'memcache.locking' => '\\OC\\Memcache\\APCu', diff --git a/src/mapping.rs b/src/mapping.rs index 36587ad..1e04ffb 100644 --- a/src/mapping.rs +++ b/src/mapping.rs @@ -133,7 +133,7 @@ pub fn default_mappings<'a>() -> impl IntoIterator> { Mapping::new(Sources, "", "/var/www/html"), Mapping::new(WorkDir, "data", "/var/www/html/data"), Mapping::new(WorkDir, "config", "/var/www/html/config"), - Mapping::new(WorkDir, "store_app", "/var/www/html/store_app"), + Mapping::new(WorkDir, "store_apps", "/var/www/html/store_apps"), Mapping::new(WorkDir, "data-autotest", "/var/www/html/data-autotest"), Mapping::new(WorkDir, "skeleton", "/var/www/html/core/skeleton"), Mapping::new( From 1a6dd904101cbff2d6848cfd90fb8d76c7db3859 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 6 Mar 2026 00:15:35 +0100 Subject: [PATCH 06/62] add appDirectories to hm module --- nix/hm-module.nix | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nix/hm-module.nix b/nix/hm-module.nix index 096d853..072fda4 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -9,6 +9,7 @@ with lib; let format = pkgs.formats.toml {}; configFile = format.generate "haze.toml" ({ sources_root = cfg.sourcesRoot; + app_directories = cfg.appDirectories; work_dir = cfg.workDir; auto_setup = { enabled = cfg.autoSetup.enable; @@ -61,6 +62,12 @@ in { description = "Directory to store instance data"; }; + appDirectories = mkOption { + type = types.listOf types.str; + default = []; + description = "Paths to additional app directories"; + }; + autoSetup = mkOption { type = types.submodule { options = { From e9cb4f08e3e7ea709441638a9682392598367e19 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 6 Mar 2026 00:15:35 +0100 Subject: [PATCH 07/62] use extra app directories for git operations --- src/args.rs | 17 +++++++++++++---- src/git.rs | 39 +++++++++++++++++++++++++-------------- src/main.rs | 7 ++++--- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/args.rs b/src/args.rs index e4e2b69..9b7a2cc 100644 --- a/src/args.rs +++ b/src/args.rs @@ -124,9 +124,12 @@ pub enum GitOperation { /// /// "main" and "master" can be used interchangeably. #[strum(props( - Args = "[branch] branch to checkout, defaults to the branch matching the current server version" + Args = "[branch] branch to checkout, defaults to the branch matching the current server version [-v] verbose" ))] - Checkout { branch: Option }, + Checkout { + branch: Option, + verbose: bool, + }, } impl SubCommand for GitOperation { @@ -320,18 +323,24 @@ impl HazeArgs { HazeCommand::Checkout => { let branch = args.next().map(S::into); Ok(HazeArgs::Git { - operation: GitOperation::Checkout { branch }, + operation: GitOperation::Checkout { + branch, + verbose: false, + }, }) } HazeCommand::Git => { + let mut args = args.peekable(); let operation = args .next() .ok_or_else(|| Report::msg("No git operation provided"))?; match operation.as_ref() { "checkout" => { + let verbose = args.next_if(|arg| arg.as_ref() == "-v").is_some(); let branch = args.next().map(S::into); + let verbose = verbose | args.next_if(|arg| arg.as_ref() == "-v").is_some(); Ok(HazeArgs::Git { - operation: GitOperation::Checkout { branch }, + operation: GitOperation::Checkout { branch, verbose }, }) } "pull" => Ok(HazeArgs::Git { diff --git a/src/git.rs b/src/git.rs index d08f2f3..3a797c3 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,22 +1,29 @@ +use crate::config::HazeConfig; use crate::Result; use git2::build::CheckoutBuilder; use git2::{Branch, BranchType, Repository, RepositoryState}; use miette::{Context, IntoDiagnostic}; use std::fs::read_dir; -use std::path::{Path, PathBuf}; +use std::iter::once; +use std::path::PathBuf; use std::process::Command; -fn find_app_repos(root: impl AsRef) -> Result> { - let apps_dir = root.as_ref().join("apps"); - Ok(read_dir(apps_dir) - .into_diagnostic()? +fn find_app_repos(config: &HazeConfig) -> Result> { + let apps_dirs = once(config.sources_root.as_path().join("apps")) + .chain(config.app_directories.iter().cloned()); + let dir_handles = apps_dirs + .map(|dir| read_dir(dir).into_diagnostic()) + .collect::>>()?; + Ok(dir_handles + .into_iter() + .flatten() .flatten() .filter(|app| app.path().join(".git").is_dir()) .map(|app| app.path())) } -fn longest_app_branch(root: impl AsRef) -> Result<(usize, usize)> { - Ok(find_app_repos(root)? +fn longest_app_branch(config: &HazeConfig) -> Result<(usize, usize)> { + Ok(find_app_repos(config)? .filter_map(|app_dir| { let app_name = app_dir.file_name()?.to_str()?; let repo = Repository::init(&app_dir).ok()?; @@ -27,27 +34,32 @@ fn longest_app_branch(root: impl AsRef) -> Result<(usize, usize)> { .unwrap_or_default()) } -pub fn checkout_all>(sources_root: P, mut name: &str) -> Result<()> { +pub fn checkout_all(config: &HazeConfig, mut name: &str, verbose: bool) -> Result<()> { // "main" and "master" are interchangeable if name == "main" { name = "master"; } - for app_dir in find_app_repos(sources_root)? { + for app_dir in find_app_repos(config)? { let repo = Repository::init(&app_dir) .into_diagnostic() .wrap_err_with(|| format!("Failed to open repository {}", app_dir.display()))?; + let app_name = app_dir.file_name().unwrap().to_string_lossy(); if let Some(branch) = get_branch(&repo, name)? { if !branch.is_head() { let is_remote = branch.get().is_remote(); - print!("{}", app_dir.file_name().unwrap().to_string_lossy()); + print!("{app_name}"); if let Err(e) = checkout(&repo, &branch, is_remote.then_some(name)) { println!(": {:#} ❌", e); } else { println!(" ✓"); } + } else if verbose { + println!("{app_name} -"); } + } else if verbose { + println!("{app_name} 🛇 Branch not found"); }; } Ok(()) @@ -68,11 +80,10 @@ const GIT_BINARY: &str = match option_env!("GIT_BINARY") { None => "git", }; -pub fn pull_all>(sources_root: P) -> Result<()> { - let sources_root = sources_root.as_ref(); - let (max_app, max_branch) = longest_app_branch(sources_root)?; +pub fn pull_all(config: &HazeConfig) -> Result<()> { + let (max_app, max_branch) = longest_app_branch(config)?; - for app_dir in find_app_repos(sources_root)? { + for app_dir in find_app_repos(config)? { let app_name = app_dir.file_name().unwrap().to_string_lossy(); let repo = Repository::init(&app_dir) .into_diagnostic() diff --git a/src/main.rs b/src/main.rs index 5deb172..5eb6e0c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -373,15 +373,16 @@ async fn main() -> Result { proxy(docker, config).await?; } HazeArgs::Git { operation } => match operation { - GitOperation::Checkout { branch } => { + GitOperation::Checkout { branch, verbose } => { let sources = Sources::new(&config.sources_root)?; checkout_all( - &config.sources_root, + &config, &branch.unwrap_or_else(|| sources.get_server_version_branch()), + verbose, )?; } GitOperation::Pull => { - pull_all(&config.sources_root)?; + pull_all(&config)?; } }, HazeArgs::Env { From bf7a8d9a3445da462a184297726944ef8e2dc748 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 6 Mar 2026 00:48:12 +0100 Subject: [PATCH 08/62] dont polute store_app --- nix/image/nginx.conf | 10 ++++++++++ src/cloud.rs | 5 +---- src/mapping.rs | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/nix/image/nginx.conf b/nix/image/nginx.conf index 3de81ca..6e707c2 100644 --- a/nix/image/nginx.conf +++ b/nix/image/nginx.conf @@ -69,6 +69,16 @@ http { access_log off; } + location ^~ /store_apps { + root /var/www; + try_files $uri /index.php$request_uri; + access_log off; # Optional: Don't log access to assets + + location ~ \.wasm$ { + default_type application/wasm; + } + } + include /conf/nginx-app.conf; location ~ \.php(?:$|/) { diff --git a/src/cloud.rs b/src/cloud.rs index c4de8de..7232e33 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -320,10 +320,7 @@ impl Cloud { }) .chain(once( [ - ( - String::from("path"), - Value::from("/var/www/html/store_apps"), - ), + (String::from("path"), Value::from("/var/www/store_apps")), (String::from("url"), Value::from("/store_apps")), (String::from("writable"), Value::from(true)), ] diff --git a/src/mapping.rs b/src/mapping.rs index 1e04ffb..9c01da0 100644 --- a/src/mapping.rs +++ b/src/mapping.rs @@ -133,7 +133,7 @@ pub fn default_mappings<'a>() -> impl IntoIterator> { Mapping::new(Sources, "", "/var/www/html"), Mapping::new(WorkDir, "data", "/var/www/html/data"), Mapping::new(WorkDir, "config", "/var/www/html/config"), - Mapping::new(WorkDir, "store_apps", "/var/www/html/store_apps"), + Mapping::new(WorkDir, "store_apps", "/var/www/store_apps"), Mapping::new(WorkDir, "data-autotest", "/var/www/html/data-autotest"), Mapping::new(WorkDir, "skeleton", "/var/www/html/core/skeleton"), Mapping::new( From 8e79e997a89f19b5caf8eae2ab7c3ec9f1f73294 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 9 Mar 2026 17:16:24 +0100 Subject: [PATCH 09/62] make proxy work for object store --- src/service/objectstore.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/service/objectstore.rs b/src/service/objectstore.rs index 2225dca..4f0b7d1 100644 --- a/src/service/objectstore.rs +++ b/src/service/objectstore.rs @@ -187,4 +187,11 @@ impl ServiceTrait for ObjectStore { Ok(Vec::new()) } } + + fn proxy_port(&self) -> u16 { + match self { + ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb => 9000, + ObjectStore::Azure => 10000, + } + } } From fdc821cb93f3f06b4fd53cc34474310873ab005d Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 9 Mar 2026 17:35:47 +0100 Subject: [PATCH 10/62] allow bare-service proxy hosts --- src/cloud.rs | 8 +++++- src/proxy.rs | 74 ++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 58 insertions(+), 24 deletions(-) diff --git a/src/cloud.rs b/src/cloud.rs index 7232e33..a1c39c9 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -218,7 +218,7 @@ fn test_option_parse() { ); } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Cloud { pub id: String, pub network: String, @@ -851,3 +851,9 @@ impl Cloud { None } } + +impl PartialEq for Cloud { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} diff --git a/src/proxy.rs b/src/proxy.rs index ee867c9..c99c216 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -1,4 +1,4 @@ -use crate::service::ServiceTrait; +use crate::service::{ServiceTrait, ServiceType}; use crate::Result; use crate::{Cloud, HazeConfig}; use axum::http::header::HOST; @@ -18,6 +18,7 @@ use std::fs::{create_dir_all, set_permissions}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; +use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::net::UnixListener; @@ -28,7 +29,7 @@ use tracing::{debug, error, info}; struct ActiveInstances { known: Mutex>, - last: Mutex>, + last: Mutex>, docker: Docker, config: HazeConfig, } @@ -48,15 +49,9 @@ impl ActiveInstances { return Some(ip); } - // service proxy - let addr = if name.matches('-').count() == 2 { - let (name, service_name) = name.rsplit_once('-').unwrap(); - let cloud = Cloud::get_by_filter(&self.docker, Some(name.into()), &self.config) - .await - .ok()?; - let service = cloud - .services() - .find(|service| service.name() == service_name)?; + let addr = if ServiceType::from_str(name).is_ok() { + let cloud = self.last()?; + let service = cloud.services().find(|service| service.name() == name)?; let ip = service .get_ips(&self.docker, &cloud.id) .await @@ -64,13 +59,33 @@ impl ActiveInstances { .next()?; SocketAddr::new(ip, service.proxy_port()) } else { - SocketAddr::new( - Cloud::get_by_filter(&self.docker, Some(name.into()), &self.config) - .await - .ok()? - .ip?, - 80, - ) + match name.matches('-').count() { + // instance + 1 => SocketAddr::new( + Cloud::get_by_filter(&self.docker, Some(name.into()), &self.config) + .await + .ok()? + .ip?, + 80, + ), + // service with instance + 2 => { + let (name, service_name) = name.rsplit_once('-').unwrap(); + let cloud = Cloud::get_by_filter(&self.docker, Some(name.into()), &self.config) + .await + .ok()?; + let service = cloud + .services() + .find(|service| service.name() == service_name)?; + let ip = service + .get_ips(&self.docker, &cloud.id) + .await + .ok()? + .next()?; + SocketAddr::new(ip, service.proxy_port()) + } + _ => return None, + } }; println!("{name} => {addr}"); @@ -79,18 +94,31 @@ impl ActiveInstances { Some(addr) } - pub fn last(&self) -> Option { - *self.last.lock().unwrap() + pub fn last_addr(&self) -> Option { + self.last + .lock() + .unwrap() + .as_ref() + .and_then(|cloud| Some(SocketAddr::new(cloud.ip?, 80))) + } + + pub fn last(&self) -> Option { + self.last.lock().unwrap().clone() } async fn update_last(&self) { let last = Cloud::get_by_filter(&self.docker, None, &self.config) .await - .ok() - .and_then(|cloud| Some(SocketAddr::new(cloud.ip?, 80))); + .ok(); let mut old = self.last.lock().unwrap(); if old.as_ref() != last.as_ref() { info!(instance = ?last, "Found new instance"); + + // remove cached base-service mappings + self.known + .lock() + .unwrap() + .retain(|key, _| ServiceType::from_str(key).is_err()); *old = last; } } @@ -181,7 +209,7 @@ async fn get_remote( }; let ip = if host == base_address { instances - .last() + .last_addr() .ok_or_else(|| String::from("No running instance known")) } else { let requested_instance = host.split('.').next().unwrap(); From 85071d7aa1555026658aeb456d0544f889b4a0e4 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 9 Mar 2026 17:48:57 +0100 Subject: [PATCH 11/62] allow service names with - in them in proxy --- src/proxy.rs | 5 +++-- src/service.rs | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/proxy.rs b/src/proxy.rs index c99c216..d5a3b0e 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -69,8 +69,9 @@ impl ActiveInstances { 80, ), // service with instance - 2 => { - let (name, service_name) = name.rsplit_once('-').unwrap(); + 2.. => { + let service_name = name.splitn(3, '-').last()?; + let name = &name[0..(name.len() - service_name.len() - 1)]; let cloud = Cloud::get_by_filter(&self.docker, Some(name.into()), &self.config) .await .ok()?; diff --git a/src/service.rs b/src/service.rs index 8874352..77ea246 100644 --- a/src/service.rs +++ b/src/service.rs @@ -208,6 +208,8 @@ pub enum ServiceType { Azure, /// Ldap user backend Ldap, + /// Ldap admin interface + LdapAdmin, /// OnlyOffice OnlyOffice, /// Libre office online @@ -304,6 +306,9 @@ impl Service { 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)]), From 04b8ec975dcca5b99553b020d11a6165d02c24fe Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 9 Mar 2026 17:56:14 +0100 Subject: [PATCH 12/62] fix ldap admin --- src/service/ldap.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/service/ldap.rs b/src/service/ldap.rs index 7526918..66d1d14 100644 --- a/src/service/ldap.rs +++ b/src/service/ldap.rs @@ -115,7 +115,10 @@ impl ServiceTrait for LdapAdmin { }); let config = ContainerCreateBody { image: Some(image.into()), - env: Some(vec!["PHPLDAPADMIN_LDAP_HOSTS=ldap".into()]), + env: Some(vec![ + "PHPLDAPADMIN_LDAP_HOSTS=ldap".into(), + "PHPLDAPADMIN_HTTPS=false".into(), + ]), host_config: Some(HostConfig { network_mode: Some(network.to_string()), ..Default::default() From e76678ec14287c949f28d31a42a740b369583eb8 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 9 Mar 2026 17:56:14 +0100 Subject: [PATCH 13/62] document service proxy --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 5eea607..3bb98ca 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,12 @@ subdomain of the configured domain, e.g. the `rolling-bees` instance will be available at `rolling-bees.haze.example.com`. Additionally, `haze.example.com` will automatically point to the last created instance. +Additionally, the proxy allows access to the server containers trough either +`-.haze.example.com` for a specific instance, or +`.haze.example.com` for the last created instead. For example +`rolling-bees-mail.haze.example.com` will give access to the smtp4dev web +interface of the `rolling-bees` instance. + ## Configuration Configuration is loaded from `~/.config/haze/haze.toml` and has the following From 88a41003401bb93af3c453efef0ae3e6e678851c Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 9 Mar 2026 18:08:34 +0100 Subject: [PATCH 14/62] show proxy addr for ldap-admin in startup message --- src/cloud.rs | 6 +++++- src/config.rs | 2 +- src/service.rs | 9 +++++++-- src/service/ldap.rs | 18 +++++++++++++----- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/cloud.rs b/src/cloud.rs index a1c39c9..8437d63 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -493,6 +493,7 @@ impl Cloud { containers.push(container); let options_clone = options.clone(); + let proxy_config = config.proxy.clone(); let cloud_id = id.clone(); let docker_clone = docker.clone(); spawn(async move { @@ -506,7 +507,10 @@ impl Cloud { return; } for service in options_clone.services { - match service.start_message(&docker_clone, &cloud_id).await { + match service + .start_message(&docker_clone, &cloud_id, &proxy_config) + .await + { Ok(Some(msg)) => { println!("{}", msg); } diff --git a/src/config.rs b/src/config.rs index fad1d0a..b790b52 100644 --- a/src/config.rs +++ b/src/config.rs @@ -181,7 +181,7 @@ fn load_secret(name: &str, path: Option, raw: Option) -> Result< } } -#[derive(Default, Deserialize, Debug)] +#[derive(Default, Deserialize, Debug, Clone)] pub struct ProxyConfig { pub listen: String, #[serde(default)] diff --git a/src/service.rs b/src/service.rs index 77ea246..344ec49 100644 --- a/src/service.rs +++ b/src/service.rs @@ -16,7 +16,7 @@ mod sharded; mod smb; use crate::cloud::CloudOptions; -use crate::config::{HazeConfig, Preset}; +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; @@ -78,7 +78,12 @@ pub trait ServiceTrait { None } - async fn start_message(&self, _docker: &Docker, _cloud_id: &str) -> Result> { + async fn start_message( + &self, + _docker: &Docker, + _cloud_id: &str, + _proxy: &ProxyConfig, + ) -> Result> { Ok(None) } diff --git a/src/service/ldap.rs b/src/service/ldap.rs index 66d1d14..aa082b0 100644 --- a/src/service/ldap.rs +++ b/src/service/ldap.rs @@ -1,5 +1,5 @@ use crate::cloud::CloudOptions; -use crate::config::HazeConfig; +use crate::config::{HazeConfig, ProxyConfig}; use crate::image::pull_image; use crate::service::ServiceTrait; use crate::Result; @@ -9,6 +9,8 @@ use bollard::query_parameters::CreateContainerOptions; use bollard::Docker; use maplit::hashmap; use miette::{IntoDiagnostic, Report}; +use std::net::IpAddr; +use std::str::FromStr; #[derive(Debug, Clone, Eq, PartialEq)] pub struct Ldap; @@ -151,9 +153,15 @@ impl ServiceTrait for LdapAdmin { Some(format!("{}-ldap-admin", cloud_id)) } - async fn start_message(&self, docker: &Docker, cloud_id: &str) -> Result> { + async fn start_message( + &self, + docker: &Docker, + cloud_id: &str, + proxy: &ProxyConfig, + ) -> Result> { + let id = self.container_name(cloud_id).unwrap(); let info = docker - .inspect_container(&self.container_name(cloud_id).unwrap(), None) + .inspect_container(&id, None) .await .into_diagnostic()?; let ip = if matches!( @@ -176,9 +184,9 @@ impl ServiceTrait for LdapAdmin { } else { return Err(Report::msg("ldap admin not started")); }; + let addr = proxy.addr(&id, IpAddr::from_str(&ip).unwrap()); Ok(Some(format!( - "Ldap admin running at: https://{} with 'cn=admin,dc=example,dc=org' and password 'haze'", - ip + "Ldap admin running at: {addr} with 'cn=admin,dc=example,dc=org' and password 'haze'" ))) } } From 80d71bd7a0107a3b3e84007d9f8de4b6f5f33a65 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 9 Mar 2026 18:30:32 +0100 Subject: [PATCH 15/62] move redis certificates --- {redis-certificates => certificates/redis}/ca.crt | 0 {redis-certificates => certificates/redis}/ca.key | 0 {redis-certificates => certificates/redis}/ca.txt | 0 .../redis}/client.crt | 0 .../redis}/client.key | 0 .../redis}/openssl.cnf | 0 .../redis}/redis.crt | 0 .../redis}/redis.dh | 0 .../redis}/redis.key | 0 .../redis}/server.crt | 0 .../redis}/server.key | 0 flake.nix | 2 +- nix/image/bootstrap.sh | 6 +++--- nix/image/configs/nc/redis-tls.php | 6 +++--- nix/image/haze.nix | 6 +++--- nix/package.nix | 2 +- src/main.rs | 14 +++++++------- 17 files changed, 18 insertions(+), 18 deletions(-) rename {redis-certificates => certificates/redis}/ca.crt (100%) rename {redis-certificates => certificates/redis}/ca.key (100%) rename {redis-certificates => certificates/redis}/ca.txt (100%) rename {redis-certificates => certificates/redis}/client.crt (100%) rename {redis-certificates => certificates/redis}/client.key (100%) rename {redis-certificates => certificates/redis}/openssl.cnf (100%) rename {redis-certificates => certificates/redis}/redis.crt (100%) rename {redis-certificates => certificates/redis}/redis.dh (100%) rename {redis-certificates => certificates/redis}/redis.key (100%) rename {redis-certificates => certificates/redis}/server.crt (100%) rename {redis-certificates => certificates/redis}/server.key (100%) diff --git a/redis-certificates/ca.crt b/certificates/redis/ca.crt similarity index 100% rename from redis-certificates/ca.crt rename to certificates/redis/ca.crt diff --git a/redis-certificates/ca.key b/certificates/redis/ca.key similarity index 100% rename from redis-certificates/ca.key rename to certificates/redis/ca.key diff --git a/redis-certificates/ca.txt b/certificates/redis/ca.txt similarity index 100% rename from redis-certificates/ca.txt rename to certificates/redis/ca.txt diff --git a/redis-certificates/client.crt b/certificates/redis/client.crt similarity index 100% rename from redis-certificates/client.crt rename to certificates/redis/client.crt diff --git a/redis-certificates/client.key b/certificates/redis/client.key similarity index 100% rename from redis-certificates/client.key rename to certificates/redis/client.key diff --git a/redis-certificates/openssl.cnf b/certificates/redis/openssl.cnf similarity index 100% rename from redis-certificates/openssl.cnf rename to certificates/redis/openssl.cnf diff --git a/redis-certificates/redis.crt b/certificates/redis/redis.crt similarity index 100% rename from redis-certificates/redis.crt rename to certificates/redis/redis.crt diff --git a/redis-certificates/redis.dh b/certificates/redis/redis.dh similarity index 100% rename from redis-certificates/redis.dh rename to certificates/redis/redis.dh diff --git a/redis-certificates/redis.key b/certificates/redis/redis.key similarity index 100% rename from redis-certificates/redis.key rename to certificates/redis/redis.key diff --git a/redis-certificates/server.crt b/certificates/redis/server.crt similarity index 100% rename from redis-certificates/server.crt rename to certificates/redis/server.crt diff --git a/redis-certificates/server.key b/certificates/redis/server.key similarity index 100% rename from redis-certificates/server.key rename to certificates/redis/server.key diff --git a/flake.nix b/flake.nix index 641c5fc..4d2f05b 100644 --- a/flake.nix +++ b/flake.nix @@ -24,7 +24,7 @@ }; extraPaths = [ - ./redis-certificates + ./certificates ]; withOverlays = [ diff --git a/nix/image/bootstrap.sh b/nix/image/bootstrap.sh index 5f85777..50c14c2 100755 --- a/nix/image/bootstrap.sh +++ b/nix/image/bootstrap.sh @@ -59,9 +59,9 @@ if [ -n "${REDIS_TLS:-}" ] then redis-server --protected-mode no \ --tls-port 6379 --port 0 \ - --tls-cert-file /redis-certificates/server.crt \ - --tls-key-file /redis-certificates/server.key \ - --tls-ca-cert-file /redis-certificates/ca.crt & + --tls-cert-file /certificates/redis/server.crt \ + --tls-key-file /certificates/redis/server.key \ + --tls-ca-cert-file /certificates/redis/ca.crt & else redis-server --protected-mode no & fi diff --git a/nix/image/configs/nc/redis-tls.php b/nix/image/configs/nc/redis-tls.php index fecc760..cb454ac 100644 --- a/nix/image/configs/nc/redis-tls.php +++ b/nix/image/configs/nc/redis-tls.php @@ -2,9 +2,9 @@ 'host' => 'tls://127.0.0.1', 'port' => 6379, 'ssl_context' => [ - 'local_cert' => '/redis-certificates/client.crt', - 'local_pk' => '/redis-certificates/client.key', - 'cafile' => '/redis-certificates/ca.crt', + 'local_cert' => '/certificates/redis/client.crt', + 'local_pk' => '/certificates/redis/client.key', + 'cafile' => '/certificates/redis/ca.crt', 'verify_peer_name' => false, ], ], diff --git a/nix/image/haze.nix b/nix/image/haze.nix index 38bc8f9..926f0fa 100644 --- a/nix/image/haze.nix +++ b/nix/image/haze.nix @@ -104,9 +104,9 @@ ''; }; - redis-certificates = runCommand "scripts" {} '' + certificates = runCommand "scripts" {} '' mkdir -p $out - cp -r ${../../redis-certificates} $out/redis-certificates + cp -r ${../../certificates} $out/certificates ''; clamav-data = runCommand "scripts" {} '' mkdir -p $out/etc @@ -170,7 +170,7 @@ in bootstrap configs scripts - redis-certificates + certificates clamav-data shadowSetupScript ]; diff --git a/nix/package.nix b/nix/package.nix index d79a216..fcd6489 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -7,7 +7,7 @@ inherit (lib) getExe; inherit (lib.sources) sourceByRegex; inherit (builtins) fromTOML readFile; - src = sourceByRegex ../. ["Cargo.*" "(src|redis-certificates)(/.*)?"]; + src = sourceByRegex ../. ["Cargo.*" "(src|certificates)(/.*)?"]; version = (fromTOML (readFile ../Cargo.toml)).package.version; in rustPlatform.buildRustPackage rec { diff --git a/src/main.rs b/src/main.rs index 5eb6e0c..2e4de9b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -421,16 +421,16 @@ async fn main() -> Result { ); if cloud.services().contains(&Service::RedisTls(RedisTls)) { - create_dir_all(config.work_dir.join("redis_certificates")) + create_dir_all(config.work_dir.join("certificates/redis")) .into_diagnostic() .wrap_err("Failed to create redis certificate directory")?; - let redis_cert_path = config.work_dir.join("redis_certificates/client.cert"); - let redis_key_path = config.work_dir.join("redis_certificates/client.key"); - let redis_ca_path = config.work_dir.join("redis_certificates/ca.cert"); + let redis_cert_path = config.work_dir.join("certificates/redis/client.cert"); + let redis_key_path = config.work_dir.join("certificates/redis/client.key"); + let redis_ca_path = config.work_dir.join("certificates/redis/ca.cert"); if !redis_cert_path.exists() { write( &redis_cert_path, - include_bytes!("../redis-certificates/client.crt"), + include_bytes!("../certificates/redis/client.crt"), ) .into_diagnostic() .wrap_err("Failed to write redis client certificate")?; @@ -438,7 +438,7 @@ async fn main() -> Result { if !redis_key_path.exists() { write( &redis_key_path, - include_bytes!("../redis-certificates/client.key"), + include_bytes!("../certificates/redis/client.key"), ) .into_diagnostic() .wrap_err("Failed to write redis client key")?; @@ -446,7 +446,7 @@ async fn main() -> Result { if !redis_ca_path.exists() { write( &redis_ca_path, - include_bytes!("../redis-certificates/ca.crt"), + include_bytes!("../certificates/redis/ca.crt"), ) .into_diagnostic() .wrap_err("Failed to write redis ca certificate")?; From b3a1e80f6f967c1c77f1be47cb0a0ae438e912ab Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 9 Mar 2026 18:42:43 +0100 Subject: [PATCH 16/62] allow custom pre-setup config options --- README.md | 1 + nix/hm-module.nix | 9 ++++++++- nix/image/configs/nc/config.php | 4 ++-- src/cloud.rs | 12 ++++++++++-- src/config.rs | 4 ++++ 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3bb98ca..0ad77bc 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,7 @@ disable_apps = ["contacts"] # apps to disable after setup, defaults to [] post_setup = [# commands to execute after setup, defaults to [] "occ group:add test", ] +config = { "foo" = "bar" } # configuration options to set before install [[volume]] # optional source = "/tmp/haze-shared" diff --git a/nix/hm-module.nix b/nix/hm-module.nix index 072fda4..7fe684d 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -12,7 +12,7 @@ with lib; let app_directories = cfg.appDirectories; work_dir = cfg.workDir; auto_setup = { - enabled = cfg.autoSetup.enable; + inherit (cfg.autoSetup) enable config; post_setup = cfg.autoSetup.postSetup; enable_apps = cfg.autoSetup.enableApps; disable_apps = cfg.autoSetup.disableApps; @@ -91,6 +91,13 @@ in { default = []; description = "Commands to run post-setup"; }; + config = mkOption { + type = types.submodule { + freeformType = format.type; + }; + description = "Configuration options to set before install"; + default = {}; + }; }; }; }; diff --git a/nix/image/configs/nc/config.php b/nix/image/configs/nc/config.php index b00d47b..06de6db 100644 --- a/nix/image/configs/nc/config.php +++ b/nix/image/configs/nc/config.php @@ -9,8 +9,8 @@ if (file_exists(__DIR__ . '/nextcloud.json')) { $CONFIG = array_merge_recursive($extra_config, [ 'debug' => true, 'memcache.local' => '\\OC\\Memcache\\APCu', - 'memcache.distributed' => '\\OC\\Memcache\\APCu', - 'memcache.locking' => '\\OC\\Memcache\\APCu', + 'memcache.distributed' => '\\OC\\Memcache\\Redis', + 'memcache.locking' => '\\OC\\Memcache\\Redis', 'allow_local_remote_servers' => true, 'trusted_domains' => ['cloud'], 'profiling.secret' => 'haze', diff --git a/src/cloud.rs b/src/cloud.rs index 8437d63..0c52e13 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -15,7 +15,7 @@ use flate2::read::GzDecoder; use futures_util::future::try_join_all; use miette::{IntoDiagnostic, Report, Result, WrapErr}; use petname::petname; -use serde_json::{Map, Value}; +use serde_json::Value; use std::borrow::Cow; use std::collections::HashMap; use std::fmt::Display; @@ -297,7 +297,15 @@ impl Cloud { .wrap_err_with(|| format!("Failed to setup work directory {}", mapping.source))?; } - let mut nc_config = Value::Object(Map::new()); + let mut nc_config = Value::Object( + config + .auto_setup + .config + .clone() + .into_iter() + .map(|(key, value)| (key, serde_json::to_value(value).unwrap())) + .collect(), + ); nc_config["apps_paths"] = Value::Array( once("apps") .chain( diff --git a/src/config.rs b/src/config.rs index b790b52..9e4548f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,6 +7,7 @@ use std::convert::TryFrom; use std::env::home_dir; use std::fs::read_to_string; use std::net::IpAddr; +use toml::map::Map; use toml::Value; #[derive(Debug, Deserialize, Default)] @@ -87,6 +88,8 @@ pub struct HazeAutoSetupConfig { pub disable_apps: Vec, #[serde(default)] pub post_setup: Vec, + #[serde(default)] + pub config: Map, } impl Default for HazeAutoSetupConfig { @@ -98,6 +101,7 @@ impl Default for HazeAutoSetupConfig { enable_apps: Vec::default(), disable_apps: Vec::default(), post_setup: Vec::default(), + config: Map::default(), } } } From 7e54fbd89fd0850505c169c99eeb267be381d880 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 9 Mar 2026 20:10:23 +0100 Subject: [PATCH 17/62] add s3 TLS option --- README.md | 9 ++- certificates/s3/private.key | 28 ++++++++ certificates/s3/public.crt | 21 ++++++ nix/image/configs/nc/s3s.php | 15 ++++ nix/image/scripts/nc-auto-config | 5 ++ src/cloud.rs | 16 +++++ src/service.rs | 12 ++++ src/service/objectstore.rs | 113 +++++++++++++++++++++++++++---- 8 files changed, 203 insertions(+), 16 deletions(-) create mode 100644 certificates/s3/private.key create mode 100644 certificates/s3/public.crt create mode 100644 nix/image/configs/nc/s3s.php diff --git a/README.md b/README.md index 0ad77bc..7c5e334 100644 --- a/README.md +++ b/README.md @@ -76,9 +76,9 @@ Additionally, you can use the following options when starting an instance: - `s3`: set up an S3 server and configure to Nextcloud to use it as primary storage. -- ``: by specifying the path to an app package this package - will be extracted into the apps. directory of the new instance (overwriting - any existing app code). This can be used to quickly test a packaged app. + - `s3s`: enable TLS for the S3 setup. + - `s3mb`: enable multi-bucket S3 setup. + - `s3m`: enable multi-instance S3 setup. - `ldap`: set up an LDAP server. - `office`: set up a Nextcloud Office server. - `onlyoffice` setup an onlyoffice document server. @@ -100,6 +100,9 @@ Additionally, you can use the following options when starting an instance: configure it the mail server. - `redis`: start a separate container for redis. - `redis-tls`: connect to redis over TLS. +- ``: by specifying the path to an app package this package + will be extracted into the apps. directory of the new instance (overwriting + any existing app code). This can be used to quickly test a packaged app. - The name of any configured preset. #### Run tests in a new instance diff --git a/certificates/s3/private.key b/certificates/s3/private.key new file mode 100644 index 0000000..939d93e --- /dev/null +++ b/certificates/s3/private.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCQuTk+irdXRGbY +JUu+AFmMM4/CCtBIBBgGIG18tesgQUeHEPdRHbBypvFKhpXvlKQbVbiaZxFjtlvn +L2ReN7gYwjiTjLWuaDgOzGQyObwSJpedlcd5Q957WNlc5OjpoK8zZq9EGnmvjIqe +5VVBne85RZVw6+i4ljEiWoXCiy0iOPIL4jKO8kfO8EmTTh6ge2sLCT5jFT/V63/B +hjEobA7+vPbAmEo+Qs4adnBSrlX9nLtL1j4gqawAQMmGl/Ti75T0uQvNxdq2gddc +n0wyWhyERnZNeMv5sfkwCOTuetNNtGLf/lTkXNdaOCRvW5jJ0EjYYdpQG5R0do1U +XFLH1cZZAgMBAAECggEAR0xpTk2Ku5yASlY9dXK4qyCv3znymLgjmckaB4mcN7zR +X1JVdYn55tImJ8AcV/bTzn+xvaevYn9x0XiAqwYqVVBCDTcSPsUrcObzKedVp1+J +7GHg7vYnwn7oPyKrOIYoKluZVyTv9DN6C4QSN4x2UbHdSM+ATIf51uHf6hMk/ilv +4uw3csxSCpLOqqsYCQarES7SypcETjFpNnIfTPt7q2Y2DbIDttTTjzrm0/GBP0WE +OYNvzZZPZRPJfy3et3r9vJbqWzGHvOttzQ+EFHjwPTMfW4tsHcCsEKSGWwLpG4bN +FPNx0+QCqDiChesdiCHFNSk+u7pRZrHdjuDJEuKSzQKBgQDMDgdCGBQfgaNnkRz/ +aiv3V200/vXegnc0Jz49Dye5AxEVu0X1m2xZpJv+qEwbOx5B+1PV3gfVP/iRf+FK +MAwFbmb7hGcDE8AGNsSpQydjwzKoi/M67YXv7T8dgWKnwz0eyU4K2IOGInGxuFty +Ik7+DTqz+Ikh1RiAoGbKfw9yhwKBgQC1kKhjxB7r/uSLcfOSg1mLcR5lTrNDQAPQ +GnsIje0nD5Tv52/k6U4tk15vjL4t6KZUFo9SJ4O1kM1veyuOJuol2AfPXS+H/Izo +5BjmoZ0jOONOZZiRIB1moQSy1qhTAeZB9S1ORxQ3dIBPqm+oyADPTTsNV67Cwnt4 +woeZRUPYHwKBgQDE0AcKJcVK+jQMUXfBlrsfTvDjO8MTwYyN/gfWxsZOeXnCFyYM +FcO01sMrJVJ6tVOi2nFrB0NQ2Om8FLbMYnlFx82GbJca7bK5i5u1kjLs0zoKPSn1 +vWEBIDhPEhuAqhxKlGk0ps580r4MZz+0XwkHmuTy7xX9TtbaQVvDljflAwKBgDKy +3hJdpTTIzBCUFSuIOezR/WbUfwH8UhQ+ELTmzJ3nn/MNcRU+gHIBgJEtf71aBXfd +hM+v8Ps2H+dNQXBENYWzuRqSLr+OKdquNrXP0w0OyYoOnHeJvCv4MlOt1Pq4wQ8R +40DEYETL5zhXoy5CCtfX/PFQ1p/Tpp6l0y9dRACJAoGALwUbyyDy85b2xRQB6RtU +I+5Vz5cd/1eQdCkoU9mX4qWA/hWpgc7Z2Jd67LW/WWtVjlF9hva/WNDSfGsXo2ew +C8OofvlfIuFDOCXrodYdHE1Q4g5TZdESr0XAqopq+QzBs89qbIy05kM9iuE4yFUo +xeimCY9oDWTeGw/XrLdHZF4= +-----END PRIVATE KEY----- diff --git a/certificates/s3/public.crt b/certificates/s3/public.crt new file mode 100644 index 0000000..f31f379 --- /dev/null +++ b/certificates/s3/public.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIULiChaTwmVx6nRTHohmPuf55/4jUwDQYJKoZIhvcNAQEL +BQAwPjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxDTALBgNVBAoM +BEhhemUxCzAJBgNVBAMMAnMzMB4XDTI2MDMwOTE3NTgwOVoXDTI3MDMwOTE3NTgw +OVowPjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxDTALBgNVBAoM +BEhhemUxCzAJBgNVBAMMAnMzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAkLk5Poq3V0Rm2CVLvgBZjDOPwgrQSAQYBiBtfLXrIEFHhxD3UR2wcqbxSoaV +75SkG1W4mmcRY7Zb5y9kXje4GMI4k4y1rmg4DsxkMjm8EiaXnZXHeUPee1jZXOTo +6aCvM2avRBp5r4yKnuVVQZ3vOUWVcOvouJYxIlqFwostIjjyC+IyjvJHzvBJk04e +oHtrCwk+YxU/1et/wYYxKGwO/rz2wJhKPkLOGnZwUq5V/Zy7S9Y+IKmsAEDJhpf0 +4u+U9LkLzcXatoHXXJ9MMlochEZ2TXjL+bH5MAjk7nrTTbRi3/5U5FzXWjgkb1uY +ydBI2GHaUBuUdHaNVFxSx9XGWQIDAQABo1MwUTAdBgNVHQ4EFgQUJJ8HiT2zmuF5 +6WHHFsTHgkrayxYwHwYDVR0jBBgwFoAUJJ8HiT2zmuF56WHHFsTHgkrayxYwDwYD +VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAUF0lB/qIrxkgZ4sqNrw4 +CInHCK29XVaMoqk1QZyS/KhWDM+zgbA92OxxuhCKw4iJEajZvgg0S9RtGkBNmquU +l0rf0JdALd0jPkWr7+3OeqlcgOs2EH7PTqrrbXTGsR12D+Ot+OerQeWXmO28Zrl8 +4O67TwQtslXwZzeCrtiwAA2DrIYpSLzh+qDtwbY5hMG5zmqqjBM20Ysgxszh4rhl +KR6skXwZwkVVhKpK76qwnU02PIMr8auL1csx8/uBTd/UzX2veqlkOP5V/Gg6eEbI +4fTOzq7k+FyuzSkrEX4Vc9GbWcRvoVZh+qAKUKstqlE2iCrqmZ+Wal6GA8JA5SZ+ +bQ== +-----END CERTIFICATE----- diff --git a/nix/image/configs/nc/s3s.php b/nix/image/configs/nc/s3s.php new file mode 100644 index 0000000..cfbf186 --- /dev/null +++ b/nix/image/configs/nc/s3s.php @@ -0,0 +1,15 @@ + 'objectstore' => [ + 'class' => 'OC\Files\ObjectStore\S3', + 'arguments' => [ + 'bucket' => 'nextcloud', + 'autocreate' => true, + 'key' => 'minio', + 'secret' => 'minio123', + 'hostname' => 's3', + 'port' => 9000, + 'use_ssl' => true, + 'use_path_style' => true, + 'uploadPartSize' => 52428800, + 'use_nextcloud_bundle' => true, + ], + ], diff --git a/nix/image/scripts/nc-auto-config b/nix/image/scripts/nc-auto-config index 6a803d9..fc8d95c 100755 --- a/nix/image/scripts/nc-auto-config +++ b/nix/image/scripts/nc-auto-config @@ -32,6 +32,11 @@ then sed -i '/\/\/PLACEHOLDER/ r /etc/nc/s3.php' /var/www/html/config/config.php fi +if [ -n "${S3S:-}" ] +then + sed -i '/\/\/PLACEHOLDER/ r /etc/nc/s3s.php' /var/www/html/config/config.php +fi + if [ -n "${S3MB:-}" ] then sed -i '/\/\/PLACEHOLDER/ r /etc/nc/s3mb.php' /var/www/html/config/config.php diff --git a/src/cloud.rs b/src/cloud.rs index 0c52e13..cb5e2f7 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -498,6 +498,22 @@ impl Cloud { } }; + for pre_setup in options + .services + .iter() + .flat_map(|service| service.pre_setup(docker, &id, config).into_iter().flatten()) + { + exec( + docker, + &container, + &uid.to_string(), + pre_setup, + vec!["NC_IS_CONFIG_READ_ONLY=1"], + Some(stdout()), + ) + .await?; + } + containers.push(container); let options_clone = options.clone(); diff --git a/src/service.rs b/src/service.rs index 344ec49..67325c3 100644 --- a/src/service.rs +++ b/src/service.rs @@ -100,6 +100,15 @@ pub trait ServiceTrait { Ok(HashMap::default()) } + fn pre_setup( + &self, + _docker: &Docker, + _cloud_id: &str, + _config: &HazeConfig, + ) -> Result>> { + Ok(Vec::new()) + } + async fn post_setup( &self, _docker: &Docker, @@ -205,6 +214,8 @@ impl ServiceTrait for RedisTls { 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 @@ -307,6 +318,7 @@ impl Service { 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)]), diff --git a/src/service/objectstore.rs b/src/service/objectstore.rs index 4f0b7d1..c0767e0 100644 --- a/src/service/objectstore.rs +++ b/src/service/objectstore.rs @@ -10,11 +10,15 @@ use bollard::models::{ use bollard::query_parameters::CreateContainerOptions; use bollard::Docker; use maplit::hashmap; -use miette::IntoDiagnostic; +use miette::{IntoDiagnostic, WrapErr}; +use serde_json::Value; +use std::collections::HashMap; +use std::fs::{create_dir_all, write}; #[derive(Debug, Clone, Eq, PartialEq)] pub enum ObjectStore { S3, + S3s, S3m, S3mb, Azure, @@ -23,7 +27,7 @@ pub enum ObjectStore { impl ObjectStore { fn image(&self) -> &str { match self { - ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb => { + ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb | ObjectStore::S3s => { "minio/minio:RELEASE.2024-07-16T23-46-41Z" } ObjectStore::Azure => "arafato/azurite:2.6.5", @@ -32,7 +36,7 @@ impl ObjectStore { fn self_env(&self) -> Vec<&str> { match self { - ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb => { + ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb | ObjectStore::S3s => { vec!["MINIO_ACCESS_KEY=minio", "MINIO_SECRET_KEY=minio123"] } ObjectStore::Azure => vec![], @@ -41,17 +45,54 @@ impl ObjectStore { fn host_name(&self) -> &str { match self { - ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb => "s3", + ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb | ObjectStore::S3s => "s3", ObjectStore::Azure => "azure", } } fn args(&self) -> &[&str] { match self { - ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb => &["server", "/data"], + ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb | ObjectStore::S3s => { + &["server", "/data"] + } _ => &[], } } + + fn volumes(&self, config: &HazeConfig) -> Option> { + match self { + ObjectStore::S3s => { + let cert_dir = config.work_dir.join("certificates/s3"); + create_dir_all(&cert_dir) + .into_diagnostic() + .wrap_err("Failed to create redis certificate directory") + .unwrap(); + let s3_cert_path = config.work_dir.join("certificates/s3/public.crt"); + let s3_key_path = config.work_dir.join("certificates/s3/private.key"); + if !s3_cert_path.exists() { + write( + &s3_cert_path, + include_bytes!("../../certificates/s3/public.crt"), + ) + .into_diagnostic() + .wrap_err("Failed to write s3 certificate") + .unwrap(); + } + if !s3_key_path.exists() { + write( + &s3_key_path, + include_bytes!("../../certificates/s3/private.key"), + ) + .into_diagnostic() + .wrap_err("Failed to write s3 key") + .unwrap(); + } + + Some(vec![format!("{cert_dir}:/root/.minio/certs:ro")]) + } + _ => None, + } + } } #[async_trait::async_trait] @@ -59,6 +100,7 @@ impl ServiceTrait for ObjectStore { fn name(&self) -> &str { match self { ObjectStore::S3 => "s3", + ObjectStore::S3s => "s3s", ObjectStore::S3m => "s3m", ObjectStore::S3mb => "s3mb", ObjectStore::Azure => "azure", @@ -68,8 +110,9 @@ impl ServiceTrait for ObjectStore { fn env(&self) -> &[&str] { match self { ObjectStore::S3 => &["S3=1"], + ObjectStore::S3s => &["S3S=1"], ObjectStore::S3m => &["S3M=1"], - ObjectStore::S3mb => &["S3MB=1"], + ObjectStore::S3mb => &["S3MB =1"], ObjectStore::Azure => &["AZURE=1"], } } @@ -79,7 +122,7 @@ impl ServiceTrait for ObjectStore { docker: &Docker, cloud_id: &str, network: &str, - _config: &HazeConfig, + config: &HazeConfig, _options: &CloudOptions, ) -> Result> { pull_image(docker, self.image()).await?; @@ -92,6 +135,7 @@ impl ServiceTrait for ObjectStore { env: Some(self.self_env().into_iter().map(String::from).collect()), host_config: Some(HostConfig { network_mode: Some(network.to_string()), + binds: self.volumes(config), ..Default::default() }), labels: Some(hashmap! { @@ -165,14 +209,47 @@ impl ServiceTrait for ObjectStore { &["files_external"] } + fn config( + &self, + _docker: &Docker, + _cloud_id: &str, + _config: &HazeConfig, + ) -> Result> { + match self { + ObjectStore::S3s => Ok(hashmap![ + "default_certificates_bundle_path".into() => Value::String("/var/www/html/data/ca-bundle.crt".into()), + ]), + _ => Ok(HashMap::default()), + } + } + + fn pre_setup( + &self, + _docker: &Docker, + _cloud_id: &str, + _config: &HazeConfig, + ) -> Result>> { + match self { + ObjectStore::S3s => Ok(vec![ + vec!["mkdir".into(), "-p".into(), "/var/www/html/data".into()], + vec![ + "sh".into(), + "-c".into(), + "cat /var/www/html/resources/config/ca-bundle.crt /certificates/s3/public.crt > /var/www/html/data/ca-bundle.crt".into(), + ], + ]), + _ => Ok(Vec::new()), + } + } + async fn post_setup( &self, _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, ) -> Result> { - if *self == ObjectStore::S3 { - Ok(vec![ + match self { + ObjectStore::S3 => Ok(vec![ "occ files_external:create s3 amazons3 amazons3::accesskey".into(), "occ files_external:config 1 bucket ext".into(), "occ files_external:config 1 hostname s3".into(), @@ -182,15 +259,25 @@ impl ServiceTrait for ObjectStore { "occ files_external:config 1 key minio".into(), "occ files_external:config 1 secret minio123".into(), "mc alias set s3 http://s3:9000 minio minio123".into(), - ]) - } else { - Ok(Vec::new()) + ]), + // ObjectStore::S3s => Ok(vec![ + // "occ files_external:create s3 amazons3 amazons3::accesskey".into(), + // "occ files_external:config 1 bucket ext".into(), + // "occ files_external:config 1 hostname s3".into(), + // "occ files_external:config 1 port 9000".into(), + // "occ files_external:config 1 use_ssl true".into(), + // "occ files_external:config 1 use_path_style true".into(), + // "occ files_external:config 1 key minio".into(), + // "occ files_external:config 1 secret minio123".into(), + // "mc alias set s3 https://s3:9000 minio minio123".into(), + // ]), + _ => Ok(Vec::new()), } } fn proxy_port(&self) -> u16 { match self { - ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb => 9000, + ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb | ObjectStore::S3s => 9000, ObjectStore::Azure => 10000, } } From 8780fe0754556c9db6a562f691b726a05868563b Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 9 Mar 2026 22:48:27 +0100 Subject: [PATCH 18/62] build new 8.0 and 8.1 images --- .forgejo/workflows/docker.yaml | 2 +- README.md | 6 +-- flake.lock | 88 +++++++++++++++++++++++++++++++++- flake.nix | 14 +++++- nix/image/php.nix | 4 ++ nix/overlay.nix | 1 + 6 files changed, 108 insertions(+), 7 deletions(-) diff --git a/.forgejo/workflows/docker.yaml b/.forgejo/workflows/docker.yaml index dfbdb95..7944c61 100644 --- a/.forgejo/workflows/docker.yaml +++ b/.forgejo/workflows/docker.yaml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - php-version: ["8.2", "8.3", "8.4"] + php-version: ["8.0", "8,1", "8.2", "8.3", "8.4"] variant: [""] name: haze-${{ matrix.php-version }}${{ matrix.variant }} diff --git a/README.md b/README.md index 7c5e334..6407db8 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,8 @@ haze start [database] [php-version] [services] [vX.Y.Z] Where `database` is one of `sqlite`, `mysql`, `mariadb`, `pgsql` or `oracle` with an optional version (e.g. `pgsql:12`), defaults to `sqlite`. And -`php-version` is one of `8.2`, `8.3` or `8.4`, defaults to the maximum version -support by the current Nextcloud version. `7.3` till `8.1` are still supported -but the docker images for those versions aren't being updated anymore so they -might be missing some newer features. +`php-version` is one of `8.0`, `8.1`, `8.2`, `8.3` or `8.4`, defaults to the +maximum version support by the current Nextcloud version. Each php version also comes with a `-dbg` variant that has php compiled in debug mode and can be used for debugging php itself with gdb. diff --git a/flake.lock b/flake.lock index 81e86a7..4ffb480 100644 --- a/flake.lock +++ b/flake.lock @@ -15,6 +15,22 @@ "type": "github" } }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, "flakelight": { "inputs": { "nixpkgs": [ @@ -72,11 +88,48 @@ "type": "indirect" } }, + "nixpkgs_2": { + "locked": { + "lastModified": 1772956932, + "narHash": "sha256-M0yS4AafhKxPPmOHGqIV0iKxgNO8bHDWdl1kOwGBwRY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "608d0cadfed240589a7eea422407a547ad626a14", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "phps": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": "nixpkgs_2", + "utils": "utils" + }, + "locked": { + "lastModified": 1772365008, + "narHash": "sha256-/ynkWKeZ1dyRIUkQas0AB35semWAwCbTKXu+/q+8MGg=", + "owner": "fossar", + "repo": "nix-phps", + "rev": "f47eb877bf1c219809e4357eec2fdab8e3263b7b", + "type": "github" + }, + "original": { + "owner": "fossar", + "repo": "nix-phps", + "type": "github" + } + }, "root": { "inputs": { "flakelight": "flakelight", "mill-scale": "mill-scale", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "phps": "phps" } }, "rust-overlay": { @@ -100,6 +153,39 @@ "repo": "rust-overlay", "type": "github" } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 4d2f05b..991f5e8 100644 --- a/flake.nix +++ b/flake.nix @@ -9,8 +9,15 @@ url = "git+https://codeberg.org/icewind/mill-scale.git"; inputs.flakelight.follows = "flakelight"; }; + phps = { + url = "github:fossar/nix-phps"; + }; }; - outputs = {mill-scale, ...}: + outputs = { + mill-scale, + phps, + ... + }: mill-scale ./. { crossTargets = [ "x86_64-unknown-linux-gnu" @@ -29,12 +36,17 @@ withOverlays = [ (import ./nix/overlay.nix) + (prev: final: { + inherit (phps.packages.${prev.system}) php81 php80; + }) ]; packages = { "haze-image-php-8.4" = pkgs: pkgs.haze-image-php-84; "haze-image-php-8.3" = pkgs: pkgs.haze-image-php-83; "haze-image-php-8.2" = pkgs: pkgs.haze-image-php-82; + "haze-image-php-8.1" = pkgs: pkgs.haze-image-php-81; + "haze-image-php-8.0" = pkgs: pkgs.haze-image-php-80; }; tools = pkgs: with pkgs; [cargo-edit bacon skopeo]; diff --git a/nix/image/php.nix b/nix/image/php.nix index 852b478..4fa7ff6 100644 --- a/nix/image/php.nix +++ b/nix/image/php.nix @@ -3,7 +3,9 @@ php, debug ? false, }: let + inherit (builtins) compareVersions; inherit (lib) optionals; + withBlackfire = !debug && ((compareVersions php.version "8.1.0") == 1); in php.buildEnv { extensions = { @@ -36,6 +38,8 @@ in ] ++ optionals (!debug) [ smbclient # this breaks the build for no apparent reason + ] + ++ optionals withBlackfire [ blackfire ]); extraConfig = '' diff --git a/nix/overlay.nix b/nix/overlay.nix index b08daca..1f927f8 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -4,4 +4,5 @@ final: prev: { haze-image-php-83 = final.callPackage ./image/haze.nix {php = final.php83;}; haze-image-php-82 = final.callPackage ./image/haze.nix {php = final.php82;}; haze-image-php-81 = final.callPackage ./image/haze.nix {php = final.php81;}; + haze-image-php-80 = final.callPackage ./image/haze.nix {php = final.php80;}; } From da6c6d754bde428e088d06b8a3b3fa92ccf52b9a Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 9 Mar 2026 23:00:21 +0100 Subject: [PATCH 19/62] add 8.5 image --- .forgejo/workflows/docker.yaml | 2 +- README.md | 4 ++-- flake.lock | 38 +++++++++++++++++----------------- flake.nix | 1 + nix/image/bootstrap.sh | 2 +- nix/overlay.nix | 1 + src/php.rs | 38 +++++++++++----------------------- 7 files changed, 37 insertions(+), 49 deletions(-) diff --git a/.forgejo/workflows/docker.yaml b/.forgejo/workflows/docker.yaml index 7944c61..4c8b3b2 100644 --- a/.forgejo/workflows/docker.yaml +++ b/.forgejo/workflows/docker.yaml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - php-version: ["8.0", "8,1", "8.2", "8.3", "8.4"] + php-version: ["8.0", "8,1", "8.2", "8.3", "8.4", "8.5"] variant: [""] name: haze-${{ matrix.php-version }}${{ matrix.variant }} diff --git a/README.md b/README.md index 6407db8..87debd4 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,8 @@ haze start [database] [php-version] [services] [vX.Y.Z] Where `database` is one of `sqlite`, `mysql`, `mariadb`, `pgsql` or `oracle` with an optional version (e.g. `pgsql:12`), defaults to `sqlite`. And -`php-version` is one of `8.0`, `8.1`, `8.2`, `8.3` or `8.4`, defaults to the -maximum version support by the current Nextcloud version. +`php-version` is one of `8.0`, `8.1`, `8.2`, `8.3`, `8.4` or `8.5`, defaults to +the maximum version support by the current Nextcloud version. Each php version also comes with a `-dbg` variant that has php compiled in debug mode and can be used for debugging php itself with gdb. diff --git a/flake.lock b/flake.lock index 4ffb480..a7766b8 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1760924934, - "narHash": "sha256-tuuqY5aU7cUkR71sO2TraVKK2boYrdW3gCSXUkF4i44=", + "lastModified": 1763938834, + "narHash": "sha256-j8iB0Yr4zAvQLueCZ5abxfk6fnG/SJ5JnGUziETjwfg=", "owner": "ipetkov", "repo": "crane", - "rev": "c6b4d5308293d0d04fcfeee92705017537cad02f", + "rev": "d9e753122e51cee64eb8d2dddfe11148f339f5a2", "type": "github" }, "original": { @@ -38,11 +38,11 @@ ] }, "locked": { - "lastModified": 1764593611, - "narHash": "sha256-6SdexcO69Dlu14YN2xuB1A6JHWSrcqMj7Na9oK7IT2M=", + "lastModified": 1773062095, + "narHash": "sha256-u+cK9IoJokO4YzQwMc2s8Vti0RL/LVSrROOEn2opc5U=", "owner": "nix-community", "repo": "flakelight", - "rev": "0d63256401341f528dd628f1a8e96d3afecade7a", + "rev": "c99e4d5f40e578cb2d8f460ea2bbd5dc26316d24", "type": "github" }, "original": { @@ -60,11 +60,11 @@ "rust-overlay": "rust-overlay" }, "locked": { - "lastModified": 1763591898, - "narHash": "sha256-aHSMj7CIa9EJYxdf05wOWRGp0KRsT/TAox7uwVSdDb8=", + "lastModified": 1772297202, + "narHash": "sha256-UEzHO/tCmhPhr8RpWtbm1MTa7ABobwt3nCjrcuDAPm0=", "ref": "refs/heads/main", - "rev": "2d9b2da2c9f384f93ef977c48f8ee35ce586529b", - "revCount": 66, + "rev": "8690e1514863b934de12f2a503c9431d186ce30b", + "revCount": 68, "type": "git", "url": "https://codeberg.org/icewind/mill-scale.git" }, @@ -75,11 +75,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1764522689, - "narHash": "sha256-SqUuBFjhl/kpDiVaKLQBoD8TLD+/cTUzzgVFoaHrkqY=", + "lastModified": 1772822230, + "narHash": "sha256-yf3iYLGbGVlIthlQIk5/4/EQDZNNEmuqKZkQssMljuw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8bb5646e0bed5dbd3ab08c7a7cc15b75ab4e1d0f", + "rev": "71caefce12ba78d84fe618cf61644dce01cf3a96", "type": "github" }, "original": { @@ -90,11 +90,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1772956932, - "narHash": "sha256-M0yS4AafhKxPPmOHGqIV0iKxgNO8bHDWdl1kOwGBwRY=", + "lastModified": 1772173633, + "narHash": "sha256-MOH58F4AIbCkh6qlQcwMycyk5SWvsqnS/TCfnqDlpj4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "608d0cadfed240589a7eea422407a547ad626a14", + "rev": "c0f3d81a7ddbc2b1332be0d8481a672b4f6004d6", "type": "github" }, "original": { @@ -141,11 +141,11 @@ ] }, "locked": { - "lastModified": 1761964689, - "narHash": "sha256-Zo3LQQDz+64EQ9zor/WmeNTFLoZkjmhp0UY3G0D3seE=", + "lastModified": 1764557621, + "narHash": "sha256-kX5PoY8hQZ80+amMQgOO9t8Tc1JZ70gYRnzaVD4AA+o=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "63d22578600f70d293aede6bc737efef60ebd97f", + "rev": "93316876c2229460a5d6f5f052766cc4cef538ce", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 991f5e8..9934eb7 100644 --- a/flake.nix +++ b/flake.nix @@ -42,6 +42,7 @@ ]; packages = { + "haze-image-php-8.5" = pkgs: pkgs.haze-image-php-85; "haze-image-php-8.4" = pkgs: pkgs.haze-image-php-84; "haze-image-php-8.3" = pkgs: pkgs.haze-image-php-83; "haze-image-php-8.2" = pkgs: pkgs.haze-image-php-82; diff --git a/nix/image/bootstrap.sh b/nix/image/bootstrap.sh index 50c14c2..a00a950 100755 --- a/nix/image/bootstrap.sh +++ b/nix/image/bootstrap.sh @@ -9,7 +9,7 @@ echo "# Options in here overwrite the builtin php.ini" > /config/php.ini echo "# xdebug.mode = debug" >> /config/php.ini echo "# xdebug.start_with_request = yes" >> /config/php.ini chmod 0777 /config/php.ini -PHP_INI_DIR="$(php --ini | grep 'Scan' | cut -d ' ' -f7)" +PHP_INI_DIR="$(php --ini | grep 'Scan' | cut -d ' ' -f7 | tr -d '"')" ln -s /config/php.ini "$PHP_INI_DIR/zz_extra.ini" HAZE_UID=${HAZE_UID:-www-data} diff --git a/nix/overlay.nix b/nix/overlay.nix index 1f927f8..276b70e 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -1,5 +1,6 @@ final: prev: { haze = final.callPackage ./package.nix {}; + haze-image-php-85 = final.callPackage ./image/haze.nix {php = final.php85;}; haze-image-php-84 = final.callPackage ./image/haze.nix {php = final.php84;}; haze-image-php-83 = final.callPackage ./image/haze.nix {php = final.php83;}; haze-image-php-82 = final.callPackage ./image/haze.nix {php = final.php82;}; diff --git a/src/php.rs b/src/php.rs index 0842ec2..2d037bc 100644 --- a/src/php.rs +++ b/src/php.rs @@ -21,20 +21,18 @@ use tokio::time::{sleep, timeout}; #[allow(dead_code)] pub enum PhpVersion { #[default] + Php85, Php84, Php83, Php82, Php81, Php80, - Php74, - Php73, + Php85Dbg, Php84Dbg, Php83Dbg, Php82Dbg, Php81Dbg, Php80Dbg, - Php74Dbg, - Php73Dbg, } pub const PHP_MEMORY_LIMIT: i64 = 2 * 1024 * 1024 * 1024; @@ -44,24 +42,20 @@ impl FromStr for PhpVersion { fn from_str(s: &str) -> Result { match s { - "7" => Ok(PhpVersion::Php74), - "7.3" => Ok(PhpVersion::Php73), - "7.4" => Ok(PhpVersion::Php74), "8" => Ok(PhpVersion::Php81), "8.0" => Ok(PhpVersion::Php80), "8.1" => Ok(PhpVersion::Php81), "8.2" => Ok(PhpVersion::Php82), "8.3" => Ok(PhpVersion::Php83), "8.4" => Ok(PhpVersion::Php84), - "7-dbg" => Ok(PhpVersion::Php74Dbg), - "7.3-dbg" => Ok(PhpVersion::Php73Dbg), - "7.4-dbg" => Ok(PhpVersion::Php74Dbg), + "8.5" => Ok(PhpVersion::Php84), "8-dbg" => Ok(PhpVersion::Php80Dbg), "8.0-dbg" => Ok(PhpVersion::Php80Dbg), "8.1-dbg" => Ok(PhpVersion::Php81Dbg), "8.2-dbg" => Ok(PhpVersion::Php82Dbg), "8.3-dbg" => Ok(PhpVersion::Php83Dbg), "8.4-dbg" => Ok(PhpVersion::Php84Dbg), + "8.5-dbg" => Ok(PhpVersion::Php85Dbg), _ => Err(()), } } @@ -73,53 +67,47 @@ impl PhpVersion { } pub fn image(&self) -> &'static str { - // for now only 7.4 match self { - PhpVersion::Php73 => "icewind1991/haze:7.3", - PhpVersion::Php74 => "icewind1991/haze:7.4", PhpVersion::Php80 => "icewind1991/haze:8.0", PhpVersion::Php81 => "icewind1991/haze:8.1", PhpVersion::Php82 => "icewind1991/haze:8.2", PhpVersion::Php83 => "icewind1991/haze:8.3", PhpVersion::Php84 => "icewind1991/haze:8.4", - PhpVersion::Php73Dbg => "icewind1991/haze:7.3-dbg", - PhpVersion::Php74Dbg => "icewind1991/haze:7.4-dbg", + PhpVersion::Php85 => "icewind1991/haze:8.5", PhpVersion::Php80Dbg => "icewind1991/haze:8.0-dbg", PhpVersion::Php81Dbg => "icewind1991/haze:8.1-dbg", PhpVersion::Php82Dbg => "icewind1991/haze:8.2-dbg", PhpVersion::Php83Dbg => "icewind1991/haze:8.3-dbg", PhpVersion::Php84Dbg => "icewind1991/haze:8.4-dbg", + PhpVersion::Php85Dbg => "icewind1991/haze:8.5-dbg", } } pub fn name(&self) -> &'static str { match self { - PhpVersion::Php73 => "7.3", - PhpVersion::Php74 => "7.4", PhpVersion::Php80 => "8.0", PhpVersion::Php81 => "8.1", PhpVersion::Php82 => "8.2", PhpVersion::Php83 => "8.3", PhpVersion::Php84 => "8.4", - PhpVersion::Php73Dbg => "7.3-dbg", - PhpVersion::Php74Dbg => "7.4-dbg", + PhpVersion::Php85 => "8.4", PhpVersion::Php80Dbg => "8.0-dbg", PhpVersion::Php81Dbg => "8.1-dbg", PhpVersion::Php82Dbg => "8.2-dbg", PhpVersion::Php83Dbg => "8.3-dbg", PhpVersion::Php84Dbg => "8.4-dbg", + PhpVersion::Php85Dbg => "8.4-dbg", } } pub fn from_number(major: u8, minor: u8) -> Option { match (major, minor) { - (7, 3) => Some(PhpVersion::Php73), - (7, 4) => Some(PhpVersion::Php74), (8, 0) => Some(PhpVersion::Php80), (8, 1) => Some(PhpVersion::Php81), (8, 2) => Some(PhpVersion::Php82), (8, 3) => Some(PhpVersion::Php83), (8, 4) => Some(PhpVersion::Php84), + (8, 5) => Some(PhpVersion::Php85), _ => None, } } @@ -127,27 +115,25 @@ impl PhpVersion { pub fn max_minor(major: u8) -> u8 { match major { 7 => 4, - 8 => 4, + 8 => 5, _ => 0, } } pub fn all() -> impl Iterator { [ - PhpVersion::Php73, - PhpVersion::Php74, PhpVersion::Php80, PhpVersion::Php81, PhpVersion::Php82, PhpVersion::Php83, PhpVersion::Php84, - PhpVersion::Php73Dbg, - PhpVersion::Php74Dbg, + PhpVersion::Php85, PhpVersion::Php80Dbg, PhpVersion::Php81Dbg, PhpVersion::Php82Dbg, PhpVersion::Php83Dbg, PhpVersion::Php84Dbg, + PhpVersion::Php85Dbg, ] .into_iter() } From ef86840e77d9c0b029ba5587d94dab57b06675e0 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 9 Mar 2026 23:05:38 +0100 Subject: [PATCH 20/62] dont fail cleaning stopped containers --- src/cloud.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/cloud.rs b/src/cloud.rs index cb5e2f7..ada7520 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -563,11 +563,9 @@ impl Cloud { pub async fn destroy(self, docker: &Docker) -> Result<()> { for container in self.containers { - docker + let _ = docker .kill_container(container.trim_start_matches('/'), None) - .await - .into_diagnostic() - .wrap_err("Failed to kill container")?; + .await; docker .remove_container( container.trim_start_matches('/'), From 00964146140390a46fa84b97ac620abe100b7424 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 9 Mar 2026 23:17:32 +0100 Subject: [PATCH 21/62] -dbg images are no longer a thing --- README.md | 3 --- src/php.rs | 33 +-------------------------------- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/README.md b/README.md index 87debd4..839e91c 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,6 @@ with an optional version (e.g. `pgsql:12`), defaults to `sqlite`. And `php-version` is one of `8.0`, `8.1`, `8.2`, `8.3`, `8.4` or `8.5`, defaults to the maximum version support by the current Nextcloud version. -Each php version also comes with a `-dbg` variant that has php compiled in debug -mode and can be used for debugging php itself with gdb. - You can specify a version number (e.g. `v32.0.2`) to use the sources from a release instead of using the local sources. diff --git a/src/php.rs b/src/php.rs index 2d037bc..b461c72 100644 --- a/src/php.rs +++ b/src/php.rs @@ -27,12 +27,6 @@ pub enum PhpVersion { Php82, Php81, Php80, - Php85Dbg, - Php84Dbg, - Php83Dbg, - Php82Dbg, - Php81Dbg, - Php80Dbg, } pub const PHP_MEMORY_LIMIT: i64 = 2 * 1024 * 1024 * 1024; @@ -48,14 +42,7 @@ impl FromStr for PhpVersion { "8.2" => Ok(PhpVersion::Php82), "8.3" => Ok(PhpVersion::Php83), "8.4" => Ok(PhpVersion::Php84), - "8.5" => Ok(PhpVersion::Php84), - "8-dbg" => Ok(PhpVersion::Php80Dbg), - "8.0-dbg" => Ok(PhpVersion::Php80Dbg), - "8.1-dbg" => Ok(PhpVersion::Php81Dbg), - "8.2-dbg" => Ok(PhpVersion::Php82Dbg), - "8.3-dbg" => Ok(PhpVersion::Php83Dbg), - "8.4-dbg" => Ok(PhpVersion::Php84Dbg), - "8.5-dbg" => Ok(PhpVersion::Php85Dbg), + "8.5" => Ok(PhpVersion::Php85), _ => Err(()), } } @@ -74,12 +61,6 @@ impl PhpVersion { PhpVersion::Php83 => "icewind1991/haze:8.3", PhpVersion::Php84 => "icewind1991/haze:8.4", PhpVersion::Php85 => "icewind1991/haze:8.5", - PhpVersion::Php80Dbg => "icewind1991/haze:8.0-dbg", - PhpVersion::Php81Dbg => "icewind1991/haze:8.1-dbg", - PhpVersion::Php82Dbg => "icewind1991/haze:8.2-dbg", - PhpVersion::Php83Dbg => "icewind1991/haze:8.3-dbg", - PhpVersion::Php84Dbg => "icewind1991/haze:8.4-dbg", - PhpVersion::Php85Dbg => "icewind1991/haze:8.5-dbg", } } @@ -91,12 +72,6 @@ impl PhpVersion { PhpVersion::Php83 => "8.3", PhpVersion::Php84 => "8.4", PhpVersion::Php85 => "8.4", - PhpVersion::Php80Dbg => "8.0-dbg", - PhpVersion::Php81Dbg => "8.1-dbg", - PhpVersion::Php82Dbg => "8.2-dbg", - PhpVersion::Php83Dbg => "8.3-dbg", - PhpVersion::Php84Dbg => "8.4-dbg", - PhpVersion::Php85Dbg => "8.4-dbg", } } @@ -128,12 +103,6 @@ impl PhpVersion { PhpVersion::Php83, PhpVersion::Php84, PhpVersion::Php85, - PhpVersion::Php80Dbg, - PhpVersion::Php81Dbg, - PhpVersion::Php82Dbg, - PhpVersion::Php83Dbg, - PhpVersion::Php84Dbg, - PhpVersion::Php85Dbg, ] .into_iter() } From 87de2dbb216e1c5360d8a8359e443cbf4aab02fe Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 10 Mar 2026 14:09:11 +0100 Subject: [PATCH 22/62] fix tests --- src/cloud.rs | 20 ++++++++++---------- src/php.rs | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/cloud.rs b/src/cloud.rs index ada7520..afa01cf 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -152,44 +152,44 @@ fn test_option_parse() { ..Default::default() } ); - let mut args = vec!["7"].into_iter().peekable(); + let mut args = vec!["8"].into_iter().peekable(); assert_eq!( CloudOptions::parse(&config, &mut args).unwrap(), CloudOptions { - php: PhpVersion::Php74, + php: PhpVersion::Php80, ..Default::default() } ); - let mut args = vec!["7", "pgsql", "rest"].into_iter().peekable(); + let mut args = vec!["8.1", "pgsql", "rest"].into_iter().peekable(); assert_eq!( CloudOptions::parse(&config, &mut args).unwrap(), CloudOptions { - php: PhpVersion::Php74, + php: PhpVersion::Php81, db: Database::Postgres, ..Default::default() } ); - let mut args = vec!["7", "ldap", "pgsql"].into_iter().peekable(); + let mut args = vec!["8", "ldap", "pgsql"].into_iter().peekable(); assert_eq!( CloudOptions::parse(&config, &mut args).unwrap(), CloudOptions { - php: PhpVersion::Php74, + php: PhpVersion::Php80, db: Database::Postgres, services: vec![Service::Ldap(Ldap), Service::LdapAdmin(LdapAdmin)], ..Default::default() } ); - let mut args = vec!["7", "pgsql", "ldap"].into_iter().peekable(); + let mut args = vec!["8", "pgsql", "ldap"].into_iter().peekable(); assert_eq!( CloudOptions::parse(&config, &mut args).unwrap(), CloudOptions { - php: PhpVersion::Php74, + php: PhpVersion::Php80, db: Database::Postgres, services: vec![Service::Ldap(Ldap), Service::LdapAdmin(LdapAdmin)], ..Default::default() } ); - let mut args = vec!["7", "pgsql", "ldap", "mypreset"] + let mut args = vec!["8", "pgsql", "ldap", "mypreset"] .into_iter() .peekable(); @@ -206,7 +206,7 @@ fn test_option_parse() { assert_eq!( CloudOptions::parse(&config, &mut args).unwrap(), CloudOptions { - php: PhpVersion::Php74, + php: PhpVersion::Php80, db: Database::Postgres, services: vec![ Service::Ldap(Ldap), diff --git a/src/php.rs b/src/php.rs index b461c72..1451108 100644 --- a/src/php.rs +++ b/src/php.rs @@ -36,7 +36,7 @@ impl FromStr for PhpVersion { fn from_str(s: &str) -> Result { match s { - "8" => Ok(PhpVersion::Php81), + "8" => Ok(PhpVersion::Php80), "8.0" => Ok(PhpVersion::Php80), "8.1" => Ok(PhpVersion::Php81), "8.2" => Ok(PhpVersion::Php82), From 4635ecf3fcb4aeb25a183cbb745d07a2a4a2c28b Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 10 Mar 2026 14:50:25 +0100 Subject: [PATCH 23/62] 2.2.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d73ab8d..aba8178 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -932,7 +932,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "haze" -version = "2.1.6" +version = "2.2.0" dependencies = [ "async-trait", "atty", diff --git a/Cargo.toml b/Cargo.toml index 5760bd8..73a4c8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "haze" -version = "2.1.6" +version = "2.2.0" authors = ["Robin Appelman "] edition = "2021" repository = "https://codeberg.org/icewind/haze" From 85ffdcea5a0e9da66170d2c07a56adc94bdb2576 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 10 Mar 2026 15:05:13 +0100 Subject: [PATCH 24/62] fix workflow typo --- .forgejo/workflows/docker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/docker.yaml b/.forgejo/workflows/docker.yaml index 4c8b3b2..5d88645 100644 --- a/.forgejo/workflows/docker.yaml +++ b/.forgejo/workflows/docker.yaml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - php-version: ["8.0", "8,1", "8.2", "8.3", "8.4", "8.5"] + php-version: ["8.0", "8.1", "8.2", "8.3", "8.4", "8.5"] variant: [""] name: haze-${{ matrix.php-version }}${{ matrix.variant }} From 0105c60a094915f7fc1fb617bc967c32d7557d84 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 10 Mar 2026 15:06:39 +0100 Subject: [PATCH 25/62] workflow cleanup --- .forgejo/workflows/docker.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.forgejo/workflows/docker.yaml b/.forgejo/workflows/docker.yaml index 5d88645..c2dbdbd 100644 --- a/.forgejo/workflows/docker.yaml +++ b/.forgejo/workflows/docker.yaml @@ -17,9 +17,8 @@ jobs: strategy: matrix: php-version: ["8.0", "8.1", "8.2", "8.3", "8.4", "8.5"] - variant: [""] - name: haze-${{ matrix.php-version }}${{ matrix.variant }} + name: haze-${{ matrix.php-version }} steps: - name: Checkout repository From 6fdadd9bad9cc4a0872573d2cae9132101608588 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 12 Mar 2026 00:21:54 +0100 Subject: [PATCH 26/62] use supervisord in image --- nix/image/bootstrap.sh | 20 ++++----------- nix/image/configs.nix | 7 ++++-- nix/image/{ => configs}/nginx.conf | 0 nix/image/{ => configs}/php-fpm.conf | 0 nix/image/configs/supervisor/blackfire.conf | 2 ++ nix/image/configs/supervisor/redis-plain.conf | 2 ++ nix/image/configs/supervisor/redis-tls.conf | 6 +++++ nix/image/configs/supervisor/supervisord.conf | 25 +++++++++++++++++++ nix/image/haze.nix | 2 ++ 9 files changed, 47 insertions(+), 17 deletions(-) rename nix/image/{ => configs}/nginx.conf (100%) rename nix/image/{ => configs}/php-fpm.conf (100%) create mode 100644 nix/image/configs/supervisor/blackfire.conf create mode 100644 nix/image/configs/supervisor/redis-plain.conf create mode 100644 nix/image/configs/supervisor/redis-tls.conf create mode 100644 nix/image/configs/supervisor/supervisord.conf diff --git a/nix/image/bootstrap.sh b/nix/image/bootstrap.sh index a00a950..c8842f9 100755 --- a/nix/image/bootstrap.sh +++ b/nix/image/bootstrap.sh @@ -28,8 +28,6 @@ echo "{}" > /var/www/html/build/integration/composer.lock echo "Starting server using $SQL database…" -# tail --follow --retry /var/log/nginx/*.log /var/log/cron/owncloud.log & - chmod +sx /sbin/sudo mkdir -p /var/log/nginx /tmp /var/run/blackfire @@ -57,23 +55,15 @@ fi if [ -n "${REDIS_TLS:-}" ] then - redis-server --protected-mode no \ - --tls-port 6379 --port 0 \ - --tls-cert-file /certificates/redis/server.crt \ - --tls-key-file /certificates/redis/server.key \ - --tls-ca-cert-file /certificates/redis/ca.crt & + cp /etc/supervisor/redis-tls.conf /etc/supervisor/enabled/ else - redis-server --protected-mode no & + cp /etc/supervisor/redis-plain.conf /etc/supervisor/enabled/ fi if [ -n "${BLACKFIRE_SERVER_ID:-}" ] then - sh -c ' - yes | blackfire agent:config --server-id=$BLACKFIRE_SERVER_ID --server-token=$BLACKFIRE_SERVER_TOKEN - BLACKFIRE_LOG_LEVEL=4 BLACKFIRE_LOG_FILE=/var/log/agent.log blackfire agent & - '& + blackfire agent:config --server-id="$BLACKFIRE_SERVER_ID" --server-token="$BLACKFIRE_SERVER_TOKEN" + cp /etc/supervisor/blackfire.conf /etc/supervisor/enabled/ fi -php-fpm --fpm-config /etc/php-fpm.conf& - -nginx -c /etc/nginx.conf \ No newline at end of file +exec supervisord -c /etc/supervisor/supervisord.conf \ No newline at end of file diff --git a/nix/image/configs.nix b/nix/image/configs.nix index 1b90bc8..79449b5 100644 --- a/nix/image/configs.nix +++ b/nix/image/configs.nix @@ -7,6 +7,9 @@ runCommand "configs" {} '' cp ${./configs/nginx-app.conf} $out/conf/nginx-app.conf cp ${./configs/sudoers} $out/etc/sudoers.d/haze cp -r ${./configs/nc} $out/etc/nc - cp ${./php-fpm.conf} $out/etc/php-fpm.conf - cp ${./nginx.conf} $out/etc/nginx.conf + cp ${./configs/php-fpm.conf} $out/etc/php-fpm.conf + cp ${./configs/nginx.conf} $out/etc/nginx.conf + cp -r ${./configs/supervisor} $out/etc/supervisor + chmod +w $out/etc/supervisor + mkdir $out/etc/supervisor/enabled/ '' diff --git a/nix/image/nginx.conf b/nix/image/configs/nginx.conf similarity index 100% rename from nix/image/nginx.conf rename to nix/image/configs/nginx.conf diff --git a/nix/image/php-fpm.conf b/nix/image/configs/php-fpm.conf similarity index 100% rename from nix/image/php-fpm.conf rename to nix/image/configs/php-fpm.conf diff --git a/nix/image/configs/supervisor/blackfire.conf b/nix/image/configs/supervisor/blackfire.conf new file mode 100644 index 0000000..ca6cd89 --- /dev/null +++ b/nix/image/configs/supervisor/blackfire.conf @@ -0,0 +1,2 @@ +[program:blackfire] +command = blackfire agent \ No newline at end of file diff --git a/nix/image/configs/supervisor/redis-plain.conf b/nix/image/configs/supervisor/redis-plain.conf new file mode 100644 index 0000000..e9284cc --- /dev/null +++ b/nix/image/configs/supervisor/redis-plain.conf @@ -0,0 +1,2 @@ +[program:redis] +command = /bin/redis-server --protected-mode no \ No newline at end of file diff --git a/nix/image/configs/supervisor/redis-tls.conf b/nix/image/configs/supervisor/redis-tls.conf new file mode 100644 index 0000000..28d9be1 --- /dev/null +++ b/nix/image/configs/supervisor/redis-tls.conf @@ -0,0 +1,6 @@ +[program:redis-tls] +command = /bin/redis-server --protected-mode no \ + --tls-port 6379 --port 0 \ + --tls-cert-file /certificates/redis/server.crt \ + --tls-key-file /certificates/redis/server.key \ + --tls-ca-cert-file /certificates/redis/ca.crt \ No newline at end of file diff --git a/nix/image/configs/supervisor/supervisord.conf b/nix/image/configs/supervisor/supervisord.conf new file mode 100644 index 0000000..dee3c56 --- /dev/null +++ b/nix/image/configs/supervisor/supervisord.conf @@ -0,0 +1,25 @@ +[supervisord] +logfile = /dev/stdout +logfile_maxbytes = 0 +nodaemon = true +pidfile = /var/run/supervisord.pid + +[unix_http_server] +file = /var/run/supervisor.sock +chmod = 0777 + + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl = unix:///var/run/supervisor.sock + +[program:nginx] +command = /bin/nginx -c /etc/nginx.conf + +[program:php-fpm] +command = /bin/php-fpm --fpm-config /etc/php-fpm.conf + +[include] +files = enabled/* diff --git a/nix/image/haze.nix b/nix/image/haze.nix index 926f0fa..ffdeb73 100644 --- a/nix/image/haze.nix +++ b/nix/image/haze.nix @@ -33,6 +33,7 @@ writeShellApplication, vim, helix, + python3Packages, }: let inherit (builtins) toString; inherit (lib) readFile getExe concatStringsSep splitString take; @@ -145,6 +146,7 @@ oracle-instantclient vim helix + python3Packages.supervisor ]; }; From 63e17d609fe0f4ebc03de810c0031b851147e053 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 20 Mar 2026 15:14:22 +0100 Subject: [PATCH 27/62] record haze version when building images and warn on out of date images --- nix/image/haze.nix | 11 ++++++++++- src/image.rs | 37 +++++++++++++++++++++++++++++++++++++ src/php.rs | 14 +++++++++++++- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/nix/image/haze.nix b/nix/image/haze.nix index ffdeb73..7c740af 100644 --- a/nix/image/haze.nix +++ b/nix/image/haze.nix @@ -38,6 +38,8 @@ inherit (builtins) toString; inherit (lib) readFile getExe concatStringsSep splitString take; + version = (fromTOML (readFile ../../Cargo.toml)).package.version; + phpVersion = concatStringsSep "." (take 2 (splitString "." php.version)); phpEnv = callPackage ./php.nix {inherit debug php;}; @@ -187,7 +189,14 @@ in ''; config = { Cmd = [(getExe bootstrap)]; - Env = ["EDITOR=hx" "WEBROOT=/var/www/html"]; + Env = [ + "EDITOR=hx" + "WEBROOT=/var/www/html" + "HAZE_IMAGE_VERSION=${toString version}" + ]; WorkingDir = "/var/www/html"; + Labels = { + "nl.icewind.haze.version" = toString version; + }; }; } diff --git a/src/image.rs b/src/image.rs index 94da352..e24abfd 100644 --- a/src/image.rs +++ b/src/image.rs @@ -5,11 +5,48 @@ use futures_util::StreamExt; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use miette::{IntoDiagnostic, Result, WrapErr}; use std::collections::HashMap; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] +pub struct ImageVersion { + pub major: u8, + pub minor: u8, + pub patch: u8, +} + +impl FromStr for ImageVersion { + type Err = (); + + fn from_str(s: &str) -> std::result::Result { + let mut parts = s.split('.'); + let major = parts.next().ok_or(())?.parse().map_err(|_| ())?; + let minor = parts.next().ok_or(())?.parse().map_err(|_| ())?; + let patch = parts.next().ok_or(())?.parse().map_err(|_| ())?; + Ok(ImageVersion { + major, + minor, + patch, + }) + } +} + +impl Display for ImageVersion { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} pub async fn image_exists(docker: &Docker, image: &str) -> bool { docker.inspect_image(image).await.is_ok() } +pub async fn image_version(docker: &Docker, image: &str) -> Option { + let labels = docker.inspect_image(image).await.ok()?.config?.labels?; + let label = labels.get("nl.icewind.haze.version")?; + ImageVersion::from_str(label).ok() +} + pub async fn update_image(docker: &Docker, image: &str) -> Result<()> { if image_exists(docker, image).await { force_pull_image(docker, image).await?; diff --git a/src/php.rs b/src/php.rs index 1451108..ab01f79 100644 --- a/src/php.rs +++ b/src/php.rs @@ -1,6 +1,7 @@ +use owo_colors::OwoColorize; use crate::config::ProxyConfig; use crate::database::Database; -use crate::image::pull_image; +use crate::image::{image_version, pull_image, ImageVersion}; use crate::network::ensure_network_exists; use crate::service::Service; use crate::service::ServiceTrait; @@ -123,6 +124,17 @@ impl PhpVersion { ) -> Result { ensure_network_exists(docker, "haze").await?; pull_image(docker, self.image()).await?; + + let image_version = image_version(&docker, self.image()).await; + let haze_version = ImageVersion::from_str(env!("CARGO_PKG_VERSION")); + if let (Some(image_version), Ok(haze_version)) = (image_version, haze_version) { + if image_version < haze_version { + eprintln!("{}: image version is out of date, run {} to update.", "Warning".red(), "haze update".blue()); + eprintln!(" Haze version: {}", haze_version.bright_yellow()); + eprintln!(" Image version: {}", image_version.bright_yellow()); + } + } + let options = Some(CreateContainerOptions { name: Some(id.to_string()), ..CreateContainerOptions::default() From ce34f302a158d200ba19539ffc4cd81e77471a3e Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 20 Mar 2026 16:10:42 +0100 Subject: [PATCH 28/62] clippy fixes --- src/php.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/php.rs b/src/php.rs index ab01f79..f4e0277 100644 --- a/src/php.rs +++ b/src/php.rs @@ -125,7 +125,7 @@ impl PhpVersion { ensure_network_exists(docker, "haze").await?; pull_image(docker, self.image()).await?; - let image_version = image_version(&docker, self.image()).await; + let image_version = image_version(docker, self.image()).await; let haze_version = ImageVersion::from_str(env!("CARGO_PKG_VERSION")); if let (Some(image_version), Ok(haze_version)) = (image_version, haze_version) { if image_version < haze_version { From 0d9866765012c0906ef0f8c434b905aad52d9ef7 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 20 Mar 2026 21:21:26 +0100 Subject: [PATCH 29/62] add dns for proxy inside container --- nix/image/bootstrap.sh | 18 ++++++++++++++++++ nix/image/configs.nix | 14 +++----------- nix/image/configs/dnsmasq.conf | 6 ++++++ nix/image/configs/nginx.conf | 2 +- nix/image/configs/{cron.conf => oc-cron.conf} | 0 nix/image/configs/{sudoers => sudoers.d/haze} | 0 nix/image/configs/supervisor/dnsmasq.conf | 2 ++ nix/image/haze.nix | 2 ++ src/php.rs | 5 +++++ 9 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 nix/image/configs/dnsmasq.conf rename nix/image/configs/{cron.conf => oc-cron.conf} (100%) rename nix/image/configs/{sudoers => sudoers.d/haze} (100%) create mode 100644 nix/image/configs/supervisor/dnsmasq.conf diff --git a/nix/image/bootstrap.sh b/nix/image/bootstrap.sh index c8842f9..51bfa68 100755 --- a/nix/image/bootstrap.sh +++ b/nix/image/bootstrap.sh @@ -66,4 +66,22 @@ then cp /etc/supervisor/blackfire.conf /etc/supervisor/enabled/ fi +if [ -n "${PROXY_BASE:-}" ]; then + UPSTREAM_DNS=$(cat /etc/resolv.conf | grep nameserver | cut -d' ' -f 2) + ( + RC=$(sed '/nameserver/d' /etc/resolv.conf) + echo "$RC" > /etc/resolv.conf + ) + echo 'nameserver 127.0.0.22' >> /etc/resolv.conf + + echo "s/UPSTREAM_DNS/${UPSTREAM_DNS}" + sed -i "s/UPSTREAM_DNS/${UPSTREAM_DNS}/" /etc/dnsmasq.conf + echo "s/PROXY_BASE/${PROXY_BASE}" + sed -i "s/PROXY_BASE/${PROXY_BASE}/" /etc/dnsmasq.conf + echo "s/HOST_IP/${HOST_IP}" + sed -i "s/HOST_IP/${HOST_IP}/" /etc/dnsmasq.conf + + cp /etc/supervisor/dnsmasq.conf /etc/supervisor/enabled/ +fi + exec supervisord -c /etc/supervisor/supervisord.conf \ No newline at end of file diff --git a/nix/image/configs.nix b/nix/image/configs.nix index 79449b5..afa06c7 100644 --- a/nix/image/configs.nix +++ b/nix/image/configs.nix @@ -1,15 +1,7 @@ {runCommand}: runCommand "configs" {} '' - mkdir -p $out/etc - mkdir -p $out/etc/sudoers.d - mkdir -p $out/conf - cp ${./configs/cron.conf} $out/etc/oc-cron.conf - cp ${./configs/nginx-app.conf} $out/conf/nginx-app.conf - cp ${./configs/sudoers} $out/etc/sudoers.d/haze - cp -r ${./configs/nc} $out/etc/nc - cp ${./configs/php-fpm.conf} $out/etc/php-fpm.conf - cp ${./configs/nginx.conf} $out/etc/nginx.conf - cp -r ${./configs/supervisor} $out/etc/supervisor - chmod +w $out/etc/supervisor + mkdir -p $out + cp -r ${./configs} $out/etc + chmod -R +w $out/etc mkdir $out/etc/supervisor/enabled/ '' diff --git a/nix/image/configs/dnsmasq.conf b/nix/image/configs/dnsmasq.conf new file mode 100644 index 0000000..2b1afc5 --- /dev/null +++ b/nix/image/configs/dnsmasq.conf @@ -0,0 +1,6 @@ +listen-address=127.0.0.22 +no-resolv + +address=/PROXY_BASE/HOST_IP + +server=UPSTREAM_DNS \ No newline at end of file diff --git a/nix/image/configs/nginx.conf b/nix/image/configs/nginx.conf index 6e707c2..b25ab52 100644 --- a/nix/image/configs/nginx.conf +++ b/nix/image/configs/nginx.conf @@ -79,7 +79,7 @@ http { } } - include /conf/nginx-app.conf; + include /etc/nginx-app.conf; location ~ \.php(?:$|/) { rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode\/proxy) /index.php$request_uri; diff --git a/nix/image/configs/cron.conf b/nix/image/configs/oc-cron.conf similarity index 100% rename from nix/image/configs/cron.conf rename to nix/image/configs/oc-cron.conf diff --git a/nix/image/configs/sudoers b/nix/image/configs/sudoers.d/haze similarity index 100% rename from nix/image/configs/sudoers rename to nix/image/configs/sudoers.d/haze diff --git a/nix/image/configs/supervisor/dnsmasq.conf b/nix/image/configs/supervisor/dnsmasq.conf new file mode 100644 index 0000000..d57ed09 --- /dev/null +++ b/nix/image/configs/supervisor/dnsmasq.conf @@ -0,0 +1,2 @@ +[program:dnsmasq] +command = /bin/dnsmasq --keep-in-foreground -u root \ No newline at end of file diff --git a/nix/image/haze.nix b/nix/image/haze.nix index 7c740af..8751e42 100644 --- a/nix/image/haze.nix +++ b/nix/image/haze.nix @@ -34,6 +34,7 @@ vim, helix, python3Packages, + dnsmasq, }: let inherit (builtins) toString; inherit (lib) readFile getExe concatStringsSep splitString take; @@ -149,6 +150,7 @@ vim helix python3Packages.supervisor + dnsmasq ]; }; diff --git a/src/php.rs b/src/php.rs index f4e0277..d1abaa2 100644 --- a/src/php.rs +++ b/src/php.rs @@ -160,6 +160,11 @@ impl PhpVersion { proxy_config.addr(id, IpAddr::V4(Ipv4Addr::LOCALHOST)) )); + env.push(format!("HOST_IP={host}")); + if !proxy_config.address.is_empty() { + env.push(format!("PROXY_BASE={}", proxy_config.address)); + } + let mut labels = hashmap! { "haze-type".to_string() => "cloud".to_string(), "haze-db".to_string() => db.name().to_string(), From 4ab23610a2d86c98d479d883dd9b444624185cba Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sun, 22 Mar 2026 14:10:19 +0100 Subject: [PATCH 30/62] fix redis-tls not starting --- nix/image/configs/supervisor/redis-tls.conf | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/nix/image/configs/supervisor/redis-tls.conf b/nix/image/configs/supervisor/redis-tls.conf index 28d9be1..d9f86bd 100644 --- a/nix/image/configs/supervisor/redis-tls.conf +++ b/nix/image/configs/supervisor/redis-tls.conf @@ -1,6 +1,2 @@ [program:redis-tls] -command = /bin/redis-server --protected-mode no \ - --tls-port 6379 --port 0 \ - --tls-cert-file /certificates/redis/server.crt \ - --tls-key-file /certificates/redis/server.key \ - --tls-ca-cert-file /certificates/redis/ca.crt \ No newline at end of file +command = /bin/redis-server --protected-mode no --tls-port 6379 --port 0 --tls-cert-file /certificates/redis/server.crt --tls-key-file /certificates/redis/server.key --tls-ca-cert-file /certificates/redis/ca.crt \ No newline at end of file From 3b4014b5e40af42e2b7fa2a3bdd703cfc5682dc5 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sun, 22 Mar 2026 14:10:19 +0100 Subject: [PATCH 31/62] basic frankenphp support fixes #17 --- nix/image/bootstrap.sh | 7 +++ nix/image/configs/supervisor/frankenphp.conf | 3 ++ nix/image/configs/supervisor/nginx.conf | 2 + nix/image/configs/supervisor/php-fpm.conf | 2 + nix/image/configs/supervisor/supervisord.conf | 7 --- nix/image/haze.nix | 7 +++ nix/image/php-ext.nix | 44 +++++++++++++++++++ nix/image/php.nix | 35 +-------------- src/service.rs | 17 +++++++ 9 files changed, 83 insertions(+), 41 deletions(-) create mode 100644 nix/image/configs/supervisor/frankenphp.conf create mode 100644 nix/image/configs/supervisor/nginx.conf create mode 100644 nix/image/configs/supervisor/php-fpm.conf create mode 100644 nix/image/php-ext.nix diff --git a/nix/image/bootstrap.sh b/nix/image/bootstrap.sh index 51bfa68..ce3d0e3 100755 --- a/nix/image/bootstrap.sh +++ b/nix/image/bootstrap.sh @@ -84,4 +84,11 @@ if [ -n "${PROXY_BASE:-}" ]; then cp /etc/supervisor/dnsmasq.conf /etc/supervisor/enabled/ fi +if [ -n "${FRANKENPHP:-}" ]; then + cp /etc/supervisor/frankenphp.conf /etc/supervisor/enabled/ +else + cp /etc/supervisor/php-fpm.conf /etc/supervisor/enabled/ + cp /etc/supervisor/nginx.conf /etc/supervisor/enabled/ +fi + exec supervisord -c /etc/supervisor/supervisord.conf \ No newline at end of file diff --git a/nix/image/configs/supervisor/frankenphp.conf b/nix/image/configs/supervisor/frankenphp.conf new file mode 100644 index 0000000..8c1b1e5 --- /dev/null +++ b/nix/image/configs/supervisor/frankenphp.conf @@ -0,0 +1,3 @@ +[program:frankenphp] +command = /bin/frankenphp php-server +directory = /var/www/html \ No newline at end of file diff --git a/nix/image/configs/supervisor/nginx.conf b/nix/image/configs/supervisor/nginx.conf new file mode 100644 index 0000000..957e4b3 --- /dev/null +++ b/nix/image/configs/supervisor/nginx.conf @@ -0,0 +1,2 @@ +[program:nginx] +command = /bin/nginx -c /etc/nginx.conf diff --git a/nix/image/configs/supervisor/php-fpm.conf b/nix/image/configs/supervisor/php-fpm.conf new file mode 100644 index 0000000..69418c7 --- /dev/null +++ b/nix/image/configs/supervisor/php-fpm.conf @@ -0,0 +1,2 @@ +[program:php-fpm] +command = /bin/php-fpm --fpm-config /etc/php-fpm.conf diff --git a/nix/image/configs/supervisor/supervisord.conf b/nix/image/configs/supervisor/supervisord.conf index dee3c56..2fedc90 100644 --- a/nix/image/configs/supervisor/supervisord.conf +++ b/nix/image/configs/supervisor/supervisord.conf @@ -8,18 +8,11 @@ pidfile = /var/run/supervisord.pid file = /var/run/supervisor.sock chmod = 0777 - [rpcinterface:supervisor] supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface [supervisorctl] serverurl = unix:///var/run/supervisor.sock -[program:nginx] -command = /bin/nginx -c /etc/nginx.conf - -[program:php-fpm] -command = /bin/php-fpm --fpm-config /etc/php-fpm.conf - [include] files = enabled/* diff --git a/nix/image/haze.nix b/nix/image/haze.nix index 8751e42..b78ec51 100644 --- a/nix/image/haze.nix +++ b/nix/image/haze.nix @@ -35,6 +35,7 @@ helix, python3Packages, dnsmasq, + frankenphp, }: let inherit (builtins) toString; inherit (lib) readFile getExe concatStringsSep splitString take; @@ -163,6 +164,12 @@ phpEnv phpEnv.packages.composer phpunit + (frankenphp.override { + php = php.withExtensions (import ./php-ext.nix { + inherit lib php; + enableBlackfire = false; + }); + }) ]; }; in diff --git a/nix/image/php-ext.nix b/nix/image/php-ext.nix new file mode 100644 index 0000000..2715cf2 --- /dev/null +++ b/nix/image/php-ext.nix @@ -0,0 +1,44 @@ +{ + lib, + php, + debug ? false, + enableBlackfire ? true, +}: let + inherit (builtins) compareVersions; + inherit (lib) optionals; + withBlackfire = enableBlackfire && !debug && ((compareVersions php.version "8.1.0") == 1); +in + { + enabled, + all, + }: + enabled + ++ (with all; + [ + xdebug + excimer + inotify + redis + oci8 + zip + pdo + pdo_pgsql + pdo_sqlite + pdo_mysql + pgsql + intl + curl + mbstring + pcntl + ldap + exif + gmp + apcu + ffi + ] + ++ optionals (!debug) [ + smbclient # this breaks the build for no apparent reason + ] + ++ optionals withBlackfire [ + blackfire + ]) diff --git a/nix/image/php.nix b/nix/image/php.nix index 4fa7ff6..4922655 100644 --- a/nix/image/php.nix +++ b/nix/image/php.nix @@ -8,40 +8,7 @@ withBlackfire = !debug && ((compareVersions php.version "8.1.0") == 1); in php.buildEnv { - extensions = { - enabled, - all, - }: - enabled - ++ (with all; - [ - xdebug - excimer - inotify - redis - oci8 - zip - pdo - pdo_pgsql - pdo_sqlite - pdo_mysql - pgsql - intl - curl - mbstring - pcntl - ldap - exif - gmp - apcu - ffi - ] - ++ optionals (!debug) [ - smbclient # this breaks the build for no apparent reason - ] - ++ optionals withBlackfire [ - blackfire - ]); + extensions = import ./php-ext.nix {inherit lib php debug;}; extraConfig = '' xdebug.mode=debug,trace,profile xdebug.start_with_request=trigger diff --git a/src/service.rs b/src/service.rs index 67325c3..49bb636 100644 --- a/src/service.rs +++ b/src/service.rs @@ -207,6 +207,19 @@ impl ServiceTrait for RedisTls { } } +#[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, )] @@ -281,6 +294,8 @@ pub enum ServiceType { Redis, /// External redis instance with TLS RedisTls, + /// Use FrankenPHP instead of PHP-FPM + FrankenPhp, } #[enum_dispatch] @@ -310,6 +325,7 @@ pub enum Service { Mail(Mail), Redis(Redis), RedisTls(RedisTls), + FrankenPhp(FrankenPhp), Preset(PresetService), } @@ -352,6 +368,7 @@ impl Service { 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 From a80354c922d11c6c59fe38bb64b2de9d3d3c7fa9 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 25 Mar 2026 17:02:34 +0100 Subject: [PATCH 32/62] fix office --- src/service/office.rs | 64 ++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/src/service/office.rs b/src/service/office.rs index 4cd72c9..9977f57 100644 --- a/src/service/office.rs +++ b/src/service/office.rs @@ -32,6 +32,23 @@ impl ServiceTrait for Office { config: &HazeConfig, _options: &CloudOptions, ) -> Result> { + let network_info = docker + .inspect_network(&network, None) + .await + .into_diagnostic()?; + let gateway = network_info + .ipam + .as_ref() + .ok_or_else(|| Report::msg("Network has no ip info"))? + .config + .as_deref() + .ok_or_else(|| Report::msg("Network has no ip info"))? + .first() + .ok_or_else(|| Report::msg("Network has no ip info"))? + .gateway + .as_deref() + .ok_or_else(|| Report::msg("Network has no ip info"))?; + let image = "collabora/code"; pull_image(docker, image).await?; let container_id = self.container_name(cloud_id).unwrap(); @@ -39,28 +56,31 @@ impl ServiceTrait for Office { name: Some(container_id.clone()), ..CreateContainerOptions::default() }); - let mut env = vec!["extra_params=--o:ssl.enable=false --o:ssl.termination=true"]; + let mut env = + vec![r#"extra_params=--o:ssl.enable=false --o:ssl.termination=true --o:net.frame_ancestors=*"#.to_string()]; + let proxy_base = &config.proxy.address; let clean_id = container_id.strip_prefix("haze-").unwrap_or(&container_id); - let server_name_opt = match (&config.proxy.address, config.proxy.https) { - (public, true) if !public.is_empty() => { - format!("server_name={clean_id}.{public}") - } - (public, false) if !public.is_empty() => { - format!("server_name={clean_id}.{public}") - } - _ => "".to_string(), - }; - - if !server_name_opt.is_empty() { - env.push(&server_name_opt); + if !proxy_base.is_empty() { + env.push(format!("server_name={clean_id}.{}", config.proxy.address)); } + let clean_cloud_id = cloud_id.strip_prefix("haze-").unwrap_or(&cloud_id); + let hosts = if proxy_base.is_empty() { + vec![] + } else { + vec![ + format!("{proxy_base}:{gateway}"), + format!("{clean_cloud_id}.{proxy_base}:{gateway}"), + ] + }; + let config = ContainerCreateBody { image: Some(image.into()), env: Some(env.into_iter().map(String::from).collect()), host_config: Some(HostConfig { network_mode: Some(network.to_string()), + extra_hosts: Some(hosts), ..Default::default() }), labels: Some(hashmap! { @@ -127,19 +147,13 @@ impl ServiceTrait for Office { } else { return Err(Report::msg("office not started")); }; + let public = config + .proxy + .addr_with_port(container, ip, self.proxy_port()); + Ok(vec![ - format!( - r#"occ config:app:set richdocuments wopi_url --value="http://{}:9980""#, - ip - ), - format!( - r#"occ config:app:set richdocuments public_wopi_url --value="{}""#, - config.proxy.addr_with_port(container, ip, 9980) - ), - format!( - r#"occ config:app:set richdocuments wopi_root --value="http://{}""#, - cloud_id - ), + format!(r#"occ config:app:set richdocuments public_wopi_url --value="{public}""#), + r#"occ richdocuments:setup --wopi-url "http://office:9980" --callback-url "http://cloud""#.into(), ]) } From 3abf183ae34667f4436cd3f7e8d7c21250bf2878 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 10 Apr 2026 17:37:20 +0200 Subject: [PATCH 33/62] app integration support --- nix/image/haze.nix | 4 ++-- nix/image/scripts/integration | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/nix/image/haze.nix b/nix/image/haze.nix index b78ec51..3dd6567 100644 --- a/nix/image/haze.nix +++ b/nix/image/haze.nix @@ -8,9 +8,7 @@ coreutils, getent, shadow, - buildEnv, runCommand, - cacert, callPackage, cronie, redis, @@ -36,6 +34,7 @@ python3Packages, dnsmasq, frankenphp, + nushell, }: let inherit (builtins) toString; inherit (lib) readFile getExe concatStringsSep splitString take; @@ -152,6 +151,7 @@ helix python3Packages.supervisor dnsmasq + nushell ]; }; diff --git a/nix/image/scripts/integration b/nix/image/scripts/integration index 4f73aef..8477132 100755 --- a/nix/image/scripts/integration +++ b/nix/image/scripts/integration @@ -1,4 +1,18 @@ -#!/bin/sh +#!/bin/nu -cd $WEBROOT/build/integration -./run.sh "$@" +def main [feature: path, ...rest] { + mut feature = $feature; + mut workdir = $"($env.WEBROOT)/build/integration" + if ($feature | str starts-with "apps/") { + let parts = $feature | split row '/' + occ app:enable $parts.1 + let parts = $feature | split row -n 2 '/features/' + $workdir = $parts.0 + $feature = $"features/($parts.1)" + } else if ($feature | str starts-with "build/integration/") { + $feature = $feature | str replace "build/integration/" "" + } + + cd $workdir + bash run.sh $feature ...$rest +} From 266b70339bf00e5b6b9bdb46fd87351aac6b80a4 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 10 Apr 2026 17:40:35 +0200 Subject: [PATCH 34/62] cleanup --- nix/image/haze.nix | 8 -------- 1 file changed, 8 deletions(-) diff --git a/nix/image/haze.nix b/nix/image/haze.nix index 3dd6567..18ea63e 100644 --- a/nix/image/haze.nix +++ b/nix/image/haze.nix @@ -74,14 +74,6 @@ php = phpEnv; }; - phpunitWrapped = majorVersion: - writeShellApplication { - name = "phpunit${toString majorVersion}"; - text = '' - ${phpunitUnwrapped (toString majorVersion)}/bin/phpunit "$@" - ''; - }; - phpunit = writeShellApplication { name = "phpunit"; runtimeInputs = [jq]; From 0a16737398bd810b3055eadc2276f197763c6f89 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 13 Apr 2026 19:24:21 +0200 Subject: [PATCH 35/62] migrate scripts to nushell --- nix/image/bootstrap | 87 +++++++++++++++++ nix/image/bootstrap.sh | 94 ------------------- nix/image/configs/nc/redis-default.php | 1 - nix/image/configs/nc/redis-tls.php | 1 - nix/image/configs/supervisor/supervisord.conf | 1 + nix/image/haze.nix | 8 +- nix/image/scripts/install | 38 ++++---- nix/image/scripts/nc-auto-config | 80 +++++----------- nix/image/scripts/occ | 8 +- nix/image/scripts/tests | 10 +- 10 files changed, 143 insertions(+), 185 deletions(-) create mode 100755 nix/image/bootstrap delete mode 100755 nix/image/bootstrap.sh diff --git a/nix/image/bootstrap b/nix/image/bootstrap new file mode 100755 index 0000000..d16e605 --- /dev/null +++ b/nix/image/bootstrap @@ -0,0 +1,87 @@ +#!/bin/nu + +touch /var/log/nginx/access.log +touch /var/log/nginx/error.log +touch /var/log/cron/owncloud.log + +mkdir /config +echo "# Options in here overwrite the builtin php.ini\n" | save /config/php.ini +echo "# xdebug.mode = debug\n" | save -a /config/php.ini +echo "# xdebug.start_with_request = yes\n\n" | save -a /config/php.ini +chmod 0777 /config/php.ini +let PHP_INI_DIR = php --ini | grep 'Scan' | cut -d ' ' -f7 | tr -d '"' +ln -s /config/php.ini $"($PHP_INI_DIR)/zz_extra.ini" + +let HAZE_UID = $env.HAZE_UID | default "1000" +let HAZE_GID = $env.HAZE_GID | default "1000" + +nc-auto-config +shadow-setup + +echo $"Running as ($HAZE_UID):($HAZE_GID)" + +mkdir /var/www/html/core/skeleton /var/www/html/build/integration/vendor /var/www/html/build/integration/output /var/www/html/build/integration/work /var/www/html/core/skeleton /var/www/.composer/cache /var/www/html/apps/spreed/tests/integration/vendor/composer +chown -R $"($HAZE_UID):($HAZE_GID)" /var/www/html/data /var/www/html/config +chown $"($HAZE_UID):($HAZE_GID)" /var/www/html/core/skeleton /var/www/html/build/integration/vendor /var/www/html/build/integration/composer.lock /var/www/html/build/integration/output /var/www/html/build/integration/work /var/www/html/core/skeleton /var/www/.composer/cache /var/www/html/apps/spreed/tests/integration/vendor/composer + +echo "{}\n" | save -f /var/www/html/build/integration/composer.lock + +echo $"Starting server using ($env.SQL) database…" + +chmod +sx /sbin/sudo + +mkdir /var/log/nginx /tmp /var/run/blackfire +touch /var/log/nginx/access.log +touch /var/log/nginx/error.log + +if ((getent group $HAZE_GID | length) > 0) { + groupadd haze + useradd -u $HAZE_UID -g $HAZE_GID -G haze haze +} else { + groupadd -g $HAZE_GID haze + useradd -u $HAZE_UID -g $HAZE_GID haze +} +chown -R $"haze:($HAZE_GID)" /home/haze + +if ("/var/run/docker.sock" | path exists) { + let dockerGid = stat --format "%g" /var/run/docker.sock + groupadd docker -g $dockerGid + usermod -a -G docker haze +} + +if ("REDIS_TLS" in $env) { + cp /etc/supervisor/redis-tls.conf /etc/supervisor/enabled/ +} else { + cp /etc/supervisor/redis-plain.conf /etc/supervisor/enabled/ +} + +if ("BLACKFIRE_SERVER_ID" in $env) { + blackfire agent:config --server-id $env.BLACKFIRE_SERVER_ID --server-token $env.BLACKFIRE_SERVER_TOKEN + cp /etc/supervisor/blackfire.conf /etc/supervisor/enabled/ +} + +if ("PROXY_BASE" in $env) { + let UPSTREAM_DNS = cat /etc/resolv.conf | grep nameserver | cut -d' ' -f 2 + let RC = sed '/nameserver/d' /etc/resolv.conf + echo $RC | save -f /etc/resolv.conf + + echo "\nnameserver 127.0.0.22\n" | save -a /etc/resolv.conf + + echo $"s/UPSTREAM_DNS/($UPSTREAM_DNS)" + sed -i $"s/UPSTREAM_DNS/($UPSTREAM_DNS)/" /etc/dnsmasq.conf + echo $"s/PROXY_BASE/($env.PROXY_BASE)" + sed -i $"s/PROXY_BASE/($env.PROXY_BASE)/" /etc/dnsmasq.conf + echo $"s/HOST_IP/($env.HOST_IP)" + sed -i $"s/HOST_IP/($env.HOST_IP)/" /etc/dnsmasq.conf + + cp /etc/supervisor/dnsmasq.conf /etc/supervisor/enabled/ +} + +if ("FRANKENPHP" in $env) { + cp /etc/supervisor/frankenphp.conf /etc/supervisor/enabled/ +} else { + cp /etc/supervisor/php-fpm.conf /etc/supervisor/enabled/ + cp /etc/supervisor/nginx.conf /etc/supervisor/enabled/ +} + +exec supervisord -c /etc/supervisor/supervisord.conf diff --git a/nix/image/bootstrap.sh b/nix/image/bootstrap.sh deleted file mode 100755 index ce3d0e3..0000000 --- a/nix/image/bootstrap.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash - -touch /var/log/nginx/access.log -touch /var/log/nginx/error.log -touch /var/log/cron/owncloud.log - -mkdir -p /config -echo "# Options in here overwrite the builtin php.ini" > /config/php.ini -echo "# xdebug.mode = debug" >> /config/php.ini -echo "# xdebug.start_with_request = yes" >> /config/php.ini -chmod 0777 /config/php.ini -PHP_INI_DIR="$(php --ini | grep 'Scan' | cut -d ' ' -f7 | tr -d '"')" -ln -s /config/php.ini "$PHP_INI_DIR/zz_extra.ini" - -HAZE_UID=${HAZE_UID:-www-data} -HAZE_GID=${HAZE_GID:-www-data} - -nc-auto-config -shadow-setup - -echo "Running as $HAZE_UID:$HAZE_GID" - -mkdir -p /var/www/html/core/skeleton /var/www/html/build/integration/vendor /var/www/html/build/integration/output /var/www/html/build/integration/work /var/www/html/core/skeleton /var/www/.composer/cache /var/www/html/apps/spreed/tests/integration/vendor/composer -chown -R "$HAZE_UID":"$HAZE_GID" /var/www/html/data /var/www/html/config -chown "$HAZE_UID":"$HAZE_GID" /var/www/html/core/skeleton /var/www/html/build/integration/vendor /var/www/html/build/integration/composer.lock /var/www/html/build/integration/output /var/www/html/build/integration/work /var/www/html/core/skeleton /var/www/.composer/cache /var/www/html/apps/spreed/tests/integration/vendor/composer - -echo "{}" > /var/www/html/build/integration/composer.lock - -echo "Starting server using $SQL database…" - -chmod +sx /sbin/sudo - -mkdir -p /var/log/nginx /tmp /var/run/blackfire -touch /var/log/nginx/access.log -touch /var/log/nginx/error.log - -HAZE_UID=${HAZE_UID:-1000} -HAZE_GID=${HAZE_GID:-1000} - -if [ "$(getent group "$HAZE_GID")" ]; then - groupadd haze - EXTRA_GROUP=" -G haze" -else - groupadd -g "$HAZE_GID" haze - EXTRA_GROUP="" -fi - -useradd -u "$HAZE_UID" -g "$HAZE_GID""$EXTRA_GROUP" haze -chown -R haze:"$HAZE_GID" /home/haze - -if [ -f "/var/run/docker.sock" ]; then - groupadd docker -g "$(stat --format "%g" /var/run/docker.sock)" - usermod -a -G docker haze -fi - -if [ -n "${REDIS_TLS:-}" ] -then - cp /etc/supervisor/redis-tls.conf /etc/supervisor/enabled/ -else - cp /etc/supervisor/redis-plain.conf /etc/supervisor/enabled/ -fi - -if [ -n "${BLACKFIRE_SERVER_ID:-}" ] -then - blackfire agent:config --server-id="$BLACKFIRE_SERVER_ID" --server-token="$BLACKFIRE_SERVER_TOKEN" - cp /etc/supervisor/blackfire.conf /etc/supervisor/enabled/ -fi - -if [ -n "${PROXY_BASE:-}" ]; then - UPSTREAM_DNS=$(cat /etc/resolv.conf | grep nameserver | cut -d' ' -f 2) - ( - RC=$(sed '/nameserver/d' /etc/resolv.conf) - echo "$RC" > /etc/resolv.conf - ) - echo 'nameserver 127.0.0.22' >> /etc/resolv.conf - - echo "s/UPSTREAM_DNS/${UPSTREAM_DNS}" - sed -i "s/UPSTREAM_DNS/${UPSTREAM_DNS}/" /etc/dnsmasq.conf - echo "s/PROXY_BASE/${PROXY_BASE}" - sed -i "s/PROXY_BASE/${PROXY_BASE}/" /etc/dnsmasq.conf - echo "s/HOST_IP/${HOST_IP}" - sed -i "s/HOST_IP/${HOST_IP}/" /etc/dnsmasq.conf - - cp /etc/supervisor/dnsmasq.conf /etc/supervisor/enabled/ -fi - -if [ -n "${FRANKENPHP:-}" ]; then - cp /etc/supervisor/frankenphp.conf /etc/supervisor/enabled/ -else - cp /etc/supervisor/php-fpm.conf /etc/supervisor/enabled/ - cp /etc/supervisor/nginx.conf /etc/supervisor/enabled/ -fi - -exec supervisord -c /etc/supervisor/supervisord.conf \ No newline at end of file diff --git a/nix/image/configs/nc/redis-default.php b/nix/image/configs/nc/redis-default.php index 2ba24aa..71b18a8 100644 --- a/nix/image/configs/nc/redis-default.php +++ b/nix/image/configs/nc/redis-default.php @@ -1,2 +1 @@ 'redis' => ['host' => 'localhost'], -//PLACEHOLDER diff --git a/nix/image/configs/nc/redis-tls.php b/nix/image/configs/nc/redis-tls.php index cb454ac..c3b9abe 100644 --- a/nix/image/configs/nc/redis-tls.php +++ b/nix/image/configs/nc/redis-tls.php @@ -8,4 +8,3 @@ 'verify_peer_name' => false, ], ], -//PLACEHOLDER diff --git a/nix/image/configs/supervisor/supervisord.conf b/nix/image/configs/supervisor/supervisord.conf index 2fedc90..bed7885 100644 --- a/nix/image/configs/supervisor/supervisord.conf +++ b/nix/image/configs/supervisor/supervisord.conf @@ -3,6 +3,7 @@ logfile = /dev/stdout logfile_maxbytes = 0 nodaemon = true pidfile = /var/run/supervisord.pid +user = root [unix_http_server] file = /var/run/supervisor.sock diff --git a/nix/image/haze.nix b/nix/image/haze.nix index 18ea63e..8c7551e 100644 --- a/nix/image/haze.nix +++ b/nix/image/haze.nix @@ -7,6 +7,7 @@ blackfire, coreutils, getent, + writers, shadow, runCommand, callPackage, @@ -44,11 +45,7 @@ phpVersion = concatStringsSep "." (take 2 (splitString "." php.version)); phpEnv = callPackage ./php.nix {inherit debug php;}; - bootstrap = writeShellApplication { - name = "bootstrap"; - runtimeInputs = [getent]; - text = readFile ./bootstrap.sh; - }; + bootstrap = writers.writeNuBin "bootstrap" (readFile ./bootstrap); shadowSetupScript = writeShellApplication { name = "shadow-setup"; text = dockerTools.shadowSetup; @@ -144,6 +141,7 @@ python3Packages.supervisor dnsmasq nushell + getent ]; }; diff --git a/nix/image/scripts/install b/nix/image/scripts/install index 2a86fcc..35d3f00 100755 --- a/nix/image/scripts/install +++ b/nix/image/scripts/install @@ -1,20 +1,22 @@ -#!/bin/sh +#!/bin/nu -USER=$1 -PASSWORD=$2 +def main [username: string, password: string] { + cd $env.WEBROOT; + let sql = match $env.SQL { + "oracle" => "oci" + "mariadb" => "mysql" + _ => $env.SQL + } + let dbName = match $env.SQL { + "oracle" => "xe" + _ => "haze" + } + let dbUser = match $env.SQL { + "oracle" => "system" + _ => "haze" + } + let dbPass = "haze" + let dbHost = $env.SQL -if [ -z "$USER" ] || [ -z "$PASSWORD" ]; then - echo "Usage: install \$USER \$PASSWORD" - exit; -fi - -cd $WEBROOT - -if [ "$SQL" = "oracle" ]; then - # oracle is a special snowflake - occ maintenance:install --admin-user=$USER --admin-pass=$PASSWORD --database=oci --database-name=xe --database-host=$SQL --database-user=system --database-pass=haze -elif [ "$SQL" = "mariadb" ]; then - occ maintenance:install --admin-user=$USER --admin-pass=$PASSWORD --database=mysql --database-name=haze --database-host=$SQL --database-user=haze --database-pass=haze -else - occ maintenance:install --admin-user=$USER --admin-pass=$PASSWORD --database=$SQL --database-name=haze --database-host=$SQL --database-user=haze --database-pass=haze -fi; + occ maintenance:install --admin-user $username --admin-pass $password --database $sql --database-name $dbName --database-host $dbHost --database-user $dbUser --database-pass $dbPass +} diff --git a/nix/image/scripts/nc-auto-config b/nix/image/scripts/nc-auto-config index fc8d95c..b7a2c07 100755 --- a/nix/image/scripts/nc-auto-config +++ b/nix/image/scripts/nc-auto-config @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/nu touch /var/log/nginx/access.log touch /var/log/nginx/error.log @@ -7,64 +7,30 @@ touch /var/log/cron/owncloud.log cp /etc/nc/config.php /var/www/html/config/config.php chmod 0755 /var/www/html/config/config.php -if [ "$SQL" = "mysql" ] -then - cp /etc/nc/autoconfig_mysql.php /var/www/html/config/autoconfig.php -fi +let configName = match $env.SQL { + "oracle" => "oci" + _ => $env.SQL -if [ "$SQL" = "mariadb" ] -then - cp /etc/nc/autoconfig_mariadb.php /var/www/html/config/autoconfig.php -fi +} +let configPath = $"/etc/nc/autoconfig_($configName).php" -if [ "$SQL" = "pgsql" ] -then - cp /etc/nc/autoconfig_pgsql.php /var/www/html/config/autoconfig.php -fi +if ($configPath | path exists) { + cp $configPath /var/www/html/config/autoconfig.php +} -if [ "$SQL" = "oci" ] -then - cp /etc/nc/autoconfig_oci.php /var/www/html/config/autoconfig.php -fi +def loadExtraConfig [name: string] { + sed -i $'/\/\/PLACEHOLDER/ r /etc/nc/($name).php' /var/www/html/config/config.php +} -if [ -n "${S3:-}" ] -then - sed -i '/\/\/PLACEHOLDER/ r /etc/nc/s3.php' /var/www/html/config/config.php -fi +let extraConfigs = ["S3", "S3S", "S3MB", "S3M", "SWIFT", "SWIFTV3", "AZURE"]; +$extraConfigs | each { + if ($in in $env) { + loadExtraConfig ($in | str downcase) + } +} -if [ -n "${S3S:-}" ] -then - sed -i '/\/\/PLACEHOLDER/ r /etc/nc/s3s.php' /var/www/html/config/config.php -fi - -if [ -n "${S3MB:-}" ] -then - sed -i '/\/\/PLACEHOLDER/ r /etc/nc/s3mb.php' /var/www/html/config/config.php -fi - -if [ -n "${S3M:-}" ] -then - sed -i '/\/\/PLACEHOLDER/ r /etc/nc/s3m.php' /var/www/html/config/config.php -fi - -if [ -n "${SWIFT:-}" ] -then - sed -i '/\/\/PLACEHOLDER/ r /etc/nc/swift.php' /var/www/html/config/config.php -fi - -if [ -n "${SWIFTV3:-}" ] -then - sed -i '/\/\/PLACEHOLDER/ r /etc/nc/swiftv3.php' /var/www/html/config/config.php -fi - -if [ -n "${AZURE:-}" ] -then - sed -i '/\/\/PLACEHOLDER/ r /etc/nc/azure.php' /var/www/html/config/config.php -fi - -if [ -n "${REDIS_TLS:-}" ] -then - sed -i '/\/\/PLACEHOLDER/ r /etc/nc/redis-tls.php' /var/www/html/config/config.php -else - sed -i '/\/\/PLACEHOLDER/ r /etc/nc/redis-default.php' /var/www/html/config/config.php -fi +if ("REDIS_TLS" in $env) { + loadExtraConfig "redis-tls" +} else { + loadExtraConfig "redis-default" +} diff --git a/nix/image/scripts/occ b/nix/image/scripts/occ index cf7d6fa..07a443b 100755 --- a/nix/image/scripts/occ +++ b/nix/image/scripts/occ @@ -1,5 +1,5 @@ -#!/bin/sh +#!/bin/nu -export XDEBUG_SESSION=haze - -php $WEBROOT/occ "$@" +def --wrapped main [...rest] { + XDEBUG_SESSION=haze php $"($env.WEBROOT)/occ" ...$rest +} diff --git a/nix/image/scripts/tests b/nix/image/scripts/tests index 70691c0..524172c 100755 --- a/nix/image/scripts/tests +++ b/nix/image/scripts/tests @@ -1,7 +1,7 @@ -#!/bin/sh +#!/bin/nu -cd $WEBROOT +def main [...rest] { + cd $env.WEBROOT -export XDEBUG_SESSION=haze - -phpunit --configuration $WEBROOT/tests/phpunit-autotest.xml $@ + XDEBUG_SESSION=haze phpunit --configuration $"($env.WEBROOT)/tests/phpunit-autotest.xml" ...$rest +} From a1ed0571be4594da4381833e5b0e1659f78d884f Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 13 Apr 2026 20:59:20 +0200 Subject: [PATCH 36/62] exclude frankenphp from older php versions --- nix/image/haze.nix | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/nix/image/haze.nix b/nix/image/haze.nix index 8c7551e..5ce1051 100644 --- a/nix/image/haze.nix +++ b/nix/image/haze.nix @@ -37,8 +37,8 @@ frankenphp, nushell, }: let - inherit (builtins) toString; - inherit (lib) readFile getExe concatStringsSep splitString take; + inherit (builtins) toString compareVersions; + inherit (lib) readFile getExe concatStringsSep splitString take optionals; version = (fromTOML (readFile ../../Cargo.toml)).package.version; @@ -150,17 +150,20 @@ tag = phpVersion; fromImage = baseImage; - copyToRoot = [ - phpEnv - phpEnv.packages.composer - phpunit - (frankenphp.override { - php = php.withExtensions (import ./php-ext.nix { - inherit lib php; - enableBlackfire = false; - }); - }) - ]; + copyToRoot = + [ + phpEnv + phpEnv.packages.composer + phpunit + ] + ++ optionals ((compareVersions phpVersion "8.2") == 1) [ + (frankenphp.override { + php = php.withExtensions (import ./php-ext.nix { + inherit lib php; + enableBlackfire = false; + }); + }) + ]; }; in dockerTools.buildLayeredImage { From 814a1c3121d57a84dee4506cdbe247e52da8c0ee Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 13 Apr 2026 23:23:05 +0200 Subject: [PATCH 37/62] 2.2.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aba8178..22fafea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -932,7 +932,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "haze" -version = "2.2.0" +version = "2.2.1" dependencies = [ "async-trait", "atty", diff --git a/Cargo.toml b/Cargo.toml index 73a4c8d..9318714 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "haze" -version = "2.2.0" +version = "2.2.1" authors = ["Robin Appelman "] edition = "2021" repository = "https://codeberg.org/icewind/haze" From 24b8fd26cac1d20b1cbb5785f5bb2f60017245f0 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 13 Apr 2026 23:28:56 +0200 Subject: [PATCH 38/62] rebuild images on version number change --- .forgejo/workflows/docker.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.forgejo/workflows/docker.yaml b/.forgejo/workflows/docker.yaml index c2dbdbd..a8c493d 100644 --- a/.forgejo/workflows/docker.yaml +++ b/.forgejo/workflows/docker.yaml @@ -4,6 +4,7 @@ on: push: branches: ["main"] paths: + - "Cargo.toml" - ".forgejo/workflows/docker.yaml" - "nix/image/**" From 96b7dd671c35a24b6a187393facde27e196888a7 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 13 Apr 2026 23:42:02 +0200 Subject: [PATCH 39/62] clippy fixes --- src/service/office.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/service/office.rs b/src/service/office.rs index 9977f57..8aaf99a 100644 --- a/src/service/office.rs +++ b/src/service/office.rs @@ -33,7 +33,7 @@ impl ServiceTrait for Office { _options: &CloudOptions, ) -> Result> { let network_info = docker - .inspect_network(&network, None) + .inspect_network(network, None) .await .into_diagnostic()?; let gateway = network_info @@ -65,7 +65,7 @@ impl ServiceTrait for Office { env.push(format!("server_name={clean_id}.{}", config.proxy.address)); } - let clean_cloud_id = cloud_id.strip_prefix("haze-").unwrap_or(&cloud_id); + let clean_cloud_id = cloud_id.strip_prefix("haze-").unwrap_or(cloud_id); let hosts = if proxy_base.is_empty() { vec![] } else { @@ -77,7 +77,7 @@ impl ServiceTrait for Office { let config = ContainerCreateBody { image: Some(image.into()), - env: Some(env.into_iter().map(String::from).collect()), + env: Some(env), host_config: Some(HostConfig { network_mode: Some(network.to_string()), extra_hosts: Some(hosts), From 19e60217ea3788be546bdb6cb3102491fc45a58b Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 14 Apr 2026 14:04:44 +0200 Subject: [PATCH 40/62] 127.0.0.1 now works for federation --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 839e91c..9234589 100644 --- a/README.md +++ b/README.md @@ -274,8 +274,7 @@ proxy to allow using a wildcard domain. ### Setup - Set a DNS record for `*.haze.exmaple.com` and `haze.example.com` pointing to - your development machine. Pointing it to `127.0.0.1` will also work, but comes - with limitations like federation not being supported. + your development machine. - Set the `proxy` configuration with your domain and desired listen endpoint. - Set up a service to run `haze proxy` in the background as your own user. A systemd user service is recommended (see [haze.service](./haze.service) for an From 948d01600ebe7881db61f4bacd356ec8ff820ece Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 14 Apr 2026 14:52:41 +0200 Subject: [PATCH 41/62] readme typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9234589..d284ea7 100644 --- a/README.md +++ b/README.md @@ -295,7 +295,7 @@ will automatically point to the last created instance. Additionally, the proxy allows access to the server containers trough either `-.haze.example.com` for a specific instance, or -`.haze.example.com` for the last created instead. For example +`.haze.example.com` for the last created instance. For example `rolling-bees-mail.haze.example.com` will give access to the smtp4dev web interface of the `rolling-bees` instance. From 53e30a94aa16e41f406f743c0f160ea50484a2be Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 15 Apr 2026 23:28:11 +0200 Subject: [PATCH 42/62] fix generated urls in integration tests --- nix/image/scripts/integration | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nix/image/scripts/integration b/nix/image/scripts/integration index 8477132..32a9708 100755 --- a/nix/image/scripts/integration +++ b/nix/image/scripts/integration @@ -13,6 +13,10 @@ def main [feature: path, ...rest] { $feature = $feature | str replace "build/integration/" "" } + # don't use the proxy urls for generated urls + occ config:system:delete overwritehost + occ config:system:delete overwriteprotocol + cd $workdir bash run.sh $feature ...$rest } From 9de626a9058be9be25e65342ee0d021465558dad Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 16 Apr 2026 16:30:04 +0200 Subject: [PATCH 43/62] typo --- src/sources.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sources.rs b/src/sources.rs index b99c54a..ccfe564 100644 --- a/src/sources.rs +++ b/src/sources.rs @@ -122,7 +122,7 @@ pub async fn download_nc(config: &HazeConfig, version: &str) -> Result Date: Fri, 17 Apr 2026 22:22:24 +0200 Subject: [PATCH 44/62] autosetup for ldap fixes #19 --- src/service/ldap.rs | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/service/ldap.rs b/src/service/ldap.rs index aa082b0..22b7383 100644 --- a/src/service/ldap.rs +++ b/src/service/ldap.rs @@ -86,6 +86,47 @@ impl ServiceTrait for Ldap { ) -> Result { self.is_running(docker, cloud_id).await } + + async fn post_setup( + &self, + _docker: &Docker, + _cloud_id: &str, + _config: &HazeConfig, + ) -> Result> { + Ok(vec![ + "occ ldap:create-empty-config".into(), + "occ ldap:set-config s01 ldapHost 'ldap://ldap'".into(), + "occ ldap:set-config s01 ldapPort '389'".into(), + "occ ldap:set-config s01 ldapAgentName 'cn=admin,dc=example,dc=org'".into(), + "occ ldap:set-config s01 ldapAgentPassword 'haze'".into(), + "occ ldap:set-config s01 ldapBase 'dc=example,dc=org'".into(), + "occ ldap:set-config s01 ldapBaseUsers 'dc=example,dc=org'".into(), + "occ ldap:set-config s01 ldapBaseGroups 'dc=example,dc=org'".into(), + "occ ldap:set-config s01 ldapLoginFilter '(&(&(objectclass=inetOrgPerson))(uid=%uid))'" + .into(), + "occ ldap:set-config s01 ldapUserFilter '((objectclass=inetOrgPerson))'".into(), + "occ ldap:set-config s01 ldapUserFilterMode '0'".into(), + "occ ldap:set-config s01 ldapUserDisplayName 'sn'".into(), + "occ ldap:set-config s01 ldapUserFilterObjectclass 'inetOrgPerson'".into(), + "occ ldap:set-config s01 ldapGroupFilter '(&(|(objectclass=posixGroup)))'".into(), + "occ ldap:set-config s01 ldapGroupFilterObjectclass 'posixGroup'".into(), + "occ ldap:set-config s01 ldapEmailAttribute 'email'".into(), + "occ ldap:set-config s01 ldapUuidUserAttribute 'email'".into(), + "occ ldap:set-config s01 ldapUuidUserAttribute 'auto'".into(), + "occ ldap:set-config s01 ldapUuidGroupAttribute 'auto'".into(), + "occ ldap:set-config s01 ldapLoginFilterUsername '1'".into(), + "occ ldap:set-config s01 ldapConfigurationActive '1'".into(), + ]) + } + + async fn start_message( + &self, + _docker: &Docker, + _cloud_id: &str, + _proxy: &ProxyConfig, + ) -> Result> { + Ok(Some(format!("\nLdap users provisioned:\n\t'cn=admin,dc=example,dc=org' and password 'haze'\n\t'cn=ldaptest,dc=example,dc=org' and password 'test'\n\nldaptest is available for login\n"))) + } } #[derive(Debug, Clone, Eq, PartialEq)] @@ -185,8 +226,6 @@ impl ServiceTrait for LdapAdmin { return Err(Report::msg("ldap admin not started")); }; let addr = proxy.addr(&id, IpAddr::from_str(&ip).unwrap()); - Ok(Some(format!( - "Ldap admin running at: {addr} with 'cn=admin,dc=example,dc=org' and password 'haze'" - ))) + Ok(Some(format!("Ldap admin running at: {addr}"))) } } From b4a77997ab31836cdf1eb47298e0ba10afacc415 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 28 Apr 2026 18:54:44 +0200 Subject: [PATCH 45/62] fpm status page --- nix/image/configs/nginx.conf | 11 +++++++++++ nix/image/configs/php-fpm.conf | 1 + 2 files changed, 12 insertions(+) diff --git a/nix/image/configs/nginx.conf b/nix/image/configs/nginx.conf index b25ab52..15a57c3 100644 --- a/nix/image/configs/nginx.conf +++ b/nix/image/configs/nginx.conf @@ -109,5 +109,16 @@ http { expires 7d; # Cache-Control policy borrowed from `.htaccess` access_log off; # Optional: Don't log access to assets } + + location /fpm-status { + include /conf/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_pass php-handler; + fastcgi_read_timeout 3600; + proxy_request_buffering off; + fastcgi_request_buffering off; + fastcgi_buffering off; + } } } diff --git a/nix/image/configs/php-fpm.conf b/nix/image/configs/php-fpm.conf index e6292fb..c7751a4 100644 --- a/nix/image/configs/php-fpm.conf +++ b/nix/image/configs/php-fpm.conf @@ -21,6 +21,7 @@ pm.max_children = 5 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 3 +pm.status_path = /fpm-status clear_env = no From ea3f89bb040f2f2fc8d089768eb450f071dff0c8 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 28 Apr 2026 18:55:08 +0200 Subject: [PATCH 46/62] fpm logs --- nix/image/configs/php-fpm.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/image/configs/php-fpm.conf b/nix/image/configs/php-fpm.conf index c7751a4..89379d8 100644 --- a/nix/image/configs/php-fpm.conf +++ b/nix/image/configs/php-fpm.conf @@ -1,11 +1,11 @@ [global] -error_log = /proc/self/fd/2 +error_log = /var/log/php-fpm-error.log daemonize = no [www] -access.log = /proc/self/fd/2 +access.log =/var/log/php-fpm-access.log user = haze group = haze From 512b669a7c02d4d5798b8579731fd7009d68d86c Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 30 Apr 2026 19:08:10 +0200 Subject: [PATCH 47/62] sqlite table mode --- nix/image/bootstrap | 1 + nix/image/configs/home/.sqliterc | 1 + 2 files changed, 2 insertions(+) create mode 100644 nix/image/configs/home/.sqliterc diff --git a/nix/image/bootstrap b/nix/image/bootstrap index d16e605..2c9dcff 100755 --- a/nix/image/bootstrap +++ b/nix/image/bootstrap @@ -42,6 +42,7 @@ if ((getent group $HAZE_GID | length) > 0) { useradd -u $HAZE_UID -g $HAZE_GID haze } chown -R $"haze:($HAZE_GID)" /home/haze +ls -af /etc/home | each {|file| ln -s $file.name $"/home/haze/($file.name | path basename)" } if ("/var/run/docker.sock" | path exists) { let dockerGid = stat --format "%g" /var/run/docker.sock diff --git a/nix/image/configs/home/.sqliterc b/nix/image/configs/home/.sqliterc new file mode 100644 index 0000000..0d5fe04 --- /dev/null +++ b/nix/image/configs/home/.sqliterc @@ -0,0 +1 @@ +.mode table From 9e080f0d544da3950337baf966068cc08246c0e1 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 7 May 2026 18:40:17 +0200 Subject: [PATCH 48/62] fix using '--' flags in test and integration commands --- nix/image/scripts/integration | 2 +- nix/image/scripts/tests | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/image/scripts/integration b/nix/image/scripts/integration index 32a9708..d8187d9 100755 --- a/nix/image/scripts/integration +++ b/nix/image/scripts/integration @@ -1,6 +1,6 @@ #!/bin/nu -def main [feature: path, ...rest] { +def --wrapped main [feature: path, ...rest] { mut feature = $feature; mut workdir = $"($env.WEBROOT)/build/integration" if ($feature | str starts-with "apps/") { diff --git a/nix/image/scripts/tests b/nix/image/scripts/tests index 524172c..868f225 100755 --- a/nix/image/scripts/tests +++ b/nix/image/scripts/tests @@ -1,6 +1,6 @@ #!/bin/nu -def main [...rest] { +def --wrapped main [...rest] { cd $env.WEBROOT XDEBUG_SESSION=haze phpunit --configuration $"($env.WEBROOT)/tests/phpunit-autotest.xml" ...$rest From 373ce0f3fd63cd782efd881d0c1e18b12eb1baec Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 7 May 2026 18:58:54 +0200 Subject: [PATCH 49/62] add php-imagick fixes #22 --- nix/image/php-ext.nix | 1 + nix/image/php.nix | 38 +++++++++++++++++--------------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/nix/image/php-ext.nix b/nix/image/php-ext.nix index 2715cf2..69d23e1 100644 --- a/nix/image/php-ext.nix +++ b/nix/image/php-ext.nix @@ -35,6 +35,7 @@ in gmp apcu ffi + imagick ] ++ optionals (!debug) [ smbclient # this breaks the build for no apparent reason diff --git a/nix/image/php.nix b/nix/image/php.nix index 4922655..8ef3d31 100644 --- a/nix/image/php.nix +++ b/nix/image/php.nix @@ -2,27 +2,23 @@ lib, php, debug ? false, -}: let - inherit (builtins) compareVersions; - inherit (lib) optionals; - withBlackfire = !debug && ((compareVersions php.version "8.1.0") == 1); -in - php.buildEnv { - extensions = import ./php-ext.nix {inherit lib php debug;}; - extraConfig = '' - xdebug.mode=debug,trace,profile - xdebug.start_with_request=trigger - xdebug.discover_client_host=false - xdebug.client_host=hazehost - xdebug.log_level=0 - xdebug.output_dir=/tmp/xdebug +}: +php.buildEnv { + extensions = import ./php-ext.nix {inherit lib php debug;}; + extraConfig = '' + xdebug.mode=debug,trace,profile + xdebug.start_with_request=trigger + xdebug.discover_client_host=false + xdebug.client_host=hazehost + xdebug.log_level=0 + xdebug.output_dir=/tmp/xdebug - memory_limit=512M + memory_limit=512M - post_max_size 10G - upload_max_filesize 10G + post_max_size 10G + upload_max_filesize 10G - apc.enable_cli=1 - opcache.enable_cli=1 - ''; - } + apc.enable_cli=1 + opcache.enable_cli=1 + ''; +} From 8771e7dc5f3f02b152c403575d3152722f701a67 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 8 May 2026 18:57:19 +0200 Subject: [PATCH 50/62] switch to multiprogress for git pull --- src/git.rs | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/git.rs b/src/git.rs index 3a797c3..c20d450 100644 --- a/src/git.rs +++ b/src/git.rs @@ -2,11 +2,13 @@ use crate::config::HazeConfig; use crate::Result; use git2::build::CheckoutBuilder; use git2::{Branch, BranchType, Repository, RepositoryState}; +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use miette::{Context, IntoDiagnostic}; use std::fs::read_dir; use std::iter::once; use std::path::PathBuf; use std::process::Command; +use std::time::Duration; fn find_app_repos(config: &HazeConfig) -> Result> { let apps_dirs = once(config.sources_root.as_path().join("apps")) @@ -83,6 +85,9 @@ const GIT_BINARY: &str = match option_env!("GIT_BINARY") { pub fn pull_all(config: &HazeConfig) -> Result<()> { let (max_app, max_branch) = longest_app_branch(config)?; + let progress = MultiProgress::new(); + let pull_style = ProgressStyle::with_template("{spinner:.green} {msg}").unwrap(); + for app_dir in find_app_repos(config)? { let app_name = app_dir.file_name().unwrap().to_string_lossy(); let repo = Repository::init(&app_dir) @@ -90,17 +95,26 @@ pub fn pull_all(config: &HazeConfig) -> Result<()> { .wrap_err_with(|| format!("Failed to open repository {}", app_dir.display()))?; let branch_name = current_branch_name(&repo).unwrap_or("unknown".into()); - print!( - "{app_name: Result<()> { .wrap_err_with(|| format!("Failed to run git pull for {}", app_dir.display()))?; if output.status.success() { - println!(" ✓"); + bar.set_message(msg(" ✓")); } else { - println!(" ❌"); - eprintln!("{}", String::from_utf8_lossy(&output.stderr)) + let err = String::from_utf8_lossy(&output.stderr); + let err_line = err.lines().next().unwrap(); + bar.set_message(msg(&format!(" ❌ {err_line}"))); } + bar.finish(); } Ok(()) } From b977cd9dfa900324e90ffb496b42ac4b5bb1b799 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 8 May 2026 19:09:31 +0200 Subject: [PATCH 51/62] parallelize git pull fixes #21 --- Cargo.lock | 46 +++++++++++++++++++++++++++ Cargo.toml | 1 + src/git.rs | 92 ++++++++++++++++++++++++++++++++---------------------- 3 files changed, 101 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22fafea..90f1ad9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -506,6 +506,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.7" @@ -956,6 +981,7 @@ dependencies = [ "opener", "owo-colors", "petname", + "rayon", "reqwest", "serde", "serde_json", @@ -2002,6 +2028,26 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.10" diff --git a/Cargo.toml b/Cargo.toml index 9318714..947de7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ zip = "8.1.0" sha2 = "0.11.0-rc.5" base16ct = { version = "1.0.0", features = ["alloc"] } indicatif = "0.18.4" +rayon = "1.12.0" hyper-reverse-proxy = { version = "0.5.2-dev", git = "https://github.com/chpio/hyper-reverse-proxy", rev = "6934877eb74465204f605cc1c05ca5a9772db7c0" } hyper = "1.8.1" diff --git a/src/git.rs b/src/git.rs index c20d450..50ebb05 100644 --- a/src/git.rs +++ b/src/git.rs @@ -4,6 +4,8 @@ use git2::build::CheckoutBuilder; use git2::{Branch, BranchType, Repository, RepositoryState}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use miette::{Context, IntoDiagnostic}; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use rayon::ThreadPoolBuilder; use std::fs::read_dir; use std::iter::once; use std::path::PathBuf; @@ -88,49 +90,63 @@ pub fn pull_all(config: &HazeConfig) -> Result<()> { let progress = MultiProgress::new(); let pull_style = ProgressStyle::with_template("{spinner:.green} {msg}").unwrap(); - for app_dir in find_app_repos(config)? { - let app_name = app_dir.file_name().unwrap().to_string_lossy(); - let repo = Repository::init(&app_dir) - .into_diagnostic() - .wrap_err_with(|| format!("Failed to open repository {}", app_dir.display()))?; - let branch_name = current_branch_name(&repo).unwrap_or("unknown".into()); + let pool = ThreadPoolBuilder::new() + .num_threads(8) + .build() + .into_diagnostic()?; + let repos = find_app_repos(config)?.collect::>(); - let bar = ProgressBar::new_spinner().with_style(pull_style.clone()); - bar.enable_steady_tick(Duration::from_millis(100)); - let bar = progress.add(bar); + pool.install(|| { + repos.par_iter().for_each(|app_dir| { + let app_name = app_dir.file_name().unwrap().to_string_lossy(); + let Ok(repo) = Repository::init(&app_dir) else { + return; + }; + let branch_name = current_branch_name(&repo).unwrap_or("unknown".into()); - let msg = |state: &str| { - format!( - "{app_name: output, + Err(error) => { + bar.set_message(msg(&format!(" ⨯ {error}"))); + return; + } + }; + + if output.status.success() { + bar.set_message(msg(" ✓")); + } else { + let err = String::from_utf8_lossy(&output.stderr); + let err_line = err.lines().next().unwrap(); + bar.set_message(msg(&format!(" ⨯ {err_line}"))); + } bar.finish(); - continue; - } + }); + }); - bar.set_message(msg("")); - - let output = Command::new(GIT_BINARY) - .arg("pull") - .current_dir(&app_dir) - .output() - .into_diagnostic() - .wrap_err_with(|| format!("Failed to run git pull for {}", app_dir.display()))?; - - if output.status.success() { - bar.set_message(msg(" ✓")); - } else { - let err = String::from_utf8_lossy(&output.stderr); - let err_line = err.lines().next().unwrap(); - bar.set_message(msg(&format!(" ❌ {err_line}"))); - } - bar.finish(); - } Ok(()) } From ad999702aabb605c73c597fca95b992f14f1ac54 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 8 May 2026 20:24:17 +0200 Subject: [PATCH 52/62] improve websocket proxying --- Cargo.lock | 49 +++++++++++++++------------- Cargo.toml | 7 ++-- nix/package.nix | 2 +- src/proxy.rs | 72 ++++++++++++++++++++++++++++++++---------- src/service.rs | 6 ++++ src/service/webhook.rs | 71 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 164 insertions(+), 43 deletions(-) create mode 100644 src/service/webhook.rs diff --git a/Cargo.lock b/Cargo.lock index 90f1ad9..ccedb17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,6 +147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ "axum-core", + "axum-macros", "bytes", "form_urlencoded", "futures-util", @@ -192,6 +193,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -921,25 +933,6 @@ dependencies = [ "url", ] -[[package]] -name = "h2" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "hashbrown" version = "0.15.2" @@ -957,7 +950,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "haze" -version = "2.2.1" +version = "2.2.2" dependencies = [ "async-trait", "atty", @@ -991,6 +984,7 @@ dependencies = [ "tar", "termion", "tokio", + "tokio-stream", "toml", "tracing", "tracing-subscriber", @@ -1098,7 +1092,6 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2", "http", "http-body", "httparse", @@ -1129,8 +1122,9 @@ dependencies = [ [[package]] name = "hyper-reverse-proxy" version = "0.5.2-dev" -source = "git+https://github.com/chpio/hyper-reverse-proxy?rev=6934877eb74465204f605cc1c05ca5a9772db7c0#6934877eb74465204f605cc1c05ca5a9772db7c0" +source = "git+https://code.betamike.com/micropelago/hyper-reverse-proxy.git?rev=d5a6f799189360d9449ae47ab3cdde511f02cf39#d5a6f799189360d9449ae47ab3cdde511f02cf39" dependencies = [ + "http-body-util", "hyper", "hyper-util", "tokio", @@ -2741,6 +2735,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.13" diff --git a/Cargo.toml b/Cargo.toml index 947de7c..6da76a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "haze" -version = "2.2.1" +version = "2.2.2" authors = ["Robin Appelman "] edition = "2021" repository = "https://codeberg.org/icewind/haze" @@ -12,6 +12,7 @@ bollard = "0.20.1" maplit = "1.0.2" camino = { version = "1.2.2", features = ["serde1"] } tokio = { version = "1.49.0", features = ["fs", "macros", "signal", "rt-multi-thread"] } +tokio-stream = { version = "0.1.18", features = ["net"] } futures-util = "0.3.32" termion = "4.0.6" opener = "0.8.4" @@ -41,10 +42,10 @@ base16ct = { version = "1.0.0", features = ["alloc"] } indicatif = "0.18.4" rayon = "1.12.0" -hyper-reverse-proxy = { version = "0.5.2-dev", git = "https://github.com/chpio/hyper-reverse-proxy", rev = "6934877eb74465204f605cc1c05ca5a9772db7c0" } +hyper-reverse-proxy = { version = "0.5.2-dev", git = "https://code.betamike.com/micropelago/hyper-reverse-proxy.git", rev = "d5a6f799189360d9449ae47ab3cdde511f02cf39" } hyper = "1.8.1" hyper-util = "0.1.20" -axum = { version = "0.8.8", features = ["tokio"] } +axum = { version = "0.8.8", features = ["tokio", "macros"] } [profile.release] lto = true diff --git a/nix/package.nix b/nix/package.nix index fcd6489..34a4388 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -20,7 +20,7 @@ in cargoLock = { lockFile = ../Cargo.lock; outputHashes = { - "hyper-reverse-proxy-0.5.2-dev" = "sha256-+ebi4FVVkiOpf75e8K5oGkHJBYQjLNJhUPNj+78zd7Q="; + "hyper-reverse-proxy-0.5.2-dev" = "sha256-awmj5aLFTea+kj81cwmfP1HWlWezwEKfyQSUJWjtamk="; }; }; } diff --git a/src/proxy.rs b/src/proxy.rs index d5a3b0e..4784a18 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -5,26 +5,34 @@ use axum::http::header::HOST; use axum::http::HeaderValue; use axum::{ body::Body, - extract::{Request, State}, + extract::Request, response::{IntoResponse, Response}, - Router, }; use bollard::Docker; +use futures_util::StreamExt; +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 std::collections::HashMap; +use std::convert::Infallible; use std::fs::{create_dir_all, set_permissions}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; +use std::pin::pin; use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::Duration; +use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::UnixListener; use tokio::signal::ctrl_c; use tokio::spawn; use tokio::time::sleep; +use tokio_stream::wrappers::{TcpListenerStream, UnixListenerStream}; use tracing::{debug, error, info}; struct ActiveInstances { @@ -163,20 +171,26 @@ async fn serve(instances: ActiveInstances, listen: String, base_address: String) ctrl_c().await.ok(); }; - let app = Router::new().fallback(handler).with_state(AppState { + let state = AppState { instances: instances.clone(), base_address: base_address.clone(), proxy_client: Arc::new(proxy_client), - }); + }; if !listen.starts_with('/') { let addr: SocketAddr = listen.parse().into_diagnostic()?; let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); println!("listening on {}", listener.local_addr().unwrap()); - axum::serve(listener, app) - .with_graceful_shutdown(cancel) - .await - .unwrap(); + let mut connections = pin!(TcpListenerStream::new(listener).take_until(cancel)); + + while let Some(stream) = connections.next().await { + match stream { + Ok(stream) => handle_connection(state.clone(), stream), + Err(error) => { + error!(%error, "connection failed"); + } + } + } } else { let listen: PathBuf = listen.into(); if let Some(parent) = listen.parent() { @@ -187,18 +201,42 @@ async fn serve(instances: ActiveInstances, listen: String, base_address: String) } let _ = tokio::fs::remove_file(&listen).await; - let uds = UnixListener::bind(&listen).unwrap(); + let listener = UnixListener::bind(&listen).unwrap(); + println!("listening on {}", listen.display()); set_permissions(&listen, PermissionsExt::from_mode(0o666)).into_diagnostic()?; - axum::serve(uds, app) - .with_graceful_shutdown(cancel) - .await - .unwrap(); + let mut connections = pin!(UnixListenerStream::new(listener).take_until(cancel)); + + while let Some(stream) = connections.next().await { + match stream { + Ok(stream) => handle_connection(state.clone(), stream), + Err(error) => { + error!(%error, "connection failed"); + } + } + } } Ok(()) } +fn handle_connection( + state: AppState, + stream: I, +) { + let io = TokioIo::new(stream); + // Spawn a tokio task to serve multiple connections concurrently + tokio::task::spawn(async move { + if let Err(err) = http1::Builder::new() + .serve_connection(io, service_fn(move |req| handler(state.clone(), req))) + .with_upgrades() + .await + { + eprintln!("Error serving connection: {:?}", err); + } + }); +} + async fn get_remote( host: Option<&HeaderValue>, instances: &ActiveInstances, @@ -232,9 +270,9 @@ async fn get_remote( } } -type Client = hyper_util::client::legacy::Client; +type Client = hyper_util::client::legacy::Client; -async fn handler(State(state): State, mut req: Request) -> Result { +async fn handler(state: AppState, mut req: Request) -> Result { let host = req.headers().get(HOST).cloned(); let remote = match get_remote(host.as_ref(), &state.instances, &state.base_address).await { Ok(remote) => remote, @@ -259,13 +297,13 @@ async fn handler(State(state): State, mut req: Request) -> Result Ok(response.map(Body::new)), Err(error) => { - error!(%error, "error while proxying request"); + error!(?error, "error while proxying request"); Ok(StatusCode::BAD_REQUEST.into_response()) } } diff --git a/src/service.rs b/src/service.rs index 49bb636..490f617 100644 --- a/src/service.rs +++ b/src/service.rs @@ -14,6 +14,7 @@ mod sftp; mod redis; mod sharded; mod smb; +mod webhook; use crate::cloud::CloudOptions; use crate::config::{HazeConfig, Preset, ProxyConfig}; @@ -32,6 +33,7 @@ use crate::service::redis::Redis; use crate::service::sftp::Sftp; use crate::service::sharded::{Sharding, ShardingMigrate, ShardingMigrateUnset, SingleShard}; use crate::service::smb::Smb; +use crate::service::webhook::Webhook; use bollard::models::ContainerState; use bollard::Docker; use enum_dispatch::enum_dispatch; @@ -296,6 +298,8 @@ pub enum ServiceType { RedisTls, /// Use FrankenPHP instead of PHP-FPM FrankenPhp, + /// Webhook test listener + Webhook, } #[enum_dispatch] @@ -326,6 +330,7 @@ pub enum Service { Redis(Redis), RedisTls(RedisTls), FrankenPhp(FrankenPhp), + Webhook(Webhook), Preset(PresetService), } @@ -369,6 +374,7 @@ impl Service { ServiceType::Redis => Some(vec![Service::Redis(Redis)]), ServiceType::RedisTls => Some(vec![Service::RedisTls(RedisTls)]), ServiceType::FrankenPhp => Some(vec![Service::FrankenPhp(FrankenPhp)]), + ServiceType::Webhook => Some(vec![Service::Webhook(Webhook)]), } } else { presets diff --git a/src/service/webhook.rs b/src/service/webhook.rs new file mode 100644 index 0000000..3967483 --- /dev/null +++ b/src/service/webhook.rs @@ -0,0 +1,71 @@ +use crate::cloud::CloudOptions; +use crate::config::HazeConfig; +use crate::image::pull_image; +use crate::service::ServiceTrait; +use crate::Result; +use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig}; +use bollard::query_parameters::CreateContainerOptions; +use bollard::Docker; +use maplit::hashmap; +use miette::IntoDiagnostic; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Webhook; + +#[async_trait::async_trait] +impl ServiceTrait for Webhook { + fn name(&self) -> &str { + "webhook" + } + + async fn spawn( + &self, + docker: &Docker, + cloud_id: &str, + network: &str, + _config: &HazeConfig, + _options: &CloudOptions, + ) -> Result> { + let image = "ghcr.io/tarampampam/webhook-tester"; + pull_image(docker, image).await?; + let options = Some(CreateContainerOptions { + name: self.container_name(cloud_id), + ..CreateContainerOptions::default() + }); + let config = ContainerCreateBody { + image: Some(image.into()), + host_config: Some(HostConfig { + network_mode: Some(network.to_string()), + ..Default::default() + }), + 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(options, config) + .await + .into_diagnostic()? + .id; + docker.start_container(&id, None).await.into_diagnostic()?; + Ok(vec![id]) + } + + fn container_name(&self, cloud_id: &str) -> Option { + Some(format!("{}-webhook", cloud_id)) + } + + fn proxy_port(&self) -> u16 { + 8080 + } +} From 39ba7a2a5309e18a583513e2efc7f688830bc75c Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 8 May 2026 20:33:48 +0200 Subject: [PATCH 53/62] mention webhook tester in readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d284ea7..da8fecc 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,8 @@ Additionally, you can use the following options when starting an instance: generation. - `mail`: start a [smtp4dev](https://github.com/rnwood/smtp4dev) server and configure it the mail server. +- `webhook` start a + [webhook tester](https://github.com/tarampampam/webhook-tester) - `redis`: start a separate container for redis. - `redis-tls`: connect to redis over TLS. - ``: by specifying the path to an app package this package From cd9740675f0c0de66b25c6d13303a286df9f0def Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 8 May 2026 22:23:40 +0200 Subject: [PATCH 54/62] clippy fixes --- src/git.rs | 4 ++-- src/service/ldap.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/git.rs b/src/git.rs index 50ebb05..fc31255 100644 --- a/src/git.rs +++ b/src/git.rs @@ -99,7 +99,7 @@ pub fn pull_all(config: &HazeConfig) -> Result<()> { pool.install(|| { repos.par_iter().for_each(|app_dir| { let app_name = app_dir.file_name().unwrap().to_string_lossy(); - let Ok(repo) = Repository::init(&app_dir) else { + let Ok(repo) = Repository::init(app_dir) else { return; }; let branch_name = current_branch_name(&repo).unwrap_or("unknown".into()); @@ -126,7 +126,7 @@ pub fn pull_all(config: &HazeConfig) -> Result<()> { let output = match Command::new(GIT_BINARY) .arg("pull") - .current_dir(&app_dir) + .current_dir(app_dir) .output() { Ok(output) => output, diff --git a/src/service/ldap.rs b/src/service/ldap.rs index 22b7383..bd41ba3 100644 --- a/src/service/ldap.rs +++ b/src/service/ldap.rs @@ -125,7 +125,7 @@ impl ServiceTrait for Ldap { _cloud_id: &str, _proxy: &ProxyConfig, ) -> Result> { - Ok(Some(format!("\nLdap users provisioned:\n\t'cn=admin,dc=example,dc=org' and password 'haze'\n\t'cn=ldaptest,dc=example,dc=org' and password 'test'\n\nldaptest is available for login\n"))) + Ok(Some("\nLdap users provisioned:\n\t'cn=admin,dc=example,dc=org' and password 'haze'\n\t'cn=ldaptest,dc=example,dc=org' and password 'test'\n\nldaptest is available for login\n".into())) } } From f99238121b0755e302c4620aaab65f90dd158a21 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sat, 9 May 2026 16:45:52 +0200 Subject: [PATCH 55/62] nix cleanup --- nix/package.nix | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nix/package.nix b/nix/package.nix index 34a4388..c6166f0 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -1,6 +1,5 @@ { rustPlatform, - pkg-config, lib, git, }: let @@ -10,7 +9,7 @@ src = sourceByRegex ../. ["Cargo.*" "(src|certificates)(/.*)?"]; version = (fromTOML (readFile ../Cargo.toml)).package.version; in - rustPlatform.buildRustPackage rec { + rustPlatform.buildRustPackage { pname = "haze"; inherit src version; From 204fb676d67aa0f794e070662310a79ae6e7e429 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 26 May 2026 20:45:29 +0200 Subject: [PATCH 56/62] add sftp with key authentication service --- README.md | 2 + certificates/sftp/id_rsa | 38 +++++++++++ certificates/sftp/id_rsa.pub | 1 + src/main.rs | 7 +- src/service.rs | 28 ++++++-- src/service/clam.rs | 46 ++++++------- src/service/dav.rs | 12 ++-- src/service/imaginary.rs | 13 ++-- src/service/kaspersky.rs | 24 +++---- src/service/ldap.rs | 47 +++++++------ src/service/mail.rs | 16 ++--- src/service/objectstore.rs | 22 +++---- src/service/oc.rs | 2 +- src/service/office.rs | 20 +++++- src/service/onlyoffice.rs | 46 ++++++++++--- src/service/push.rs | 16 +++-- src/service/redis.rs | 8 ++- src/service/sftp.rs | 123 ++++++++++++++++++++++++++++++++--- src/service/smb.rs | 14 ++-- 19 files changed, 349 insertions(+), 136 deletions(-) create mode 100644 certificates/sftp/id_rsa create mode 100644 certificates/sftp/id_rsa.pub diff --git a/README.md b/README.md index da8fecc..026c190 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ Additionally, you can use the following options when starting an instance: - `smb`: set up a samba server for external storage use. - `dav`: set up a WebDAV server for external storage use. - `sftp`: set up a SFTP server for external storage use. +- `sftp-key`: set up a SFTP server for external storage use with public key + authentication. - `kaspersky`: set up a kaspersky scan engine server in http mode. ( Requires [manually setting up the image](https://github.com/icewind1991/kaspersky-docker)) - `kaspersky-icap`: setup a kaspersky scan engine server in ICAP mode. diff --git a/certificates/sftp/id_rsa b/certificates/sftp/id_rsa new file mode 100644 index 0000000..c8dcc4d --- /dev/null +++ b/certificates/sftp/id_rsa @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn +NhAAAAAwEAAQAAAYEA323aWqH6YwRLbCBO94UKOkfnJ2m6Zsic0dMt3TmDnjLU0JzpOt7w +t5+mMZrEKQTpefozyUHo3z+HkmllLAGOupNy3A+jG2O955UUgw0dGfu6j6OOb66Du9jpqt +8BQ6gr3cEYASplPI7B889/cVpJ5l1HiBUgyR7Z16v15qCDtmpFVIECAdICEmPosfmZutt3 +YYl9xLay5WCmUztWS/amPcGs0DOEGrWeCtdxGKWT3TywdBKyQ0PbdYMamgDIT7JV1ZSzZP +aly4sB7E+dpS5AgBFVXmZ61151KN1TJ8gyoUjFhY7ctYEIpncZmyT4PYvyIvxRsbJtvERi +eNH8DoX5DwtqcxbgHK0OwYtdl4ydRXToYo3l+qIidf+g8ADVea/mbkfTPegdToo3LOuThX +OwExDlukpM8obFDpz1Yl1L6rRJAVNO1KmHWhn6to23jtYjBhczA2nkemQXQbVSjc/hItjQ +DIFNMOsLW33P+Y2k9LkpI0TL09ogOxOFZzGZp2tNAAAFgIgMIZ+IDCGfAAAAB3NzaC1yc2 +EAAAGBAN9t2lqh+mMES2wgTveFCjpH5ydpumbInNHTLd05g54y1NCc6Tre8LefpjGaxCkE +6Xn6M8lB6N8/h5JpZSwBjrqTctwPoxtjveeVFIMNHRn7uo+jjm+ug7vY6arfAUOoK93BGA +EqZTyOwfPPf3FaSeZdR4gVIMke2der9eagg7ZqRVSBAgHSAhJj6LH5mbrbd2GJfcS2suVg +plM7Vkv2pj3BrNAzhBq1ngrXcRilk908sHQSskND23WDGpoAyE+yVdWUs2T2pcuLAexPna +UuQIARVV5metdedSjdUyfIMqFIxYWO3LWBCKZ3GZsk+D2L8iL8UbGybbxEYnjR/A6F+Q8L +anMW4BytDsGLXZeMnUV06GKN5fqiInX/oPAA1Xmv5m5H0z3oHU6KNyzrk4VzsBMQ5bpKTP +KGxQ6c9WJdS+q0SQFTTtSph1oZ+raNt47WIwYXMwNp5HpkF0G1Uo3P4SLY0AyBTTDrC1t9 +z/mNpPS5KSNEy9PaIDsThWcxmadrTQAAAAMBAAEAAAGAWCkM/TEnztU9e3M+JX253OhNRe +h6lB75ffOxh7avgAc3oP8hKkkYu6PDnJQgbb0R8T7wGywmGp0DPhrXQGd27ZjLvBhxeBfB +sbTJ7LIKdxu0cAQN6nR2Z3M+NF2dLpiXgn80HRWg76W20yDffRcuzLamyIPptWI2e9rPAw +r4HczOAXuMErLOfXotsbg22BvL/dEWLr4WVdruli32LbArxXd73IVPTYi3TTjYV+zRrPzK +9WoBK/iFClfKcdT4NTY82llQesuUNu640lEJtT2G3Iba8UZnohyzm/S+UbeU65z8DKD5co +P7+QehxQSV+kj2BZnTi0WEwsD+GTznJYR5rvUsJCCAzoISsWrncSSgOQhF2XeW/T4ewvH+ +njLZViEhdG8R3kkdDjJG91OrSgrEqlk6Qhz1xEsv1rCOR69En7EJP3TNNrymPXPASrAnuE +HQkrVgGUfGqyD1sw1e6nBfNWisuw+g99CieIB8EI9WwpxQdKqWNU9Hjx+SAdC3NrPjAAAA +wQCo0hUGjSf6xhcgeaNa0gWSKEVuFhxR/FaCPTKadV7Md0APW4toeQZDujzDFlCZbQTZjC +0723B4lKugDzXfsOgvOTKp4vEjZOu0YGruS00LFWM7Sutdzx68b/ZMFALzITt/myKVMdpv +WpaO+3+PyEYIQH44QrSWw7cKLzNiZ8kt2drPkPktub4o2h5TdIBluEQLJDPMejy8IqQEU8 +aOyJOMvYxAbGAWY7Ck9DGlcJgaFdORROW8d8ZGrHQkyRl41JQAAADBAPeUMsrbI17wPP/s +Tsrkms5ws5yTz0xle2Wn6HwDSzQRSdn5abnIDYb3QSy0nRBvczef6ssH65dl50+2V4BV2L +MwHcmKD6/UoFsWwP/RMf1EoacPFiEAWJGxFbOthNX+rx5BpbUHNoQd8xby+88saDI0e8W6 +36HPBZrAZhQljkMa4OJqZDDCpOJvSndXwkZ789E96uprKopJZGwlLmfMtikQpNXT9R+I0b +SQCJj4yNakcdOE/7UifkOR02u+pgux3wAAAMEA5wdelKwGQ0EdkIF2TM844uLPszo3ZSH+ +Heff/Lbxs1Y+oL0NTJQicwMF0d9WEwBoTZJpuzsQEA1zkfmW0gi2womIRmiY0ZhpxbBuhO +6XePMIhUfQmWWjaUbAkrNB0eJkSTuUGzwxVkMXehrMuj4gYe8GMC8GgULbP0A8FjH01fKk +jFwgg4WAg6zUTpck12bh49NZRFyXIbXNk/jjxJtb0p//5TRTUQ6mR5IloaNTM23EiF6tle +Y6CAchnyhHO0BTAAAACWhhemVAaGF6ZQE= +-----END OPENSSH PRIVATE KEY----- diff --git a/certificates/sftp/id_rsa.pub b/certificates/sftp/id_rsa.pub new file mode 100644 index 0000000..81a5db6 --- /dev/null +++ b/certificates/sftp/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDfbdpaofpjBEtsIE73hQo6R+cnabpmyJzR0y3dOYOeMtTQnOk63vC3n6YxmsQpBOl5+jPJQejfP4eSaWUsAY66k3LcD6MbY73nlRSDDR0Z+7qPo45vroO72Omq3wFDqCvdwRgBKmU8jsHzz39xWknmXUeIFSDJHtnXq/XmoIO2akVUgQIB0gISY+ix+Zm623dhiX3EtrLlYKZTO1ZL9qY9wazQM4QatZ4K13EYpZPdPLB0ErJDQ9t1gxqaAMhPslXVlLNk9qXLiwHsT52lLkCAEVVeZnrXXnUo3VMnyDKhSMWFjty1gQimdxmbJPg9i/Ii/FGxsm28RGJ40fwOhfkPC2pzFuAcrQ7Bi12XjJ1FdOhijeX6oiJ1/6DwANV5r+ZuR9M96B1Oijcs65OFc7ATEOW6SkzyhsUOnPViXUvqtEkBU07UqYdaGfq2jbeO1iMGFzMDaeR6ZBdBtVKNz+Ei2NAMgU0w6wtbfc/5jaT0uSkjRMvT2iA7E4VnMZmna00= haze@haze diff --git a/src/main.rs b/src/main.rs index 2e4de9b..a58e922 100644 --- a/src/main.rs +++ b/src/main.rs @@ -633,12 +633,7 @@ async fn setup(docker: &Docker, options: CloudOptions, config: &HazeConfig) -> R for service in cloud.services() { for cmd in service.post_setup(docker, &cloud.id, config).await? { cloud - .exec( - docker, - shell_words::split(&cmd).into_diagnostic()?, - false, - Vec::::default(), - ) + .exec(docker, cmd, false, Vec::::default()) .await?; } } diff --git a/src/service.rs b/src/service.rs index 490f617..cb517cb 100644 --- a/src/service.rs +++ b/src/service.rs @@ -30,7 +30,7 @@ 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::sftp::{Sftp, SftpKey}; use crate::service::sharded::{Sharding, ShardingMigrate, ShardingMigrateUnset, SingleShard}; use crate::service::smb::Smb; use crate::service::webhook::Webhook; @@ -116,7 +116,7 @@ pub trait ServiceTrait { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(Vec::new()) } @@ -267,6 +267,8 @@ pub enum ServiceType { Dav, /// Sftp external storage Sftp, + /// Sftp external storage with public key authentication + SftpKey, /// ownCloud instance for migration Oc, /// Imaginary for preview generation @@ -318,6 +320,7 @@ pub enum Service { ShardingMigrate(ShardingMigrate), ShardingMigrateUnset(ShardingMigrateUnset), Sftp(Sftp), + SftpKey(SftpKey), Kaspersky(Kaspersky), KasperskyIcap(KasperskyIcap), Clam(Clam), @@ -361,6 +364,7 @@ impl Service { } ServiceType::Dav => Some(vec![Service::Dav(Dav)]), ServiceType::Sftp => Some(vec![Service::Sftp(Sftp)]), + ServiceType::SftpKey => Some(vec![Service::SftpKey(SftpKey)]), ServiceType::Oc => Some(vec![Service::Oc(Oc)]), ServiceType::Imaginary => Some(vec![Service::Imaginary(Imaginary)]), ServiceType::Kaspersky => Some(vec![Service::Kaspersky(Kaspersky)]), @@ -437,15 +441,29 @@ impl ServiceTrait for PresetService { _docker: &Docker, _cloud_id: &str, config: &HazeConfig, - ) -> Result> { + ) -> Result>> { 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")) + .map(|app| { + vec![ + "occ".into(), + "app:enable".into(), + app.clone(), + "--force".into(), + ] + }) .collect(); - commands.extend_from_slice(&preset.commands); + for cmnd in &preset.commands { + commands.push(shell_words::split(cmnd).into_diagnostic()?); + } + Ok(commands) } } + +fn split_cmnd(s: &str) -> Vec { + s.split(' ').map(String::from).collect() +} diff --git a/src/service/clam.rs b/src/service/clam.rs index 74b790d..7221309 100644 --- a/src/service/clam.rs +++ b/src/service/clam.rs @@ -2,7 +2,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::exec::exec; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig}; use bollard::query_parameters::CreateContainerOptions; @@ -85,14 +85,13 @@ impl ServiceTrait for ClamIcap { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ config:app:set files_antivirus av_mode --value=icap".into(), - "occ config:app:set files_antivirus av_host --value=clamav-icap".into(), - "occ config:app:set files_antivirus av_port --value=1344".into(), - "occ config:app:set files_antivirus av_icap_request_service --value=avscan".into(), - "occ config:app:set files_antivirus av_icap_response_header --value=X-Infection-Found" - .into(), + split_cmnd("occ config:app:set files_antivirus av_mode --value=icap"), + split_cmnd("occ config:app:set files_antivirus av_host --value=clamav-icap"), + split_cmnd("occ config:app:set files_antivirus av_port --value=1344"), + split_cmnd("occ config:app:set files_antivirus av_icap_request_service --value=avscan"), + split_cmnd("occ config:app:set files_antivirus av_icap_response_header --value=X-Infection-Found"), ]) } } @@ -171,7 +170,7 @@ impl ServiceTrait for ClamIcapTls { docker: &Docker, cloud_id: &str, config: &HazeConfig, - ) -> Result> { + ) -> Result>> { let mut cert = Vec::new(); exec( docker, @@ -191,14 +190,13 @@ impl ServiceTrait for ClamIcapTls { .wrap_err("Failed to write icap certificate")?; Ok(vec![ - "occ config:app:set files_antivirus av_mode --value=icap".into(), - "occ config:app:set files_antivirus av_icap_tls --value=1".into(), - "occ config:app:set files_antivirus av_host --value=clamav-icap-tls".into(), - "occ config:app:set files_antivirus av_port --value=1345".into(), - "occ config:app:set files_antivirus av_icap_request_service --value=avscan".into(), - "occ config:app:set files_antivirus av_icap_response_header --value=X-Infection-Found" - .into(), - "occ security:certificates:import data/icap-cert.pem".into(), + split_cmnd("occ config:app:set files_antivirus av_mode --value=icap"), + split_cmnd("occ config:app:set files_antivirus av_icap_tls --value=1"), + split_cmnd("occ config:app:set files_antivirus av_host --value=clamav-icap-tls"), + split_cmnd("occ config:app:set files_antivirus av_port --value=1345"), + split_cmnd("occ config:app:set files_antivirus av_icap_request_service --value=avscan"), + split_cmnd("occ config:app:set files_antivirus av_icap_response_header --value=X-Infection-Found"), + split_cmnd("occ security:certificates:import data/icap-cert.pem"), ]) } } @@ -221,10 +219,10 @@ impl ServiceTrait for Clam { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ config:app:set files_antivirus av_mode --value=executable".into(), - "occ config:app:set files_antivirus av_path --value=/bin/clamscan".into(), + split_cmnd("occ config:app:set files_antivirus av_mode --value=executable"), + split_cmnd("occ config:app:set files_antivirus av_path --value=/bin/clamscan"), ]) } } @@ -294,10 +292,12 @@ impl ServiceTrait for ClamSocket { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ config:app:set files_antivirus av_mode --value=socket".into(), - "occ config:app:set files_antivirus av_socket --value=tcp://clamav-socket:3310".into(), + split_cmnd("occ config:app:set files_antivirus av_mode --value=socket"), + split_cmnd( + "occ config:app:set files_antivirus av_socket --value=tcp://clamav-socket:3310", + ), ]) } } diff --git a/src/service/dav.rs b/src/service/dav.rs index d328925..e0b20f3 100644 --- a/src/service/dav.rs +++ b/src/service/dav.rs @@ -1,7 +1,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::config::ContainerCreateBody; use bollard::models::{EndpointSettings, HostConfig, NetworkingConfig}; @@ -76,12 +76,12 @@ impl ServiceTrait for Dav { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ files_external:create dav dav password::password".into(), - "occ files_external:config 1 host dav".into(), - "occ files_external:config 1 user test".into(), - "occ files_external:config 1 password test".into(), + split_cmnd("occ files_external:create dav dav password::password"), + split_cmnd("occ files_external:config 1 host dav"), + split_cmnd("occ files_external:config 1 user test"), + split_cmnd("occ files_external:config 1 password test"), ]) } } diff --git a/src/service/imaginary.rs b/src/service/imaginary.rs index 693c173..4ec4d96 100644 --- a/src/service/imaginary.rs +++ b/src/service/imaginary.rs @@ -1,7 +1,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::config::NetworkingConfig; use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig}; @@ -71,11 +71,14 @@ impl ServiceTrait for Imaginary { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ config:system:set enabledPreviewProviders 0 --value='OC\\Preview\\Imaginary'" - .into(), - "occ config:system:set preview_imaginary_url --value='http://imaginary:9000'".into(), + split_cmnd( + "occ config:system:set enabledPreviewProviders 0 --value='OC\\Preview\\Imaginary'", + ), + split_cmnd( + "occ config:system:set preview_imaginary_url --value='http://imaginary:9000'", + ), ]) } } diff --git a/src/service/kaspersky.rs b/src/service/kaspersky.rs index bc4f08a..1c0a67f 100644 --- a/src/service/kaspersky.rs +++ b/src/service/kaspersky.rs @@ -2,7 +2,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::exec::exec; use crate::image::{image_exists, pull_image}; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig}; use bollard::query_parameters::CreateContainerOptions; @@ -101,11 +101,11 @@ impl ServiceTrait for Kaspersky { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ config:app:set files_antivirus av_mode --value=kaspersky".into(), - "occ config:app:set files_antivirus av_host --value=kaspersky".into(), - "occ config:app:set files_antivirus av_port --value=80".into(), + split_cmnd("occ config:app:set files_antivirus av_mode --value=kaspersky"), + split_cmnd("occ config:app:set files_antivirus av_host --value=kaspersky"), + split_cmnd("occ config:app:set files_antivirus av_port --value=80"), ]) } } @@ -187,13 +187,15 @@ impl ServiceTrait for KasperskyIcap { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ config:app:set files_antivirus av_mode --value=icap".into(), - "occ config:app:set files_antivirus av_host --value=kaspersky-icap".into(), - "occ config:app:set files_antivirus av_port --value=1344".into(), - "occ config:app:set files_antivirus av_icap_request_service --value=req".into(), - "occ config:app:set files_antivirus av_icap_response_header --value=X-Virus-ID".into(), + split_cmnd("occ config:app:set files_antivirus av_mode --value=icap"), + split_cmnd("occ config:app:set files_antivirus av_host --value=kaspersky-icap"), + split_cmnd("occ config:app:set files_antivirus av_port --value=1344"), + split_cmnd("occ config:app:set files_antivirus av_icap_request_service --value=req"), + split_cmnd( + "occ config:app:set files_antivirus av_icap_response_header --value=X-Virus-ID", + ), ]) } } diff --git a/src/service/ldap.rs b/src/service/ldap.rs index bd41ba3..0f22643 100644 --- a/src/service/ldap.rs +++ b/src/service/ldap.rs @@ -1,7 +1,7 @@ use crate::cloud::CloudOptions; use crate::config::{HazeConfig, ProxyConfig}; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::config::NetworkingConfig; use bollard::models::{ContainerCreateBody, ContainerState, EndpointSettings, HostConfig}; @@ -92,30 +92,29 @@ impl ServiceTrait for Ldap { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ ldap:create-empty-config".into(), - "occ ldap:set-config s01 ldapHost 'ldap://ldap'".into(), - "occ ldap:set-config s01 ldapPort '389'".into(), - "occ ldap:set-config s01 ldapAgentName 'cn=admin,dc=example,dc=org'".into(), - "occ ldap:set-config s01 ldapAgentPassword 'haze'".into(), - "occ ldap:set-config s01 ldapBase 'dc=example,dc=org'".into(), - "occ ldap:set-config s01 ldapBaseUsers 'dc=example,dc=org'".into(), - "occ ldap:set-config s01 ldapBaseGroups 'dc=example,dc=org'".into(), - "occ ldap:set-config s01 ldapLoginFilter '(&(&(objectclass=inetOrgPerson))(uid=%uid))'" - .into(), - "occ ldap:set-config s01 ldapUserFilter '((objectclass=inetOrgPerson))'".into(), - "occ ldap:set-config s01 ldapUserFilterMode '0'".into(), - "occ ldap:set-config s01 ldapUserDisplayName 'sn'".into(), - "occ ldap:set-config s01 ldapUserFilterObjectclass 'inetOrgPerson'".into(), - "occ ldap:set-config s01 ldapGroupFilter '(&(|(objectclass=posixGroup)))'".into(), - "occ ldap:set-config s01 ldapGroupFilterObjectclass 'posixGroup'".into(), - "occ ldap:set-config s01 ldapEmailAttribute 'email'".into(), - "occ ldap:set-config s01 ldapUuidUserAttribute 'email'".into(), - "occ ldap:set-config s01 ldapUuidUserAttribute 'auto'".into(), - "occ ldap:set-config s01 ldapUuidGroupAttribute 'auto'".into(), - "occ ldap:set-config s01 ldapLoginFilterUsername '1'".into(), - "occ ldap:set-config s01 ldapConfigurationActive '1'".into(), + split_cmnd("occ ldap:create-empty-config"), + split_cmnd("occ ldap:set-config s01 ldapHost 'ldap://ldap'"), + split_cmnd("occ ldap:set-config s01 ldapPort '389'"), + split_cmnd("occ ldap:set-config s01 ldapAgentName 'cn=admin,dc=example,dc=org'"), + split_cmnd("occ ldap:set-config s01 ldapAgentPassword 'haze'"), + split_cmnd("occ ldap:set-config s01 ldapBase 'dc=example,dc=org'"), + split_cmnd("occ ldap:set-config s01 ldapBaseUsers 'dc=example,dc=org'"), + split_cmnd("occ ldap:set-config s01 ldapBaseGroups 'dc=example,dc=org'"), + split_cmnd("occ ldap:set-config s01 ldapLoginFilter '(&(&(objectclass=inetOrgPerson))(uid=%uid))'"), + split_cmnd("occ ldap:set-config s01 ldapUserFilter '((objectclass=inetOrgPerson))'"), + split_cmnd("occ ldap:set-config s01 ldapUserFilterMode '0'"), + split_cmnd("occ ldap:set-config s01 ldapUserDisplayName 'sn'"), + split_cmnd("occ ldap:set-config s01 ldapUserFilterObjectclass 'inetOrgPerson'"), + split_cmnd("occ ldap:set-config s01 ldapGroupFilter '(&(|(objectclass=posixGroup)))'"), + split_cmnd("occ ldap:set-config s01 ldapGroupFilterObjectclass 'posixGroup'"), + split_cmnd("occ ldap:set-config s01 ldapEmailAttribute 'email'"), + split_cmnd("occ ldap:set-config s01 ldapUuidUserAttribute 'email'"), + split_cmnd("occ ldap:set-config s01 ldapUuidUserAttribute 'auto'"), + split_cmnd("occ ldap:set-config s01 ldapUuidGroupAttribute 'auto'"), + split_cmnd("occ ldap:set-config s01 ldapLoginFilterUsername '1'"), + split_cmnd("occ ldap:set-config s01 ldapConfigurationActive '1'"), ]) } diff --git a/src/service/mail.rs b/src/service/mail.rs index c52fdac..ad40471 100644 --- a/src/service/mail.rs +++ b/src/service/mail.rs @@ -1,7 +1,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig}; use bollard::query_parameters::CreateContainerOptions; @@ -70,14 +70,14 @@ impl ServiceTrait for Mail { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ config:system:set mail_smtpmode --value smtp".into(), - "occ config:system:set mail_sendmailmode --value smtp".into(), - "occ config:system:set mail_domain --value haze".into(), - "occ config:system:set mail_smtphost --value mail".into(), - "occ config:system:set mail_smtpport --value 25".into(), - "occ user:setting admin settings email admin@haze".into(), + split_cmnd("occ config:system:set mail_smtpmode --value smtp"), + split_cmnd("occ config:system:set mail_sendmailmode --value smtp"), + split_cmnd("occ config:system:set mail_domain --value haze"), + split_cmnd("occ config:system:set mail_smtphost --value mail"), + split_cmnd("occ config:system:set mail_smtpport --value 25"), + split_cmnd("occ user:setting admin settings email admin@haze"), ]) } } diff --git a/src/service/objectstore.rs b/src/service/objectstore.rs index c0767e0..20e586c 100644 --- a/src/service/objectstore.rs +++ b/src/service/objectstore.rs @@ -2,7 +2,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::exec::exec; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::models::{ ContainerCreateBody, ContainerState, EndpointSettings, HostConfig, NetworkingConfig, @@ -247,18 +247,18 @@ impl ServiceTrait for ObjectStore { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { match self { ObjectStore::S3 => Ok(vec![ - "occ files_external:create s3 amazons3 amazons3::accesskey".into(), - "occ files_external:config 1 bucket ext".into(), - "occ files_external:config 1 hostname s3".into(), - "occ files_external:config 1 port 9000".into(), - "occ files_external:config 1 use_ssl false".into(), - "occ files_external:config 1 use_path_style true".into(), - "occ files_external:config 1 key minio".into(), - "occ files_external:config 1 secret minio123".into(), - "mc alias set s3 http://s3:9000 minio minio123".into(), + split_cmnd("occ files_external:create s3 amazons3 amazons3::accesskey"), + split_cmnd("occ files_external:config 1 bucket ext"), + split_cmnd("occ files_external:config 1 hostname s3"), + split_cmnd("occ files_external:config 1 port 9000"), + split_cmnd("occ files_external:config 1 use_ssl false"), + split_cmnd("occ files_external:config 1 use_path_style true"), + split_cmnd("occ files_external:config 1 key minio"), + split_cmnd("occ files_external:config 1 secret minio123"), + split_cmnd("mc alias set s3 http://s3:9000 minio minio123"), ]), // ObjectStore::S3s => Ok(vec![ // "occ files_external:create s3 amazons3 amazons3::accesskey".into(), diff --git a/src/service/oc.rs b/src/service/oc.rs index 2eb3a22..296b2a5 100644 --- a/src/service/oc.rs +++ b/src/service/oc.rs @@ -83,7 +83,7 @@ impl ServiceTrait for Oc { docker: &Docker, cloud_id: &str, config: &HazeConfig, - ) -> Result> { + ) -> Result>> { if let Some(ip) = self.get_ips(docker, cloud_id).await?.next() { let container = self.container_name(cloud_id).unwrap(); let addr = config.proxy.addr(&container, ip); diff --git a/src/service/office.rs b/src/service/office.rs index 8aaf99a..e7db10d 100644 --- a/src/service/office.rs +++ b/src/service/office.rs @@ -119,7 +119,7 @@ impl ServiceTrait for Office { docker: &Docker, cloud_id: &str, config: &HazeConfig, - ) -> Result> { + ) -> Result>> { let container = &self.container_name(cloud_id).unwrap(); let info = docker .inspect_container(container, None) @@ -152,8 +152,22 @@ impl ServiceTrait for Office { .addr_with_port(container, ip, self.proxy_port()); Ok(vec![ - format!(r#"occ config:app:set richdocuments public_wopi_url --value="{public}""#), - r#"occ richdocuments:setup --wopi-url "http://office:9980" --callback-url "http://cloud""#.into(), + vec![ + "occ".into(), + "config:app:set".into(), + "richdocuments".into(), + "public_wopi_url".into(), + "--value".into(), + public, + ], + vec![ + "occ".into(), + "richdocuments:setup".into(), + "--wopi-url".into(), + "http://office:9980".into(), + "--callback-url".into(), + "http://cloud".into(), + ], ]) } diff --git a/src/service/onlyoffice.rs b/src/service/onlyoffice.rs index b7c8a14..fa12c2c 100644 --- a/src/service/onlyoffice.rs +++ b/src/service/onlyoffice.rs @@ -2,7 +2,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::exec::exec; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::models::{ ContainerCreateBody, ContainerState, EndpointSettings, HostConfig, NetworkingConfig, @@ -82,7 +82,7 @@ impl ServiceTrait for OnlyOffice { docker: &Docker, cloud_id: &str, config: &HazeConfig, - ) -> Result> { + ) -> Result>> { let info = docker .inspect_container(&self.container_name(cloud_id).unwrap(), None) .await @@ -137,16 +137,44 @@ impl ServiceTrait for OnlyOffice { ); Ok(vec![ - format!("occ config:app:set onlyoffice DocumentServerUrl --value {addr}/"), - format!("occ config:app:set onlyoffice jwt_secret --value {secret}"), - "occ onlyoffice:documentserver --check".into(), + vec![ + "occ".into(), + "config:app:set".into(), + "onlyoffice".into(), + "DocumentServerUrl".into(), + "--value".into(), + addr, + ], + vec![ + "occ".into(), + "config:app:set".into(), + "onlyoffice".into(), + "jwt_secret".into(), + "--value".into(), + secret.into(), + ], + split_cmnd("occ onlyoffice:documentserver --check"), ]) } else { Ok(vec![ - format!("occ config:app:set onlyoffice DocumentServerUrl --value https://{ip}/"), - "occ config:app:set onlyoffice verify_peer_off --value true".into(), - format!("occ config:app:set onlyoffice jwt_secret --value {secret}"), - "occ onlyoffice:documentserver --check".into(), + vec![ + "occ".into(), + "config:app:set".into(), + "onlyoffice".into(), + "DocumentServerUrl".into(), + "--value".into(), + format!("https://{ip}/"), + ], + split_cmnd("occ config:app:set onlyoffice verify_peer_off --value true"), + vec![ + "occ".into(), + "config:app:set".into(), + "onlyoffice".into(), + "jwt_secret".into(), + "--value".into(), + secret.into(), + ], + split_cmnd("occ onlyoffice:documentserver --check"), ]) } } diff --git a/src/service/push.rs b/src/service/push.rs index 22b3ac2..b0bbab2 100644 --- a/src/service/push.rs +++ b/src/service/push.rs @@ -87,7 +87,7 @@ impl ServiceTrait for NotifyPush { docker: &Docker, cloud_id: &str, config: &HazeConfig, - ) -> Result> { + ) -> Result>> { let mut ips: Vec<_> = self.get_ips(docker, cloud_id).await?.collect(); if let Ok(local_interfaces) = list_afinet_netifas() { ips.extend(local_interfaces.into_iter().map(|(_, ip)| ip)); @@ -97,10 +97,14 @@ impl ServiceTrait for NotifyPush { .iter() .enumerate() .map(|(i, ip)| { - format!( - "occ config:system:set trusted_proxies {} --value {ip}", - i + 1 - ) + vec![ + "occ".into(), + "config:system:set".into(), + "trusted_proxies".into(), + (i + 1).to_string(), + "--value".into(), + ip.to_string(), + ] }) .collect(); @@ -108,7 +112,7 @@ impl ServiceTrait for NotifyPush { config .proxy .addr_with_port(&self.container_name(cloud_id).unwrap(), ips[0], 7867); - commands.push(format!("occ notify_push:setup {}", addr)); + commands.push(vec!["occ".into(), "notify_push:setup".into(), addr]); Ok(commands) } diff --git a/src/service/redis.rs b/src/service/redis.rs index 29a22ee..db69667 100644 --- a/src/service/redis.rs +++ b/src/service/redis.rs @@ -1,7 +1,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig}; use bollard::query_parameters::CreateContainerOptions; @@ -70,7 +70,9 @@ impl ServiceTrait for Redis { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { - Ok(vec!["occ config:system:set redis host --value redis".into()]) + ) -> Result>> { + Ok(vec![split_cmnd( + "occ config:system:set redis host --value redis", + )]) } } diff --git a/src/service/sftp.rs b/src/service/sftp.rs index 80405e2..c414b16 100644 --- a/src/service/sftp.rs +++ b/src/service/sftp.rs @@ -1,13 +1,14 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig}; use bollard::query_parameters::CreateContainerOptions; use bollard::Docker; use maplit::hashmap; -use miette::IntoDiagnostic; +use miette::{Context, IntoDiagnostic}; +use std::fs::{create_dir_all, write}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct Sftp; @@ -75,13 +76,119 @@ impl ServiceTrait for Sftp { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ files_external:create sftp sftp password::password".into(), - "occ files_external:config 1 host sftp".into(), - "occ files_external:config 1 user test".into(), - "occ files_external:config 1 root data".into(), - "occ files_external:config 1 password test".into(), + split_cmnd("occ files_external:create sftp sftp password::password"), + split_cmnd("occ files_external:config 1 host sftp"), + split_cmnd("occ files_external:config 1 user test"), + split_cmnd("occ files_external:config 1 root data"), + split_cmnd("occ files_external:config 1 password test"), + ]) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct SftpKey; + +#[async_trait::async_trait] +impl ServiceTrait for SftpKey { + fn name(&self) -> &str { + "sftp-key" + } + + async fn spawn( + &self, + docker: &Docker, + cloud_id: &str, + network: &str, + config: &HazeConfig, + _options: &CloudOptions, + ) -> Result> { + let image = "atmoz/sftp:alpine"; + pull_image(docker, image).await?; + let options = Some(CreateContainerOptions { + name: self.container_name(cloud_id), + ..CreateContainerOptions::default() + }); + let key_dir = config.work_dir.join("certificates/sftp"); + create_dir_all(&key_dir) + .into_diagnostic() + .wrap_err("Failed to create sftp certificate directory")?; + let private_path = key_dir.join("id_rsa"); + let public_path = key_dir.join("id_rsa.pub"); + let private_key = include_str!("../../certificates/sftp/id_rsa"); + let public_key = include_str!("../../certificates/sftp/id_rsa.pub"); + if !private_path.exists() { + write(&private_path, private_key) + .into_diagnostic() + .wrap_err("Failed to write sftp client certificate")?; + } + if !public_path.exists() { + write(&public_path, public_key) + .into_diagnostic() + .wrap_err("Failed to write sftp client key")?; + } + + let volumes = vec![format!("{public_path}:/home/test/.ssh/keys/id_rsa:ro")]; + + let config = ContainerCreateBody { + image: Some(image.into()), + host_config: Some(HostConfig { + network_mode: Some(network.to_string()), + binds: Some(volumes), + ..Default::default() + }), + 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() + } + }), + }), + cmd: Some(vec!["test::::data".into()]), + ..Default::default() + }; + let id = docker + .create_container(options, config) + .await + .into_diagnostic()? + .id; + docker.start_container(&id, None).await.into_diagnostic()?; + Ok(vec![id]) + } + + fn container_name(&self, cloud_id: &str) -> Option { + Some(format!("{}-sftp-key", cloud_id)) + } + + fn apps(&self) -> &'static [&'static str] { + &["files_external"] + } + + async fn post_setup( + &self, + _docker: &Docker, + _cloud_id: &str, + _config: &HazeConfig, + ) -> Result>> { + Ok(vec![ + split_cmnd("occ files_external:create sftp sftp publickey::rsa_private"), + split_cmnd("occ files_external:config 1 host sftp-key"), + split_cmnd("occ files_external:config 1 user test"), + split_cmnd("occ files_external:config 1 root data"), + vec![ + "occ".into(), + "files_external:config".into(), + "--value-from-file".into(), + "1".into(), + "private_key".into(), + "/certificates/sftp/id_rsa".into(), + ], ]) } } diff --git a/src/service/smb.rs b/src/service/smb.rs index a40b105..da8f1f6 100644 --- a/src/service/smb.rs +++ b/src/service/smb.rs @@ -1,7 +1,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig}; use bollard::query_parameters::CreateContainerOptions; @@ -79,13 +79,13 @@ impl ServiceTrait for Smb { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ files_external:create smb smb password::password".into(), - "occ files_external:config 1 host smb".into(), - "occ files_external:config 1 user test".into(), - "occ files_external:config 1 password test".into(), - "occ files_external:config 1 share test".into(), + split_cmnd("occ files_external:create smb smb password::password"), + split_cmnd("occ files_external:config 1 host smb"), + split_cmnd("occ files_external:config 1 user test"), + split_cmnd("occ files_external:config 1 password test"), + split_cmnd("occ files_external:config 1 share test"), ]) } } From 92fbc74a5b7bb5d4f309b22cdbe690705ffadb6a Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 28 May 2026 18:40:20 +0200 Subject: [PATCH 57/62] fix ldap --- src/service/ldap.rs | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/service/ldap.rs b/src/service/ldap.rs index 0f22643..4f317e6 100644 --- a/src/service/ldap.rs +++ b/src/service/ldap.rs @@ -95,26 +95,26 @@ impl ServiceTrait for Ldap { ) -> Result>> { Ok(vec![ split_cmnd("occ ldap:create-empty-config"), - split_cmnd("occ ldap:set-config s01 ldapHost 'ldap://ldap'"), - split_cmnd("occ ldap:set-config s01 ldapPort '389'"), - split_cmnd("occ ldap:set-config s01 ldapAgentName 'cn=admin,dc=example,dc=org'"), - split_cmnd("occ ldap:set-config s01 ldapAgentPassword 'haze'"), - split_cmnd("occ ldap:set-config s01 ldapBase 'dc=example,dc=org'"), - split_cmnd("occ ldap:set-config s01 ldapBaseUsers 'dc=example,dc=org'"), - split_cmnd("occ ldap:set-config s01 ldapBaseGroups 'dc=example,dc=org'"), - split_cmnd("occ ldap:set-config s01 ldapLoginFilter '(&(&(objectclass=inetOrgPerson))(uid=%uid))'"), - split_cmnd("occ ldap:set-config s01 ldapUserFilter '((objectclass=inetOrgPerson))'"), - split_cmnd("occ ldap:set-config s01 ldapUserFilterMode '0'"), - split_cmnd("occ ldap:set-config s01 ldapUserDisplayName 'sn'"), - split_cmnd("occ ldap:set-config s01 ldapUserFilterObjectclass 'inetOrgPerson'"), - split_cmnd("occ ldap:set-config s01 ldapGroupFilter '(&(|(objectclass=posixGroup)))'"), - split_cmnd("occ ldap:set-config s01 ldapGroupFilterObjectclass 'posixGroup'"), - split_cmnd("occ ldap:set-config s01 ldapEmailAttribute 'email'"), - split_cmnd("occ ldap:set-config s01 ldapUuidUserAttribute 'email'"), - split_cmnd("occ ldap:set-config s01 ldapUuidUserAttribute 'auto'"), - split_cmnd("occ ldap:set-config s01 ldapUuidGroupAttribute 'auto'"), - split_cmnd("occ ldap:set-config s01 ldapLoginFilterUsername '1'"), - split_cmnd("occ ldap:set-config s01 ldapConfigurationActive '1'"), + split_cmnd("occ ldap:set-config s01 ldapHost ldap://ldap"), + split_cmnd("occ ldap:set-config s01 ldapPort 389"), + split_cmnd("occ ldap:set-config s01 ldapAgentName cn=admin,dc=example,dc=org"), + split_cmnd("occ ldap:set-config s01 ldapAgentPassword haze"), + split_cmnd("occ ldap:set-config s01 ldapBase dc=example,dc=org"), + split_cmnd("occ ldap:set-config s01 ldapBaseUsers dc=example,dc=org"), + split_cmnd("occ ldap:set-config s01 ldapBaseGroups dc=example,dc=org"), + split_cmnd("occ ldap:set-config s01 ldapLoginFilter (&(&(objectclass=inetOrgPerson))(uid=%uid))"), + split_cmnd("occ ldap:set-config s01 ldapUserFilter ((objectclass=inetOrgPerson))"), + split_cmnd("occ ldap:set-config s01 ldapUserFilterMode 0"), + split_cmnd("occ ldap:set-config s01 ldapUserDisplayName sn"), + split_cmnd("occ ldap:set-config s01 ldapUserFilterObjectclass inetOrgPerson"), + split_cmnd("occ ldap:set-config s01 ldapGroupFilter (&(|(objectclass=posixGroup)))"), + split_cmnd("occ ldap:set-config s01 ldapGroupFilterObjectclass posixGroup"), + split_cmnd("occ ldap:set-config s01 ldapEmailAttribute email"), + split_cmnd("occ ldap:set-config s01 ldapUuidUserAttribute email"), + split_cmnd("occ ldap:set-config s01 ldapUuidUserAttribute auto"), + split_cmnd("occ ldap:set-config s01 ldapUuidGroupAttribute auto"), + split_cmnd("occ ldap:set-config s01 ldapLoginFilterUsername 1"), + split_cmnd("occ ldap:set-config s01 ldapConfigurationActive 1"), ]) } From d852f9db4fe4d7d7ddbe94a93b07f2771cd69e8a Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 28 May 2026 18:40:20 +0200 Subject: [PATCH 58/62] remove php-smbclient for now as it's broken --- nix/image/php-ext.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/image/php-ext.nix b/nix/image/php-ext.nix index 69d23e1..0a7e53e 100644 --- a/nix/image/php-ext.nix +++ b/nix/image/php-ext.nix @@ -38,7 +38,7 @@ in imagick ] ++ optionals (!debug) [ - smbclient # this breaks the build for no apparent reason + # smbclient # this breaks the build for no apparent reason ] ++ optionals withBlackfire [ blackfire From a3f2355deae7ebd87004fcb4a82588e1b35d7850 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 28 May 2026 18:40:20 +0200 Subject: [PATCH 59/62] create smb/sftp accounts for ldaptest --- src/service/sftp.rs | 5 ++++- src/service/smb.rs | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/service/sftp.rs b/src/service/sftp.rs index c414b16..1f47a40 100644 --- a/src/service/sftp.rs +++ b/src/service/sftp.rs @@ -51,7 +51,10 @@ impl ServiceTrait for Sftp { } }), }), - cmd: Some(vec!["test:test:::data".into()]), + cmd: Some(vec![ + "test:test:::data".into(), + "ldaptest:test:::data".into(), + ]), ..Default::default() }; let id = docker diff --git a/src/service/smb.rs b/src/service/smb.rs index da8f1f6..be1143b 100644 --- a/src/service/smb.rs +++ b/src/service/smb.rs @@ -40,8 +40,10 @@ impl ServiceTrait for Smb { }), env: Some(vec![ "ACCOUNT_test=test".into(), + "ACCOUNT_ldaptest=test".into(), "UID_test=1000".into(), "SAMBA_VOLUME_CONFIG_test=[test]; path=/tmp; valid users = test; guest ok = no; read only = no; browseable = yes".into(), + "SAMBA_VOLUME_CONFIG_ldaptest=[ldaptest]; path=/tmp; valid users = ldaptest; guest ok = no; read only = no; browseable = yes".into(), ]), labels: Some(hashmap! { "haze-type".into() => self.name().into(), From fc6cde41613fa9d6f45501d74ac29069c8ad676c Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 3 Jun 2026 15:00:53 +0200 Subject: [PATCH 60/62] fix mysql 8, again fixes #25 --- src/database.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/database.rs b/src/database.rs index ff969a3..0bc31d7 100644 --- a/src/database.rs +++ b/src/database.rs @@ -219,14 +219,6 @@ impl Database { } }), }), - cmd: if self.image() == "mysql:8" { - Some(vec![ - "--default-authentication-plugin".into(), - "mysql_native_password".into(), - ]) - } else { - None - }, ..Default::default() }; let id = docker From d138740bbd7e1ba101b472d4c0e05b36b037430b Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 3 Jun 2026 15:28:25 +0200 Subject: [PATCH 61/62] allow exec'ing service containers --- README.md | 5 +++- src/args.rs | 9 +++++++ src/main.rs | 55 ++++++++++++++++++++++++++++++------------ src/service.rs | 9 +++++++ src/service/redis.rs | 4 +++ src/service/smb.rs | 4 +++ src/service/webhook.rs | 4 +++ 7 files changed, 74 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 026c190..aa538d2 100644 --- a/README.md +++ b/README.md @@ -151,11 +151,14 @@ haze [match] db #### Execute a command on an instance ```bash -haze [match] exec [cmd] +haze [match] [service] [cmd] ``` If no `cmd` is specified it will launch `bash` +If a service name or `db` is provided, the command will be in the container of +the service or database. + #### Create a new instance and run a command ```bash diff --git a/src/args.rs b/src/args.rs index 9b7a2cc..59bca87 100644 --- a/src/args.rs +++ b/src/args.rs @@ -165,6 +165,7 @@ impl LogService { #[derive(Debug, Clone, Eq, PartialEq)] pub enum ExecService { Db, + Service(Service), } impl HazeArgs { @@ -232,6 +233,14 @@ impl HazeArgs { args.next(); Some(ExecService::Db) } + Some(arg) => Service::from_type(&[], arg.as_ref()) + .into_iter() + .filter_map(|services| services.into_iter().next()) + .next() + .map(|service| { + args.next(); + ExecService::Service(service) + }), _ => None, }; diff --git a/src/main.rs b/src/main.rs index a58e922..d999b7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -148,6 +148,9 @@ async fn main() -> Result { root, } => { let cloud = Cloud::get_by_filter(&docker, filter, &config).await?; + let env = get_forward_env(); + let tty = atty::is(atty::Stream::Stdout); + match service { None => { let command = if command.is_empty() { @@ -155,8 +158,7 @@ async fn main() -> Result { } else { command }; - let env = get_forward_env(); - let tty = atty::is(atty::Stream::Stdout); + let user = if root { "root" } else { "haze" }; if tty { exec_tty(&docker, &cloud.id, user, command, env).await?; @@ -165,19 +167,42 @@ async fn main() -> Result { } } Some(ExecService::Db) => { - cloud - .db() - .exec_sh( - &docker, - &cloud.id, - if command.is_empty() { - vec!["bash".to_string()] - } else { - command - }, - atty::is(atty::Stream::Stdout), - ) - .await?; + let command = if command.is_empty() { + vec!["bash".to_string()] + } else { + command + }; + + cloud.db().exec_sh(&docker, &cloud.id, command, tty).await?; + } + Some(ExecService::Service(service)) => { + let Some(container) = service.container_name(&cloud.id) else { + eprintln!( + "Service {} can't be exec'ed as it has no associated container", + service.name() + ); + return Ok(ExitCode::FAILURE); + }; + if service.exec_shell() == "" && command.is_empty() { + eprintln!( + "Service {} can't be exec'ed as it has no shell", + service.name() + ); + return Ok(ExitCode::FAILURE); + } + + let command = if command.is_empty() { + vec![service.exec_shell().to_string()] + } else { + command + }; + + let user = if root { "root" } else { service.exec_user() }; + if tty { + exec_tty(&docker, &container, user, command, env).await?; + } else { + exec(&docker, &container, user, command, env, Some(stdout())).await?; + } } } } diff --git a/src/service.rs b/src/service.rs index cb517cb..c514449 100644 --- a/src/service.rs +++ b/src/service.rs @@ -194,6 +194,14 @@ pub trait ServiceTrait { fn proxy_port(&self) -> u16 { 80 } + + fn exec_user(&self) -> &'static str { + "root" + } + + fn exec_shell(&self) -> &'static str { + "bash" + } } #[derive(Clone, Eq, PartialEq, Debug)] @@ -242,6 +250,7 @@ pub enum ServiceType { /// Ldap admin interface LdapAdmin, /// OnlyOffice + #[strum(serialize = "onlyoffice", serialize = "only-office")] OnlyOffice, /// Libre office online Office, diff --git a/src/service/redis.rs b/src/service/redis.rs index db69667..6d5d5e6 100644 --- a/src/service/redis.rs +++ b/src/service/redis.rs @@ -75,4 +75,8 @@ impl ServiceTrait for Redis { "occ config:system:set redis host --value redis", )]) } + + fn exec_shell(&self) -> &'static str { + "sh" + } } diff --git a/src/service/smb.rs b/src/service/smb.rs index be1143b..08f69bd 100644 --- a/src/service/smb.rs +++ b/src/service/smb.rs @@ -90,4 +90,8 @@ impl ServiceTrait for Smb { split_cmnd("occ files_external:config 1 share test"), ]) } + + fn exec_shell(&self) -> &'static str { + "sh" + } } diff --git a/src/service/webhook.rs b/src/service/webhook.rs index 3967483..e7ebae0 100644 --- a/src/service/webhook.rs +++ b/src/service/webhook.rs @@ -68,4 +68,8 @@ impl ServiceTrait for Webhook { fn proxy_port(&self) -> u16 { 8080 } + + fn exec_shell(&self) -> &'static str { + "" + } } From a38de3fdee3c3be4d07fbb200abf458857ebcb13 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 3 Jun 2026 15:44:33 +0200 Subject: [PATCH 62/62] 2.3.0 + updates --- Cargo.lock | 105 ++++++++++++++++++++++++++++++----------------------- Cargo.toml | 8 ++-- 2 files changed, 64 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ccedb17..821ff04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,7 +25,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -266,9 +266,9 @@ dependencies = [ [[package]] name = "bollard" -version = "0.20.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227aa051deec8d16bd9c34605e7aaf153f240e35483dd42f6f78903847934738" +checksum = "c9d0a013e3d3ee4edd61e779adf117944c08902d375f18630a0c5b8f95659734" dependencies = [ "base64", "bollard-stubs", @@ -298,9 +298,9 @@ dependencies = [ [[package]] name = "bollard-stubs" -version = "1.52.1-rc.29.1.3" +version = "1.53.1-rc.29.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0a8ca8799131c1837d1282c3f81f31e76ceb0ce426e04a7fe1ccee3287c066" +checksum = "ce412eb6f7096743011dc3cb5c674caeb24ced61d8c498fe07cf7998a4fea889" dependencies = [ "serde", "serde_json", @@ -384,6 +384,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "cipher" version = "0.4.4" @@ -509,6 +520,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -897,6 +917,7 @@ dependencies = [ "js-sys", "libc", "r-efi", + "rand_core 0.10.1", "wasip2", "wasip3", "wasm-bindgen", @@ -922,15 +943,14 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "git2" -version = "0.20.4" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" +checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e" dependencies = [ "bitflags", "libc", "libgit2-sys", "log", - "url", ] [[package]] @@ -950,7 +970,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "haze" -version = "2.2.2" +version = "2.3.0" dependencies = [ "async-trait", "atty", @@ -1491,9 +1511,9 @@ checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libgit2-sys" -version = "0.18.3+1.9.2" +version = "0.18.5+1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" +checksum = "005d6ae6eac1912906073e069f7db60b1fa98e052a68227824afe3e3a1c59ca2" dependencies = [ "cc", "libc", @@ -1794,16 +1814,24 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petname" -version = "2.0.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd31dcfdbbd7431a807ef4df6edd6473228e94d5c805e8cf671227a21bad068" +checksum = "2ce610bff48dd7b6a127e45631795fbb0b302b99a39bef7e6da3d297e8eb2b6b" dependencies = [ - "anyhow", "clap", - "itertools", + "petname-macros", + "rand 0.10.1", +] + +[[package]] +name = "petname-macros" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "324239bd00dcf61f1a0e301d4d8f6f8c080a755248fc3fdc817ee1fdbbc27b8b" +dependencies = [ "proc-macro2", "quote", - "rand 0.8.5", + "syn", ] [[package]] @@ -1963,35 +1991,25 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha 0.9.0", + "rand_chacha", "rand_core 0.9.5", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "rand" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "chacha20", + "getrandom 0.4.1", + "rand_core 0.10.1", ] [[package]] @@ -2004,15 +2022,6 @@ dependencies = [ "rand_core 0.9.5", ] -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.15", -] - [[package]] name = "rand_core" version = "0.9.5" @@ -2022,6 +2031,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rayon" version = "1.12.0" @@ -2374,7 +2389,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -2385,7 +2400,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -2396,7 +2411,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.11.1", ] diff --git a/Cargo.toml b/Cargo.toml index 6da76a8..631f10f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "haze" -version = "2.2.2" +version = "2.3.0" authors = ["Robin Appelman "] edition = "2021" repository = "https://codeberg.org/icewind/haze" @@ -8,7 +8,7 @@ license = "MIT" description = "Easy setup and management of Nextcloud test instances using docker" [dependencies] -bollard = "0.20.1" +bollard = "0.21.0" maplit = "1.0.2" camino = { version = "1.2.2", features = ["serde1"] } tokio = { version = "1.49.0", features = ["fs", "macros", "signal", "rt-multi-thread"] } @@ -20,7 +20,7 @@ toml = "1.0.3" directories-next = "2.0.0" serde = "1.0.228" serde_json = "1.0.149" -petname = "2.0.2" +petname = "3.0.0" reqwest = { version = "0.13.2", default-features = false, features = ["rustls"] } tar = "0.4.44" flate2 = "1.1.9" @@ -31,7 +31,7 @@ shell-words = "1.1.1" tracing = "0.1.44" tracing-subscriber = "0.3.22" atty = "0.2.14" -git2 = { version = "0.20.4", default-features = false } +git2 = { version = "0.21.0", default-features = false } itertools = { version = "0.14.0", features = ["use_alloc"] } local-ip-address = "0.6.10" strum = { version = "0.28.0", features = ["derive"] }