Kieran's opinionated (and probably slightly dumb) nix config
at main 243 lines 7.7 kB view raw
1/** Service utility functions for the atelier infrastructure. 2 3 These functions operate on NixOS configurations to extract 4 service metadata for dashboards, monitoring, and documentation. 5*/ 6{ lib }: 7 8{ 9 /** 10 Check whether an atelier service config value has the standard 11 mkService shape (has `enable`, `domain`, `port`, `_description`). 12 13 # Arguments 14 15 - `cfg` an attribute set from `config.atelier.services.<name>` 16 17 # Type 18 19 ``` 20 AttrSet -> Bool 21 ``` 22 23 # Example 24 25 ```nix 26 isMkService config.atelier.services.cachet 27 => true 28 ``` 29 */ 30 isMkService = cfg: 31 (cfg.enable or false) 32 && (cfg ? domain) 33 && (cfg ? port) 34 && (cfg ? _description); 35 36 /** 37 Convert a single mkService config into a manifest entry. 38 39 # Arguments 40 41 - `name` the service name (attribute key) 42 - `cfg` the service config attrset 43 44 # Type 45 46 ``` 47 String -> AttrSet -> AttrSet 48 ``` 49 50 # Example 51 52 ```nix 53 mkServiceEntry "cachet" config.atelier.services.cachet 54 => { name = "cachet"; domain = "cachet.dunkirk.sh"; ... } 55 ``` 56 */ 57 mkServiceEntry = name: cfg: { 58 inherit name; 59 description = cfg._description or "${name} service"; 60 domain = cfg.domain; 61 port = cfg.port; 62 runtime = cfg._runtime or "unknown"; 63 repository = cfg.repository or null; 64 health_url = cfg.healthUrl or null; 65 data = { 66 sqlite = cfg.data.sqlite or null; 67 postgres = cfg.data.postgres or null; 68 files = cfg.data.files or []; 69 }; 70 }; 71 72 /** 73 Build a services manifest from an evaluated NixOS config. 74 75 Discovers all enabled mkService-based services plus emojibot 76 instances. Returns a sorted list of service entries suitable 77 for JSON serialisation. 78 79 # Arguments 80 81 - `config` the fully evaluated NixOS configuration 82 83 # Type 84 85 ``` 86 AttrSet -> [ AttrSet ] 87 ``` 88 89 # Example 90 91 ```nix 92 mkManifest config 93 => [ { name = "cachet"; domain = "cachet.dunkirk.sh"; ... } ... ] 94 ``` 95 */ 96 mkManifest = config: 97 let 98 allServices = config.atelier.services; 99 100 isMkSvc = _: v: 101 (v.enable or false) 102 && (v ? domain) 103 && (v ? port) 104 && (v ? _description); 105 106 standardServices = lib.filterAttrs isMkSvc allServices; 107 108 mkEntry = name: cfg: { 109 inherit name; 110 description = cfg._description or "${name} service"; 111 domain = cfg.domain; 112 port = cfg.port; 113 runtime = cfg._runtime or "unknown"; 114 repository = cfg.repository or null; 115 health_url = cfg.healthUrl or null; 116 data = { 117 sqlite = cfg.data.sqlite or null; 118 postgres = cfg.data.postgres or null; 119 files = cfg.data.files or []; 120 }; 121 }; 122 123 emojibotInstances = 124 let 125 instances = allServices.emojibot.instances or {}; 126 enabled = lib.filterAttrs (_: v: v.enable or false) instances; 127 in 128 lib.mapAttrsToList (name: inst: { 129 name = "emojibot-${name}"; 130 description = "Emojibot for ${inst.workspace or name}"; 131 domain = inst.domain; 132 port = inst.port; 133 runtime = "bun"; 134 repository = inst.repository or null; 135 health_url = inst.healthUrl or null; 136 data = { sqlite = null; postgres = null; files = []; }; 137 }) enabled; 138 139 # Custom services that don't use mkService but should appear in the manifest 140 customServices = let 141 noData = { sqlite = null; postgres = null; files = []; }; 142 mkCustom = name: attrs: { inherit name; data = noData; } // attrs; 143 in lib.concatLists [ 144 (lib.optional ((allServices.herald.enable or false) && (allServices.herald ? domain)) (mkCustom "herald" { 145 description = "RSS-to-Email via SSH"; 146 domain = allServices.herald.domain; 147 port = allServices.herald.httpPort or 8085; 148 runtime = "go"; 149 repository = null; 150 health_url = "https://${allServices.herald.domain}"; 151 })) 152 (lib.optional ((allServices.triage-agent.enable or false) && (allServices.triage-agent ? domain)) (mkCustom "triage-agent" { 153 description = "AI-powered service triage webhook"; 154 domain = allServices.triage-agent.domain; 155 port = allServices.triage-agent.port or 3200; 156 runtime = "bun"; 157 repository = null; 158 health_url = "https://${allServices.triage-agent.domain}/health"; 159 })) 160 (lib.optional ((allServices.frps.enable or false) && (allServices.frps ? domain)) (mkCustom "bore" { 161 description = "HTTP/TCP/UDP tunnel proxy"; 162 domain = allServices.frps.domain; 163 port = allServices.frps.vhostHTTPPort or 7080; 164 runtime = "go"; 165 repository = null; 166 health_url = "https://${allServices.frps.domain}"; 167 })) 168 (lib.optional (config.services.tangled.knot.enable or false) (mkCustom "knot" { 169 description = "Tangled git hosting"; 170 domain = config.services.tangled.knot.server.hostname or "knot.dunkirk.sh"; 171 port = 5555; 172 runtime = "go"; 173 repository = null; 174 health_url = "https://${config.services.tangled.knot.server.hostname or "knot.dunkirk.sh"}"; 175 })) 176 (lib.optional (config.services.tangled.spindle.enable or false) (mkCustom "spindle" { 177 description = "Tangled CI"; 178 domain = config.services.tangled.spindle.server.hostname or "spindle.dunkirk.sh"; 179 port = 6555; 180 runtime = "go"; 181 repository = null; 182 health_url = "https://${config.services.tangled.spindle.server.hostname or "spindle.dunkirk.sh"}"; 183 })) 184 (lib.optional (config.services.n8n.enable or false) (mkCustom "n8n" { 185 description = "Workflow automation"; 186 domain = config.services.n8n.environment.N8N_HOST or "n8n.dunkirk.sh"; 187 port = 5678; 188 runtime = "node"; 189 repository = null; 190 health_url = "https://${config.services.n8n.environment.N8N_HOST or "n8n.dunkirk.sh"}/healthz"; 191 })) 192 ]; 193 194 serviceList = (lib.mapAttrsToList mkEntry standardServices) ++ emojibotInstances ++ customServices; 195 in 196 lib.sort (a: b: a.name < b.name) serviceList; 197 198 /** 199 Build a manifest of all machines and their services. 200 201 Takes one or more attrsets of system configurations (NixOS, Darwin, 202 or home-manager) and returns an attrset keyed by machine name. 203 Only machines with `atelier.machine.enable = true` are included. 204 205 # Arguments 206 207 - `configSets` list of attrsets of system configurations 208 209 # Type 210 211 ``` 212 [ AttrSet ] -> AttrSet 213 ``` 214 215 # Example 216 217 ```nix 218 mkMachinesManifest [ self.nixosConfigurations self.darwinConfigurations ] 219 => { terebithia = { hostname = "terebithia"; services = [ ... ]; }; } 220 ``` 221 */ 222 mkMachinesManifest = configSets: 223 let 224 self = import ./services.nix { inherit lib; }; 225 merged = lib.foldl (acc: cs: acc // cs) {} configSets; 226 enabled = lib.filterAttrs (_: sys: 227 sys.config.atelier.machine.enable or false 228 ) merged; 229 mkMachineEntry = name: sys: 230 let 231 config = sys.config; 232 hasAtelierServices = config ? atelier && config.atelier ? services; 233 services = if hasAtelierServices then self.mkManifest config else []; 234 in { 235 hostname = config.networking.hostName or name; 236 type = config.atelier.machine.type or "server"; 237 tailscale_host = config.atelier.machine.tailscaleHost or null; 238 triage_url = config.atelier.machine.triageUrl or null; 239 services = services; 240 }; 241 in 242 lib.mapAttrs mkMachineEntry enabled; 243}