mirror of
https://github.com/vale981/phoebe
synced 2025-03-04 09:21:40 -05:00
security, rails, postgresql: Import files from original repo
This commit is contained in:
commit
3980c37fa0
20 changed files with 873 additions and 0 deletions
26
LICENSE
Normal file
26
LICENSE
Normal file
|
@ -0,0 +1,26 @@
|
|||
Copyright (c) 2016-2018 Peter J. Jones <pjones@devalot.com>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
26
README.md
Normal file
26
README.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
Phoebe is a set of [NixOS][] modules that provide additional
|
||||
functionality on top of the existing modules in [Nixpkgs][]. The name
|
||||
of this package was taken from the name of [Saturn's moon][phoebe].
|
||||
|
||||
Module List
|
||||
-----------
|
||||
|
||||
* `phoebe.security`:
|
||||
|
||||
Automatically enable various security related settings for NixOS.
|
||||
|
||||
* `phoebe.services.postgresql`:
|
||||
|
||||
Start and manage PostgreSQL, including automatic user and database
|
||||
creation.
|
||||
|
||||
* `phoebe.services.rails`:
|
||||
|
||||
Configure and manage Ruby on Rails applications. Includes a
|
||||
helper function to help package Rails applications so they can be
|
||||
used by this service.
|
||||
|
||||
|
||||
[nixos]: https://nixos.org/
|
||||
[nixpkgs]: https://nixos.org/nixpkgs/
|
||||
[phoebe]: https://en.wikipedia.org/wiki/Phoebe_(moon)
|
7
default.nix
Normal file
7
default.nix
Normal file
|
@ -0,0 +1,7 @@
|
|||
{ config, lib, pkgs, ...}:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./modules
|
||||
];
|
||||
}
|
14
helpers.nix
Normal file
14
helpers.nix
Normal file
|
@ -0,0 +1,14 @@
|
|||
{ pkgs }:
|
||||
|
||||
let
|
||||
callPackage = pkgs.lib.callPackageWith self;
|
||||
|
||||
self = {
|
||||
inherit pkgs;
|
||||
|
||||
rails = callPackage ./modules/services/web/rails/helpers.nix { };
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit (self.rails) mkRailsDerivation;
|
||||
}
|
8
modules/default.nix
Normal file
8
modules/default.nix
Normal file
|
@ -0,0 +1,8 @@
|
|||
{ config, lib, pkgs, ...}:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./security
|
||||
./services
|
||||
];
|
||||
}
|
41
modules/security/default.nix
Normal file
41
modules/security/default.nix
Normal file
|
@ -0,0 +1,41 @@
|
|||
{ config, lib, pkgs, ...}:
|
||||
|
||||
# Bring in library functions:
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.phoebe.security;
|
||||
|
||||
in
|
||||
{
|
||||
#### Interface
|
||||
options.phoebe.security = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether or not to enable security settings. Usually this will
|
||||
be left at the default value of true. However, for testing
|
||||
inside virtual machines you probably wnat to turn this off.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
#### Implementation
|
||||
config = mkMerge [
|
||||
(mkIf (!cfg.enable) {
|
||||
# Only really useful for development VMs:
|
||||
networking.firewall.enable = false;
|
||||
})
|
||||
|
||||
(mkIf cfg.enable {
|
||||
# Firewall:
|
||||
networking.firewall = {
|
||||
enable = true;
|
||||
allowPing = true;
|
||||
pingLimit = "--limit 1/minute --limit-burst 5";
|
||||
allowedTCPPorts = config.services.openssh.ports;
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
7
modules/services/databases/default.nix
Normal file
7
modules/services/databases/default.nix
Normal file
|
@ -0,0 +1,7 @@
|
|||
{ config, lib, pkgs, ...}:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./postgresql
|
||||
];
|
||||
}
|
24
modules/services/databases/postgresql/create-user.nix
Normal file
24
modules/services/databases/postgresql/create-user.nix
Normal file
|
@ -0,0 +1,24 @@
|
|||
{ config, lib, pkgs, ...}:
|
||||
|
||||
pkgs.stdenvNoCC.mkDerivation {
|
||||
name = "pg-create-user";
|
||||
phases = [ "installPhase" "fixupPhase" ];
|
||||
|
||||
installPhase = ''
|
||||
# Substitution variables:
|
||||
export sudo=${pkgs.sudo}/bin/sudo
|
||||
export superuser=${config.services.postgresql.superUser}
|
||||
|
||||
mkdir -p $out/bin $out/sql
|
||||
cp ${./create-user.sql} $out/sql/create-user.sql
|
||||
substituteAll ${./create-user.sh} $out/bin/create-user.sh
|
||||
chmod 555 $out/bin/create-user.sh
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "Automatically create PosgreSQL databases and users as needed.";
|
||||
homepage = https://git.devalot.com/pjones/phoebe/;
|
||||
maintainers = with maintainers; [ pjones ];
|
||||
platforms = platforms.all;
|
||||
};
|
||||
}
|
120
modules/services/databases/postgresql/create-user.sh
Executable file
120
modules/services/databases/postgresql/create-user.sh
Executable file
|
@ -0,0 +1,120 @@
|
|||
#!/bin/bash
|
||||
|
||||
################################################################################
|
||||
set -e
|
||||
|
||||
################################################################################
|
||||
option_username=""
|
||||
option_password_file=""
|
||||
option_database=""
|
||||
option_extensions=""
|
||||
option_sqlfile="@out@/sql/create-user.sql"
|
||||
|
||||
################################################################################
|
||||
usage () {
|
||||
cat <<EOF
|
||||
Usage: create-user.sh [options]
|
||||
|
||||
-d NAME Database name to create for USER
|
||||
-e LIST Space-separated list of extensions to enable
|
||||
-h This message
|
||||
-p FILE File containing USER's password
|
||||
-s FILE The SQL template file (pg-create-user.sql)
|
||||
-u USER Username to create
|
||||
EOF
|
||||
}
|
||||
|
||||
################################################################################
|
||||
while getopts "d:e:hp:s:u:" o; do
|
||||
case "${o}" in
|
||||
d) option_database=$OPTARG
|
||||
;;
|
||||
|
||||
e) option_extensions=$OPTARG
|
||||
;;
|
||||
|
||||
h) usage
|
||||
exit
|
||||
;;
|
||||
|
||||
p) option_password_file=$OPTARG
|
||||
;;
|
||||
|
||||
s) option_sqlfile=$OPTARG
|
||||
;;
|
||||
|
||||
u) option_username=$OPTARG
|
||||
;;
|
||||
|
||||
*) exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
shift $((OPTIND-1))
|
||||
|
||||
################################################################################
|
||||
tmp_sql_file=$(mktemp --suffix=.sql --tmpdir new-user.XXXXXXXXX)
|
||||
|
||||
cleanup() {
|
||||
rm -f "$tmp_sql_file"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
################################################################################
|
||||
_psql() {
|
||||
@sudo@ -u @superuser@ -H psql "$@"
|
||||
}
|
||||
|
||||
################################################################################
|
||||
mksql() {
|
||||
# FIXME: Passwords can't contain single quotes due to this simple logic:
|
||||
if head -n 1 "$option_password_file" | grep -q "'"; then
|
||||
>&2 echo "ERROR: password for $option_username contains single quote!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
password=$(head -n 1 "$option_password_file")
|
||||
|
||||
awk -v 'USERNAME'="$option_username" \
|
||||
-v 'PASSWORD'="$password" \
|
||||
' { gsub(/@@USERNAME@@/, USERNAME);
|
||||
gsub(/@@PASSWORD@@/, PASSWORD);
|
||||
print;
|
||||
}
|
||||
' < "$option_sqlfile" > "$tmp_sql_file"
|
||||
|
||||
# Let the database user read the generated file.
|
||||
chmod go+r "$tmp_sql_file"
|
||||
}
|
||||
|
||||
################################################################################
|
||||
create_user() {
|
||||
mksql
|
||||
_psql -d postgres -f "$tmp_sql_file" > /dev/null
|
||||
}
|
||||
|
||||
################################################################################
|
||||
create_database() {
|
||||
has_db=$(_psql -tAl | cut -d'|' -f1 | grep -cF "$option_database" || :)
|
||||
|
||||
if [ "$has_db" -eq 0 ]; then
|
||||
@sudo@ -u @superuser@ -H \
|
||||
createdb --owner "$option_username" "$option_database"
|
||||
fi
|
||||
}
|
||||
|
||||
################################################################################
|
||||
enable_extensions() {
|
||||
if [ -n "$option_extensions" ]; then
|
||||
for ext in $option_extensions; do
|
||||
_psql "$option_database" -c "CREATE EXTENSION IF NOT EXISTS $ext"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
################################################################################
|
||||
create_user
|
||||
create_database
|
||||
enable_extensions
|
12
modules/services/databases/postgresql/create-user.sql
Normal file
12
modules/services/databases/postgresql/create-user.sql
Normal file
|
@ -0,0 +1,12 @@
|
|||
DO
|
||||
$body$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT
|
||||
FROM pg_catalog.pg_roles
|
||||
WHERE rolname = '@@USERNAME@@') THEN
|
||||
|
||||
CREATE ROLE @@USERNAME@@ LOGIN ENCRYPTED PASSWORD '@@PASSWORD@@';
|
||||
END IF;
|
||||
END
|
||||
$body$;
|
145
modules/services/databases/postgresql/default.nix
Normal file
145
modules/services/databases/postgresql/default.nix
Normal file
|
@ -0,0 +1,145 @@
|
|||
# Configure PostgreSQL:
|
||||
{ config, lib, pkgs, ...}:
|
||||
|
||||
# Bring in library functions:
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.phoebe.services.postgresql;
|
||||
superuser = config.services.postgresql.superUser;
|
||||
create-user = import ./create-user.nix { inherit config lib pkgs; };
|
||||
afterservices = concatMap (a: a.afterServices) (attrValues cfg.accounts);
|
||||
|
||||
# Per-account options:
|
||||
account = { name, ... }: {
|
||||
|
||||
#### Interface:
|
||||
options = {
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = null;
|
||||
example = "jdoe";
|
||||
description = "The name of the account (username).";
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.path;
|
||||
default = null;
|
||||
example = "/run/keys/pgpass.txt";
|
||||
description = ''
|
||||
A file containing the password of this database user.
|
||||
You'll want to use something like NixOps to get the password
|
||||
file onto the target machine.
|
||||
'';
|
||||
};
|
||||
|
||||
afterServices = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "dbpassword.service" ];
|
||||
description = ''
|
||||
A list of services that need to run before this user account
|
||||
can be created. This is really useful if you are using
|
||||
NixOps to deploy the password file and want to wait for the
|
||||
key to appear in /run/keys.
|
||||
'';
|
||||
};
|
||||
|
||||
database = mkOption {
|
||||
type = types.str;
|
||||
default = null;
|
||||
example = "jdoe";
|
||||
description = ''
|
||||
The name of the database this user can access. Defaults to
|
||||
the account name.
|
||||
'';
|
||||
};
|
||||
|
||||
extensions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "pg_trgm" ];
|
||||
description = "A list of extension modules to enable for the database.";
|
||||
};
|
||||
|
||||
netmask = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "127.0.0.1/32";
|
||||
description = ''
|
||||
IP netmask of remote machines allowed to connect. Leaving
|
||||
this at it's default value means this account can only
|
||||
connect through Unix domain sockets.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
#### Implementation:
|
||||
config = {
|
||||
user = mkDefault name;
|
||||
database = mkDefault name;
|
||||
};
|
||||
};
|
||||
|
||||
# Create HBA authentication entries:
|
||||
accountToHBA = account:
|
||||
''
|
||||
local ${account.database} ${account.user} md5
|
||||
host ${account.database} ${account.user} 127.0.0.1/32 md5
|
||||
host ${account.database} ${account.user} ::1/28 md5
|
||||
'' + optionalString (account.netmask != null) ''
|
||||
host ${account.database} ${account.user} ${account.netmask} md5
|
||||
'';
|
||||
|
||||
# Commands to run to create accounts/databases:
|
||||
createScript = account:
|
||||
''
|
||||
${create-user}/bin/create-user.sh \
|
||||
-u "${account.user}" \
|
||||
-d "${account.database}" \
|
||||
-p "${account.passwordFile}" \
|
||||
-e "${concatStringsSep " " account.extensions}"
|
||||
'';
|
||||
|
||||
in
|
||||
{
|
||||
#### Interface
|
||||
options.phoebe.services.postgresql = {
|
||||
enable = mkEnableOption "PostgreSQL";
|
||||
|
||||
accounts = mkOption {
|
||||
type = types.attrsOf (types.submodule account);
|
||||
default = { };
|
||||
description = "Additional user accounts";
|
||||
};
|
||||
};
|
||||
|
||||
#### Implementation
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
# Set up PosgreSQL:
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
enableTCPIP = true;
|
||||
package = pkgs.postgresql;
|
||||
|
||||
# The superuser can access all databases locally, remote access
|
||||
# for some users.
|
||||
authentication = mkForce (
|
||||
"local all ${superuser} peer\n" +
|
||||
"host all ${superuser} 127.0.0.1/32 ident\n" +
|
||||
"host all ${superuser} ::1/128 ident\n" +
|
||||
concatMapStringsSep "\n" accountToHBA (attrValues cfg.accounts));
|
||||
};
|
||||
|
||||
# Create missing accounts:
|
||||
systemd.services.pg-accounts = mkIf (length (attrValues cfg.accounts) > 0) {
|
||||
description = "PostgreSQL Account Manager";
|
||||
path = [ pkgs.gawk config.services.postgresql.package ];
|
||||
script = (concatMapStringsSep "\n" createScript (attrValues cfg.accounts));
|
||||
wantedBy = [ "postgresql.service" ];
|
||||
after = [ "postgresql.service" ] ++ afterservices;
|
||||
wants = afterservices;
|
||||
};
|
||||
};
|
||||
}
|
8
modules/services/default.nix
Normal file
8
modules/services/default.nix
Normal file
|
@ -0,0 +1,8 @@
|
|||
{ config, lib, pkgs, ...}:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./databases
|
||||
./web
|
||||
];
|
||||
}
|
7
modules/services/web/default.nix
Normal file
7
modules/services/web/default.nix
Normal file
|
@ -0,0 +1,7 @@
|
|||
{ config, lib, pkgs, ...}:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./rails
|
||||
];
|
||||
}
|
10
modules/services/web/rails/database.yml
Normal file
10
modules/services/web/rails/database.yml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<%= ENV['RAILS_ENV'] %>:
|
||||
adapter: postgresql
|
||||
encoding: unicode
|
||||
pool: 5
|
||||
timeout: 5000
|
||||
host: <%= ENV['DATABASE_HOST'] %>
|
||||
port: <%= ENV['DATABASE_PORT'] %>
|
||||
database: <%= ENV['DATABASE_NAME'] %>
|
||||
username: <%= ENV['DATABASE_USER'] %>
|
||||
password: '<%= File.read(ENV['DATABASE_PASSWORD_FILE']).chomp %>'
|
58
modules/services/web/rails/db-migrate.sh
Executable file
58
modules/services/web/rails/db-migrate.sh
Executable file
|
@ -0,0 +1,58 @@
|
|||
#!/bin/bash
|
||||
|
||||
################################################################################
|
||||
# Migrate a Ruby on Rails database to its latest version (which might
|
||||
# mean going back in time for a rollback).
|
||||
set -e
|
||||
set -u
|
||||
|
||||
################################################################################
|
||||
option_env=${RAILS_ENV:-production}
|
||||
option_root=$(pwd)
|
||||
|
||||
################################################################################
|
||||
usage () {
|
||||
cat <<EOF
|
||||
Usage: db-migrate.sh [options]
|
||||
|
||||
-e NAME Set RAILS_ENV to NAME
|
||||
-h This message
|
||||
-r DIR The root directory of the Rails app
|
||||
EOF
|
||||
}
|
||||
|
||||
################################################################################
|
||||
while getopts "he:r:" o; do
|
||||
case "${o}" in
|
||||
e) option_env=$OPTARG
|
||||
;;
|
||||
|
||||
h) usage
|
||||
exit
|
||||
;;
|
||||
|
||||
r) option_root=$OPTARG
|
||||
;;
|
||||
|
||||
*) exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
shift $((OPTIND-1))
|
||||
|
||||
################################################################################
|
||||
cd "$option_root"
|
||||
export RAILS_ENV=$option_env
|
||||
|
||||
################################################################################
|
||||
# If this is a new database, load the schema file:
|
||||
if [ ! -e config/database-loaded.flag ]; then
|
||||
rake db:schema:load
|
||||
touch config/database-loaded.flag
|
||||
fi
|
||||
|
||||
################################################################################
|
||||
# Migrate to the most recent migration version:
|
||||
latest=$(find db/migrate -type f -exec basename '{}' ';' | sort | tail -n 1 | cut -d_ -f1)
|
||||
rake db:migrate VERSION="$latest"
|
159
modules/services/web/rails/default.nix
Normal file
159
modules/services/web/rails/default.nix
Normal file
|
@ -0,0 +1,159 @@
|
|||
# Configure Ruby on Rails applications:
|
||||
{ config, lib, pkgs, ...}:
|
||||
|
||||
# Bring in library functions:
|
||||
with lib;
|
||||
|
||||
let
|
||||
##############################################################################
|
||||
# Save some typing.
|
||||
cfg = config.phoebe.services.rails;
|
||||
scripts = import ./scripts.nix { inherit lib pkgs; };
|
||||
options = import ./options.nix { inherit config lib pkgs; };
|
||||
|
||||
##############################################################################
|
||||
# Is PostgreSQL local?
|
||||
localpg = config.phoebe.services.postgresql.enable;
|
||||
|
||||
##############################################################################
|
||||
# Packages to put in the application's PATH. FIXME:
|
||||
# propagatedBuildInputs won't always be set.
|
||||
appPath = app: [ app.package.rubyEnv ] ++ app.package.propagatedBuildInputs;
|
||||
|
||||
##############################################################################
|
||||
# Collect all apps into a single set using the given function:
|
||||
collectApps = f: foldr (a: b: recursiveUpdate b (f a)) {} (attrValues cfg.apps);
|
||||
|
||||
##############################################################################
|
||||
# Generate an NGINX configuration for an application:
|
||||
appToVirtualHost = app: {
|
||||
"${app.domain}" = {
|
||||
forceSSL = config.phoebe.security.enable;
|
||||
enableACME = config.phoebe.security.enable;
|
||||
root = "${app.package}/share/${app.name}/public";
|
||||
|
||||
locations = {
|
||||
"/assets/" = {
|
||||
extraConfig = ''
|
||||
gzip_static on;
|
||||
expires 1M;
|
||||
add_header Cache-Control public;
|
||||
'';
|
||||
};
|
||||
|
||||
"/" = {
|
||||
tryFiles = "$uri @app";
|
||||
};
|
||||
|
||||
"@app" = {
|
||||
proxyPass = "http://localhost:${toString app.port}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
##############################################################################
|
||||
# Generate a systemd service for a Ruby on Rails application:
|
||||
appService = app: {
|
||||
"rails-${app.name}" = {
|
||||
description = "${app.name} (Ruby on Rails)";
|
||||
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}/config/database.password";
|
||||
} // app.environment;
|
||||
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
wants = optional (app.database.passwordService != null) app.database.passwordService;
|
||||
after = [ "network.target" ] ++
|
||||
optional localpg "postgresql.service" ++
|
||||
optional (app.database.passwordService != null) app.database.passwordService;
|
||||
|
||||
preStart = ''
|
||||
# Prepare the config directory:
|
||||
rm -rf ${app.home}/config
|
||||
mkdir -p ${app.home}/{config,log,tmp,db}
|
||||
cp -rf ${app.package}/share/${app.name}/config.dist/* ${app.home}/config/
|
||||
cp ${app.package}/share/${app.name}/db/schema.rb.dist ${app.home}/db/schema.rb
|
||||
cp ${./database.yml} ${app.home}/config/database.yml
|
||||
cp ${app.database.passwordFile} ${app.home}/config/database.password
|
||||
|
||||
mkdir -p ${app.home}/home
|
||||
ln -nfs ${app.package}/share/${app.name} ${app.home}/home/${app.name}
|
||||
|
||||
# Fix permissions:
|
||||
chown -R rails-${app.name}:rails-${app.name} ${app.home}
|
||||
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 \
|
||||
${scripts}/bin/db-migrate.sh -r ${app.package}/share/${app.name}
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
WorkingDirectory = "${app.package}/share/${app.name}";
|
||||
Restart = "on-failure";
|
||||
TimeoutSec = "infinity"; # FIXME: what's a reasonable amount of time?
|
||||
Type = "simple";
|
||||
PermissionsStartOnly = true;
|
||||
User = "rails-${app.name}";
|
||||
Group = "rails-${app.name}";
|
||||
UMask = "0077";
|
||||
ExecStart = "${app.package.rubyEnv}/bin/puma -e ${app.railsEnv} -p ${toString app.port}";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
##############################################################################
|
||||
# Generate a user account for a Ruby on Rails application:
|
||||
appUser = app: {
|
||||
users."rails-${app.name}" = {
|
||||
description = "${app.name} Ruby on Rails Application";
|
||||
home = "${app.home}/home";
|
||||
createHome = true;
|
||||
group = "rails-${app.name}";
|
||||
shell = "${pkgs.bash}/bin/bash";
|
||||
extraGroups = [ config.services.nginx.group ];
|
||||
packages = appPath app;
|
||||
};
|
||||
groups."rails-${app.name}" = {};
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
#### Interface
|
||||
options.phoebe.services.rails = {
|
||||
apps = mkOption {
|
||||
type = types.attrsOf (types.submodule options.application);
|
||||
default = { };
|
||||
description = "Rails applications to configure.";
|
||||
};
|
||||
};
|
||||
|
||||
#### Implementation
|
||||
config = mkIf (length (attrValues cfg.apps) != 0) {
|
||||
# Use NGINX to proxy requests to the apps:
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
recommendedTlsSettings = config.phoebe.security.enable;
|
||||
recommendedOptimisation = true;
|
||||
recommendedGzipSettings = true;
|
||||
recommendedProxySettings = true;
|
||||
virtualHosts = collectApps appToVirtualHost;
|
||||
};
|
||||
|
||||
# Each application gets a user account:
|
||||
users = collectApps appUser;
|
||||
|
||||
# Each application gets a systemd service to keep it running.
|
||||
systemd.services = collectApps appService;
|
||||
};
|
||||
}
|
7
modules/services/web/rails/functions.nix
Normal file
7
modules/services/web/rails/functions.nix
Normal file
|
@ -0,0 +1,7 @@
|
|||
rec {
|
||||
# The default base directory for Rails applications:
|
||||
base = "/var/lib/rails";
|
||||
|
||||
# Where a Rails application lives:
|
||||
home = name: "${base}/${name}";
|
||||
}
|
52
modules/services/web/rails/helpers.nix
Normal file
52
modules/services/web/rails/helpers.nix
Normal file
|
@ -0,0 +1,52 @@
|
|||
# Helper package Ruby on Rails applications that work with the rails
|
||||
# service in this directory.
|
||||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
functions = import ./functions.nix;
|
||||
|
||||
mkRailsDerivation =
|
||||
{ name
|
||||
, env # The output of bundlerEnv
|
||||
, extraPackages ? [ ]
|
||||
, buildPhase ? ""
|
||||
, installPhase ? ""
|
||||
, buildInputs ? [ ]
|
||||
, propagatedBuildInputs ? [ ]
|
||||
, ...
|
||||
}@args:
|
||||
pkgs.stdenv.mkDerivation (args // {
|
||||
buildInputs = [ env env.wrappedRuby ] ++ buildInputs;
|
||||
propagatedBuildInputs = extraPackages ++ propagatedBuildInputs;
|
||||
passthru = { rubyEnv = env; ruby = env.wrappedRuby; };
|
||||
|
||||
buildPhase = ''
|
||||
${buildPhase}
|
||||
|
||||
# Build all the assets into the package:
|
||||
rake assets:precompile
|
||||
|
||||
# Move some files out of the way since they will be created
|
||||
# in production:
|
||||
rm config/database.yml
|
||||
mv config config.dist
|
||||
mv db/schema.rb db/schema.rb.dist
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p "$out/share"
|
||||
${installPhase}
|
||||
|
||||
cp -r . "$out/share/${name}"
|
||||
rm -rf "$out/share/${name}/log"
|
||||
rm -rf "$out/share/${name}/tmp"
|
||||
|
||||
# Install some links to where the app lives in production:
|
||||
for f in log config tmp db/schema.rb; do
|
||||
ln -sf "${functions.home name}/$f" "$out/share/${name}/$f"
|
||||
done
|
||||
'';
|
||||
});
|
||||
in
|
||||
{ inherit mkRailsDerivation;
|
||||
}
|
123
modules/services/web/rails/options.nix
Normal file
123
modules/services/web/rails/options.nix
Normal file
|
@ -0,0 +1,123 @@
|
|||
{ config, lib, pkgs, ...}:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
##############################################################################
|
||||
functions = import ./functions.nix;
|
||||
|
||||
##############################################################################
|
||||
# Database configuration:
|
||||
database = {
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
example = "marketing";
|
||||
description = "Database name.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
example = "jdoe";
|
||||
description = "Database user name.";
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.path;
|
||||
example = "/run/keys/db-password";
|
||||
description = ''
|
||||
A file containing the database password. This allows you to
|
||||
deploy a password with NixOps.
|
||||
'';
|
||||
};
|
||||
|
||||
passwordService = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "db-password.service";
|
||||
description = ''
|
||||
A service to wait on before starting the Rails application.
|
||||
This service should provide the password file for the
|
||||
passwordFile option. Useful when deploying passwords with
|
||||
NixOps.
|
||||
'';
|
||||
};
|
||||
|
||||
migrate = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
example = false;
|
||||
description = "Whether or not database migrations should run on start.";
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "Host name for the database server.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.int;
|
||||
default = config.services.postgresql.port;
|
||||
description = "Port number for the database server";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
##############################################################################
|
||||
# Application configuration:
|
||||
application = { name, ... }: {
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
description = "The name of the Ruby on Rails application.";
|
||||
};
|
||||
|
||||
home = mkOption {
|
||||
type = types.path;
|
||||
description = "The directory where the application is deployed to.";
|
||||
};
|
||||
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
default = null;
|
||||
description = "The FQDN to use for this application.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.int;
|
||||
default = null;
|
||||
description = "The port number to forward requests to.";
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
description = "The derivation for the Ruby on Rails application.";
|
||||
};
|
||||
|
||||
database = mkOption {
|
||||
type = types.submodule database;
|
||||
description = "Database configuration.";
|
||||
};
|
||||
|
||||
railsEnv = mkOption {
|
||||
type = types.str;
|
||||
default = "production";
|
||||
example = "development";
|
||||
description = "What to use for RAILS_ENV.";
|
||||
};
|
||||
|
||||
environment = mkOption {
|
||||
type = types.attrs;
|
||||
default = { };
|
||||
description = "Environment variables.";
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
name = mkDefault name;
|
||||
home = mkDefault (functions.home name);
|
||||
};
|
||||
};
|
||||
|
||||
in { inherit database application; }
|
19
modules/services/web/rails/scripts.nix
Normal file
19
modules/services/web/rails/scripts.nix
Normal file
|
@ -0,0 +1,19 @@
|
|||
{ lib, pkgs, ...}:
|
||||
|
||||
pkgs.stdenvNoCC.mkDerivation {
|
||||
name = "rails-scripts";
|
||||
phases = [ "installPhase" "fixupPhase" ];
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
substituteAll ${./db-migrate.sh} $out/bin/db-migrate.sh
|
||||
find $out/bin -type f -exec chmod 555 '{}' ';'
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "Scripts for working with Ruby on Rails applications.";
|
||||
homepage = https://git.devalot.com/pjones/phoebe/;
|
||||
maintainers = with maintainers; [ pjones ];
|
||||
platforms = platforms.all;
|
||||
};
|
||||
}
|
Loading…
Add table
Reference in a new issue