diff --git a/.docker/bin/build-nextcloud-image b/.docker/bin/build-nextcloud-image new file mode 100644 index 00000000..351e652b --- /dev/null +++ b/.docker/bin/build-nextcloud-image @@ -0,0 +1,38 @@ +#!/bin/sh +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +set -eu + +repo_root="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)" +compose_file="$repo_root/docker-compose.yml" + +image="$( + docker compose --project-directory "$repo_root" --file "$compose_file" config --images | + grep '^ghcr.io/librecodecoop/nextcloud-dev-php' | + head -n 1 +)" + +[ -n "$image" ] || { + echo "Could not resolve the Nextcloud development image from docker-compose.yml" >&2 + exit 2 +} + +case "$image" in + ghcr.io/librecodecoop/nextcloud-dev-php*:latest) ;; + *) + echo "Unexpected Nextcloud development image: $image" >&2 + exit 2 + ;; +esac + +php_version="${image#*nextcloud-dev-php}" +php_version="${php_version%%:*}" +dockerfile="$repo_root/.docker/Dockerfile.php$php_version" + +[ -f "$dockerfile" ] || { + echo "No Dockerfile for PHP $php_version: $dockerfile" >&2 + exit 2 +} + +docker build -f "$dockerfile" -t "$image" "$repo_root/.docker" diff --git a/.github/workflows/proxy-tests.yml b/.github/workflows/proxy-tests.yml index 62a893a2..4392e6e9 100644 --- a/.github/workflows/proxy-tests.yml +++ b/.github/workflows/proxy-tests.yml @@ -23,7 +23,7 @@ jobs: detik-install: false file-install: false - name: Run unit tests - 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 + run: bats tests/proxy/lease.bats tests/proxy/infrastructure.bats tests/proxy/services.bats tests/proxy/diagnostics.bats tests/proxy/compose-policy.bats integration: name: Proxy Docker integration tests @@ -44,114 +44,3 @@ jobs: REPO_ROOT="$GITHUB_WORKSPACE" COMPOSE_PROJECT_NAME=proxytesta docker compose --file tests/proxy/fixtures/compose.yml config --quiet - name: Run Docker integration tests run: bats tests/proxy/integration.bats - - sqlite-worker: - name: SQLite worker integration - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Build PHP 8.3 development image - run: docker build -f .docker/Dockerfile.php83 -t ghcr.io/librecodecoop/nextcloud-dev-php83:latest .docker - - name: Start isolated SQLite workers - run: | - DB_TYPE=sqlite sh ./dev-worker sqlite-a up - DB_TYPE=sqlite sh ./dev-worker sqlite-b up - - name: Verify SQLite runtime - run: | - DB_TYPE=sqlite sh ./dev-worker sqlite-a exec php -r 'exit(extension_loaded("pdo_sqlite") ? 0 : 1);' - DB_TYPE=sqlite sh ./dev-worker sqlite-a exec occ status | grep -q 'installed: true' - DB_TYPE=sqlite sh ./dev-worker sqlite-b exec occ status | grep -q 'installed: true' - - name: Verify SQLite worker state isolation - run: | - DB_TYPE=sqlite sh ./dev-worker sqlite-a exec sh -c 'printf alpha > /var/www/html/data/worker-probe' - DB_TYPE=sqlite sh ./dev-worker sqlite-b exec sh -c 'printf beta > /var/www/html/data/worker-probe' - alpha="$(DB_TYPE=sqlite sh ./dev-worker sqlite-a exec cat /var/www/html/data/worker-probe)" - beta="$(DB_TYPE=sqlite sh ./dev-worker sqlite-b exec cat /var/www/html/data/worker-probe)" - printf 'sqlite-a worker_probe=%s\nsqlite-b worker_probe=%s\n' "$alpha" "$beta" - test "$alpha" = alpha - test "$beta" = beta - - name: Verify SQLite starts no database service - run: | - database_containers="$(docker ps --format '{{.Names}}' | grep -E '^ncdev-sqlite-(a|b)-database-1$' || true)" - if [ -n "$database_containers" ]; then - printf 'Unexpected database containers:\n%s\n' "$database_containers" >&2 - exit 1 - fi - - name: Collect worker logs - if: failure() - run: | - DB_TYPE=sqlite sh ./dev-worker sqlite-a logs || true - DB_TYPE=sqlite sh ./dev-worker sqlite-b logs || true - - name: Destroy workers - if: always() - run: | - DB_TYPE=sqlite sh ./dev-worker sqlite-a destroy || true - DB_TYPE=sqlite sh ./dev-worker sqlite-b destroy || true - - mariadb-worker: - name: MariaDB ${{ matrix.mariadb }} worker integration - runs-on: ubuntu-latest - timeout-minutes: 15 - strategy: - fail-fast: false - matrix: - mariadb: ['10.6', '10.11'] - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Build PHP 8.3 development image - run: docker build -f .docker/Dockerfile.php83 -t ghcr.io/librecodecoop/nextcloud-dev-php83:latest .docker - - name: Start MariaDB worker - env: - MARIADB_VERSION: ${{ matrix.mariadb }} - DB_SQL_MODE: ONLY_FULL_GROUP_BY - run: | - worker="maria-${MARIADB_VERSION//./-}" - DB_TYPE=mariadb sh ./dev-worker "$worker" up - - name: Verify MariaDB version and SQL mode - env: - MARIADB_VERSION: ${{ matrix.mariadb }} - DB_SQL_MODE: ONLY_FULL_GROUP_BY - run: | - worker="maria-${MARIADB_VERSION//./-}" - version="$(DB_TYPE=mariadb sh ./dev-worker "$worker" db-exec mariadb -uroot -proot -Nse 'SELECT VERSION()')" - case "$version" in - "$MARIADB_VERSION".*) ;; - *) echo "Unexpected MariaDB version: $version" >&2; exit 1 ;; - esac - DB_TYPE=mariadb sh ./dev-worker "$worker" db-exec mariadb -uroot -proot -Nse 'SELECT @@GLOBAL.sql_mode' | grep -q 'ONLY_FULL_GROUP_BY' - DB_TYPE=mariadb sh ./dev-worker "$worker" status | grep -q 'installed: true' - - name: Verify two-worker isolation - if: matrix.mariadb == '10.11' - env: - MARIADB_VERSION: ${{ matrix.mariadb }} - DB_SQL_MODE: ONLY_FULL_GROUP_BY - run: | - DB_TYPE=mariadb sh ./dev-worker maria-peer up - DB_TYPE=mariadb sh ./dev-worker maria-10-11 exec sh -c 'printf alpha > /var/www/html/data/worker-probe' - DB_TYPE=mariadb sh ./dev-worker maria-peer exec sh -c 'printf beta > /var/www/html/data/worker-probe' - alpha="$(DB_TYPE=mariadb sh ./dev-worker maria-10-11 exec cat /var/www/html/data/worker-probe)" - beta="$(DB_TYPE=mariadb sh ./dev-worker maria-peer exec cat /var/www/html/data/worker-probe)" - printf 'maria-10-11 worker_probe=%s\nmaria-peer worker_probe=%s\n' "$alpha" "$beta" - test "$alpha" = alpha - test "$beta" = beta - - name: Collect worker logs - if: failure() - env: - MARIADB_VERSION: ${{ matrix.mariadb }} - run: | - worker="maria-${MARIADB_VERSION//./-}" - DB_TYPE=mariadb sh ./dev-worker "$worker" logs || true - if [ "$MARIADB_VERSION" = 10.11 ]; then - DB_TYPE=mariadb sh ./dev-worker maria-peer logs || true - fi - - name: Destroy workers - if: always() - env: - MARIADB_VERSION: ${{ matrix.mariadb }} - run: | - worker="maria-${MARIADB_VERSION//./-}" - DB_TYPE=mariadb sh ./dev-worker "$worker" destroy || true - if [ "$MARIADB_VERSION" = 10.11 ]; then - DB_TYPE=mariadb sh ./dev-worker maria-peer destroy || true - fi diff --git a/.github/workflows/worker-tests.yml b/.github/workflows/worker-tests.yml new file mode 100644 index 00000000..ac6b3cd8 --- /dev/null +++ b/.github/workflows/worker-tests.yml @@ -0,0 +1,175 @@ +name: Worker tests + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + unit: + name: Worker contract tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Setup Bats-core + uses: bats-core/bats-action@77d6fb60505b4d0d1d73e48bd035b55074bbfb43 # 4.0.0 + with: + support-install: false + assert-install: false + detik-install: false + file-install: false + - name: Run worker contract tests + run: bats tests/worker/contract.bats + + sqlite: + name: SQLite worker integration + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Build default PHP development image + run: sh .docker/bin/build-nextcloud-image + - name: Start isolated SQLite workers + run: | + DB_TYPE=sqlite sh ./dev-worker sqlite-a up + DB_TYPE=sqlite sh ./dev-worker sqlite-b up + - name: Verify SQLite runtime + run: | + DB_TYPE=sqlite sh ./dev-worker sqlite-a exec php -r 'exit(extension_loaded("pdo_sqlite") ? 0 : 1);' + DB_TYPE=sqlite sh ./dev-worker sqlite-a exec occ status | grep -q 'installed: true' + DB_TYPE=sqlite sh ./dev-worker sqlite-b exec occ status | grep -q 'installed: true' + - name: Verify SQLite worker state isolation + run: | + DB_TYPE=sqlite sh ./dev-worker sqlite-a exec sh -c 'printf alpha > /var/www/html/data/worker-probe' + DB_TYPE=sqlite sh ./dev-worker sqlite-b exec sh -c 'printf beta > /var/www/html/data/worker-probe' + alpha="$(DB_TYPE=sqlite sh ./dev-worker sqlite-a exec cat /var/www/html/data/worker-probe)" + beta="$(DB_TYPE=sqlite sh ./dev-worker sqlite-b exec cat /var/www/html/data/worker-probe)" + printf 'sqlite-a worker_probe=%s\nsqlite-b worker_probe=%s\n' "$alpha" "$beta" + test "$alpha" = alpha + test "$beta" = beta + - name: Verify SQLite starts no database service + run: | + database_containers="$(docker ps --format '{{.Names}}' | grep -E '^ncdev-sqlite-(a|b)-database-1$' || true)" + if [ -n "$database_containers" ]; then + printf 'Unexpected database containers:\n%s\n' "$database_containers" >&2 + exit 1 + fi + - name: Collect worker logs + if: failure() + run: | + DB_TYPE=sqlite sh ./dev-worker sqlite-a logs || true + DB_TYPE=sqlite sh ./dev-worker sqlite-b logs || true + - name: Destroy workers + if: always() + run: | + DB_TYPE=sqlite sh ./dev-worker sqlite-a destroy || true + DB_TYPE=sqlite sh ./dev-worker sqlite-b destroy || true + + mariadb: + name: MariaDB ${{ matrix.mariadb }} worker integration + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + mariadb: ['10.6', '10.11'] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Build default PHP development image + run: sh .docker/bin/build-nextcloud-image + - name: Start MariaDB worker + env: + MARIADB_VERSION: ${{ matrix.mariadb }} + DB_SQL_MODE: ONLY_FULL_GROUP_BY + run: | + worker="maria-${MARIADB_VERSION//./-}" + DB_TYPE=mariadb sh ./dev-worker "$worker" up + - name: Verify MariaDB version and SQL mode + env: + MARIADB_VERSION: ${{ matrix.mariadb }} + DB_SQL_MODE: ONLY_FULL_GROUP_BY + run: | + worker="maria-${MARIADB_VERSION//./-}" + version="$(DB_TYPE=mariadb sh ./dev-worker "$worker" db-exec mariadb -uroot -proot -Nse 'SELECT VERSION()')" + case "$version" in + "$MARIADB_VERSION".*) ;; + *) echo "Unexpected MariaDB version: $version" >&2; exit 1 ;; + esac + DB_TYPE=mariadb sh ./dev-worker "$worker" db-exec mariadb -uroot -proot -Nse 'SELECT @@GLOBAL.sql_mode' | grep -q 'ONLY_FULL_GROUP_BY' + DB_TYPE=mariadb sh ./dev-worker "$worker" status | grep -q 'installed: true' + - name: Verify two-worker isolation + if: matrix.mariadb == '10.11' + env: + MARIADB_VERSION: ${{ matrix.mariadb }} + DB_SQL_MODE: ONLY_FULL_GROUP_BY + run: | + DB_TYPE=mariadb sh ./dev-worker maria-peer up + DB_TYPE=mariadb sh ./dev-worker maria-10-11 exec sh -c 'printf alpha > /var/www/html/data/worker-probe' + DB_TYPE=mariadb sh ./dev-worker maria-peer exec sh -c 'printf beta > /var/www/html/data/worker-probe' + alpha="$(DB_TYPE=mariadb sh ./dev-worker maria-10-11 exec cat /var/www/html/data/worker-probe)" + beta="$(DB_TYPE=mariadb sh ./dev-worker maria-peer exec cat /var/www/html/data/worker-probe)" + printf 'maria-10-11 worker_probe=%s\nmaria-peer worker_probe=%s\n' "$alpha" "$beta" + test "$alpha" = alpha + test "$beta" = beta + - name: Collect worker logs + if: failure() + env: + MARIADB_VERSION: ${{ matrix.mariadb }} + run: | + worker="maria-${MARIADB_VERSION//./-}" + DB_TYPE=mariadb sh ./dev-worker "$worker" logs || true + if [ "$MARIADB_VERSION" = 10.11 ]; then + DB_TYPE=mariadb sh ./dev-worker maria-peer logs || true + fi + - name: Destroy workers + if: always() + env: + MARIADB_VERSION: ${{ matrix.mariadb }} + run: | + worker="maria-${MARIADB_VERSION//./-}" + DB_TYPE=mariadb sh ./dev-worker "$worker" destroy || true + if [ "$MARIADB_VERSION" = 10.11 ]; then + DB_TYPE=mariadb sh ./dev-worker maria-peer destroy || true + fi + + compose-extension: + name: Compose extension integration + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Build default PHP development image + run: sh .docker/bin/build-nextcloud-image + - name: Start a worker with two externally mounted apps + env: + NCDD_COMPOSE_OVERRIDE: ${{ github.workspace }}/tests/worker/fixtures/compose-extension.yml + TEST_APP_A: ${{ github.workspace }}/tests/worker/fixtures/sample-app + TEST_APP_B: ${{ github.workspace }}/tests/worker/fixtures/other-app + run: DB_TYPE=sqlite sh ./dev-worker extension up + - name: Verify both apps use the same worker runtime + env: + NCDD_COMPOSE_OVERRIDE: ${{ github.workspace }}/tests/worker/fixtures/compose-extension.yml + TEST_APP_A: ${{ github.workspace }}/tests/worker/fixtures/sample-app + TEST_APP_B: ${{ github.workspace }}/tests/worker/fixtures/other-app + run: | + test "$(DB_TYPE=sqlite sh ./dev-worker extension exec cat /var/www/html/apps-extra/sample_app/marker.txt)" = alpha + test "$(DB_TYPE=sqlite sh ./dev-worker extension exec cat /var/www/html/apps-extra/other_app/marker.txt)" = other + DB_TYPE=sqlite sh ./dev-worker extension status | grep -q 'installed: true' + - name: Collect worker logs + if: failure() + env: + NCDD_COMPOSE_OVERRIDE: ${{ github.workspace }}/tests/worker/fixtures/compose-extension.yml + TEST_APP_A: ${{ github.workspace }}/tests/worker/fixtures/sample-app + TEST_APP_B: ${{ github.workspace }}/tests/worker/fixtures/other-app + run: DB_TYPE=sqlite sh ./dev-worker extension logs || true + - name: Destroy worker + if: always() + env: + NCDD_COMPOSE_OVERRIDE: ${{ github.workspace }}/tests/worker/fixtures/compose-extension.yml + TEST_APP_A: ${{ github.workspace }}/tests/worker/fixtures/sample-app + TEST_APP_B: ${{ github.workspace }}/tests/worker/fixtures/other-app + run: DB_TYPE=sqlite sh ./dev-worker extension destroy || true diff --git a/README.md b/README.md index 21d20d30..b9e1e66b 100644 --- a/README.md +++ b/README.md @@ -21,4 +21,5 @@ and other advanced configuration, see the - [Advanced setup](docs/advanced-setup.md) - [App development](docs/apps-development.md) -- [FAQ](docs/faq.md) \ No newline at end of file +- [Compose extensions and devcontainers](docs/compose-extensions.md) +- [FAQ](docs/faq.md) diff --git a/dev-worker b/dev-worker index 28131854..5a3e041c 100644 --- a/dev-worker +++ b/dev-worker @@ -14,6 +14,7 @@ Commands: logs Print worker logs config Render the resolved Compose configuration status Run occ status in the worker + urls Print the worker's Nextcloud and Mailpit URLs db-exec Run a command in the selected database container destroy Stop the worker and remove only its mutable state @@ -24,6 +25,7 @@ Environment: DB_SQL_MODE= Optional global SQL mode for MySQL-compatible backends PHP_VERSION= Existing PHP image selector VERSION_NEXTCLOUD= Existing Nextcloud ref selector + NCDD_COMPOSE_OVERRIDE= Optional Compose override appended to the base topology WORKER_READY_TIMEOUT=180 Seconds to wait for a clean installation EOF } @@ -54,6 +56,26 @@ esac repo_root="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" db_type="${DB_TYPE:-mysql}" +compose_override="${NCDD_COMPOSE_OVERRIDE:-}" + +if [ -n "$compose_override" ]; then + case "$compose_override" in + /*) ;; + *) + override_dir="$(dirname -- "$compose_override")" + override_name="$(basename -- "$compose_override")" + [ -d "$override_dir" ] || { + echo "NCDD_COMPOSE_OVERRIDE directory does not exist: $override_dir" >&2 + exit 2 + } + compose_override="$(CDPATH= cd -- "$override_dir" && pwd -P)/$override_name" + ;; + esac + [ -f "$compose_override" ] || { + echo "NCDD_COMPOSE_OVERRIDE is not a file: $compose_override" >&2 + exit 2 + } +fi case "$db_type" in sqlite|mysql|mariadb|pgsql) ;; @@ -89,7 +111,13 @@ else fi compose() { - docker compose --project-directory "$repo_root" --file "$repo_root/docker-compose.yml" "$@" + if [ -n "$compose_override" ]; then + docker compose --project-directory "$repo_root" \ + --file "$repo_root/docker-compose.yml" \ + --file "$compose_override" "$@" + else + docker compose --project-directory "$repo_root" --file "$repo_root/docker-compose.yml" "$@" + fi } wait_for_mariadb() { @@ -169,6 +197,10 @@ case "$command" in status) compose exec -T nextcloud occ status ;; + urls) + printf 'Nextcloud: https://%s.localhost\n' "$COMPOSE_PROJECT_NAME" + printf 'Mailpit: https://%s-mailpit.localhost\n' "$COMPOSE_PROJECT_NAME" + ;; db-exec) [ "$db_type" != "sqlite" ] || { echo "SQLite workers do not have a database container" >&2 @@ -186,7 +218,9 @@ case "$command" in # worker root through the existing Nextcloud image as root instead of # relying on host ownership. if [ -d "$worker_root" ]; then - compose run --rm --no-deps -u 0 --entrypoint sh -v "$worker_root:/worker" nextcloud -c 'find /worker -mindepth 1 -exec rm -rf -- {} +' >/dev/null 2>&1 || true + docker compose --project-directory "$repo_root" --file "$repo_root/docker-compose.yml" \ + run --rm --no-deps -u 0 --entrypoint sh -v "$worker_root:/worker" nextcloud \ + -c 'find /worker -mindepth 1 -exec rm -rf -- {} +' >/dev/null 2>&1 || true fi case "$worker_root" in "$repo_root"/.workers/"$worker_id"|"$repo_root"/.workers/"$worker_id"/) ;; diff --git a/docker-compose.yml b/docker-compose.yml index 977ef331..7a880da9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,9 +9,6 @@ services: - ${DB_HOST:-${DB_TYPE:-mysql}} nextcloud: image: ghcr.io/librecodecoop/nextcloud-dev-php${PHP_VERSION:-83}:latest - # build: - # context: .docker/ - # dockerfile: Dockerfile.php${PHP_VERSION:-83} volumes: - ./.docker/scripts/wait-for-db.php:/var/www/scripts/wait-for-db.php - ${WORKER_VOLUMES_DIR:-./volumes}/php:/var/www/php-config diff --git a/docs/apps-development.md b/docs/apps-development.md index e7ab966f..7827125c 100644 --- a/docs/apps-development.md +++ b/docs/apps-development.md @@ -1,29 +1,61 @@ # Start development of apps -You will need create (or clone) the folder of the app that you will work inside the folder `volumes/nextcloud/apps-extra`. - -It's not required install all dependencis like php or nodejs to develop apps, with this project is only use the bash in container to compile app. - -## Sample - -Using the [LibreSign](https://github.com/LibreSign/libresign): - -To install LibreSign in the structure of develop is required [up servicer](#up-services). After nextcloud config and install. - - open folder `volumes/nextcloud/app-extra` - - clone project with `git clone https://github.com/LibreSign/libresign.git` - - open bash in nextcloud container with `docker compose exec -u www-data nextcloud bash` - - go to folder `apps-extra/libresign` - ```bash - cd apps-extra/libresign - ``` - - Now you can run all the necessaries commands to build the project, i.e: - ```bash - # download composer dependencies - composer install - # download JS dependencies - npm ci - # build and watch JS changes - npm run watch - ``` +Nextcloud applications can continue to be developed directly under +`volumes/nextcloud/apps-extra`. That directory is an app space and may contain +as many applications as a development setup needs. + +For repositories that keep application checkouts outside this repository, use a +[Compose extension](compose-extensions.md) rather than creating another +Nextcloud topology. The extension may mount one app, several apps, or add +supporting services while NCDD remains responsible for the runtime. + +## Local apps-extra workflow + +Clone or create applications under: + +```text +volumes/nextcloud/apps-extra/ +``` + +Then use the `nextcloud` container to install dependencies or run development +commands: + +```bash +docker compose exec -u www-data nextcloud bash +cd apps-extra/my_app +composer install +npm ci +``` + +## External application checkouts + +A project-specific Compose override can mount any number of source trees into +the same worker. For example: + +```yaml +services: + nextcloud: + volumes: + - /work/my_app:/var/www/html/apps-extra/my_app + - /work/my_dependency:/var/www/html/apps-extra/my_dependency +``` + +Start the runtime with: + +```bash +NCDD_COMPOSE_OVERRIDE=/work/project/ncdd.override.yml \ +DB_TYPE=sqlite \ +sh ./dev-worker my-project up +``` + +Application-specific setup remains owned by the application repository. It can +run commands explicitly through the worker, for example: + +```bash +NCDD_COMPOSE_OVERRIDE=/work/project/ncdd.override.yml \ +DB_TYPE=sqlite \ +sh ./dev-worker my-project exec sh -lc \ + 'cd /var/www/html/apps-extra/my_app && composer install' +``` ⬅️ [Back to index](../README.md) diff --git a/docs/compose-extensions.md b/docs/compose-extensions.md new file mode 100644 index 00000000..00e4115b --- /dev/null +++ b/docs/compose-extensions.md @@ -0,0 +1,103 @@ +# Compose extensions and devcontainers + +The base Docker Compose topology in this repository is the canonical Nextcloud +development runtime. Projects may extend that topology without teaching NCDD +which application is being developed. + +This keeps one lifecycle for Nextcloud, PHP, databases, proxy, mail and optional +services while allowing a consumer to mount zero, one or many applications and +add project-specific services. + +## Worker extension contract + +Create a normal Compose override in the consuming project. For example: + +```yaml +services: + nextcloud: + volumes: + - /work/app-a:/var/www/html/apps-extra/app_a + - /work/app-b:/var/www/html/apps-extra/app_b + nginx: + volumes: + - /work/app-a:/var/www/html/apps-extra/app_a:ro + - /work/app-b:/var/www/html/apps-extra/app_b:ro +``` + +Pass that file to the existing worker lifecycle: + +```bash +NCDD_COMPOSE_OVERRIDE=/work/project/ncdd.override.yml \ +DB_TYPE=sqlite \ +sh ./dev-worker project-a up +``` + +The same override must be supplied to worker commands that need the complete +Compose model: + +```bash +NCDD_COMPOSE_OVERRIDE=/work/project/ncdd.override.yml \ +DB_TYPE=sqlite \ +sh ./dev-worker project-a status + +NCDD_COMPOSE_OVERRIDE=/work/project/ncdd.override.yml \ +DB_TYPE=sqlite \ +sh ./dev-worker project-a destroy +``` + +The override is deliberately generic. NCDD does not assign a primary app, +persist an app id, decide where application source lives, or execute +application-specific setup hooks. + +## Runtime dimensions + +Compose extensions reuse the same worker dimensions as the base environment: + +- `PHP_VERSION` +- `VERSION_NEXTCLOUD` +- `DB_TYPE` +- `MARIADB_VERSION` where applicable +- `DB_SQL_MODE` where applicable + +Run: + +```bash +sh ./dev-worker project-a urls +``` + +to print the deterministic Nextcloud and Mailpit hostnames for a worker. + +## Application setup + +Dependency installation, app enablement and other initialization belong to the +consumer repository. Keep those steps in its scripts, Makefile, task runner or +devcontainer lifecycle and invoke the NCDD worker when container access is +needed. + +For example: + +```bash +NCDD_COMPOSE_OVERRIDE=/work/project/ncdd.override.yml \ +sh ./dev-worker project-a exec sh -lc \ + 'cd /var/www/html/apps-extra/app_a && composer install && occ app:enable app_a' +``` + +This avoids growing an NCDD API for application-specific setup. + +## Devcontainers + +A downstream devcontainer should remain a thin adapter over the same Compose +topology. It can use the `nextcloud` service as its runtime service and add a +project-owned override with its source mounts or extra services. + +Do not copy the Nextcloud, database, proxy, mail or Redis definitions into the +application repository. Those remain owned by NCDD. + +## Isolation + +Each worker owns its Compose project name and mutable state under +`.workers/`. A Compose extension changes that worker's service +model but does not create a second lifecycle. + +The worker id is an isolation namespace for development convenience, not a +security boundary between untrusted workloads sharing the same Docker daemon. diff --git a/tests/worker/contract.bats b/tests/worker/contract.bats index 668074f7..6a5ee5ce 100644 --- a/tests/worker/contract.bats +++ b/tests/worker/contract.bats @@ -69,8 +69,42 @@ setup() { [[ "$output" == *"Unsupported MARIADB_VERSION"* ]] } -@test "supported PHP images install PDO SQLite" { - for dockerfile in "$REPO_ROOT"/.docker/Dockerfile.php81 "$REPO_ROOT"/.docker/Dockerfile.php82 "$REPO_ROOT"/.docker/Dockerfile.php83; do +@test "all PHP development images install PDO SQLite" { + found=0 + for dockerfile in "$REPO_ROOT"/.docker/Dockerfile.php*; do + [ -f "$dockerfile" ] || continue + found=1 grep -q 'pdo_sqlite' "$dockerfile" done + [ "$found" -eq 1 ] +} + +@test "worker accepts a generic Compose override with multiple app mounts" { + override="$REPO_ROOT/tests/worker/fixtures/compose-extension.yml" + app_a="$REPO_ROOT/tests/worker/fixtures/sample-app" + app_b="$REPO_ROOT/tests/worker/fixtures/other-app" + + run env \ + NCDD_COMPOSE_OVERRIDE="$override" \ + TEST_APP_A="$app_a" \ + TEST_APP_B="$app_b" \ + DB_TYPE=sqlite \ + sh "$WORKER" compose-extension config + + [ "$status" -eq 0 ] + [[ "$output" == *"source: $app_a"* ]] + [[ "$output" == *"target: /var/www/html/apps-extra/sample_app"* ]] + [[ "$output" == *"source: $app_b"* ]] + [[ "$output" == *"target: /var/www/html/apps-extra/other_app"* ]] +} + +@test "worker rejects a missing Compose override before startup" { + run env NCDD_COMPOSE_OVERRIDE="$REPO_ROOT/does-not-exist.yml" sh "$WORKER" missing-override config + [ "$status" -eq 2 ] + [[ "$output" == *"NCDD_COMPOSE_OVERRIDE is not a file"* ]] +} + +@test "worker CI does not pin a concrete PHP series" { + workflow="$REPO_ROOT/.github/workflows/worker-tests.yml" + ! grep -Eq 'Dockerfile\.php[0-9]+|nextcloud-dev-php[0-9]+' "$workflow" } diff --git a/tests/worker/fixtures/compose-extension.yml b/tests/worker/fixtures/compose-extension.yml new file mode 100644 index 00000000..d6a818d0 --- /dev/null +++ b/tests/worker/fixtures/compose-extension.yml @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +services: + nextcloud: + volumes: + - type: bind + source: ${TEST_APP_A:?TEST_APP_A is required} + target: /var/www/html/apps-extra/sample_app + - type: bind + source: ${TEST_APP_B:?TEST_APP_B is required} + target: /var/www/html/apps-extra/other_app + nginx: + volumes: + - type: bind + source: ${TEST_APP_A:?TEST_APP_A is required} + target: /var/www/html/apps-extra/sample_app + read_only: true + - type: bind + source: ${TEST_APP_B:?TEST_APP_B is required} + target: /var/www/html/apps-extra/other_app + read_only: true + playwright: + volumes: + - type: bind + source: ${TEST_APP_A:?TEST_APP_A is required} + target: /var/www/html/apps-extra/sample_app + - type: bind + source: ${TEST_APP_B:?TEST_APP_B is required} + target: /var/www/html/apps-extra/other_app diff --git a/tests/worker/fixtures/other-app/appinfo/info.xml b/tests/worker/fixtures/other-app/appinfo/info.xml new file mode 100644 index 00000000..2195178d --- /dev/null +++ b/tests/worker/fixtures/other-app/appinfo/info.xml @@ -0,0 +1,18 @@ + + + + other_app + Worker fixture app + Fixture for the downstream worker contract + Fixture for the downstream worker contract. + 1.0.0 + agpl + LibreCode coop and contributors + OtherApp + + + + diff --git a/tests/worker/fixtures/other-app/marker.txt b/tests/worker/fixtures/other-app/marker.txt new file mode 100644 index 00000000..e45c9c26 --- /dev/null +++ b/tests/worker/fixtures/other-app/marker.txt @@ -0,0 +1 @@ +other diff --git a/tests/worker/fixtures/sample-app/appinfo/info.xml b/tests/worker/fixtures/sample-app/appinfo/info.xml new file mode 100644 index 00000000..abb1a440 --- /dev/null +++ b/tests/worker/fixtures/sample-app/appinfo/info.xml @@ -0,0 +1,18 @@ + + + + sample_app + Worker fixture app + Fixture for the downstream worker contract + Fixture for the downstream worker contract. + 1.0.0 + agpl + LibreCode coop and contributors + SampleApp + + + + diff --git a/tests/worker/fixtures/sample-app/marker.txt b/tests/worker/fixtures/sample-app/marker.txt new file mode 100644 index 00000000..4a580070 --- /dev/null +++ b/tests/worker/fixtures/sample-app/marker.txt @@ -0,0 +1 @@ +alpha