nix/modules: add complete appview config paramaters #778

merged
opened by anirudh.fi targeting master from push-xvuzrrtnyqmx

Secrets alone can be loaded from the environmentFile.

Signed-off-by: Anirudh Oppiliappan anirudh@tangled.org

Changed files
+270 -10
nix
modules
+270 -10
nix/modules/appview.nix
··· 13 13 default = false; 14 14 description = "Enable tangled appview"; 15 15 }; 16 + 16 17 package = mkOption { 17 18 type = types.package; 18 19 description = "Package to use for the appview"; 19 20 }; 21 + 22 + # core configuration 20 23 port = mkOption { 21 - type = types.int; 24 + type = types.port; 22 25 default = 3000; 23 26 description = "Port to run the appview on"; 24 27 }; 28 + 29 + listenAddr = mkOption { 30 + type = types.str; 31 + default = "0.0.0.0:${toString cfg.port}"; 32 + description = "Listen address for the appview service"; 33 + }; 34 + 35 + dbPath = mkOption { 36 + type = types.str; 37 + default = "/var/lib/appview/appview.db"; 38 + description = "Path to the SQLite database file"; 39 + }; 40 + 41 + appviewHost = mkOption { 42 + type = types.str; 43 + default = "https://tangled.org"; 44 + example = "https://example.com"; 45 + description = "Public host URL for the appview instance"; 46 + }; 47 + 48 + appviewName = mkOption { 49 + type = types.str; 50 + default = "Tangled"; 51 + description = "Display name for the appview instance"; 52 + }; 53 + 54 + dev = mkOption { 55 + type = types.bool; 56 + default = false; 57 + description = "Enable development mode"; 58 + }; 59 + 60 + disallowedNicknamesFile = mkOption { 61 + type = types.nullOr types.path; 62 + default = null; 63 + description = "Path to file containing disallowed nicknames"; 64 + }; 65 + 66 + # redis configuration 67 + redis = { 68 + addr = mkOption { 69 + type = types.str; 70 + default = "localhost:6379"; 71 + description = "Redis server address"; 72 + }; 73 + 74 + db = mkOption { 75 + type = types.int; 76 + default = 0; 77 + description = "Redis database number"; 78 + }; 79 + }; 80 + 81 + # jetstream configuration 82 + jetstream = { 83 + endpoint = mkOption { 84 + type = types.str; 85 + default = "wss://jetstream1.us-east.bsky.network/subscribe"; 86 + description = "Jetstream WebSocket endpoint"; 87 + }; 88 + }; 89 + 90 + # knotstream consumer configuration 91 + knotstream = { 92 + retryInterval = mkOption { 93 + type = types.str; 94 + default = "60s"; 95 + description = "Initial retry interval for knotstream consumer"; 96 + }; 97 + 98 + maxRetryInterval = mkOption { 99 + type = types.str; 100 + default = "120m"; 101 + description = "Maximum retry interval for knotstream consumer"; 102 + }; 103 + 104 + connectionTimeout = mkOption { 105 + type = types.str; 106 + default = "5s"; 107 + description = "Connection timeout for knotstream consumer"; 108 + }; 109 + 110 + workerCount = mkOption { 111 + type = types.int; 112 + default = 64; 113 + description = "Number of workers for knotstream consumer"; 114 + }; 115 + 116 + queueSize = mkOption { 117 + type = types.int; 118 + default = 100; 119 + description = "Queue size for knotstream consumer"; 120 + }; 121 + }; 122 + 123 + # spindlestream consumer configuration 124 + spindlestream = { 125 + retryInterval = mkOption { 126 + type = types.str; 127 + default = "60s"; 128 + description = "Initial retry interval for spindlestream consumer"; 129 + }; 130 + 131 + maxRetryInterval = mkOption { 132 + type = types.str; 133 + default = "120m"; 134 + description = "Maximum retry interval for spindlestream consumer"; 135 + }; 136 + 137 + connectionTimeout = mkOption { 138 + type = types.str; 139 + default = "5s"; 140 + description = "Connection timeout for spindlestream consumer"; 141 + }; 142 + 143 + workerCount = mkOption { 144 + type = types.int; 145 + default = 64; 146 + description = "Number of workers for spindlestream consumer"; 147 + }; 148 + 149 + queueSize = mkOption { 150 + type = types.int; 151 + default = 100; 152 + description = "Queue size for spindlestream consumer"; 153 + }; 154 + }; 155 + 156 + # resend configuration 157 + resend = { 158 + sentFrom = mkOption { 159 + type = types.str; 160 + default = "noreply@notifs.tangled.sh"; 161 + description = "Email address to send notifications from"; 162 + }; 163 + }; 164 + 165 + # posthog configuration 166 + posthog = { 167 + endpoint = mkOption { 168 + type = types.str; 169 + default = "https://eu.i.posthog.com"; 170 + description = "PostHog API endpoint"; 171 + }; 172 + }; 173 + 174 + # camo configuration 175 + camo = { 176 + host = mkOption { 177 + type = types.str; 178 + default = "https://camo.tangled.sh"; 179 + description = "Camo proxy host URL"; 180 + }; 181 + }; 182 + 183 + # avatar configuration 184 + avatar = { 185 + host = mkOption { 186 + type = types.str; 187 + default = "https://avatar.tangled.sh"; 188 + description = "Avatar service host URL"; 189 + }; 190 + }; 191 + 192 + plc = { 193 + url = mkOption { 194 + type = types.str; 195 + default = "https://plc.directory"; 196 + description = "PLC directory URL"; 197 + }; 198 + }; 199 + 200 + pds = { 201 + host = mkOption { 202 + type = types.str; 203 + default = "https://tngl.sh"; 204 + description = "PDS host URL"; 205 + }; 206 + }; 207 + 208 + label = { 209 + defaults = mkOption { 210 + type = types.listOf types.str; 211 + default = [ 212 + "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/wontfix" 213 + "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/good-first-issue" 214 + "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/duplicate" 215 + "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/documentation" 216 + "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/assignee" 217 + ]; 218 + description = "Default label definitions"; 219 + }; 220 + 221 + goodFirstIssue = mkOption { 222 + type = types.str; 223 + default = "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/good-first-issue"; 224 + description = "Good first issue label definition"; 225 + }; 226 + }; 227 + 25 228 environmentFile = mkOption { 26 229 type = with types; nullOr path; 27 230 default = null; 28 - example = "/etc-/appview.env"; 231 + example = "/etc/appview.env"; 29 232 description = '' 30 233 Additional environment file as defined in {manpage}`systemd.exec(5)`. 31 234 32 - Sensitive secrets such as {env}`TANGLED_COOKIE_SECRET` may be 33 - passed to the service without makeing them world readable in the 34 - nix store. 35 - 235 + Sensitive secrets such as {env}`TANGLED_COOKIE_SECRET`, 236 + {env}`TANGLED_OAUTH_CLIENT_SECRET`, {env}`TANGLED_RESEND_API_KEY`, 237 + {env}`TANGLED_CAMO_SHARED_SECRET`, {env}`TANGLED_AVATAR_SHARED_SECRET`, 238 + {env}`TANGLED_REDIS_PASS`, {env}`TANGLED_PDS_ADMIN_SECRET`, 239 + {env}`TANGLED_CLOUDFLARE_API_TOKEN`, {env}`TANGLED_CLOUDFLARE_ZONE_ID`, 240 + {env}`TANGLED_CLOUDFLARE_TURNSTILE_SITE_KEY`, 241 + {env}`TANGLED_CLOUDFLARE_TURNSTILE_SECRET_KEY`, 242 + {env}`TANGLED_POSTHOG_API_KEY`, {env}`TANGLED_APP_PASSWORD`, 243 + and {env}`TANGLED_ALT_APP_PASSWORD` may be passed to the service 244 + without making them world readable in the nix store. 36 245 ''; 37 246 }; 38 247 }; ··· 47 256 systemd.services.appview = { 48 257 description = "tangled appview service"; 49 258 wantedBy = ["multi-user.target"]; 50 - after = ["redis-appview.service"]; 259 + after = ["redis-appview.service" "network-online.target"]; 51 260 requires = ["redis-appview.service"]; 261 + wants = ["network-online.target"]; 52 262 53 263 serviceConfig = { 54 - ListenStream = "0.0.0.0:${toString cfg.port}"; 264 + Type = "simple"; 55 265 ExecStart = "${cfg.package}/bin/appview"; 56 266 Restart = "always"; 57 - EnvironmentFile = optional (cfg.environmentFile != null) cfg.environmentFile; 267 + RestartSec = "10s"; 268 + EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; 269 + 270 + # state directory 271 + StateDirectory = "appview"; 272 + WorkingDirectory = "/var/lib/appview"; 273 + 274 + # security hardening 275 + NoNewPrivileges = true; 276 + PrivateTmp = true; 277 + ProtectSystem = "strict"; 278 + ProtectHome = true; 279 + ReadWritePaths = ["/var/lib/appview"]; 58 280 }; 59 281 60 282 environment = { 61 - TANGLED_DB_PATH = "appview.db"; 283 + TANGLED_DB_PATH = cfg.dbPath; 284 + TANGLED_LISTEN_ADDR = cfg.listenAddr; 285 + TANGLED_APPVIEW_HOST = cfg.appviewHost; 286 + TANGLED_APPVIEW_NAME = cfg.appviewName; 287 + TANGLED_DEV = if cfg.dev then "true" else "false"; 288 + } // optionalAttrs (cfg.disallowedNicknamesFile != null) { 289 + TANGLED_DISALLOWED_NICKNAMES_FILE = cfg.disallowedNicknamesFile; 290 + } // { 291 + TANGLED_REDIS_ADDR = cfg.redis.addr; 292 + TANGLED_REDIS_DB = toString cfg.redis.db; 293 + 294 + TANGLED_JETSTREAM_ENDPOINT = cfg.jetstream.endpoint; 295 + 296 + TANGLED_KNOTSTREAM_RETRY_INTERVAL = cfg.knotstream.retryInterval; 297 + TANGLED_KNOTSTREAM_MAX_RETRY_INTERVAL = cfg.knotstream.maxRetryInterval; 298 + TANGLED_KNOTSTREAM_CONNECTION_TIMEOUT = cfg.knotstream.connectionTimeout; 299 + TANGLED_KNOTSTREAM_WORKER_COUNT = toString cfg.knotstream.workerCount; 300 + TANGLED_KNOTSTREAM_QUEUE_SIZE = toString cfg.knotstream.queueSize; 301 + 302 + TANGLED_SPINDLESTREAM_RETRY_INTERVAL = cfg.spindlestream.retryInterval; 303 + TANGLED_SPINDLESTREAM_MAX_RETRY_INTERVAL = cfg.spindlestream.maxRetryInterval; 304 + TANGLED_SPINDLESTREAM_CONNECTION_TIMEOUT = cfg.spindlestream.connectionTimeout; 305 + TANGLED_SPINDLESTREAM_WORKER_COUNT = toString cfg.spindlestream.workerCount; 306 + TANGLED_SPINDLESTREAM_QUEUE_SIZE = toString cfg.spindlestream.queueSize; 307 + 308 + TANGLED_RESEND_SENT_FROM = cfg.resend.sentFrom; 309 + 310 + TANGLED_POSTHOG_ENDPOINT = cfg.posthog.endpoint; 311 + 312 + TANGLED_CAMO_HOST = cfg.camo.host; 313 + 314 + TANGLED_AVATAR_HOST = cfg.avatar.host; 315 + 316 + TANGLED_PLC_URL = cfg.plc.url; 317 + 318 + TANGLED_PDS_HOST = cfg.pds.host; 319 + 320 + TANGLED_LABEL_DEFAULTS = concatStringsSep "," cfg.label.defaults; 321 + TANGLED_LABEL_GFI = cfg.label.goodFirstIssue; 62 322 }; 63 323 }; 64 324 };