Skip to content

Commit bf0ceb6

Browse files
authored
Merge pull request #144 from LibreCodeCoop/feat/worker-lifecycle-foundation
Add isolated worker lifecycle foundation
2 parents 5a12393 + ab6d1b1 commit bf0ceb6

9 files changed

Lines changed: 268 additions & 33 deletions

File tree

‎.docker/database-services.yml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ services:
44
# runs with the application user, which cannot read mysql.component there.
55
image: mysql:8.4@sha256:b3b90af2a6552ae30c266fdb7d5dd55f3afb72404bb78d37fe8a23eb857fd3fb
66
volumes:
7-
- ../volumes/mysql/init:/docker-entrypoint-initdb.d
8-
- ../volumes/mysql/data:/var/lib/mysql
7+
- ${WORKER_VOLUMES_DIR:-../volumes}/mysql/init:/docker-entrypoint-initdb.d
8+
- ${WORKER_VOLUMES_DIR:-../volumes}/mysql/data:/var/lib/mysql
99
ports:
1010
- target: 3306
1111
published: "3306-3325"
@@ -20,7 +20,7 @@ services:
2020
pgsql:
2121
image: postgres:13-alpine@sha256:fb9065b6e3e213bdc07edd372a5b2a26245840b7fb65d1fd8b6700106d51805c
2222
volumes:
23-
- ../volumes/postgres/data:/var/lib/postgresql/data
23+
- ${WORKER_VOLUMES_DIR:-../volumes}/postgres/data:/var/lib/postgresql/data
2424
ports:
2525
- target: 5432
2626
published: "5432-5451"

‎.docker/scripts/nextcloud-entrypoint.sh‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ install -d -o www-data -g www-data \
2121
config \
2222
apps-extra
2323

24+
db_type="${DB_TYPE:-${DB_HOST:-mysql}}"
25+
db_host="${DB_HOST:-$db_type}"
26+
db_driver="${DB_DRIVER:-$db_type}"
27+
28+
export DB_TYPE="$db_type"
29+
export DB_HOST="$db_host"
30+
export DB_DRIVER="$db_driver"
31+
2432
# Wait for database
2533
php /var/www/scripts/wait-for-db.php
2634

@@ -35,22 +43,22 @@ fi
3543
# Set configurations, if needed
3644
if [[ ! -f "config/config.php" && ${AUTOINSTALL} -eq 1 ]]; then
3745
echo "⌛️ Starting installation ..."
38-
if [[ ${DB_HOST} == 'mysql' ]]; then
39-
occ maintenance:install --verbose --database="${DB_HOST}" --database-name="${MYSQL_DATABASE}" --database-host="${DB_HOST}" --database-port= --database-user="${MYSQL_USER}" --database-pass="${MYSQL_PASSWORD}" --admin-user="${NEXTCLOUD_ADMIN_USER}" --admin-pass="${NEXTCLOUD_ADMIN_PASSWORD}" --admin-email="${NEXTCLOUD_ADMIN_EMAIL}"
46+
if [[ "${db_type}" == 'mysql' ]]; then
47+
occ maintenance:install --verbose --database="${db_driver}" --database-name="${MYSQL_DATABASE}" --database-host="${db_host}" --database-port= --database-user="${MYSQL_USER}" --database-pass="${MYSQL_PASSWORD}" --admin-user="${NEXTCLOUD_ADMIN_USER}" --admin-pass="${NEXTCLOUD_ADMIN_PASSWORD}" --admin-email="${NEXTCLOUD_ADMIN_EMAIL}"
4048
install_cmd_status=$?
41-
elif [[ "${DB_HOST}" == 'pgsql' ]]; then
42-
occ maintenance:install --verbose --database="${DB_HOST}" --database-name="${POSTGRES_DB}" --database-host="${DB_HOST}" --database-port= --database-user="${POSTGRES_USER}" --database-pass="${POSTGRES_PASSWORD}" --admin-user="${NEXTCLOUD_ADMIN_USER}" --admin-pass="${NEXTCLOUD_ADMIN_PASSWORD}" --admin-email="${NEXTCLOUD_ADMIN_EMAIL}"
49+
elif [[ "${db_type}" == 'pgsql' ]]; then
50+
occ maintenance:install --verbose --database="${db_driver}" --database-name="${POSTGRES_DB}" --database-host="${db_host}" --database-port= --database-user="${POSTGRES_USER}" --database-pass="${POSTGRES_PASSWORD}" --admin-user="${NEXTCLOUD_ADMIN_USER}" --admin-pass="${NEXTCLOUD_ADMIN_PASSWORD}" --admin-email="${NEXTCLOUD_ADMIN_EMAIL}"
4351
install_cmd_status=$?
4452
else
45-
occ maintenance:install --verbose --admin-user="${NEXTCLOUD_ADMIN_USER}" --admin-pass="${NEXTCLOUD_ADMIN_PASSWORD}" --admin-email="${NEXTCLOUD_ADMIN_EMAIL}"
53+
occ maintenance:install --verbose --database="${db_driver}" --admin-user="${NEXTCLOUD_ADMIN_USER}" --admin-pass="${NEXTCLOUD_ADMIN_PASSWORD}" --admin-email="${NEXTCLOUD_ADMIN_EMAIL}"
4654
install_cmd_status=$?
4755
fi
4856

