Browse Source

security, rails, postgresql: Import files from original repo

pjones/monitoring
Peter J. Jones 5 months ago
commit
3980c37fa0
Signed by: Peter Jones <pjones@devalot.com> GPG Key ID: 9DAFAA8D01941E49

+ 26
- 0
LICENSE View File

@@ -0,0 +1,26 @@
1
+Copyright (c) 2016-2018 Peter J. Jones <pjones@devalot.com>
2
+All rights reserved.
3
+
4
+Redistribution and use in source and binary forms, with or without
5
+modification, are permitted provided that the following conditions are
6
+met:
7
+
8
+1. Redistributions of source code must retain the above copyright
9
+   notice, this list of conditions and the following disclaimer.
10
+
11
+2. Redistributions in binary form must reproduce the above copyright
12
+   notice, this list of conditions and the following disclaimer in the
13
+   documentation and/or other materials provided with the
14
+   distribution.
15
+
16
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 26
- 0
README.md View File

@@ -0,0 +1,26 @@
1
+Phoebe is a set of [NixOS][] modules that provide additional
2
+functionality on top of the existing modules in [Nixpkgs][].  The name
3
+of this package was taken from the name of [Saturn's moon][phoebe].
4
+
5
+Module List
6
+-----------
7
+
8
+  * `phoebe.security`:
9
+
10
+     Automatically enable various security related settings for NixOS.
11
+
12
+  * `phoebe.services.postgresql`:
13
+
14
+    Start and manage PostgreSQL, including automatic user and database
15
+    creation.
16
+
17
+  * `phoebe.services.rails`:
18
+
19
+    Configure and manage Ruby on Rails applications.  Includes a
20
+    helper function to help package Rails applications so they can be
21
+    used by this service.
22
+
23
+
24
+[nixos]: https://nixos.org/
25
+[nixpkgs]: https://nixos.org/nixpkgs/
26
+[phoebe]: https://en.wikipedia.org/wiki/Phoebe_(moon)

+ 7
- 0
default.nix View File

@@ -0,0 +1,7 @@
1
+{ config, lib, pkgs, ...}:
2
+
3
+{
4
+  imports = [
5
+    ./modules
6
+  ];
7
+}

+ 14
- 0
helpers.nix View File

@@ -0,0 +1,14 @@
1
+{ pkgs }:
2
+
3
+let
4
+  callPackage = pkgs.lib.callPackageWith self;
5
+
6
+  self = {
7
+    inherit pkgs;
8
+
9
+    rails = callPackage ./modules/services/web/rails/helpers.nix { };
10
+  };
11
+in
12
+{
13
+  inherit (self.rails) mkRailsDerivation;
14
+}

+ 8
- 0
modules/default.nix View File

@@ -0,0 +1,8 @@
1
+{ config, lib, pkgs, ...}:
2
+
3
+{
4
+  imports = [
5
+    ./security
6
+    ./services
7
+  ];
8
+}

+ 41
- 0
modules/security/default.nix View File

@@ -0,0 +1,41 @@
1
+{ config, lib, pkgs, ...}:
2
+
3
+# Bring in library functions:
4
+with lib;
5
+
6
+let
7
+  cfg = config.phoebe.security;
8
+
9
+in
10
+{
11
+  #### Interface
12
+  options.phoebe.security = {
13
+    enable = mkOption {
14
+      type = types.bool;
15
+      default = true;
16
+      description = ''
17
+        Whether or not to enable security settings.  Usually this will
18
+        be left at the default value of true.  However, for testing
19
+        inside virtual machines you probably wnat to turn this off.
20
+      '';
21
+    };
22
+  };
23
+
24
+  #### Implementation
25
+  config = mkMerge [
26
+    (mkIf (!cfg.enable) {
27
+      # Only really useful for development VMs:
28
+      networking.firewall.enable = false;
29
+    })
30
+
31
+    (mkIf cfg.enable {
32
+      # Firewall:
33
+      networking.firewall = {
34
+        enable = true;
35
+        allowPing = true;
36
+        pingLimit = "--limit 1/minute --limit-burst 5";
37
+        allowedTCPPorts = config.services.openssh.ports;
38
+      };
39
+    })
40
+  ];
41
+}

+ 7
- 0
modules/services/databases/default.nix View File

@@ -0,0 +1,7 @@
1
+{ config, lib, pkgs, ...}:
2
+
3
+{
4
+  imports = [
5
+    ./postgresql
6
+  ];
7
+}

+ 24
- 0
modules/services/databases/postgresql/create-user.nix View File

@@ -0,0 +1,24 @@
1
+{ config, lib, pkgs, ...}:
2
+
3
+pkgs.stdenvNoCC.mkDerivation {
4
+  name = "pg-create-user";
5
+  phases = [ "installPhase" "fixupPhase" ];
6
+
7
+  installPhase = ''
8
+    # Substitution variables:
9
+    export sudo=${pkgs.sudo}/bin/sudo
10
+    export superuser=${config.services.postgresql.superUser}
11
+
12
+    mkdir -p $out/bin $out/sql
13
+    cp ${./create-user.sql} $out/sql/create-user.sql
14
+    substituteAll ${./create-user.sh} $out/bin/create-user.sh
15
+    chmod 555 $out/bin/create-user.sh
16
+  '';
17
+
18
+  meta = with lib; {
19
+    description = "Automatically create PosgreSQL databases and users as needed.";
20
+    homepage = https://git.devalot.com/pjones/phoebe/;
21
+    maintainers = with maintainers; [ pjones ];
22
+    platforms = platforms.all;
23
+  };
24
+}

+ 120
- 0
modules/services/databases/postgresql/create-user.sh View File

@@ -0,0 +1,120 @@
1
+#!/bin/bash
2
+
3
+################################################################################
4
+set -e
5
+
6
+################################################################################
7
+option_username=""
8
+option_password_file=""
9
+option_database=""
10
+option_extensions=""
11
+option_sqlfile="@out@/sql/create-user.sql"
12
+
13
+################################################################################
14
+usage () {
15
+cat <<EOF
16
+Usage: create-user.sh [options]
17
+
18
+  -d NAME Database name to create for USER
19
+  -e LIST Space-separated list of extensions to enable
20
+  -h      This message
21
+  -p FILE File containing USER's password
22
+  -s FILE The SQL template file (pg-create-user.sql)
23
+  -u USER Username to create
24
+EOF
25
+}
26
+
27
+################################################################################
28
+while getopts "d:e:hp:s:u:" o; do
29
+  case "${o}" in
30
+    d) option_database=$OPTARG
31
+       ;;
32
+
33
+    e) option_extensions=$OPTARG
34
+       ;;
35
+
36
+    h) usage
37
+       exit
38
+       ;;
39
+
40
+    p) option_password_file=$OPTARG
41
+       ;;
42
+
43
+    s) option_sqlfile=$OPTARG
44
+       ;;
45
+
46
+    u) option_username=$OPTARG
47
+       ;;
48
+
49
+    *) exit 1
50
+       ;;
51
+  esac
52
+done
53
+
54
+shift $((OPTIND-1))
55
+
56
+################################################################################
57
+tmp_sql_file=$(mktemp --suffix=.sql --tmpdir new-user.XXXXXXXXX)
58
+
59
+cleanup() {
60
+  rm -f "$tmp_sql_file"
61
+}
62
+
63
+trap cleanup EXIT
64
+
65
+################################################################################
66
+_psql() {
67
+  @sudo@ -u @superuser@ -H psql "$@"
68
+}
69
+
70
+################################################################################
71
+mksql() {
72
+  # FIXME: Passwords can't contain single quotes due to this simple logic:
73
+  if head -n 1 "$option_password_file" | grep -q "'"; then
74
+    >&2 echo "ERROR: password for $option_username contains single quote!"
75
+    exit 1
76
+  fi
77
+
78
+  password=$(head -n 1 "$option_password_file")
79
+
80
+  awk -v 'USERNAME'="$option_username" \
81
+      -v 'PASSWORD'="$password" \
82
+      ' { gsub(/@@USERNAME@@/, USERNAME);
83
+          gsub(/@@PASSWORD@@/, PASSWORD);
84
+          print;
85
+        }
86
+      ' < "$option_sqlfile" > "$tmp_sql_file"
87
+
88
+  # Let the database user read the generated file.
89
+  chmod go+r "$tmp_sql_file"
90
+}
91
+
92
+################################################################################
93
+create_user() {
94
+  mksql
95
+  _psql -d postgres -f "$tmp_sql_file" > /dev/null
96
+}
97
+
98
+################################################################################
99
+create_database() {
100
+  has_db=$(_psql -tAl | cut -d'|' -f1 | grep -cF "$option_database" || :)
101
+
102
+  if [ "$has_db" -eq 0 ]; then
103
+    @sudo@ -u @superuser@ -H \
104
+     createdb --owner "$option_username" "$option_database"
105
+  fi
106
+}
107
+
108
+################################################################################
109
+enable_extensions() {
110
+  if [ -n "$option_extensions" ]; then
111
+    for ext in $option_extensions; do
112
+      _psql "$option_database" -c "CREATE EXTENSION IF NOT EXISTS $ext"
113
+    done
114
+  fi
115
+}
116
+
117
+################################################################################
118
+create_user
119
+create_database
120
+enable_extensions

