mirror of
https://github.com/vale981/phoebe
synced 2025-03-04 17:31:41 -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