Kieran's opinionated (and probably slightly dumb) nix config
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}