From 85ffdcea5a0e9da66170d2c07a56adc94bdb2576 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 10 Mar 2026 15:05:13 +0100 Subject: [PATCH 01/36] 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 02/36] 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 03/36] 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 04/36] 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 05/36] 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 06/36] 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 07/36] 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 08/36] 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 09/36] 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 10/36] 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 11/36] 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 12/36] 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 13/36] 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 14/36] 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 15/36] 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 16/36] 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 17/36] 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 18/36] 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 19/36] 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 20/36] 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 21/36] 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 22/36] 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 23/36] 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 24/36] 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 25/36] 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 26/36] 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 27/36] 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 28/36] 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 29/36] 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 30/36] 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 31/36] 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 32/36] 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 33/36] 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 34/36] 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 35/36] 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 36/36] 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(),