Files
minne/nix/nixos/minne.nix
T

331 lines
9.7 KiB
Nix

{
self ? null,
}:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.minne;
surrealAddress = "ws://${cfg.surrealdb.host}:${toString cfg.surrealdb.port}";
surrealUnits =
lib.optional cfg.surrealdb.enable "surrealdb.service"
++ cfg.surrealdb.after
++ cfg.surrealdb.requires;
minneAfter = [ "network.target" ] ++ surrealUnits;
minneRequires = surrealUnits;
surrealAuthArgs =
if cfg.surrealdb.username != null then
"--user ${cfg.surrealdb.username} --pass ${cfg.surrealdb.password}"
else
"";
baseEnvironment = {
SURREALDB_ADDRESS = surrealAddress;
HTTP_PORT = toString cfg.port;
RUST_LOG = cfg.logLevel;
DATA_DIR = cfg.dataDir;
SURREALDB_NAMESPACE = cfg.surrealdb.namespace;
SURREALDB_DATABASE = cfg.surrealdb.database;
}
// lib.optionalAttrs (cfg.surrealdb.username != null) {
SURREALDB_USERNAME = cfg.surrealdb.username;
SURREALDB_PASSWORD = cfg.surrealdb.password;
}
// cfg.environment;
mkService =
{ description, bin }:
{
inherit description;
wantedBy = [ "multi-user.target" ];
after = minneAfter;
requires = minneRequires;
environment = baseEnvironment;
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
WorkingDirectory = cfg.dataDir;
ExecStart = "${cfg.package}/bin/${bin}";
Restart = "always";
RestartSec = "10";
EnvironmentFile = lib.optional (cfg.environmentFile != null) cfg.environmentFile;
};
};
in
{
options.services.minne = {
enable = lib.mkEnableOption "the Minne knowledge management server";
package = lib.mkOption {
type = lib.types.package;
default =
if self != null then
self.packages.${pkgs.stdenv.hostPlatform.system}.default
else
throw "services.minne.package must be set when the Minne flake is not available";
defaultText = lib.literalExpression "minne.packages.\${system}.default";
description = "Minne package providing the main/server/worker binaries.";
};
mode = lib.mkOption {
type = lib.types.enum [
"combined"
"split"
];
default = "combined";
description = ''
"combined" runs a single `main` process (API, web UI, and worker).
"split" runs the API-only `server` alongside a standalone `worker`.
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "minne";
description = "User account under which Minne runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "minne";
description = "Group under which Minne runs.";
};
port = lib.mkOption {
type = lib.types.port;
default = 3000;
description = "TCP port the Minne HTTP server listens on (HTTP_PORT).";
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/minne";
description = "State directory for Minne (DATA_DIR).";
};
logLevel = lib.mkOption {
type = lib.types.str;
default = "info";
description = "Value for RUST_LOG.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open Minne `port` in the firewall.";
};
environment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = {
EMBEDDING_BACKEND = "hashed";
};
description = "Extra environment variables passed to every Minne service.";
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/secrets/minne.env";
description = ''
Path to a systemd EnvironmentFile holding secrets such as
`OPENAI_API_KEY`. SurrealDB credentials can be set via
`services.minne.surrealdb` or included here.
'';
};
surrealdb = {
enable = lib.mkEnableOption "a bundled SurrealDB instance for Minne";
package = lib.mkOption {
type = lib.types.package;
default = import ../packages/surrealdb { inherit pkgs; };
description = "SurrealDB package used when `services.minne.surrealdb.enable` is true.";
};
host = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Host SurrealDB binds to and Minne connects to.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8000;
description = "Port SurrealDB listens on and Minne connects to.";
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/surrealdb";
description = "RocksDB data directory for bundled SurrealDB.";
};
user = lib.mkOption {
type = lib.types.str;
default = "surrealdb";
description = "User account under which bundled SurrealDB runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "surrealdb";
description = "Group under which bundled SurrealDB runs.";
};
namespace = lib.mkOption {
type = lib.types.str;
default = "minne";
description = "SurrealDB namespace passed to Minne (SURREALDB_NAMESPACE).";
};
database = lib.mkOption {
type = lib.types.str;
default = "minne";
description = "SurrealDB database passed to Minne (SURREALDB_DATABASE).";
};
username = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "root";
description = ''
Root username for bundled SurrealDB (`--user`). When set, `password`
must also be set and Minne receives matching SURREALDB_USERNAME/PASSWORD.
'';
};
password = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Root password for bundled SurrealDB (`--pass`).";
};
credentialsFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/secrets/surrealdb-credentials";
description = ''
Optional EnvironmentFile for bundled SurrealDB. Use when credentials
should not appear in the Nix store. Expects SURREALDB_USER and
SURREALDB_PASS (or SURREALDB_USERNAME / SURREALDB_PASSWORD).
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open bundled SurrealDB `port` in the firewall.";
};
after = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "network-online.target" ];
description = "Extra units ordered before Minne when using an external SurrealDB.";
};
requires = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "surrealdb.service" ];
description = "Extra units Minne hard-depends on (external SurrealDB).";
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion =
!cfg.surrealdb.enable || cfg.surrealdb.username == null || cfg.surrealdb.password != null;
message = "services.minne.surrealdb.password must be set when username is set.";
}
];
systemd.services = lib.mkMerge [
(lib.mkIf cfg.surrealdb.enable {
surrealdb = {
description = "SurrealDB for Minne";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
Type = "simple";
User = cfg.surrealdb.user;
Group = cfg.surrealdb.group;
WorkingDirectory = cfg.surrealdb.dataDir;
ExecStart = "${cfg.surrealdb.package}/bin/surreal start --bind ${cfg.surrealdb.host}:${toString cfg.surrealdb.port} ${surrealAuthArgs} rocksdb:${cfg.surrealdb.dataDir}/data.db";
Restart = "always";
RestartSec = "10";
EnvironmentFile = lib.optional (
cfg.surrealdb.credentialsFile != null
) cfg.surrealdb.credentialsFile;
};
};
})
(lib.mkIf (cfg.mode == "combined") {
minne = mkService {
description = "Minne — API, web UI, and background worker";
bin = "main";
};
})
(lib.mkIf (cfg.mode == "split") {
minne-server = mkService {
description = "Minne — API-only server";
bin = "server";
};
minne-worker = mkService {
description = "Minne — standalone background worker";
bin = "worker";
};
})
];
users.users = lib.mkMerge [
(lib.mkIf (cfg.user == "minne") {
minne = {
isSystemUser = true;
inherit (cfg) group;
home = cfg.dataDir;
createHome = true;
};
})
(lib.mkIf (cfg.surrealdb.enable && cfg.surrealdb.user == "surrealdb") {
surrealdb = {
isSystemUser = true;
inherit (cfg.surrealdb) group;
home = cfg.surrealdb.dataDir;
createHome = true;
};
})
];
users.groups = lib.mkMerge [
(lib.mkIf (cfg.group == "minne") {
minne = { };
})
(lib.mkIf (cfg.surrealdb.enable && cfg.surrealdb.group == "surrealdb") {
surrealdb = { };
})
];
systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} -"
]
++ lib.optional cfg.surrealdb.enable "d ${cfg.surrealdb.dataDir} 0750 ${cfg.surrealdb.user} ${cfg.surrealdb.group} -";
networking.firewall.allowedTCPPorts =
(lib.optional cfg.openFirewall cfg.port)
++ lib.optional (cfg.surrealdb.enable && cfg.surrealdb.openFirewall) cfg.surrealdb.port;
};
}