main
1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9let
10 cfg = config.services.harmonia-cache;
11in
12{
13 options = {
14 services.harmonia-cache = {
15 enable = mkEnableOption "Harmonia binary cache server";
16
17 signKeyPath = mkOption {
18 type = types.str;
19 description = ''
20 Path to the signing key for the binary cache.
21 Typically provided by agenix secret.
22 '';
23 };
24
25 port = mkOption {
26 type = types.port;
27 default = 5000;
28 description = ''
29 Port to bind the Harmonia server to.
30 '';
31 };
32
33 workers = mkOption {
34 type = types.int;
35 default = 4;
36 description = ''
37 Number of worker threads for Harmonia.
38 '';
39 };
40
41 maxConnectionRate = mkOption {
42 type = types.int;
43 default = 256;
44 description = ''
45 Maximum connection rate for Harmonia.
46 '';
47 };
48
49 priority = mkOption {
50 type = types.int;
51 default = 30;
52 description = ''
53 Priority of this cache relative to other substituters.
54 Lower values = higher priority.
55 '';
56 };
57
58 builder = {
59 enable = mkEnableOption "Nightly cache pre-population builds";
60
61 systems = mkOption {
62 type = types.listOf types.str;
63 default = [ ];
64 example = [
65 "aomi"
66 "kyushu"
67 ];
68 description = ''
69 List of NixOS system configurations to build nightly.
70 These should be system names from the flake's nixosConfigurations.
71 '';
72 };
73
74 flakePath = mkOption {
75 type = types.str;
76 default = "/home/vincent/src/home";
77 description = ''
78 Path to the flake repository containing system configurations.
79 '';
80 };
81
82 schedule = mkOption {
83 type = types.str;
84 default = "daily";
85 example = "02:00";
86 description = ''
87 When to run the cache builder. Can be a systemd calendar expression
88 or one of: hourly, daily, weekly, monthly.
89 '';
90 };
91
92 notification = {
93 enable = mkOption {
94 type = types.bool;
95 default = false;
96 description = "Enable ntfy notifications for build status";
97 };
98
99 ntfyUrl = mkOption {
100 type = types.str;
101 default = "https://ntfy.sbr.pm";
102 description = "ntfy server URL";
103 };
104
105 topic = mkOption {
106 type = types.str;
107 default = "builds";
108 description = "ntfy topic to publish to";
109 };
110
111 tokenFile = mkOption {
112 type = types.nullOr types.path;
113 default = null;
114 description = "Path to file containing ntfy auth token";
115 };
116 };
117 };
118 };
119 };
120
121 config = mkIf cfg.enable (mkMerge [
122 # Harmonia cache server
123 {
124 services.harmonia = {
125 enable = true;
126 signKeyPaths = [ cfg.signKeyPath ];
127 settings = {
128 bind = "[::]:${toString cfg.port}";
129 inherit (cfg) workers;
130 max_connection_rate = cfg.maxConnectionRate;
131 inherit (cfg) priority;
132 };
133 };
134
135 networking.firewall.allowedTCPPorts = [ cfg.port ];
136
137 # Ensure the signing key file exists and has correct permissions
138 assertions = [
139 {
140 assertion = cfg.signKeyPath != "";
141 message = "services.harmonia-cache.signKeyPath must be set";
142 }
143 ];
144 }
145
146 # Cache pre-population builder
147 (mkIf cfg.builder.enable {
148 assertions = [
149 {
150 assertion = cfg.builder.systems != [ ];
151 message = "services.harmonia-cache.builder.systems must not be empty when builder is enabled";
152 }
153 ];
154
155 systemd.services.harmonia-cache-builder = {
156 description = "Build NixOS systems for cache pre-population";
157 wants = [ "network-online.target" ];
158 after = [
159 "network-online.target"
160 "harmonia.service"
161 ];
162
163 serviceConfig = {
164 Type = "oneshot";
165 User = "vincent";
166 # Set a reasonable timeout (2 hours)
167 TimeoutStartSec = "2h";
168 };
169
170 path = with pkgs; [
171 openssh
172 git
173 nix
174 nixos-rebuild
175 ];
176
177 script =
178 let
179 notifyStart = optionalString cfg.builder.notification.enable ''
180 ${pkgs.curl}/bin/curl -X POST \
181 ${
182 optionalString (cfg.builder.notification.tokenFile != null)
183 ''-H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${cfg.builder.notification.tokenFile})"''
184 } \
185 -H "Title: Cache Builder Starting (${config.networking.hostName})" \
186 -d "Building ${toString (length cfg.builder.systems)} system(s) for cache pre-population" \
187 ${cfg.builder.notification.ntfyUrl}/${cfg.builder.notification.topic} || true
188 '';
189
190 notifySuccess = optionalString cfg.builder.notification.enable ''
191 ${pkgs.curl}/bin/curl -X POST \
192 ${
193 optionalString (cfg.builder.notification.tokenFile != null)
194 ''-H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${cfg.builder.notification.tokenFile})"''
195 } \
196 -H "Title: Cache Builder Complete (${config.networking.hostName})" \
197 -H "Tags: white_check_mark" \
198 -d "Successfully built ${toString (length cfg.builder.systems)} system(s)" \
199 ${cfg.builder.notification.ntfyUrl}/${cfg.builder.notification.topic} || true
200 '';
201
202 notifyFailure = optionalString cfg.builder.notification.enable ''
203 ${pkgs.curl}/bin/curl -X POST \
204 ${
205 optionalString (cfg.builder.notification.tokenFile != null)
206 ''-H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${cfg.builder.notification.tokenFile})"''
207 } \
208 -H "Title: Cache Builder Failed (${config.networking.hostName})" \
209 -H "Tags: x,warning" \
210 -H "Priority: high" \
211 -d "Failed to build systems. Check logs: journalctl -u harmonia-cache-builder.service" \
212 ${cfg.builder.notification.ntfyUrl}/${cfg.builder.notification.topic} || true
213 '';
214
215 buildSystems = concatMapStringsSep "\n" (system: ''
216 echo "Building system: ${system}"
217 ${pkgs.nixos-rebuild}/bin/nixos-rebuild build --flake ${cfg.builder.flakePath}#${system} || {
218 echo "Failed to build ${system}"
219 ${notifyFailure}
220 exit 1
221 }
222 '') cfg.builder.systems;
223 in
224 ''
225 set -euo pipefail
226
227 ${notifyStart}
228
229 cd ${cfg.builder.flakePath}
230
231 # Update flake inputs
232 echo "Updating flake inputs..."
233 ${pkgs.git}/bin/git pull || echo "Warning: Failed to pull latest changes"
234
235 ${buildSystems}
236
237 echo "All systems built successfully!"
238 ${notifySuccess}
239 '';
240 };
241
242 systemd.timers.harmonia-cache-builder = {
243 description = "Timer for harmonia cache pre-population";
244 wantedBy = [ "timers.target" ];
245
246 timerConfig = {
247 OnCalendar = cfg.builder.schedule;
248 Persistent = true;
249 RandomizedDelaySec = "30m";
250 };
251 };
252 })
253 ]);
254}