Skip to content

Commit 9f03b92

Browse files
committed
nixos/firefox-syncserver: add PostgreSQL backend support
1 parent c680897 commit 9f03b92

6 files changed

Lines changed: 216 additions & 93 deletions

File tree

nixos/doc/manual/redirects.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@
229229
"module-services-crab-hole-upstream-options": [
230230
"index.html#module-services-crab-hole-upstream-options"
231231
],
232+
"module-services-firefox-syncserver-database": [
233+
"index.html#module-services-firefox-syncserver-database"
234+
],
232235
"module-services-firefox-syncserver-clients": [
233236
"index.html#module-services-firefox-syncserver-clients"
234237
],

nixos/doc/manual/release-notes/rl-2605.section.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ See <https://github.com/NixOS/nixpkgs/issues/481673>.
245245

246246
- IPVLAN interfaces can now be configured through the `networking.ipvlans` option in the networking module.
247247

248+
- [](#opt-services.firefox-syncserver.database.type) has been added to allow choosing between MySQL/MariaDB (default) and PostgreSQL as the database backend for the Firefox Sync server.
249+
248250
- `services.caddy` now supports setting `httpPort` and `httpsPort` and opening them in the firewall via `openFirewall`.
249251

250252
- `boot.initrd.secrets` is now deprecated in favour of `boot.initrd.secretPaths` and `boot.initrd.extraSecretsHook`.

nixos/modules/services/networking/firefox-syncserver.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,29 @@ This configuration should never be used in production. It is not encrypted and
3232
stores its secrets in a world-readable location.
3333
:::
3434

35+
## Database backends {#module-services-firefox-syncserver-database}
36+
37+
The sync server supports MySQL/MariaDB (the default) and PostgreSQL as database
38+
backends. Set `database.type` to choose the backend:
39+
40+
```nix
41+
{
42+
services.firefox-syncserver = {
43+
enable = true;
44+
database.type = "postgresql";
45+
secrets = "/run/secrets/firefox-syncserver";
46+
singleNode = {
47+
enable = true;
48+
hostname = "localhost";
49+
url = "http://localhost:5000";
50+
};
51+
};
52+
}
53+
```
54+
55+
When `database.createLocally` is `true` (the default), the module will
56+
automatically enable and configure the corresponding database service.
57+
3558
## More detailed setup {#module-services-firefox-syncserver-configuration}
3659

3760
The `firefox-syncserver` service provides a number of options to make setting up

nixos/modules/services/networking/firefox-syncserver.nix

Lines changed: 137 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,21 @@ let
1313
defaultUser = "firefox-syncserver";
1414

1515
dbIsLocal = cfg.database.host == "localhost";
16-
dbURL = "mysql://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}${lib.optionalString dbIsLocal "?socket=/run/mysqld/mysqld.sock"}";
16+
dbIsMySQL = cfg.database.type == "mysql";
17+
dbIsPostgreSQL = cfg.database.type == "postgresql";
18+
19+
dbURL =
20+
if dbIsMySQL then
21+
"mysql://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}${lib.optionalString dbIsLocal "?socket=/run/mysqld/mysqld.sock"}"
22+
else
23+
"postgres://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}${lib.optionalString dbIsLocal "?host=/run/postgresql"}";
24+
25+
# postgresql.target waits for postgresql-setup.service (which runs
26+
# ensureDatabases / ensureUsers) to complete, avoiding race conditions
27+
# where the syncserver starts before its database and role exist.
28+
dbService = if dbIsMySQL then "mysql.service" else "postgresql.target";
29+
30+
syncserver = cfg.package.override { dbBackend = cfg.database.type; };
1731

1832
format = pkgs.formats.toml { };
1933
settings = {
@@ -22,7 +36,7 @@ let
2236
database_url = dbURL;
2337
};
2438
tokenserver = {
25-
node_type = "mysql";
39+
node_type = if dbIsMySQL then "mysql" else "postgres";
2640
database_url = dbURL;
2741
fxa_email_domain = "api.accounts.firefox.com";
2842
fxa_oauth_server_url = "https://oauth.accounts.firefox.com/v1";
@@ -41,44 +55,75 @@ let
4155
};
4256
};
4357
configFile = format.generate "syncstorage.toml" (lib.recursiveUpdate settings cfg.settings);
44-
setupScript = pkgs.writeShellScript "firefox-syncserver-setup" ''
45-
set -euo pipefail
46-
shopt -s inherit_errexit
47-
48-
schema_configured() {
49-
mysql ${cfg.database.name} -Ne 'SHOW TABLES' | grep -q services
50-
}
51-
52-
update_config() {
53-
mysql ${cfg.database.name} <<"EOF"
54-
BEGIN;
55-
56-
INSERT INTO `services` (`id`, `service`, `pattern`)
57-
VALUES (1, 'sync-1.5', '{node}/1.5/{uid}')
58-
ON DUPLICATE KEY UPDATE service='sync-1.5', pattern='{node}/1.5/{uid}';
59-
INSERT INTO `nodes` (`id`, `service`, `node`, `available`, `current_load`,
60-
`capacity`, `downed`, `backoff`)
61-
VALUES (1, 1, '${cfg.singleNode.url}', ${toString cfg.singleNode.capacity},
62-
0, ${toString cfg.singleNode.capacity}, 0, 0)
63-
ON DUPLICATE KEY UPDATE node = '${cfg.singleNode.url}', capacity=${toString cfg.singleNode.capacity};
64-
65-
COMMIT;
66-
EOF
67-
}
6858

