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}