1 { config, lib, pkgs, ... }:
3 jenkinsCfg = config.services.jenkins;
4 cfg = config.services.jenkins.jobBuilder;
8 services.jenkins.jobBuilder = {
9 enable = lib.mkEnableOption ''
10 the Jenkins Job Builder (JJB) service. It
11 allows defining jobs for Jenkins in a declarative manner.
13 Jobs managed through the Jenkins WebUI (or by other means) are left
16 Note that it really is declarative configuration; if you remove a
17 previously defined job, the corresponding job directory will be
20 Please see the Jenkins Job Builder documentation for more info:
21 <https://jenkins-job-builder.readthedocs.io/>
24 accessUser = lib.mkOption {
28 User id in Jenkins used to reload config.
32 accessToken = lib.mkOption {
36 User token in Jenkins used to reload config.
37 WARNING: This token will be world readable in the Nix store. To keep
38 it secret, use the {option}`accessTokenFile` option instead.
42 accessTokenFile = lib.mkOption {
43 default = "${config.services.jenkins.home}/secrets/initialAdminPassword";
44 defaultText = lib.literalExpression ''"''${config.services.jenkins.home}/secrets/initialAdminPassword"'';
46 example = "/run/keys/jenkins-job-builder-access-token";
48 File containing the API token for the {option}`accessUser`
53 yamlJobs = lib.mkOption {
55 type = lib.types.lines;
58 name: jenkins-job-test-1
60 - shell: echo 'Hello world!'
63 Job descriptions for Jenkins Job Builder in YAML format.
67 jsonJobs = lib.mkOption {
69 type = lib.types.listOf lib.types.str;
70 example = lib.literalExpression ''
74 { "name": "jenkins-job-test-2",
75 "builders": [ "shell": "echo 'Hello world!'" ]
83 Job descriptions for Jenkins Job Builder in JSON format.
87 nixJobs = lib.mkOption {
89 type = lib.types.listOf lib.types.attrs;
90 example = lib.literalExpression ''
92 { name = "jenkins-job-test-3";
94 { shell = "echo 'Hello world!'"; }
101 Job descriptions for Jenkins Job Builder in Nix format.
103 This is a trivial wrapper around jsonJobs, using builtins.toJSON
110 config = lib.mkIf (jenkinsCfg.enable && cfg.enable) {
113 if cfg.accessUser != ""
114 then (cfg.accessToken != "" && cfg.accessTokenFile == "") ||
115 (cfg.accessToken == "" && cfg.accessTokenFile != "")
118 One of accessToken and accessTokenFile options must be non-empty
119 strings, but not both. Current values:
120 services.jenkins.jobBuilder.accessToken = "${cfg.accessToken}"
121 services.jenkins.jobBuilder.accessTokenFile = "${cfg.accessTokenFile}"
126 systemd.services.jenkins-job-builder = {
127 description = "Jenkins Job Builder Service";
128 # JJB can run either before or after jenkins. We chose after, so we can
129 # always use curl to notify (running) jenkins to reload its config.
130 after = [ "jenkins.service" ];
131 wantedBy = [ "multi-user.target" ];
133 path = with pkgs; [ jenkins-job-builder curl ];
135 # Q: Why manipulate files directly instead of using "jenkins-jobs upload [...]"?
136 # A: Because this module is for administering a local jenkins install,
137 # and using local file copy allows us to not worry about
141 yamlJobsFile = builtins.toFile "jobs.yaml" cfg.yamlJobs;
143 map (x: (builtins.toFile "jobs.json" x))
144 (cfg.jsonJobs ++ [(builtins.toJSON cfg.nixJobs)]);
145 jobBuilderOutputDir = "/run/jenkins-job-builder/output";
146 # Stamp file is placed in $JENKINS_HOME/jobs/$JOB_NAME/ to indicate
147 # ownership. Enables tracking and removal of stale jobs.
148 ownerStamp = ".config-xml-managed-by-nixos-jenkins-job-builder";
150 echo "Asking Jenkins to reload config"
151 curl_opts="--silent --fail --show-error"
152 access_token_file=${if cfg.accessTokenFile != ""
153 then cfg.accessTokenFile
154 else "$RUNTIME_DIRECTORY/jenkins_access_token.txt"}
155 if [ "${cfg.accessToken}" != "" ]; then
156 (umask 0077; printf "${cfg.accessToken}" >"$access_token_file")
158 jenkins_url="http://${jenkinsCfg.listenAddress}:${toString jenkinsCfg.port}${jenkinsCfg.prefix}"
159 auth_file="$RUNTIME_DIRECTORY/jenkins_auth_file.txt"
160 trap 'rm -f "$auth_file"' EXIT
161 (umask 0077; printf "${cfg.accessUser}:@password_placeholder@" >"$auth_file")
162 "${pkgs.replace-secret}/bin/replace-secret" "@password_placeholder@" "$access_token_file" "$auth_file"
164 if ! "${pkgs.jenkins}/bin/jenkins-cli" -s "$jenkins_url" -auth "@$auth_file" reload-configuration; then
165 echo "error: failed to reload configuration"
177 printf "%s" "$first" "''${@/#/$separator}"
180 # Map a relative directory path in the output from
181 # jenkins-job-builder (jobname) to the layout expected by jenkins:
182 # each directory level gets prepended "jobs/".
185 IFS='/' read -ra input_dirs <<< "$1"
187 joinByString "/jobs/" "''${input_dirs[@]}"
190 # The inverse of getJenkinsJobDir (remove the "jobs/" prefixes)
193 IFS='/' read -ra input_dirs <<< "$1"
195 local nelem=''${#input_dirs[@]}
196 for e in "''${input_dirs[@]}"; do
197 if [ $((i % 2)) -eq 1 ]; then
199 if [ $i -lt $(( nelem - 1 )) ]; then
207 rm -rf ${jobBuilderOutputDir}
208 cur_decl_jobs=/run/jenkins-job-builder/declarative-jobs
209 rm -f "$cur_decl_jobs"
211 # Create / update jobs
212 mkdir -p ${jobBuilderOutputDir}
213 for inputFile in ${yamlJobsFile} ${lib.concatStringsSep " " jsonJobsFiles}; do
214 HOME="${jenkinsCfg.home}" "${pkgs.jenkins-job-builder}/bin/jenkins-jobs" --ignore-cache test --config-xml -o "${jobBuilderOutputDir}" "$inputFile"
217 find "${jobBuilderOutputDir}" -type f -name config.xml | while read -r f; do echo "$(dirname "$f")"; done | sort | while read -r dir; do
218 jobname="$(realpath --relative-to="${jobBuilderOutputDir}" "$dir")"
219 jenkinsjobname=$(getJenkinsJobDir "$jobname")
220 jenkinsjobdir="${jenkinsCfg.home}/$jenkinsjobname"
221 echo "Creating / updating job \"$jobname\""
222 mkdir -p "$jenkinsjobdir"
223 touch "$jenkinsjobdir/${ownerStamp}"
224 cp "$dir"/config.xml "$jenkinsjobdir/config.xml"
225 echo "$jenkinsjobname" >> "$cur_decl_jobs"
229 find "${jenkinsCfg.home}" -type f -name "${ownerStamp}" | while read -r f; do echo "$(dirname "$f")"; done | sort --reverse | while read -r dir; do
230 jenkinsjobname="$(realpath --relative-to="${jenkinsCfg.home}" "$dir")"
231 grep --quiet --line-regexp "$jenkinsjobname" "$cur_decl_jobs" 2>/dev/null && continue
232 jobname=$(getJobname "$jenkinsjobname")
233 echo "Deleting stale job \"$jobname\""
234 jobdir="${jenkinsCfg.home}/$jenkinsjobname"
237 '' + (lib.optionalString (cfg.accessUser != "") reloadScript);
240 User = jenkinsCfg.user;
241 RuntimeDirectory = "jenkins-job-builder";