aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/apps/jenkins/default.nix149
-rw-r--r--modules/apps/jenkins/instance.nix93
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;
+ };
+
+ };
+}
+