Monorepo for Tangled
tangled.org
1{
2 nixpkgs,
3 system,
4 hostSystem,
5 self,
6}: let
7 envVar = name: let
8 var = builtins.getEnv name;
9 in
10 if var == ""
11 then throw "\$${name} must be defined, see https://docs.tangled.org/hacking-on-tangled.html#hacking-on-tangled for more details"
12 else var;
13 envVarOr = name: default: let
14 var = builtins.getEnv name;
15 in
16 if var != ""
17 then var
18 else default;
19
20 plcUrl = envVarOr "TANGLED_VM_PLC_URL" "https://plc.directory";
21 jetstream = envVarOr "TANGLED_VM_JETSTREAM_ENDPOINT" "wss://jetstream1.us-west.bsky.network/subscribe";
22in
23 nixpkgs.lib.nixosSystem {
24 inherit system;
25 modules = [
26 self.nixosModules.knot
27 self.nixosModules.spindle
28 self.nixosModules.knotmirror
29 ({
30 lib,
31 config,
32 pkgs,
33 ...
34 }: {
35 virtualisation.vmVariant.virtualisation = {
36 host.pkgs = import nixpkgs {system = hostSystem;};
37
38 graphics = false;
39 memorySize = 2048;
40 diskSize = 10 * 1024;
41 cores = 2;
42 forwardPorts = [
43 # ssh
44 {
45 from = "host";
46 host.port = 2222;
47 guest.port = 22;
48 }
49 # knot
50 {
51 from = "host";
52 host.port = 6444;
53 guest.port = 6444;
54 }
55 # spindle
56 {
57 from = "host";
58 host.port = 6555;
59 guest.port = 6555;
60 }
61 # knotmirror
62 {
63 from = "host";
64 host.port = 7007; # 7000 is deserved in macos for Airplay
65 guest.port = 7000;
66 }
67 # knotmirror-tap
68 {
69 from = "host";
70 host.port = 7480;
71 guest.port = 7480;
72 }
73 # knotmirror-admin
74 {
75 from = "host";
76 host.port = 7200;
77 guest.port = 7200;
78 }
79 {
80 from = "host";
81 host.port = 7100;
82 guest.port = 7100;
83 }
84 {
85 from = "host";
86 host.port = 6314;
87 guest.port = 6314;
88 }
89 ];
90 sharedDirectories = {
91 # We can't use the 9p mounts directly for most of these
92 # as SQLite is incompatible with them. So instead we
93 # mount the shared directories to a different location
94 # and copy the contents around on service start/stop.
95 knotData = {
96 source = "$TANGLED_VM_DATA_DIR/knot";
97 target = "/mnt/knot-data";
98 };
99 spindleData = {
100 source = "$TANGLED_VM_DATA_DIR/spindle";
101 target = "/mnt/spindle-data";
102 };
103 spindleLogs = {
104 source = "$TANGLED_VM_DATA_DIR/spindle-logs";
105 target = "/var/log/spindle";
106 };
107 };
108 };
109 # 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
110 networking.firewall.enable = false;
111 services.timesyncd.enable = lib.mkForce true;
112 time.timeZone = "Europe/London";
113 services.getty.autologinUser = "root";
114 environment.systemPackages = with pkgs; [curl vim git sqlite litecli postgresql_14];
115 services.tangled.knot = {
116 enable = true;
117 motd = "Welcome to the development knot!\n";
118 server = {
119 secureMode = false;
120 owner = envVar "TANGLED_VM_KNOT_OWNER";
121 hostname = envVarOr "TANGLED_VM_KNOT_HOST" "localhost:6444";
122 plcUrl = plcUrl;
123 jetstreamEndpoint = jetstream;
124 listenAddr = "0.0.0.0:6444";
125 dev = true;
126 };
127 knotmirrors = [
128 "http://localhost:7000"
129 ];
130 };
131 services.tangled.spindle = {
132 enable = true;
133 server = {
134 owner = envVar "TANGLED_VM_SPINDLE_OWNER";
135 hostname = envVarOr "TANGLED_VM_SPINDLE_HOST" "localhost:6555";
136 plcUrl = plcUrl;
137 jetstreamEndpoint = jetstream;
138 listenAddr = "0.0.0.0:6555";
139 dev = true;
140 queueSize = 100;
141 maxJobCount = 2;
142 secrets = {
143 provider = "sqlite";
144 };
145 };
146
147 pipelines = {
148 logBucket = envVarOr "SPINDLE_S3_LOG_BUCKET" "";
149 };
150 };
151 services.postgresql = {
152 enable = true;
153 package = pkgs.postgresql_14;
154 ensureDatabases = ["mirror" "tap"];
155 ensureUsers = [
156 {name = "tnglr";}
157 ];
158 authentication = ''
159 local all tnglr trust
160 host all tnglr 127.0.0.1/32 trust
161 '';
162 };
163 services.porxie = {
164 enable = true;
165 settings = {
166 PORXIE_SERVER_ADDRESS = "ip:0.0.0.0:6314";
167 PORXIE_BLOB_ALLOWED_MIMETYPES = ["application/gzip" "image/*" "text/plain"];
168 PORXIE_IDENTITY_PLC_URL = plcUrl;
169 };
170 };
171 services.tangled.knotmirror = {
172 enable = true;
173 knotSSRF = false;
174 listenAddr = "0.0.0.0:7000";
175 metricsListenAddr = "0.0.0.0:7100";
176 adminListenAddr = "0.0.0.0:7200";
177 hostname = "localhost:7000";
178 dbUrl = "postgresql://tnglr@127.0.0.1:5432/mirror";
179 fullNetwork = false;
180 tap.dbUrl = "postgresql://tnglr@127.0.0.1:5432/tap";
181 };
182 users = {
183 # So we don't have to deal with permission clashing between
184 # blank disk VMs and existing state
185 users.${config.services.tangled.knot.gitUser}.uid = 666;
186 groups.${config.services.tangled.knot.gitUser}.gid = 666;
187
188 # TODO: separate spindle user
189 };
190 systemd.services = let
191 mkDataSyncScripts = source: target: {
192 enableStrictShellChecks = true;
193
194 preStart = lib.mkBefore ''
195 mkdir -p ${target}
196 ${lib.getExe pkgs.rsync} -a ${source}/ ${target}
197 '';
198
199 postStop = lib.mkAfter ''
200 ${lib.getExe pkgs.rsync} -a ${target}/ ${source}
201 '';
202
203 serviceConfig.PermissionsStartOnly = true;
204 };
205 in {
206 knot = mkDataSyncScripts "/mnt/knot-data" config.services.tangled.knot.stateDir;
207 spindle = mkDataSyncScripts "/mnt/spindle-data" (builtins.dirOf config.services.tangled.spindle.server.dbPath);
208 knotmirror.after = ["postgresql.target"];
209 tap-knotmirror.after = ["postgresql.target"];
210 };
211 })
212 ];
213 }