1
0
Fork 0
mirror of https://codeberg.org/icewind/haze.git synced 2026-06-03 17:14:08 +02:00

Compare commits

...

71 commits

Author SHA1 Message Date
a38de3fdee 2.3.0 + updates 2026-06-03 15:44:38 +02:00
d138740bbd allow exec'ing service containers 2026-06-03 15:32:37 +02:00
fc6cde4161 fix mysql 8, again
fixes #25
2026-06-03 15:01:19 +02:00
a3f2355dea create smb/sftp accounts for ldaptest 2026-05-28 18:48:40 +02:00
d852f9db4f remove php-smbclient for now as it's broken 2026-05-28 18:48:19 +02:00
92fbc74a5b fix ldap 2026-05-28 18:40:21 +02:00
204fb676d6 add sftp with key authentication service 2026-05-26 20:49:19 +02:00
f99238121b nix cleanup 2026-05-09 16:45:55 +02:00
cd9740675f clippy fixes 2026-05-08 22:23:40 +02:00
39ba7a2a53 mention webhook tester in readme 2026-05-08 22:22:42 +02:00
ad999702aa improve websocket proxying 2026-05-08 20:41:19 +02:00
b977cd9dfa parallelize git pull
fixes #21
2026-05-08 19:09:43 +02:00
8771e7dc5f switch to multiprogress for git pull 2026-05-08 18:57:19 +02:00
373ce0f3fd add php-imagick
fixes #22
2026-05-07 18:59:05 +02:00
9e080f0d54 fix using '--' flags in test and integration commands 2026-05-07 18:40:30 +02:00
512b669a7c sqlite table mode 2026-04-30 19:08:13 +02:00
ea3f89bb04 fpm logs 2026-04-28 18:55:12 +02:00
b4a77997ab fpm status page 2026-04-28 18:54:48 +02:00
87f6907778 autosetup for ldap
fixes #19
2026-04-17 22:22:33 +02:00
9de626a905 typo 2026-04-16 16:30:09 +02:00
53e30a94aa fix generated urls in integration tests 2026-04-15 23:28:46 +02:00
948d01600e readme typo 2026-04-14 14:52:46 +02:00
19e60217ea 127.0.0.1 now works for federation 2026-04-14 14:05:02 +02:00
96b7dd671c clippy fixes 2026-04-13 23:42:07 +02:00
24b8fd26ca rebuild images on version number change 2026-04-13 23:29:04 +02:00
814a1c3121 2.2.1 2026-04-13 23:23:10 +02:00
a1ed0571be exclude frankenphp from older php versions 2026-04-13 20:59:29 +02:00
0a16737398 migrate scripts to nushell 2026-04-13 20:53:43 +02:00
266b70339b cleanup 2026-04-10 17:40:44 +02:00
3abf183ae3 app integration support 2026-04-10 17:37:30 +02:00
a80354c922 fix office 2026-03-25 17:31:27 +01:00
3b4014b5e4 basic frankenphp support
fixes #17
2026-03-22 15:03:50 +01:00
4ab23610a2 fix redis-tls not starting 2026-03-22 14:10:27 +01:00
0d98667650 add dns for proxy inside container 2026-03-20 21:22:37 +01:00
ce34f302a1 clippy fixes 2026-03-20 16:10:42 +01:00
63e17d609f record haze version when building images and warn on out of date images 2026-03-20 15:14:45 +01:00
6fdadd9bad use supervisord in image 2026-03-12 15:43:32 +01:00
0105c60a09 workflow cleanup 2026-03-10 15:06:39 +01:00
85ffdcea5a fix workflow typo 2026-03-10 15:05:19 +01:00
4635ecf3fc 2.2.0 2026-03-10 14:50:28 +01:00
87de2dbb21 fix tests 2026-03-10 14:09:14 +01:00
0096414614 -dbg images are no longer a thing 2026-03-09 23:18:18 +01:00
ef86840e77 dont fail cleaning stopped containers 2026-03-09 23:18:18 +01:00
da6c6d754b add 8.5 image 2026-03-09 23:18:18 +01:00
8780fe0754 build new 8.0 and 8.1 images 2026-03-09 22:48:36 +01:00
7e54fbd89f add s3 TLS option 2026-03-09 20:10:29 +01:00
b3a1e80f6f allow custom pre-setup config options 2026-03-09 18:48:25 +01:00
80d71bd7a0 move redis certificates 2026-03-09 18:30:37 +01:00
88a4100340 show proxy addr for ldap-admin in startup message 2026-03-09 18:08:49 +01:00
e76678ec14 document service proxy 2026-03-09 17:57:16 +01:00
04b8ec975d fix ldap admin 2026-03-09 17:56:20 +01:00
85071d7aa1 allow service names with - in them in proxy 2026-03-09 17:49:15 +01:00
fdc821cb93 allow bare-service proxy hosts 2026-03-09 17:41:42 +01:00
8e79e997a8 make proxy work for object store 2026-03-09 17:16:36 +01:00
bf7a8d9a34 dont polute store_app 2026-03-06 00:48:31 +01:00
e9cb4f08e3 use extra app directories for git operations 2026-03-06 00:16:02 +01:00
1a6dd90410 add appDirectories to hm module 2026-03-06 00:15:38 +01:00
9629dea8df enable appstore 2026-03-05 23:37:42 +01:00
b7ea4e9760 allow configuring additional app directories and add a writable app directory
fixes #15
2026-03-05 23:32:02 +01:00
provokateurin
862d33b017 feat(cloud): Kill containers before removing them for faster destruction
Signed-off-by: provokateurin <kate@provokateurin.de>
2026-03-05 22:37:38 +01:00
provokateurin
d89c547f2a
fix(install): Fix OCI
Signed-off-by: provokateurin <kate@provokateurin.de>
2026-03-05 15:21:47 +01:00
903b3d25a8 localhost proxy doesn't support federation 2026-03-02 15:43:38 +01:00
37248955ab 2.1.6 2026-02-27 23:03:24 +01:00
2ea01b6570 cargo update 2026-02-27 22:56:11 +01:00
df38f16f10 update bollard 2026-02-27 22:52:44 +01:00
f569ca17e2 allow using release sources 2026-02-27 22:04:13 +01:00
8941c697fb fix integration not using all passed options 2026-02-27 17:07:49 +01:00
cead37fae7 fix custom php.ini, again 2026-02-25 19:30:15 +01:00
4f6b1fbd9b readme tweaks and example user service 2026-02-24 20:42:36 +01:00
e3161f34ae 127.0.0.1 now works for proxy 2026-02-24 20:33:53 +01:00
f7b7c1bf37 add common php option to example 2026-02-17 00:07:51 +01:00
84 changed files with 3944 additions and 1496 deletions

View file

@ -4,6 +4,7 @@ on:
push:
branches: ["main"]
paths:
- "Cargo.toml"
- ".forgejo/workflows/docker.yaml"
- "nix/image/**"
@ -16,10 +17,9 @@ jobs:
strategy:
matrix:
php-version: ["8.2", "8.3", "8.4"]
variant: [""]
php-version: ["8.0", "8.1", "8.2", "8.3", "8.4", "8.5"]
name: haze-${{ matrix.php-version }}${{ matrix.variant }}
name: haze-${{ matrix.php-version }}
steps:
- name: Checkout repository

2044
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "haze"
version = "2.1.5"
version = "2.3.0"
authors = ["Robin Appelman <robin@icewind.nl>"]
edition = "2021"
repository = "https://codeberg.org/icewind/haze"
@ -8,38 +8,44 @@ license = "MIT"
description = "Easy setup and management of Nextcloud test instances using docker"
[dependencies]
bollard = "0.18.1"
bollard = "0.21.0"
maplit = "1.0.2"
camino = { version = "1.1.7", features = ["serde1"] }
tokio = { version = "1.38.0", features = ["fs", "macros", "signal", "rt-multi-thread"] }
futures-util = "0.3.30"
termion = "4.0.1"
opener = "0.7.1"
toml = "0.8.14"
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"
toml = "1.0.3"
directories-next = "2.0.0"
serde = "1.0.203"
serde_json = "1.0.117"
petname = "2.0.2"
reqwest = { version = "0.12.4", default-features = false }
tar = "0.4.41"
flate2 = "1.0.30"
async-trait = "0.1.80"
serde = "1.0.228"
serde_json = "1.0.149"
petname = "3.0.0"
reqwest = { version = "0.13.2", default-features = false, features = ["rustls"] }
tar = "0.4.44"
flate2 = "1.1.9"
async-trait = "0.1.89"
enum_dispatch = "0.3.13"
miette = { version = "7.2.0", features = ["fancy"] }
shell-words = "1.1.0"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
miette = { version = "7.6.0", features = ["fancy"] }
shell-words = "1.1.1"
tracing = "0.1.44"
tracing-subscriber = "0.3.22"
atty = "0.2.14"
git2 = { version = "0.20.0", default-features = false }
git2 = { version = "0.21.0", default-features = false }
itertools = { version = "0.14.0", features = ["use_alloc"] }
local-ip-address = "0.6.5"
strum = { version = "0.27.2", features = ["derive"] }
owo-colors = { version = "4.2.2", features = ["supports-colors"] }
local-ip-address = "0.6.10"
strum = { version = "0.28.0", features = ["derive"] }
owo-colors = { version = "4.3.0", features = ["supports-colors"] }
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.6.0"
hyper-util = "0.1.10"
axum = { version = "0.8.1", features = ["tokio"] }
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", "macros"] }
[profile.release]
lto = true

View file

