mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-18 17:13:58 +00:00
* fix(deb): keep plugin_packages in-tree to fix admin-installed plugins
The .deb postinstall symlinked /opt/etherpad/src/plugin_packages to
/var/lib/etherpad/plugin_packages so the etherpad user could install
plugins under ProtectSystem=strict. Node.js resolves symlinks to their
realpath before walking node_modules, so a plugin installed via the
admin UI lived under /var/lib/etherpad/... and could no longer reach
the bundled ep_etherpad-lite in /opt/etherpad/node_modules. Every
require('ep_etherpad-lite/...') in installed plugins (and the matching
esbuild client-bundle build) failed with MODULE_NOT_FOUND, etherpad
exited, and the systemd unit restart-looped (ether/ep_comments_page#416).
Keep plugin_packages as a real in-tree directory, make it (and its
.versions/ subdir) group-writable by etherpad like node_modules already
is, and add it to ReadWritePaths= in the unit. Migrate the contents of
any pre-existing /var/lib/etherpad/plugin_packages symlink target back
in-tree on upgrade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(deb): update assertions for in-tree plugin_packages + cover migration
The previous assertions in packaging/test-local.sh and the deb-package
workflow checked that /opt/etherpad/src/plugin_packages was a symlink to
/var/lib/etherpad/plugin_packages -- the layout this PR is removing.
They would have failed under the new postinst.
- Assert plugin_packages is a real directory (not a symlink), owned by
group etherpad with mode 2775, matching node_modules.
- After the happy-path /health check, simulate a pre-fix install by
recreating the symlink with a marker plugin, re-run the postinst via
dpkg-reconfigure, and assert the marker payload was migrated back
in-tree and the symlink is gone. Locks in regression coverage for the
upgrade path (ether/ep_comments_page#416).
* ci(deb): add ep_layout_trip_wire fixture to gate plugin_packages layout
Layout assertions alone don't actually exercise the failure mode from
ether/ep_comments_page#416: they confirm /opt/etherpad/src/plugin_packages
is a real directory but never load a plugin whose index.js does
require('ep_etherpad-lite/...') from the on-disk realpath.
Ship a tiny test plugin under packaging/test-fixtures/ep_layout_trip_wire
that exercises the four require() patterns from the bug report (eejs,
Settings, log4js, pad_utils) and emits a marker line from
expressCreateServer. Both packaging/test-local.sh and the deb-package
workflow now stage it into plugin_packages/.versions/, wire up the
toplevel + node_modules symlinks live-plugin-manager would create,
list it in installed_plugins.json, restart etherpad, and assert:
* marker line appears in the journal (every require resolved), and
* no "Cannot find module 'ep_etherpad-lite" appears anywhere.
Verified locally that the fixture loads under the in-tree layout
(marker present) and fails under a symlinked-out-of-tree layout
(marker absent, MODULE_NOT_FOUND in the log) -- so the gate catches
the regression in both directions.
* fix(deb): clean up runtime plugin artifacts on purge
With plugin_packages now living under /opt/etherpad/src/plugin_packages,
admin-installed plugins land in dpkg-unmanaged paths (the .versions/
stage that live-plugin-manager populates plus matching ep_* symlinks
under src/node_modules/). dpkg --purge would leave them behind because
the manifest never recorded them.
In postremove's purge branch: explicitly rm -rf plugin_packages and any
runtime ep_* symlinks in node_modules, then rm -rf the whole APP_DIR
as belt-and-braces against other runtime drift. Verified in a sandbox
that a staged ep_runtime@1.0.0 + node_modules/ep_runtime symlink are
both gone after purge runs.
Add post-purge assertions to both packaging/test-local.sh and the
deb-package workflow: /opt/etherpad/src/plugin_packages and
/var/lib/etherpad must not exist after dpkg --purge.
Addresses Qodo PR #7750 review item 3 (\"Purge cleanup misses
plugins\", Reliability).
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
134 lines
5.5 KiB
Bash
Executable file
134 lines
5.5 KiB
Bash
Executable file
#!/bin/sh
|
|
# postinstall - runs after files have been unpacked.
|
|
# Debian actions: configure | abort-upgrade | abort-remove | abort-deconfigure
|
|
set -e
|
|
|
|
ETC_DIR=/etc/etherpad
|
|
VAR_DIR=/var/lib/etherpad
|
|
LOG_DIR=/var/log/etherpad
|
|
APP_DIR=/opt/etherpad
|
|
RUNTIME_VAR="${VAR_DIR}/var"
|
|
DIST_SETTINGS=/usr/share/etherpad/settings.json.dist
|
|
ACTIVE_SETTINGS="${ETC_DIR}/settings.json"
|
|
INSTALLED_PLUGINS="${RUNTIME_VAR}/installed_plugins.json"
|
|
|
|
case "$1" in
|
|
configure)
|
|
mkdir -p "${ETC_DIR}" "${VAR_DIR}" "${LOG_DIR}" "${RUNTIME_VAR}"
|
|
chown root:etherpad "${ETC_DIR}"
|
|
chmod 0750 "${ETC_DIR}"
|
|
chown etherpad:etherpad "${VAR_DIR}" "${LOG_DIR}" "${RUNTIME_VAR}"
|
|
chmod 0750 "${VAR_DIR}" "${LOG_DIR}" "${RUNTIME_VAR}"
|
|
|
|
if [ ! -e "${ACTIVE_SETTINGS}" ]; then
|
|
cp "${DIST_SETTINGS}" "${ACTIVE_SETTINGS}"
|
|
# Switch the shipped default from dirty (dev-only, per the template's
|
|
# own comment) to sqlite, and point the file at /var/lib/etherpad so
|
|
# ProtectSystem=strict doesn't block writes.
|
|
sed -i \
|
|
-e 's|"dbType": "dirty"|"dbType": "sqlite"|' \
|
|
-e 's|"filename": "var/dirty.db"|"filename": "/var/lib/etherpad/etherpad.db"|' \
|
|
"${ACTIVE_SETTINGS}"
|
|
# Owned by the etherpad service user with group=etherpad mode 0660
|
|
# so the admin /admin/settings UI can save changes back to disk
|
|
# while still keeping the file unreadable by other users (DB
|
|
# creds live here).
|
|
chown etherpad:etherpad "${ACTIVE_SETTINGS}"
|
|
chmod 0660 "${ACTIVE_SETTINGS}"
|
|
fi
|
|
|
|
# Etherpad reads settings.json from CWD (/opt/etherpad). Expose
|
|
# the /etc copy there via symlink.
|
|
ln -sfn "${ACTIVE_SETTINGS}" "${APP_DIR}/settings.json"
|
|
|
|
# Redirect /opt/etherpad/var to a writable location under
|
|
# /var/lib/etherpad. Etherpad writes var/js, var/installed_plugins.json,
|
|
# etc. on startup; ProtectSystem=strict blocks /opt writes, and the
|
|
# symlink keeps ReadWritePaths=/var/lib/etherpad sufficient.
|
|
if [ -e "${APP_DIR}/var" ] && [ ! -L "${APP_DIR}/var" ]; then
|
|
# Migrate any payload from a previous install that wrote into /opt.
|
|
cp -a "${APP_DIR}/var/." "${RUNTIME_VAR}/" 2>/dev/null || true
|
|
rm -rf "${APP_DIR}/var"
|
|
fi
|
|
ln -sfn "${RUNTIME_VAR}" "${APP_DIR}/var"
|
|
|
|
# Seed installed_plugins.json so checkForMigration() does not spawn
|
|
# `pnpm ls` on first boot. pnpm is not a package dependency, and the
|
|
# bundled node_modules already contains every shipped plugin.
|
|
if [ ! -e "${INSTALLED_PLUGINS}" ]; then
|
|
VERSION=$(node -p "require('${APP_DIR}/src/package.json').version" 2>/dev/null \
|
|
|| node -p "require('${APP_DIR}/package.json').version" 2>/dev/null \
|
|
|| echo "0.0.0")
|
|
cat >"${INSTALLED_PLUGINS}" <<EOF
|
|
{"plugins":[{"name":"ep_etherpad-lite","version":"${VERSION}"}]}
|
|
EOF
|
|
fi
|
|
|
|
chown -hR etherpad:etherpad "${RUNTIME_VAR}"
|
|
|
|
# Plugin install paths. Etherpad's admin UI installs plugins into
|
|
# ${root}/src/plugin_packages and creates symlinks under
|
|
# ${root}/src/node_modules. Both ship root-owned but need to be
|
|
# group-writable by the etherpad user so the admin UI can add packages.
|
|
PLUGIN_PKG_DIR="${APP_DIR}/src/plugin_packages"
|
|
NODE_MODULES_DIR="${APP_DIR}/src/node_modules"
|
|
|
|
# Migrate any previous install that symlinked plugin_packages outside
|
|
# the tree (see ether/ep_comments_page#416). Node.js resolves symlinks
|
|
# to their realpath before walking node_modules, so plugins installed
|
|
# under /var/lib/etherpad/plugin_packages couldn't reach the bundled
|
|
# ep_etherpad-lite in /opt/etherpad/node_modules and every
|
|
# require('ep_etherpad-lite/...') threw MODULE_NOT_FOUND. Pull the
|
|
# symlink target's contents back in-tree and drop the symlink.
|
|
if [ -L "${PLUGIN_PKG_DIR}" ]; then
|
|
OLD_PLUGIN_PKG_LIVE=$(readlink -f "${PLUGIN_PKG_DIR}" 2>/dev/null || true)
|
|
rm -f "${PLUGIN_PKG_DIR}"
|
|
mkdir -p "${PLUGIN_PKG_DIR}"
|
|
if [ -n "${OLD_PLUGIN_PKG_LIVE}" ] && [ -d "${OLD_PLUGIN_PKG_LIVE}" ]; then
|
|
cp -a "${OLD_PLUGIN_PKG_LIVE}/." "${PLUGIN_PKG_DIR}/" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
mkdir -p "${PLUGIN_PKG_DIR}"
|
|
|
|
# plugin_packages and node_modules contain bundled (root-owned)
|
|
# packages; the *directories themselves* (plus plugin_packages/.versions
|
|
# where live-plugin-manager stages downloads) must be group-writable by
|
|
# etherpad so the admin UI can install new plugins alongside the
|
|
# shipped deps. The unit's ReadWritePaths= also exposes both paths as
|
|
# writable under ProtectSystem=strict.
|
|
for dir in "${PLUGIN_PKG_DIR}" "${PLUGIN_PKG_DIR}/.versions" "${NODE_MODULES_DIR}"; do
|
|
[ -d "${dir}" ] || continue
|
|
chgrp etherpad "${dir}"
|
|
chmod 2775 "${dir}"
|
|
done
|
|
|
|
if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then
|
|
systemctl daemon-reload || true
|
|
# Enable on first install; leave state alone on upgrade.
|
|
if [ -z "$2" ]; then
|
|
systemctl enable etherpad.service >/dev/null 2>&1 || true
|
|
fi
|
|
# Restart on upgrade to pick up new code (skip on fresh install --
|
|
# admin may want to configure first).
|
|
if [ -n "$2" ]; then
|
|
systemctl try-restart etherpad.service >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
|
|
cat <<EOF
|
|
Etherpad installed. Edit /etc/etherpad/settings.json, then:
|
|
sudo systemctl start etherpad
|
|
Default port 9001. Service logs: journalctl -u etherpad -f
|
|
EOF
|
|
;;
|
|
|
|
abort-upgrade|abort-remove|abort-deconfigure)
|
|
;;
|
|
|
|
*)
|
|
echo "postinstall called with unknown argument: $1" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
exit 0
|