1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.jenkins;
9 jenkinsUrl = "http://${cfg.listenAddress}:${toString cfg.port}${cfg.prefix}";
10in
11{
12 options = {
13 services.jenkins = {
14 enable = lib.mkEnableOption "Jenkins, a continuous integration server";
15
16 user = lib.mkOption {
17 default = "jenkins";
18 type = lib.types.str;
19 description = ''
20 User the jenkins server should execute under.
21 '';
22 };
23
24 group = lib.mkOption {
25 default = "jenkins";
26 type = lib.types.str;
27 description = ''
28 If the default user "jenkins" is configured then this is the primary
29 group of that user.
30 '';
31 };
32
33 extraGroups = lib.mkOption {
34 type = lib.types.listOf lib.types.str;
35 default = [ ];
36 example = [
37 "wheel"
38 "dialout"
39 ];
40 description = ''
41 List of extra groups that the "jenkins" user should be a part of.
42 '';
43 };
44
45 home = lib.mkOption {
46 default = "/var/lib/jenkins";
47 type = lib.types.path;
48 description = ''
49 The path to use as JENKINS_HOME. If the default user "jenkins" is configured then
50 this is the home of the "jenkins" user.
51 '';
52 };
53
54 listenAddress = lib.mkOption {
55 default = "0.0.0.0";
56 example = "localhost";
57 type = lib.types.str;
58 description = ''
59 Specifies the bind address on which the jenkins HTTP interface listens.
60 The default is the wildcard address.
61 '';
62 };
63
64 port = lib.mkOption {
65 default = 8080;
66 type = lib.types.port;
67 description = ''
68 Specifies port number on which the jenkins HTTP interface listens.
69 The default is 8080.
70 '';
71 };
72
73 prefix = lib.mkOption {
74 default = "";
75 example = "/jenkins";
76 type = lib.types.str;
77 description = ''
78 Specifies a urlPrefix to use with jenkins.
79 If the example /jenkins is given, the jenkins server will be
80 accessible using localhost:8080/jenkins.
81 '';
82 };
83
84 package = lib.mkPackageOption pkgs "jenkins" { };
85
86 javaPackage = lib.mkPackageOption pkgs "jdk21" { };
87
88 packages = lib.mkOption {
89 default = [
90 pkgs.stdenv
91 pkgs.git
92 pkgs.jdk21
93 config.programs.ssh.package
94 pkgs.nix
95 ];
96 defaultText = lib.literalExpression "[ pkgs.stdenv pkgs.git pkgs.jdk17 config.programs.ssh.package pkgs.nix ]";
97 type = lib.types.listOf lib.types.package;
98 description = ''
99 Packages to add to PATH for the jenkins process.
100 '';
101 };
102
103 environment = lib.mkOption {
104 default = { };
105 type = with lib.types; attrsOf str;
106 description = ''
107 Additional environment variables to be passed to the jenkins process.
108 As a base environment, jenkins receives NIX_PATH from
109 {option}`environment.sessionVariables`, NIX_REMOTE is set to
110 "daemon" and JENKINS_HOME is set to the value of
111 {option}`services.jenkins.home`.
112 This option has precedence and can be used to override those
113 mentioned variables.
114 '';
115 };
116
117 plugins = lib.mkOption {
118 default = null;
119 type = lib.types.nullOr (lib.types.attrsOf lib.types.package);
120 description = ''
121 A set of plugins to activate. Note that this will completely
122 remove and replace any previously installed plugins. If you
123 have manually-installed plugins that you want to keep while
124 using this module, set this option to
125 `null`. You can generate this set with a
126 tool such as `jenkinsPlugins2nix`.
127 '';
128 example = lib.literalExpression ''
129 import path/to/jenkinsPlugins2nix-generated-plugins.nix { inherit (pkgs) fetchurl stdenv; }
130 '';
131 };
132
133 extraOptions = lib.mkOption {
134 type = lib.types.listOf lib.types.str;
135 default = [ ];
136 example = [ "--debug=9" ];
137 description = ''
138 Additional command line arguments to pass to Jenkins.
139 '';
140 };
141
142 extraJavaOptions = lib.mkOption {
143 type = lib.types.listOf lib.types.str;
144 default = [ ];
145 example = [ "-Xmx80m" ];
146 description = ''
147 Additional command line arguments to pass to the Java run time (as opposed to Jenkins).
148 '';
149 };
150
151 withCLI = lib.mkOption {
152 type = lib.types.bool;
153 default = false;
154 description = ''
155 Whether to make the CLI available.
156
157 More info about the CLI available at
158 [
159 https://www.jenkins.io/doc/book/managing/cli](https://www.jenkins.io/doc/book/managing/cli) .
160 '';
161 };
162 };
163 };
164
165 config = lib.mkIf cfg.enable {
166 environment = {
167 # server references the dejavu fonts
168 systemPackages = [
169 pkgs.dejavu_fonts
170 ]
171 ++ lib.optional cfg.withCLI cfg.package;
172
173 variables =
174 { }
175 // lib.optionalAttrs cfg.withCLI {
176 # Make it more convenient to use the `jenkins-cli`.
177 JENKINS_URL = jenkinsUrl;
178 };
179 };
180
181 users.groups = lib.optionalAttrs (cfg.group == "jenkins") {
182 jenkins.gid = config.ids.gids.jenkins;
183 };
184
185 users.users = lib.optionalAttrs (cfg.user == "jenkins") {
186 jenkins = {
187 description = "jenkins user";
188 createHome = true;
189 home = cfg.home;
190 group = cfg.group;
191 extraGroups = cfg.extraGroups;
192 useDefaultShell = true;
193 uid = config.ids.uids.jenkins;
194 };
195 };
196
197 systemd.services.jenkins = {
198 description = "Jenkins Continuous Integration Server";
199 after = [ "network.target" ];
200 wantedBy = [ "multi-user.target" ];
201
202 environment =
203 let
204 selectedSessionVars = lib.filterAttrs (
205 n: v: builtins.elem n [ "NIX_PATH" ]
206 ) config.environment.sessionVariables;
207 in
208 selectedSessionVars
209 // {
210 JENKINS_HOME = cfg.home;
211 NIX_REMOTE = "daemon";
212 }
213 // cfg.environment;
214
215 path = cfg.packages;
216
217 # Force .war (re)extraction, or else we might run stale Jenkins.
218
219 preStart =
220 let
221 replacePlugins = lib.optionalString (cfg.plugins != null) (
222 let
223 pluginCmds = lib.mapAttrsToList (n: v: "cp ${v} ${cfg.home}/plugins/${n}.jpi") cfg.plugins;
224 in
225 ''
226 rm -r ${cfg.home}/plugins || true
227 mkdir -p ${cfg.home}/plugins
228 ${lib.concatStringsSep "\n" pluginCmds}
229 ''
230 );
231 in
232 ''
233 rm -rf ${cfg.home}/war
234 ${replacePlugins}
235 '';
236
237 # For reference: https://wiki.jenkins.io/display/JENKINS/JenkinsLinuxStartupScript
238 script = ''
239 ${cfg.javaPackage}/bin/java ${lib.concatStringsSep " " cfg.extraJavaOptions} -jar ${cfg.package}/webapps/jenkins.war --httpListenAddress=${cfg.listenAddress} \
240 --httpPort=${toString cfg.port} \
241 --prefix=${cfg.prefix} \
242 -Djava.awt.headless=true \
243 ${lib.concatStringsSep " " cfg.extraOptions}
244 '';
245
246 postStart = ''
247 until [[ $(${pkgs.curl.bin}/bin/curl -L -s --head -w '\n%{http_code}' ${jenkinsUrl} | tail -n1) =~ ^(200|403)$ ]]; do
248 sleep 1
249 done
250 '';
251
252 serviceConfig = {
253 User = cfg.user;
254 StateDirectory = lib.mkIf (lib.hasPrefix "/var/lib/jenkins" cfg.home) "jenkins";
255 # For (possible) socket use
256 RuntimeDirectory = "jenkins";
257 AmbientCapabilities = "";
258 CapabilityBoundingSet = "";
259 LockPersonality = true;
260 # MemoryDenyWriteExecute = false; Breaks execution;
261 NoNewPrivileges = true;
262 PrivateDevices = true;
263 PrivateMounts = true;
264 PrivateTmp = true;
265 ProtectClock = true;
266 ProtectControlGroups = true;
267 ProtectHome = true;
268 ProtectHostname = true;
269 ProtectKernelLogs = true;
270 ProtectKernelModules = true;
271 ProtectKernelTunables = true;
272 ProtectSystem = "full";
273 RemoveIPC = true;
274 RestrictAddressFamilies = [
275 "AF_UNIX"
276 "AF_INET"
277 "AF_INET6"
278 ];
279 RestrictNamespaces = true;
280 RestrictRealtime = true;
281 RestrictSUIDSGID = true;
282 SystemCallArchitectures = "native";
283 UMask = 27;
284 };
285 };
286 };
287}