4957
if [[ ${install_cmd_status} -ne 0 ]]; then
5058
db_reset_hint="volumes/nextcloud/config and volumes/nextcloud/data"
51-
if [[ ${DB_HOST} == 'mysql' ]]; then
59+
if [[ "${db_type}" == 'mysql' ]]; then
5260
db_reset_hint="volumes/mysql/data, ${db_reset_hint}"
53-
elif [[ ${DB_HOST} == 'pgsql' ]]; then
61+
elif [[ "${db_type}" == 'pgsql' ]]; then
5462
db_reset_hint="volumes/postgres/data, ${db_reset_hint}"
5563
fi
5664

‎.docker/scripts/wait-for-db.php‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
#!/usr/bin/env php
22
<?php
3-
$dbName = (getenv('DB_HOST') === 'mysql' ? '🐬' : '🐘' ) . getenv('DB_HOST');
3+
$dbType = getenv('DB_TYPE') ?: (getenv('DB_HOST') ?: 'mysql');
4+
$dbHost = getenv('DB_HOST') ?: $dbType;
5+
$dbName = ($dbType === 'mysql' ? '🐬' : ($dbType === 'pgsql' ? '🐘' : '💾')) . $dbType;
46

57
echo "⌛ Waiting for database $dbName\n";
68

