Skip to content

Commit 3533b67

Browse files
committed
nixos/firefox-syncserver: add PostgreSQL backend support
1 parent 40063ae commit 3533b67

6 files changed

Lines changed: 235 additions & 91 deletions

File tree

nixos/doc/manual/redirects.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,9 @@
337337
"module-services-crab-hole-upstream-options": [
338338
"index.html#module-services-crab-hole-upstream-options"
339339
],
340+
"module-services-firefox-syncserver-database": [
341+
"index.html#module-services-firefox-syncserver-database"
342+
],
340343
"module-services-firefox-syncserver-clients": [
341344
"index.html#module-services-firefox-syncserver-clients"
342345
],

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
6969

7070
- `services.plantuml-server.packages.jetty` now supports `jetty_12`, it no longer supports `jetty_11`.
7171

72+
- [firefox-syncserver.database.type](#opt-services.firefox-syncserver.database.type) no longer defaults to `"mysql"`. You must now explicitly choose between `"mysql"` and `"postgresql"`. New deployments should prefer PostgreSQL.
73+
7274
## Other Notable Changes {#sec-release-26.11-notable-changes}
7375

7476
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->

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: 156 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,36 @@
33
pkgs,
44
lib,
55
options,
6+
utils,
67
...
78
}:
8-
99
let
1010
cfg = config.services.firefox-syncserver;
1111
opt = options.services.firefox-syncserver;
1212
defaultDatabase = "firefox_syncserver";
1313
defaultUser = "firefox-syncserver";
1414

15-
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"}";
15+
dbIsMySQL = cfg.database.type == "mysql";
16+
dbIsPostgreSQL = cfg.database.type == "postgresql";
17+
18+
dbIsLocal =
19+
cfg.database.host == (if dbIsMySQL then "/run/mysqld/mysqld.sock" else "/run/postgresql");
20+
21+
dbURL =
22+
if dbIsMySQL then
23+
"mysql://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}${lib.optionalString dbIsLocal "?socket=/run/mysqld/mysqld.sock"}"
24+
else if dbIsLocal then
25+
# Use Unix socket connection with peer authentication by default.
26+
"postgres:///${cfg.database.name}?host=/run/postgresql&user=${cfg.database.user}"
27+
else
28+
"postgres://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}";
29+
30+
# postgresql.target waits for postgresql-setup.service (which runs
31+
# ensureDatabases / ensureUsers) to complete, avoiding race conditions
32+
# where the syncserver starts before its database and role exist.
33+
dbService = if dbIsMySQL then "mysql.service" else "postgresql.target";
34+
35+
syncserver = cfg.package.override { dbBackend = cfg.database.type; };
1736

1837
format = pkgs.formats.toml { };
1938
settings = {
@@ -22,7 +41,7 @@ let
2241
database_url = dbURL;
2342
};
2443
tokenserver = {
25-
node_type = "mysql";
44+
node_type = if dbIsMySQL then "mysql" else "postgres";
2645
database_url = dbURL;
2746
fxa_email_domain = "api.accounts.firefox.com";
2847
fxa_oauth_server_url = "https://oauth.accounts.firefox.com/v1";
@@ -41,44 +60,75 @@ let
4160
};
4261
};
4362
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-
}
68-
6963

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

84134
{
@@ -88,25 +138,36 @@ in
88138
the Firefox Sync storage service.
89139
90140
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.
141+
one service and one nodes by inserting them into the database manually, e.g.
92142
by running
93143
94144
```
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');
145+
INSERT INTO services (id, service, pattern) VALUES (1, 'sync-1.5', '{node}/1.5/{uid}');
146+
INSERT INTO nodes (id, service, node, available, current_load,
147+
capacity, downed, backoff)
148+
VALUES (1, 1, 'https://mydomain.tld', 1, 0, 10, 0, 0);
99149
```
100150
101151
{option}`${opt.singleNode.enable}` does this automatically when enabled
102152
'';
103153

104154
package = lib.mkPackageOption pkgs "syncstorage-rs" { };
105155

156+
database.type = lib.mkOption {
157+
type = lib.types.enum [
158+
"mysql"
159+
"postgresql"
160+
];
161+
description = ''
162+
Which database backend to use for storage.
163+
::: {.note}
164+
`"postgresql"` is recommended for new deployments. `"mysql"` is the
165+
backend used by legacy installations.
166+
:::
167+
'';
168+
};
169+
106170
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.
110171
type = lib.types.strMatching "[a-z_][a-z0-9_]*";
111172
default = defaultDatabase;
112173
description = ''
@@ -117,9 +178,15 @@ in
117178

118179
database.user = lib.mkOption {
119180
type = lib.types.str;
120-
default = defaultUser;
181+
default = if dbIsPostgreSQL then defaultDatabase else defaultUser;
182+
defaultText = lib.literalExpression ''
183+
if database.type == "postgresql" then "${defaultDatabase}" else "${defaultUser}"
184+
'';
121185
description = ''
122-
Username for database connections.
186+
Username for database connections. When using PostgreSQL with
187+
`createLocally`, this defaults to the database name so that
188+
`ensureDBOwnership` works (it requires user and database names
189+
to match).
123190
'';
124191
};
125192

@@ -137,7 +204,8 @@ in
137204
default = true;
138205
description = ''
139206
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.
207+
This includes enabling the configured database service and setting up
208+
authentication for the configured user.
141209
'';
142210
};
143211