@ -6,7 +6,7 @@ Easy setup and management of Nextcloud test instances using docker
## What
`haze` provides an easy way to setup Nextcloud test instances with a choice of
`haze` provides an easy way to set up Nextcloud test instances with a choice of
php version, database server, optional s3 or ldap setup and more.
## Setup
@ -56,47 +56,52 @@ See the [configuration section](#configuration) for more options.
#### Start an instance
```bash
haze start [database] [php-version] [services]
haze start [database] [php-version] [services] [vX.Y.Z]
```
Where `database` is one of `sqlite`, `mysql`, `mariadb`, `pgsql` or `oracle`
with an optional version (e.g. `pgsql:12`), defaults to `sqlite`. And
`php-version` is one of `8.2`, `8.3` or `8.4`, defaults to the maximum version
support by the current Nextcloud version. `7.3` till `8.1` are still supported
but the docker images for those versions aren't being updated anymore so they
might be missing some newer features.
`php-version` is one of `8.0`, `8.1`, `8.2`, `8.3`, `8.4` or `8.5`, defaults to
the maximum version support by the current Nextcloud version.
Each php version also comes with a `-dbg` variant that has php compiled in debug
mode and can be used for debugging php itself with gdb.
You can specify a version number (e.g. `v32.0.2`) to use the sources from a
release instead of using the local sources.
Additionally, you can use the following options when starting an instance:
- `s3`: setup an S3 server and configure to Nextcloud to use it as primary
- `s3`: set up an S3 server and configure to Nextcloud to use it as primary
storage.
- `<path to app.tar.gz>`: by specifying the path to an app package this package
will be extracted into the apps. directory of the new instance (overwriting
any existing app code). This can be used to quickly test a packaged app.
- `ldap`: setup an LDAP server.
- `office`: setup a Nextcloud Office server.
- `s3s`: enable TLS for the S3 setup.
- `s3mb`: enable multi-bucket S3 setup.
- `s3m`: enable multi-instance S3 setup.
- `ldap`: set up an LDAP server.
- `office`: set up a Nextcloud Office server.
- `onlyoffice` setup an onlyoffice document server.
- `push` setup [client push](https://github.com/nextcloud/notify_push).
- `smb`: setup a samba server for external storage use.
- `dav`: setup a WebDAV server for external storage use.
- `sftp`: setup a SFTP server for external storage use.
- `kaspersky`: setup a kaspersky scan engine server in http mode. ( Requires
- `push` set up [client push](https://github.com/nextcloud/notify_push).
- `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.
- `clamav`: setup a local clam av scanner in executable mode.
- `clamav-socket`: setup a clam av scanner in socket mode.
- `clamav-icap`: setup a clam av scanner in ICAP mode.
- `clamav-icap-tls`: setup a clam av scanner in ICAP mode with TLS encryption.
- `clamav`: set up a local clam av scanner in executable mode.
- `clamav-socket`: set up a clam av scanner in socket mode.
- `clamav-icap`: set up a clam av scanner in ICAP mode.
- `clamav-icap-tls`: set up a clam av scanner in ICAP mode with TLS encryption.
- `oc`: start an ownCloud instance in the same network.
- `imaginary`: start an Imaginary service and configure it for preview
generation.
- `mail`: start an [smtp4dev](https://github.com/rnwood/smtp4dev) server and
- `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.
- `<path to app.tar.gz>`: by specifying the path to an app package this package
will be extracted into the apps. directory of the new instance (overwriting
any existing app code). This can be used to quickly test a packaged app.
- The name of any configured preset.
#### Run tests in a new instance
@ -105,7 +110,7 @@ Additionally, you can use the following options when starting an instance:
haze test [database] [php-version] [path]
```
Where `path` is a file or folder to run phpunit in, relative to the sources
Where `path` is a file or folder to run PHPUnit in, relative to the sources
root.
### List running instances
@ -129,7 +134,7 @@ haze clean
## Controlling running instances
The following commands run against the most recently started instance and allow
optionally providing a `match` to select a specific instance by it's name.
optionally providing a `match` to select a specific instance by its name.
#### Open an instance
@ -146,11 +151,14 @@ haze [match] db
#### Execute a command on an instance
```bash
haze [match] exec [cmd]
haze [match] [service] [cmd]
```
If no `cmd` is specified it will launch `bash`
If a service name or `db` is provided, the command will be in the container of
the service or database.
#### Create a new instance and run a command
```bash
@ -206,7 +214,7 @@ haze [match] env <cmd> [args]
Runs the provided command with `NEXTCLOUD_URL`, `DATABASE_URL` and `REDIS_URL`
environment variables set for the matched instance.
This is intented to run a local
This is intended to run a local
[push daemon](https://github.com/nextcloud/notify_push) against an instance.
#### Update the container images
@ -227,7 +235,7 @@ haze [match] edit <path>
haze [match] reload
```
The php configuration can edit changed with `haze edit /php.ini`
The php configuration can edit changed with `haze edit /config/php.ini`
#### Checkout a branch for all local apps
@ -254,29 +262,30 @@ Performs a pull in all git repositories within the apps folder.
Multiple instances can reach each other by using their instance name as domain
name to allow for testing federation between instances. Alternatively, you can
setup the haze proxy and the proxied domains to get https support between
set up the haze proxy and the proxied domains to get https support between
instances.
## Proxy
By default, instances can be accessed by their IP. In order to get more
memorable urls and allow supporting https, haze comes with a builtin reverse
memorable URLs and allow supporting https, haze comes with a builtin reverse
proxy to allow using a wildcard domain.
### Requirements
- A domain name you can set wildcard DNS records for
- A reverse proxy like nginx or apache
- A reverse proxy like nginx or Apache
- (optionally) a wildcard ssl certificate (can be acquiring using letsencrypt
and dns verification)
### Setup
- Set a DNS record for `*.haze.exmaple.com` and `haze.example.com` pointing to
your development machine. (127.0.0.1 will not work)
- Set the `proxy` configuration with your domain and desired listen endpoint
- Setup a service to run `haze proxy` in the background as your own user. A
systemd user service is recommended.
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
example).
- Configure your reverse proxy of choice to proxy `*.haze.example.com` and
`haze.example.com` to the proxy's listen endpoint
- (optional) acquire a wildcard ssl certificate for your domain and set your
@ -286,11 +295,17 @@ proxy to allow using a wildcard domain.
### Usage
When the proxy is configured, generated urls for the instances will use a
When the proxy is configured, generated URLs for the instances will use a
subdomain of the configured domain, e.g. the `rolling-bees` instance will be
available at `rolling-bees.haze.example.com`. Additionally, `haze.example.com`
will automatically point to the last created instance.
Additionally, the proxy allows access to the server containers trough either
`<instance id>-<service id>.haze.example.com` for a specific instance, or
`<service-id>.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.
## Configuration
Configuration is loaded from `~/.config/haze/haze.toml` and has the following
@ -298,6 +313,7 @@ options
```toml
sources_root = "/path/to/sources" # path of the nextcloud sources. required
app_directories = ["/path/to/sources/more_app"] # paths to additional app directories.
work_dir = "/path/to/temp/dir" # path to temporary directory. optional, defaults to "/tmp/haze"
[auto_setup] # optional
@ -309,6 +325,7 @@ disable_apps = ["contacts"] # apps to disable after setup, defaults to []
post_setup = [# commands to execute after setup, defaults to []
"occ group:add test",
]
config = { "foo" = "bar" } # configuration options to set before install
[[volume]] # optional
source = "/tmp/haze-shared"
@ -322,7 +339,7 @@ read_only = true
[proxy] # optional
address = "haze.example.com" # base domain
https = true # Is the proxy behind an https terminating proxy
https = true # Is the proxy behind a https terminating proxy
listen = "/run/haze/haze.sock" # either a unix socket path
#listen = "127.0.0.1:8080" # or a socket address

View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCQuTk+irdXRGbY
JUu+AFmMM4/CCtBIBBgGIG18tesgQUeHEPdRHbBypvFKhpXvlKQbVbiaZxFjtlvn
L2ReN7gYwjiTjLWuaDgOzGQyObwSJpedlcd5Q957WNlc5OjpoK8zZq9EGnmvjIqe
5VVBne85RZVw6+i4ljEiWoXCiy0iOPIL4jKO8kfO8EmTTh6ge2sLCT5jFT/V63/B
hjEobA7+vPbAmEo+Qs4adnBSrlX9nLtL1j4gqawAQMmGl/Ti75T0uQvNxdq2gddc
n0wyWhyERnZNeMv5sfkwCOTuetNNtGLf/lTkXNdaOCRvW5jJ0EjYYdpQG5R0do1U
XFLH1cZZAgMBAAECggEAR0xpTk2Ku5yASlY9dXK4qyCv3znymLgjmckaB4mcN7zR
X1JVdYn55tImJ8AcV/bTzn+xvaevYn9x0XiAqwYqVVBCDTcSPsUrcObzKedVp1+J
7GHg7vYnwn7oPyKrOIYoKluZVyTv9DN6C4QSN4x2UbHdSM+ATIf51uHf6hMk/ilv
4uw3csxSCpLOqqsYCQarES7SypcETjFpNnIfTPt7q2Y2DbIDttTTjzrm0/GBP0WE
OYNvzZZPZRPJfy3et3r9vJbqWzGHvOttzQ+EFHjwPTMfW4tsHcCsEKSGWwLpG4bN
FPNx0+QCqDiChesdiCHFNSk+u7pRZrHdjuDJEuKSzQKBgQDMDgdCGBQfgaNnkRz/
aiv3V200/vXegnc0Jz49Dye5AxEVu0X1m2xZpJv+qEwbOx5B+1PV3gfVP/iRf+FK
MAwFbmb7hGcDE8AGNsSpQydjwzKoi/M67YXv7T8dgWKnwz0eyU4K2IOGInGxuFty
Ik7+DTqz+Ikh1RiAoGbKfw9yhwKBgQC1kKhjxB7r/uSLcfOSg1mLcR5lTrNDQAPQ
GnsIje0nD5Tv52/k6U4tk15vjL4t6KZUFo9SJ4O1kM1veyuOJuol2AfPXS+H/Izo
5BjmoZ0jOONOZZiRIB1moQSy1qhTAeZB9S1ORxQ3dIBPqm+oyADPTTsNV67Cwnt4
woeZRUPYHwKBgQDE0AcKJcVK+jQMUXfBlrsfTvDjO8MTwYyN/gfWxsZOeXnCFyYM
FcO01sMrJVJ6tVOi2nFrB0NQ2Om8FLbMYnlFx82GbJca7bK5i5u1kjLs0zoKPSn1
vWEBIDhPEhuAqhxKlGk0ps580r4MZz+0XwkHmuTy7xX9TtbaQVvDljflAwKBgDKy
3hJdpTTIzBCUFSuIOezR/WbUfwH8UhQ+ELTmzJ3nn/MNcRU+gHIBgJEtf71aBXfd
hM+v8Ps2H+dNQXBENYWzuRqSLr+OKdquNrXP0w0OyYoOnHeJvCv4MlOt1Pq4wQ8R
40DEYETL5zhXoy5CCtfX/PFQ1p/Tpp6l0y9dRACJAoGALwUbyyDy85b2xRQB6RtU
I+5Vz5cd/1eQdCkoU9mX4qWA/hWpgc7Z2Jd67LW/WWtVjlF9hva/WNDSfGsXo2ew
C8OofvlfIuFDOCXrodYdHE1Q4g5TZdESr0XAqopq+QzBs89qbIy05kM9iuE4yFUo
xeimCY9oDWTeGw/XrLdHZF4=
-----END PRIVATE KEY-----

View file

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIULiChaTwmVx6nRTHohmPuf55/4jUwDQYJKoZIhvcNAQEL
BQAwPjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxDTALBgNVBAoM
BEhhemUxCzAJBgNVBAMMAnMzMB4XDTI2MDMwOTE3NTgwOVoXDTI3MDMwOTE3NTgw
OVowPjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxDTALBgNVBAoM
BEhhemUxCzAJBgNVBAMMAnMzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEAkLk5Poq3V0Rm2CVLvgBZjDOPwgrQSAQYBiBtfLXrIEFHhxD3UR2wcqbxSoaV
75SkG1W4mmcRY7Zb5y9kXje4GMI4k4y1rmg4DsxkMjm8EiaXnZXHeUPee1jZXOTo
6aCvM2avRBp5r4yKnuVVQZ3vOUWVcOvouJYxIlqFwostIjjyC+IyjvJHzvBJk04e
oHtrCwk+YxU/1et/wYYxKGwO/rz2wJhKPkLOGnZwUq5V/Zy7S9Y+IKmsAEDJhpf0
4u+U9LkLzcXatoHXXJ9MMlochEZ2TXjL+bH5MAjk7nrTTbRi3/5U5FzXWjgkb1uY
ydBI2GHaUBuUdHaNVFxSx9XGWQIDAQABo1MwUTAdBgNVHQ4EFgQUJJ8HiT2zmuF5
6WHHFsTHgkrayxYwHwYDVR0jBBgwFoAUJJ8HiT2zmuF56WHHFsTHgkrayxYwDwYD
VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAUF0lB/qIrxkgZ4sqNrw4
CInHCK29XVaMoqk1QZyS/KhWDM+zgbA92OxxuhCKw4iJEajZvgg0S9RtGkBNmquU
l0rf0JdALd0jPkWr7+3OeqlcgOs2EH7PTqrrbXTGsR12D+Ot+OerQeWXmO28Zrl8
4O67TwQtslXwZzeCrtiwAA2DrIYpSLzh+qDtwbY5hMG5zmqqjBM20Ysgxszh4rhl
KR6skXwZwkVVhKpK76qwnU02PIMr8auL1csx8/uBTd/UzX2veqlkOP5V/Gg6eEbI
4fTOzq7k+FyuzSkrEX4Vc9GbWcRvoVZh+qAKUKstqlE2iCrqmZ+Wal6GA8JA5SZ+
bQ==
-----END CERTIFICATE-----

38
certificates/sftp/id_rsa Normal file
View file

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

View file

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDfbdpaofpjBEtsIE73hQo6R+cnabpmyJzR0y3dOYOeMtTQnOk63vC3n6YxmsQpBOl5+jPJQejfP4eSaWUsAY66k3LcD6MbY73nlRSDDR0Z+7qPo45vroO72Omq3wFDqCvdwRgBKmU8jsHzz39xWknmXUeIFSDJHtnXq/XmoIO2akVUgQIB0gISY+ix+Zm623dhiX3EtrLlYKZTO1ZL9qY9wazQM4QatZ4K13EYpZPdPLB0ErJDQ9t1gxqaAMhPslXVlLNk9qXLiwHsT52lLkCAEVVeZnrXXnUo3VMnyDKhSMWFjty1gQimdxmbJPg9i/Ii/FGxsm28RGJ40fwOhfkPC2pzFuAcrQ7Bi12XjJ1FdOhijeX6oiJ1/6DwANV5r+ZuR9M96B1Oijcs65OFc7ATEOW6SkzyhsUOnPViXUvqtEkBU07UqYdaGfq2jbeO1iMGFzMDaeR6ZBdBtVKNz+Ei2NAMgU0w6wtbfc/5jaT0uSkjRMvT2iA7E4VnMZmna00= haze@haze

120
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": {
"crane": {
"locked": {
"lastModified": 1760924934,
"narHash": "sha256-tuuqY5aU7cUkR71sO2TraVKK2boYrdW3gCSXUkF4i44=",
"lastModified": 1763938834,
"narHash": "sha256-j8iB0Yr4zAvQLueCZ5abxfk6fnG/SJ5JnGUziETjwfg=",
"owner": "ipetkov",
"repo": "crane",
"rev": "c6b4d5308293d0d04fcfeee92705017537cad02f",
"rev": "d9e753122e51cee64eb8d2dddfe11148f339f5a2",
"type": "github"
},
"original": {
@ -15,6 +15,22 @@
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
},
"flakelight": {
"inputs": {
"nixpkgs": [
@ -22,11 +38,11 @@
]
},
"locked": {
"lastModified": 1764593611,
"narHash": "sha256-6SdexcO69Dlu14YN2xuB1A6JHWSrcqMj7Na9oK7IT2M=",
"lastModified": 1773062095,
"narHash": "sha256-u+cK9IoJokO4YzQwMc2s8Vti0RL/LVSrROOEn2opc5U=",
"owner": "nix-community",
"repo": "flakelight",
"rev": "0d63256401341f528dd628f1a8e96d3afecade7a",
"rev": "c99e4d5f40e578cb2d8f460ea2bbd5dc26316d24",
"type": "github"
},
"original": {
@ -44,11 +60,11 @@
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1763591898,
"narHash": "sha256-aHSMj7CIa9EJYxdf05wOWRGp0KRsT/TAox7uwVSdDb8=",
"lastModified": 1772297202,
"narHash": "sha256-UEzHO/tCmhPhr8RpWtbm1MTa7ABobwt3nCjrcuDAPm0=",
"ref": "refs/heads/main",
"rev": "2d9b2da2c9f384f93ef977c48f8ee35ce586529b",
"revCount": 66,
"rev": "8690e1514863b934de12f2a503c9431d186ce30b",
"revCount": 68,
"type": "git",
"url": "https://codeberg.org/icewind/mill-scale.git"
},
@ -59,11 +75,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1764522689,
"narHash": "sha256-SqUuBFjhl/kpDiVaKLQBoD8TLD+/cTUzzgVFoaHrkqY=",
"lastModified": 1772822230,
"narHash": "sha256-yf3iYLGbGVlIthlQIk5/4/EQDZNNEmuqKZkQssMljuw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8bb5646e0bed5dbd3ab08c7a7cc15b75ab4e1d0f",
"rev": "71caefce12ba78d84fe618cf61644dce01cf3a96",
"type": "github"
},
"original": {
@ -72,11 +88,48 @@
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1772173633,
"narHash": "sha256-MOH58F4AIbCkh6qlQcwMycyk5SWvsqnS/TCfnqDlpj4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c0f3d81a7ddbc2b1332be0d8481a672b4f6004d6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"phps": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": "nixpkgs_2",
"utils": "utils"
},
"locked": {
"lastModified": 1772365008,
"narHash": "sha256-/ynkWKeZ1dyRIUkQas0AB35semWAwCbTKXu+/q+8MGg=",
"owner": "fossar",
"repo": "nix-phps",
"rev": "f47eb877bf1c219809e4357eec2fdab8e3263b7b",
"type": "github"
},
"original": {
"owner": "fossar",
"repo": "nix-phps",
"type": "github"
}
},
"root": {
"inputs": {
"flakelight": "flakelight",
"mill-scale": "mill-scale",
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"phps": "phps"
}
},
"rust-overlay": {
@ -88,11 +141,11 @@
]
},
"locked": {
"lastModified": 1761964689,
"narHash": "sha256-Zo3LQQDz+64EQ9zor/WmeNTFLoZkjmhp0UY3G0D3seE=",
"lastModified": 1764557621,
"narHash": "sha256-kX5PoY8hQZ80+amMQgOO9t8Tc1JZ70gYRnzaVD4AA+o=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "63d22578600f70d293aede6bc737efef60ebd97f",
"rev": "93316876c2229460a5d6f5f052766cc4cef538ce",
"type": "github"
},
"original": {
@ -100,6 +153,39 @@
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",

View file

@ -9,8 +9,15 @@
url = "git+https://codeberg.org/icewind/mill-scale.git";
inputs.flakelight.follows = "flakelight";
};
phps = {
url = "github:fossar/nix-phps";
};
outputs = {mill-scale, ...}:
};
outputs = {
mill-scale,
phps,
...
}:
mill-scale ./. {
crossTargets = [
"x86_64-unknown-linux-gnu"
@ -24,17 +31,23 @@
};
extraPaths = [
./redis-certificates
./certificates
];
withOverlays = [
(import ./nix/overlay.nix)
(prev: final: {
inherit (phps.packages.${prev.system}) php81 php80;
})
];
packages = {
"haze-image-php-8.5" = pkgs: pkgs.haze-image-php-85;
"haze-image-php-8.4" = pkgs: pkgs.haze-image-php-84;
"haze-image-php-8.3" = pkgs: pkgs.haze-image-php-83;
"haze-image-php-8.2" = pkgs: pkgs.haze-image-php-82;
"haze-image-php-8.1" = pkgs: pkgs.haze-image-php-81;
"haze-image-php-8.0" = pkgs: pkgs.haze-image-php-80;
};
tools = pkgs: with pkgs; [cargo-edit bacon skopeo];

10
haze.service Normal file
View file

@ -0,0 +1,10 @@
[Install]
WantedBy=default.target
[Service]
ExecStart=/usr/bin/haze proxy
Restart=on-failure
RestartSec=10
[Unit]
Description=Haze reverse proxy

View file

@ -9,9 +9,10 @@ with lib; let
format = pkgs.formats.toml {};
configFile = format.generate "haze.toml" ({
sources_root = cfg.sourcesRoot;
app_directories = cfg.appDirectories;
work_dir = cfg.workDir;
auto_setup = {
enabled = cfg.autoSetup.enable;
inherit (cfg.autoSetup) enable config;
post_setup = cfg.autoSetup.postSetup;
enable_apps = cfg.autoSetup.enableApps;
disable_apps = cfg.autoSetup.disableApps;
@ -61,6 +62,12 @@ in {
description = "Directory to store instance data";
};
appDirectories = mkOption {
type = types.listOf types.str;
default = [];
description = "Paths to additional app directories";
};
autoSetup = mkOption {
type = types.submodule {
options = {
@ -84,6 +91,13 @@ in {
default = [];
description = "Commands to run post-setup";
};
config = mkOption {
type = types.submodule {
freeformType = format.type;
};
description = "Configuration options to set before install";
default = {};
};
};
};
};

88
nix/image/bootstrap Executable file
View file

@ -0,0 +1,88 @@
#!/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
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
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

View file

@ -1,76 +0,0 @@
#!/usr/bin/env bash
touch /var/log/nginx/access.log
touch /var/log/nginx/error.log
touch /var/log/cron/owncloud.log
echo "# Options in here overwrite the builtin php.ini" > /php.ini
chmod 0777 /php.ini
PHP_INI_DIR="$(php --ini | grep 'Scan' | cut -d ' ' -f7)"
ln -s /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…"
# 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
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
redis-server --protected-mode no \
--tls-port 6379 --port 0 \
--tls-cert-file /redis-certificates/server.crt \
--tls-key-file /redis-certificates/server.key \
--tls-ca-cert-file /redis-certificates/ca.crt &
else
redis-server --protected-mode no &
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 &
'&
fi
php-fpm --fpm-config /etc/php-fpm.conf&
nginx -c /etc/nginx.conf

View file

@ -1,12 +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 ${./php-fpm.conf} $out/etc/php-fpm.conf
cp ${./nginx.conf} $out/etc/nginx.conf
mkdir -p $out
cp -r ${./configs} $out/etc
chmod -R +w $out/etc
mkdir $out/etc/supervisor/enabled/
''

View file

@ -0,0 +1,6 @@
listen-address=127.0.0.22
no-resolv
address=/PROXY_BASE/HOST_IP
server=UPSTREAM_DNS

View file

@ -0,0 +1 @@
.mode table

View file

@ -1,12 +1,19 @@
<?php $CONFIG=[
<?php
$extra_config = [];
if (file_exists(__DIR__ . '/nextcloud.json')) {
$extra_config = json_decode(file_get_contents(__DIR__ . '/nextcloud.json'), true);
}
$CONFIG = array_merge_recursive($extra_config, [
'debug' => true,
'appstoreenabled' => false,
'memcache.local' => '\\OC\\Memcache\\APCu',
'memcache.distributed' => '\\OC\\Memcache\\APCu',
'memcache.locking' => '\\OC\\Memcache\\APCu',
'memcache.distributed' => '\\OC\\Memcache\\Redis',
'memcache.locking' => '\\OC\\Memcache\\Redis',
'allow_local_remote_servers' => true,
'trusted_domains' => ['cloud'],
'profiling.secret' => 'haze',
'profiling.path' => '/tmp/profiling',
//PLACEHOLDER
];
]);

View file

@ -1,2 +1 @@
'redis' => ['host' => 'localhost'],
//PLACEHOLDER

View file

@ -2,10 +2,9 @@
'host' => 'tls://127.0.0.1',
'port' => 6379,
'ssl_context' => [
'local_cert' => '/redis-certificates/client.crt',
'local_pk' => '/redis-certificates/client.key',
'cafile' => '/redis-certificates/ca.crt',
'local_cert' => '/certificates/redis/client.crt',
'local_pk' => '/certificates/redis/client.key',
'cafile' => '/certificates/redis/ca.crt',
'verify_peer_name' => false,
],
],
//PLACEHOLDER

View file

@ -0,0 +1,15 @@
'objectstore' => [
'class' => 'OC\Files\ObjectStore\S3',
'arguments' => [
'bucket' => 'nextcloud',
'autocreate' => true,
'key' => 'minio',
'secret' => 'minio123',
'hostname' => 's3',
'port' => 9000,
'use_ssl' => true,
'use_path_style' => true,
'uploadPartSize' => 52428800,
'use_nextcloud_bundle' => true,
],
],

View file

@ -69,7 +69,17 @@ http {
access_log off;
}
include /conf/nginx-app.conf;
location ^~ /store_apps {
root /var/www;
try_files $uri /index.php$request_uri;
access_log off; # Optional: Don't log access to assets
location ~ \.wasm$ {
default_type application/wasm;
}
}
include /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;
@ -99,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;
}
}
}

View file

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

View file

@ -0,0 +1,2 @@
[program:blackfire]
command = blackfire agent

View file

@ -0,0 +1,2 @@
[program:dnsmasq]
command = /bin/dnsmasq --keep-in-foreground -u root

View file

@ -0,0 +1,3 @@
[program:frankenphp]
command = /bin/frankenphp php-server
directory = /var/www/html

View file

@ -0,0 +1,2 @@
[program:nginx]
command = /bin/nginx -c /etc/nginx.conf

View file

@ -0,0 +1,2 @@
[program:php-fpm]
command = /bin/php-fpm --fpm-config /etc/php-fpm.conf

View file

@ -0,0 +1,2 @@
[program:redis]
command = /bin/redis-server --protected-mode no

View file

@ -0,0 +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

View file

@ -0,0 +1,19 @@
[supervisord]
logfile = /dev/stdout
logfile_maxbytes = 0
nodaemon = true
pidfile = /var/run/supervisord.pid
user = root
[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
[include]
files = enabled/*

View file

@ -7,10 +7,9 @@
blackfire,
coreutils,
getent,
writers,
shadow,
buildEnv,
runCommand,
cacert,
callPackage,
cronie,
redis,
@ -33,18 +32,20 @@
writeShellApplication,
vim,
helix,
python3Packages,
dnsmasq,
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;
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;
@ -70,14 +71,6 @@
php = phpEnv;
};
phpunitWrapped = majorVersion:
writeShellApplication {
name = "phpunit${toString majorVersion}";
text = ''
${phpunitUnwrapped (toString majorVersion)}/bin/phpunit "$@"
'';
};
phpunit = writeShellApplication {
name = "phpunit";
runtimeInputs = [jq];
@ -104,9 +97,9 @@
'';
};
redis-certificates = runCommand "scripts" {} ''
certificates = runCommand "scripts" {} ''
mkdir -p $out
cp -r ${../../redis-certificates} $out/redis-certificates
cp -r ${../../certificates} $out/certificates
'';
clamav-data = runCommand "scripts" {} ''
mkdir -p $out/etc
@ -145,6 +138,10 @@
oracle-instantclient
vim
helix
python3Packages.supervisor
dnsmasq
nushell
getent
];
};
@ -153,10 +150,19 @@
tag = phpVersion;
fromImage = baseImage;
copyToRoot = [
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
@ -170,7 +176,7 @@ in
bootstrap
configs
scripts
redis-certificates
certificates
clamav-data
shadowSetupScript
];
@ -185,7 +191,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;
};
};
}

45
nix/image/php-ext.nix Normal file
View file

@ -0,0 +1,45 @@
{
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
imagick
]
++ optionals (!debug) [
# smbclient # this breaks the build for no apparent reason
]
++ optionals withBlackfire [
blackfire
])

View file

@ -2,42 +2,9 @@
lib,
php,
debug ? false,
}: let
inherit (lib) optionals;
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
blackfire
]);
}:
php.buildEnv {
extensions = import ./php-ext.nix {inherit lib php debug;};
extraConfig = ''
xdebug.mode=debug,trace,profile
xdebug.start_with_request=trigger
@ -54,4 +21,4 @@ in
apc.enable_cli=1
opcache.enable_cli=1
'';
}
}

View file

@ -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" = "oci" ]; then
# oracle is a special snowflake
occ maintenance:install --admin-user=$USER --admin-pass=$PASSWORD --database=$SQL --database-name=xe --database-host=$SQL --database-user=system --database-pass=haze
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
}

View file

@ -1,4 +1,22 @@
#!/bin/sh
#!/bin/nu
cd $WEBROOT/build/integration
./run.sh "$@"
def --wrapped 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/" ""
}
# 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
}

View file

@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/nu
touch /var/log/nginx/access.log
touch /var/log/nginx/error.log
@ -7,59 +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 "${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"
}

View file

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

View file

@ -1,7 +1,7 @@
#!/bin/sh
#!/bin/nu
cd $WEBROOT
def --wrapped 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
}

View file

@ -1,7 +1,9 @@
final: prev: {
haze = final.callPackage ./package.nix {};
haze-image-php-85 = final.callPackage ./image/haze.nix {php = final.php85;};
haze-image-php-84 = final.callPackage ./image/haze.nix {php = final.php84;};
haze-image-php-83 = final.callPackage ./image/haze.nix {php = final.php83;};
haze-image-php-82 = final.callPackage ./image/haze.nix {php = final.php82;};
haze-image-php-81 = final.callPackage ./image/haze.nix {php = final.php81;};
haze-image-php-80 = final.callPackage ./image/haze.nix {php = final.php80;};
}

View file

@ -1,16 +1,15 @@
{
rustPlatform,
pkg-config,
lib,
git,
}: let
inherit (lib) getExe;
inherit (lib.sources) sourceByRegex;
inherit (builtins) fromTOML readFile;
src = sourceByRegex ../. ["Cargo.*" "(src|redis-certificates)(/.*)?"];
src = sourceByRegex ../. ["Cargo.*" "(src|certificates)(/.*)?"];
version = (fromTOML (readFile ../Cargo.toml)).package.version;
in
rustPlatform.buildRustPackage rec {
rustPlatform.buildRustPackage {
pname = "haze";
inherit src version;
@ -20,7 +19,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=";
};
};
}

View file

@ -124,9 +124,12 @@ pub enum GitOperation {
///
/// "main" and "master" can be used interchangeably.
#[strum(props(
Args = "[branch] branch to checkout, defaults to the branch matching the current server version"
Args = "[branch] branch to checkout, defaults to the branch matching the current server version [-v] verbose"
))]
Checkout { branch: Option<String> },
Checkout {
branch: Option<String>,
verbose: bool,
},
}
impl SubCommand for GitOperation {
@ -162,6 +165,7 @@ impl LogService {
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ExecService {
Db,
Service(Service),
}
impl HazeArgs {
@ -229,6 +233,14 @@ impl HazeArgs {
args.next();
Some(ExecService::Db)
}
Some(arg) => Service::from_type(&[], arg.as_ref())
.into_iter()
.filter_map(|services| services.into_iter().next())
.next()
.map(|service| {
args.next();
ExecService::Service(service)
}),
_ => None,
};
@ -320,18 +332,24 @@ impl HazeArgs {
HazeCommand::Checkout => {
let branch = args.next().map(S::into);
Ok(HazeArgs::Git {
operation: GitOperation::Checkout { branch },
operation: GitOperation::Checkout {
branch,
verbose: false,
},
})
}
HazeCommand::Git => {
let mut args = args.peekable();
let operation = args
.next()
.ok_or_else(|| Report::msg("No git operation provided"))?;
match operation.as_ref() {
"checkout" => {
let verbose = args.next_if(|arg| arg.as_ref() == "-v").is_some();
let branch = args.next().map(S::into);
let verbose = verbose | args.next_if(|arg| arg.as_ref() == "-v").is_some();
Ok(HazeArgs::Git {
operation: GitOperation::Checkout { branch },
operation: GitOperation::Checkout { branch, verbose },
})
}
"pull" => Ok(HazeArgs::Git {
@ -469,7 +487,7 @@ pub enum HazeCommand {
Edit,
/// Reload the php configuration in the instance
#[strum(props(
Details = "note: you can overwrite <yellow>php.ini</yellow> settings with <literal>haze</literal> <arg>[filter]</arg> <literal>edit /php.ini</literal>"
Details = "note: you can overwrite <yellow>php.ini</yellow> settings with <literal>haze</literal> <arg>[filter]</arg> <literal>edit /config/php.ini</literal>"
))]
Reload,
}
@ -505,6 +523,7 @@ impl SubCommand for HazeCommand {
fn test_arg_parse() {
let config = HazeConfig {
sources_root: Default::default(),
app_directories: Default::default(),
work_dir: Default::default(),
auto_setup: Default::default(),
volume: vec![],

View file

@ -1,13 +1,14 @@
use crate::config::{HazeConfig, HazeVolumeConfig};
use crate::database::Database;
use crate::exec::{exec, exec_io, exec_tty, ExitCode};
use crate::mapping::{default_mappings, Mapping};
use crate::mapping::{for_config, Mapping};
use crate::php::{PhpVersion, PHP_MEMORY_LIMIT};
use crate::service::Service;
use crate::service::ServiceTrait;
use bollard::container::{ListContainersOptions, RemoveContainerOptions, UpdateContainerOptions};
use bollard::models::ContainerState;
use bollard::network::CreateNetworkOptions;
use crate::sources::download_nc;
use bollard::config::NetworkCreateRequest;
use bollard::models::{ContainerState, ContainerUpdateBody};
use bollard::query_parameters::{ListContainersOptions, RemoveContainerOptions};
use bollard::Docker;
use camino::{Utf8Path, Utf8PathBuf};
use flate2::read::GzDecoder;
@ -19,9 +20,9 @@ use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::Display;
use std::fs;
use std::fs::read_to_string;
use std::fs::{read_to_string, write};
use std::io::{stdout, Cursor, Read, Stdout, Write};
use std::iter::Peekable;
use std::iter::{once, Peekable};
use std::net::IpAddr;
use std::os::unix::fs::MetadataExt;
use std::str::FromStr;
@ -57,6 +58,7 @@ pub struct CloudOptions {
pub php: PhpVersion,
pub services: Vec<Service>,
pub app_packages: Vec<Utf8PathBuf>,
pub version: Option<String>,
}
impl CloudOptions {
@ -69,6 +71,7 @@ impl CloudOptions {
db: Database::default(),
services: vec![],
app_packages: vec![],
version: None,
}
}
@ -82,6 +85,7 @@ impl CloudOptions {
let mut name = None;
let mut services = Vec::new();
let mut app_package = Vec::new();
let mut version = None;
while let Some(option) = args.peek() {
if let Ok(db_option) = Database::from_str(option.as_ref()) {
@ -96,6 +100,9 @@ impl CloudOptions {
} else if option.as_ref().ends_with(".tar.gz") {
app_package.push(option.to_string().into());
let _ = args.next();
} else if let Some(v) = option.as_ref().strip_prefix("v") {
version = Some(v.into());
let _ = args.next();
} else if option.as_ref() == "--name" {
let _ = args.next();
name = args.next().map(|s| s.into());
@ -112,6 +119,7 @@ impl CloudOptions {
.unwrap_or_default(),
services,
app_packages: app_package,
version,
})
}
}
@ -144,44 +152,44 @@ fn test_option_parse() {
..Default::default()
}
);
let mut args = vec!["7"].into_iter().peekable();
let mut args = vec!["8"].into_iter().peekable();
assert_eq!(
CloudOptions::parse(&config, &mut args).unwrap(),
CloudOptions {
php: PhpVersion::Php74,
php: PhpVersion::Php80,
..Default::default()
}
);
let mut args = vec!["7", "pgsql", "rest"].into_iter().peekable();
let mut args = vec!["8.1", "pgsql", "rest"].into_iter().peekable();
assert_eq!(
CloudOptions::parse(&config, &mut args).unwrap(),
CloudOptions {
php: PhpVersion::Php74,
php: PhpVersion::Php81,
db: Database::Postgres,
..Default::default()
}
);
let mut args = vec!["7", "ldap", "pgsql"].into_iter().peekable();
let mut args = vec!["8", "ldap", "pgsql"].into_iter().peekable();
assert_eq!(
CloudOptions::parse(&config, &mut args).unwrap(),
CloudOptions {
php: PhpVersion::Php74,
php: PhpVersion::Php80,
db: Database::Postgres,
services: vec![Service::Ldap(Ldap), Service::LdapAdmin(LdapAdmin)],
..Default::default()
}
);
let mut args = vec!["7", "pgsql", "ldap"].into_iter().peekable();
let mut args = vec!["8", "pgsql", "ldap"].into_iter().peekable();
assert_eq!(
CloudOptions::parse(&config, &mut args).unwrap(),
CloudOptions {
php: PhpVersion::Php74,
php: PhpVersion::Php80,
db: Database::Postgres,
services: vec![Service::Ldap(Ldap), Service::LdapAdmin(LdapAdmin)],
..Default::default()
}
);
let mut args = vec!["7", "pgsql", "ldap", "mypreset"]
let mut args = vec!["8", "pgsql", "ldap", "mypreset"]
.into_iter()
.peekable();
@ -198,7 +206,7 @@ fn test_option_parse() {
assert_eq!(
CloudOptions::parse(&config, &mut args).unwrap(),
CloudOptions {
php: PhpVersion::Php74,
php: PhpVersion::Php80,
db: Database::Postgres,
services: vec![
Service::Ldap(Ldap),
@ -210,7 +218,7 @@ fn test_option_parse() {
);
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Cloud {
pub id: String,
pub network: String,
@ -245,6 +253,12 @@ impl Cloud {
.wrap_err("Failed to create directory for app packages")?;
}
let source_root = if let Some(version) = options.version.as_deref() {
download_nc(config, version).await?
} else {
config.sources_root.clone()
};
let app_volumes = options
.app_packages
.iter()
@ -272,11 +286,8 @@ impl Cloud {
})
})
.collect::<Result<Vec<_>>>()?;
let mappings = config
.volume
.iter()
.map(Mapping::from)
.chain(default_mappings())
let mappings = for_config(config)
.chain(app_volumes.iter().map(Mapping::from))
.collect::<Vec<_>>();
for mapping in &mappings {
@ -286,9 +297,56 @@ impl Cloud {
.wrap_err_with(|| format!("Failed to setup work directory {}", mapping.source))?;
}
let mut nc_config = Value::Object(
config
.auto_setup
.config
.clone()
.into_iter()
.map(|(key, value)| (key, serde_json::to_value(value).unwrap()))
.collect(),
);
nc_config["apps_paths"] = Value::Array(
once("apps")
.chain(
config
.app_directories
.iter()
.filter_map(|dir| dir.file_name()),
)
.map(|name| {
[
(
String::from("path"),
Value::from(format!("/var/www/html/{}", name)),
),
(String::from("url"), Value::from(format!("/{}", name))),
(String::from("writable"), Value::from(false)),
]
.into_iter()
.collect()
})
.chain(once(
[
(String::from("path"), Value::from("/var/www/store_apps")),
(String::from("url"), Value::from("/store_apps")),
(String::from("writable"), Value::from(true)),
]
.into_iter()
.collect(),
))
.collect(),
);
write(
workdir.join("config/nextcloud.json"),
serde_json::to_string_pretty(&nc_config).unwrap(),
)
.into_diagnostic()
.wrap_err("Failed to write config json")?;
let network = docker
.create_network(CreateNetworkOptions {
name: id.as_str(),
.create_network(NetworkCreateRequest {
name: id.clone(),
..Default::default()
})
.await
@ -296,7 +354,7 @@ impl Cloud {
.id;
let network_info = docker
.inspect_network::<String>(&network, None)
.inspect_network(&network, None)
.await
.into_diagnostic()?;
let gateway = network_info
@ -327,7 +385,7 @@ impl Cloud {
];
let volumes: Vec<String> = mappings
.into_iter()
.filter_map(|mapping| mapping.get_volume_arg(&id, config))
.filter_map(|mapping| mapping.get_volume_arg(&id, config, &source_root))
.collect();
if let Some(db_name) = options
@ -382,6 +440,7 @@ impl Cloud {
gateway,
&options.services,
&config.proxy,
options.version.as_deref(),
)
.await
.wrap_err("Failed to start php container")
@ -439,9 +498,26 @@ impl Cloud {
}
};
for pre_setup in options
.services
.iter()
.flat_map(|service| service.pre_setup(docker, &id, config).into_iter().flatten())
{
exec(
docker,
&container,
&uid.to_string(),
pre_setup,
vec!["NC_IS_CONFIG_READ_ONLY=1"],
Some(stdout()),
)
.await?;
}
containers.push(container);
let options_clone = options.clone();
let proxy_config = config.proxy.clone();
let cloud_id = id.clone();
let docker_clone = docker.clone();
spawn(async move {
@ -455,7 +531,10 @@ impl Cloud {
return;
}
for service in options_clone.services {
match service.start_message(&docker_clone, &cloud_id).await {
match service
.start_message(&docker_clone, &cloud_id, &proxy_config)
.await
{
Ok(Some(msg)) => {
println!("{}", msg);
}
@ -484,6 +563,9 @@ impl Cloud {
pub async fn destroy(self, docker: &Docker) -> Result<()> {
for container in self.containers {
let _ = docker
.kill_container(container.trim_start_matches('/'), None)
.await;
docker
.remove_container(
container.trim_start_matches('/'),
@ -583,7 +665,7 @@ impl Cloud {
config: &HazeConfig,
) -> Result<Vec<Cloud>> {
let containers = docker
.list_containers::<String>(Some(ListContainersOptions {
.list_containers(Some(ListContainersOptions {
all: true,
..Default::default()
}))
@ -620,6 +702,7 @@ impl Cloud {
let labels = cloud.labels?;
let db = labels.get("haze-db")?.parse().ok()?;
let php = labels.get("haze-php")?.parse().ok()?;
let version = labels.get("haze-version").cloned();
let found_services = labels
.get("haze-services")?
@ -665,6 +748,7 @@ impl Cloud {
db,
services: found_services,
app_packages: vec![],
version,
},
pinned,
address,
@ -735,9 +819,9 @@ impl Cloud {
docker
.update_container(
&self.id,
UpdateContainerOptions::<String> {
ContainerUpdateBody {
memory: Some(PHP_MEMORY_LIMIT + 1),
..UpdateContainerOptions::default()
..ContainerUpdateBody::default()
},
)
.await
@ -749,9 +833,9 @@ impl Cloud {
docker
.update_container(
&self.id,
UpdateContainerOptions::<String> {
ContainerUpdateBody {
memory: Some(PHP_MEMORY_LIMIT),
..UpdateContainerOptions::default()
..ContainerUpdateBody::default()
},
)
.await
@ -777,20 +861,25 @@ impl Cloud {
format!("/var/www/html/{path}").into()
};
let mut mappings = config
.volume
.iter()
.map(Mapping::from)
.chain(default_mappings())
.collect::<Vec<_>>();
let mut mappings = for_config(config).collect::<Vec<_>>();
mappings.sort_by_key(|mapping| usize::MAX - mapping.target.as_str().len());
for mapping in mappings {
if let Some(rel_path) = path.strip_prefix(mapping.target.as_str()) {
let rel_path = rel_path.trim_matches('/');
return Some(mapping.source(&self.id, config).join(rel_path));
return Some(
mapping
.source(&self.id, config, &config.sources_root)
.join(rel_path),
);
}
}
None
}
}
impl PartialEq for Cloud {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}

View file

@ -4,15 +4,17 @@ use miette::{IntoDiagnostic, Report, Result, WrapErr};
use serde::Deserialize;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::env::var;
use std::env::home_dir;
use std::fs::read_to_string;
use std::net::IpAddr;
use toml::map::Map;
use toml::Value;
#[derive(Debug, Deserialize, Default)]
#[serde(from = "RawHazeConfig")]
pub struct HazeConfig {
pub sources_root: Utf8PathBuf,
pub app_directories: Vec<Utf8PathBuf>,
pub work_dir: Utf8PathBuf,
pub auto_setup: HazeAutoSetupConfig,
pub volume: Vec<HazeVolumeConfig>,
@ -27,6 +29,8 @@ pub struct RawHazeConfig {
#[serde(default = "default_work_dir")]
pub work_dir: Utf8PathBuf,
#[serde(default)]
pub app_directories: Vec<Utf8PathBuf>,
#[serde(default)]
pub auto_setup: HazeAutoSetupConfig,
#[serde(default)]
pub volume: Vec<HazeVolumeConfig>,
@ -42,7 +46,11 @@ impl From<RawHazeConfig> for HazeConfig {
fn from(raw: RawHazeConfig) -> Self {
fn normalize_path(path: Utf8PathBuf) -> Utf8PathBuf {
if path.starts_with("~") {
let home = var("HOME").expect("HOME not set");
let home = home_dir().expect("can't detect home directory");
let home = home
.into_os_string()
.into_string()
.expect("non-utf8 home directory");
format!("{}{}", home, &path.as_str()[1..]).into()
} else {
path
@ -51,6 +59,11 @@ impl From<RawHazeConfig> for HazeConfig {
HazeConfig {
sources_root: normalize_path(raw.sources_root),
app_directories: raw
.app_directories
.into_iter()
.map(normalize_path)
.collect(),
work_dir: normalize_path(raw.work_dir),
auto_setup: raw.auto_setup,
volume: raw.volume,
@ -75,6 +88,8 @@ pub struct HazeAutoSetupConfig {
pub disable_apps: Vec<String>,
#[serde(default)]
pub post_setup: Vec<String>,
#[serde(default)]
pub config: Map<String, Value>,
}
impl Default for HazeAutoSetupConfig {
@ -86,6 +101,7 @@ impl Default for HazeAutoSetupConfig {
enable_apps: Vec::default(),
disable_apps: Vec::default(),
post_setup: Vec::default(),
config: Map::default(),
}
}
}
@ -169,7 +185,7 @@ fn load_secret(name: &str, path: Option<String>, raw: Option<String>) -> Result<
}
}
#[derive(Default, Deserialize, Debug)]
#[derive(Default, Deserialize, Debug, Clone)]
pub struct ProxyConfig {
pub listen: String,
#[serde(default)]

View file

@ -1,7 +1,8 @@
use crate::exec::{exec, exec_tty, ExitCode};
use crate::image::pull_image;
use bollard::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{EndpointSettings, HostConfig};
use bollard::config::ContainerCreateBody;
use bollard::models::{EndpointSettings, HostConfig, NetworkingConfig};
use bollard::query_parameters::CreateContainerOptions;
use bollard::Docker;
use maplit::hashmap;
use miette::{IntoDiagnostic, Report, Result, WrapErr};
@ -193,39 +194,31 @@ impl Database {
.wrap_err("Failed to pull database image")?;
}
let options = Some(CreateContainerOptions {
name: format!("{}-db{}", cloud_id, postfix),
name: Some(format!("{}-db{}", cloud_id, postfix)),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(self.image()),
env: Some(self.env()),
let config = ContainerCreateBody {
image: Some(self.image().into()),
env: Some(self.env().into_iter().map(String::from).collect()),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
..Default::default()
}),
labels: Some(hashmap! {
"haze-type" => "db",
"haze-cloud-id" => cloud_id
"haze-type".into() => "db".into(),
"haze-cloud-id".into() => cloud_id.into()
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
String::from(network) => EndpointSettings {
aliases: Some(vec![
format!("{}{}", self.name(), postfix),
format!("db{}", postfix),
]),
..Default::default()
}
},
}),
cmd: if self.image() == "mysql:8" {
Some(vec![
"--default-authentication-plugin",
"mysql_native_password",
])
} else {
None
},
}),
..Default::default()
};
let id = docker
@ -233,10 +226,7 @@ impl Database {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(Some(id))
}

View file

@ -1,5 +1,5 @@
use bollard::container::LogsOptions;
use bollard::exec::{CreateExecOptions, ResizeExecOptions, StartExecResults};
use bollard::query_parameters::LogsOptions;
use bollard::Docker;
use futures_util::StreamExt;
use miette::{IntoDiagnostic, Report, Result, WrapErr};
@ -189,7 +189,7 @@ pub async fn container_logs(
count: usize,
follow: bool,
) -> Result<()> {
let mut stream = docker.logs::<String>(
let mut stream = docker.logs(
container,
Some(LogsOptions {
stdout: true,

View file

@ -1,22 +1,33 @@
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 rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use rayon::ThreadPoolBuilder;
use std::fs::read_dir;
use std::path::{Path, PathBuf};
use std::iter::once;
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
fn find_app_repos(root: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>> {
let apps_dir = root.as_ref().join("apps");
Ok(read_dir(apps_dir)
.into_diagnostic()?
fn find_app_repos(config: &HazeConfig) -> Result<impl Iterator<Item = PathBuf>> {
let apps_dirs = once(config.sources_root.as_path().join("apps"))
.chain(config.app_directories.iter().cloned());
let dir_handles = apps_dirs
.map(|dir| read_dir(dir).into_diagnostic())
.collect::<Result<Vec<_>>>()?;
Ok(dir_handles
.into_iter()
.flatten()
.flatten()
.filter(|app| app.path().join(".git").is_dir())
.map(|app| app.path()))
}
fn longest_app_branch(root: impl AsRef<Path>) -> Result<(usize, usize)> {
Ok(find_app_repos(root)?
fn longest_app_branch(config: &HazeConfig) -> Result<(usize, usize)> {
Ok(find_app_repos(config)?
.filter_map(|app_dir| {
let app_name = app_dir.file_name()?.to_str()?;
let repo = Repository::init(&app_dir).ok()?;
@ -27,27 +38,32 @@ fn longest_app_branch(root: impl AsRef<Path>) -> Result<(usize, usize)> {
.unwrap_or_default())
}
pub fn checkout_all<P: AsRef<Path>>(sources_root: P, mut name: &str) -> Result<()> {
pub fn checkout_all(config: &HazeConfig, mut name: &str, verbose: bool) -> Result<()> {
// "main" and "master" are interchangeable
if name == "main" {
name = "master";
}
for app_dir in find_app_repos(sources_root)? {
for app_dir in find_app_repos(config)? {
let repo = Repository::init(&app_dir)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to open repository {}", app_dir.display()))?;
let app_name = app_dir.file_name().unwrap().to_string_lossy();
if let Some(branch) = get_branch(&repo, name)? {
if !branch.is_head() {
let is_remote = branch.get().is_remote();
print!("{}", app_dir.file_name().unwrap().to_string_lossy());
print!("{app_name}");
if let Err(e) = checkout(&repo, &branch, is_remote.then_some(name)) {
println!(": {:#}", e);
} else {
println!("");
}
} else if verbose {
println!("{app_name} -");
}
} else if verbose {
println!("{app_name} 🛇 Branch not found");
};
}
Ok(())
@ -68,42 +84,69 @@ const GIT_BINARY: &str = match option_env!("GIT_BINARY") {
None => "git",
};
pub fn pull_all<P: AsRef<Path>>(sources_root: P) -> Result<()> {
let sources_root = sources_root.as_ref();
let (max_app, max_branch) = longest_app_branch(sources_root)?;
pub fn pull_all(config: &HazeConfig) -> Result<()> {
let (max_app, max_branch) = longest_app_branch(config)?;
for app_dir in find_app_repos(sources_root)? {
let progress = MultiProgress::new();
let pull_style = ProgressStyle::with_template("{spinner:.green} {msg}").unwrap();
let pool = ThreadPoolBuilder::new()
.num_threads(8)
.build()
.into_diagnostic()?;
let repos = find_app_repos(config)?.collect::<Vec<_>>();
pool.install(|| {
repos.par_iter().for_each(|app_dir| {
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 Ok(repo) = Repository::init(app_dir) else {
return;
};
let branch_name = current_branch_name(&repo).unwrap_or("unknown".into());
print!(
"{app_name:<app_width$} - {branch_name:<branch_width$}",
let bar = ProgressBar::new_spinner().with_style(pull_style.clone());
bar.enable_steady_tick(Duration::from_millis(100));
let bar = progress.add(bar);
let msg = |state: &str| {
format!(
"{app_name:<app_width$} - {branch_name:<branch_width$}{state}",
app_width = max_app,
branch_width = max_branch
);
)
};
if repo.state() != RepositoryState::Clean {
println!(": repository not clean ❌");
continue;
bar.set_message(msg(" repository not clean"));
bar.finish();
return;
}
let output = Command::new(GIT_BINARY)
bar.set_message(msg(""));
let output = match Command::new(GIT_BINARY)
.arg("pull")
.current_dir(&app_dir)
.current_dir(app_dir)
.output()
.into_diagnostic()
.wrap_err_with(|| format!("Failed to run git pull for {}", app_dir.display()))?;
{
Ok(output) => output,
Err(error) => {
bar.set_message(msg(&format!(" {error}")));
return;
}
};
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(())
}

View file

@ -88,6 +88,7 @@ fn subcommand_help(command: &dyn SubCommand) {
print!(" {}", "[php version]".green());
print!(" {}", "[database type]".green());
print!(" {}", "[services]".green());
print!(" {}", "[vX.Y.Z]".green());
}
let args = if let Some(args) = command.get_str("Args") {

View file

@ -1,17 +1,52 @@
use bollard::image::CreateImageOptions;
use bollard::models::CreateImageInfo;
use bollard::query_parameters::CreateImageOptions;
use bollard::Docker;
use futures_util::StreamExt;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use miette::{IntoDiagnostic, Result, WrapErr};
use std::collections::HashMap;
use std::io::stdout;
use std::io::Write;
use termion::cursor;
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<Self, Self::Err> {
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<ImageVersion> {
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?;
@ -32,9 +67,9 @@ pub async fn force_pull_image(docker: &Docker, image: &str) -> Result<()> {
let mut info_stream = docker.create_image(
Some(CreateImageOptions {
from_image: if image.contains(':') {
image.to_string()
Some(image.to_string())
} else {
format!("{}:latest", image)
Some(format!("{}:latest", image))
},
..Default::default()
}),
@ -42,36 +77,34 @@ pub async fn force_pull_image(docker: &Docker, image: &str) -> Result<()> {
None,
);
let mut bars: HashMap<String, u16> = HashMap::new();
let bar_style = ProgressStyle::with_template(
"{spinner:.green} {msg} [{elapsed_precise}] [{bar:.cyan/blue}] {bytes:>12}/{total_bytes}",
)
.unwrap();
let mut bars: HashMap<String, ProgressBar> = HashMap::new();
let mp = MultiProgress::new();
let mut stdout = stdout();
while let Some(info) = info_stream.next().await {
let info: CreateImageInfo = info
.into_diagnostic()
.wrap_err_with(|| format!("Error while pulling image {}", image))?;
if let (Some(id), Some(status), Some(progress)) = (info.id, info.status, info.progress) {
match bars.get(&id) {
Some(pos) => {
let offset = bars.len() as u16 - pos;
write!(
stdout,
"{}{}{} - {:12} {}{}",
cursor::Save,
cursor::Up(offset),
id,
status,
progress,
cursor::Restore
)
.into_diagnostic()?;
if let (Some(id), Some(status), Some(progress)) =
(info.id, info.status, info.progress_detail)
{
let bar = bars.entry(id.clone()).or_insert_with(|| {
let bar = ProgressBar::new(progress.total.unwrap_or_default() as u64)
.with_style(bar_style.clone())
.with_message(format!("{id:20} - {status:10}"));
mp.add(bar)
});
bar.set_message(format!("{id:10} - {status:20}"));
if let Some(total) = progress.total {
bar.set_length(total as u64);
}
None => {
writeln!(stdout, "{} - {:12} {}", id, status, progress).into_diagnostic()?;
bars.insert(id, bars.len() as u16);
if let Some(current) = progress.current {
bar.set_position(current as u64);
}
}
stdout.flush().into_diagnostic()?;
}
}
Ok(())
}

View file

@ -103,13 +103,18 @@ async fn main() -> Result<ExitCode> {
services.push(cloud.db().name());
let services = services.join(", ");
let pin = if cloud.pinned { "*" } else { "" };
let version = match cloud.options.version.as_ref() {
Some(version) => format!(", v{version}"),
None => String::new(),
};
println!(
"Cloud {}{}, {}, {}, running on {}",
"Cloud {}{}, {}, {}{}, running on {}",
cloud.id,
pin,
cloud.php().name(),
services,
cloud.address
version,
cloud.address,
);
}
}
@ -143,6 +148,9 @@ async fn main() -> Result<ExitCode> {
root,
} => {
let cloud = Cloud::get_by_filter(&docker, filter, &config).await?;
let env = get_forward_env();
let tty = atty::is(atty::Stream::Stdout);
match service {
None => {
let command = if command.is_empty() {
@ -150,8 +158,7 @@ async fn main() -> Result<ExitCode> {
} else {
command
};
let env = get_forward_env();
let tty = atty::is(atty::Stream::Stdout);
let user = if root { "root" } else { "haze" };
if tty {
exec_tty(&docker, &cloud.id, user, command, env).await?;
@ -160,19 +167,42 @@ async fn main() -> Result<ExitCode> {
}
}
Some(ExecService::Db) => {
cloud
.db()
.exec_sh(
&docker,
&cloud.id,
if command.is_empty() {
let command = if command.is_empty() {
vec!["bash".to_string()]
} else {
command
},
atty::is(atty::Stream::Stdout),
)
.await?;
};
cloud.db().exec_sh(&docker, &cloud.id, command, tty).await?;
}
Some(ExecService::Service(service)) => {
let Some(container) = service.container_name(&cloud.id) else {
eprintln!(
"Service {} can't be exec'ed as it has no associated container",
service.name()
);
return Ok(ExitCode::FAILURE);
};
if service.exec_shell() == "" && command.is_empty() {
eprintln!(
"Service {} can't be exec'ed as it has no shell",
service.name()
);
return Ok(ExitCode::FAILURE);
}
let command = if command.is_empty() {
vec![service.exec_shell().to_string()]
} else {
command
};
let user = if root { "root" } else { service.exec_user() };
if tty {
exec_tty(&docker, &container, user, command, env).await?;
} else {
exec(&docker, &container, user, command, env, Some(stdout())).await?;
}
}
}
}
@ -269,26 +299,8 @@ async fn main() -> Result<ExitCode> {
return Ok(result.into());
}
HazeArgs::Integration { options, mut args } => {
let cloud = Cloud::create(&docker, options, &config).await?;
println!("Waiting for servers to start");
cloud.wait_for_start(&docker).await?;
println!("Installing");
if let Err(e) = cloud
.exec(
&docker,
vec![
"install",
&config.auto_setup.username,
&config.auto_setup.password,
],
false,
Vec::<String>::default(),
)
.await
{
cloud.destroy(&docker).await?;
return Err(e);
}
let cloud = setup(&docker, options, &config).await?;
args.insert(0, "integration".to_string());
cloud.exec(&docker, args, false, get_forward_env()).await?;
cloud.destroy(&docker).await?;
@ -386,15 +398,16 @@ async fn main() -> Result<ExitCode> {
proxy(docker, config).await?;
}
HazeArgs::Git { operation } => match operation {
GitOperation::Checkout { branch } => {
GitOperation::Checkout { branch, verbose } => {
let sources = Sources::new(&config.sources_root)?;
checkout_all(
&config.sources_root,
&config,
&branch.unwrap_or_else(|| sources.get_server_version_branch()),
verbose,
)?;
}
GitOperation::Pull => {
pull_all(&config.sources_root)?;
pull_all(&config)?;
}
},
HazeArgs::Env {
@ -433,16 +446,16 @@ async fn main() -> Result<ExitCode> {
);
if cloud.services().contains(&Service::RedisTls(RedisTls)) {
create_dir_all(config.work_dir.join("redis_certificates"))
create_dir_all(config.work_dir.join("certificates/redis"))
.into_diagnostic()
.wrap_err("Failed to create redis certificate directory")?;
let redis_cert_path = config.work_dir.join("redis_certificates/client.cert");
let redis_key_path = config.work_dir.join("redis_certificates/client.key");
let redis_ca_path = config.work_dir.join("redis_certificates/ca.cert");
let redis_cert_path = config.work_dir.join("certificates/redis/client.cert");
let redis_key_path = config.work_dir.join("certificates/redis/client.key");
let redis_ca_path = config.work_dir.join("certificates/redis/ca.cert");
if !redis_cert_path.exists() {
write(
&redis_cert_path,
include_bytes!("../redis-certificates/client.crt"),
include_bytes!("../certificates/redis/client.crt"),
)
.into_diagnostic()
.wrap_err("Failed to write redis client certificate")?;
@ -450,7 +463,7 @@ async fn main() -> Result<ExitCode> {
if !redis_key_path.exists() {
write(
&redis_key_path,
include_bytes!("../redis-certificates/client.key"),
include_bytes!("../certificates/redis/client.key"),
)
.into_diagnostic()
.wrap_err("Failed to write redis client key")?;
@ -458,7 +471,7 @@ async fn main() -> Result<ExitCode> {
if !redis_ca_path.exists() {
write(
&redis_ca_path,
include_bytes!("../redis-certificates/ca.crt"),
include_bytes!("../certificates/redis/ca.crt"),
)
.into_diagnostic()
.wrap_err("Failed to write redis ca certificate")?;
@ -645,12 +658,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::<String>::default(),
)
.exec(docker, cmd, false, Vec::<String>::default())
.await?;
}
}

View file

@ -1,13 +1,14 @@
use crate::config::{HazeConfig, HazeVolumeConfig};
use camino::{Utf8Path, Utf8PathBuf};
use miette::{IntoDiagnostic, Result};
use std::borrow::Cow;
use tokio::fs::{create_dir_all, write};
#[derive(Debug)]
pub struct Mapping<'a> {
source_type: MappingSourceType,
pub source: &'a Utf8Path,
pub target: &'a Utf8Path,
pub source: Cow<'a, Utf8Path>,
pub target: Cow<'a, Utf8Path>,
mapping_type: MappingType,
read_only: bool,
map: bool,
@ -23,6 +24,26 @@ impl<'a> Mapping<'a> {
where
Target: Into<&'a Utf8Path>,
Source: Into<&'a Utf8Path>,
{
Mapping {
source_type,
source: Cow::Borrowed(source.into()),
target: Cow::Borrowed(target.into()),
mapping_type: MappingType::Folder,
read_only: false,
map: true,
create: true,
}
}
pub fn owned<Source, Target>(
source_type: MappingSourceType,
source: Source,
target: Target,
) -> Self
where
Target: Into<Cow<'a, Utf8Path>>,
Source: Into<Cow<'a, Utf8Path>>,
{
Mapping {
source_type,
@ -65,10 +86,10 @@ impl<'a> Mapping<'a> {
return Ok(());
}
let source = match self.source_type {
MappingSourceType::WorkDir => config.work_dir.join(id).join(self.source),
MappingSourceType::GlobalWorkDir => config.work_dir.join(self.source),
MappingSourceType::WorkDir => config.work_dir.join(id).join(self.source.as_ref()),
MappingSourceType::GlobalWorkDir => config.work_dir.join(self.source.as_ref()),
MappingSourceType::Sources => return Ok(()),
MappingSourceType::Absolute => self.source.into(),
MappingSourceType::Absolute => self.source.as_ref().into(),
};
match self.mapping_type {
MappingType::Folder => create_dir_all(source).await.into_diagnostic()?,
@ -78,20 +99,25 @@ impl<'a> Mapping<'a> {
Ok(())
}
pub fn source(&self, id: &str, config: &HazeConfig) -> Utf8PathBuf {
pub fn source(&self, id: &str, config: &HazeConfig, source_root: &Utf8Path) -> Utf8PathBuf {
match self.source_type {
MappingSourceType::WorkDir => config.work_dir.join(id).join(self.source),
MappingSourceType::GlobalWorkDir => config.work_dir.join(self.source),
MappingSourceType::Sources => config.sources_root.join(self.source),
MappingSourceType::Absolute => self.source.into(),
MappingSourceType::WorkDir => config.work_dir.join(id).join(self.source.as_ref()),
MappingSourceType::GlobalWorkDir => config.work_dir.join(self.source.as_ref()),
MappingSourceType::Sources => source_root.join(self.source.as_ref()),
MappingSourceType::Absolute => self.source.as_ref().into(),
}
}
pub fn get_volume_arg(&self, id: &str, config: &HazeConfig) -> Option<String> {
pub fn get_volume_arg(
&self,
id: &str,
config: &HazeConfig,
source_root: &Utf8Path,
) -> Option<String> {
if !self.map {
return None;
}
let source = self.source(id, config);
let source = self.source(id, config, source_root);
Some(if self.read_only {
format!("{}:{}:ro", source, self.target)
} else {
@ -107,6 +133,7 @@ pub fn default_mappings<'a>() -> impl IntoIterator<Item = Mapping<'a>> {
Mapping::new(Sources, "", "/var/www/html"),
Mapping::new(WorkDir, "data", "/var/www/html/data"),
Mapping::new(WorkDir, "config", "/var/www/html/config"),
Mapping::new(WorkDir, "store_apps", "/var/www/store_apps"),
Mapping::new(WorkDir, "data-autotest", "/var/www/html/data-autotest"),
Mapping::new(WorkDir, "skeleton", "/var/www/html/core/skeleton"),
Mapping::new(
@ -161,11 +188,32 @@ pub fn default_mappings<'a>() -> impl IntoIterator<Item = Mapping<'a>> {
.dont_create(),
Mapping::new(WorkDir, "xdebug", "/tmp/xdebug"),
Mapping::new(WorkDir, "profiling", "/tmp/profiling"),
Mapping::new(WorkDir, "php.ini", "/php.ini").file(),
Mapping::new(WorkDir, "php-config", "/config"),
];
IntoIterator::into_iter(mappings)
}
pub fn for_config<'a>(config: &'a HazeConfig) -> impl Iterator<Item = Mapping<'a>> {
let app_dir_mappings = config.app_directories.iter().map(|dir| {
Mapping::owned(
MappingSourceType::Absolute,
dir.as_path(),
Cow::Owned(Utf8PathBuf::from(format!(
"/var/www/html/{}",
dir.file_name().unwrap()
))),
)
});
config
.volume
.iter()
.map(Mapping::from)
.chain(app_dir_mappings)
.chain(default_mappings())
}
#[derive(Debug, Copy, Clone)]
pub enum MappingSourceType {
Sources,
@ -189,8 +237,8 @@ impl<'a> From<&'a HazeVolumeConfig> for Mapping<'a> {
};
Mapping {
source_type: MappingSourceType::Absolute,
source: config.source.as_path(),
target: config.target.as_path(),
source: Cow::Borrowed(config.source.as_path()),
target: Cow::Borrowed(config.target.as_path()),
mapping_type: ty,
read_only: config.read_only,
map: true,

View file

@ -1,11 +1,11 @@
use crate::cloud::Cloud;
use bollard::network::CreateNetworkOptions;
use bollard::config::NetworkCreateRequest;
use bollard::Docker;
use miette::{IntoDiagnostic, Result, WrapErr};
pub async fn clear_networks(docker: &Docker, instances: &[Cloud]) -> Result<()> {
let networks = docker
.list_networks::<&str>(None)
.list_networks(None)
.await
.into_diagnostic()
.wrap_err("Failed to list docker networks")?;
@ -23,7 +23,7 @@ pub async fn clear_networks(docker: &Docker, instances: &[Cloud]) -> Result<()>
async fn get_network_id(docker: &Docker, name: &str) -> Result<Option<String>> {
let networks = docker
.list_networks::<&str>(None)
.list_networks(None)
.await
.into_diagnostic()
.wrap_err("Failed to list docker networks")?;
@ -41,9 +41,8 @@ pub async fn ensure_network_exists(docker: &Docker, name: &str) -> Result<String
Ok(id)
} else {
Ok(docker
.create_network(CreateNetworkOptions {
name,
check_duplicate: true,
.create_network(NetworkCreateRequest {
name: name.into(),
..Default::default()
})
.await

View file

@ -1,12 +1,13 @@
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;
use bollard::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::config::{ContainerCreateBody, NetworkConnectRequest, NetworkingConfig};
use bollard::models::{EndpointSettings, HostConfig};
use bollard::network::ConnectNetworkOptions;
use bollard::query_parameters::CreateContainerOptions;
use bollard::Docker;
use itertools::Itertools;
use maplit::hashmap;
@ -21,20 +22,12 @@ use tokio::time::{sleep, timeout};
#[allow(dead_code)]
pub enum PhpVersion {
#[default]
Php85,
Php84,
Php83,
Php82,
Php81,
Php80,
Php74,
Php73,
Php84Dbg,
Php83Dbg,
Php82Dbg,
Php81Dbg,
Php80Dbg,
Php74Dbg,
Php73Dbg,
}
pub const PHP_MEMORY_LIMIT: i64 = 2 * 1024 * 1024 * 1024;
@ -44,24 +37,13 @@ impl FromStr for PhpVersion {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"7" => Ok(PhpVersion::Php74),
"7.3" => Ok(PhpVersion::Php73),
"7.4" => Ok(PhpVersion::Php74),
"8" => Ok(PhpVersion::Php81),
"8" => Ok(PhpVersion::Php80),
"8.0" => Ok(PhpVersion::Php80),
"8.1" => Ok(PhpVersion::Php81),
"8.2" => Ok(PhpVersion::Php82),
"8.3" => Ok(PhpVersion::Php83),
"8.4" => Ok(PhpVersion::Php84),
"7-dbg" => Ok(PhpVersion::Php74Dbg),
"7.3-dbg" => Ok(PhpVersion::Php73Dbg),
"7.4-dbg" => Ok(PhpVersion::Php74Dbg),
"8-dbg" => Ok(PhpVersion::Php80Dbg),
"8.0-dbg" => Ok(PhpVersion::Php80Dbg),
"8.1-dbg" => Ok(PhpVersion::Php81Dbg),
"8.2-dbg" => Ok(PhpVersion::Php82Dbg),
"8.3-dbg" => Ok(PhpVersion::Php83Dbg),
"8.4-dbg" => Ok(PhpVersion::Php84Dbg),
"8.5" => Ok(PhpVersion::Php85),
_ => Err(()),
}
}
@ -73,53 +55,35 @@ impl PhpVersion {
}
pub fn image(&self) -> &'static str {
// for now only 7.4
match self {
PhpVersion::Php73 => "icewind1991/haze:7.3",
PhpVersion::Php74 => "icewind1991/haze:7.4",
PhpVersion::Php80 => "icewind1991/haze:8.0",
PhpVersion::Php81 => "icewind1991/haze:8.1",
PhpVersion::Php82 => "icewind1991/haze:8.2",
PhpVersion::Php83 => "icewind1991/haze:8.3",
PhpVersion::Php84 => "icewind1991/haze:8.4",
PhpVersion::Php73Dbg => "icewind1991/haze:7.3-dbg",
PhpVersion::Php74Dbg => "icewind1991/haze:7.4-dbg",
PhpVersion::Php80Dbg => "icewind1991/haze:8.0-dbg",
PhpVersion::Php81Dbg => "icewind1991/haze:8.1-dbg",
PhpVersion::Php82Dbg => "icewind1991/haze:8.2-dbg",
PhpVersion::Php83Dbg => "icewind1991/haze:8.3-dbg",
PhpVersion::Php84Dbg => "icewind1991/haze:8.4-dbg",
PhpVersion::Php85 => "icewind1991/haze:8.5",
}
}
pub fn name(&self) -> &'static str {
match self {
PhpVersion::Php73 => "7.3",
PhpVersion::Php74 => "7.4",
PhpVersion::Php80 => "8.0",
PhpVersion::Php81 => "8.1",
PhpVersion::Php82 => "8.2",
PhpVersion::Php83 => "8.3",
PhpVersion::Php84 => "8.4",
PhpVersion::Php73Dbg => "7.3-dbg",
PhpVersion::Php74Dbg => "7.4-dbg",
PhpVersion::Php80Dbg => "8.0-dbg",
PhpVersion::Php81Dbg => "8.1-dbg",
PhpVersion::Php82Dbg => "8.2-dbg",
PhpVersion::Php83Dbg => "8.3-dbg",
PhpVersion::Php84Dbg => "8.4-dbg",
PhpVersion::Php85 => "8.4",
}
}
pub fn from_number(major: u8, minor: u8) -> Option<Self> {
match (major, minor) {
(7, 3) => Some(PhpVersion::Php73),
(7, 4) => Some(PhpVersion::Php74),
(8, 0) => Some(PhpVersion::Php80),
(8, 1) => Some(PhpVersion::Php81),
(8, 2) => Some(PhpVersion::Php82),
(8, 3) => Some(PhpVersion::Php83),
(8, 4) => Some(PhpVersion::Php84),
(8, 5) => Some(PhpVersion::Php85),
_ => None,
}
}
@ -127,27 +91,19 @@ impl PhpVersion {
pub fn max_minor(major: u8) -> u8 {
match major {
7 => 4,
8 => 4,
8 => 5,
_ => 0,
}
}
pub fn all() -> impl Iterator<Item = Self> {
[
PhpVersion::Php73,
PhpVersion::Php74,
PhpVersion::Php80,
PhpVersion::Php81,
PhpVersion::Php82,
PhpVersion::Php83,
PhpVersion::Php84,
PhpVersion::Php73Dbg,
PhpVersion::Php74Dbg,
PhpVersion::Php80Dbg,
PhpVersion::Php81Dbg,
PhpVersion::Php82Dbg,
PhpVersion::Php83Dbg,
PhpVersion::Php84Dbg,
PhpVersion::Php85,
]
.into_iter()
}
@ -164,11 +120,23 @@ impl PhpVersion {
host: &str,
services: &[Service],
proxy_config: &ProxyConfig,
version: Option<&str>,
) -> Result<String> {
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: id.to_string(),
name: Some(id.to_string()),
..CreateContainerOptions::default()
});
let clean_id = id.strip_prefix("haze-").unwrap_or(id);
@ -192,7 +160,23 @@ impl PhpVersion {
proxy_config.addr(id, IpAddr::V4(Ipv4Addr::LOCALHOST))
));
let config = Config {
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(),
"haze-php".to_string() => self.name().to_string(),
"haze-cloud-id".to_string() => id.to_string(),
"haze-services".to_string() => services.iter().map(|s| s.name()).join(","),
};
if let Some(version) = version {
labels.insert("haze-version".to_string(), version.to_string());
}
let config = ContainerCreateBody {
image: Some(self.image().to_string()),
env: Some(env),
host_config: Some(HostConfig {
@ -204,20 +188,14 @@ impl PhpVersion {
..Default::default()
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
endpoints_config: Some(hashmap! {
network.to_string() => EndpointSettings {
aliases: Some(vec!["cloud".to_string()]),
..Default::default()
}
},
}),
labels: Some(hashmap! {
"haze-type".to_string() => "cloud".to_string(),
"haze-db".to_string() => db.name().to_string(),
"haze-php".to_string() => self.name().to_string(),
"haze-cloud-id".to_string() => id.to_string(),
"haze-services".to_string() => services.iter().map(|s| s.name()).join(","),
}),
labels: Some(labels),
..Default::default()
};
@ -227,11 +205,7 @@ impl PhpVersion {
.into_diagnostic()?
.id;
if let Err(e) = docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()
{
if let Err(e) = docker.start_container(&id, None).await.into_diagnostic() {
docker.remove_container(&id, None).await.ok();
return Err(e);
}
@ -239,12 +213,12 @@ impl PhpVersion {
if let Err(e) = docker
.connect_network(
"haze",
ConnectNetworkOptions {
container: id.as_str(),
endpoint_config: EndpointSettings {
NetworkConnectRequest {
container: id.to_string(),
endpoint_config: Some(EndpointSettings {
aliases: Some(vec![id.to_string()]),
..Default::default()
},
}),
},
)
.await

View file

@ -1,34 +1,43 @@
use crate::service::ServiceTrait;
use crate::service::{ServiceTrait, ServiceType};
use crate::Result;
use crate::{Cloud, HazeConfig};
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 {
known: Mutex<HashMap<String, SocketAddr>>,
last: Mutex<Option<SocketAddr>>,
last: Mutex<Option<Cloud>>,
docker: Docker,
config: HazeConfig,
}
@ -48,9 +57,29 @@ impl ActiveInstances {
return Some(ip);
}
// service proxy
let addr = if name.matches('-').count() == 2 {
let (name, service_name) = name.rsplit_once('-').unwrap();
let addr = if ServiceType::from_str(name).is_ok() {
let cloud = self.last()?;
let service = cloud.services().find(|service| service.name() == name)?;
let ip = service
.get_ips(&self.docker, &cloud.id)
.await
.ok()?
.next()?;
SocketAddr::new(ip, service.proxy_port())
} else {
match name.matches('-').count() {
// instance
1 => SocketAddr::new(
Cloud::get_by_filter(&self.docker, Some(name.into()), &self.config)
.await
.ok()?
.ip?,
80,
),
// service with instance
2.. => {
let service_name = name.splitn(3, '-').last()?;
let name = &name[0..(name.len() - service_name.len() - 1)];
let cloud = Cloud::get_by_filter(&self.docker, Some(name.into()), &self.config)
.await
.ok()?;
@ -63,14 +92,9 @@ impl ActiveInstances {
.ok()?
.next()?;
SocketAddr::new(ip, service.proxy_port())
} else {
SocketAddr::new(
Cloud::get_by_filter(&self.docker, Some(name.into()), &self.config)
.await
.ok()?
.ip?,
80,
)
}
_ => return None,
}
};
println!("{name} => {addr}");
@ -79,18 +103,31 @@ impl ActiveInstances {
Some(addr)
}
pub fn last(&self) -> Option<SocketAddr> {
*self.last.lock().unwrap()
pub fn last_addr(&self) -> Option<SocketAddr> {
self.last
.lock()
.unwrap()
.as_ref()
.and_then(|cloud| Some(SocketAddr::new(cloud.ip?, 80)))
}
pub fn last(&self) -> Option<Cloud> {
self.last.lock().unwrap().clone()
}
async fn update_last(&self) {
let last = Cloud::get_by_filter(&self.docker, None, &self.config)
.await
.ok()
.and_then(|cloud| Some(SocketAddr::new(cloud.ip?, 80)));
.ok();
let mut old = self.last.lock().unwrap();
if old.as_ref() != last.as_ref() {
info!(instance = ?last, "Found new instance");
// remove cached base-service mappings
self.known
.lock()
.unwrap()
.retain(|key, _| ServiceType::from_str(key).is_err());
*old = last;
}
}
@ -134,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() {
@ -158,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<I: AsyncRead + AsyncWrite + Unpin + Send + 'static>(
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,
@ -181,7 +248,7 @@ async fn get_remote(
};
let ip = if host == base_address {
instances
.last()
.last_addr()
.ok_or_else(|| String::from("No running instance known"))
} else {
let requested_instance = host.split('.').next().unwrap();
@ -203,9 +270,9 @@ async fn get_remote(
}
}
type Client = hyper_util::client::legacy::Client<HttpConnector, Body>;
type Client = hyper_util::client::legacy::Client<HttpConnector, Incoming>;
async fn handler(State(state): State<AppState>, mut req: Request) -> Result<Response, StatusCode> {
async fn handler(state: AppState, mut req: Request<Incoming>) -> Result<Response, Infallible> {
let host = req.headers().get(HOST).cloned();
let remote = match get_remote(host.as_ref(), &state.instances, &state.base_address).await {
Ok(remote) => remote,
@ -230,13 +297,13 @@ async fn handler(State(state): State<AppState>, mut req: Request) -> Result<Resp
IpAddr::V4(Ipv4Addr::UNSPECIFIED),
&uri,
req,
&state.proxy_client,
state.proxy_client.as_ref(),
)
.await
{
Ok(response) => 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())
}
}

View file

@ -14,9 +14,10 @@ mod sftp;
mod redis;
mod sharded;
mod smb;
mod webhook;
use crate::cloud::CloudOptions;
use crate::config::{HazeConfig, Preset};
use crate::config::{HazeConfig, Preset, ProxyConfig};
pub use crate::service::clam::{Clam, ClamIcap, ClamIcapTls, ClamSocket};
use crate::service::dav::Dav;
use crate::service::imaginary::Imaginary;
@ -29,9 +30,10 @@ 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;
use bollard::models::ContainerState;
use bollard::Docker;
use enum_dispatch::enum_dispatch;
@ -78,7 +80,12 @@ pub trait ServiceTrait {
None
}
async fn start_message(&self, _docker: &Docker, _cloud_id: &str) -> Result<Option<String>> {
async fn start_message(
&self,
_docker: &Docker,
_cloud_id: &str,
_proxy: &ProxyConfig,
) -> Result<Option<String>> {
Ok(None)
}
@ -95,12 +102,21 @@ pub trait ServiceTrait {
Ok(HashMap::default())
}
fn pre_setup(
&self,
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<Vec<String>>> {
Ok(Vec::new())
}
async fn post_setup(
&self,
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
Ok(Vec::new())
}
@ -142,7 +158,7 @@ pub trait ServiceTrait {
return Ok(Box::new(empty()));
};
docker
.start_container::<String>(&container, None)
.start_container(&container, None)
.await
.into_diagnostic()?;
self.wait_for_running(docker, cloud_id).await?;
@ -178,6 +194,14 @@ pub trait ServiceTrait {
fn proxy_port(&self) -> u16 {
80
}
fn exec_user(&self) -> &'static str {
"root"
}
fn exec_shell(&self) -> &'static str {
"bash"
}
}
#[derive(Clone, Eq, PartialEq, Debug)]
@ -193,6 +217,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,
)]
@ -200,6 +237,8 @@ impl ServiceTrait for RedisTls {
pub enum ServiceType {
/// S3 Primary storage and external storage
S3,
/// S3 Primary storage with TLS
S3s,
/// S3 multi-object store Primary storage and external storage
S3m,
/// S3 multi-bucket Primary storage and external storage
@ -208,7 +247,10 @@ pub enum ServiceType {
Azure,
/// Ldap user backend
Ldap,
/// Ldap admin interface
LdapAdmin,
/// OnlyOffice
#[strum(serialize = "onlyoffice", serialize = "only-office")]
OnlyOffice,
/// Libre office online
Office,
@ -234,6 +276,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
@ -263,6 +307,10 @@ pub enum ServiceType {
Redis,
/// External redis instance with TLS
RedisTls,
/// Use FrankenPHP instead of PHP-FPM
FrankenPhp,
/// Webhook test listener
Webhook,
}
#[enum_dispatch]
@ -281,6 +329,7 @@ pub enum Service {
ShardingMigrate(ShardingMigrate),
ShardingMigrateUnset(ShardingMigrateUnset),
Sftp(Sftp),
SftpKey(SftpKey),
Kaspersky(Kaspersky),
KasperskyIcap(KasperskyIcap),
Clam(Clam),
@ -292,6 +341,8 @@ pub enum Service {
Mail(Mail),
Redis(Redis),
RedisTls(RedisTls),
FrankenPhp(FrankenPhp),
Webhook(Webhook),
Preset(PresetService),
}
@ -300,10 +351,14 @@ impl Service {
if let Ok(ty) = ServiceType::from_str(ty) {
match ty {
ServiceType::S3 => Some(vec![Service::ObjectStore(ObjectStore::S3)]),
ServiceType::S3s => Some(vec![Service::ObjectStore(ObjectStore::S3s)]),
ServiceType::S3m => Some(vec![Service::ObjectStore(ObjectStore::S3m)]),
ServiceType::S3mb => Some(vec![Service::ObjectStore(ObjectStore::S3mb)]),
ServiceType::Azure => Some(vec![Service::ObjectStore(ObjectStore::Azure)]),
ServiceType::Ldap => Some(vec![Service::Ldap(Ldap), Service::LdapAdmin(LdapAdmin)]),
ServiceType::LdapAdmin => {
Some(vec![Service::Ldap(Ldap), Service::LdapAdmin(LdapAdmin)])
}
ServiceType::OnlyOffice => Some(vec![Service::OnlyOffice(OnlyOffice)]),
ServiceType::Office => Some(vec![Service::Office(Office)]),
ServiceType::Push => Some(vec![Service::Push(NotifyPush)]),
@ -318,6 +373,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)]),
@ -330,6 +386,8 @@ 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)]),
ServiceType::Webhook => Some(vec![Service::Webhook(Webhook)]),
}
} else {
presets
@ -392,15 +450,29 @@ impl ServiceTrait for PresetService {
_docker: &Docker,
_cloud_id: &str,
config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
let preset =
get_preset(&config.preset, &self.0).ok_or_else(|| Report::msg("invalid preset"))?;
let mut commands: Vec<_> = preset
.apps
.iter()
.map(|app| format!("occ app:enable {app} --force"))
.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<String> {
s.split(' ').map(String::from).collect()
}

View file

@ -2,10 +2,10 @@ 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::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{EndpointSettings, HostConfig};
use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig};
use bollard::query_parameters::CreateContainerOptions;
use bollard::Docker;
use maplit::hashmap;
use miette::{IntoDiagnostic, WrapErr};
@ -40,26 +40,26 @@ impl ServiceTrait for ClamIcap {
let image = "ghcr.io/icewind1991/icap-clamav-service-tls";
pull_image(docker, image).await?;
let options = Some(CreateContainerOptions {
name: self.container_name(cloud_id).unwrap(),
name: self.container_name(cloud_id),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(image),
let config = ContainerCreateBody {
image: Some(image.into()),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
..Default::default()
}),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into()
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
}),
..Default::default()
};
@ -68,10 +68,7 @@ impl ServiceTrait for ClamIcap {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -88,14 +85,13 @@ impl ServiceTrait for ClamIcap {
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
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"),
])
}
}
@ -129,26 +125,26 @@ impl ServiceTrait for ClamIcapTls {
let image = "ghcr.io/icewind1991/icap-clamav-service-tls";
pull_image(docker, image).await?;
let options = Some(CreateContainerOptions {
name: self.container_name(cloud_id).unwrap(),
name: self.container_name(cloud_id),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(image),
let config = ContainerCreateBody {
image: Some(image.into()),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
..Default::default()
}),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into()
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
}),
..Default::default()
};
@ -157,10 +153,7 @@ impl ServiceTrait for ClamIcapTls {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -177,7 +170,7 @@ impl ServiceTrait for ClamIcapTls {
docker: &Docker,
cloud_id: &str,
config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
let mut cert = Vec::new();
exec(
docker,
@ -197,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"),
])
}
}
@ -227,10 +219,10 @@ impl ServiceTrait for Clam {
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
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"),
])
}
}
@ -255,26 +247,26 @@ impl ServiceTrait for ClamSocket {
let image = "clamav/clamav";
pull_image(docker, image).await?;
let options = Some(CreateContainerOptions {
name: self.container_name(cloud_id).unwrap(),
name: self.container_name(cloud_id),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(image),
let config = ContainerCreateBody {
image: Some(image.into()),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
..Default::default()
}),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into()
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
}),
..Default::default()
};
@ -283,10 +275,7 @@ impl ServiceTrait for ClamSocket {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -303,10 +292,12 @@ impl ServiceTrait for ClamSocket {
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
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",
),
])
}
}

View file

@ -1,10 +1,11 @@
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::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{EndpointSettings, HostConfig};
use bollard::config::ContainerCreateBody;
use bollard::models::{EndpointSettings, HostConfig, NetworkingConfig};
use bollard::query_parameters::CreateContainerOptions;
use bollard::Docker;
use maplit::hashmap;
use miette::IntoDiagnostic;
@ -29,27 +30,27 @@ impl ServiceTrait for Dav {
let image = "ugeek/webdav:amd64";
pull_image(docker, image).await?;
let options = Some(CreateContainerOptions {
name: self.container_name(cloud_id).unwrap(),
name: self.container_name(cloud_id),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(image),
let config = ContainerCreateBody {
image: Some(image.into()),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
..Default::default()
}),
env: Some(vec!["USERNAME=test", "PASSWORD=test"]),
env: Some(vec!["USERNAME=test".into(), "PASSWORD=test".into()]),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into()
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
}),
..Default::default()
};
@ -58,10 +59,7 @@ impl ServiceTrait for Dav {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -78,12 +76,12 @@ impl ServiceTrait for Dav {
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
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"),
])
}
}

View file

@ -1,10 +1,11 @@
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::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{EndpointSettings, HostConfig};
use bollard::config::NetworkingConfig;
use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig};
use bollard::query_parameters::CreateContainerOptions;
use bollard::Docker;
use maplit::hashmap;
use miette::IntoDiagnostic;
@ -29,26 +30,26 @@ impl ServiceTrait for Imaginary {
let image = "nextcloud/aio-imaginary:latest";
pull_image(docker, image).await?;
let options = Some(CreateContainerOptions {
name: self.container_name(cloud_id).unwrap(),
name: self.container_name(cloud_id),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(image),
let config = ContainerCreateBody {
image: Some(image.into()),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
..Default::default()
}),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into()
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
}),
..Default::default()
};
@ -57,10 +58,7 @@ impl ServiceTrait for Imaginary {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -73,11 +71,14 @@ impl ServiceTrait for Imaginary {
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
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'",
),
])
}
}

View file

@ -2,10 +2,10 @@ 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::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{EndpointSettings, HostConfig};
use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig};
use bollard::query_parameters::CreateContainerOptions;
use bollard::Docker;
use maplit::hashmap;
use miette::{bail, IntoDiagnostic};
@ -38,26 +38,26 @@ impl ServiceTrait for Kaspersky {
}
pull_image(docker, image).await?;
let options = Some(CreateContainerOptions {
name: self.container_name(cloud_id).unwrap(),
name: self.container_name(cloud_id),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(image),
let config = ContainerCreateBody {
image: Some(image.into()),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
..Default::default()
}),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into()
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
}),
..Default::default()
};
@ -66,10 +66,7 @@ impl ServiceTrait for Kaspersky {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -104,11 +101,11 @@ impl ServiceTrait for Kaspersky {
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
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"),
])
}
}
@ -145,26 +142,26 @@ impl ServiceTrait for KasperskyIcap {
}
pull_image(docker, image).await?;
let options = Some(CreateContainerOptions {
name: self.container_name(cloud_id).unwrap(),
name: self.container_name(cloud_id),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(image),
let config = ContainerCreateBody {
image: Some(image.into()),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
..Default::default()
}),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into(),
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
}),
..Default::default()
};
@ -173,10 +170,7 @@ impl ServiceTrait for KasperskyIcap {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -193,13 +187,15 @@ impl ServiceTrait for KasperskyIcap {
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
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",
),
])
}
}

View file

@ -1,13 +1,16 @@
use crate::cloud::CloudOptions;
use crate::config::HazeConfig;
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::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{ContainerState, EndpointSettings, HostConfig};
use bollard::config::NetworkingConfig;
use bollard::models::{ContainerCreateBody, ContainerState, EndpointSettings, HostConfig};
use bollard::query_parameters::CreateContainerOptions;
use bollard::Docker;
use maplit::hashmap;
use miette::{IntoDiagnostic, Report};
use std::net::IpAddr;
use std::str::FromStr;
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Ldap;
@ -33,29 +36,29 @@ impl ServiceTrait for Ldap {
let image = "icewind1991/haze-ldap";
pull_image(docker, image).await?;
let options = Some(CreateContainerOptions {
name: self.container_name(cloud_id).unwrap(),
name: self.container_name(cloud_id),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(image),
env: Some(vec!["LDAP_ADMIN_PASSWORD=haze"]),
let config = ContainerCreateBody {
image: Some(image.into()),
env: Some(vec!["LDAP_ADMIN_PASSWORD=haze".into()]),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
..Default::default()
}),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into()
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
cmd: Some(vec!["--copy-service"]),
}),
cmd: Some(vec!["--copy-service".into()]),
..Default::default()
};
let id = docker
@ -63,10 +66,7 @@ impl ServiceTrait for Ldap {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -86,6 +86,46 @@ impl ServiceTrait for Ldap {
) -> Result<bool> {
self.is_running(docker, cloud_id).await
}
async fn post_setup(
&self,
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<Vec<String>>> {
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"),
])
}
async fn start_message(
&self,
_docker: &Docker,
_cloud_id: &str,
_proxy: &ProxyConfig,
) -> Result<Option<String>> {
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()))
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
@ -112,29 +152,32 @@ impl ServiceTrait for LdapAdmin {
let image = "osixia/phpldapadmin";
pull_image(docker, image).await?;
let options = Some(CreateContainerOptions {
name: self.container_name(cloud_id).unwrap(),
name: self.container_name(cloud_id),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(image),
env: Some(vec!["PHPLDAPADMIN_LDAP_HOSTS=ldap"]),
let config = ContainerCreateBody {
image: Some(image.into()),
env: Some(vec![
"PHPLDAPADMIN_LDAP_HOSTS=ldap".into(),
"PHPLDAPADMIN_HTTPS=false".into(),
]),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
..Default::default()
}),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into(),
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
cmd: Some(vec!["--copy-service"]),
}),
cmd: Some(vec!["--copy-service".into()]),
..Default::default()
};
let id = docker
@ -142,10 +185,7 @@ impl ServiceTrait for LdapAdmin {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -153,9 +193,15 @@ impl ServiceTrait for LdapAdmin {
Some(format!("{}-ldap-admin", cloud_id))
}
async fn start_message(&self, docker: &Docker, cloud_id: &str) -> Result<Option<String>> {
async fn start_message(
&self,
docker: &Docker,
cloud_id: &str,
proxy: &ProxyConfig,
) -> Result<Option<String>> {
let id = self.container_name(cloud_id).unwrap();
let info = docker
.inspect_container(&self.container_name(cloud_id).unwrap(), None)
.inspect_container(&id, None)
.await
.into_diagnostic()?;
let ip = if matches!(
@ -178,9 +224,7 @@ impl ServiceTrait for LdapAdmin {
} else {
return Err(Report::msg("ldap admin not started"));
};
Ok(Some(format!(
"Ldap admin running at: https://{} with 'cn=admin,dc=example,dc=org' and password 'haze'",
ip
)))
let addr = proxy.addr(&id, IpAddr::from_str(&ip).unwrap());
Ok(Some(format!("Ldap admin running at: {addr}")))
}
}

View file

@ -1,10 +1,10 @@
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::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{EndpointSettings, HostConfig};
use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig};
use bollard::query_parameters::CreateContainerOptions;
use bollard::Docker;
use maplit::hashmap;
use miette::IntoDiagnostic;
@ -29,26 +29,26 @@ impl ServiceTrait for Mail {
let image = "rnwood/smtp4dev";
pull_image(docker, image).await?;
let options = Some(CreateContainerOptions {
name: self.container_name(cloud_id).unwrap(),
name: self.container_name(cloud_id),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(image),
let config = ContainerCreateBody {
image: Some(image.into()),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
..Default::default()
}),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into(),
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
}),
..Default::default()
};
@ -57,10 +57,7 @@ impl ServiceTrait for Mail {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -73,14 +70,14 @@ impl ServiceTrait for Mail {
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
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"),
])
}
}

View file

@ -2,17 +2,23 @@ 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::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{ContainerState, EndpointSettings, HostConfig};
use bollard::models::{
ContainerCreateBody, ContainerState, EndpointSettings, HostConfig, NetworkingConfig,
};
use bollard::query_parameters::CreateContainerOptions;
use bollard::Docker;
use maplit::hashmap;
use miette::IntoDiagnostic;
use miette::{IntoDiagnostic, WrapErr};
use serde_json::Value;
use std::collections::HashMap;
use std::fs::{create_dir_all, write};
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ObjectStore {
S3,
S3s,
S3m,
S3mb,
Azure,
@ -21,7 +27,7 @@ pub enum ObjectStore {
impl ObjectStore {
fn image(&self) -> &str {
match self {
ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb => {
ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb | ObjectStore::S3s => {
"minio/minio:RELEASE.2024-07-16T23-46-41Z"
}
ObjectStore::Azure => "arafato/azurite:2.6.5",
@ -30,7 +36,7 @@ impl ObjectStore {
fn self_env(&self) -> Vec<&str> {
match self {
ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb => {
ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb | ObjectStore::S3s => {
vec!["MINIO_ACCESS_KEY=minio", "MINIO_SECRET_KEY=minio123"]
}
ObjectStore::Azure => vec![],
@ -39,17 +45,54 @@ impl ObjectStore {
fn host_name(&self) -> &str {
match self {
ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb => "s3",
ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb | ObjectStore::S3s => "s3",
ObjectStore::Azure => "azure",
}
}
fn args(&self) -> &[&str] {
match self {
ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb => &["server", "/data"],
ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb | ObjectStore::S3s => {
&["server", "/data"]
}
_ => &[],
}
}
fn volumes(&self, config: &HazeConfig) -> Option<Vec<String>> {
match self {
ObjectStore::S3s => {
let cert_dir = config.work_dir.join("certificates/s3");
create_dir_all(&cert_dir)
.into_diagnostic()
.wrap_err("Failed to create redis certificate directory")
.unwrap();
let s3_cert_path = config.work_dir.join("certificates/s3/public.crt");
let s3_key_path = config.work_dir.join("certificates/s3/private.key");
if !s3_cert_path.exists() {
write(
&s3_cert_path,
include_bytes!("../../certificates/s3/public.crt"),
)
.into_diagnostic()
.wrap_err("Failed to write s3 certificate")
.unwrap();
}
if !s3_key_path.exists() {
write(
&s3_key_path,
include_bytes!("../../certificates/s3/private.key"),
)
.into_diagnostic()
.wrap_err("Failed to write s3 key")
.unwrap();
}
Some(vec![format!("{cert_dir}:/root/.minio/certs:ro")])
}
_ => None,
}
}
}
#[async_trait::async_trait]
@ -57,6 +100,7 @@ impl ServiceTrait for ObjectStore {
fn name(&self) -> &str {
match self {
ObjectStore::S3 => "s3",
ObjectStore::S3s => "s3s",
ObjectStore::S3m => "s3m",
ObjectStore::S3mb => "s3mb",
ObjectStore::Azure => "azure",
@ -66,8 +110,9 @@ impl ServiceTrait for ObjectStore {
fn env(&self) -> &[&str] {
match self {
ObjectStore::S3 => &["S3=1"],
ObjectStore::S3s => &["S3S=1"],
ObjectStore::S3m => &["S3M=1"],
ObjectStore::S3mb => &["S3MB=1"],
ObjectStore::S3mb => &["S3MB =1"],
ObjectStore::Azure => &["AZURE=1"],
}
}
@ -77,33 +122,34 @@ impl ServiceTrait for ObjectStore {
docker: &Docker,
cloud_id: &str,
network: &str,
_config: &HazeConfig,
config: &HazeConfig,
_options: &CloudOptions,
) -> Result<Vec<String>> {
pull_image(docker, self.image()).await?;
let options = Some(CreateContainerOptions {
name: format!("{}-object", cloud_id),
name: Some(format!("{}-object", cloud_id)),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(self.image()),
env: Some(self.self_env()),
let config = ContainerCreateBody {
image: Some(self.image().into()),
env: Some(self.self_env().into_iter().map(String::from).collect()),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
binds: self.volumes(config),
..Default::default()
}),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into(),
}),
cmd: Some(self.args().into()),
cmd: Some(self.args().iter().copied().map(String::from).collect()),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.host_name().to_string()]),
..Default::default()
}
},
}),
}),
..Default::default()
};
@ -112,10 +158,7 @@ impl ServiceTrait for ObjectStore {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -166,26 +209,76 @@ impl ServiceTrait for ObjectStore {
&["files_external"]
}
fn config(
&self,
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<HashMap<String, Value>> {
match self {
ObjectStore::S3s => Ok(hashmap![
"default_certificates_bundle_path".into() => Value::String("/var/www/html/data/ca-bundle.crt".into()),
]),
_ => Ok(HashMap::default()),
}
}
fn pre_setup(
&self,
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<Vec<String>>> {
match self {
ObjectStore::S3s => Ok(vec![
vec!["mkdir".into(), "-p".into(), "/var/www/html/data".into()],
vec![
"sh".into(),
"-c".into(),
"cat /var/www/html/resources/config/ca-bundle.crt /certificates/s3/public.crt > /var/www/html/data/ca-bundle.crt".into(),
],
]),
_ => Ok(Vec::new()),
}
}
async fn post_setup(
&self,
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<String>> {
if *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(),
])
} else {
Ok(Vec::new())
) -> Result<Vec<Vec<String>>> {
match self {
ObjectStore::S3 => Ok(vec![
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(),
// "occ files_external:config 1 bucket ext".into(),
// "occ files_external:config 1 hostname s3".into(),
// "occ files_external:config 1 port 9000".into(),
// "occ files_external:config 1 use_ssl true".into(),
// "occ files_external:config 1 use_path_style true".into(),
// "occ files_external:config 1 key minio".into(),
// "occ files_external:config 1 secret minio123".into(),
// "mc alias set s3 https://s3:9000 minio minio123".into(),
// ]),
_ => Ok(Vec::new()),
}
}
fn proxy_port(&self) -> u16 {
match self {
ObjectStore::S3 | ObjectStore::S3m | ObjectStore::S3mb | ObjectStore::S3s => 9000,
ObjectStore::Azure => 10000,
}
}
}

View file

@ -4,8 +4,9 @@ use crate::exec::exec;
use crate::image::pull_image;
use crate::service::ServiceTrait;
use crate::Result;
use bollard::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{EndpointSettings, HostConfig};
use bollard::config::NetworkingConfig;
use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig};
use bollard::query_parameters::CreateContainerOptions;
use bollard::Docker;
use maplit::hashmap;
use miette::IntoDiagnostic;
@ -33,7 +34,7 @@ impl ServiceTrait for Oc {
let image = "owncloud/server:10.12.2";
pull_image(docker, image).await?;
let options = Some(CreateContainerOptions {
name: self.container_name(cloud_id).unwrap(),
name: self.container_name(cloud_id),
..CreateContainerOptions::default()
});
let addr = config.proxy.addr(
@ -43,24 +44,24 @@ impl ServiceTrait for Oc {
let domain = addr.split_once("://").unwrap().1;
let env_trusted_domain = format!("OWNCLOUD_TRUSTED_DOMAINS={domain}");
let env_domain = format!("OWNCLOUD_DOMAIN={domain}");
let config = Config {
image: Some(image),
let config = ContainerCreateBody {
image: Some(image.into()),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
..Default::default()
}),
env: Some(vec![&env_trusted_domain, &env_domain]),
env: Some(vec![env_trusted_domain, env_domain]),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into(),
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
}),
..Default::default()
};
@ -69,10 +70,7 @@ impl ServiceTrait for Oc {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -85,7 +83,7 @@ impl ServiceTrait for Oc {
docker: &Docker,
cloud_id: &str,
config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
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);

View file

@ -3,8 +3,10 @@ use crate::config::HazeConfig;
use crate::image::pull_image;
use crate::service::ServiceTrait;
use crate::Result;
use bollard::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{ContainerState, EndpointSettings, HostConfig};
use bollard::models::{
ContainerCreateBody, ContainerState, EndpointSettings, HostConfig, NetworkingConfig,
};
use bollard::query_parameters::CreateContainerOptions;
use bollard::Docker;
use maplit::hashmap;
use miette::{IntoDiagnostic, Report};
@ -30,48 +32,68 @@ impl ServiceTrait for Office {
config: &HazeConfig,
_options: &CloudOptions,
) -> Result<Vec<String>> {
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();
let options = Some(CreateContainerOptions {
name: container_id.clone(),
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}")
if !proxy_base.is_empty() {
env.push(format!("server_name={clean_id}.{}", config.proxy.address));
}
(public, false) if !public.is_empty() => {
format!("server_name={clean_id}.{public}")
}
_ => "".to_string(),
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}"),
]
};
if !server_name_opt.is_empty() {
env.push(&server_name_opt);
}
let config = Config {
image: Some(image),
let config = ContainerCreateBody {
image: Some(image.into()),
env: Some(env),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
extra_hosts: Some(hosts),
..Default::default()
}),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into(),
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
}),
..Default::default()
};
@ -80,10 +102,7 @@ impl ServiceTrait for Office {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -100,7 +119,7 @@ impl ServiceTrait for Office {
docker: &Docker,
cloud_id: &str,
config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
let container = &self.container_name(cloud_id).unwrap();
let info = docker
.inspect_container(container, None)
@ -128,19 +147,27 @@ 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
),
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(),
],
])
}

View file

@ -2,10 +2,12 @@ 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::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{ContainerState, EndpointSettings, HostConfig};
use bollard::models::{
ContainerCreateBody, ContainerState, EndpointSettings, HostConfig, NetworkingConfig,
};
use bollard::query_parameters::CreateContainerOptions;
use bollard::Docker;
use maplit::hashmap;
use miette::{IntoDiagnostic, Report};
@ -35,26 +37,26 @@ impl ServiceTrait for OnlyOffice {
let image = "onlyoffice/documentserver";
pull_image(docker, image).await?;
let options = Some(CreateContainerOptions {
name: self.container_name(cloud_id).unwrap(),
name: self.container_name(cloud_id),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(image),
let config = ContainerCreateBody {
image: Some(image.into()),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
..Default::default()
}),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into(),
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
}),
..Default::default()
};
@ -63,10 +65,7 @@ impl ServiceTrait for OnlyOffice {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -83,7 +82,7 @@ impl ServiceTrait for OnlyOffice {
docker: &Docker,
cloud_id: &str,
config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
let info = docker
.inspect_container(&self.container_name(cloud_id).unwrap(), None)
.await
@ -138,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"),
])
}
}

View file

@ -2,8 +2,8 @@ use crate::cloud::CloudOptions;
use crate::config::HazeConfig;
use crate::image::pull_image;
use crate::service::ServiceTrait;
use bollard::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{EndpointSettings, HostConfig};
use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig};
use bollard::query_parameters::CreateContainerOptions;
use bollard::Docker;
use local_ip_address::list_afinet_netifas;
use maplit::hashmap;
@ -33,11 +33,11 @@ impl ServiceTrait for NotifyPush {
let image = "icewind1991/notify_push";
pull_image(docker, image).await?;
let options = Some(CreateContainerOptions {
name: self.container_name(cloud_id).unwrap(),
name: self.container_name(cloud_id),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(image),
let config = ContainerCreateBody {
image: Some(image.into()),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
binds: Some(vec![
@ -47,23 +47,23 @@ impl ServiceTrait for NotifyPush {
..Default::default()
}),
env: Some(vec![
"NEXTCLOUD_URL=http://cloud/",
"LOG=debug",
"REDIS_URL=redis://cloud/",
"NEXTCLOUD_URL=http://cloud/".into(),
"LOG=debug".into(),
"REDIS_URL=redis://cloud/".into(),
]),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into(),
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
cmd: Some(vec!["/notify_push", "/config/config.php"]),
}),
cmd: Some(vec!["/notify_push".into(), "/config/config.php".into()]),
..Default::default()
};
let id = docker
@ -87,7 +87,7 @@ impl ServiceTrait for NotifyPush {
docker: &Docker,
cloud_id: &str,
config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
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)
}

View file

@ -1,10 +1,10 @@
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::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{EndpointSettings, HostConfig};
use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig};
use bollard::query_parameters::CreateContainerOptions;
use bollard::Docker;
use maplit::hashmap;
use miette::IntoDiagnostic;
@ -29,26 +29,26 @@ impl ServiceTrait for Redis {
let image = "redis:8-alpine";
pull_image(docker, image).await?;
let options = Some(CreateContainerOptions {
name: self.container_name(cloud_id).unwrap(),
name: self.container_name(cloud_id),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(image),
let config = ContainerCreateBody {
image: Some(image.into()),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
..Default::default()
}),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into(),
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
}),
..Default::default()
};
@ -57,10 +57,7 @@ impl ServiceTrait for Redis {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -73,7 +70,13 @@ impl ServiceTrait for Redis {
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<String>> {
Ok(vec!["occ config:system:set redis host --value redis".into()])
) -> Result<Vec<Vec<String>>> {
Ok(vec![split_cmnd(
"occ config:system:set redis host --value redis",
)])
}
fn exec_shell(&self) -> &'static str {
"sh"
}
}

View file

@ -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::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{EndpointSettings, HostConfig};
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;
@ -29,28 +30,31 @@ impl ServiceTrait for Sftp {
let image = "atmoz/sftp:alpine";
pull_image(docker, image).await?;
let options = Some(CreateContainerOptions {
name: self.container_name(cloud_id).unwrap(),
name: self.container_name(cloud_id),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(image),
let config = ContainerCreateBody {
image: Some(image.into()),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
..Default::default()
}),
labels: Some(hashmap! {
"haze-type" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into(),
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
cmd: Some(vec!["test:test:::data"]),
}),
cmd: Some(vec![
"test:test:::data".into(),
"ldaptest:test:::data".into(),
]),
..Default::default()
};
let id = docker
@ -58,10 +62,7 @@ impl ServiceTrait for Sftp {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -78,13 +79,119 @@ impl ServiceTrait for Sftp {
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
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<Vec<String>> {
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<String> {
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<Vec<Vec<String>>> {
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(),
],
])
}
}

View file

@ -1,10 +1,10 @@
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::container::{Config, CreateContainerOptions, NetworkingConfig};
use bollard::models::{EndpointSettings, HostConfig};
use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig};
use bollard::query_parameters::CreateContainerOptions;
use bollard::Docker;
use maplit::hashmap;
use miette::IntoDiagnostic;
@ -29,31 +29,33 @@ impl ServiceTrait for Smb {
let image = "ghcr.io/servercontainers/samba:smbd-only-a3.18.0-s4.18.2-r0";
pull_image(docker, image).await?;
let options = Some(CreateContainerOptions {
name: self.container_name(cloud_id).unwrap(),
name: self.container_name(cloud_id),
..CreateContainerOptions::default()
});
let config = Config {
image: Some(image),
let config = ContainerCreateBody {
image: Some(image.into()),
host_config: Some(HostConfig {
network_mode: Some(network.to_string()),
..Default::default()
}),
env: Some(vec![
"ACCOUNT_test=test",
"UID_test=1000",
"SAMBA_VOLUME_CONFIG_test=[test]; path=/tmp; valid users = test; guest ok = no; read only = no; browseable = yes",
"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" => self.name(),
"haze-cloud-id" => cloud_id
"haze-type".into() => self.name().into(),
"haze-cloud-id".into() => cloud_id.into(),
}),
networking_config: Some(NetworkingConfig {
endpoints_config: hashmap! {
network => EndpointSettings {
endpoints_config: Some(hashmap! {
network.into() => EndpointSettings {
aliases: Some(vec![self.name().to_string()]),
..Default::default()
}
},
}),
}),
..Default::default()
};
@ -62,10 +64,7 @@ impl ServiceTrait for Smb {
.await
.into_diagnostic()?
.id;
docker
.start_container::<String>(&id, None)
.await
.into_diagnostic()?;
docker.start_container(&id, None).await.into_diagnostic()?;
Ok(vec![id])
}
@ -82,13 +81,17 @@ impl ServiceTrait for Smb {
_docker: &Docker,
_cloud_id: &str,
_config: &HazeConfig,
) -> Result<Vec<String>> {
) -> Result<Vec<Vec<String>>> {
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"),
])
}
fn exec_shell(&self) -> &'static str {
"sh"
}
}

75
src/service/webhook.rs Normal file
View file

@ -0,0 +1,75 @@
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<Vec<String>> {
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<String> {
Some(format!("{}-webhook", cloud_id))
}
fn proxy_port(&self) -> u16 {
8080
}
fn exec_shell(&self) -> &'static str {
""
}
}

View file

@ -1,6 +1,17 @@
use crate::config::HazeConfig;
use camino::Utf8PathBuf;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use miette::{Context, IntoDiagnostic, Report, Result};
use reqwest::header::HeaderName;
use reqwest::{Client, IntoUrl, Response};
use sha2::{Digest, Sha512};
use std::fs::read_to_string;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::time::Duration;
use tokio::fs::create_dir_all;
use zip::read::root_dir_common_filter;
use zip::ZipArchive;
pub struct Sources {
#[allow(dead_code)]
@ -49,3 +60,116 @@ impl Sources {
}
}
}
pub async fn download_nc(config: &HazeConfig, version: &str) -> Result<Utf8PathBuf> {
if !version.chars().all(|c| c.is_ascii_digit() || c == '.') {
return Err(Report::msg(format!("Invalid version: {version}")));
}
let root = config.work_dir.join("sources");
create_dir_all(&root)
.await
.into_diagnostic()
.wrap_err("failed to create parent directory for sources")?;
let dest = root.join(version);
if !dest.exists() {
let progress = MultiProgress::new();
let download_style = ProgressStyle::with_template("{spinner:.green} {msg} [{elapsed_precise}] [{bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")
.unwrap();
let download_bar = ProgressBar::new(0)
.with_message("Downloading")
.with_style(download_style.clone());
let download_bar = progress.add(download_bar);
let archive = download_url(
format!("https://download.nextcloud.com/server/releases/nextcloud-{version}.zip"),
|size| {
download_bar.set_length(size);
},
|count| {
download_bar.inc(count);
},
)
.await
.wrap_err_with(|| format!("Failed to download archive for {}", version))?;
download_bar.finish();
let expected_hash = download_text(format!(
"https://download.nextcloud.com/server/releases/nextcloud-{version}.zip.sha512"
))
.await
.wrap_err_with(|| format!("Failed to download hash for {}", version))?;
let expected_hash = expected_hash
.split_once(' ')
.map(|(hash, _)| hash)
.unwrap_or(expected_hash.trim());
let hash_bar = ProgressBar::new(download_bar.length().unwrap_or_default())
.with_message("Validating")
.with_style(download_style.clone());
let hash_bar = progress.add(hash_bar);
let mut hasher = Sha512::new();
for chunk in archive.chunks(1014 * 1024) {
hash_bar.inc(chunk.len() as u64);
hasher.update(chunk);
}
let hash = hasher.finalize();
let hash = base16ct::lower::encode_string(&hash);
if expected_hash != hash {
return Err(Report::msg(format!(
"Invalid hash for downloaded: {version}, expected {expected_hash} but got {hash}"
)));
}
hash_bar.finish();
let extract_bar = ProgressBar::new_spinner().with_message("Extracting");
extract_bar.enable_steady_tick(Duration::from_millis(100));
let extract_bar = progress.add(extract_bar);
let mut archive = ZipArchive::new(Cursor::new(archive)).into_diagnostic()?;
archive
.extract_unwrapped_root_dir(&dest, root_dir_common_filter)
.into_diagnostic()
.wrap_err("Failed to extract archive")?;
extract_bar.finish();
}
Ok(dest)
}
async fn download_url<U: IntoUrl, SizeFN: FnOnce(u64), ProgressFN: Fn(u64)>(
url: U,
size: SizeFN,
progress: ProgressFN,
) -> Result<Vec<u8>> {
let mut res = get_url(url).await?.error_for_status().into_diagnostic()?;
let mut buff = Vec::new();
size(res.content_length().unwrap_or_default());
while let Some(chunk) = res.chunk().await.into_diagnostic()? {
progress(chunk.len() as u64);
buff.extend(chunk);
}
Ok(buff)
}
async fn download_text<U: IntoUrl>(url: U) -> Result<String> {
get_url(url)
.await?
.error_for_status()
.into_diagnostic()?
.text()
.await
.into_diagnostic()
}
async fn get_url<U: IntoUrl>(url: U) -> Result<Response> {
Client::builder()
.build()
.into_diagnostic()?
.get(url)
.header(
HeaderName::from_static("user-agent"),
format!("haze {}", env!("CARGO_PKG_VERSION")),
)
.send()
.await
.into_diagnostic()
}