+ 12
- 0
modules/services/databases/postgresql/create-user.sql View File

@@ -0,0 +1,12 @@
1
+DO
2
+$body$
3
+BEGIN
4
+  IF NOT EXISTS (
5
+    SELECT
6
+      FROM pg_catalog.pg_roles
7
+     WHERE rolname = '@@USERNAME@@') THEN
8
+
9
+    CREATE ROLE @@USERNAME@@ LOGIN ENCRYPTED PASSWORD '@@PASSWORD@@';
10
+  END IF;
11
+END
12
+$body$;

+ 145
- 0
modules/services/databases/postgresql/default.nix View File

@@ -0,0 +1,145 @@
1
+# Configure PostgreSQL:
2
+{ config, lib, pkgs, ...}:
3
+
4
+# Bring in library functions:
5
+with lib;
6
+
7
+let
8
+  cfg = config.phoebe.services.postgresql;
9
+  superuser = config.services.postgresql.superUser;
10
+  create-user = import ./create-user.nix { inherit config lib pkgs; };
11
+  afterservices = concatMap (a: a.afterServices) (attrValues cfg.accounts);
12
+
13
+  # Per-account options:
14
+  account = { name, ... }: {
15
+
16
+    #### Interface:
17
+    options = {
18
+      user = mkOption {
19
+        type = types.str;
20
+        default = null;
21
+        example = "jdoe";
22
+        description = "The name of the account (username).";
23
+      };
24
+
25
+      passwordFile = mkOption {
26
+        type = types.path;
27
+        default = null;
28
+        example = "/run/keys/pgpass.txt";
29
+        description = ''
30
+          A file containing the password of this database user.
31
+          You'll want to use something like NixOps to get the password
32
+          file onto the target machine.
33
+        '';
34
+      };
35
+
36
+      afterServices = mkOption {
37
+        type = types.listOf types.str;
38
+        default = [ ];
39
+        example = [ "dbpassword.service" ];
40
+        description = ''
41
+          A list of services that need to run before this user account
42
+          can be created.  This is really useful if you are using
43
+          NixOps to deploy the password file and want to wait for the
44
+          key to appear in /run/keys.
45
+        '';
46
+      };
47
+
48
+      database = mkOption {
49
+        type = types.str;
50
+        default = null;
51
+        example = "jdoe";
52
+        description = ''
53
+          The name of the database this user can access.  Defaults to
54
+          the account name.
55
+        '';
56
+      };
57
+
58
+      extensions = mkOption {
59
+        type = types.listOf types.str;
60
+        default = [ ];
61
+        example = [ "pg_trgm" ];
62
+        description = "A list of extension modules to enable for the database.";
63
+      };
64
+
65
+      netmask = mkOption {
66
+        type = types.nullOr types.str;
67
+        default = null;
68
+        example = "127.0.0.1/32";
69
+        description = ''
70
+          IP netmask of remote machines allowed to connect.  Leaving
71
+          this at it's default value means this account can only
72
+          connect through Unix domain sockets.
73
+        '';
74
+      };
75
+    };
76
+
77
+    #### Implementation:
78
+    config = {
79
+      user = mkDefault name;
80
+      database = mkDefault name;
81
+    };
82
+  };
83
+
84
+  # Create HBA authentication entries:
85
+  accountToHBA = account:
86
+    ''
87
+      local ${account.database} ${account.user}              md5
88
+      host  ${account.database} ${account.user} 127.0.0.1/32 md5
89
+      host  ${account.database} ${account.user} ::1/28       md5
90
+    '' + optionalString (account.netmask != null) ''
91
+      host  ${account.database} ${account.user} ${account.netmask} md5
92
+    '';
93
+
94
+  # Commands to run to create accounts/databases:
95
+  createScript = account:
96
+    ''
97
+      ${create-user}/bin/create-user.sh \
98
+        -u "${account.user}" \
99
+        -d "${account.database}" \
100
+        -p "${account.passwordFile}" \
101
+        -e "${concatStringsSep " " account.extensions}"
102
+    '';
103
+
104
+in
105
+{
106
+  #### Interface
107
+  options.phoebe.services.postgresql = {
108
+    enable = mkEnableOption "PostgreSQL";
109
+
110
+    accounts = mkOption {
111
+      type = types.attrsOf (types.submodule account);
112
+      default = { };
113
+      description = "Additional user accounts";
114
+    };
115
+  };
116
+
117
+  #### Implementation
118
+  config = mkIf cfg.enable {
119
+
120
+    # Set up PosgreSQL:
121
+    services.postgresql = {
122
+      enable = true;
123
+      enableTCPIP = true;
124
+      package = pkgs.postgresql;
125
+
126
+      # The superuser can access all databases locally, remote access
127
+      # for some users.
128
+      authentication = mkForce (
129
+        "local all ${superuser} peer\n" +
130
+        "host  all ${superuser} 127.0.0.1/32 ident\n" +
131
+        "host  all ${superuser} ::1/128      ident\n" +
132
+        concatMapStringsSep "\n" accountToHBA (attrValues cfg.accounts));
133
+    };
134
+
135
+    # Create missing accounts:
136
+    systemd.services.pg-accounts = mkIf (length (attrValues cfg.accounts) > 0) {
137
+      description = "PostgreSQL Account Manager";
138
+      path = [ pkgs.gawk config.services.postgresql.package ];
139
+      script = (concatMapStringsSep "\n" createScript (attrValues cfg.accounts));
140
+      wantedBy = [ "postgresql.service" ];
141
+      after = [ "postgresql.service" ] ++ afterservices;
142
+      wants = afterservices;
143
+    };
144
+  };
145
+}

