Monorepo for Tangled
tangled.org
1{
2 nixpkgs,
3 system,
4 hostSystem,
5 self,
6}: let
7 lib = nixpkgs.lib;
8
9 envVar = name: let
10 var = builtins.getEnv name;
11 in
12 if var == ""
13 then throw "\$${name} must be defined, see https://docs.tangled.org/hacking-on-tangled.html#hacking-on-tangled for more details"
14 else var;
15 envVarOr = name: default: let
16 var = builtins.getEnv name;
17 in
18 if var != ""
19 then var
20 else default;
21
22 plcUrl = envVarOr "TANGLED_VM_PLC_URL" "https://plc.directory";
23 jetstream = envVarOr "TANGLED_VM_JETSTREAM_ENDPOINT" "wss://jetstream1.us-west.bsky.network/subscribe";
24
25 checkFile = value: path:
26 if builtins.pathExists path
27 then lib.hasPrefix value (builtins.readFile path)
28 else false;
29 _nestedVirt =
30 (checkFile "1" /sys/module/kvm_amd/parameters/nested)
31 || (checkFile "Y" /sys/module/kvm_intel/parameters/nested);
32 nestedVirtWarning = ''
33 KVM nested virtualisation is not enabled on this host.
34 You should enable it if you can for better performance when testing the QEMU spindle engine!
35 '';
36 nestedVirt = lib.warnIf (!_nestedVirt) nestedVirtWarning _nestedVirt;
37in
38 lib.nixosSystem {
39 inherit system;
40 modules = [
41 self.nixosModules.knot
42 self.nixosModules.spindle
43 self.nixosModules.knotmirror
44 ({
45 lib,
46 config,
47 pkgs,
48 ...
49 }: {
50 virtualisation.vmVariant.virtualisation = {
51 host.pkgs = import nixpkgs {system = hostSystem;};
52
53 graphics = false;
54 memorySize = 3072;
55 diskSize = 20 * 1024;
56 cores = 2;
57 qemu.options = lib.optionals nestedVirt ["-enable-kvm" "-cpu host"];
58
59 forwardPorts = [
60 # ssh
61 {
62 from = "host";
63 host.port = 2222;
64 guest.port = 22;
65 }
66 # knot
67 {
68 from = "host";
69 host.port = 6444;
70 guest.port = 6444;
71 }
72 # spindle
73 {
74 from = "host";
75 host.port = 6555;
76 guest.port = 6555;
77 }
78 # knotmirror
79 {
80 from = "host";
81 host.port = 7007; # 7000 is deserved in macos for Airplay
82 guest.port = 7000;
83 }
84 # knotmirror-tap
85 {
86 from = "host";
87 host.port = 7480;
88 guest.port = 7480;
89 }
90 # knotmirror-admin
91 {
92 from = "host";
93 host.port = 7200;
94 guest.port = 7200;
95 }
96 {
97 from = "host";
98 host.port = 7100;
99 guest.port = 7100;
100 }
101 ];
102 sharedDirectories = {
103 # We can't use the 9p mounts directly for most of these
104 # as SQLite is incompatible with them. So instead we
105 # mount the shared directories to a different location
106 # and copy the contents around on service start/stop.
107 knotData = {
108 source = "$TANGLED_VM_DATA_DIR/knot";
109 target = "/mnt/knot-data";
110 };
111 spindleData = {
112 source = "$TANGLED_VM_DATA_DIR/spindle";
113 target = "/mnt/spindle-data";
114 };
115 spindleLogs = {
116 source = "$TANGLED_VM_DATA_DIR/spindle-logs";
117 target = "/var/log/spindle";
118 };
119 };
120 };
121 systemd.tmpfiles.rules = [
122 "L+ /var/lib/spindle/images/nixos-x86_64 - - - - ${self.packages.${system}.spindle-nixos-image}"
123 "L+ /var/lib/spindle/images/nixos - - - - /var/lib/spindle/images/nixos-x86_64"
124 "L+ /var/lib/spindle/images/alpine-x86_64 - - - - ${self.packages.${system}.spindle-alpine-image}"
125 "L+ /var/lib/spindle/images/alpine - - - - /var/lib/spindle/images/alpine-x86_64"
126 ];
127 # This is fine because any and all ports that are forwarded to host are explicitly marked above, we don't need a separate guest firewall
128 networking.firewall.enable = false;
129 services.timesyncd.enable = lib.mkForce true;
130 time.timeZone = "Europe/London";
131 services.getty.autologinUser = "root";
132 environment.systemPackages = with pkgs; [curl vim git sqlite litecli postgresql_14];
133 services.tangled.knot = {
134 enable = true;
135 motd = "Welcome to the development knot!\n";
136 server = {
137 secureMode = false;
138 owner = envVar "TANGLED_VM_KNOT_OWNER";
139 hostname = envVarOr "TANGLED_VM_KNOT_HOST" "localhost:6444";
140 plcUrl = plcUrl;
141 jetstreamEndpoint = jetstream;
142 listenAddr = "0.0.0.0:6444";
143 dev = true;
144 };
145 knotmirrors = [
146 "http://localhost:7000"
147 ];
148 };
149 services.tangled.spindle = {
150 enable = true;
151 server = {
152 owner = envVar "TANGLED_VM_SPINDLE_OWNER";
153 hostname = envVarOr "TANGLED_VM_SPINDLE_HOST" "localhost:6555";
154 plcUrl = plcUrl;
155 jetstreamEndpoint = jetstream;
156 listenAddr = "0.0.0.0:6555";
157 dev = true;
158 queueSize = 100;
159 maxJobCount = 2;
160 secrets = {
161 provider = "sqlite";
162 };
163 };
164
165 pipelines = {
166 logBucket = envVarOr "SPINDLE_S3_LOG_BUCKET" "";
167 microvm = {
168 enableKVM = nestedVirt;
169 };
170 };
171
172 cache = {
173 readUrls = ["http://127.0.0.1:8501"];
174 trustedPublicKeys = ["cache.local:F7YqpMzuBdILYd/v+wMZN2YKxCzliXQyFmeezOxw7rU="];
175 uploadUrl = "http://127.0.0.1:8501/upload";
176 };
177 };
178 services.ncps = {
179 enable = true;
180 cache = {
181 allowPutVerb = true;
182 allowDeleteVerb = true;
183 hostName = "cache.local";
184 secretKeyPath = pkgs.writeText "ncps-secret-key" "cache.local:hay0+jvBNguou2tNt19FvrBCogHwHc+mqQe3bww5ZX4XtiqkzO4F0gth3+/7Axk3ZgrELOWJdDIWZ57M7HDutQ==";
185 upstream = {
186 urls = ["https://cache.nixos.org"];
187 publicKeys = ["cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="];
188 };
189 };
190 server.addr = "127.0.0.1:8501";
191 };
192 services.postgresql = {
193 enable = true;
194 package = pkgs.postgresql_14;
195 ensureDatabases = ["mirror" "tap"];
196 ensureUsers = [
197 {name = "tnglr";}
198 ];
199 authentication = ''
200 local all tnglr trust
201 host all tnglr 127.0.0.1/32 trust
202 '';
203 };
204 services.tangled.knotmirror = {
205 enable = true;
206 knotSSRF = false;
207 listenAddr = "0.0.0.0:7000";
208 metricsListenAddr = "0.0.0.0:7100";
209 adminListenAddr = "0.0.0.0:7200";
210 hostname = "localhost:7000";
211 dbUrl = "postgresql://tnglr@127.0.0.1:5432/mirror";
212 fullNetwork = false;
213 tap.dbUrl = "postgresql://tnglr@127.0.0.1:5432/tap";
214 };
215 users = {
216 # So we don't have to deal with permission clashing between
217 # blank disk VMs and existing state
218 users.${config.services.tangled.knot.gitUser}.uid = 666;
219 groups.${config.services.tangled.knot.gitUser}.gid = 666;
220
221 # TODO: separate spindle user
222 };
223 systemd.services = let
224 mkDataSyncScripts = source: target: {
225 enableStrictShellChecks = true;
226
227 preStart = lib.mkBefore ''
228 mkdir -p ${target}
229 ${lib.getExe pkgs.rsync} -a ${source}/ ${target}
230 '';
231
232 postStop = lib.mkAfter ''
233 ${lib.getExe pkgs.rsync} -a ${target}/ ${source}
234 '';
235
236 serviceConfig.PermissionsStartOnly = true;
237 };
238 in {
239 knot = mkDataSyncScripts "/mnt/knot-data" config.services.tangled.knot.stateDir;
240 spindle = mkDataSyncScripts "/mnt/spindle-data" (dirOf config.services.tangled.spindle.server.dbPath);
241 knotmirror.after = ["postgresql.target"];
242 tap-knotmirror.after = ["postgresql.target"];
243 };
244 })
245 ];
246 }