69-
70-
for (( try = 0; try < 60; try++ )); do
71-
if ! schema_configured; then
72-
sleep 2
73-
else
74-
update_config
75-
exit 0
76-
fi
77-
done
78-
79-
echo "Single-node setup failed"
80-
exit 1
81-
'';
59+
setupScript =
60+
let
61+
dbSpecific =
62+
if dbIsMySQL then
63+
{
64+
listTables = "mysql ${cfg.database.name} -Ne 'SHOW TABLES'";
65+
execSql = "mysql ${cfg.database.name}";
66+
upsertSql = ''
67+
BEGIN;
68+
69+
INSERT INTO `services` (`id`, `service`, `pattern`)
70+
VALUES (1, 'sync-1.5', '{node}/1.5/{uid}')
71+
ON DUPLICATE KEY UPDATE service='sync-1.5', pattern='{node}/1.5/{uid}';
72+
INSERT INTO `nodes` (`id`, `service`, `node`, `available`, `current_load`,
73+
`capacity`, `downed`, `backoff`)
74+
VALUES (1, 1, '${cfg.singleNode.url}', ${toString cfg.singleNode.capacity},
75+
0, ${toString cfg.singleNode.capacity}, 0, 0)
76+
ON DUPLICATE KEY UPDATE node = '${cfg.singleNode.url}', capacity=${toString cfg.singleNode.capacity};
77+
78+
COMMIT;
79+
'';
80+
}
81+
else
82+
{
83+
listTables = "psql -d ${cfg.database.name} -tAc \"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'services')\"";
84+
execSql = "psql -d ${cfg.database.name}";
85+
upsertSql = ''
86+
BEGIN;
87+
88+
INSERT INTO services (id, service, pattern)
89+
VALUES (1, 'sync-1.5', '{node}/1.5/{uid}')
90+
ON CONFLICT (id) DO UPDATE SET service = 'sync-1.5', pattern = '{node}/1.5/{uid}';
91+
INSERT INTO nodes (id, service, node, available, current_load,
92+
capacity, downed, backoff)
93+
VALUES (1, 1, '${cfg.singleNode.url}', ${toString cfg.singleNode.capacity},
94+
0, ${toString cfg.singleNode.capacity}, 0, 0)
95+
ON CONFLICT (id) DO UPDATE SET node = '${cfg.singleNode.url}', capacity = ${toString cfg.singleNode.capacity};
96+
97+
COMMIT;
98+
'';
99+
};
100+
in
101+
pkgs.writeShellScript "firefox-syncserver-setup" ''
102+
set -euo pipefail
103+
shopt -s inherit_errexit
104+
105+
schema_configured() {
106+
${dbSpecific.listTables} | grep -q services
107+
}
108+
109+
update_config() {
110+
${dbSpecific.execSql} <<'EOF'
111+
${dbSpecific.upsertSql}
112+
EOF
113+
}
114+
115+
for (( try = 0; try < 60; try++ )); do
116+
if ! schema_configured; then
117+
sleep 2
118+
else
119+
update_config
120+
exit 0
121+
fi
122+
done
123+
124+
echo "Single-node setup failed"
125+
exit 1
126+
'';
82127
in
83128