+ 8
- 0
modules/services/default.nix View File

@@ -0,0 +1,8 @@
1
+{ config, lib, pkgs, ...}:
2
+
3
+{
4
+  imports = [
5
+    ./databases
6
+    ./web
7
+  ];
8
+}

+ 7
- 0
modules/services/web/default.nix View File

@@ -0,0 +1,7 @@
1
+{ config, lib, pkgs, ...}:
2
+
3
+{
4
+  imports = [
5
+    ./rails
6
+  ];
7
+}

+ 10
- 0
modules/services/web/rails/database.yml View File

@@ -0,0 +1,10 @@
1
+<%= ENV['RAILS_ENV'] %>:
2
+  adapter: postgresql
3
+  encoding: unicode
4
+  pool: 5
5
+  timeout: 5000
6
+  host: <%= ENV['DATABASE_HOST'] %>
7
+  port: <%= ENV['DATABASE_PORT'] %>
8
+  database: <%= ENV['DATABASE_NAME'] %>
9
+  username: <%= ENV['DATABASE_USER'] %>
10
+  password: '<%= File.read(ENV['DATABASE_PASSWORD_FILE']).chomp %>'

+ 58
- 0
modules/services/web/rails/db-migrate.sh View File

@@ -0,0 +1,58 @@
1
+#!/bin/bash
2
+
3
+################################################################################
4
+# Migrate a Ruby on Rails database to its latest version (which might
5
+# mean going back in time for a rollback).
6
+set -e
7
+set -u
8
+
9
+################################################################################
10
+option_env=${RAILS_ENV:-production}
11
+option_root=$(pwd)
12
+
13
+################################################################################
14
+usage () {
15
+cat <<EOF
16
+Usage: db-migrate.sh [options]
17
+
18
+  -e NAME Set RAILS_ENV to NAME
19
+  -h      This message
20
+  -r DIR  The root directory of the Rails app
21
+EOF
22
+}
23
+
24
+################################################################################
25
+while getopts "he:r:" o; do
26
+  case "${o}" in
27
+    e) option_env=$OPTARG
28
+       ;;
29
+
30
+    h) usage
31
+       exit
32
+       ;;
33
+
34
+    r) option_root=$OPTARG
35
+       ;;
36
+
37
+    *) exit 1
38
+       ;;
39
+  esac
40
+done
41
+
42
+shift $((OPTIND-1))
43
+
44
+################################################################################
45
+cd "$option_root"
46
+export RAILS_ENV=$option_env
47
+
48
+################################################################################
49
+# If this is a new database, load the schema file:
50
+if [ ! -e config/database-loaded.flag ]; then
51
+  rake db:schema:load
52
+  touch config/database-loaded.flag
53
+fi
54
+
55
+################################################################################
56
+# Migrate to the most recent migration version:
57
+latest=$(find db/migrate -type f -exec basename '{}' ';' | sort | tail -n 1 | cut -d_ -f1)
58
+rake db:migrate VERSION="$latest"

