diff options
-rw-r--r-- | modules/apps/jenkins/default.nix | 149 | ||||
-rw-r--r-- | modules/apps/jenkins/instance.nix | 93 |
2 files changed, 242 insertions, 0 deletions
diff --git a/modules/apps/jenkins/default.nix b/modules/apps/jenkins/default.nix new file mode 100644 index 0000000..d0ccb25 --- /dev/null +++ b/modules/apps/jenkins/default.nix @@ -0,0 +1,149 @@ +{ config, lib, pkgs, ... }: + +let + + inherit (builtins) + attrNames isBool isString ; + + inherit (lib) + concatMapStringsSep concatStringsSep escape filter filterAttrs + foldAttrs foldl hasPrefix mapAttrs mapAttrsToList mkOption nameValuePair + optionalString ; + + inherit (lib.types) + attrsOf submodule ; + + explicit = filterAttrs (n: v: n != "_module" && v != null); + isKey = s: s != null && hasPrefix "/run/keys/" s; + + instances = explicit config.nixsap.apps.jenkins; + users = mapAttrsToList (_: i: i.user) instances; + + maybeFile = n: c: if hasPrefix "/" c then c else pkgs.writeXML n c; + configFiles = name: cfg: mapAttrs (n: v: maybeFile "jenkins-${name}-${n}" v) cfg.config; + jobFiles = name: cfg: mapAttrs (n: v: maybeFile "jenkins-${name}-job-${n}.xml" v) cfg.jobs; + + keyrings = + let + # This requires read-write mode of evaluation: + keys = n: i: filter isKey (import (pkgs.xinclude2nix ( + (mapAttrsToList (_: f: f) (configFiles n i)) + ++ (mapAttrsToList (_: f: f) (jobFiles n i)) + ))); + ik = mapAttrsToList (n: i: { "${i.user}" = keys n i; } ) instances; + in foldAttrs (l: r: l ++ r) [] ik; + + mkService = name: cfg: + let + + mkOpt = n: v: if isBool v then (if v then "--${n}" else "") + else if isString v then "--${n}='${v}'" + else "--${n}=${toString v}"; + + path = ".war.path"; + + startJenkins = pkgs.writeBashScript "jenkins-${name}-start" '' + set -euo pipefail + umask 0027 + export HOME='${cfg.home}' + + cd '${cfg.home}' + + find -maxdepth 1 \( \ + -iname '*.xml' \ + -o -iname '*.bak' \ + -o -iname '*.log' \ + -o -iname '*.tmp' \ + -o -iname '*.txt' \ + \) -delete + + ${concatStringsSep "\n" ( + mapAttrsToList (n: p: + # XXX Jenkins does not support XInclude + # XXX We use XInclude to include secret files (keys) + '' + ${pkgs.libxml2}/bin/xmllint --xinclude --format '${p}' > '${n}' + '') (configFiles name cfg) + )} + + if [ -d jobs ]; then + find jobs -maxdepth 1 -mindepth 1 -type d \ + ${concatMapStringsSep " " (k: "-not -name '${escape [ "[" ] k}'") (attrNames cfg.jobs)} \ + -print0 | xargs -0 --verbose --no-run-if-empty rm -rf + fi + + ${concatStringsSep "\n" ( + mapAttrsToList (n: p: '' + mkdir -p -- 'jobs/${n}' + rm -rf -- 'jobs/${n}/config.xml' + ${pkgs.libxml2}/bin/xmllint --xinclude --format '${p}' > 'jobs/${n}/config.xml' + '') (jobFiles name cfg) + )} + + if [ -f ${path} ]; then + old=$(cat ${path}) + else + old= + fi + + # FIXME: make sure old content is flushed + if [ '${cfg.war}' != "$old" ]; then + rm -rf war plugins + echo '${cfg.war}' > ${path} + fi + + exec ${cfg.jre}/bin/java \ + -DJENKINS_HOME='${cfg.home}' \ + -jar '${cfg.war}' \ + ${concatStringsSep " \\\n " ( + mapAttrsToList mkOpt (explicit cfg.options))} + ''; + + in { + "jenkins-${name}" = { + description = "Jenkins (${name}) automation server"; + wantedBy = [ "multi-user.target" ]; + after = [ "keys.target" "network.target" "local-fs.target" ]; + inherit (cfg) path; + preStart = '' + mkdir -p -- '${cfg.home}' + + # XXX ignore potentially dangling symlinks + # XXX like lastStable -> builds/lastStableBuild. + # XXX chmod/chown fail on them + find '${cfg.home}' -not -type l \( \ + -not -user '${cfg.user}' \ + -or -not -group '${cfg.user}' \ + -or \( -type d -not -perm -u=wrx,g=rx \) \ + -or \( -type f -not -perm -u=rw,g=r \) \ + \) \ + -exec chown -c -- '${cfg.user}:${cfg.user}' {} + \ + -exec chmod -c -- u=rwX,g=rX,o= {} + + + ''; + serviceConfig = { + ExecStart = startJenkins; + KillMode = "mixed"; + PermissionsStartOnly = true; + Restart = "always"; + TimeoutSec = 0; + User = cfg.user; + }; + }; + }; + +in { + + options.nixsap.apps.jenkins = mkOption { + description = "Jenkins instances"; + default = {}; + type = attrsOf (submodule (import ./instance.nix pkgs)); + }; + + config = { + systemd.services = foldl (a: b: a//b) {} (mapAttrsToList mkService instances); + nixsap.deployment.keyrings = keyrings; + nixsap.system.users.daemons = users; + }; + +} diff --git a/modules/apps/jenkins/instance.nix b/modules/apps/jenkins/instance.nix new file mode 100644 index 0000000..692b066 --- /dev/null +++ b/modules/apps/jenkins/instance.nix @@ -0,0 +1,93 @@ +pkgs: +{ lib, name, ... }: + +let + + inherit (builtins) all attrNames; + + inherit (lib) + concatStrings filterAttrs hasSuffix mapAttrsToList mkOption ; + + inherit (lib.types) + addCheck attrsOf either enum int listOf nullOr package path str submodule ; + + default = d: t: mkOption { type = t; default = d; }; + optional = t: mkOption { type = nullOr t; default = null; }; + +in { + options = { + + jre = mkOption { + description = "Java runtime package"; + default = pkgs.jre8; + type = package; + }; + + war = mkOption { + description = "Jenkins web application archive (WAR)"; + default = pkgs.jenkins; + type = path; + }; + + user = mkOption { + description = "User to run as"; + default = "jenkins-${name}"; + type = str; + }; + + home = mkOption { + description = "Jenkins data directory"; + type = path; + default = "/jenkins/${name}"; + }; + + jobs = mkOption { + description = '' + Jenkins jobs. Each value is either inline XML text or an XML file. + Any existing jobs, not mentioned here, are physically removed. + ''; + type = attrsOf (either str path); + default = {}; + }; + + config = mkOption { + description = '' + Jenkins XML configuration files. Either inline text or file. Any + existing XML files, not mentioned here, are physically removed. You + might want to add `config.xml` at least. You can use XInclude + facility to include sensitive pieces of configuration like passwords + or private keys. Those grains will be processed (expanded) to + create proper configuration files. Also they will be automatically + picked up and deployed (requires read-write mode of evaluation). + E. g. if you write '<xi:include href="/run/keys/github-oauth.xml"/>', + that file will be deployed as a secret key, and when Jenkins starts, + that piece will be replaced by the file contents. All configuration + files reside in Jenkins private directory so secrets remain secret. + ''; + type = addCheck (attrsOf (either str path)) (aa: all (hasSuffix ".xml") (attrNames aa)); + default = {}; + }; + + path = mkOption { + description = '' + Additional packages available to Jenkins in PATH. You also may opt in specifying + paths to executables in various config files. + ''; + type = listOf package; + default = []; + example = [ pkgs.gitMinimal ]; + }; + + options = { + controlPort = optional int; + debug = optional (enum [1 2 3 4 5 6 7 8 9]); + httpKeepAliveTimeout = optional int; + httpListenAddress = default "127.0.0.1" str; + httpPort = default 8080 int; + prefix = optional str; + sessionTimeout = optional int; + }; + + }; +} + |