@@ -237,7 +305,7 @@ in
237305
};
238306

239307
config = lib.mkIf cfg.enable {
240-
services.mysql = lib.mkIf cfg.database.createLocally {
308+
services.mysql = lib.mkIf (cfg.database.createLocally && dbIsMySQL) {
241309
enable = true;
242310
ensureDatabases = [ cfg.database.name ];
243311
ensureUsers = [
@@ -250,16 +318,31 @@ in
250318
];
251319
};
252320

321+
services.postgresql = lib.mkIf (cfg.database.createLocally && dbIsPostgreSQL) {
322+
enable = true;
323+
ensureDatabases = [ cfg.database.name ];
324+
ensureUsers = [
325+
{
326+
name = cfg.database.user;
327+
ensureDBOwnership = true;
328+
}
329+
];
330+
};
331+
253332
systemd.services.firefox-syncserver = {
254333
wantedBy = [ "multi-user.target" ];
255-
requires = lib.mkIf dbIsLocal [ "mysql.service" ];
256-
after = lib.mkIf dbIsLocal [ "mysql.service" ];
334+
requires = lib.mkIf dbIsLocal [ dbService ];
335+
after = lib.mkIf dbIsLocal [ dbService ];
257336
restartTriggers = lib.optional cfg.singleNode.enable setupScript;
258337
environment.RUST_LOG = cfg.logLevel;
259338
serviceConfig = {
260-
User = defaultUser;
261-
Group = defaultUser;
262-
ExecStart = "${cfg.package}/bin/syncserver --config ${configFile}";
339+
ExecStart = utils.escapeSystemdExecArgs [
340+
(lib.getExe syncserver)
341+
"--config"
342+
configFile
343+
];
344+
User = cfg.database.user;
345+
Group = cfg.database.user;
263346
EnvironmentFile = lib.mkIf (cfg.secrets != null) "${cfg.secrets}";
264347

265348
# hardening
@@ -303,10 +386,21 @@ in
303386

304387
systemd.services.firefox-syncserver-setup = lib.mkIf cfg.singleNode.enable {
305388
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}" ];
389+
requires = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal dbService;
390+
after = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal dbService;
391+
path =
392+
if dbIsMySQL then [ config.services.mysql.package ] else [ config.services.postgresql.package ];
393+
serviceConfig = {
394+
ExecStart = [ "${setupScript}" ];
395+
}
396+
// lib.optionalAttrs dbIsPostgreSQL {
397+
# Peer authentication maps the system user to the database role of the
398+
# same name, which owns the database (ensureDBOwnership), so no
399+
# superuser access is required.
400+
User = cfg.database.user;
401+
Group = cfg.database.user;
402+
DynamicUser = true;
403+
};
310404
};
311405

312406
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
@@ -602,7 +602,7 @@ in
602602
imports = [ ./firefox.nix ];
603603
_module.args.firefoxPackage = pkgs.firefox-esr-140;
604604
};
605-
firefox-syncserver = runTest ./firefox-syncserver.nix;
605+
firefox-syncserver = discoverTests (import ./firefox-syncserver.nix);
606606
firefox_decrypt = runTest ./firefox_decrypt.nix;
607607
firefoxpwa = runTest ./firefoxpwa.nix;
608608
firejail = runTest ./firejail.nix;

0 commit comments

Comments
 (0)