Skip to content

Commit e4d6d84

Browse files
committed
nixos/firefox-syncserver: add PostgreSQL backend support
1 parent e9b9822 commit e4d6d84

5 files changed

Lines changed: 188 additions & 57 deletions

File tree

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

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

399399
- `prosody` gained a config check option named `services.prosody.checkConfig` which runs `prosodyctl check config` and is turned on by default.
400400

401+
- [](#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.
402+
401403
- Revamp of the ACME certificate acquisition and renewal process to help scale systems with lots (100+) of certificates.
402404

403405
Units and targets have been reshaped to better support more specific dependency propagation and avoid

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: 112 additions & 28 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,7 +55,8 @@ let
4155
};
4256
};
4357
configFile = format.generate "syncstorage.toml" (lib.recursiveUpdate settings cfg.settings);
44-
setupScript = pkgs.writeShellScript "firefox-syncserver-setup" ''
58+
59+
mysqlSetupScript = pkgs.writeShellScript "firefox-syncserver-setup" ''
4560
set -euo pipefail
4661
shopt -s inherit_errexit
4762
@@ -79,6 +94,47 @@ let
7994
echo "Single-node setup failed"
8095
exit 1
8196
'';
97+
98+
postgresqlSetupScript = pkgs.writeShellScript "firefox-syncserver-setup" ''
99+
set -euo pipefail
100+
shopt -s inherit_errexit
101+
102+
schema_configured() {
103+
psql -d ${cfg.database.name} -tAc "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'services')" | grep -q t
104+
}
105+
106+
update_config() {
107+
psql -d ${cfg.database.name} <<'EOF'
108+
BEGIN;
109+
110+
INSERT INTO services (id, service, pattern)
111+
VALUES (1, 'sync-1.5', '{node}/1.5/{uid}')
112+
ON CONFLICT (id) DO UPDATE SET service = 'sync-1.5', pattern = '{node}/1.5/{uid}';
113+
INSERT INTO nodes (id, service, node, available, current_load,
114+
capacity, downed, backoff)
115+
VALUES (1, 1, '${cfg.singleNode.url}', ${toString cfg.singleNode.capacity},
116+
0, ${toString cfg.singleNode.capacity}, 0, 0)
117+
ON CONFLICT (id) DO UPDATE SET node = '${cfg.singleNode.url}', capacity = ${toString cfg.singleNode.capacity};
118+
119+
COMMIT;
120+
EOF
121+
}
122+
123+
124+
for (( try = 0; try < 60; try++ )); do
125+
if ! schema_configured; then
126+
sleep 2
127+
else
128+
update_config
129+
exit 0
130+
fi
131+
done
132+
133+
echo "Single-node setup failed"
134+
exit 1
135+
'';
136+
137+
setupScript = if dbIsMySQL then mysqlSetupScript else postgresqlSetupScript;
82138
in
83139