84129
{
@@ -88,25 +133,26 @@ in
88133
the Firefox Sync storage service.
89134
90135
Out of the box this will not be very useful unless you also configure at least
91-
one service and one nodes by inserting them into the mysql database manually, e.g.
92-
by running
93-
94-
```
95-
INSERT INTO `services` (`id`, `service`, `pattern`) VALUES ('1', 'sync-1.5', '{node}/1.5/{uid}');
96-
INSERT INTO `nodes` (`id`, `service`, `node`, `available`, `current_load`,
97-
`capacity`, `downed`, `backoff`)
98-
VALUES ('1', '1', 'https://mydomain.tld', '1', '0', '10', '0', '0');
99-
```
136+
one service and one nodes by inserting them into the database manually, e.g.
137+
by running the equivalent SQL for your database backend.
100138
101139
{option}`${opt.singleNode.enable}` does this automatically when enabled
102140
'';
103141

104142
package = lib.mkPackageOption pkgs "syncstorage-rs" { };
105143

144+
database.type = lib.mkOption {
145+
type = lib.types.enum [
146+
"mysql"
147+
"postgresql"
148+
];
149+
default = "mysql";
150+
description = ''
151+
Which database backend to use for storage.
152+
'';
153+
};
154+
106155
database.name = lib.mkOption {
107-
# the mysql module does not allow `-quoting without resorting to shell
108-
# escaping, so we restrict db names for forward compaitiblity should this
109-
# behavior ever change.
110156
type = lib.types.strMatching "[a-z_][a-z0-9_]*";
111157
default = defaultDatabase;
112158
description = ''
@@ -117,9 +163,15 @@ in
117163

118164
database.user = lib.mkOption {
119165
type = lib.types.str;
120-
default = defaultUser;
166+
default = if dbIsPostgreSQL then defaultDatabase else defaultUser;
167+
defaultText = lib.literalExpression ''
168+
if database.type == "postgresql" then "${defaultDatabase}" else "${defaultUser}"
169+
'';
121170
description = ''
122-
Username for database connections.
171+
Username for database connections. When using PostgreSQL with
172+
`createLocally`, this defaults to the database name so that
173+
`ensureDBOwnership` works (it requires user and database names
174+
to match).
123175
'';
124176
};
125177

@@ -137,7 +189,8 @@ in
137189
default = true;
138190
description = ''
139191
Whether to create database and user on the local machine if they do not exist.
140-
This includes enabling unix domain socket authentication for the configured user.
192+
This includes enabling the configured database service and setting up
193+
authentication for the configured user.
141194
'';
142195
};
143196

@@ -237,7 +290,7 @@ in
237290
};
238291

239292
config = lib.mkIf cfg.enable {
240-
services.mysql = lib.mkIf cfg.database.createLocally {
293+
services.mysql = lib.mkIf (cfg.database.createLocally && dbIsMySQL) {
241294
enable = true;
242295
ensureDatabases = [ cfg.database.name ];
243296
ensureUsers = [
@@ -250,16 +303,27 @@ in
250303
];
251304
};
252305

306+
services.postgresql = lib.mkIf (cfg.database.createLocally && dbIsPostgreSQL) {
307+
enable = true;
308+
ensureDatabases = [ cfg.database.name ];
309+
ensureUsers = [
310+
{
311+
name = cfg.database.user;
312+
ensureDBOwnership = true;
313+
}
314+
];
315+
};
316+
253317
systemd.services.firefox-syncserver = {
254318
wantedBy = [ "multi-user.target" ];
255-
requires = lib.mkIf dbIsLocal [ "mysql.service" ];
256-
after = lib.mkIf dbIsLocal [ "mysql.service" ];
319+
requires = lib.mkIf dbIsLocal [ dbService ];
320+
after = lib.mkIf dbIsLocal [ dbService ];
257321
restartTriggers = lib.optional cfg.singleNode.enable setupScript;
258322
environment.RUST_LOG = cfg.logLevel;
259323
serviceConfig = {
260-
User = defaultUser;
261-
Group = defaultUser;
262-
ExecStart = "${cfg.package}/bin/syncserver --config ${configFile}";
324+
User = cfg.database.user;
325+
Group = cfg.database.user;
326+
ExecStart = "${syncserver}/bin/syncserver --config ${configFile}";
263327
EnvironmentFile = lib.mkIf (cfg.secrets != null) "${cfg.secrets}";
264328

265329
# hardening
@@ -303,10 +367,19 @@ in
303367

304368
systemd.services.firefox-syncserver-setup = lib.mkIf cfg.singleNode.enable {
305369
wantedBy = [ "firefox-syncserver.service" ];
306-
requires = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal "mysql.service";
307-
after = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal "mysql.service";
308-
path = [ config.services.mysql.package ];
309-
serviceConfig.ExecStart = [ "${setupScript}" ];
370+
requires = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal dbService;
371+
after = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal dbService;
372+
path =
373+
if dbIsMySQL then [ config.services.mysql.package ] else [ config.services.postgresql.package ];
374+
serviceConfig = {
375+
ExecStart = [ "${setupScript}" ];
376+
}
377+
// lib.optionalAttrs dbIsPostgreSQL {
378+
# PostgreSQL peer authentication requires the system user to match the
379+
# database user. Run as the superuser so we can access all databases.
380+
User = "postgres";
381+
Group = "postgres";
382+
};
310383
};
311384

312385
services.nginx.virtualHosts = lib.mkIf cfg.singleNode.enableNginx {

nixos/tests/all-tests.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,7 @@ in
584584
imports = [ ./firefox.nix ];
585585
_module.args.firefoxPackage = pkgs.firefox-esr-140;
586586
};
587-
firefox-syncserver = runTest ./firefox-syncserver.nix;
587+
firefox-syncserver = discoverTests (import ./firefox-syncserver.nix);
588588
firefox_decrypt = runTest ./firefox_decrypt.nix;
589589
firefoxpwa = runTest ./firefoxpwa.nix;
590590
firejail = runTest ./firejail.nix;

0 commit comments

Comments
 (0)