diff --git a/Makefile b/Makefile index 3ddea93..567dfd2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,2 @@ -PHP_VERSION=7.4 - include $(FAB_PATH)/common/mk/turnkey/lamp.mk include $(FAB_PATH)/common/mk/turnkey.mk diff --git a/README.rst b/README.rst index 7a72c83..44ffd94 100644 --- a/README.rst +++ b/README.rst @@ -9,16 +9,22 @@ wherever you are, when you need it. This appliance includes all the standard features in `TurnKey Core`_, and on top of that: -- ownCloud Server: +- ownCloud Server 11: - - Installed from official zip file to /var/www/owncloud. - - Data directory (/var/www/owncloud-data) outside the webroot (security). + - Runs from the official ownCloud container image, pinned to a verified + amd64 manifest digest. + - Stores persistent configuration and user files under + /var/lib/owncloud. + - Uses Debian MariaDB and Redis services on a dedicated local Docker + network. - Includes occ_ script for command line administration and configuration. Also includes turnkey-occ_ wrapper script (runs occ as www-data user). - **Security note**: Updates to ownCloud may require supervision so - they **ARE NOT** configured to install automatically. See `ownCloud - documentation`_ for upgrading. + **Security note**: ownCloud updates require supervision and are not + installed automatically. Use ``owncloud-update --check VERSION`` to + inspect the official amd64 image digest, then run + ``owncloud-update VERSION DIGEST`` after reviewing the release and + backing up the appliance. See `ownCloud documentation`_ for upgrading. - SSL support out of the box. - `Adminer`_ administration frontend for MySQL (listening on port @@ -39,5 +45,5 @@ Credentials *(passwords set at first boot)* .. _TurnKey Core: https://www.turnkeylinux.org/core .. _occ: https://doc.owncloud.com/server/admin_manual/configuration/server/occ_command.html .. _turnkey-occ: https://github.com/turnkeylinux-apps/owncloud/blob/master/overlay/usr/local/bin/turnkey-occ -.. _ownCloud documentation: https://doc.owncloud.org/server/admin_manual/maintenance/upgrade.html +.. _ownCloud documentation: https://doc.owncloud.com/server/11.0/admin_manual/maintenance/upgrading/manual_upgrade.html .. _Adminer: https://www.adminer.org diff --git a/changelog b/changelog index 9630130..a7b463b 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,14 @@ +turnkey-owncloud-19.0 (1) turnkey; urgency=low + + * Run ownCloud Server 11.0.0 from the official container image pinned to + its amd64 manifest digest. + + * Use Debian 13 Trixie for Docker, Apache, MariaDB, Redis and Adminer. + + * Add a supervised ownCloud container update command. + + -- TurnKey Linux Developers Mon, 24 Aug 2026 13:00:00 +0000 + turnkey-owncloud-18.0 (1) turnkey; urgency=low * Includes latest stable ownCloud - v10.13.1. @@ -293,4 +304,3 @@ turnkey-owncloud-12.0 (1) turnkey; urgency=low appliances. Here we only describe changes specific to this appliance. -- Alon Swartz Wed, 01 Aug 2012 08:00:00 +0200 - diff --git a/conf.d/downloads b/conf.d/downloads index f2a5c1a..f80e5b3 100755 --- a/conf.d/downloads +++ b/conf.d/downloads @@ -1,10 +1,21 @@ -#!/bin/bash -ex +#!/bin/bash +set -Eeuo pipefail -dl() { - [ "$FAB_HTTP_PROXY" ] && PROXY="--proxy $FAB_HTTP_PROXY" - cd $2; curl -L -f $PROXY $1 > $3; cd - -} +image=owncloud/server@sha256:dbebc24fe77a35c5de621d38a3f7264ffb43e3ee321928cde9a563a72b8ff366 +archive=/usr/local/src/owncloud-server.tar +local_tag=owncloud/server:11.0.0-turnkey-dbebc24fe77a -URL="https://download.owncloud.com/server/stable/owncloud-complete-20230906.zip" +if [[ -n ${FAB_HTTP_PROXY:-} ]]; then + export HTTP_PROXY=$FAB_HTTP_PROXY + export HTTPS_PROXY=$FAB_HTTP_PROXY +fi -dl $URL /usr/local/src owncloud.zip +skopeo copy --override-os linux --override-arch amd64 \ + "docker://docker.io/$image" "docker-archive:$archive:$local_tag" + +cat > /usr/local/src/owncloud-image-source <ownCloud Appliance \– Powered by TurnKey Linux|" $TEMPLATE +REDIS_PASS=$(mcookie) # configure apache a2dissite 000-default a2ensite owncloud.conf a2enmod rewrite a2enmod headers -a2enmod env -a2enmod dir -a2enmod mime +a2enmod proxy +a2enmod proxy_http # start services service mysql start @@ -36,40 +19,53 @@ service apache2 start # setup the database mysqladmin create $DB_NAME -mysql --batch --execute "grant all privileges on $DB_NAME.* to $DB_USER@localhost identified by '$DB_PASS'; flush privileges;" +mysql --batch --execute "CREATE USER '$DB_USER'@'172.28.0.%' IDENTIFIED BY '$DB_PASS'; GRANT ALL PRIVILEGES ON $DB_NAME.* TO '$DB_USER'@'172.28.0.%'; FLUSH PRIVILEGES;" -# command-line install -chown -R www-data:www-data $WEBROOT -cd $WEBROOT -php occ maintenance:install --database "mysql" \ - --database-name "$DB_NAME" --database-user "$DB_USER" --database-pass "$DB_PASS" \ - --admin-user "$ADMIN_NAME" --admin-pass "$ADMIN_PASS" \ - --data-dir "$DATAROOT" +install -d -m 700 /etc/owncloud +install -d -m 700 /var/lib/owncloud +install -m 600 /usr/local/src/owncloud-image-source /etc/owncloud/image-source -# enable notify owncloud that we are using cron -mysql --batch --database=$DB_NAME --execute "INSERT INTO oc_appconfig (appid, configkey, configvalue) VALUES ('core', 'backgroundjobs_mode', 'cron');" - -# adjust owncloud config for redis -sed -i '/^);/d' $CONF -cat >> $CONF < '\OC\Memcache\Redis', - 'redis' => [ - 'host' => '/var/run/redis/redis.sock', - 'port' => 0, - ], - 'memcache.locking' => '\OC\Memcache\Redis', -); +cat > /etc/owncloud/image.conf < /etc/owncloud/owncloud.env < /etc/mysql/mariadb.conf.d/60-owncloud.cnf <<'EOF' +[mariadbd] +bind-address = 172.28.0.1 +EOF -chown -R www-data:www-data $WEBROOT $DATAROOT +sed -i 's/^bind .*/bind 172.28.0.1/' /etc/redis/redis.conf +sed -i "s/^# requirepass .*/requirepass $REDIS_PASS/" /etc/redis/redis.conf # stop services service mysql stop service apache2 stop + +systemctl enable docker.service +install -d /etc/systemd/system/multi-user.target.wants +ln -s ../owncloud-network.service \ + /etc/systemd/system/multi-user.target.wants/owncloud-network.service +ln -s ../owncloud.service \ + /etc/systemd/system/multi-user.target.wants/owncloud.service diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md new file mode 100644 index 0000000..23c49c3 --- /dev/null +++ b/docs/v19.0-testing.md @@ -0,0 +1,125 @@ +# ownCloud 19.0 testing + +## Scope and source decision + +ownCloud Server 11 supports PHP 8.3 or newer but supports deployment only +through the official ownCloud container image. Debian 13 Trixie provides PHP +8.4, so continuing the v18 manual archive installation would leave the +appliance outside ownCloud's maintained deployment contract. A disposable +Trixie probe confirmed that the 11.0.0 manual archive installs but fails when +it next opens its SQLite database on PHP 8.4. + +The appliance therefore uses the official ownCloud Server 11.0.0 image for +amd64, pinned to manifest digest +`sha256:dbebc24fe77a35c5de621d38a3f7264ffb43e3ee321928cde9a563a72b8ff366`. +Skopeo retrieves and verifies that digest during the build. Docker, its +overlay-filesystem fallback, Skopeo, Apache, MariaDB, Redis and Adminer come +from signed Debian Trixie repositories. + +The ownCloud container joins a dedicated `172.28.0.0/24` bridge. Debian +MariaDB and Redis listen only on its gateway, `172.28.0.1`, and Redis requires +a generated password. The ownCloud HTTP port binds only to host loopback. +Apache provides the public HTTPS endpoint. Persistent ownCloud configuration +and user data are stored under `/var/lib/owncloud`. + +## Acceptance command + +```sh +TKLDEV_CONTAINER=tkldev19-wave2 \ +TKL_HARNESS_STATE_DIR=/home/agent/.local/state/turnkey-v19-harness-wave2 \ +TKL_HARNESS_LOCK_FILE=/home/agent/.local/state/turnkey-v19-harness-wave2/build.lock \ +TKL_HARNESS_DOCKER_LIMIT_BYTES=42949672960 \ +/sandboxed-git/turnkey/tools/test-v19-appliance owncloud \ + --source /home/agent/.local/worktrees/turnkey-apps/owncloud/wish-owncloud-v19-trixie +``` + +The command builds the Trixie root filesystem, imports it into Docker, boots +the normal systemd and firstboot path, then runs `tests/v19.sh`. The accepted +run, tested source, harness commit, report path and exact installed versions +are recorded below after exact acceptance passes. + +## README crosswalk + +| README contract | Focused check | Required result | Evidence | +| --- | --- | --- | --- | +| ownCloud is available through the HTTPS web endpoint | Request `/` and `/status.php` through Apache | The login page and installed ownCloud 11 status are returned over HTTPS; HTTP redirects to HTTPS | `tests/v19.sh` | +| The firstboot `admin` credential controls ownCloud | Request the authenticated OCS user endpoint | The response identifies the `admin` account | `tests/v19.sh` | +| Users can store and retrieve files | Create, read, update and delete a file through WebDAV, with MariaDB readback | Each content comparison passes, the database records the file and the deleted path returns HTTP 404 | `tests/v19.sh` | +| The opening description names contacts, photo galleries and calendars | Classify these product-level examples against focused appliance coverage | Deferred; v19 acceptance covers the WebDAV file lifecycle, not CardDAV, CalDAV or the gallery interface | Deferred | +| `turnkey-occ` provides command-line administration as `www-data` | Create, list and delete a temporary user through the wrapper | The user appears in both `occ` and MariaDB before deletion | `tests/v19.sh` | +| Persistent data is outside the application image | Inspect the running container and its `/mnt/data` mount | The official image runs with `/var/lib/owncloud` mounted at `/mnt/data` | `tests/v19.sh` | +| MariaDB and Redis are private application backends | Inspect listeners, Docker network addressing and Redis authentication | Both services listen only on the private bridge gateway; Redis rejects unauthenticated requests | `tests/v19.sh` | +| Background jobs run through cron | Inspect the installed schedule and OwnCloud background-job mode, then execute the scheduled command | The cron service and schedule are enabled, the mode is `cron` and `turnkey-occ system:cron` succeeds | `tests/v19.sh` | +| Adminer manages MariaDB over HTTPS on port 12322 | Submit the firstboot `adminer` login to the ownCloud database | Adminer accepts the credentials and exposes the ownCloud database | `tests/v19.sh` | +| ownCloud and Debian components have maintained update channels | Query `owncloud-update --check` and refresh APT metadata without installing changes | The official image digest and eligible signed Trixie candidates are returned; installed versions and the running image remain unchanged | `tests/v19.sh` | +| Webmin, SSH, Postfix, backup and other base services come from the inherited appliance layers | Exercise app-specific services and cite the unchanged Core and LAMP boundaries | Application services pass; inherited services retain their existing contracts | Core 19 and LAMP 19 acceptance reports | + +## Update channel + +`owncloud-update --check VERSION` resolves the current official amd64 digest +for an explicit ownCloud version tag without changing the appliance. After an +administrator reviews the release and takes a backup, +`owncloud-update VERSION sha256:DIGEST` requires the reviewed digest to match +the official tag, pulls that exact image, assigns a digest-derived local tag, +updates `/etc/owncloud/image.conf` and restarts the service. ownCloud performs +its supported database migration when the new container starts. + +Debian and TurnKey components continue through their signed Trixie APT +repositories. The focused updater check refreshes metadata, verifies eligible +candidates and proves that the check itself leaves installed packages and the +running ownCloud image unchanged. + +## Probe evidence + +The official 11.0.0 image was exercised in a disposable bridge setup before +implementation. It connected to Trixie MariaDB and Redis on the bridge +gateway, initialized the administrator, reported version 11.0.0, created a +second user and persisted both users in MariaDB. The failed manual archive and +Unix-socket connection probes are recorded as disproved assumptions rather +than supported deployment paths. + +## Accepted evidence + +Exact acceptance passed on 26 August 2026. + +- Run: `20260826t051749z-4089-14788` +- Tested source: `15e205df0a866da126ce1c9a54fdb1ce8422da51` +- Harness: `54ca2cff6b98a034089d186b249835b58fc467da` +- Report: `/home/agent/.local/state/turnkey-v19-harness-wave2/runs/owncloud/20260826t051749z-4089-14788/report.txt` +- Report SHA-256: `5f3f3c7e0b02071ee2e0b2551cadd2f9379f6591f65ac78abc3d9de9395a11ab` +- Retained evidence: all 8 checksums verified; credential-disclosure scan found 0 matches +- Verdict: `PASS` +- Installed versions: ownCloud 11.0.0, Apache 2.4.68, + MariaDB 11.8.6, Redis 8.0.2, Docker 26.1.5, + fuse-overlayfs 1.14 and Skopeo 1.18.0 + +The run completed normal initialization and firstboot, authenticated the +administrator, exercised WebDAV create, read, update and delete with MariaDB +readback, created and deleted a user through `turnkey-occ`, ran the cron +background command, verified authenticated Redis access and unauthenticated +denial, logged in to Adminer and checked both update channels without changing +the running image or installed packages. The report records the persistent +data mount, private network, signed Trixie package metadata and pinned official +amd64 image digest. + +## Known limitations + +The Docker acceptance path does not exercise the installer, kernel, +bootloader or physical hardware. It does exercise the appliance's nested +Docker daemon in the privileged systemd runtime, the same services and +network layout used on a normal installed appliance. The nested runtime uses +`fuse-overlayfs` because its root is already overlay-backed; a normal appliance +installed on a supported filesystem can use Docker's `overlay2` driver. + +ownCloud Server 11 supports only container deployment. Administrators who add +applications requiring extra operating-system binaries must derive and pin a +custom image as described by ownCloud. This appliance intentionally keeps the +official image unchanged. + +## Deferred issues + +The acceptance flow covers the administrator endpoint, WebDAV file lifecycle, +MariaDB persistence, Redis authentication and command-line user management. +Desktop and mobile synchronization clients are not exercised because their +network behavior uses the same WebDAV endpoint and requires external client +systems. diff --git a/overlay/etc/apache2/sites-available/owncloud.conf b/overlay/etc/apache2/sites-available/owncloud.conf index f4a9db5..134fc15 100644 --- a/overlay/etc/apache2/sites-available/owncloud.conf +++ b/overlay/etc/apache2/sites-available/owncloud.conf @@ -11,15 +11,12 @@ ServerName localhost SSLEngine on ServerAdmin webmaster@localhost - DocumentRoot /var/www/owncloud/ + ProxyPreserveHost On + ProxyPass / http://127.0.0.1:8080/ + ProxyPassReverse / http://127.0.0.1:8080/ + RequestHeader set X-Forwarded-Proto "https" Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains" - - - Options +FollowSymLinks - AllowOverride All - Require all granted - diff --git a/overlay/etc/cron.d/owncloud b/overlay/etc/cron.d/owncloud index c8b8754..479afba 100644 --- a/overlay/etc/cron.d/owncloud +++ b/overlay/etc/cron.d/owncloud @@ -1,2 +1,2 @@ # run cron.php for owncloud -*/15 * * * * www-data /usr/bin/php -f /var/www/owncloud/occ system:cron /dev/null 2>&1 +*/15 * * * * root /usr/local/bin/turnkey-occ system:cron >/dev/null 2>&1 diff --git a/overlay/etc/systemd/system/mariadb.service.d/owncloud-network.conf b/overlay/etc/systemd/system/mariadb.service.d/owncloud-network.conf new file mode 100644 index 0000000..872ed0c --- /dev/null +++ b/overlay/etc/systemd/system/mariadb.service.d/owncloud-network.conf @@ -0,0 +1,3 @@ +[Unit] +Requires=owncloud-network.service +After=owncloud-network.service diff --git a/overlay/etc/systemd/system/owncloud-network.service b/overlay/etc/systemd/system/owncloud-network.service new file mode 100644 index 0000000..0d8d436 --- /dev/null +++ b/overlay/etc/systemd/system/owncloud-network.service @@ -0,0 +1,13 @@ +[Unit] +Description=Prepare the private ownCloud container network +Requires=docker.service +After=docker.service +Before=mariadb.service redis-server.service owncloud.service + +[Service] +Type=oneshot +ExecStart=/usr/local/lib/owncloud/network-ready +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target diff --git a/overlay/etc/systemd/system/owncloud.service b/overlay/etc/systemd/system/owncloud.service new file mode 100644 index 0000000..ef38ce7 --- /dev/null +++ b/overlay/etc/systemd/system/owncloud.service @@ -0,0 +1,15 @@ +[Unit] +Description=ownCloud Server container +Requires=docker.service owncloud-network.service mariadb.service redis-server.service +After=docker.service owncloud-network.service mariadb.service redis-server.service + +[Service] +Type=simple +ExecStartPre=/usr/local/lib/owncloud/wait-firstboot +ExecStart=/usr/local/lib/owncloud/run +ExecStop=-/usr/bin/docker stop --time 30 owncloud_server +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/overlay/etc/systemd/system/redis-server.service.d/owncloud-network.conf b/overlay/etc/systemd/system/redis-server.service.d/owncloud-network.conf new file mode 100644 index 0000000..872ed0c --- /dev/null +++ b/overlay/etc/systemd/system/redis-server.service.d/owncloud-network.conf @@ -0,0 +1,3 @@ +[Unit] +Requires=owncloud-network.service +After=owncloud-network.service diff --git a/overlay/usr/lib/inithooks/bin/owncloud.py b/overlay/usr/lib/inithooks/bin/owncloud.py index b7bc7d2..600bd74 100755 --- a/overlay/usr/lib/inithooks/bin/owncloud.py +++ b/overlay/usr/lib/inithooks/bin/owncloud.py @@ -9,10 +9,11 @@ import sys import getopt +import json +import os import subprocess -from subprocess import call -from os.path import * -from os import chdir +import time +import urllib.request from libinithooks.dialog_wrapper import Dialog @@ -62,18 +63,61 @@ def main(): if domain == "DEFAULT": domain = DEFAULT_DOMAIN - sedcom = """ - /0 => 'localhost',/ a\ - 1 => '%s', - """ - - conf = '/var/www/owncloud/config/config.php' - call(['sed', '-i', "/1 => /d", conf]) - call(['sed', '-i', sedcom % domain, conf]) - - call(['/usr/local/bin/turnkey-occ', 'user:resetpassword', '--password-from-env admin'], - cwd='/var/www/owncloud', - env={"OC_PASS": password}) + occ = '/usr/local/bin/turnkey-occ' + for _ in range(150): + status = subprocess.run([occ, 'status'], check=False, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True) + if status.returncode == 0 and '- installed: true' in status.stdout: + break + time.sleep(2) + else: + raise RuntimeError('ownCloud did not become ready within 300 seconds') + + env = os.environ.copy() + env['OC_PASS'] = password + subprocess.run([occ, 'user:resetpassword', '--password-from-env', 'admin'], + check=True, env=env) + subprocess.run([occ, 'config:system:set', 'trusted_domains', '1', + f'--value={domain}'], check=True) + subprocess.run([occ, 'config:system:set', 'overwrite.cli.url', + f'--value=https://{domain}'], check=True) + + env_path = '/etc/owncloud/owncloud.env' + with open(env_path, encoding='utf-8') as source: + lines = source.readlines() + replacements = { + 'OWNCLOUD_DOMAIN': domain, + 'OWNCLOUD_TRUSTED_DOMAINS': f'localhost,127.0.0.1,{domain}', + 'OWNCLOUD_OVERWRITE_CLI_URL': f'https://{domain}', + } + with open(env_path, 'w', encoding='utf-8') as target: + for line in lines: + key = line.partition('=')[0] + if key == 'OWNCLOUD_ADMIN_PASSWORD': + continue + if key in replacements: + target.write(f'{key}={replacements[key]}\n') + else: + target.write(line) + os.chmod(env_path, 0o600) + + for _ in range(150): + try: + with urllib.request.urlopen( + 'http://127.0.0.1:8080/status.php', timeout=2) as response: + status = json.load(response) + if status.get('installed') is True: + break + except (OSError, ValueError): + pass + time.sleep(2) + else: + raise RuntimeError('ownCloud HTTP endpoint was not ready within 300 seconds') + + with open('/etc/owncloud/configured', 'w', encoding='utf-8'): + pass if __name__ == "__main__": diff --git a/overlay/usr/lib/inithooks/firstboot.d/20regen-owncloud-secrets b/overlay/usr/lib/inithooks/firstboot.d/20regen-owncloud-secrets index 9d65116..575e5a5 100755 --- a/overlay/usr/lib/inithooks/firstboot.d/20regen-owncloud-secrets +++ b/overlay/usr/lib/inithooks/firstboot.d/20regen-owncloud-secrets @@ -1,19 +1,26 @@ -#!/bin/bash -e -# regenerate owncloud salt and mysql password +#!/bin/bash +set -Eeuo pipefail +# Regenerate the database and Redis credentials before ownCloud starts. . /etc/default/inithooks -CONF=/var/www/owncloud/config/config.php +for _ in $(seq 1 60); do + mariadb-admin ping --silent >/dev/null 2>&1 && break + sleep 1 +done +mariadb-admin ping --silent >/dev/null -SALT=$(mcookie) -sed -i "s|passwordsalt.*|passwordsalt' => '$SALT',|" $CONF +PASSWORD=$(mcookie) +REDIS_PASSWORD=$(mcookie) -INSTANCEID=$(mcookie | cut -b -13) -sed -i "s|instanceid.*|instanceid' => '$INSTANCEID',|" $CONF +mysql --batch --execute "ALTER USER 'owncloud'@'172.28.0.%' IDENTIFIED BY '$PASSWORD'; FLUSH PRIVILEGES;" +sed -i "s/^OWNCLOUD_DB_PASSWORD=.*/OWNCLOUD_DB_PASSWORD=$PASSWORD/" \ + /etc/owncloud/owncloud.env -PASSWORD=$(mcookie) -sed -i "s|dbpassword.*|dbpassword' => '$PASSWORD',|" $CONF -$INITHOOKS_PATH/bin/mysqlconf.py --user=owncloud --pass="$PASSWORD" +sed -i "s/^requirepass .*/requirepass $REDIS_PASSWORD/" \ + /etc/redis/redis.conf +sed -i "s/^OWNCLOUD_REDIS_PASSWORD=.*/OWNCLOUD_REDIS_PASSWORD=$REDIS_PASSWORD/" \ + /etc/owncloud/owncloud.env +systemctl restart redis-server.service -SECRET=$(mcookie)$(mcookie)$(mcookie) -sed -i "s|secret.*|secret' => '$SECRET',|" $CONF +touch /etc/owncloud/firstboot-ready diff --git a/overlay/usr/local/bin/turnkey-occ b/overlay/usr/local/bin/turnkey-occ index 9cbc3c0..3f5950e 100755 --- a/overlay/usr/local/bin/turnkey-occ +++ b/overlay/usr/local/bin/turnkey-occ @@ -1,10 +1,11 @@ -#!/bin/bash -e +#!/bin/bash +set -Eeuo pipefail -[[ -z "$DEBUG" ]] || set -x +[[ -z ${DEBUG:-} ]] || set -x -OC_USR=${OC_USR:-www-data} -OCC=/var/www/owncloud/occ +args=(--user "${OC_USR:-www-data}") +if [[ -n ${OC_PASS:-} ]]; then + args+=(--env "OC_PASS=$OC_PASS") +fi -COMMAND="/usr/bin/php $OCC $@" - -runuser $OC_USR -s /bin/bash -c "$COMMAND" +exec docker exec "${args[@]}" owncloud_server occ "$@" diff --git a/overlay/usr/local/lib/owncloud/network-ready b/overlay/usr/local/lib/owncloud/network-ready new file mode 100755 index 0000000..916b1d0 --- /dev/null +++ b/overlay/usr/local/lib/owncloud/network-ready @@ -0,0 +1,17 @@ +#!/bin/bash +set -Eeuo pipefail + +network=owncloud +subnet=172.28.0.0/24 +gateway=172.28.0.1 + +if docker network inspect "$network" >/dev/null 2>&1; then + actual_subnet=$(docker network inspect --format \ + '{{(index .IPAM.Config 0).Subnet}}' "$network") + actual_gateway=$(docker network inspect --format \ + '{{(index .IPAM.Config 0).Gateway}}' "$network") + [[ $actual_subnet == "$subnet" && $actual_gateway == "$gateway" ]] +else + docker network create --driver bridge --subnet "$subnet" \ + --gateway "$gateway" "$network" >/dev/null +fi diff --git a/overlay/usr/local/lib/owncloud/run b/overlay/usr/local/lib/owncloud/run new file mode 100755 index 0000000..2b537ab --- /dev/null +++ b/overlay/usr/local/lib/owncloud/run @@ -0,0 +1,18 @@ +#!/bin/bash +set -Eeuo pipefail + +source /etc/owncloud/image.conf + +if ! docker image inspect "$OWNCLOUD_IMAGE" >/dev/null 2>&1; then + archive=/usr/local/src/owncloud-server.tar + [[ -s $archive ]] + docker load --input "$archive" >/dev/null + docker image inspect "$OWNCLOUD_IMAGE" >/dev/null + truncate --size 0 "$archive" +fi + +exec docker run --rm --name owncloud_server --network owncloud \ + --publish 127.0.0.1:8080:8080 \ + --env-file /etc/owncloud/owncloud.env \ + --volume /var/lib/owncloud:/mnt/data \ + "$OWNCLOUD_IMAGE" diff --git a/overlay/usr/local/lib/owncloud/wait-firstboot b/overlay/usr/local/lib/owncloud/wait-firstboot new file mode 100755 index 0000000..a8acd8e --- /dev/null +++ b/overlay/usr/local/lib/owncloud/wait-firstboot @@ -0,0 +1,14 @@ +#!/bin/bash +set -Eeuo pipefail + +source /etc/default/inithooks + +[[ ${RUN_FIRSTBOOT:-true} == false ]] && exit 0 + +for _ in $(seq 1 150); do + [[ -e /etc/owncloud/firstboot-ready ]] && exit 0 + sleep 2 +done + +echo 'ownCloud firstboot credentials were not prepared within 300 seconds' >&2 +exit 1 diff --git a/overlay/usr/local/sbin/owncloud-update b/overlay/usr/local/sbin/owncloud-update new file mode 100755 index 0000000..c045d0b --- /dev/null +++ b/overlay/usr/local/sbin/owncloud-update @@ -0,0 +1,58 @@ +#!/bin/bash +set -Eeuo pipefail + +usage() { + echo 'Usage: owncloud-update --check VERSION' >&2 + echo ' owncloud-update VERSION sha256:DIGEST' >&2 + exit 1 +} + +candidate_digest() { + skopeo inspect --raw "docker://docker.io/owncloud/server:$1" | \ + python3 -c ' +import json +import sys + +manifest = json.load(sys.stdin) +for item in manifest.get("manifests", []): + platform = item.get("platform", {}) + if platform.get("os") == "linux" and platform.get("architecture") == "amd64": + print(item["digest"]) + break +else: + raise SystemExit("official tag has no linux/amd64 manifest") +' +} + +if [[ ${1:-} == --check ]]; then + [[ $# == 2 ]] || usage + digest=$(candidate_digest "$2") + printf 'version=%s\ndigest=%s\nsource=docker.io/owncloud/server\n' \ + "$2" "$digest" + exit 0 +fi + +[[ $# == 2 ]] || usage +version=$1 +digest=$2 +[[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] +[[ $digest =~ ^sha256:[0-9a-f]{64}$ ]] + +current=$(candidate_digest "$version") +if [[ $current != "$digest" ]]; then + echo "official amd64 digest is $current, not $digest" >&2 + exit 1 +fi + +docker pull "owncloud/server@$digest" +suffix=${digest#sha256:} +local_tag="owncloud/server:${version}-turnkey-${suffix:0:12}" +docker tag "owncloud/server@$digest" "$local_tag" + +cat > /etc/owncloud/image.conf < -#include - -php7.4-gd -php7.4-cli -php7.4-curl -php7.4-json -php7.4-intl -php7.4-imagick -php7.4-zip -php7.4-mbstring -php-pear -php7.4-xml +#include +docker.io +docker-cli +fuse-overlayfs +skopeo redis-server -php7.4-igbinary -php7.4-redis - -smbclient -php7.4-ldap -php7.4-imap -php7.4-gmp - -bzip2 -ffmpeg diff --git a/tests/v19.sh b/tests/v19.sh new file mode 100755 index 0000000..12547a5 --- /dev/null +++ b/tests/v19.sh @@ -0,0 +1,208 @@ +#!/bin/bash +set -Eeuo pipefail +umask 077 + +result=${TKL_TEST_RESULT:?TKL_TEST_RESULT is required} +app_password=${TKL_TEST_APP_PASS:?TKL_TEST_APP_PASS is required} +db_password=${TKL_TEST_DB_PASS:?TKL_TEST_DB_PASS is required} +base=https://localhost +remote_file=tkl-v19-acceptance-$$.txt +payload_one=/tmp/tkl-owncloud-payload-one.$$ +payload_two=/tmp/tkl-owncloud-payload-two.$$ +response=/tmp/tkl-owncloud-response.$$ +headers=/tmp/tkl-owncloud-headers.$$ +adminer_cookies=/tmp/tkl-owncloud-adminer-cookies.$$ +update_check=/tmp/tkl-owncloud-update.$$ +policy=/tmp/tkl-owncloud-policy.$$ +test_user=tkl-v19-user-$$ + +report_error() { + printf 'test_failure line=%s status=%s command=%q\n' \ + "$1" "$2" "$3" >&2 + exit "$2" +} +trap 'report_error "$LINENO" "$?" "$BASH_COMMAND"' ERR + +cleanup() { + curl --insecure --silent --user "admin:$app_password" --request DELETE \ + "$base/remote.php/dav/files/admin/$remote_file" >/dev/null 2>&1 || true + OC_PASS=$app_password turnkey-occ user:delete "$test_user" >/dev/null 2>&1 || true + rm -f -- "$payload_one" "$payload_two" "$response" "$headers" \ + "$adminer_cookies" "$update_check" "$policy" +} +trap cleanup EXIT + +systemctl --quiet is-active docker.service owncloud-network.service \ + owncloud.service apache2.service mariadb.service redis-server.service \ + cron.service postfix.service multi-user.target +systemctl --quiet is-enabled docker.service owncloud-network.service \ + owncloud.service apache2.service mariadb.service redis-server.service \ + cron.service postfix.service +apache2ctl -t +apache2ctl -M 2>/dev/null | grep -F ' proxy_module ' >/dev/null +apache2ctl -M 2>/dev/null | grep -F ' proxy_http_module ' >/dev/null +test -e /etc/owncloud/configured + +owncloud_version=$(turnkey-occ status | awk '/versionstring:/ {print $3}') +test "$owncloud_version" = 11.0.0 +grep -Fxq 'version=11.0.0' /etc/owncloud/image-source +grep -Fxq \ + 'digest=sha256:dbebc24fe77a35c5de621d38a3f7264ffb43e3ee321928cde9a563a72b8ff366' \ + /etc/owncloud/image-source +source /etc/owncloud/image.conf +test "$OWNCLOUD_VERSION" = 11.0.0 +test "$OWNCLOUD_DIGEST" = \ + sha256:dbebc24fe77a35c5de621d38a3f7264ffb43e3ee321928cde9a563a72b8ff366 +test "$(docker inspect --format '{{.Config.Image}}' owncloud_server)" = \ + "$OWNCLOUD_IMAGE" +test "$(docker inspect --format '{{.State.Running}}' owncloud_server)" = true +test "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/mnt/data"}}{{.Source}}{{end}}{{end}}' owncloud_server)" = \ + /var/lib/owncloud +test "$(docker inspect --format '{{range $name, $_ := .NetworkSettings.Networks}}{{$name}}{{end}}' owncloud_server)" = owncloud +test "$(docker network inspect --format '{{(index .IPAM.Config 0).Subnet}}' owncloud)" = 172.28.0.0/24 +test "$(docker network inspect --format '{{(index .IPAM.Config 0).Gateway}}' owncloud)" = 172.28.0.1 +docker_storage_driver=$(docker info --format '{{.Driver}}') +case "$docker_storage_driver" in + overlay2|fuse-overlayfs) ;; + *) echo "unexpected Docker storage driver: $docker_storage_driver" >&2; exit 1;; +esac + +curl --insecure --silent --show-error --dump-header "$headers" \ + --output /dev/null http://localhost/ +grep -Eq '^HTTP/.* 301' "$headers" +grep -Eqi '^location: https://localhost/' "$headers" +curl --insecure --fail --silent --show-error "$base/status.php" >"$response" +grep -q '"installed":true' "$response" +grep -q '"versionstring":"11.0.0"' "$response" +curl --insecure --fail --silent --show-error --location "$base/" >"$response" +grep -qi 'owncloud' "$response" + +curl --insecure --fail --silent --show-error --user "admin:$app_password" \ + "$base/ocs/v1.php/cloud/user?format=json" >"$response" +grep -q '"statuscode":100' "$response" +grep -q '"id":"admin"' "$response" + +printf 'ownCloud v19 first payload\n' >"$payload_one" +printf 'ownCloud v19 updated payload\n' >"$payload_two" +curl --insecure --fail --silent --show-error --user "admin:$app_password" \ + --upload-file "$payload_one" \ + "$base/remote.php/dav/files/admin/$remote_file" +curl --insecure --fail --silent --show-error --user "admin:$app_password" \ + "$base/remote.php/dav/files/admin/$remote_file" >"$response" +cmp "$payload_one" "$response" +curl --insecure --fail --silent --show-error --user "admin:$app_password" \ + --upload-file "$payload_two" \ + "$base/remote.php/dav/files/admin/$remote_file" +curl --insecure --fail --silent --show-error --user "admin:$app_password" \ + "$base/remote.php/dav/files/admin/$remote_file" >"$response" +cmp "$payload_two" "$response" +mariadb --batch --skip-column-names owncloud \ + --execute="SELECT path FROM oc_filecache WHERE name='$remote_file';" | \ + grep -F "/$remote_file" +curl --insecure --fail --silent --show-error --user "admin:$app_password" \ + --request DELETE "$base/remote.php/dav/files/admin/$remote_file" +if curl --insecure --silent --user "admin:$app_password" --output /dev/null \ + --write-out '%{http_code}' \ + "$base/remote.php/dav/files/admin/$remote_file" | grep -vq '^404$'; then + echo 'deleted WebDAV file remained accessible' >&2 + exit 1 +fi + +OC_PASS=$app_password turnkey-occ user:add --password-from-env "$test_user" +turnkey-occ user:list | grep -F "$test_user" +mariadb --batch --skip-column-names owncloud \ + --execute="SELECT uid FROM oc_users WHERE uid='$test_user';" | \ + grep -Fx "$test_user" +turnkey-occ user:delete "$test_user" + +grep -Fxq \ + '*/15 * * * * root /usr/local/bin/turnkey-occ system:cron >/dev/null 2>&1' \ + /etc/cron.d/owncloud +test "$(turnkey-occ config:app:get core backgroundjobs_mode)" = cron +turnkey-occ system:cron >/dev/null + +ss -ltn | grep -Eq '127\.0\.0\.1:8080[[:space:]]' +ss -ltn | grep -Eq '172\.28\.0\.1:3306[[:space:]]' +ss -ltn | grep -Eq '172\.28\.0\.1:6379[[:space:]]' +if ss -ltn | grep -Eq '(0\.0\.0\.0|\[::\]):(3306|6379|8080)[[:space:]]'; then + echo 'an ownCloud backend unexpectedly listens on every interface' >&2 + exit 1 +fi +redis_password=$(sed -n 's/^OWNCLOUD_REDIS_PASSWORD=//p' \ + /etc/owncloud/owncloud.env) +grep -Fxq "requirepass $redis_password" /etc/redis/redis.conf +docker exec owncloud_server php -r ' +$redis = new Redis(); +$redis->connect(getenv("OWNCLOUD_REDIS_HOST"), + (int) getenv("OWNCLOUD_REDIS_PORT")); +if (!$redis->auth(getenv("OWNCLOUD_REDIS_PASSWORD")) || !$redis->ping()) { + exit(1); +} +' +python3 - <<'PY' +import socket + +with socket.create_connection(('172.28.0.1', 6379), timeout=5) as redis: + redis.sendall(b'*1\r\n$4\r\nPING\r\n') + response = redis.recv(256) +if not response.startswith(b'-NOAUTH '): + raise SystemExit('Redis did not reject unauthenticated PING: %r' % response) +PY +echo 'redis_unauthenticated_ping=denied' + +curl --insecure --fail --silent --show-error \ + https://127.0.0.1:12322/ >"$response" +grep -qi 'Adminer' "$response" +curl --insecure --silent --show-error --location \ + --cookie-jar "$adminer_cookies" --cookie "$adminer_cookies" \ + --data-urlencode 'auth[driver]=server' \ + --data-urlencode 'auth[server]=localhost' \ + --data-urlencode 'auth[username]=adminer' \ + --data-urlencode "auth[password]=$db_password" \ + --data-urlencode 'auth[db]=owncloud' \ + https://127.0.0.1:12322/ >"$response" +grep -qi 'owncloud' "$response" +grep -qi 'Logout' "$response" +if grep -qi 'Invalid credentials\|Access denied' "$response"; then + echo 'Adminer rejected its firstboot MariaDB credentials' >&2 + exit 1 +fi + +docker_image_before=$(docker image inspect --format '{{.Id}}' "$OWNCLOUD_IMAGE") +owncloud-update --check 11.0.0 >"$update_check" +grep -Fxq 'version=11.0.0' "$update_check" +grep -Eq '^digest=sha256:[0-9a-f]{64}$' "$update_check" +grep -Fxq 'source=docker.io/owncloud/server' "$update_check" +test "$(docker image inspect --format '{{.Id}}' "$OWNCLOUD_IMAGE")" = \ + "$docker_image_before" + +apache_version=$(dpkg-query -W -f='${Version}' apache2) +mariadb_version=$(dpkg-query -W -f='${Version}' mariadb-server) +redis_version=$(dpkg-query -W -f='${Version}' redis-server) +docker_version=$(dpkg-query -W -f='${Version}' docker.io) +docker_cli_version=$(dpkg-query -W -f='${Version}' docker-cli) +fuse_overlayfs_version=$(dpkg-query -W -f='${Version}' fuse-overlayfs) +skopeo_version=$(dpkg-query -W -f='${Version}' skopeo) +before="$apache_version|$mariadb_version|$redis_version|$docker_version|$docker_cli_version|$fuse_overlayfs_version|$skopeo_version" +apt-get update >/dev/null +for package in apache2 mariadb-server redis-server docker.io docker-cli fuse-overlayfs skopeo; do + apt-cache policy "$package" >"$policy" + candidate=$(awk '/Candidate:/ {print $2}' "$policy") + test -n "$candidate" + test "$candidate" != '(none)' + grep -Eq 'trixie|deb13' "$policy" +done +after="$(dpkg-query -W -f='${Version}' apache2)|$(dpkg-query -W -f='${Version}' mariadb-server)|$(dpkg-query -W -f='${Version}' redis-server)|$(dpkg-query -W -f='${Version}' docker.io)|$(dpkg-query -W -f='${Version}' docker-cli)|$(dpkg-query -W -f='${Version}' fuse-overlayfs)|$(dpkg-query -W -f='${Version}' skopeo)" +test "$after" = "$before" +grep -Rqs '^Suites: trixie' /etc/apt/sources.list.d +! grep -Rqi bookworm /etc/apt/sources.list.d + +cat >"$result" <