+ 159
- 0
modules/services/web/rails/default.nix View File

@@ -0,0 +1,159 @@
1
+# Configure Ruby on Rails applications:
2
+{ config, lib, pkgs, ...}:
3
+
4
+# Bring in library functions:
5
+with lib;
6
+
7
+let
8
+  ##############################################################################
9
+  # Save some typing.
10
+  cfg = config.phoebe.services.rails;
11
+  scripts = import ./scripts.nix { inherit lib pkgs; };
12
+  options = import ./options.nix { inherit config lib pkgs; };
13
+
14
+  ##############################################################################
15
+  # Is PostgreSQL local?
16
+  localpg = config.phoebe.services.postgresql.enable;
17
+
18
+  ##############################################################################
19
+  # Packages to put in the application's PATH.  FIXME:
20
+  # propagatedBuildInputs won't always be set.
21
+  appPath = app: [ app.package.rubyEnv ] ++ app.package.propagatedBuildInputs;
22
+
23
+  ##############################################################################
24
+  # Collect all apps into a single set using the given function:
25
+  collectApps = f: foldr (a: b: recursiveUpdate b (f a)) {} (attrValues cfg.apps);
26
+
27
+  ##############################################################################
28
+  # Generate an NGINX configuration for an application:
29
+  appToVirtualHost = app: {
30
+    "${app.domain}" = {
31
+      forceSSL = config.phoebe.security.enable;
32
+      enableACME = config.phoebe.security.enable;
33
+      root = "${app.package}/share/${app.name}/public";
34
+
35
+      locations = {
36
+        "/assets/" = {
37
+          extraConfig = ''
38
+            gzip_static on;
39
+            expires 1M;
40
+            add_header Cache-Control public;
41
+          '';
42
+        };
43
+
44
+        "/" = {
45
+          tryFiles = "$uri @app";
46
+        };
47
+
48
+        "@app" = {
49
+          proxyPass = "http://localhost:${toString app.port}";
50
+        };
51
+      };
52
+    };
53
+  };
54
+
55
+  ##############################################################################
56
+  # Generate a systemd service for a Ruby on Rails application:
57
+  appService = app: {
58
+    "rails-${app.name}" = {
59
+      description = "${app.name} (Ruby on Rails)";
60
+      path = appPath app;
61
+
62
+      environment = {
63
+        HOME = "${app.home}/home";
64
+        RAILS_ENV = app.railsEnv;
65
+        DATABASE_HOST = app.database.host;
66
+        DATABASE_PORT = toString app.database.port;
67
+        DATABASE_NAME = app.database.name;
68
+        DATABASE_USER = app.database.user;
69
+        DATABASE_PASSWORD_FILE = "${app.home}/config/database.password";
70
+      } // app.environment;
71
+
72
+      wantedBy = [ "multi-user.target" ];
73
+      wants = optional (app.database.passwordService != null) app.database.passwordService;
74
+      after = [ "network.target" ] ++
75
+        optional localpg  "postgresql.service" ++
76
+        optional (app.database.passwordService != null) app.database.passwordService;
77
+
78
+      preStart = ''
79
+        # Prepare the config directory:
80
+        rm -rf ${app.home}/config
81
+        mkdir -p ${app.home}/{config,log,tmp,db}
82
+        cp -rf ${app.package}/share/${app.name}/config.dist/* ${app.home}/config/
83
+        cp ${app.package}/share/${app.name}/db/schema.rb.dist ${app.home}/db/schema.rb
84
+        cp ${./database.yml} ${app.home}/config/database.yml
85
+        cp ${app.database.passwordFile} ${app.home}/config/database.password
86
+
87
+        mkdir -p ${app.home}/home
88
+        ln -nfs ${app.package}/share/${app.name} ${app.home}/home/${app.name}
89
+
90
+        # Fix permissions:
91
+        chown -R rails-${app.name}:rails-${app.name} ${app.home}
92
+        chmod go+rx $(dirname "${app.home}")
93
+        chmod u+w ${app.home}/db/schema.rb
94
+
95
+      '' + optionalString app.database.migrate ''
96
+        # Migrate the database (use sudo so environment variables go through):
97
+        ${pkgs.sudo}/bin/sudo -u rails-${app.name} -EH \
98
+          ${scripts}/bin/db-migrate.sh -r ${app.package}/share/${app.name}
99
+      '';
100
+
101
+      serviceConfig = {
102
+        WorkingDirectory = "${app.package}/share/${app.name}";
103
+        Restart = "on-failure";
104
+        TimeoutSec = "infinity"; # FIXME: what's a reasonable amount of time?
105
+        Type = "simple";
106
+        PermissionsStartOnly = true;
107
+        User = "rails-${app.name}";
108
+        Group = "rails-${app.name}";
109
+        UMask = "0077";
110
+        ExecStart = "${app.package.rubyEnv}/bin/puma -e ${app.railsEnv} -p ${toString app.port}";
111
+      };
112
+    };
113
+  };
114
+
115
+  ##############################################################################
116
+  # Generate a user account for a Ruby on Rails application:
117
+  appUser = app: {
118
+    users."rails-${app.name}" = {
119
+      description = "${app.name} Ruby on Rails Application";
120
+      home = "${app.home}/home";
121
+      createHome = true;
122
+      group = "rails-${app.name}";
123
+      shell = "${pkgs.bash}/bin/bash";
124
+      extraGroups = [ config.services.nginx.group ];
125
+      packages = appPath app;
126
+    };
127
+    groups."rails-${app.name}" = {};
128
+  };
129
+
130
+in
131
+{
132
+  #### Interface
133
+  options.phoebe.services.rails = {
134
+    apps = mkOption {
135
+      type = types.attrsOf (types.submodule options.application);
136
+      default = { };
137
+      description = "Rails applications to configure.";
138
+    };
139
+  };
140
+
141
+  #### Implementation
142
+  config = mkIf (length (attrValues cfg.apps) != 0) {
143
+    # Use NGINX to proxy requests to the apps:
144
+    services.nginx = {
145
+      enable = true;
146
+      recommendedTlsSettings   = config.phoebe.security.enable;
147
+      recommendedOptimisation  = true;
148
+      recommendedGzipSettings  = true;
149
+      recommendedProxySettings = true;
150
+      virtualHosts = collectApps appToVirtualHost;
151
+    };
152
+
153
+    # Each application gets a user account:
154
+    users = collectApps appUser;
155
+
156
+    # Each application gets a systemd service to keep it running.
157
+    systemd.services = collectApps appService;
158
+  };
159
+}

+ 7
- 0
modules/services/web/rails/functions.nix View File

@@ -0,0 +1,7 @@
1
+rec {
2
+  # The default base directory for Rails applications:
3
+  base = "/var/lib/rails";
4
+
5
+  # Where a Rails application lives:
6
+  home = name: "${base}/${name}";
7
+}

+ 52
- 0
modules/services/web/rails/helpers.nix View File

@@ -0,0 +1,52 @@
1
+# Helper package Ruby on Rails applications that work with the rails
2
+# service in this directory.
3
+{ pkgs, ... }:
4
+
5
+let
6
+  functions = import ./functions.nix;
7
+
8
+  mkRailsDerivation =
9
+    { name
10
+    , env # The output of bundlerEnv
11
+    , extraPackages ? [ ]
12
+    , buildPhase ? ""
13
+    , installPhase ? ""
14
+    , buildInputs ? [ ]
15
+    , propagatedBuildInputs ? [ ]
16
+    , ...
17
+    }@args:
18
+      pkgs.stdenv.mkDerivation (args // {
19
+        buildInputs = [ env env.wrappedRuby ] ++ buildInputs;
20
+        propagatedBuildInputs = extraPackages ++ propagatedBuildInputs;
21
+        passthru = { rubyEnv = env; ruby = env.wrappedRuby; };
22
+
23
+        buildPhase = ''
24
+          ${buildPhase}
25
+
26
+          # Build all the assets into the package:
27
+          rake assets:precompile
28
+
29
+          # Move some files out of the way since they will be created
30
+          # in production:
31
+          rm config/database.yml
32
+          mv config config.dist
33
+          mv db/schema.rb db/schema.rb.dist
34
+        '';
35
+
36
+        installPhase = ''
37
+          mkdir -p "$out/share"
38
+          ${installPhase}
39
+
40
+          cp -r . "$out/share/${name}"
41
+          rm -rf  "$out/share/${name}/log"
42
+          rm -rf  "$out/share/${name}/tmp"
43
+
44
+          # Install some links to where the app lives in production:
45
+          for f in log config tmp db/schema.rb; do
46
+            ln -sf "${functions.home name}/$f" "$out/share/${name}/$f"
47
+          done
48
+        '';
49
+      });
50
+in
51
+{ inherit mkRailsDerivation;
52
+}

+ 123
- 0
modules/services/web/rails/options.nix View File

@@ -0,0 +1,123 @@
1
+{ config, lib, pkgs, ...}:
2
+
3
+with lib;
4
+
5
+let
6
+  ##############################################################################
7
+  functions = import ./functions.nix;
8
+
9
+  ##############################################################################
10
+  # Database configuration:
11
+  database = {
12
+    options = {
13
+      name = mkOption {
14
+        type = types.str;
15
+        example = "marketing";
16
+        description = "Database name.";
17
+      };
18
+
19
+      user = mkOption {
20
+        type = types.str;
21
+        example = "jdoe";
22
+        description = "Database user name.";
23
+      };
24
+
25
+      passwordFile = mkOption {
26
+        type = types.path;
27
+        example = "/run/keys/db-password";
28
+        description = ''
29
+          A file containing the database password.  This allows you to
30
+          deploy a password with NixOps.
31
+        '';
32
+      };
33
+
34
+      passwordService = mkOption {
35
+        type = types.nullOr types.str;
36
+        default = null;
37
+        example = "db-password.service";
38
+        description = ''
39
+          A service to wait on before starting the Rails application.
40
+          This service should provide the password file for the
41
+          passwordFile option.  Useful when deploying passwords with
42
+          NixOps.
43
+        '';
44
+      };
45
+
46
+      migrate = mkOption {
47
+        type = types.bool;
48
+        default = true;
49
+        example = false;
50
+        description = "Whether or not database migrations should run on start.";
51
+      };
52
+
53
+      host = mkOption {
54
+        type = types.str;
55
+        default = "localhost";
56
+        description = "Host name for the database server.";
57
+      };
58
+
59
+      port = mkOption {
60
+        type = types.int;
61
+        default = config.services.postgresql.port;
62
+        description = "Port number for the database server";
63
+      };
64
+    };
65
+  };
66
+
67
+  ##############################################################################
68
+  # Application configuration:
69
+  application = { name, ... }: {
70
+    options = {
71
+      name = mkOption {
72
+        type = types.str;
73
+        description = "The name of the Ruby on Rails application.";
74
+      };
75
+
76
+      home = mkOption {
77
+        type = types.path;
78
+        description = "The directory where the application is deployed to.";
79
+      };
80
+
81
+      domain = mkOption {
82
+        type = types.str;
83
+        default = null;
84
+        description = "The FQDN to use for this application.";
85
+      };
86
+
87
+      port = mkOption {
88
+        type = types.int;
89
+        default = null;
90
+        description = "The port number to forward requests to.";
91
+      };
92
+
93
+      package = mkOption {
94
+        type = types.package;
95
+        description = "The derivation for the Ruby on Rails application.";
96
+      };
97
+
98
+      database = mkOption {
99
+        type = types.submodule database;
100
+        description = "Database configuration.";
101
+      };
102
+
103
+      railsEnv = mkOption {
104
+        type = types.str;
105
+        default = "production";
106
+        example = "development";
107
+        description = "What to use for RAILS_ENV.";
108
+      };
109
+
110
+      environment = mkOption {
111
+        type = types.attrs;
112
+        default = { };
113
+        description = "Environment variables.";
114
+      };
115
+    };
116
+
117
+    config = {
118
+      name = mkDefault name;
119
+      home = mkDefault (functions.home name);
120
+    };
121
+  };
122
+
123
+in { inherit database application; }

+ 19
- 0
modules/services/web/rails/scripts.nix View File

@@ -0,0 +1,19 @@
1
+{ lib, pkgs, ...}:
2
+
3
+pkgs.stdenvNoCC.mkDerivation {
4
+  name = "rails-scripts";
5
+  phases = [ "installPhase" "fixupPhase" ];
6
+
7
+  installPhase = ''
8
+    mkdir -p $out/bin
9
+    substituteAll ${./db-migrate.sh} $out/bin/db-migrate.sh
10
+    find $out/bin -type f -exec chmod 555 '{}' ';'
11
+  '';
12
+
13
+  meta = with lib; {
14
+    description = "Scripts for working with Ruby on Rails applications.";
15
+    homepage = https://git.devalot.com/pjones/phoebe/;
16
+    maintainers = with maintainers; [ pjones ];
17
+    platforms = platforms.all;
18
+  };
19
+}

Loading…
Cancel
Save