From 7a61fff82d9d01f9d26ab3b0905bb1dc88bd11c6 Mon Sep 17 00:00:00 2001 From: jenkins Date: Sun, 4 Oct 2026 04:25:46 -0500 Subject: [PATCH] nextcloud: preserve installed apps during ordinary restarts --- services/nextcloud/deployment.yaml | 57 ++++++++++++----------- testing/tests/test_nextcloud_bootstrap.py | 51 ++++++++++++++++++++ 2 files changed, 82 insertions(+), 26 deletions(-) create mode 100644 testing/tests/test_nextcloud_bootstrap.py diff --git a/services/nextcloud/deployment.yaml b/services/nextcloud/deployment.yaml index 13ee7e5b..aa71b24b 100644 --- a/services/nextcloud/deployment.yaml +++ b/services/nextcloud/deployment.yaml @@ -129,8 +129,8 @@ spec: if [ "${installed}" = "true" ]; then configure_oidc() { su -s /bin/sh www-data -c "php /var/www/html/occ config:system:set oidc_login_provider_url --value='https://sso.bstein.dev/realms/atlas'" - su -s /bin/sh www-data -c "php /var/www/html/occ config:system:set oidc_login_client_id --value='${OIDC_CLIENT_ID}'" - su -s /bin/sh www-data -c "php /var/www/html/occ config:system:set oidc_login_client_secret --value='${OIDC_CLIENT_SECRET}'" + su -s /bin/sh www-data -c "php /var/www/html/occ config:system:set oidc_login_client_id --value='${OIDC_CLIENT_ID}'" >/dev/null + su -s /bin/sh www-data -c "php /var/www/html/occ config:system:set oidc_login_client_secret --value='${OIDC_CLIENT_SECRET}'" >/dev/null su -s /bin/sh www-data -c "php /var/www/html/occ config:system:set oidc_login_auto_redirect --type=boolean --value=true" su -s /bin/sh www-data -c "php /var/www/html/occ config:system:set oidc_login_hide_password_form --type=boolean --value=true" su -s /bin/sh www-data -c "php /var/www/html/occ config:system:set oidc_login_disable_registration --type=boolean --value=false" @@ -143,10 +143,10 @@ spec: cfg_dir="/var/www/html/resources/config" mkdir -p "${cfg_dir}" if [ ! -s "${cfg_dir}/mimetypemapping.dist.json" ]; then - curl -fsSL https://raw.githubusercontent.com/nextcloud/server/v29.0.16/resources/config/mimetypemapping.dist.json -o "${cfg_dir}/mimetypemapping.dist.json" || true + curl --connect-timeout 10 --max-time 120 -fsSL https://raw.githubusercontent.com/nextcloud/server/v29.0.16/resources/config/mimetypemapping.dist.json -o "${cfg_dir}/mimetypemapping.dist.json" || true fi if [ ! -s "${cfg_dir}/mimetypealiases.dist.json" ]; then - curl -fsSL https://raw.githubusercontent.com/nextcloud/server/v29.0.16/resources/config/mimetypealiases.dist.json -o "${cfg_dir}/mimetypealiases.dist.json" || true + curl --connect-timeout 10 --max-time 120 -fsSL https://raw.githubusercontent.com/nextcloud/server/v29.0.16/resources/config/mimetypealiases.dist.json -o "${cfg_dir}/mimetypealiases.dist.json" || true fi chown -R 33:33 "${cfg_dir}" || true } @@ -154,35 +154,40 @@ spec: app="$1" url="$2" target="/var/www/html/custom_apps/${app}" - rm -rf "${target}" - mkdir -p /tmp/nextcloud-apps - curl -fsSL "${url}" -o "/tmp/nextcloud-apps/${app}.tar.gz" - tar -xzf "/tmp/nextcloud-apps/${app}.tar.gz" -C /var/www/html/custom_apps - rm -f "/tmp/nextcloud-apps/${app}.tar.gz" + # Installed apps survive pod replacement; upgrades are maintenance. + if [ -s "${target}/appinfo/info.xml" ]; then + return 0 + fi + stage="$(mktemp -d /var/www/html/custom_apps/.bootstrap-XXXXXX)" + if ! curl --connect-timeout 10 --max-time 180 -fsSL "${url}" -o "${stage}/app.tar.gz" \ + || ! tar -xzf "${stage}/app.tar.gz" -C "${stage}" \ + || [ ! -s "${stage}/${app}/appinfo/info.xml" ]; then + rm -rf "${stage}" + return 1 + fi + # Rename on the same PVC only after extraction is complete. + if [ -e "${target}" ]; then + mv "${target}" "${stage}/previous" + fi + mv "${stage}/${app}" "${target}" chown -R 33:33 "${target}" - su -s /bin/sh www-data -c "php /var/www/html/occ app:enable --force ${app}" || true - } - reset_external_config() { - su -s /bin/sh www-data -c "php /var/www/html/occ app:remove external" || true - su -s /bin/sh www-data -c "php /var/www/html/occ config:app:delete external jwt_token_privkey_es256" || true - su -s /bin/sh www-data -c "php /var/www/html/occ config:app:delete external jwt_token_pubkey_es256" || true - su -s /bin/sh www-data -c "php /var/www/html/occ config:app:delete external jwt_token_privkey_ed25519" || true - su -s /bin/sh www-data -c "php /var/www/html/occ config:app:delete external jwt_token_pubkey_ed25519" || true + rm -rf "${stage}" + su -s /bin/sh www-data -c "php /var/www/html/occ app:enable --force ${app}" } ensure_app() { app="$1" target="/var/www/html/custom_apps/${app}" - rm -rf "${target}" - su -s /bin/sh www-data -c "php /var/www/html/occ app:remove ${app}" || true - su -s /bin/sh www-data -c "php /var/www/html/occ app:install --force ${app}" || true - su -s /bin/sh www-data -c "php /var/www/html/occ app:enable --force ${app}" || true + if [ -s "${target}/appinfo/info.xml" ]; then + return 0 + fi + su -s /bin/sh www-data -c "php /var/www/html/occ app:install ${app}" || return 1 + su -s /bin/sh www-data -c "php /var/www/html/occ app:enable ${app}" } ensure_mime_defaults - reset_external_config - install_app external https://github.com/nextcloud-releases/external/releases/download/v5.4.1/external-v5.4.1.tar.gz - install_app mail https://github.com/nextcloud-releases/mail/releases/download/v3.7.24/mail-stable3.7.tar.gz - install_app oidc_login https://github.com/pulsejet/nextcloud-oidc-login/releases/download/v3.2.2/oidc_login.tar.gz - ensure_app richdocuments + install_app external https://github.com/nextcloud-releases/external/releases/download/v5.4.1/external-v5.4.1.tar.gz || exit 1 + install_app mail https://github.com/nextcloud-releases/mail/releases/download/v3.7.24/mail-stable3.7.tar.gz || exit 1 + install_app oidc_login https://github.com/pulsejet/nextcloud-oidc-login/releases/download/v3.2.2/oidc_login.tar.gz || exit 1 + ensure_app richdocuments || exit 1 configure_office configure_oidc fi diff --git a/testing/tests/test_nextcloud_bootstrap.py b/testing/tests/test_nextcloud_bootstrap.py new file mode 100644 index 00000000..42de2616 --- /dev/null +++ b/testing/tests/test_nextcloud_bootstrap.py @@ -0,0 +1,51 @@ +"""Check that a routine Nextcloud restart preserves installed app files.""" + +from pathlib import Path +import shlex +import subprocess + +import yaml + + +ROOT = Path(__file__).resolve().parents[2] + + +def functions(tmp_path: Path) -> str: + """Extract the deployed shell functions into a synthetic filesystem.""" + manifest = yaml.safe_load((ROOT / "services/nextcloud/deployment.yaml").read_text()) + containers = manifest["spec"]["template"]["spec"]["initContainers"] + script = next(c for c in containers if c["name"] == "install-nextcloud")["args"][0] + start = script.index(" install_app() {") + end = script.index(" ensure_mime_defaults\n", start) + return script[start:end].replace("/var/www/html", str(tmp_path)) + + +def test_existing_apps_need_no_download_or_reinstallation(tmp_path: Path): + """Unchanged app files survive without network or CLI configuration calls.""" + app = tmp_path / "custom_apps" / "synthetic" + (app / "appinfo").mkdir(parents=True) + (app / "appinfo/info.xml").write_text("") + preserved = app / "keep.txt" + preserved.write_text("original") + shell = functions(tmp_path) + """ +curl() { exit 91; } +su() { exit 92; } +install_app synthetic https://invalid.example/test.tar.gz +ensure_app synthetic +""" + subprocess.run(["sh", "-eu", "-c", shell], check=True) + assert preserved.read_text() == "original" + + +def test_failed_download_preserves_existing_directory(tmp_path: Path): + """A failed first install never removes an existing incomplete app tree.""" + app = tmp_path / "custom_apps" / "synthetic" + app.mkdir(parents=True) + (app / "keep.txt").write_text("original") + shell = functions(tmp_path) + """ +curl() { return 1; } +if install_app synthetic https://invalid.example/test.tar.gz; then exit 93; fi +""" + subprocess.run(["sh", "-eu", "-c", shell], check=True) + assert (app / "keep.txt").read_text() == "original" + assert not list((tmp_path / "custom_apps").glob(".bootstrap-*"))