This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / nix / modules / spindle.nix
16 kB 403 lines
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: let 7 cfg = config.services.tangled.spindle; 8in 9 with lib; { 10 imports = [ 11 (lib.mkRemovedOptionModule [ "services" "tangled" "spindle" "server" "owner" ] "The spindle owner setting is deprecated.") 12 ]; 13 options = { 14 services.tangled.spindle = { 15 enable = mkOption { 16 type = types.bool; 17 default = false; 18 description = "Enable a tangled spindle"; 19 }; 20 package = mkOption { 21 type = types.package; 22 description = "Package to use for the spindle"; 23 }; 24 25 server = { 26 listenAddr = mkOption { 27 type = types.str; 28 default = "0.0.0.0:6555"; 29 description = "Address to listen on"; 30 }; 31 32 dbPath = mkOption { 33 type = types.path; 34 default = "/var/lib/spindle/spindle.db"; 35 description = "Path to the database file"; 36 }; 37 38 repoDir = mkOption { 39 type = types.path; 40 default = "/var/lib/spindle/repos"; 41 description = "Path where synced git repositories live"; 42 }; 43 44 hostname = mkOption { 45 type = types.str; 46 example = "my.spindle.com"; 47 description = "Hostname for the server (required)"; 48 }; 49 50 plcUrl = mkOption { 51 type = types.str; 52 default = "https://plc.directory"; 53 description = "atproto PLC directory"; 54 }; 55 56 jetstreamEndpoint = mkOption { 57 type = types.str; 58 default = "wss://jetstream1.us-west.bsky.network/subscribe"; 59 description = "Jetstream endpoint to subscribe to"; 60 }; 61 62 dev = mkOption { 63 type = types.bool; 64 default = false; 65 description = "Enable development mode (disables signature verification)"; 66 }; 67 68 inviteOnly = mkOption { 69 type = types.bool; 70 default = true; 71 description = "Only accept repos from members of this spindle"; 72 }; 73 74 maxJobCount = mkOption { 75 type = types.int; 76 default = 2; 77 example = 5; 78 description = "Maximum number of concurrent jobs to run"; 79 }; 80 81 queueSize = mkOption { 82 type = types.int; 83 default = 100; 84 example = 100; 85 description = "Maximum number of jobs queue up"; 86 }; 87 88 secrets = { 89 provider = mkOption { 90 type = types.str; 91 default = "sqlite"; 92 description = "Backend to use for secret management, valid options are 'sqlite', and 'openbao'."; 93 }; 94 95 openbao = { 96 proxyAddr = mkOption { 97 type = types.str; 98 default = "http://127.0.0.1:8200"; 99 description = "Address of the OpenBAO proxy server"; 100 }; 101 mount = mkOption { 102 type = types.str; 103 default = "spindle"; 104 description = "Mount path in OpenBAO to read secrets from"; 105 }; 106 }; 107 }; 108 109 tap = { 110 embed = mkOption { 111 type = types.bool; 112 default = true; 113 description = "Run an embedded tap inside the spindle process"; 114 }; 115 116 url = mkOption { 117 type = types.str; 118 default = "http://[::1]:2480"; 119 description = "URL the spindle's tap client dials"; 120 }; 121 122 bind = mkOption { 123 type = types.str; 124 default = "[::1]:2480"; 125 description = "Loopback address the embedded tap server listens on"; 126 }; 127 128 dbPath = mkOption { 129 type = types.path; 130 default = "/var/lib/spindle/tap.db"; 131 description = "Path to the embedded tap sqlite database"; 132 }; 133 134 relayUrl = mkOption { 135 type = types.str; 136 default = "https://bsky.network"; 137 description = "Relay used by the embedded tap firehose"; 138 }; 139 }; 140 }; 141 142 pipelines = { 143 logBucket = mkOption { 144 type = types.str; 145 default = "tangled-logs"; 146 description = "S3 bucket for workflow logs"; 147 }; 148 workflowTimeout = mkOption { 149 type = types.str; 150 default = "5m"; 151 description = "Timeout for each workflow step"; 152 }; 153 154 nixery = { 155 nixery = mkOption { 156 type = types.str; 157 default = "nixery.tangled.sh"; # note: this is *not* on tangled.org yet 158 description = "Nixery instance to use"; 159 }; 160 161 maxJobMemoryMb = mkOption { 162 type = types.int; 163 default = 6144; 164 description = "Memory limit per nixery workflow container in MiB (default 6 GiB)"; 165 }; 166 maxConcurrentWorkflows = mkOption { 167 type = types.int; 168 default = 8; 169 description = "Maximum number of nixery workflows running simultaneously. Zero disables this limit."; 170 }; 171 }; 172 173 microvm = { 174 enableKVM = mkOption { 175 type = types.bool; 176 default = true; 177 description = "Enable KVM hardware acceleration"; 178 }; 179 180 imageDir = mkOption { 181 type = types.str; 182 default = "/var/lib/spindle/images"; 183 description = "Directory containing microVM image spec JSONs or image spec directories"; 184 }; 185 overlayDir = mkOption { 186 type = types.str; 187 default = "/tmp"; 188 description = "Directory to store microVM temporary overlay files"; 189 }; 190 defaultImage = mkOption { 191 type = types.str; 192 default = "nixos"; 193 description = "Default microVM image spec to use if none is specified in workflow"; 194 }; 195 agentPort = mkOption { 196 type = types.port; 197 default = 10240; 198 description = "Host vsock port the microVM agent connects back to"; 199 }; 200 201 limits = { 202 total = { 203 memoryMiB = mkOption { 204 type = types.int; 205 default = 0; 206 description = "Maximum declared guest memory in MiB allowed across all running microVM workflows. Zero disables this limit."; 207 }; 208 vcpus = mkOption { 209 type = types.int; 210 default = 0; 211 description = "Maximum declared vCPUs allowed across all running microVM workflows. Zero disables this limit."; 212 }; 213 diskMiB = mkOption { 214 type = types.int; 215 default = 0; 216 description = "Maximum declared disk in MiB allowed across all running microVM workflows. Zero disables this limit."; 217 }; 218 }; 219 220 workflow = { 221 memoryMiB = mkOption { 222 type = types.int; 223 default = 0; 224 description = "Maximum declared guest memory in MiB allowed for a single microVM workflow. Zero disables this limit."; 225 }; 226 vcpus = mkOption { 227 type = types.int; 228 default = 0; 229 description = "Maximum declared vCPUs allowed for a single microVM workflow. Zero disables this limit."; 230 }; 231 diskMiB = mkOption { 232 type = types.int; 233 default = 0; 234 description = "Maximum declared disk in MiB allowed for a single microVM workflow. Zero disables this limit."; 235 }; 236 }; 237 }; 238 239 cgroup = { 240 enable = mkOption { 241 type = types.bool; 242 default = false; 243 description = "Enable cgroup v2 containment for microVM processes."; 244 }; 245 parent = mkOption { 246 type = types.str; 247 default = "self"; 248 description = "Parent cgroup for microVM workflow cgroups. Use 'self' to resolve the spindle service cgroup."; 249 }; 250 pidsMax = mkOption { 251 type = types.int; 252 default = 4096; 253 description = "Maximum number of processes allowed in each microVM workflow cgroup."; 254 }; 255 swapMaxMiB = mkOption { 256 type = types.int; 257 default = 0; 258 description = "Maximum swap in MiB allowed in each microVM workflow cgroup. Zero disables swap."; 259 }; 260 supervisorMinMiB = mkOption { 261 type = types.int; 262 default = 512; 263 description = '' 264 Amount of memory in MiB that will be protected by the cgroup for the spindle 265 (allowing it to not get OOMed first.) 266 ''; 267 }; 268 }; 269 }; 270 271 nixCache = { 272 readUrls = mkOption { 273 type = types.listOf types.str; 274 default = []; 275 example = ["http://ncps.internal:8501" "ssh-ng://user@my-awesome-cache"]; 276 description = "Nix binary cache URLs the Spindle guest should read from."; 277 }; 278 279 trustedPublicKeys = mkOption { 280 type = types.listOf types.str; 281 default = []; 282 example = ["internal-1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="]; 283 description = "Public keys trusted for the configured Nix binary caches."; 284 }; 285 286 uploadUrl = mkOption { 287 type = types.str; 288 default = ""; 289 example = "local"; 290 description = "Optional cache upload URL used by live cache import paths."; 291 }; 292 }; 293 }; 294 295 environmentFile = mkOption { 296 type = with types; nullOr path; 297 default = null; 298 example = "/etc/spindle.env"; 299 description = '' 300 Additional environment file as defined in {manpage}`systemd.exec(5)`. 301 302 Sensitive secrets such as {env}`SPINDLE_SERVER_ADMIN_PASSWORD` 303 (without it the `/admin` member management endpoints are not mounted), 304 {env}`AWS_SECRET_ACCESS_KEY`, 305 {env}`AWS_ACCESS_KEY_ID`, {env}`AWS_REGION` 306 may be passed to the service 307 without making them world readable in the nix store. 308 ''; 309 }; 310 }; 311 }; 312 313 config = let 314 deps = [ 315 pkgs.git 316 pkgs.qemu 317 pkgs.e2fsprogs 318 pkgs.slirp4netns 319 pkgs.iproute2 320 pkgs.util-linux 321 config.nix.package 322 ]; 323 in 324 mkIf cfg.enable { 325 environment.systemPackages = [ 326 (pkgs.writeShellScriptBin "spindle" '' 327 export PATH="${lib.makeBinPath deps}:$PATH" 328 ${lib.optionalString (cfg.environmentFile != null) "set -a; source ${cfg.environmentFile}; set +a"} 329 ${lib.concatMapStringsSep "\n" ( 330 e: "export ${e}" 331 ) 332 config.systemd.services.spindle.serviceConfig.Environment} 333 exec ${cfg.package}/bin/spindle "$@" 334 '') 335 ]; 336 337 virtualisation.docker.enable = true; 338 339 systemd.services.spindle = { 340 description = "spindle service"; 341 after = [ 342 "network.target" 343 "docker.service" 344 ]; 345 wantedBy = ["multi-user.target"]; 346 path = deps; 347 serviceConfig = { 348 LogsDirectory = "spindle"; 349 StateDirectory = "spindle"; 350 Delegate = cfg.pipelines.microvm.cgroup.enable; 351 EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; 352 353 Environment = [ 354 "SPINDLE_SERVER_LISTEN_ADDR=${cfg.server.listenAddr}" 355 "SPINDLE_SERVER_DB_PATH=${cfg.server.dbPath}" 356 "SPINDLE_SERVER_REPO_DIR=${cfg.server.repoDir}" 357 "SPINDLE_SERVER_HOSTNAME=${cfg.server.hostname}" 358 "SPINDLE_SERVER_PLC_URL=${cfg.server.plcUrl}" 359 "SPINDLE_SERVER_JETSTREAM_ENDPOINT=${cfg.server.jetstreamEndpoint}" 360 "SPINDLE_SERVER_DEV=${lib.boolToString cfg.server.dev}" 361 "SPINDLE_SERVER_INVITE_ONLY=${lib.boolToString cfg.server.inviteOnly}" 362 "SPINDLE_SERVER_MAX_JOB_COUNT=${toString cfg.server.maxJobCount}" 363 "SPINDLE_SERVER_QUEUE_SIZE=${toString cfg.server.queueSize}" 364 "SPINDLE_SERVER_SECRETS_PROVIDER=${cfg.server.secrets.provider}" 365 "SPINDLE_SERVER_SECRETS_OPENBAO_PROXY_ADDR=${cfg.server.secrets.openbao.proxyAddr}" 366 "SPINDLE_SERVER_SECRETS_OPENBAO_MOUNT=${cfg.server.secrets.openbao.mount}" 367 "SPINDLE_SERVER_TAP_EMBED=${lib.boolToString cfg.server.tap.embed}" 368 "SPINDLE_SERVER_TAP_URL=${cfg.server.tap.url}" 369 "SPINDLE_SERVER_TAP_BIND=${cfg.server.tap.bind}" 370 "SPINDLE_SERVER_TAP_DB_PATH=${cfg.server.tap.dbPath}" 371 "SPINDLE_SERVER_TAP_RELAY_URL=${cfg.server.tap.relayUrl}" 372 "SPINDLE_NIXERY_PIPELINES_NIXERY=${cfg.pipelines.nixery.nixery}" 373 "SPINDLE_NIXERY_PIPELINES_WORKFLOW_TIMEOUT=${cfg.pipelines.workflowTimeout}" 374 "SPINDLE_NIXERY_PIPELINES_MAX_JOB_MEMORY_MB=${toString cfg.pipelines.nixery.maxJobMemoryMb}" 375 "SPINDLE_NIXERY_PIPELINES_MAX_CONCURRENT_WORKFLOWS=${toString cfg.pipelines.nixery.maxConcurrentWorkflows}" 376 "SPINDLE_MICROVM_PIPELINES_IMAGE_DIR=${cfg.pipelines.microvm.imageDir}" 377 "SPINDLE_MICROVM_PIPELINES_OVERLAY_DIR=${cfg.pipelines.microvm.overlayDir}" 378 "SPINDLE_MICROVM_PIPELINES_DEFAULT_IMAGE=${cfg.pipelines.microvm.defaultImage}" 379 "SPINDLE_MICROVM_PIPELINES_AGENT_PORT=${toString cfg.pipelines.microvm.agentPort}" 380 "SPINDLE_MICROVM_PIPELINES_ENABLE_KVM=${lib.boolToString cfg.pipelines.microvm.enableKVM}" 381 "SPINDLE_MICROVM_PIPELINES_WORKFLOW_TIMEOUT=${cfg.pipelines.workflowTimeout}" 382 "SPINDLE_MICROVM_PIPELINES_MAX_TOTAL_MEMORY_MIB=${toString cfg.pipelines.microvm.limits.total.memoryMiB}" 383 "SPINDLE_MICROVM_PIPELINES_MAX_TOTAL_VCPUS=${toString cfg.pipelines.microvm.limits.total.vcpus}" 384 "SPINDLE_MICROVM_PIPELINES_MAX_TOTAL_DISK_MIB=${toString cfg.pipelines.microvm.limits.total.diskMiB}" 385 "SPINDLE_MICROVM_PIPELINES_MAX_WORKFLOW_MEMORY_MIB=${toString cfg.pipelines.microvm.limits.workflow.memoryMiB}" 386 "SPINDLE_MICROVM_PIPELINES_MAX_WORKFLOW_VCPUS=${toString cfg.pipelines.microvm.limits.workflow.vcpus}" 387 "SPINDLE_MICROVM_PIPELINES_MAX_WORKFLOW_DISK_MIB=${toString cfg.pipelines.microvm.limits.workflow.diskMiB}" 388 "SPINDLE_MICROVM_PIPELINES_ENABLE_CGROUPS=${lib.boolToString cfg.pipelines.microvm.cgroup.enable}" 389 "SPINDLE_MICROVM_PIPELINES_CGROUP_PARENT=${cfg.pipelines.microvm.cgroup.parent}" 390 "SPINDLE_MICROVM_PIPELINES_CGROUP_PIDS_MAX=${toString cfg.pipelines.microvm.cgroup.pidsMax}" 391 "SPINDLE_MICROVM_PIPELINES_CGROUP_SWAP_MAX_MIB=${toString cfg.pipelines.microvm.cgroup.swapMaxMiB}" 392 "SPINDLE_MICROVM_PIPELINES_CGROUP_SUPERVISOR_MEMORY_MIN_MIB=${toString cfg.pipelines.microvm.cgroup.supervisorMinMiB}" 393 "SPINDLE_NIX_CACHE_READ_URLS=${concatStringsSep "," cfg.pipelines.nixCache.readUrls}" 394 "SPINDLE_NIX_CACHE_TRUSTED_PUBLIC_KEYS=${concatStringsSep "," cfg.pipelines.nixCache.trustedPublicKeys}" 395 "SPINDLE_NIX_CACHE_UPLOAD_URL=${cfg.pipelines.nixCache.uploadUrl}" 396 "SPINDLE_S3_LOG_BUCKET=${cfg.pipelines.logBucket}" 397 ]; 398 ExecStart = "${cfg.package}/bin/spindle"; 399 Restart = "always"; 400 }; 401 }; 402 }; 403 }