Rails: Rework service execution so `nix-copy-closure' deployments work better

These changes allow service restarts to pick up a new
environment (including the correct gemset) when restarted.

Also, make migrations its own service that other services need to wait
for.
This commit is contained in:
Peter Jones 2019-06-20 12:04:51 -07:00
parent 66525b8df1
commit 3399464b17
No known key found for this signature in database
GPG key ID: 9DAFAA8D01941E49
7 changed files with 101 additions and 30 deletions

View file

@ -0,0 +1,52 @@
#!/bin/bash
################################################################################
# Build a PATH variable given a nix-store path.
set -e
set -u
################################################################################
dirs=()
################################################################################
if [ $# -ne 1 ]; then
>&2 echo "ERROR: missing store path"
exit 1
fi
################################################################################
function join() {
local sep=$1; shift
local IFS="$sep";
echo "$*";
}
################################################################################
function maybe_add_dir() {
local path=$1
if [ -n "$path" ] && [ -d "$path/bin" ]; then
dirs+=( "$path/bin" )
fi
}
################################################################################
function main() {
local path=$1
path=$(realpath "$path")
maybe_add_dir "$path"
for drv in $(nix-store --query --references "$path"); do
maybe_add_dir "$drv"
done
if [ "${#dirs[@]}" -gt 0 ]; then
echo "export PATH=$(join : "${dirs[@]}"):$PATH"
else
echo "export PATH=$PATH"
fi
}
################################################################################
main "$1"

View file

@ -72,7 +72,6 @@ let
group = "rails-${app.name}"; group = "rails-${app.name}";
shell = "${pkgs.bash}/bin/bash"; shell = "${pkgs.bash}/bin/bash";
extraGroups = [ config.services.nginx.group ]; extraGroups = [ config.services.nginx.group ];
packages = funcs.appPath app;
}; };
groups."rails-${app.name}" = {}; groups."rails-${app.name}" = {};
}; };

View file

@ -12,11 +12,6 @@ rec {
# Path to where the app is actually installed: # Path to where the app is actually installed:
appLink = app: "${app.home}/package"; appLink = app: "${app.home}/package";
##############################################################################
# Packages to put in the application's PATH. FIXME:
# propagatedBuildInputs won't always be set.
appPath = app: [ app.package.rubyEnv ] ++ app.package.propagatedBuildInputs;
############################################################################## ##############################################################################
# All of the environment variables that a Rails app needs: # All of the environment variables that a Rails app needs:
appEnv = app: { appEnv = app: {

View file

@ -86,6 +86,13 @@ let
default = false; default = false;
description = "Is this the main Rails process?"; description = "Is this the main Rails process?";
}; };
isMigration = mkOption {
internal = true;
type = types.bool;
default = false;
description = "Is this the migration service?";
};
}; };
config = { config = {

View file

@ -1,4 +1,7 @@
# Rails user shell profile. # Rails user shell profile.
if [ -e "$HOME/.path" ]; then
. "$HOME/.path"
fi
if [ -e "$HOME/.env" ]; then if [ -e "$HOME/.env" ]; then
. "$HOME/.env" . "$HOME/.env"

View file

@ -8,6 +8,7 @@
installPhase = '' installPhase = ''
mkdir -p $out/bin mkdir -p $out/bin
substituteAll ${./db-migrate.sh} $out/bin/db-migrate.sh substituteAll ${./db-migrate.sh} $out/bin/db-migrate.sh
substituteAll ${./build-path.sh} $out/bin/build-path.sh
find $out/bin -type f -exec chmod 555 '{}' ';' find $out/bin -type f -exec chmod 555 '{}' ';'
''; '';

View file

@ -23,39 +23,56 @@ let
name = "main"; name = "main";
schedule = null; schedule = null;
isMain = true; isMain = true;
isMigration = false;
script = '' script = ''
puma -e ${app.railsEnv} -p ${toString app.port} puma -e ${app.railsEnv} -p ${toString app.port}
''; '';
}; };
##############################################################################
# The database migration service:
migrationService = app: {
name = "migrations";
schedule = null;
isMain = false;
isMigration = true;
script = ''
${scripts.user}/bin/db-migrate.sh \
-r ${funcs.appLink app}/share/${app.name} \
-s ${app.home}/state
'';
};
############################################################################## ##############################################################################
# Generate a systemd service for a Ruby on Rails application: # Generate a systemd service for a Ruby on Rails application:
appService = app: service: { appService = app: service: {
"rails-${app.name}-${service.name}" = { "rails-${app.name}-${service.name}" = {
description = "${app.name} (Ruby on Rails) ${service.name}"; description = "${app.name} (Ruby on Rails) ${service.name}";
path = funcs.appPath app; path = with pkgs; [ coreutils nix ];
environment = funcs.appEnv app; environment = funcs.appEnv app;
# Only start this service if it isn't scheduled by a timer. # Only start this service if it isn't scheduled by a timer.
partOf = optional (service.schedule == null) "rails-${app.name}.target"; partOf = optional (service.schedule == null) "rails-${app.name}.target";
wantedBy = optional (service.schedule == null) "rails-${app.name}.target"; wantedBy = optional (service.schedule == null) "rails-${app.name}.target";
wants = wants = plib.keyService app.database.passwordFile
plib.keyService app.database.passwordFile ++ ++ plib.keyService app.sourcedFile
plib.keyService app.sourcedFile ++ ++ app.afterServices
app.afterServices; ++ optional (!service.isMigration && app.database.migrate) "rails-${app.name}-migrations"
++ optional (!service.isMain && !service.isMigration) "rails-${app.name}-main";
after = after = [ "network.target" ]
[ "network.target" ] ++ ++ optional localpg "postgresql.service"
optional localpg "postgresql.service" ++ ++ optional localpg "postgres-account-manager.service"
optional localpg "pg-accounts.service" ++ ++ plib.keyService app.database.passwordFile
optional (!service.isMain) "rails-${app.name}-main" ++ ++ plib.keyService app.sourcedFile
plib.keyService app.database.passwordFile ++ ++ app.afterServices
plib.keyService app.sourcedFile ++ ++ optional (!service.isMigration && app.database.migrate) "rails-${app.name}-migrations"
app.afterServices; ++ optional (!service.isMain && !service.isMigration) "rails-${app.name}-main";
preStart = optionalString service.isMain '' preStart = optionalString (service.isMain || service.isMigration) ''
# Link the package into the application's home directory: # Link the package into the application's home directory:
if [ ! -e "${funcs.appLink app}" ] || [ "${toString app.deployedExternally}" -ne 1 ]; then if [ ! -e "${funcs.appLink app}" ] || [ "${toString app.deployedExternally}" -ne 1 ]; then
ln -nfs "${app.package}" "${funcs.appLink app}" ln -nfs "${app.package}" "${funcs.appLink app}"
@ -73,6 +90,7 @@ let
mkdir -p ${app.home}/home mkdir -p ${app.home}/home
ln -nfs ${funcs.appLink app}/share/${app.name} ${app.home}/home/app ln -nfs ${funcs.appLink app}/share/${app.name} ${app.home}/home/app
ln -nfs ${plib.attrsToShellExports "rails-${app.name}-env" (funcs.appEnv app)} ${app.home}/home/.env ln -nfs ${plib.attrsToShellExports "rails-${app.name}-env" (funcs.appEnv app)} ${app.home}/home/.env
echo 'eval $(${scripts.user}/bin/build-path.sh "${funcs.appLink app}")' > ${app.home}/home/.path
cp ${./profile.sh} ${app.home}/home/.profile cp ${./profile.sh} ${app.home}/home/.profile
chmod 0700 ${app.home}/home/.profile chmod 0700 ${app.home}/home/.profile
@ -85,25 +103,19 @@ let
chown -R rails-${app.name}:rails-${app.name} ${app.home} chown -R rails-${app.name}:rails-${app.name} ${app.home}
chmod go+rx $(dirname "${app.home}") "${app.home}" chmod go+rx $(dirname "${app.home}") "${app.home}"
chmod u+w ${app.home}/db/schema.rb chmod u+w ${app.home}/db/schema.rb
'' + optionalString (service.isMain && app.database.migrate) ''
# Migrate the database:
${pkgs.sudo}/bin/sudo --user=rails-${app.name} --login \
${scripts.user}/bin/db-migrate.sh \
-r ${funcs.appLink app}/share/${app.name} \
-s ${app.home}/state
''; '';
script = '' script = ''
${optionalString (app.sourcedFile != null) ". ${app.home}/state/sourcedFile.sh"} ${optionalString (app.sourcedFile != null) ". ${app.home}/state/sourcedFile.sh"}
eval $(${scripts.user}/bin/build-path.sh "${funcs.appLink app}")
${service.script} ${service.script}
''; '';
serviceConfig = { serviceConfig = {
WorkingDirectory = "-${funcs.appLink app}/share/${app.name}"; WorkingDirectory = "-${funcs.appLink app}/share/${app.name}";
Restart = "on-failure"; Type = if service.isMigration then "oneshot" else "simple";
Restart = if service.isMigration then "no" else "on-failure";
TimeoutSec = "infinity"; # FIXME: what's a reasonable amount of time? TimeoutSec = "infinity"; # FIXME: what's a reasonable amount of time?
Type = "simple";
PermissionsStartOnly = true; PermissionsStartOnly = true;
User = "rails-${app.name}"; User = "rails-${app.name}";
Group = "rails-${app.name}"; Group = "rails-${app.name}";
@ -128,7 +140,9 @@ let
# systemd services. # systemd services.
appServices = app: appServices = app:
foldr (service: set: recursiveUpdate set (appService app service)) {} foldr (service: set: recursiveUpdate set (appService app service)) {}
( [(mainService app)] ++ attrValues app.services ); ( singleton (mainService app)
++ optional app.database.migrate (migrationService app)
++ attrValues app.services );
############################################################################## ##############################################################################
# Collect all services and turn them into systemd timers: # Collect all services and turn them into systemd timers: