Unverified Commit 21a56567 authored by Ashley Hooper's avatar Ashley Hooper
Browse files

nixos/redis: rework Sentinel options

- Add sentinelMasterHost, sentinelMasterPort, sentinelMasterQuorum
  options
- Add special handling for mutable state in /var/lib/redis-xxx

Redis/Valkey Sentinel persists dynamic cluster state by rewriting its
configuration file at runtime. This behaviour cannot be disabled.

Add conditional preStart logic to avoid overwriting Sentinel-owned
configuration while preserving NixOS-managed defaults. This introduces
a necessary, documented deviation from strict declarative semantics.
parent 1a05178b
Loading
Loading
Loading
Loading
+75 −8
Original line number Diff line number Diff line
@@ -371,17 +371,23 @@ in
                    example = "/run/keys/redis-password";
                  };

                  sentinelAuthPassFile = lib.mkOption {
                    type = with types; nullOr path;
                    default = null;
                    description = "File with password for connecting to other Sentinel instances.";
                    example = "/run/keys/sentinel-password";
                  };

                  sentinelAuthUser = lib.mkOption {
                    type = with types; nullOr str;
                    default = null;
                    description = "The username to use to monitor a master from Sentinel.";
                  };

                  sentinelAuthPassFile = lib.mkOption {
                    type = with types; nullOr path;
                  sentinelMasterHost = lib.mkOption {
                    type = with types; nullOr str;
                    default = null;
                    description = "File with password for connecting to other Sentinel instances.";
                    example = "/run/keys/sentinel-password";
                    description = "The IP address (recommended) or hostname of the Redis master that Sentinel will monitor.";
                  };

                  sentinelMasterName = lib.mkOption {
@@ -390,6 +396,18 @@ in
                    description = "The master name of the Redis master that Sentinel will monitor.";
                  };

                  sentinelMasterPort = lib.mkOption {
                    type = with types; nullOr int;
                    default = null;
                    description = "The TCP port of the Redis master that Sentinel will monitor.";
                  };

                  sentinelMasterQuorum = lib.mkOption {
                    type = with types; nullOr int;
                    default = null;
                    description = "The Sentinel quorum (minimum number of Sentinel nodes online for failover)";
                  };

                  appendOnly = lib.mkOption {
                    type = types.bool;
                    default = false;
@@ -511,11 +529,28 @@ in
            services.redis.servers.${name}.masterAuth must be provided
          '';
        }
        {
          assertion =
            conf.sentinelMasterName != null
            -> (
              conf.sentinelMasterHost != null
              && conf.sentinelMasterPort != null
              && conf.sentinelMasterQuorum != null
            );
          message = ''
            For Sentinel,
            services.redis.servers.${name}.sentinelMasterName,
            services.redis.servers.${name}.sentinelMasterHost,
            services.redis.servers.${name}.sentinelMasterPort,
            and services.redis.servers.${name}.sentinelMasterQuorum
            must all be provided
          '';
        }
        {
          assertion = conf.sentinelAuthPassFile != null -> conf.sentinelMasterName != null;
          message = ''
            For Sentinel authentication, services.redis.servers.${name}.sentinelMasterName
            must be specified
            For Sentinel authentication, services.redis.servers.${name}.sentinelMasterName,
            must be provided
          '';
        }
      ]) enabledServers
@@ -557,6 +592,17 @@ in
          ExecStart = "${cfg.package}/bin/${
            cfg.package.serverBin or "redis-server"
          } /var/lib/${redisName name}/redis.conf ${lib.escapeShellArgs conf.extraParams}";

          # NOTE: Redis/Valkey Sentinel persists dynamic cluster state by rewriting its
          # configuration file at runtime (redis.conf). This includes monitors,
          # authentication credentials, and failover metadata, and this behaviour
          # cannot be disabled.
          # As a result, a fully declarative configuration is not possible for
          # Sentinel-managed options. The preStart logic below appends sentinel
          # configuration only if it is not already present, in order to avoid
          # overwriting state that is owned and maintained by Sentinel itself.
          # This is an intentional deviation from strict declarative semantics and
          # is required for correct Sentinel operation.
          ExecStartPre =
            "+"
            + pkgs.writeShellScript "${redisName name}-prep-conf" (
@@ -582,11 +628,32 @@ in
                ${lib.optionalString (conf.masterAuthFile != null) ''
                  echo "masterauth $(cat ${lib.escapeShellArg conf.masterAuthFile})" >> "${redisConfRun}"
                ''}
                ${lib.optionalString (conf.sentinelMasterHost != null) ''
                  sentinel_monitor_line="sentinel monitor ${conf.sentinelMasterName} ${conf.sentinelMasterHost} ${toString conf.sentinelMasterPort} ${toString conf.sentinelMasterQuorum}"
                  if grep -qE "^sentinel monitor ${conf.sentinelMasterName}\b" "${redisConfVar}"; then
                    sed -i \
                      "s|^sentinel monitor ${conf.sentinelMasterName}\b.*|$sentinel_monitor_line|" "${redisConfVar}"
                  else
                    echo "$sentinel_monitor_line" >> "${redisConfVar}"
                  fi
                ''}
                ${lib.optionalString (conf.sentinelAuthUser != null) ''
                  echo "sentinel auth-user ${conf.sentinelMasterName} ${conf.sentinelAuthUser}" >> "${redisConfRun}"
                  sentinel_auth_user_line="sentinel auth-user ${conf.sentinelMasterName} ${conf.sentinelAuthUser}"
                  if grep -qE "^sentinel auth-user ${conf.sentinelMasterName}\b" "${redisConfVar}"; then
                    sed -i \
                      "s|^sentinel auth-user ${conf.sentinelMasterName}\b.*|$sentinel_auth_user_line|" "${redisConfVar}"
                  else
                    echo "$sentinel_auth_user_line" >> "${redisConfVar}"
                  fi
                ''}
                ${lib.optionalString (conf.sentinelAuthPassFile != null) ''
                  echo "sentinel auth-pass ${conf.sentinelMasterName} $(cat ${lib.escapeShellArg conf.sentinelAuthPassFile})" >> "${redisConfRun}"
                  sentinel_auth_pass_line="sentinel auth-pass ${conf.sentinelMasterName} $(cat ${lib.escapeShellArg conf.sentinelAuthPassFile})"
                  if grep -qE "^sentinel auth-pass ${conf.sentinelMasterName}\b" "${redisConfVar}"; then
                    sed -i \
                      "s|^sentinel auth-pass ${conf.sentinelMasterName}\b.*|$sentinel_auth_pass_line|" "${redisConfVar}"
                  else
                    echo "$sentinel_auth_pass_line" >> "${redisConfVar}"
                  fi
                ''}
              ''
            );