84140
{
@@ -88,25 +144,26 @@ in
88144
the Firefox Sync storage service.
89145
90146
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-
```
147+
one service and one nodes by inserting them into the database manually, e.g.
148+
by running the equivalent SQL for your database backend.
100149
101150
{option}`${opt.singleNode.enable}` does this automatically when enabled
102151
'';
103152

104153
package = lib.mkPackageOption pkgs "syncstorage-rs" { };
105154

155+
database.type = lib.mkOption {
156+
type = lib.types.enum [
157+
"mysql"
158+
"postgresql"
159+
];
160+
default = "mysql";
161+
description = ''
162+
Which database backend to use for storage.
163+
'';
164+
};
165+
106166
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.
110167
type = lib.types.strMatching "[a-z_][a-z0-9_]*";
111168
default = defaultDatabase;
112169
description = ''
@@ -117,9 +174,15 @@ in
117174

118175
database.user = lib.mkOption {
119176
type = lib.types.str;
120-
default = defaultUser;
177+
default = if dbIsPostgreSQL then defaultDatabase else defaultUser;
178+
defaultText = lib.literalExpression ''
179+
if database.type == "postgresql" then "${defaultDatabase}" else "${defaultUser}"
180+
'';
121181
description = ''
122-
Username for database connections.
182+
Username for database connections. When using PostgreSQL with
183+
`createLocally`, this defaults to the database name so that
184+
`ensureDBOwnership` works (it requires user and database names
185+
to match).
123186
'';
124187
};
125188

@@ -137,7 +200,8 @@ in
137200
default = true;
138201
description = ''
139202
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.
203+
This includes enabling the configured database service and setting up
204+
authentication for the configured user.
141205
'';
142206
};
143207

@@ -237,7 +301,7 @@ in
237301
};
238302

239303
config = lib.mkIf cfg.enable {
240-
services.mysql = lib.mkIf cfg.database.createLocally {
304+
services.mysql = lib.mkIf (cfg.database.createLocally && dbIsMySQL) {
241305
enable = true;
242306
ensureDatabases = [ cfg.database.name ];
243307
ensureUsers = [
@@ -250,16 +314,27 @@ in
250314
];
251315
};
252316

317+
services.postgresql = lib.mkIf (cfg.database.createLocally && dbIsPostgreSQL) {
318+
enable = true;
319+
ensureDatabases = [ cfg.database.name ];
320+
ensureUsers = [
321+
{
322+
name = cfg.database.user;
323+
ensureDBOwnership = true;
324+
}
325+
];
326+
};
327+
253328
systemd.services.firefox-syncserver = {
254329
wantedBy = [ "multi-user.target" ];
255-
requires = lib.mkIf dbIsLocal [ "mysql.service" ];
256-
after = lib.mkIf dbIsLocal [ "mysql.service" ];
330+
requires = lib.mkIf dbIsLocal [ dbService ];
331+
after = lib.mkIf dbIsLocal [ dbService ];
257332
restartTriggers = lib.optional cfg.singleNode.enable setupScript;
258333
environment.RUST_LOG = cfg.logLevel;
259334
serviceConfig = {
260-
User = defaultUser;
261-
Group = defaultUser;
262-
ExecStart = "${cfg.package}/bin/syncserver --config ${configFile}";
335+
User = cfg.database.user;
336+
Group = cfg.database.user;
337+
ExecStart = "${syncserver}/bin/syncserver --config ${configFile}";
263338
EnvironmentFile = lib.mkIf (cfg.secrets != null) "${cfg.secrets}";
264339

265340
# hardening
@@ -303,10 +378,19 @@ in
303378

304379
systemd.services.firefox-syncserver-setup = lib.mkIf cfg.singleNode.enable {
305380
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}" ];
381+
requires = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal dbService;
382+
after = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal dbService;
383+
path =
384+
if dbIsMySQL then [ config.services.mysql.package ] else [ config.services.postgresql.package ];
385+
serviceConfig = {
386+
ExecStart = [ "${setupScript}" ];
387+
}
388+
// lib.optionalAttrs dbIsPostgreSQL {
389+
# PostgreSQL peer authentication requires the system user to match the
390+
# database user. Run as the superuser so we can access all databases.
391+
User = "postgres";
392+
Group = "postgres";
393+
};
310394
};
311395

312396
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;

nixos/tests/firefox-syncserver.nix

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,54 @@
1-
{
2-
pkgs,
3-
...
4-
}:
1+
let
2+
makeFirefoxSyncserverTest =
3+
name:
4+
{
5+
backend ? name,
6+
}:
7+
import ./make-test-python.nix (
8+
{ lib, pkgs, ... }:
9+
{
10+
name = "firefox-syncserver-${name}";
511

6-
{
7-
name = "firefox-syncserver";
8-
nodes.machine = {
9-
services.mysql = {
10-
enable = true;
11-
package = pkgs.mariadb;
12-
};
12+
nodes.machine =
13+
{ pkgs, ... }:
14+
lib.mkMerge [
15+
{
16+
mysql = {
17+
services.mysql = {
18+
enable = true;
19+
package = pkgs.mariadb;
20+
};
21+
};
22+
postgresql = { };
23+
}
24+
.${backend}
1325

14-
services.firefox-syncserver = {
15-
enable = true;
16-
secrets = pkgs.writeText "secret" "this-is-a-test";
17-
singleNode = {
18-
enable = true;
19-
hostname = "firefox-syncserver.local";
20-
capacity = 1;
21-
};
22-
};
23-
};
26+
{
27+
services.firefox-syncserver = {
28+
enable = true;
29+
database.type = backend;
30+
secrets = pkgs.writeText "sync-secrets" ''
31+
SYNC_MASTER_SECRET=a-]test-secret-that-is-not-real
32+
'';
33+
singleNode = {
34+
enable = true;
35+
hostname = "firefox-syncserver.local";
36+
capacity = 1;
37+
};
38+
};
39+
}
40+
];
2441

25-
testScript = ''
26-
machine.wait_for_unit("firefox-syncserver.service")
27-
machine.wait_for_open_port(5000)
28-
29-
machine.wait_until_succeeds("curl --fail http://127.0.0.1:5000")
30-
31-
'';
42+
testScript = ''
43+
machine.wait_for_unit("multi-user.target")
44+
machine.wait_until_succeeds("systemctl is-active firefox-syncserver.service", timeout=120)
45+
machine.wait_for_open_port(5000)
46+
machine.wait_until_succeeds("curl --fail http://127.0.0.1:5000")
47+
'';
48+
}
49+
);
50+
in
51+
builtins.mapAttrs (k: v: makeFirefoxSyncserverTest k v) {
52+
mysql = { };
53+
postgresql = { };
3254
}

0 commit comments

Comments
 (0)