python3Packages.orjson: Disable failing tests on 32 bit
[NixPkgs.git] / nixos / modules / services / misc / gitea.nix
blobac598108a01efe8b7d3ce9a5e2c8c464a29f08a3
1 { config, lib, options, pkgs, ... }:
3 with lib;
5 let
6   cfg = config.services.gitea;
7   opt = options.services.gitea;
8   gitea = cfg.package;
9   pg = config.services.postgresql;
10   useMysql = cfg.database.type == "mysql";
11   usePostgresql = cfg.database.type == "postgres";
12   useSqlite = cfg.database.type == "sqlite3";
13   format = pkgs.formats.ini { };
14   configFile = pkgs.writeText "app.ini" ''
15     APP_NAME = ${cfg.appName}
16     RUN_USER = ${cfg.user}
17     RUN_MODE = prod
19     ${generators.toINI {} cfg.settings}
21     ${optionalString (cfg.extraConfig != null) cfg.extraConfig}
22   '';
26   imports = [
27     (mkRenamedOptionModule [ "services" "gitea" "cookieSecure" ] [ "services" "gitea" "settings" "session" "COOKIE_SECURE" ])
28     (mkRenamedOptionModule [ "services" "gitea" "disableRegistration" ] [ "services" "gitea" "settings" "service" "DISABLE_REGISTRATION" ])
29     (mkRenamedOptionModule [ "services" "gitea" "log" "level" ] [ "services" "gitea" "settings" "log" "LEVEL" ])
30     (mkRenamedOptionModule [ "services" "gitea" "log" "rootPath" ] [ "services" "gitea" "settings" "log" "ROOT_PATH" ])
31     (mkRenamedOptionModule [ "services" "gitea" "ssh" "clonePort" ] [ "services" "gitea" "settings" "server" "SSH_PORT" ])
33     (mkRemovedOptionModule [ "services" "gitea" "ssh" "enable" ] "services.gitea.ssh.enable has been migrated into freeform setting services.gitea.settings.server.DISABLE_SSH. Keep in mind that the setting is inverted")
34   ];
36   options = {
37     services.gitea = {
38       enable = mkOption {
39         default = false;
40         type = types.bool;
41         description = lib.mdDoc "Enable Gitea Service.";
42       };
44       package = mkOption {
45         default = pkgs.gitea;
46         type = types.package;
47         defaultText = literalExpression "pkgs.gitea";
48         description = lib.mdDoc "gitea derivation to use";
49       };
51       useWizard = mkOption {
52         default = false;
53         type = types.bool;
54         description = lib.mdDoc "Do not generate a configuration and use gitea' installation wizard instead. The first registered user will be administrator.";
55       };
57       stateDir = mkOption {
58         default = "/var/lib/gitea";
59         type = types.str;
60         description = lib.mdDoc "gitea data directory.";
61       };
63       user = mkOption {
64         type = types.str;
65         default = "gitea";
66         description = lib.mdDoc "User account under which gitea runs.";
67       };
69       database = {
70         type = mkOption {
71           type = types.enum [ "sqlite3" "mysql" "postgres" ];
72           example = "mysql";
73           default = "sqlite3";
74           description = lib.mdDoc "Database engine to use.";
75         };
77         host = mkOption {
78           type = types.str;
79           default = "127.0.0.1";
80           description = lib.mdDoc "Database host address.";
81         };
83         port = mkOption {
84           type = types.port;
85           default = if !usePostgresql then 3306 else pg.port;
86           defaultText = literalExpression ''
87             if config.${opt.database.type} != "postgresql"
88             then 3306
89             else config.${options.services.postgresql.port}
90           '';
91           description = lib.mdDoc "Database host port.";
92         };
94         name = mkOption {
95           type = types.str;
96           default = "gitea";
97           description = lib.mdDoc "Database name.";
98         };
100         user = mkOption {
101           type = types.str;
102           default = "gitea";
103           description = lib.mdDoc "Database user.";
104         };
106         password = mkOption {
107           type = types.str;
108           default = "";
109           description = lib.mdDoc ''
110             The password corresponding to {option}`database.user`.
111             Warning: this is stored in cleartext in the Nix store!
112             Use {option}`database.passwordFile` instead.
113           '';
114         };
116         passwordFile = mkOption {
117           type = types.nullOr types.path;
118           default = null;
119           example = "/run/keys/gitea-dbpassword";
120           description = lib.mdDoc ''
121             A file containing the password corresponding to
122             {option}`database.user`.
123           '';
124         };
126         socket = mkOption {
127           type = types.nullOr types.path;
128           default = if (cfg.database.createDatabase && usePostgresql) then "/run/postgresql" else if (cfg.database.createDatabase && useMysql) then "/run/mysqld/mysqld.sock" else null;
129           defaultText = literalExpression "null";
130           example = "/run/mysqld/mysqld.sock";
131           description = lib.mdDoc "Path to the unix socket file to use for authentication.";
132         };
134         path = mkOption {
135           type = types.str;
136           default = "${cfg.stateDir}/data/gitea.db";
137           defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/gitea.db"'';
138           description = lib.mdDoc "Path to the sqlite3 database file.";
139         };
141         createDatabase = mkOption {
142           type = types.bool;
143           default = true;
144           description = lib.mdDoc "Whether to create a local database automatically.";
145         };
146       };
148       dump = {
149         enable = mkOption {
150           type = types.bool;
151           default = false;
152           description = lib.mdDoc ''
153             Enable a timer that runs gitea dump to generate backup-files of the
154             current gitea database and repositories.
155           '';
156         };
158         interval = mkOption {
159           type = types.str;
160           default = "04:31";
161           example = "hourly";
162           description = lib.mdDoc ''
163             Run a gitea dump at this interval. Runs by default at 04:31 every day.
165             The format is described in
166             {manpage}`systemd.time(7)`.
167           '';
168         };
170         backupDir = mkOption {
171           type = types.str;
172           default = "${cfg.stateDir}/dump";
173           defaultText = literalExpression ''"''${config.${opt.stateDir}}/dump"'';
174           description = lib.mdDoc "Path to the dump files.";
175         };
177         type = mkOption {
178           type = types.enum [ "zip" "rar" "tar" "sz" "tar.gz" "tar.xz" "tar.bz2" "tar.br" "tar.lz4" ];
179           default = "zip";
180           description = lib.mdDoc "Archive format used to store the dump file.";
181         };
183         file = mkOption {
184           type = types.nullOr types.str;
185           default = null;
186           description = lib.mdDoc "Filename to be used for the dump. If `null` a default name is choosen by gitea.";
187           example = "gitea-dump";
188         };
189       };
191       lfs = {
192         enable = mkOption {
193           type = types.bool;
194           default = false;
195           description = lib.mdDoc "Enables git-lfs support.";
196         };
198         contentDir = mkOption {
199           type = types.str;
200           default = "${cfg.stateDir}/data/lfs";
201           defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/lfs"'';
202           description = lib.mdDoc "Where to store LFS files.";
203         };
204       };
206       appName = mkOption {
207         type = types.str;
208         default = "gitea: Gitea Service";
209         description = lib.mdDoc "Application name.";
210       };
212       repositoryRoot = mkOption {
213         type = types.str;
214         default = "${cfg.stateDir}/repositories";
215         defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"'';
216         description = lib.mdDoc "Path to the git repositories.";
217       };
219       domain = mkOption {
220         type = types.str;
221         default = "localhost";
222         description = lib.mdDoc "Domain name of your server.";
223       };
225       rootUrl = mkOption {
226         type = types.str;
227         default = "http://localhost:3000/";
228         description = lib.mdDoc "Full public URL of gitea server.";
229       };
231       httpAddress = mkOption {
232         type = types.str;
233         default = "0.0.0.0";
234         description = lib.mdDoc "HTTP listen address.";
235       };
237       httpPort = mkOption {
238         type = types.int;
239         default = 3000;
240         description = lib.mdDoc "HTTP listen port.";
241       };
243       enableUnixSocket = mkOption {
244         type = types.bool;
245         default = false;
246         description = lib.mdDoc "Configure Gitea to listen on a unix socket instead of the default TCP port.";
247       };
249       staticRootPath = mkOption {
250         type = types.either types.str types.path;
251         default = gitea.data;
252         defaultText = literalExpression "package.data";
253         example = "/var/lib/gitea/data";
254         description = lib.mdDoc "Upper level of template and static files path.";
255       };
257       mailerPasswordFile = mkOption {
258         type = types.nullOr types.str;
259         default = null;
260         example = "/var/lib/secrets/gitea/mailpw";
261         description = lib.mdDoc "Path to a file containing the SMTP password.";
262       };
264       settings = mkOption {
265         default = {};
266         description = lib.mdDoc ''
267           Gitea configuration. Refer to <https://docs.gitea.io/en-us/config-cheat-sheet/>
268           for details on supported values.
269         '';
270         example = literalExpression ''
271           {
272             "cron.sync_external_users" = {
273               RUN_AT_START = true;
274               SCHEDULE = "@every 24h";
275               UPDATE_EXISTING = true;
276             };
277             mailer = {
278               ENABLED = true;
279               MAILER_TYPE = "sendmail";
280               FROM = "do-not-reply@example.org";
281               SENDMAIL_PATH = "''${pkgs.system-sendmail}/bin/sendmail";
282             };
283             other = {
284               SHOW_FOOTER_VERSION = false;
285             };
286           }
287         '';
288         type = with types; submodule {
289           freeformType = format.type;
290           options = {
291             log = {
292               ROOT_PATH = mkOption {
293                 default = "${cfg.stateDir}/log";
294                 defaultText = literalExpression ''"''${config.${opt.stateDir}}/log"'';
295                 type = types.str;
296                 description = lib.mdDoc "Root path for log files.";
297               };
298               LEVEL = mkOption {
299                 default = "Info";
300                 type = types.enum [ "Trace" "Debug" "Info" "Warn" "Error" "Critical" ];
301                 description = lib.mdDoc "General log level.";
302               };
303             };
305             server = {
306               DISABLE_SSH = mkOption {
307                 type = types.bool;
308                 default = false;
309                 description = lib.mdDoc "Disable external SSH feature.";
310               };
312               SSH_PORT = mkOption {
313                 type = types.int;
314                 default = 22;
315                 example = 2222;
316                 description = lib.mdDoc ''
317                   SSH port displayed in clone URL.
318                   The option is required to configure a service when the external visible port
319                   differs from the local listening port i.e. if port forwarding is used.
320                 '';
321               };
322             };
324             service = {
325               DISABLE_REGISTRATION = mkEnableOption (lib.mdDoc "the registration lock") // {
326                 description = lib.mdDoc ''
327                   By default any user can create an account on this `gitea` instance.
328                   This can be disabled by using this option.
330                   *Note:* please keep in mind that this should be added after the initial
331                   deploy unless [](#opt-services.gitea.useWizard)
332                   is `true` as the first registered user will be the administrator if
333                   no install wizard is used.
334                 '';
335               };
336             };
338             session = {
339               COOKIE_SECURE = mkOption {
340                 type = types.bool;
341                 default = false;
342                 description = lib.mdDoc ''
343                   Marks session cookies as "secure" as a hint for browsers to only send
344                   them via HTTPS. This option is recommend, if gitea is being served over HTTPS.
345                 '';
346               };
347             };
348           };
349         };
350       };
352       extraConfig = mkOption {
353         type = with types; nullOr str;
354         default = null;
355         description = lib.mdDoc "Configuration lines appended to the generated gitea configuration file.";
356       };
357     };
358   };
360   config = mkIf cfg.enable {
361     assertions = [
362       { assertion = cfg.database.createDatabase -> cfg.database.user == cfg.user;
363         message = "services.gitea.database.user must match services.gitea.user if the database is to be automatically provisioned";
364       }
365     ];
367     services.gitea.settings = {
368       database = mkMerge [
369         {
370           DB_TYPE = cfg.database.type;
371         }
372         (mkIf (useMysql || usePostgresql) {
373           HOST = if cfg.database.socket != null then cfg.database.socket else cfg.database.host + ":" + toString cfg.database.port;
374           NAME = cfg.database.name;
375           USER = cfg.database.user;
376           PASSWD = "#dbpass#";
377         })
378         (mkIf useSqlite {
379           PATH = cfg.database.path;
380         })
381         (mkIf usePostgresql {
382           SSL_MODE = "disable";
383         })
384       ];
386       repository = {
387         ROOT = cfg.repositoryRoot;
388       };
390       server = mkMerge [
391         {
392           DOMAIN = cfg.domain;
393           STATIC_ROOT_PATH = toString cfg.staticRootPath;
394           LFS_JWT_SECRET = "#lfsjwtsecret#";
395           ROOT_URL = cfg.rootUrl;
396         }
397         (mkIf cfg.enableUnixSocket {
398           PROTOCOL = "unix";
399           HTTP_ADDR = "/run/gitea/gitea.sock";
400         })
401         (mkIf (!cfg.enableUnixSocket) {
402           HTTP_ADDR = cfg.httpAddress;
403           HTTP_PORT = cfg.httpPort;
404         })
405         (mkIf cfg.lfs.enable {
406           LFS_START_SERVER = true;
407           LFS_CONTENT_PATH = cfg.lfs.contentDir;
408         })
410       ];
412       session = {
413         COOKIE_NAME = lib.mkDefault "session";
414       };
416       security = {
417         SECRET_KEY = "#secretkey#";
418         INTERNAL_TOKEN = "#internaltoken#";
419         INSTALL_LOCK = true;
420       };
422       mailer = mkIf (cfg.mailerPasswordFile != null) {
423         PASSWD = "#mailerpass#";
424       };
426       oauth2 = {
427         JWT_SECRET = "#oauth2jwtsecret#";
428       };
429     };
431     services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) {
432       enable = mkDefault true;
434       ensureDatabases = [ cfg.database.name ];
435       ensureUsers = [
436         { name = cfg.database.user;
437           ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
438         }
439       ];
440     };
442     services.mysql = optionalAttrs (useMysql && cfg.database.createDatabase) {
443       enable = mkDefault true;
444       package = mkDefault pkgs.mariadb;
446       ensureDatabases = [ cfg.database.name ];
447       ensureUsers = [
448         { name = cfg.database.user;
449           ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
450         }
451       ];
452     };
454     systemd.tmpfiles.rules = [
455       "d '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -"
456       "z '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -"
457       "Z '${cfg.dump.backupDir}' - ${cfg.user} gitea - -"
458       "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -"
459       "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -"
460       "Z '${cfg.lfs.contentDir}' - ${cfg.user} gitea - -"
461       "d '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -"
462       "z '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -"
463       "Z '${cfg.repositoryRoot}' - ${cfg.user} gitea - -"
464       "d '${cfg.stateDir}' 0750 ${cfg.user} gitea - -"
465       "d '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -"
466       "d '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -"
467       "d '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -"
468       "d '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -"
469       "z '${cfg.stateDir}' 0750 ${cfg.user} gitea - -"
470       "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} gitea - -"
471       "z '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -"
472       "z '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -"
473       "z '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -"
474       "z '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -"
475       "Z '${cfg.stateDir}' - ${cfg.user} gitea - -"
477       # If we have a folder or symlink with gitea locales, remove it
478       # And symlink the current gitea locales in place
479       "L+ '${cfg.stateDir}/conf/locale' - - - - ${gitea.out}/locale"
480     ];
482     systemd.services.gitea = {
483       description = "gitea";
484       after = [ "network.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
485       wantedBy = [ "multi-user.target" ];
486       path = [ gitea pkgs.git ];
488       # In older versions the secret naming for JWT was kind of confusing.
489       # The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET
490       # wasn't persistant at all.
491       # To fix that, there is now the file oauth2_jwt_secret containing the
492       # values for JWT_SECRET and the file jwt_secret gets renamed to
493       # lfs_jwt_secret.
494       # We have to consider this to stay compatible with older installations.
495       preStart = let
496         runConfig = "${cfg.stateDir}/custom/conf/app.ini";
497         secretKey = "${cfg.stateDir}/custom/conf/secret_key";
498         oauth2JwtSecret = "${cfg.stateDir}/custom/conf/oauth2_jwt_secret";
499         oldLfsJwtSecret = "${cfg.stateDir}/custom/conf/jwt_secret"; # old file for LFS_JWT_SECRET
500         lfsJwtSecret = "${cfg.stateDir}/custom/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET
501         internalToken = "${cfg.stateDir}/custom/conf/internal_token";
502         replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret";
503       in ''
504         # copy custom configuration and generate a random secret key if needed
505         ${optionalString (!cfg.useWizard) ''
506           function gitea_setup {
507             cp -f ${configFile} ${runConfig}
509             if [ ! -s ${secretKey} ]; then
510                 ${gitea}/bin/gitea generate secret SECRET_KEY > ${secretKey}
511             fi
513             # Migrate LFS_JWT_SECRET filename
514             if [[ -s ${oldLfsJwtSecret} && ! -s ${lfsJwtSecret} ]]; then
515                 mv ${oldLfsJwtSecret} ${lfsJwtSecret}
516             fi
518             if [ ! -s ${oauth2JwtSecret} ]; then
519                 ${gitea}/bin/gitea generate secret JWT_SECRET > ${oauth2JwtSecret}
520             fi
522             if [ ! -s ${lfsJwtSecret} ]; then
523                 ${gitea}/bin/gitea generate secret LFS_JWT_SECRET > ${lfsJwtSecret}
524             fi
526             if [ ! -s ${internalToken} ]; then
527                 ${gitea}/bin/gitea generate secret INTERNAL_TOKEN > ${internalToken}
528             fi
530             chmod u+w '${runConfig}'
531             ${replaceSecretBin} '#secretkey#' '${secretKey}' '${runConfig}'
532             ${replaceSecretBin} '#dbpass#' '${cfg.database.passwordFile}' '${runConfig}'
533             ${replaceSecretBin} '#oauth2jwtsecret#' '${oauth2JwtSecret}' '${runConfig}'
534             ${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}'
535             ${replaceSecretBin} '#internaltoken#' '${internalToken}' '${runConfig}'
537             ${lib.optionalString (cfg.mailerPasswordFile != null) ''
538               ${replaceSecretBin} '#mailerpass#' '${cfg.mailerPasswordFile}' '${runConfig}'
539             ''}
540             chmod u-w '${runConfig}'
541           }
542           (umask 027; gitea_setup)
543         ''}
545         # run migrations/init the database
546         ${gitea}/bin/gitea migrate
548         # update all hooks' binary paths
549         ${gitea}/bin/gitea admin regenerate hooks
551         # update command option in authorized_keys
552         if [ -r ${cfg.stateDir}/.ssh/authorized_keys ]
553         then
554           ${gitea}/bin/gitea admin regenerate keys
555         fi
556       '';
558       serviceConfig = {
559         Type = "simple";
560         User = cfg.user;
561         Group = "gitea";
562         WorkingDirectory = cfg.stateDir;
563         ExecStart = "${gitea}/bin/gitea web --pid /run/gitea/gitea.pid";
564         Restart = "always";
565         # Runtime directory and mode
566         RuntimeDirectory = "gitea";
567         RuntimeDirectoryMode = "0755";
568         # Access write directories
569         ReadWritePaths = [ cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ];
570         UMask = "0027";
571         # Capabilities
572         CapabilityBoundingSet = "";
573         # Security
574         NoNewPrivileges = true;
575         # Sandboxing
576         ProtectSystem = "strict";
577         ProtectHome = true;
578         PrivateTmp = true;
579         PrivateDevices = true;
580         PrivateUsers = true;
581         ProtectHostname = true;
582         ProtectClock = true;
583         ProtectKernelTunables = true;
584         ProtectKernelModules = true;
585         ProtectKernelLogs = true;
586         ProtectControlGroups = true;
587         RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6" ];
588         LockPersonality = true;
589         MemoryDenyWriteExecute = true;
590         RestrictRealtime = true;
591         RestrictSUIDSGID = true;
592         PrivateMounts = true;
593         # System Call Filtering
594         SystemCallArchitectures = "native";
595         SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @reboot @setuid @swap";
596       };
598       environment = {
599         USER = cfg.user;
600         HOME = cfg.stateDir;
601         GITEA_WORK_DIR = cfg.stateDir;
602       };
603     };
605     users.users = mkIf (cfg.user == "gitea") {
606       gitea = {
607         description = "Gitea Service";
608         home = cfg.stateDir;
609         useDefaultShell = true;
610         group = "gitea";
611         isSystemUser = true;
612       };
613     };
615     users.groups.gitea = {};
617     warnings =
618       optional (cfg.database.password != "") "config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead." ++
619       optional (cfg.extraConfig != null) ''
620         services.gitea.`extraConfig` is deprecated, please use services.gitea.`settings`.
621       '';
623     # Create database passwordFile default when password is configured.
624     services.gitea.database.passwordFile =
625       mkDefault (toString (pkgs.writeTextFile {
626         name = "gitea-database-password";
627         text = cfg.database.password;
628       }));
630     systemd.services.gitea-dump = mkIf cfg.dump.enable {
631        description = "gitea dump";
632        after = [ "gitea.service" ];
633        wantedBy = [ "default.target" ];
634        path = [ gitea ];
636        environment = {
637          USER = cfg.user;
638          HOME = cfg.stateDir;
639          GITEA_WORK_DIR = cfg.stateDir;
640        };
642        serviceConfig = {
643          Type = "oneshot";
644          User = cfg.user;
645          ExecStart = "${gitea}/bin/gitea dump --type ${cfg.dump.type}" + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}";
646          WorkingDirectory = cfg.dump.backupDir;
647        };
648     };
650     systemd.timers.gitea-dump = mkIf cfg.dump.enable {
651       description = "Update timer for gitea-dump";
652       partOf = [ "gitea-dump.service" ];
653       wantedBy = [ "timers.target" ];
654       timerConfig.OnCalendar = cfg.dump.interval;
655     };
656   };
657   meta.maintainers = with lib.maintainers; [ srhb ma27 ];