79
function dbIsUp(string $dbName): bool {
810
try {
9-
if (getenv('DB_HOST') === 'mysql') {
10-
$dsn = getenv('DB_HOST') . ':dbname='.getenv('MYSQL_DATABASE').';host='.getenv('DB_HOST');
11+
if ($GLOBALS['dbType'] === 'mysql') {
12+
$dsn = 'mysql:dbname='.getenv('MYSQL_DATABASE').';host='.$GLOBALS['dbHost'];
1113
new PDO($dsn, getenv('MYSQL_USER'), getenv('MYSQL_PASSWORD'));
12-
} elseif (getenv('DB_HOST') === 'pgsql') {
13-
$dsn = getenv('DB_HOST') . ':dbname='.getenv('POSTGRES_DB').';host='.getenv('DB_HOST');
14+
} elseif ($GLOBALS['dbType'] === 'pgsql') {
15+
$dsn = 'pgsql:dbname='.getenv('POSTGRES_DB').';host='.$GLOBALS['dbHost'];
1416
new PDO($dsn, getenv('POSTGRES_USER'), getenv('POSTGRES_PASSWORD'));
1517
} else {
1618
// Will use SQLite

‎.github/workflows/proxy-tests.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
detik-install: false
2424
file-install: false
2525
- name: Run unit tests
26-
run: bats tests/proxy/lease.bats tests/proxy/infrastructure.bats tests/proxy/services.bats tests/proxy/diagnostics.bats tests/proxy/compose-policy.bats
26+
run: bats tests/proxy/lease.bats tests/proxy/infrastructure.bats tests/proxy/services.bats tests/proxy/diagnostics.bats tests/proxy/compose-policy.bats tests/worker/contract.bats
2727

2828
integration:
2929
name: Proxy Docker integration tests

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
# Docker volumes
66
/volumes
7+
/.workers
78

89
.env
910
docker-compose.override.yml

‎dev-worker‎

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/bin/sh
2+
# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
set -eu
6+
7+
usage() {
8+
cat <<'EOF'
9+
Usage: sh ./dev-worker <worker-id> <command> [arguments...]
10+
11+
Commands:
12+
up Start the isolated worker and wait for Nextcloud installation
13+
exec <command...> Run a command in the worker's Nextcloud container
14+
logs Print worker logs
15+
config Render the resolved Compose configuration
16+
status Run occ status in the worker
17+
destroy Stop the worker and remove only its mutable state
18+
19+
Environment:
20+
DB_TYPE=mysql|pgsql Database backend for the foundation lifecycle
21+
DB_HOST=<hostname> Connection hostname (defaults to DB_TYPE)
22+
PHP_VERSION=<version> Existing PHP image selector
23+
VERSION_NEXTCLOUD=<ref> Existing Nextcloud ref selector
24+
WORKER_READY_TIMEOUT=180 Seconds to wait for a clean installation
25+
EOF
26+
}
27+
28+
[ "$#" -ge 2 ] || {
29+
usage >&2
30+
exit 2
31+
}
32+
33+
worker_id="$1"
34+
command="$2"
35+
shift 2
36+
37+
case "$worker_id" in
38+
*[!a-z0-9_-]*|'')
39+
echo "Invalid worker id: $worker_id" >&2
40+
exit 2
41+
;;
42+
esac
43+
44+
case "$worker_id" in
45+
[a-z0-9]*) ;;
46+
*)
47+
echo "Worker id must start with a lowercase letter or digit: $worker_id" >&2
48+
exit 2
49+
;;
50+
esac
51+
52+
repo_root="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
53+
db_type="${DB_TYPE:-mysql}"
54+
55+
case "$db_type" in
56+
mysql|pgsql) ;;
57+
sqlite|mariadb)
58+
echo "DB_TYPE=$db_type is reserved by the worker contract but is not implemented by this foundation yet." >&2
59+
exit 3
60+
;;
61+
*)
62+
echo "Unsupported DB_TYPE: $db_type" >&2
63+
exit 2
64+
;;
65+
esac
66+
67+
worker_root="$repo_root/.workers/$worker_id"
68+
worker_volumes_dir="$worker_root/volumes"
69+
70+
export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-ncdev-$worker_id}"
71+
export WORKER_VOLUMES_DIR="$worker_volumes_dir"
72+
export DB_TYPE="$db_type"
73+
export DB_HOST="${DB_HOST:-$db_type}"
74+
75+
compose() {
76+
docker compose --project-directory "$repo_root" --file "$repo_root/docker-compose.yml" "$@"
77+
}
78+
79+
wait_until_ready() {
80+
timeout="${WORKER_READY_TIMEOUT:-180}"
81+
started="$(date +%s)"
82+
83+
while :; do
84+
if compose exec -T nextcloud occ status 2>/dev/null | grep -q 'installed: true'; then
85+
return 0
86+
fi
87+
88+
now="$(date +%s)"
89+
if [ $((now - started)) -ge "$timeout" ]; then
90+
echo "Worker $worker_id did not become ready within ${timeout}s." >&2
91+
compose logs --no-color nextcloud >&2 || true
92+
return 1
93+
fi
94+
95+
sleep 2
96+
done
97+
}
98+
99+
case "$command" in
100+
up)
101+
mkdir -p "$worker_volumes_dir"
102+
compose up -d
103+
wait_until_ready
104+
;;
105+
exec)
106+
[ "$#" -gt 0 ] || {
107+
echo "exec requires a command" >&2
108+
exit 2
109+
}
110+
compose exec -T nextcloud "$@"
111+
;;
112+
logs)
113+
compose logs --no-color "$@"
114+
;;
115+
config)
116+
mkdir -p "$worker_volumes_dir"
117+
compose config "$@"
118+
;;
119+
status)
120+
compose exec -T nextcloud occ status
121+
;;
122+
destroy)
123+
compose down --remove-orphans
124+
case "$worker_root" in
125+
"$repo_root"/.workers/"$worker_id"|"$repo_root"/.workers/"$worker_id"/) ;;
126+
*)
127+
echo "Refusing to remove unexpected worker path: $worker_root" >&2
128+
exit 4
129+
;;
130+
esac
131+
rm -rf -- "$worker_root"
132+
;;
133+
*)
134+
usage >&2
135+
exit 2
136+
;;
137+
esac

