1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.gitea;
7 gitea = cfg.package;
8 pg = config.services.postgresql;
9 usePostgresql = cfg.database.type == "postgres";
10 configFile = pkgs.writeText "app.ini" ''
11 APP_NAME = ${cfg.appName}
12 RUN_USER = ${cfg.user}
13 RUN_MODE = prod
14
15 [database]
16 DB_TYPE = ${cfg.database.type}
17 HOST = ${cfg.database.host}:${toString cfg.database.port}
18 NAME = ${cfg.database.name}
19 USER = ${cfg.database.user}
20 PASSWD = #dbpass#
21 PATH = ${cfg.database.path}
22 ${optionalString usePostgresql ''
23 SSL_MODE = disable
24 ''}
25
26 [repository]
27 ROOT = ${cfg.repositoryRoot}
28
29 [server]
30 DOMAIN = ${cfg.domain}
31 HTTP_ADDR = ${cfg.httpAddress}
32 HTTP_PORT = ${toString cfg.httpPort}
33 ROOT_URL = ${cfg.rootUrl}
34 STATIC_ROOT_PATH = ${cfg.staticRootPath}
35
36 [session]
37 COOKIE_NAME = session
38 COOKIE_SECURE = ${boolToString cfg.cookieSecure}
39
40 [security]
41 SECRET_KEY = #secretkey#
42 INSTALL_LOCK = true
43
44 [log]
45 ROOT_PATH = ${cfg.log.rootPath}
46 LEVEL = ${cfg.log.level}
47
48 ${cfg.extraConfig}
49 '';
50in
51
52{
53 options = {
54 services.gitea = {
55 enable = mkOption {
56 default = false;
57 type = types.bool;
58 description = "Enable Gitea Service.";
59 };
60
61 package = mkOption {
62 default = pkgs.gitea;
63 type = types.package;
64 defaultText = "pkgs.gitea";
65 description = "gitea derivation to use";
66 };
67
68 useWizard = mkOption {
69 default = false;
70 type = types.bool;
71 description = "Do not generate a configuration and use gitea' installation wizard instead. The first registered user will be administrator.";
72 };
73
74 stateDir = mkOption {
75 default = "/var/lib/gitea";
76 type = types.str;
77 description = "gitea data directory.";
78 };
79
80 log = {
81 rootPath = mkOption {
82 default = "${cfg.stateDir}/log";
83 type = types.str;
84 description = "Root path for log files.";
85 };
86 level = mkOption {
87 default = "Trace";
88 type = types.enum [ "Trace" "Debug" "Info" "Warn" "Error" "Critical" ];
89 description = "General log level.";
90 };
91 };
92
93 user = mkOption {
94 type = types.str;
95 default = "gitea";
96 description = "User account under which gitea runs.";
97 };
98
99 database = {
100 type = mkOption {
101 type = types.enum [ "sqlite3" "mysql" "postgres" ];
102 example = "mysql";
103 default = "sqlite3";
104 description = "Database engine to use.";
105 };
106
107 host = mkOption {
108 type = types.str;
109 default = "127.0.0.1";
110 description = "Database host address.";
111 };
112
113 port = mkOption {
114 type = types.int;
115 default = (if !usePostgresql then 3306 else pg.port);
116 description = "Database host port.";
117 };
118
119 name = mkOption {
120 type = types.str;
121 default = "gitea";
122 description = "Database name.";
123 };
124
125 user = mkOption {
126 type = types.str;
127 default = "gitea";
128 description = "Database user.";
129 };
130
131 password = mkOption {
132 type = types.str;
133 default = "";
134 description = ''
135 The password corresponding to <option>database.user</option>.
136 Warning: this is stored in cleartext in the Nix store!
137 Use <option>database.passwordFile</option> instead.
138 '';
139 };
140
141 passwordFile = mkOption {
142 type = types.nullOr types.path;
143 default = null;
144 example = "/run/keys/gitea-dbpassword";
145 description = ''
146 A file containing the password corresponding to
147 <option>database.user</option>.
148 '';
149 };
150
151 path = mkOption {
152 type = types.str;
153 default = "${cfg.stateDir}/data/gitea.db";
154 description = "Path to the sqlite3 database file.";
155 };
156
157 createDatabase = mkOption {
158 type = types.bool;
159 default = true;
160 description = ''
161 Whether to create a local postgresql database automatically.
162 This only applies if database type "postgres" is selected.
163 '';
164 };
165 };
166
167 dump = {
168 enable = mkOption {
169 type = types.bool;
170 default = false;
171 description = ''
172 Enable a timer that runs gitea dump to generate backup-files of the
173 current gitea database and repositories.
174 '';
175 };
176
177 interval = mkOption {
178 type = types.str;
179 default = "04:31";
180 example = "hourly";
181 description = ''
182 Run a gitea dump at this interval. Runs by default at 04:31 every day.
183
184 The format is described in
185 <citerefentry><refentrytitle>systemd.time</refentrytitle>
186 <manvolnum>7</manvolnum></citerefentry>.
187 '';
188 };
189 };
190
191 appName = mkOption {
192 type = types.str;
193 default = "gitea: Gitea Service";
194 description = "Application name.";
195 };
196
197 repositoryRoot = mkOption {
198 type = types.str;
199 default = "${cfg.stateDir}/repositories";
200 description = "Path to the git repositories.";
201 };
202
203 domain = mkOption {
204 type = types.str;
205 default = "localhost";
206 description = "Domain name of your server.";
207 };
208
209 rootUrl = mkOption {
210 type = types.str;
211 default = "http://localhost:3000/";
212 description = "Full public URL of gitea server.";
213 };
214
215 httpAddress = mkOption {
216 type = types.str;
217 default = "0.0.0.0";
218 description = "HTTP listen address.";
219 };
220
221 httpPort = mkOption {
222 type = types.int;
223 default = 3000;
224 description = "HTTP listen port.";
225 };
226
227 cookieSecure = mkOption {
228 type = types.bool;
229 default = false;
230 description = ''
231 Marks session cookies as "secure" as a hint for browsers to only send
232 them via HTTPS. This option is recommend, if gitea is being served over HTTPS.
233 '';
234 };
235
236 staticRootPath = mkOption {
237 type = types.str;
238 default = "${gitea.data}";
239 example = "/var/lib/gitea/data";
240 description = "Upper level of template and static files path.";
241 };
242
243 extraConfig = mkOption {
244 type = types.str;
245 default = "";
246 description = "Configuration lines appended to the generated gitea configuration file.";
247 };
248 };
249 };
250
251 config = mkIf cfg.enable {
252 services.postgresql.enable = mkIf usePostgresql (mkDefault true);
253
254 systemd.services.gitea = {
255 description = "gitea";
256 after = [ "network.target" "postgresql.service" ];
257 wantedBy = [ "multi-user.target" ];
258 path = [ gitea.bin ];
259
260 preStart = let
261 runConfig = "${cfg.stateDir}/custom/conf/app.ini";
262 secretKey = "${cfg.stateDir}/custom/conf/secret_key";
263 in ''
264 # Make sure that the stateDir exists, as well as the conf dir in there
265 mkdir -p ${cfg.stateDir}/conf
266
267 # copy custom configuration and generate a random secret key if needed
268 ${optionalString (cfg.useWizard == false) ''
269 mkdir -p ${cfg.stateDir}/custom/conf
270 cp -f ${configFile} ${runConfig}
271
272 if [ ! -e ${secretKey} ]; then
273 head -c 16 /dev/urandom | base64 > ${secretKey}
274 fi
275
276 KEY=$(head -n1 ${secretKey})
277 DBPASS=$(head -n1 ${cfg.database.passwordFile})
278 sed -e "s,#secretkey#,$KEY,g" \
279 -e "s,#dbpass#,$DBPASS,g" \
280 -i ${runConfig}
281 chmod 640 ${runConfig} ${secretKey}
282 ''}
283
284 mkdir -p ${cfg.repositoryRoot}
285 # update all hooks' binary paths
286 HOOKS=$(find ${cfg.repositoryRoot} -mindepth 4 -maxdepth 6 -type f -wholename "*git/hooks/*")
287 if [ "$HOOKS" ]
288 then
289 sed -ri 's,/nix/store/[a-z0-9.-]+/bin/gitea,${gitea.bin}/bin/gitea,g' $HOOKS
290 sed -ri 's,/nix/store/[a-z0-9.-]+/bin/env,${pkgs.coreutils}/bin/env,g' $HOOKS
291 sed -ri 's,/nix/store/[a-z0-9.-]+/bin/bash,${pkgs.bash}/bin/bash,g' $HOOKS
292 sed -ri 's,/nix/store/[a-z0-9.-]+/bin/perl,${pkgs.perl}/bin/perl,g' $HOOKS
293 fi
294 # If we have a folder or symlink with gitea locales, remove it
295 if [ -e ${cfg.stateDir}/conf/locale ]
296 then
297 rm -r ${cfg.stateDir}/conf/locale
298 fi
299 # And symlink the current gitea locales in place
300 ln -s ${gitea.out}/locale ${cfg.stateDir}/conf/locale
301 # update command option in authorized_keys
302 if [ -r ${cfg.stateDir}/.ssh/authorized_keys ]
303 then
304 sed -ri 's,/nix/store/[a-z0-9.-]+/bin/gitea,${gitea.bin}/bin/gitea,g' ${cfg.stateDir}/.ssh/authorized_keys
305 fi
306 '' + optionalString (usePostgresql && cfg.database.createDatabase) ''
307 if ! test -e "${cfg.stateDir}/db-created"; then
308 echo "CREATE ROLE ${cfg.database.user}
309 WITH ENCRYPTED PASSWORD '$(head -n1 ${cfg.database.passwordFile})'
310 NOCREATEDB NOCREATEROLE LOGIN" |
311 ${pkgs.sudo}/bin/sudo -u ${pg.superUser} ${pg.package}/bin/psql
312 ${pkgs.sudo}/bin/sudo -u ${pg.superUser} \
313 ${pg.package}/bin/createdb \
314 --owner=${cfg.database.user} \
315 --encoding=UTF8 \
316 --lc-collate=C \
317 --lc-ctype=C \
318 --template=template0 \
319 ${cfg.database.name}
320 touch "${cfg.stateDir}/db-created"
321 fi
322 '' + ''
323 chown ${cfg.user} -R ${cfg.stateDir}
324 '';
325
326 serviceConfig = {
327 Type = "simple";
328 User = cfg.user;
329 WorkingDirectory = cfg.stateDir;
330 PermissionsStartOnly = true;
331 ExecStart = "${gitea.bin}/bin/gitea web";
332 Restart = "always";
333 };
334
335 environment = {
336 USER = cfg.user;
337 HOME = cfg.stateDir;
338 GITEA_WORK_DIR = cfg.stateDir;
339 };
340 };
341
342 users = mkIf (cfg.user == "gitea") {
343 users.gitea = {
344 description = "Gitea Service";
345 home = cfg.stateDir;
346 createHome = true;
347 useDefaultShell = true;
348 };
349 };
350
351 warnings = optional (cfg.database.password != "")
352 ''config.services.gitea.database.password will be stored as plaintext
353 in the Nix store. Use database.passwordFile instead.'';
354
355 # Create database passwordFile default when password is configured.
356 services.gitea.database.passwordFile =
357 (mkDefault (toString (pkgs.writeTextFile {
358 name = "gitea-database-password";
359 text = cfg.database.password;
360 })));
361
362 systemd.services.gitea-dump = mkIf cfg.dump.enable {
363 description = "gitea dump";
364 after = [ "gitea.service" ];
365 wantedBy = [ "default.target" ];
366 path = [ gitea.bin ];
367
368 environment = {
369 USER = cfg.user;
370 HOME = cfg.stateDir;
371 GITEA_WORK_DIR = cfg.stateDir;
372 };
373
374 serviceConfig = {
375 Type = "oneshot";
376 User = cfg.user;
377 ExecStart = "${gitea.bin}/bin/gitea dump";
378 WorkingDirectory = cfg.stateDir;
379 };
380 };
381
382 systemd.timers.gitea-dump = mkIf cfg.dump.enable {
383 description = "Update timer for gitea-dump";
384 partOf = [ "gitea-dump.service" ];
385 wantedBy = [ "timers.target" ];
386 timerConfig.OnCalendar = cfg.dump.interval;
387 };
388 };
389}