mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 03:38:23 +00:00
fix(web-app-moodle): swap the code tree only after a complete copy
The entrypoint removed the old tree before it copied the new one. A copy that failed left the volume without a working Moodle, and every restart removed again instead of recovering. It now copies into a staging directory inside the volume and swaps the trees once the copy is complete, so a failed copy leaves the old tree in place. An image that ships an older release than the volume records no longer replaces it and exits with a message instead. Two instances on different releases would otherwise overwrite each other's tree. upgrade.php reports "No upgrade needed" on stderr, so the task counted every run on a current site as a change. It reads stderr now. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
0ecb2bc4fc
commit
0c748f74ed
4 files changed
+41
-4
No files matched your search
@@ -177,7 +177,7 @@ This role builds its own Moodle image from upstream Moodle source on top of the
|
||||
|
||||
## Updates
|
||||
|
||||
The image carries the Moodle release pinned as `services.moodle.app_version` in `meta/services.yml`. On start the entrypoint compares that release with the one recorded in the code volume and replaces the volume's tree with the image's when they differ. `tasks/01_manager_ops.yml` then runs `admin/cli/upgrade.php`, which migrates the database.
|
||||
The image carries the Moodle release pinned as `services.moodle.app_version` in `meta/services.yml`. On start the entrypoint compares that release with the one recorded in the code volume. When the image is newer, it copies the image's tree next to the old one and swaps them once the copy is complete. An image that is older than the recorded release refuses to start. `tasks/01_manager_ops.yml` then runs `admin/cli/upgrade.php`, which migrates the database.
|
||||
|
||||
Plugins installed through the web interface live only in the code volume and are not carried over to the next release. Declare a plugin under `meta/addons/` so the image ships it.
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ set -euo pipefail
|
||||
|
||||
MOODLE_BOOTSTRAP_SENTINEL="${MOODLE_CODE_DIR}/.bootstrap.done"
|
||||
MOODLE_BOOTSTRAP_LOCK="${MOODLE_CODE_DIR}/.bootstrap.lock"
|
||||
MOODLE_BOOTSTRAP_STAGE="${MOODLE_CODE_DIR}/.bootstrap.stage"
|
||||
|
||||
mkdir -p "${MOODLE_DATA_DIR}"
|
||||
chown -R "${MOODLE_RUNTIME_USER}:${MOODLE_RUNTIME_USER}" "${MOODLE_DATA_DIR}" || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
|
||||
@@ -31,12 +32,28 @@ moodle_code_dir_is_current() {
|
||||
[ "$(cat "${MOODLE_BOOTSTRAP_SENTINEL}" 2>/dev/null)" = "${MOODLE_RELEASE}" ]
|
||||
}
|
||||
|
||||
moodle_code_dir_is_newer() {
|
||||
local recorded newest
|
||||
recorded="$(cat "${MOODLE_BOOTSTRAP_SENTINEL}" 2>/dev/null)" || return 1
|
||||
[ -n "${recorded}" ] || return 1
|
||||
newest="$(printf '%s\n' "${recorded}" "${MOODLE_RELEASE}" | sort -V | tail -n 1)"
|
||||
[ "${newest}" = "${recorded}" ] && [ "${recorded}" != "${MOODLE_RELEASE}" ]
|
||||
}
|
||||
|
||||
moodle_bootstrap_code_dir() {
|
||||
if moodle_code_dir_is_current; then
|
||||
return 0
|
||||
fi
|
||||
find "${MOODLE_CODE_DIR}" -mindepth 1 -maxdepth 1 ! -name "${MOODLE_BOOTSTRAP_LOCK##*/}" -exec rm -rf {} +
|
||||
cp -a "${MOODLE_SOURCE_DIR}/." "${MOODLE_CODE_DIR}/"
|
||||
if moodle_code_dir_is_newer; then
|
||||
echo "The code volume holds Moodle $(cat "${MOODLE_BOOTSTRAP_SENTINEL}"), this image ships the older ${MOODLE_RELEASE}; refusing to replace it." >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "${MOODLE_BOOTSTRAP_STAGE}"
|
||||
mkdir "${MOODLE_BOOTSTRAP_STAGE}"
|
||||
cp -a "${MOODLE_SOURCE_DIR}/." "${MOODLE_BOOTSTRAP_STAGE}/"
|
||||
find "${MOODLE_CODE_DIR}" -mindepth 1 -maxdepth 1 ! -name "${MOODLE_BOOTSTRAP_LOCK##*/}" ! -name "${MOODLE_BOOTSTRAP_STAGE##*/}" -exec rm -rf {} +
|
||||
find "${MOODLE_BOOTSTRAP_STAGE}" -mindepth 1 -maxdepth 1 -exec mv -t "${MOODLE_CODE_DIR}" {} +
|
||||
rmdir "${MOODLE_BOOTSTRAP_STAGE}"
|
||||
chown -R "${MOODLE_RUNTIME_USER}:${MOODLE_RUNTIME_USER}" "${MOODLE_CODE_DIR}" || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
|
||||
find "${MOODLE_CODE_DIR}" -type d -exec chmod 0755 {} + || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
|
||||
find "${MOODLE_CODE_DIR}" -type f -exec chmod 0644 {} + || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
container exec --user {{ MOODLE_RUNTIME_USER }} {{ MOODLE_CONTAINER_ADDRESS }}
|
||||
php {{ MOODLE_CLI_DIR }}/upgrade.php --non-interactive
|
||||
register: moodle_upgrade
|
||||
changed_when: "'No upgrade needed' not in moodle_upgrade.stdout"
|
||||
changed_when: "'No upgrade needed' not in moodle_upgrade.stderr"
|
||||
|
||||
- name: "🔑 Realign the administrator password Moodle persisted at install"
|
||||
when: not (MOODLE_OIDC_ENABLED | bool)
|
||||
|
||||
@@ -7,6 +7,7 @@ import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from utils import PROJECT_ROOT
|
||||
from utils.cache.files import read_text
|
||||
|
||||
ENTRYPOINT = PROJECT_ROOT / "roles/web-app-moodle/files/build/moodle-entrypoint.sh"
|
||||
|
||||
@@ -99,6 +100,25 @@ class TestMoodleEntrypoint(unittest.TestCase):
|
||||
self.assertNotEqual(done.returncode, 0)
|
||||
self.assertFalse((self.code / ".bootstrap.done").exists())
|
||||
|
||||
def test_a_failed_copy_keeps_the_tree_the_volume_held(self):
|
||||
self.ship(LAYOUT_4)
|
||||
self.start("4.5.11")
|
||||
shutil.rmtree(self.source)
|
||||
done = self.run_entrypoint("5.3.0")
|
||||
self.assertNotEqual(done.returncode, 0)
|
||||
self.assertEqual(self.shipped_files_in_the_volume(), set(LAYOUT_4))
|
||||
self.assertEqual(read_text(str(self.code / ".bootstrap.done")), "4.5.11")
|
||||
|
||||
def test_an_image_older_than_the_recorded_release_refuses_to_replace_it(self):
|
||||
self.ship(LAYOUT_5)
|
||||
self.start("5.10.0")
|
||||
self.ship(LAYOUT_4)
|
||||
done = self.run_entrypoint("5.9.2")
|
||||
self.assertNotEqual(done.returncode, 0)
|
||||
self.assertIn("5.10.0", done.stderr)
|
||||
self.assertEqual(self.shipped_files_in_the_volume(), set(LAYOUT_5))
|
||||
self.assertEqual(read_text(str(self.code / ".bootstrap.done")), "5.10.0")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user