From ab57092a60123e361cf0de1c1a314a9888c45219 Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Sat, 21 Mar 2026 09:24:39 -0400 Subject: [PATCH] temp --- .../services/networking/firefox-syncserver.md | 23 +++ .../networking/firefox-syncserver.nix | 140 ++++++++++++++---- pkgs/by-name/sy/syncstorage-rs/package.nix | 49 ++++-- 3 files changed, 174 insertions(+), 38 deletions(-) diff --git a/nixos/modules/services/networking/firefox-syncserver.md b/nixos/modules/services/networking/firefox-syncserver.md index 991e97f799d6..3bc45cfa5640 100644 --- a/nixos/modules/services/networking/firefox-syncserver.md +++ b/nixos/modules/services/networking/firefox-syncserver.md @@ -32,6 +32,29 @@ This configuration should never be used in production. It is not encrypted and stores its secrets in a world-readable location. ::: +## Database backends {#module-services-firefox-syncserver-database} + +The sync server supports MySQL/MariaDB (the default) and PostgreSQL as database +backends. Set `database.type` to choose the backend: + +```nix +{ + services.firefox-syncserver = { + enable = true; + database.type = "postgresql"; + secrets = "/run/secrets/firefox-syncserver"; + singleNode = { + enable = true; + hostname = "localhost"; + url = "http://localhost:5000"; + }; + }; +} +``` + +When `database.createLocally` is `true` (the default), the module will +automatically enable and configure the corresponding database service. + ## More detailed setup {#module-services-firefox-syncserver-configuration} The `firefox-syncserver` service provides a number of options to make setting up diff --git a/nixos/modules/services/networking/firefox-syncserver.nix b/nixos/modules/services/networking/firefox-syncserver.nix index 6a50e49fc096..70a56314e323 100644 --- a/nixos/modules/services/networking/firefox-syncserver.nix +++ b/nixos/modules/services/networking/firefox-syncserver.nix @@ -13,7 +13,21 @@ let defaultUser = "firefox-syncserver"; dbIsLocal = cfg.database.host == "localhost"; - dbURL = "mysql://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}${lib.optionalString dbIsLocal "?socket=/run/mysqld/mysqld.sock"}"; + dbIsMySQL = cfg.database.type == "mysql"; + dbIsPostgreSQL = cfg.database.type == "postgresql"; + + dbURL = + if dbIsMySQL then + "mysql://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}${lib.optionalString dbIsLocal "?socket=/run/mysqld/mysqld.sock"}" + else + "postgres://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}${lib.optionalString dbIsLocal "?host=/run/postgresql"}"; + + # postgresql.target waits for postgresql-setup.service (which runs + # ensureDatabases / ensureUsers) to complete, avoiding race conditions + # where the syncserver starts before its database and role exist. + dbService = if dbIsMySQL then "mysql.service" else "postgresql.target"; + + syncserver = cfg.package.override { dbBackend = cfg.database.type; }; format = pkgs.formats.toml { }; settings = { @@ -22,7 +36,7 @@ let database_url = dbURL; }; tokenserver = { - node_type = "mysql"; + node_type = if dbIsMySQL then "mysql" else "postgres"; database_url = dbURL; fxa_email_domain = "api.accounts.firefox.com"; fxa_oauth_server_url = "https://oauth.accounts.firefox.com/v1"; @@ -41,7 +55,8 @@ let }; }; configFile = format.generate "syncstorage.toml" (lib.recursiveUpdate settings cfg.settings); - setupScript = pkgs.writeShellScript "firefox-syncserver-setup" '' + + mysqlSetupScript = pkgs.writeShellScript "firefox-syncserver-setup" '' set -euo pipefail shopt -s inherit_errexit @@ -79,6 +94,47 @@ let echo "Single-node setup failed" exit 1 ''; + + postgresqlSetupScript = pkgs.writeShellScript "firefox-syncserver-setup" '' + set -euo pipefail + shopt -s inherit_errexit + + schema_configured() { + psql -d ${cfg.database.name} -tAc "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'services')" | grep -q t + } + + update_config() { + psql -d ${cfg.database.name} <<'EOF' + BEGIN; + + INSERT INTO services (id, service, pattern) + VALUES (1, 'sync-1.5', '{node}/1.5/{uid}') + ON CONFLICT (id) DO UPDATE SET service = 'sync-1.5', pattern = '{node}/1.5/{uid}'; + INSERT INTO nodes (id, service, node, available, current_load, + capacity, downed, backoff) + VALUES (1, 1, '${cfg.singleNode.url}', ${toString cfg.singleNode.capacity}, + 0, ${toString cfg.singleNode.capacity}, 0, 0) + ON CONFLICT (id) DO UPDATE SET node = '${cfg.singleNode.url}', capacity = ${toString cfg.singleNode.capacity}; + + COMMIT; + EOF + } + + + for (( try = 0; try < 60; try++ )); do + if ! schema_configured; then + sleep 2 + else + update_config + exit 0 + fi + done + + echo "Single-node setup failed" + exit 1 + ''; + + setupScript = if dbIsMySQL then mysqlSetupScript else postgresqlSetupScript; in { @@ -88,25 +144,26 @@ in the Firefox Sync storage service. Out of the box this will not be very useful unless you also configure at least - one service and one nodes by inserting them into the mysql database manually, e.g. - by running - - ``` - INSERT INTO `services` (`id`, `service`, `pattern`) VALUES ('1', 'sync-1.5', '{node}/1.5/{uid}'); - INSERT INTO `nodes` (`id`, `service`, `node`, `available`, `current_load`, - `capacity`, `downed`, `backoff`) - VALUES ('1', '1', 'https://mydomain.tld', '1', '0', '10', '0', '0'); - ``` + one service and one nodes by inserting them into the database manually, e.g. + by running the equivalent SQL for your database backend. {option}`${opt.singleNode.enable}` does this automatically when enabled ''; package = lib.mkPackageOption pkgs "syncstorage-rs" { }; + database.type = lib.mkOption { + type = lib.types.enum [ + "mysql" + "postgresql" + ]; + default = "mysql"; + description = '' + Which database backend to use for storage. + ''; + }; + database.name = lib.mkOption { - # the mysql module does not allow `-quoting without resorting to shell - # escaping, so we restrict db names for forward compaitiblity should this - # behavior ever change. type = lib.types.strMatching "[a-z_][a-z0-9_]*"; default = defaultDatabase; description = '' @@ -117,9 +174,15 @@ in database.user = lib.mkOption { type = lib.types.str; - default = defaultUser; + default = if dbIsPostgreSQL then defaultDatabase else defaultUser; + defaultText = lib.literalExpression '' + if database.type == "postgresql" then "${defaultDatabase}" else "${defaultUser}" + ''; description = '' - Username for database connections. + Username for database connections. When using PostgreSQL with + `createLocally`, this defaults to the database name so that + `ensureDBOwnership` works (it requires user and database names + to match). ''; }; @@ -137,7 +200,8 @@ in default = true; description = '' Whether to create database and user on the local machine if they do not exist. - This includes enabling unix domain socket authentication for the configured user. + This includes enabling the configured database service and setting up + authentication for the configured user. ''; }; @@ -237,7 +301,7 @@ in }; config = lib.mkIf cfg.enable { - services.mysql = lib.mkIf cfg.database.createLocally { + services.mysql = lib.mkIf (cfg.database.createLocally && dbIsMySQL) { enable = true; ensureDatabases = [ cfg.database.name ]; ensureUsers = [ @@ -250,16 +314,27 @@ in ]; }; + services.postgresql = lib.mkIf (cfg.database.createLocally && dbIsPostgreSQL) { + enable = true; + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [ + { + name = cfg.database.user; + ensureDBOwnership = true; + } + ]; + }; + systemd.services.firefox-syncserver = { wantedBy = [ "multi-user.target" ]; - requires = lib.mkIf dbIsLocal [ "mysql.service" ]; - after = lib.mkIf dbIsLocal [ "mysql.service" ]; + requires = lib.mkIf dbIsLocal [ dbService ]; + after = lib.mkIf dbIsLocal [ dbService ]; restartTriggers = lib.optional cfg.singleNode.enable setupScript; environment.RUST_LOG = cfg.logLevel; serviceConfig = { - User = defaultUser; - Group = defaultUser; - ExecStart = "${cfg.package}/bin/syncserver --config ${configFile}"; + User = cfg.database.user; + Group = cfg.database.user; + ExecStart = "${syncserver}/bin/syncserver --config ${configFile}"; EnvironmentFile = lib.mkIf (cfg.secrets != null) "${cfg.secrets}"; # hardening @@ -303,10 +378,19 @@ in systemd.services.firefox-syncserver-setup = lib.mkIf cfg.singleNode.enable { wantedBy = [ "firefox-syncserver.service" ]; - requires = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal "mysql.service"; - after = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal "mysql.service"; - path = [ config.services.mysql.package ]; - serviceConfig.ExecStart = [ "${setupScript}" ]; + requires = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal dbService; + after = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal dbService; + path = + if dbIsMySQL then [ config.services.mysql.package ] else [ config.services.postgresql.package ]; + serviceConfig = { + ExecStart = [ "${setupScript}" ]; + } + // lib.optionalAttrs dbIsPostgreSQL { + # PostgreSQL peer authentication requires the system user to match the + # database user. Run as the superuser so we can access all databases. + User = "postgres"; + Group = "postgres"; + }; }; services.nginx.virtualHosts = lib.mkIf cfg.singleNode.enableNginx { diff --git a/pkgs/by-name/sy/syncstorage-rs/package.nix b/pkgs/by-name/sy/syncstorage-rs/package.nix index 39b2b53ab03c..944ed72525af 100644 --- a/pkgs/by-name/sy/syncstorage-rs/package.nix +++ b/pkgs/by-name/sy/syncstorage-rs/package.nix @@ -1,14 +1,18 @@ { fetchFromGitHub, + fetchurl, rustPlatform, pkg-config, python3, cmake, libmysqlclient, + libpq, + openssl, makeBinaryWrapper, lib, nix-update-script, nixosTests, + dbBackend ? "mysql", }: let @@ -19,17 +23,23 @@ let p.tokenlib p.cryptography ]); + # utoipa-swagger-ui downloads Swagger UI assets at build time. + # Prefetch the archive for sandboxed builds. + swaggerUi = fetchurl { + url = "https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.17.14.zip"; + hash = "sha256-SBJE0IEgl7Efuu73n3HZQrFxYX+cn5UU5jrL4T5xzNw="; + }; in -rustPlatform.buildRustPackage rec { +rustPlatform.buildRustPackage (finalAttrs: { pname = "syncstorage-rs"; - version = "0.21.1-unstable-2026-01-26"; + version = "0.21.1-unstable-2026-02-24"; src = fetchFromGitHub { owner = "mozilla-services"; repo = "syncstorage-rs"; - rev = "11659d98f9c69948a0aab353437ce2036c388711"; - hash = "sha256-G37QvxTNh/C3gmKG0UYHI6QBr0F+KLGRNI/Sx33uOsc="; + rev = "50a739b58dc9ec81995f86e71d992aa14ccc450e"; + hash = "sha256-idq0RGdwoV6GVuq36IVVVCFbyMTe8i/EpVWE59D/dhM="; }; nativeBuildInputs = [ @@ -39,16 +49,35 @@ rustPlatform.buildRustPackage rec { python3 ]; - buildInputs = [ - libmysqlclient - ]; + buildInputs = + lib.optional (dbBackend == "mysql") libmysqlclient + ++ lib.optionals (dbBackend == "postgresql") [ + libpq + openssl + ]; + + buildNoDefaultFeatures = true; + # The syncserver "postgres" feature only enables syncstorage-db/postgres. + # tokenserver-db/postgres must be enabled separately so the tokenserver + # can also connect to PostgreSQL (it dispatches on the URL scheme at runtime). + buildFeatures = + let + cargoFeature = if dbBackend == "postgresql" then "postgres" else dbBackend; + in + [ + cargoFeature + "tokenserver-db/${cargoFeature}" + "py_verifier" + ]; + + SWAGGER_UI_DOWNLOAD_URL = "file://${swaggerUi}"; preFixup = '' wrapProgram $out/bin/syncserver \ --prefix PATH : ${lib.makeBinPath [ pyFxADeps ]} ''; - cargoHash = "sha256-9Dcf5mDyK/XjsKTlCPXTHoBkIq+FFPDg1zfK24Y9nHQ="; + cargoHash = "sha256-80EztkSX+SnmqsRWIXbChUB8AeV1Tp9WXoWNbDY8rUE="; # almost all tests need a DB to test against doCheck = false; @@ -60,10 +89,10 @@ rustPlatform.buildRustPackage rec { meta = { description = "Mozilla Sync Storage built with Rust"; homepage = "https://github.com/mozilla-services/syncstorage-rs"; - changelog = "https://github.com/mozilla-services/syncstorage-rs/releases/tag/${version}"; + changelog = "https://github.com/mozilla-services/syncstorage-rs/releases/tag/${finalAttrs.version}"; license = lib.licenses.mpl20; maintainers = [ ]; platforms = lib.platforms.linux; mainProgram = "syncserver"; }; -} +}) -- 2.53.0