‎docker-compose.yml‎

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@ services:
22
database:
33
extends:
44
file: .docker/database-services.yml
5-
service: ${DB_HOST:-mysql}
5+
service: ${DB_TYPE:-${DB_HOST:-mysql}}
66
networks:
77
default:
88
aliases:
9-
- ${DB_HOST:-mysql}
9+
- ${DB_HOST:-${DB_TYPE:-mysql}}
1010
nextcloud:
1111
image: ghcr.io/librecodecoop/nextcloud-dev-php${PHP_VERSION:-83}:latest
1212
# build:
1313
# context: .docker/
1414
# dockerfile: Dockerfile.php${PHP_VERSION:-83}
1515
volumes:
1616
- ./.docker/scripts/wait-for-db.php:/var/www/scripts/wait-for-db.php
17-
- ./volumes/php:/var/www/php-config
17+
- ${WORKER_VOLUMES_DIR:-./volumes}/php:/var/www/php-config
1818
- ~/.composer:/var/www/.composer/
1919
- ~/.npm:/var/www/.npm/
20-
- ./volumes/nextcloud:/var/www/html
20+
- ${WORKER_VOLUMES_DIR:-./volumes}/nextcloud:/var/www/html
2121
restart: unless-stopped
2222
depends_on:
2323
- database
@@ -28,7 +28,9 @@ services:
2828
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER:-admin}
2929
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD:-admin}
3030
- NEXTCLOUD_ADMIN_EMAIL=${NEXTCLOUD_ADMIN_EMAIL:-a@test.tld}
31-
- DB_HOST=${DB_HOST:-mysql}
31+
- DB_TYPE=${DB_TYPE:-${DB_HOST:-mysql}}
32+
- DB_HOST=${DB_HOST:-${DB_TYPE:-mysql}}
33+
- DB_DRIVER=${DB_DRIVER:-${DB_TYPE:-${DB_HOST:-mysql}}}
3234
- MYSQL_DATABASE=${MYSQL_DATABASE:-nextcloud}
3335
- MYSQL_USER=${MYSQL_USER:-nextcloud}
3436
- MYSQL_PASSWORD=${MYSQL_PASSWORD:-nextcloud}
@@ -56,9 +58,9 @@ services:
5658
depends_on:
5759
- nextcloud
5860
volumes:
59-
- ./volumes/nextcloud:/var/www/html:ro
60-
- ./volumes/nginx/includes:/etc/nginx/conf.d/includes/
61-
- ./volumes/nginx/certs:/certs
61+
- ${WORKER_VOLUMES_DIR:-./volumes}/nextcloud:/var/www/html:ro
62+
- ${WORKER_VOLUMES_DIR:-./volumes}/nginx/includes:/etc/nginx/conf.d/includes/
63+
- ${WORKER_VOLUMES_DIR:-./volumes}/nginx/certs:/certs
6264
environment:
6365
- VIRTUAL_HOST=${COMPOSE_PROJECT_NAME}.localhost
6466
- VIRTUAL_PORT=80
@@ -97,9 +99,9 @@ services:
9799
- eurooffice
98100
restart: always
99101
volumes:
100-
- ./volumes/eurooffice:/home/eurooffice/.eurooffice
101-
- ./volumes/eurooffice/local.json:/etc/onlyoffice/documentserver/local.json
102-
- ./volumes/nextcloud/apps-extra/eurooffice-plugin:/var/www/onlyoffice/documentserver/sdkjs-plugins/libresign:ro
102+
- ${WORKER_VOLUMES_DIR:-./volumes}/eurooffice:/home/eurooffice/.eurooffice
103+
- ${WORKER_VOLUMES_DIR:-./volumes}/eurooffice/local.json:/etc/onlyoffice/documentserver/local.json
104+
- ${WORKER_VOLUMES_DIR:-./volumes}/nextcloud/apps-extra/eurooffice-plugin:/var/www/onlyoffice/documentserver/sdkjs-plugins/libresign:ro
103105
environment:
104106
- EXAMPLE_ENABLED=true
105107
- JWT_SECRET=${JWT_SECRET:-my_jwt_secret}
@@ -116,7 +118,7 @@ services:
116118
- playwright
117119
user: pwuser
118120
volumes:
119-
- ./volumes/nextcloud:/var/www/html
121+
- ${WORKER_VOLUMES_DIR:-./volumes}/nextcloud:/var/www/html
120122
working_dir: /var/www/html
121123
environment:
122124
- PLAYWRIGHT_BASE_URL=${PLAYWRIGHT_BASE_URL:-https://${COMPOSE_PROJECT_NAME}.localhost}
@@ -137,7 +139,7 @@ services:
137139
- signal
138140
restart: unless-stopped
139141
volumes:
140-
- ./volumes/signal:/home/.local/share/signal-cli
142+
- ${WORKER_VOLUMES_DIR:-./volumes}/signal:/home/.local/share/signal-cli
141143
environment:
142144
- LOG_LEVEL=info
143145
- VIRTUAL_HOST=${COMPOSE_PROJECT_NAME}-signal.localhost
@@ -159,4 +161,4 @@ services:
159161
- WHATSAPP_WEBHOOK_EVENTS=${WHATSAPP_WEBHOOK_EVENTS:-message,message.ack,group.participants,chat_presence}
160162
- WHATSAPP_AUTO_REPLY="Conta de sistema. Para falar com a LibreCode, mande email para contato@libresign.coop"
161163
volumes:
162-
- ./volumes/go-whatsapp-web:/usr/src/app/files
164+
- ${WORKER_VOLUMES_DIR:-./volumes}/go-whatsapp-web:/usr/src/app/files

‎docs/advanced-setup.md‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,50 @@ MySQL is used by default:
2828
docker compose up
2929
```
3030

31-
The supported `DB_HOST` values are:
31+
Use `DB_TYPE` to select the database backend and keep `DB_HOST` for the
32+
connection hostname.
3233

33-
- `mysql`
34+
The currently implemented backends are:
35+
36+
- `mysql` (default)
3437
- `pgsql`
3538

3639
To use PostgreSQL:
3740

3841
```bash
39-
DB_HOST=pgsql docker compose up
42+
DB_TYPE=pgsql docker compose up
43+
```
44+
45+
Existing callers that use `DB_HOST=pgsql docker compose up` remain supported
46+
for compatibility. New automation should use `DB_TYPE`.
47+
48+
### Isolated workers
49+
50+
The `dev-worker` helper gives automation a deterministic Compose project and
51+
mutable data directory per worker:
52+
53+
```bash
54+
sh ./dev-worker test-a up
55+
sh ./dev-worker test-a exec occ status
56+
sh ./dev-worker test-a logs
57+
sh ./dev-worker test-a destroy
4058
```
4159

60+
Select the existing PHP and Nextcloud dimensions in the same invocation:
61+
62+
```bash
63+
PHP_VERSION=83 VERSION_NEXTCLOUD=stable35 DB_TYPE=pgsql \
64+
sh ./dev-worker test-pg up
65+
```
66+
67+
Each worker stores mutable state under `.workers/<worker-id>/volumes` and uses
68+
its own Compose project name. Destroying one worker removes only that worker's
69+
Compose resources and mutable directory.
70+
71+
The lifecycle currently enables MySQL and PostgreSQL. SQLite and MariaDB are
72+
reserved by the `DB_TYPE` contract and are added by their database-specific
73+
workstreams.
74+
4275
## Optional services
4376

4477
Optional services are enabled with Docker Compose profiles:

0 commit comments

Comments
 (0)