diff --git a/lib/keys.nix b/lib/keys.nix index 361d3e2..3532b90 100644 --- a/lib/keys.nix +++ b/lib/keys.nix @@ -1,5 +1,5 @@ # Functions for working with NixOps keys. -{ lib }: +{ lib, ... }: with lib; diff --git a/lib/shell.nix b/lib/shell.nix new file mode 100644 index 0000000..98a85ea --- /dev/null +++ b/lib/shell.nix @@ -0,0 +1,27 @@ +# Functions for generating and working with shell scripts: +{ lib, pkgs, ... }: + + +with lib; + +let + funcs = rec { + + # Generate a shell script that exports the given variables. + # + # Type: + # + # string -> attrset -> derivation + # + # Arguments: + # + # fileName: The name of the file in the nix store to create. + # attrs: The variables to include in the generated script. + # + attrsToShellExports = fileName: attrs: + let export = name: value: "export ${name}=${escapeShellArg value}"; + lines = mapAttrsToList export attrs; + in pkgs.writeText fileName (concatStringsSep "\n" lines); + }; + +in funcs diff --git a/modules/default.nix b/modules/default.nix index e84f11c..0135b44 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -5,9 +5,10 @@ with lib; let libFiles = [ ../lib/keys.nix + ../lib/shell.nix ]; - loadLib = path: import path { inherit lib; }; + loadLib = path: import path { inherit lib pkgs; }; libs = foldr (a: b: recursiveUpdate (loadLib a) b) {} libFiles; in diff --git a/modules/services/web/rails/default.nix b/modules/services/web/rails/default.nix index bcfc871..17472ab 100644 --- a/modules/services/web/rails/default.nix +++ b/modules/services/web/rails/default.nix @@ -12,6 +12,17 @@ let scripts = import ./scripts.nix { inherit lib pkgs; }; options = import ./options.nix { inherit config lib pkgs; }; + ############################################################################## + # The main Rails service: + mainService = app: { + name = "main"; + isMain = true; + + script = '' + puma -e ${app.railsEnv} -p ${toString app.port} + ''; + }; + ############################################################################## # Is PostgreSQL local? localpg = config.phoebe.services.postgresql.enable; @@ -21,6 +32,18 @@ let # propagatedBuildInputs won't always be set. appPath = app: [ app.package.rubyEnv ] ++ app.package.propagatedBuildInputs; + ############################################################################## + # All of the environment variables that a Rails app needs: + appEnv = app: { + HOME = "${app.home}/home"; + RAILS_ENV = app.railsEnv; + DATABASE_HOST = app.database.host; + DATABASE_PORT = toString app.database.port; + DATABASE_NAME = app.database.name; + DATABASE_USER = app.database.user; + DATABASE_PASSWORD_FILE = "${app.home}/state/database.password"; + } // app.environment; + ############################################################################## # Collect all apps into a single set using the given function: collectApps = f: foldr (a: b: recursiveUpdate b (f a)) {} (attrValues cfg.apps); @@ -55,24 +78,15 @@ let ############################################################################## # Generate a systemd service for a Ruby on Rails application: - appService = app: { - "rails-${app.name}" = { - description = "${app.name} (Ruby on Rails)"; + appService = app: service: { + "rails-${app.name}-${service.name}" = { + description = "${app.name} (Ruby on Rails) ${service.name}"; path = appPath app; - - environment = { - HOME = "${app.home}/home"; - RAILS_ENV = app.railsEnv; - DATABASE_HOST = app.database.host; - DATABASE_PORT = toString app.database.port; - DATABASE_NAME = app.database.name; - DATABASE_USER = app.database.user; - DATABASE_PASSWORD_FILE = "${app.home}/state/database.password"; - } // app.environment; - + environment = appEnv app; wantedBy = [ "multi-user.target" ]; wants = + optional (!service.isMain) "rails-${app.name}-main" ++ plib.keyService app.database.passwordFile ++ plib.keyService app.sourcedFile; @@ -80,10 +94,11 @@ let [ "network.target" ] ++ optional localpg "postgresql.service" ++ optional localpg "pg-accounts.service" ++ + optional (!service.isMain) "rails-${app.name}-main" ++ plib.keyService app.database.passwordFile ++ plib.keyService app.sourcedFile; - preStart = '' + preStart = optionalString service.isMain '' # Prepare the config directory: rm -rf ${app.home}/config mkdir -p ${app.home}/{config,log,tmp,db,state} @@ -93,8 +108,12 @@ let cp ${./database.yml} ${app.home}/config/database.yml cp ${app.database.passwordFile} ${app.home}/state/database.password + # Additional set up for the home directory: mkdir -p ${app.home}/home ln -nfs ${app.package}/share/${app.name} ${app.home}/home/${app.name} + ln -nfs ${plib.attrsToShellExports "rails-${app.name}-env" (appEnv app)} ${app.home}/home/.env + cp ${./profile.sh} ${app.home}/home/.profile + chmod 0700 ${app.home}/home/.profile # Copy the sourcedFile if necessary: ${optionalString (app.sourcedFile != null) '' @@ -106,9 +125,9 @@ let chmod go+rx $(dirname "${app.home}") chmod u+w ${app.home}/db/schema.rb - '' + optionalString app.database.migrate '' - # Migrate the database (use sudo so environment variables go through): - ${pkgs.sudo}/bin/sudo -u rails-${app.name} -EH \ + '' + optionalString (service.isMain && app.database.migrate) '' + # Migrate the database: + ${pkgs.sudo}/bin/sudo --user=rails-${app.name} --login \ ${scripts}/bin/db-migrate.sh \ -r ${app.package}/share/${app.name} \ -s ${app.home}/state @@ -116,7 +135,7 @@ let script = '' ${optionalString (app.sourcedFile != null) ". ${app.home}/state/sourcedFile.sh"} - ${app.package.rubyEnv}/bin/puma -e ${app.railsEnv} -p ${toString app.port} + ${service.script} ''; serviceConfig = { @@ -132,6 +151,13 @@ let }; }; + ############################################################################## + # Collect all services for a given application and turn them into + # systemd services. + appServices = app: + foldr (service: set: recursiveUpdate set (appService app service)) {} + ( [(mainService app)] ++ attrValues app.services ); + ############################################################################## # Generate a user account for a Ruby on Rails application: appUser = app: { @@ -173,7 +199,8 @@ in # Each application gets a user account: users = collectApps appUser; - # Each application gets a systemd service to keep it running. - systemd.services = collectApps appService; + # Each application gets one or more systemd services to keep it + # running. + systemd.services = collectApps appServices; }; } diff --git a/modules/services/web/rails/options.nix b/modules/services/web/rails/options.nix index cb0eb82..de83da6 100644 --- a/modules/services/web/rails/options.nix +++ b/modules/services/web/rails/options.nix @@ -52,6 +52,35 @@ let }; }; + ############################################################################## + # Service configuration: + service = { name, ... }: { + options = { + name = mkOption { + type = types.str; + example = "sidekiq"; + description = "The name of the additional service to run."; + }; + + script = mkOption { + type = types.lines; + example = "sidekiq -c 5 -v -q default"; + description = "Shell commands executed as the service's main process."; + }; + + isMain = mkOption { + internal = true; + type = types.bool; + default = false; + description = "Is this the main Rails process?"; + }; + }; + + config = { + name = mkDefault name; + }; + }; + ############################################################################## # Application configuration: application = { name, ... }: { @@ -88,6 +117,20 @@ let description = "Database configuration."; }; + services = mkOption { + type = types.attrsOf (types.submodule service); + default = { }; + description = '' + Additional services to run for this Rails application. For + example, if you need to have background queue processing + scripts running this is where you'd want to do that. + + All of the listed services are run via systemd and are + executed in the same environment as the main Rails + application itself. + ''; + }; + railsEnv = mkOption { type = types.str; default = "production"; diff --git a/modules/services/web/rails/profile.sh b/modules/services/web/rails/profile.sh new file mode 100644 index 0000000..58d412b --- /dev/null +++ b/modules/services/web/rails/profile.sh @@ -0,0 +1,9 @@ +# Rails user shell profile. + +if [ -e "$HOME/.env" ]; then + . "$HOME/.env" +fi + +if [ -e "$HOME/../state/sourcedFile.sh" ]; then + . "$HOME/../state/sourcedFile.sh" +fi