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