aboutsummaryrefslogtreecommitdiff
path: root/modules/apps/postgresql/default.nix
blob: f479e8624d3455c62460c72f97497fd9f53468aa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
{ config, pkgs, lib, ... }:
let

  inherit (builtins)
    match toString ;

  inherit (lib)
    concatMapStrings concatStringsSep filterAttrs foldAttrs filter foldl
    hasPrefix isBool isInt isList isString length mapAttrs' mapAttrsToList
    mkDefault mkIf mkOption nameValuePair types ;

  inherit (types)
    attrsOf lines listOf nullOr package path str submodule ;

  concatNonEmpty = sep: list: concatStringsSep sep (filter (s: s != "") list);
  explicit = filterAttrs (n: v: n != "_module" && v != null);

  instances = explicit config.nixsap.apps.postgresql;
  users = mapAttrsToList (_: v: v.user) instances;

  isFloat = x: match "^[0-9]+(\\.[0-9]+)?$" (toString x) != null;

  keyrings =
    let
      ik = mapAttrsToList (_: i: { "${i.user}" = [ i.server.ssl_key_file ]; } ) instances;
    in foldAttrs (l: r: l ++ r) [] ik;

  mkService = name: opts:
    let
      inherit (opts) user initdb;
      inherit (opts.server) data_directory port hba_file ident_file;
      ident_file_path = pkgs.writeText "${name}-ident_file" ''
                        postgres ${user} postgres
                        ${ident_file}
                      '';
      hba_file_path = pkgs.writeText "${name}-hba_file" ''
                        local all postgres peer map=postgres
                        ${hba_file}
                      '';
      show = n: v: if isBool v then (if v then "yes" else "no")
           else if n == "ident_file" then "'${ident_file_path}'"
           else if n == "hba_file" then "'${hba_file_path}'"
           else if isFloat v then toString v
           else if isString v then "'${v}'"
           else if isList v then "'${concatStringsSep "," v}'"
           else toString v;
      conf = pkgs.writeText "pgsql-${name}.conf" (
        concatStringsSep "\n" (mapAttrsToList (n: v: "${n} = ${show n v}") (explicit opts.server))
      );

      preStart = ''
        mkdir -v -p '${data_directory}'
        chown -R '${user}:${user}' '${data_directory}'
        chmod -R u=rwX,g=,o= '${data_directory}'
      '';

      main = pkgs.writeBashScriptBin "pgsql-${name}" ''
        set -euo pipefail
        if [ ! -f '${data_directory}/PG_VERSION' ]; then
          ${initdb} '${data_directory}'
          rm -f '${data_directory}/'*hba.conf
          rm -f '${data_directory}/'*ident.conf
          rm -f '${data_directory}/postgresql.conf'
        fi
        exec '${opts.package}/bin/postgres' -c 'config_file=${conf}'
      '';

      psql = "${opts.package}/bin/psql -v ON_ERROR_STOP=1 -p${toString port} -U postgres";

      configure =
        let
          create = pkgs.writeText "pgsql-${name}-create.sql" ''
            ${concatMapStrings (r: ''
              SELECT create_role_if_not_exists('${r}');
            '') opts.roles}
            ${concatMapStrings (d: ''
              SELECT create_db_if_not_exists('${d}');
            '') opts.databases}
          '';
        in pkgs.writeBashScriptBin "pgsql-${name}-conf" ''
          set -euo pipefail
          while ! ${psql} -c ';'; do
            sleep 5s
          done
          ${psql} -f ${./functions.pgsql}
          ${psql} -f ${create}
          ${psql} -f ${pkgs.writeText "pgsql-${name}.sql" opts.configure}
        '';

      needConf = (opts.configure != "") || (opts.roles != []) || (opts.databases != []);

    in {
      "pgsql-${name}" = {
        wantedBy = [ "multi-user.target" ];
        wants = [ "keys.target" ];
        after = [ "keys.target" "network.target" "local-fs.target" ];
        inherit preStart;
        serviceConfig = {
          ExecStart = "${main}/bin/pgsql-${name}";
          KillMode = "mixed";
          KillSignal = "SIGINT";
          PermissionsStartOnly = true;
          TimeoutSec = 0;
          User = user;
        };
      };
      "pgsql-${name}-conf" = mkIf needConf {
        wantedBy = [ "multi-user.target" ];
        after = [ "pgsql-${name}.service" ];
        requires = [ "pgsql-${name}.service" ];
        serviceConfig = {
          ExecStart = "${configure}/bin/pgsql-${name}-conf";
          RemainAfterExit = true;
          Type = "oneshot";
          User = user;
        };
      };
    };

  instance = submodule ( { config, name, ... }: {
    options = {
      user = mkOption {
        description = "User to run as. Default is instance name";
        type = str;
        default = "pgsql-${name}";
      };
      roles = mkOption {
        description = ''
          List of roles to be created. These roles will be created if do
          not exist.  That's it. You will have to ALTER these roles and GRANT
          privileges using the `configure` option. Note that if you remove
          roles from this list, they will NOT be deleted from the database.
          You do not need this if this instance is a replica.
        '';
        type = listOf str;
        default = [];
      };
      databases = mkOption {
        description = ''
          List of databases to be created. These databases will be created
          if do not exist.  You do not need this if this instance is a replica.
        '';
        type = listOf str;
        default = [];
      };
      configure = mkOption {
        description = ''
          SQL statements to be executed. This should be idempotent.
          May include creation of roles and databases, granting privileges.
          Usage of PL/pgSQL is hightly encouraged.
          You do not need this if this instance is a replica.
          '';
        type = lines;
        default = "";
        example = ''
          SELECT create_role_if_not_exists('sproxy');
          ALTER ROLE sproxy RESET ALL;
          ALTER ROLE sproxy LOGIN;
          SELECT create_db_if_not_exists('sproxy');
          ALTER DATABASE sproxy OWNER TO sproxy;
        '';
      };
      package = mkOption {
        description = "PostgreSQL package";
        type = package;
        default = pkgs.postgresql;
      };
      server = mkOption {
        description = "PostgreSQL server configuration";
        type = submodule (import ./server.nix);
      };
      initdb = mkOption {
          description = ''
            Specifies the command to initialize data directory.
            This command will be executed after the data directory is created.
            The path to the data directory will be appended to this command.
            '';
          default = "${config.package}/bin/initdb -U postgres";
          example = "\${pkgs.postgresql94}/bin/pg_basebackup ... -R -D";
          type = path;
      };
    };
    config = {
      server = {
        data_directory = mkDefault "/postgresql/${name}";
        syslog_ident = mkDefault "pgsql-${name}";
      };
    };
  });

in {
  options.nixsap.apps.postgresql = mkOption {
    description = "Instances of PostgreSQL.";
    type = attrsOf instance;
    default = {};
  };

  config = {
    nixsap.deployment.keyrings = keyrings;
    systemd.services = foldl (a: b: a//b) {} (mapAttrsToList mkService instances);
    nixsap.system.users.daemons = users;
  };
}