22349: Refine Passenger agent install
[arvados.git] / build / rails-package-scripts / postinst.sh
1 #!/bin/sh
2 # Copyright (C) The Arvados Authors. All rights reserved.
3 #
4 # SPDX-License-Identifier: AGPL-3.0
5
6 # This code runs after package variable definitions.
7
8 set -e
9
10 for DISTRO_FAMILY in $(. /etc/os-release && echo "${ID:-} ${ID_LIKE:-}"); do
11     case "$DISTRO_FAMILY" in
12         debian)
13             RESETUP_CMD="dpkg-reconfigure $PACKAGE_NAME"
14             break ;;
15         rhel)
16             RESETUP_CMD="dnf reinstall $PACKAGE_NAME"
17             break ;;
18     esac
19 done
20 if [ -z "$RESETUP_CMD" ]; then
21    echo "$PACKAGE_NAME postinst skipped: don't recognize the distribution from /etc/os-release" >&2
22    exit 0
23 fi
24 # Default documentation URL. This can be set to a more specific URL.
25 NOT_READY_DOC_URL="https://doc.arvados.org/install/install-api-server.html"
26 # This will be set to a command path after we install the version we need.
27 BUNDLE=
28
29 systemd_quote() {
30     if [ $# -ne 1 ]; then
31         echo "error: systemd_quote requires exactly one argument" >&2
32         return 2
33     fi
34     # See systemd.syntax(7) - Use double quotes with backslash escapes
35     echo "$1" | sed -re 's/[\\"]/\\\0/g; s/^/"/; s/$/"/'
36 }
37
38 report_web_service_warning() {
39     local warning="$1"; shift
40     cat >&2 <<EOF
41
42 WARNING: $warning.
43
44 To override, set the WEB_SERVICE environment variable to the name of the service
45 hosting the Rails server.
46
47 After you do that, resume $PACKAGE_NAME setup by running:
48   $RESETUP_CMD
49 EOF
50 }
51
52 run_and_report() {
53     # Usage: run_and_report ACTION_MSG CMD
54     # This is the usual wrapper that prints ACTION_MSG, runs CMD, then writes
55     # a message about whether CMD succeeded or failed.  Returns the exit code
56     # of CMD.
57     local action_message="$1"; shift
58     local retcode=0
59     echo -n "$action_message..."
60     if "$@"; then
61         echo " done."
62     else
63         retcode=$?
64         echo " failed."
65     fi
66     return $retcode
67 }
68
69 setup_confdirs() {
70     for confdir in "$@"; do
71         if [ ! -d "$confdir" ]; then
72             install -d -g "$WWW_OWNER" -m 0750 "$confdir"
73         fi
74     done
75 }
76
77 setup_conffile() {
78     # Usage: setup_conffile CONFFILE_PATH [SOURCE_PATH]
79     # Both paths are relative to RELEASE_CONFIG_PATH.
80     # This function will try to safely ensure that a symbolic link for
81     # the configuration file points from RELEASE_CONFIG_PATH to CONFIG_PATH.
82     # If SOURCE_PATH is given, this function will try to install that file as
83     # the configuration file in CONFIG_PATH, and return 1 if the file in
84     # CONFIG_PATH is unmodified from the source.
85     local conffile_relpath="$1"; shift
86     local conffile_source="$1"
87     local release_conffile="$RELEASE_CONFIG_PATH/$conffile_relpath"
88     local etc_conffile="$CONFIG_PATH/$(basename "$conffile_relpath")"
89
90     # Note that -h can return true and -e will return false simultaneously
91     # when the target is a dangling symlink.  We're okay with that outcome,
92     # so check -h first.
93     if [ ! -h "$release_conffile" ]; then
94         if [ ! -e "$release_conffile" ]; then
95             ln -s "$etc_conffile" "$release_conffile"
96         # If there's a config file in /var/www identical to the one in /etc,
97         # overwrite it with a symlink after porting its permissions.
98         elif cmp --quiet "$release_conffile" "$etc_conffile"; then
99             local ownership="$(stat -c "%u:%g" "$release_conffile")"
100             local owning_group="${ownership#*:}"
101             if [ 0 != "$owning_group" ]; then
102                 chgrp "$owning_group" "$CONFIG_PATH" /etc/arvados
103             fi
104             chown "$ownership" "$etc_conffile"
105             chmod --reference="$release_conffile" "$etc_conffile"
106             ln --force -s "$etc_conffile" "$release_conffile"
107         fi
108     fi
109
110     if [ -n "$conffile_source" ]; then
111         if [ ! -e "$etc_conffile" ]; then
112             install -g "$WWW_OWNER" -m 0640 \
113                     "$RELEASE_CONFIG_PATH/$conffile_source" "$etc_conffile"
114             return 1
115         # Even if $etc_conffile already existed, it might be unmodified from
116         # the source.  This is especially likely when a user installs, updates
117         # database.yml, then reconfigures before they update application.yml.
118         # Use cmp to be sure whether $etc_conffile is modified.
119         elif cmp --quiet "$RELEASE_CONFIG_PATH/$conffile_source" "$etc_conffile"; then
120             return 1
121         fi
122     fi
123 }
124
125 prepare_database() {
126   # Prevent PostgreSQL from trying to page output
127   unset PAGER
128   DB_MIGRATE_STATUS=`"$BUNDLE" exec bin/rake db:migrate:status 2>&1 || true`
129   if echo "$DB_MIGRATE_STATUS" | grep -qF 'Schema migrations table does not exist yet.'; then
130       # The database exists, but the migrations table doesn't.
131       run_and_report "Setting up database" "$BUNDLE" exec bin/rake db:schema:load db:seed
132   elif echo "$DB_MIGRATE_STATUS" | grep -q '^database: '; then
133       run_and_report "Running db:migrate" "$BUNDLE" exec bin/rake db:migrate
134   elif echo "$DB_MIGRATE_STATUS" | grep -q 'database .* does not exist'; then
135       run_and_report "Running db:setup" "$BUNDLE" exec bin/rake db:setup
136   else
137       # We don't have enough configuration to even check the database.
138       return 1
139   fi
140 }
141
142 configure_version() {
143   if [ -n "$WEB_SERVICE" ]; then
144     :
145   elif command -v nginx >/dev/null 2>&1; then
146     WEB_SERVICE=nginx
147   else
148     report_web_service_warning "Web service (nginx) not found"
149   fi
150
151   case "$DISTRO_FAMILY" in
152       debian) WWW_OWNER=www-data ;;
153       rhel) WWW_OWNER="$(id --group --name nginx || true)" ;;
154   esac
155
156   # Before we do anything else, make sure some directories and files are in place
157   if [ ! -e $SHARED_PATH/log ]; then mkdir -p $SHARED_PATH/log; fi
158   if [ ! -e $RELEASE_PATH/tmp ]; then mkdir -p $RELEASE_PATH/tmp; fi
159   if [ ! -e $RELEASE_PATH/log ]; then ln -s $SHARED_PATH/log $RELEASE_PATH/log; fi
160   if [ ! -e $SHARED_PATH/log/production.log ]; then touch $SHARED_PATH/log/production.log; fi
161
162   cd "$RELEASE_PATH"
163   export RAILS_ENV=production
164
165   run_and_report "Installing bundler" gem install --conservative --version '~> 2.4.0' bundler
166   local ruby_minor_ver="$(ruby -e 'puts RUBY_VERSION.split(".")[..1].join(".")')"
167   BUNDLE="$(gem contents --version '~> 2.4.0' bundler | grep -E '/(bin|exe)/bundle$' | tail -n1)"
168   if ! [ -x "$BUNDLE" ]; then
169       # Some distros (at least Ubuntu 24.04) append the Ruby version to the
170       # executable name, but that isn't reflected in the output of
171       # `gem contents`. Check for that version.
172       BUNDLE="$BUNDLE$ruby_minor_ver"
173       if ! [ -x "$BUNDLE" ]; then
174           echo "Error: failed to find \`bundle\` command after installing bundler gem" >&2
175           return 1
176       fi
177   fi
178
179   local bundle_path="$SHARED_PATH/vendor_bundle"
180   run_and_report "Running bundle config set --local path $SHARED_PATH/vendor_bundle" \
181                  "$BUNDLE" config set --local path "$bundle_path"
182
183   # As of April 2024/Bundler 2.4, `bundle install` tends not to install gems
184   # which are already installed system-wide, which causes bundle activation to
185   # fail later. Work around this by installing all gems manually.
186   find vendor/cache -maxdepth 1 -name '*.gem' -print0 \
187       | run_and_report "Installing bundle gems" xargs -0r \
188                        gem install --conservative --ignore-dependencies \
189                        --local --no-document --quiet \
190                        --install-dir="$bundle_path/ruby/$ruby_minor_ver.0"
191   run_and_report "Running bundle install" "$BUNDLE" install --prefer-local --quiet
192   run_and_report "Verifying bundle is complete" "$BUNDLE" exec true
193
194   local passenger="$("$BUNDLE" exec gem contents passenger | grep -E '/(bin|exe)/passenger$' | tail -n1)"
195   if ! [ -x "$passenger" ]; then
196       echo "Error: failed to find \`passenger\` command after installing bundle" >&2
197       return 1
198   fi
199   "$BUNDLE" exec "$passenger-config" build-native-support
200   "$BUNDLE" exec "$passenger-config" install-standalone-runtime --auto --brief
201
202   echo -n "Creating symlinks to configuration in $CONFIG_PATH ..."
203   setup_confdirs /etc/arvados "$CONFIG_PATH"
204   setup_conffile environments/production.rb environments/production.rb.example \
205       || true
206   # Rails 5.2 does not tolerate dangling symlinks in the initializers
207   # directory, and this one can still be there, left over from a previous
208   # version of the API server package.
209   rm -f $RELEASE_PATH/config/initializers/omniauth.rb
210   echo "... done."
211
212   echo -n "Extending systemd unit configuration ..."
213   local systemd_group
214   if [ -z "$WWW_OWNER" ]; then
215       systemd_group="%N"
216   else
217       systemd_group="$(systemd_quote "$WWW_OWNER")"
218   fi
219   install -d /lib/systemd/system/arvados-railsapi.service.d
220   # The 20 prefix is chosen so most user overrides should come after, which
221   # is what most admins will expect, but there's still space to put drop-ins
222   # earlier.
223   cat >/lib/systemd/system/arvados-railsapi.service.d/20-postinst.conf <<EOF
224 [Service]
225 ExecStartPre=+/bin/chgrp $systemd_group log tmp
226 ExecStartPre=+-/bin/chgrp $systemd_group \${PASSENGER_LOG_FILE}
227 ExecStart=
228 ExecStart=$(systemd_quote "$BUNDLE") exec $(systemd_quote "$passenger") start --daemonize --pid-file %t/%N/passenger.pid
229 ExecStop=
230 ExecStop=$(systemd_quote "$BUNDLE") exec $(systemd_quote "$passenger") stop --pid-file %t/%N/passenger.pid
231 ExecReload=
232 ExecReload=$(systemd_quote "$BUNDLE") exec $(systemd_quote "$passenger-config") reopen-logs
233 ${WWW_OWNER:+SupplementaryGroups=$WWW_OWNER}
234 EOF
235   systemctl daemon-reload
236   echo "... done."
237
238   if [ -n "$NOT_READY_REASON" ]; then
239       :
240   # warn about config errors (deprecated/removed keys from
241   # previous version, etc)
242   elif ! run_and_report "Checking configuration for completeness" "$BUNDLE" exec bin/rake config:check; then
243       NOT_READY_REASON="you must add required configuration settings to /etc/arvados/config.yml"
244       NOT_READY_DOC_URL="https://doc.arvados.org/install/install-api-server.html#update-config"
245   elif ! prepare_database; then
246       NOT_READY_REASON="database setup could not be completed"
247   fi
248
249   if [ -z "$NOT_READY_REASON" ]; then
250       systemctl try-restart arvados-railsapi.service
251   fi
252 }
253
254 configure_version
255 if [ -n "$NOT_READY_REASON" ]; then
256     cat >&2 <<EOF
257 NOTE: The $PACKAGE_NAME package was not configured completely because
258 $NOT_READY_REASON.
259 Please refer to the documentation for next steps:
260   <$NOT_READY_DOC_URL>
261
262 After you do that, resume $PACKAGE_NAME setup by running:
263   $RESETUP_CMD
264 EOF
265 fi