forked from tangled.org/core
Monorepo for Tangled
1{ 2 config, 3 pkgs, 4 lib, 5 ... 6}: let 7 cfg = config.services.tangled.knot; 8in 9 with lib; { 10 options = { 11 services.tangled.knot = { 12 enable = mkOption { 13 type = types.bool; 14 default = false; 15 description = "Enable a tangled knot"; 16 }; 17 18 package = mkOption { 19 type = types.package; 20 description = "Package to use for the knot"; 21 }; 22 23 appviewEndpoint = mkOption { 24 type = types.str; 25 default = "https://tangled.org"; 26 description = "Appview endpoint"; 27 }; 28 29 gitUser = mkOption { 30 type = types.str; 31 default = "git"; 32 description = "User that hosts git repos and performs git operations"; 33 }; 34 35 openFirewall = mkOption { 36 type = types.bool; 37 default = true; 38 description = "Open port 22 in the firewall for ssh"; 39 }; 40 41 stateDir = mkOption { 42 type = types.path; 43 default = "/home/${cfg.gitUser}"; 44 description = "Tangled knot data directory"; 45 }; 46 47 repo = { 48 scanPath = mkOption { 49 type = types.path; 50 default = cfg.stateDir; 51 description = "Path where repositories are scanned from"; 52 }; 53 54 readme = mkOption { 55 type = types.listOf types.str; 56 default = [ 57 "README.md" 58 "readme.md" 59 "README" 60 "readme" 61 "README.markdown" 62 "readme.markdown" 63 "README.txt" 64 "readme.txt" 65 "README.rst" 66 "readme.rst" 67 "README.org" 68 "readme.org" 69 "README.asciidoc" 70 "readme.asciidoc" 71 ]; 72 description = "List of README filenames to look for (in priority order)"; 73 }; 74 75 mainBranch = mkOption { 76 type = types.str; 77 default = "main"; 78 description = "Default branch name for repositories"; 79 }; 80 }; 81 82 git = { 83 userName = mkOption { 84 type = types.str; 85 default = "Tangled"; 86 description = "Git user name used as committer"; 87 }; 88 89 userEmail = mkOption { 90 type = types.str; 91 default = "noreply@tangled.org"; 92 description = "Git user email used as committer"; 93 }; 94 }; 95 96 motd = mkOption { 97 type = types.nullOr types.str; 98 default = null; 99 description = '' 100 Message of the day 101 102 The contents are shown as-is; eg. you will want to add a newline if 103 setting a non-empty message since the knot won't do this for you. 104 ''; 105 }; 106 107 motdFile = mkOption { 108 type = types.nullOr types.path; 109 default = null; 110 description = '' 111 File containing message of the day 112 113 The contents are shown as-is; eg. you will want to add a newline if 114 setting a non-empty message since the knot won't do this for you. 115 ''; 116 }; 117 118 server = { 119 listenAddr = mkOption { 120 type = types.str; 121 default = "0.0.0.0:5555"; 122 description = "Address to listen on"; 123 }; 124 125 internalListenAddr = mkOption { 126 type = types.str; 127 default = "127.0.0.1:5444"; 128 description = "Internal address for inter-service communication"; 129 }; 130 131 owner = mkOption { 132 type = types.str; 133 example = "did:plc:qfpnj4og54vl56wngdriaxug"; 134 description = "DID of owner (required)"; 135 }; 136 137 dbPath = mkOption { 138 type = types.path; 139 default = "${cfg.stateDir}/knotserver.db"; 140 description = "Path to the database file"; 141 }; 142 143 hostname = mkOption { 144 type = types.str; 145 example = "my.knot.com"; 146 description = "Hostname for the server (required)"; 147 }; 148 149 plcUrl = mkOption { 150 type = types.str; 151 default = "https://plc.directory"; 152 description = "atproto PLC directory"; 153 }; 154 155 jetstreamEndpoint = mkOption { 156 type = types.str; 157 default = "wss://jetstream1.us-west.bsky.network/subscribe"; 158 description = "Jetstream endpoint to subscribe to"; 159 }; 160 161 logDids = mkOption { 162 type = types.bool; 163 default = true; 164 description = "Enable logging of DIDs"; 165 }; 166 167 dev = mkOption { 168 type = types.bool; 169 default = false; 170 description = "Enable development mode (disables signature verification)"; 171 }; 172 }; 173 174 environmentFile = mkOption { 175 type = with types; nullOr path; 176 default = null; 177 example = "/etc/appview.env"; 178 description = '' 179 Additional environment file as defined in {manpage}`systemd.exec(5)`. 180 181 Sensitive secrets such as {env}`KNOT_COOKIE_SECRET`, 182 {env}`KNOT_OAUTH_CLIENT_SECRET`, and {env}`KNOT_OAUTH_CLIENT_KID` 183 may be passed to the service without making them world readable in the nix store. 184 ''; 185 }; 186 }; 187 }; 188 189 config = mkIf cfg.enable { 190 environment.systemPackages = [ 191 pkgs.git 192 cfg.package 193 ]; 194 195 users.users.${cfg.gitUser} = { 196 isSystemUser = true; 197 useDefaultShell = true; 198 home = cfg.stateDir; 199 createHome = true; 200 group = cfg.gitUser; 201 }; 202 203 users.groups.${cfg.gitUser} = {}; 204 205 services.openssh = { 206 enable = true; 207 extraConfig = '' 208 Match User ${cfg.gitUser} 209 AuthorizedKeysCommand /etc/ssh/keyfetch_wrapper 210 AuthorizedKeysCommandUser nobody 211 ChallengeResponseAuthentication no 212 PasswordAuthentication no 213 ''; 214 }; 215 216 environment.etc."ssh/keyfetch_wrapper" = { 217 mode = "0555"; 218 text = '' 219 #!${pkgs.stdenv.shell} 220 ${cfg.package}/bin/knot keys \ 221 -config ${cfg.stateDir}/config.yml \ 222 -output authorized-keys 223 ''; 224 }; 225 226 systemd.services.knot = { 227 description = "knot service"; 228 after = ["network.target" "sshd.service"]; 229 wantedBy = ["multi-user.target"]; 230 enableStrictShellChecks = true; 231 232 preStart = let 233 setMotd = 234 if cfg.motdFile != null && cfg.motd != null 235 then throw "motdFile and motd cannot be both set" 236 else '' 237 ${optionalString (cfg.motdFile != null) "cat ${cfg.motdFile} > ${cfg.stateDir}/motd"} 238 ${optionalString (cfg.motd != null) ''printf "${cfg.motd}" > ${cfg.stateDir}/motd''} 239 ''; 240 in '' 241 mkdir -p "${cfg.repo.scanPath}" 242 chown -R ${cfg.gitUser}:${cfg.gitUser} "${cfg.repo.scanPath}" 243 244 mkdir -p "${cfg.stateDir}/.config/git" 245 cat > "${cfg.stateDir}/.config/git/config" << EOF 246 [user] 247 name = ${cfg.git.userName} 248 email = ${cfg.git.userEmail} 249 [receive] 250 advertisePushOptions = true 251 [uploadpack] 252 allowFilter = true 253 EOF 254 ${setMotd} 255 chown -R ${cfg.gitUser}:${cfg.gitUser} "${cfg.stateDir}" 256 ''; 257 258 serviceConfig = { 259 User = cfg.gitUser; 260 PermissionsStartOnly = true; 261 WorkingDirectory = cfg.stateDir; 262 Environment = [ 263 "KNOT_REPO_SCAN_PATH=${cfg.repo.scanPath}" 264 "KNOT_REPO_README=${concatStringsSep "," cfg.repo.readme}" 265 "KNOT_REPO_MAIN_BRANCH=${cfg.repo.mainBranch}" 266 "KNOT_GIT_USER_NAME=${cfg.git.userName}" 267 "KNOT_GIT_USER_EMAIL=${cfg.git.userEmail}" 268 "APPVIEW_ENDPOINT=${cfg.appviewEndpoint}" 269 "KNOT_SERVER_INTERNAL_LISTEN_ADDR=${cfg.server.internalListenAddr}" 270 "KNOT_SERVER_LISTEN_ADDR=${cfg.server.listenAddr}" 271 "KNOT_SERVER_DB_PATH=${cfg.server.dbPath}" 272 "KNOT_SERVER_HOSTNAME=${cfg.server.hostname}" 273 "KNOT_SERVER_PLC_URL=${cfg.server.plcUrl}" 274 "KNOT_SERVER_JETSTREAM_ENDPOINT=${cfg.server.jetstreamEndpoint}" 275 "KNOT_SERVER_OWNER=${cfg.server.owner}" 276 "KNOT_SERVER_LOG_DIDS=${ 277 if cfg.server.logDids 278 then "true" 279 else "false" 280 }" 281 "KNOT_SERVER_DEV=${ 282 if cfg.server.dev 283 then "true" 284 else "false" 285 }" 286 ]; 287 EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; 288 ExecStart = "${cfg.package}/bin/knot server -config ${cfg.stateDir}/config.yml"; 289 Restart = "always"; 290 RestartSec = 5; 291 }; 292 }; 293 294 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [22]; 295 }; 296 }