Monorepo for Tangled
tangled.org
1{
2 config,
3 lib,
4 ...
5}: let
6 cfg = config.services.tangled.spindle;
7in
8 with lib; {
9 options = {
10 services.tangled.spindle = {
11 enable = mkOption {
12 type = types.bool;
13 default = false;
14 description = "Enable a tangled spindle";
15 };
16 package = mkOption {
17 type = types.package;
18 description = "Package to use for the spindle";
19 };
20
21 server = {
22 listenAddr = mkOption {
23 type = types.str;
24 default = "0.0.0.0:6555";
25 description = "Address to listen on";
26 };
27
28 dbPath = mkOption {
29 type = types.path;
30 default = "/var/lib/spindle/spindle.db";
31 description = "Path to the database file";
32 };
33
34 hostname = mkOption {
35 type = types.str;
36 example = "my.spindle.com";
37 description = "Hostname for the server (required)";
38 };
39
40 plcUrl = mkOption {
41 type = types.str;
42 default = "https://plc.directory";
43 description = "atproto PLC directory";
44 };
45
46 jetstreamEndpoint = mkOption {
47 type = types.str;
48 default = "wss://jetstream1.us-west.bsky.network/subscribe";
49 description = "Jetstream endpoint to subscribe to";
50 };
51
52 dev = mkOption {
53 type = types.bool;
54 default = false;
55 description = "Enable development mode (disables signature verification)";
56 };
57
58 owner = mkOption {
59 type = types.str;
60 example = "did:plc:qfpnj4og54vl56wngdriaxug";
61 description = "DID of owner (required)";
62 };
63
64 maxJobCount = mkOption {
65 type = types.int;
66 default = 2;
67 example = 5;
68 description = "Maximum number of concurrent jobs to run";
69 };
70
71 queueSize = mkOption {
72 type = types.int;
73 default = 100;
74 example = 100;
75 description = "Maximum number of jobs queue up";
76 };
77
78 maxConcurrentWorkflows = mkOption {
79 type = types.int;
80 default = 8;
81 description = "Maximum number of workflow containers running simultaneously (controls total memory usage)";
82 };
83
84 secrets = {
85 provider = mkOption {
86 type = types.str;
87 default = "sqlite";
88 description = "Backend to use for secret management, valid options are 'sqlite', and 'openbao'.";
89 };
90
91 openbao = {
92 proxyAddr = mkOption {
93 type = types.str;
94 default = "http://127.0.0.1:8200";
95 description = "Address of the OpenBAO proxy server";
96 };
97 mount = mkOption {
98 type = types.str;
99 default = "spindle";
100 description = "Mount path in OpenBAO to read secrets from";
101 };
102 };
103 };
104
105 tap = {
106 embed = mkOption {
107 type = types.bool;
108 default = true;
109 description = "Run an embedded tap inside the spindle process";
110 };
111
112 url = mkOption {
113 type = types.str;
114 default = "http://[::1]:2480";
115 description = "URL the spindle's tap client dials";
116 };
117
118 bind = mkOption {
119 type = types.str;
120 default = "[::1]:2480";
121 description = "Loopback address the embedded tap server listens on";
122 };
123
124 dbPath = mkOption {
125 type = types.path;
126 default = "/var/lib/spindle/tap.db";
127 description = "Path to the embedded tap sqlite database";
128 };
129
130 relayUrl = mkOption {
131 type = types.str;
132 default = "https://bsky.network";
133 description = "Relay used by the embedded tap firehose";
134 };
135 };
136 };
137
138 pipelines = {
139 nixery = mkOption {
140 type = types.str;
141 default = "nixery.tangled.sh"; # note: this is *not* on tangled.org yet
142 description = "Nixery instance to use";
143 };
144
145 workflowTimeout = mkOption {
146 type = types.str;
147 default = "5m";
148 description = "Timeout for each step of a pipeline";
149 };
150
151 maxJobMemoryMb = mkOption {
152 type = types.int;
153 default = 6144;
154 description = "Memory limit per workflow container in MiB (default 6 GiB)";
155 };
156
157 logBucket = mkOption {
158 type = types.str;
159 default = "tangled-logs";
160 description = "S3 bucket for workflow logs";
161 };
162 };
163
164 environmentFile = mkOption {
165 type = with types; nullOr path;
166 default = null;
167 example = "/etc/spindle.env";
168 description = ''
169 Additional environment file as defined in {manpage}`systemd.exec(5)`.
170
171 Sensitive secrets such as {env}`AWS_SECRET_ACCESS_KEY`,
172 {env}`AWS_ACCESS_KEY_ID`, {env}`AWS_REGION`
173 may be passed to the service
174 without making them world readable in the nix store.
175 '';
176 };
177 };
178 };
179
180 config = mkIf cfg.enable {
181 virtualisation.docker.enable = true;
182
183 systemd.services.spindle = {
184 description = "spindle service";
185 after = ["network.target" "docker.service"];
186 wantedBy = ["multi-user.target"];
187 serviceConfig = {
188 LogsDirectory = "spindle";
189 StateDirectory = "spindle";
190 EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
191
192 Environment = [
193 "SPINDLE_SERVER_LISTEN_ADDR=${cfg.server.listenAddr}"
194 "SPINDLE_SERVER_DB_PATH=${cfg.server.dbPath}"
195 "SPINDLE_SERVER_HOSTNAME=${cfg.server.hostname}"
196 "SPINDLE_SERVER_PLC_URL=${cfg.server.plcUrl}"
197 "SPINDLE_SERVER_JETSTREAM_ENDPOINT=${cfg.server.jetstreamEndpoint}"
198 "SPINDLE_SERVER_DEV=${lib.boolToString cfg.server.dev}"
199 "SPINDLE_SERVER_OWNER=${cfg.server.owner}"
200 "SPINDLE_SERVER_MAX_JOB_COUNT=${toString cfg.server.maxJobCount}"
201 "SPINDLE_SERVER_QUEUE_SIZE=${toString cfg.server.queueSize}"
202 "SPINDLE_SERVER_MAX_CONCURRENT_WORKFLOWS=${toString cfg.server.maxConcurrentWorkflows}"
203 "SPINDLE_SERVER_SECRETS_PROVIDER=${cfg.server.secrets.provider}"
204 "SPINDLE_SERVER_SECRETS_OPENBAO_PROXY_ADDR=${cfg.server.secrets.openbao.proxyAddr}"
205 "SPINDLE_SERVER_SECRETS_OPENBAO_MOUNT=${cfg.server.secrets.openbao.mount}"
206 "SPINDLE_SERVER_TAP_EMBED=${lib.boolToString cfg.server.tap.embed}"
207 "SPINDLE_SERVER_TAP_URL=${cfg.server.tap.url}"
208 "SPINDLE_SERVER_TAP_BIND=${cfg.server.tap.bind}"
209 "SPINDLE_SERVER_TAP_DB_PATH=${cfg.server.tap.dbPath}"
210 "SPINDLE_SERVER_TAP_RELAY_URL=${cfg.server.tap.relayUrl}"
211 "SPINDLE_NIXERY_PIPELINES_NIXERY=${cfg.pipelines.nixery}"
212 "SPINDLE_NIXERY_PIPELINES_WORKFLOW_TIMEOUT=${cfg.pipelines.workflowTimeout}"
213 "SPINDLE_NIXERY_PIPELINES_MAX_JOB_MEMORY_MB=${toString cfg.pipelines.maxJobMemoryMb}"
214 "SPINDLE_S3_LOG_BUCKET=${cfg.pipelines.logBucket}"
215 ];
216 ExecStart = "${cfg.package}/bin/spindle";
217 Restart = "always";
218 };
219 };
220 };
221 }