sourcebot/nix/nixosModule.nix
Alexander Braverman Masis 568395aba0 flake.nix
Package Sourcebot with Nix, NixOS module for deployment, integration
test and microvm.
2025-09-22 09:09:42 -04:00

241 lines
8.3 KiB
Nix

self: {
config,
pkgs,
lib ? pkgs.lib,
...
}:
with lib; let
cfg = config.services.sourcebot;
in {
options.services.sourcebot = {
enable = mkEnableOption "Enable SourceBot";
dataDir = mkOption {
type = types.path;
default = "/var/lib/sourcebot";
description = "Root data directory for SourceBot";
};
package = lib.mkOption {
type = types.package;
default = self.packages.${pkgs.system}.sourcebot;
description = "Package to use for nixos-cli";
};
# Override default, based on DATA_DIR
dataCacheDir = mkOption {
type = types.path;
default = "/var/cache/sourcebot";
description = "Directory for SourceBot data cache";
};
envFile = mkOption {
type = types.nullOr types.path;
default = null;
description = "Environment file for additional settings";
};
logLevel = mkOption {
type = types.enum ["debug" "info" "warn" "error"];
default = "info";
description = "SourceBot logging level";
};
authEnabled = mkOption {
type = types.bool;
default = true;
description = "Enables authentication in SourceBot";
};
telemetryDisabled = mkOption {
type = types.bool;
default = true;
description = "Disables telemetry collection in SourceBot";
};
openFirewall = mkOption {
type = types.bool;
default = true;
description = "Open Firwall ports for SourceBot";
};
configPath = mkOption {
type = types.str;
description = "Path to the SourceBot configuration file";
};
port = mkOption {
type = types.int;
default = 7734;
description = "TCP port for the SourceBot web server to listen on";
};
hostname = mkOption {
type = types.str;
default = "0.0.0.0";
description = "Hostname or IP address for the SourceBot web server to bind to";
};
authUrl = mkOption {
type = types.str;
default = "http://${cfg.hostname}:${toString cfg.port}";
description = "Hostname or IP address for the SourceBot web server to bind to";
};
redisPort = mkOption {
type = types.int;
default = 16379;
description = "TCP port for the SourceBot Redis server to listen on";
};
databaseUrl = mkOption {
type = types.nullOr types.str;
default = "postgresql://sourcebot@localhost:${toString config.services.postgresql.settings.port}/sourcebot";
description = "PostgreSQL connection URL for SourceBot. If not set, a local PostgreSQL server will be configured and used.";
};
};
config = mkIf cfg.enable {
# Create a dedicated system group for SourceBot
users.groups.sourcebot = {};
# Create a dedicated system user for SourceBot
users.users.sourcebot = {
isSystemUser = true;
group = "sourcebot"; # primary group
description = "Service account for SourceBot";
home = cfg.dataDir;
shell = "/run/current-system/sw/bin/false";
};
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [cfg.port];
# Enable redis using the existing NixOS module
services.redis.servers.sourcebot = {
enable = true;
user = "sourcebot";
port = cfg.redisPort;
};
# Enable Postgres for SourceBot with isolated data directory and database
services.postgresql = {
enable = true;
enableTCPIP = true;
ensureDatabases = ["sourcebot"];
ensureUsers = [
{
name = "sourcebot";
ensureDBOwnership = true;
}
];
# Allow connections from any container IP addresses
authentication = mkBefore ''
local sourcebot sourcebot trust
host sourcebot sourcebot 127.0.0.1/32 trust
host sourcebot sourcebot ::1/128 trust
'';
};
# Create and own data directories
systemd.tmpfiles.rules = [
# ensure root data dir
"d ${cfg.dataDir} 0755 sourcebot sourcebot -"
# ensure cache dir
"d ${cfg.dataCacheDir} 0755 sourcebot sourcebot -"
];
# Zoekt search service
systemd.services.sourcebot-zoekt = {
description = "SourceBot Zoekt Search Service";
after = ["network.target" "redis.service"];
wants = ["redis.service"];
serviceConfig = {
# Run under the sourcebot user
User = "sourcebot";
Group = "sourcebot";
ExecStart = "${pkgs.zoekt}/bin/zoekt-webserver -index ${cfg.dataCacheDir}/index -rpc";
Restart = "on-failure";
RestartSec = "5s";
Environment = [
"DATA_DIR=${cfg.dataDir}"
"DATA_CACHE_DIR=${cfg.dataCacheDir}"
];
};
};
systemd.services.sourcebot-db-setup = {
description = "SourceBot Database setup";
after = ["network.target" "postgresql.service"];
wants = ["postgresql.service"];
serviceConfig = {
# Run under the sourcebot user
Type = "oneshot";
User = "sourcebot";
Group = "sourcebot";
ExecStart = "${pkgs.prisma}/bin/prisma migrate deploy --schema ${cfg.package}/packages/db/prisma/schema.prisma";
Environment = [
"PATH=${makeBinPath (with pkgs; [prisma openssl])}"
"DATABASE_URL=${cfg.databaseUrl}"
];
Restart = "on-failure";
RestartSec = "5s";
};
};
# Web frontend service
systemd.services.sourcebot-web = {
description = "SourceBot Web Service";
after = ["network.target" "sourcebot-zoekt.service" "sourcebot-db-setup.service"];
wants = ["sourcebot-zoekt.service" "sourcebot-db-setup.service"];
wantedBy = ["multi-user.target"];
serviceConfig = {
# Run under the sourcebot user
User = "sourcebot";
Group = "sourcebot";
Environment =
[
"DATA_DIR=${cfg.dataDir}"
"DATA_CACHE_DIR=${cfg.dataCacheDir}"
"PORT=${toString cfg.port}"
"HOSTNAME=${cfg.hostname}"
"DATABASE_URL=${cfg.databaseUrl}"
"REDIS_URL=redis://localhost:${toString cfg.redisPort}"
"CONFIG_PATH=${cfg.configPath}"
"SOURCEBOT_LOG_LEVEL=${cfg.logLevel}"
"SOURCEBOT_TENANCY_MODE=single"
"AUTH_CREDENTIALS_LOGIN_ENABLED=${boolToString cfg.authEnabled}"
"SOURCEBOT_TELEMETRY_DISABLED=${boolToString cfg.telemetryDisabled}"
"SOURCEBOT_PUBLIC_KEY_PATH=${cfg.package}/public.pem"
"AUTH_URL=http://${cfg.hostname}:${toString cfg.port}"
]
++ optional (cfg.envFile == null) [
"AUTH_SECRET=00000000000000000000000000000000000000000000"
"SOURCEBOT_ENCRYPTION_KEY=00000000000000000000000000000000"
];
EnvironmentFile = cfg.envFile;
ExecStart = "${cfg.package}/bin/sourcebot-web";
Restart = "always";
};
};
# Backend API service
systemd.services.sourcebot-backend = {
description = "SourceBot Backend Service";
after = ["network.target" "sourcebot-zoekt.service" "sourcebot-db-setup.service"];
wants = ["sourcebot-zoekt.service" "sourcebot-db-setup.service"];
wantedBy = ["multi-user.target"];
serviceConfig = {
# Run under the sourcebot user
User = "sourcebot";
Group = "sourcebot";
Environment =
[
"DATA_DIR=${cfg.dataDir}"
"DATA_CACHE_DIR=${cfg.dataCacheDir}"
"DATABASE_URL=postgresql://sourcebot@localhost:${toString config.services.postgresql.settings.port}/sourcebot"
"REDIS_URL=redis://localhost:${toString cfg.redisPort}"
"CONFIG_PATH=${cfg.configPath}"
"SOURCEBOT_LOG_LEVEL=${cfg.logLevel}"
"SOURCEBOT_TENANCY_MODE=single"
"AUTH_CREDENTIALS_LOGIN_ENABLED=${boolToString cfg.authEnabled}"
"SOURCEBOT_TELEMETRY_DISABLED=${boolToString cfg.telemetryDisabled}"
"SOURCEBOT_PUBLIC_KEY_PATH=${cfg.package}/public.pem"
"AUTH_URL=http://${cfg.hostname}:${toString cfg.port}"
]
++ optional (cfg.envFile == null) [
"AUTH_SECRET=00000000000000000000000000000000000000000000"
"SOURCEBOT_ENCRYPTION_KEY=00000000000000000000000000000000"
];
EnvironmentFile = cfg.envFile;
ExecStart = "${cfg.package}/bin/sourcebot-backend --cacheDir ${cfg.dataCacheDir}";
Restart = "on-failure";
RestartSec = "5s";
};
};
};
}