1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 jenkinsCfg = config.services.jenkins;
7 cfg = config.services.jenkins.jobBuilder;
8
9in {
10 options = {
11 services.jenkins.jobBuilder = {
12 enable = mkOption {
13 type = types.bool;
14 default = false;
15 description = ''
16 Whether or not to enable the Jenkins Job Builder (JJB) service. It
17 allows defining jobs for Jenkins in a declarative manner.
18
19 Jobs managed through the Jenkins WebUI (or by other means) are left
20 unchanged.
21
22 Note that it really is declarative configuration; if you remove a
23 previously defined job, the corresponding job directory will be
24 deleted.
25
26 Please see the Jenkins Job Builder documentation for more info:
27 <link xlink:href="http://docs.openstack.org/infra/jenkins-job-builder/">
28 http://docs.openstack.org/infra/jenkins-job-builder/</link>
29 '';
30 };
31
32 accessUser = mkOption {
33 default = "";
34 type = types.str;
35 description = ''
36 User id in Jenkins used to reload config.
37 '';
38 };
39
40 accessToken = mkOption {
41 default = "";
42 type = types.str;
43 description = ''
44 User token in Jenkins used to reload config.
45 WARNING: This token will be world readable in the Nix store. To keep
46 it secret, use the <option>accessTokenFile</option> option instead.
47 '';
48 };
49
50 accessTokenFile = mkOption {
51 default = "";
52 type = types.str;
53 example = "/run/keys/jenkins-job-builder-access-token";
54 description = ''
55 File containing the API token for the <option>accessUser</option>
56 user.
57 '';
58 };
59
60 yamlJobs = mkOption {
61 default = "";
62 type = types.lines;
63 example = ''
64 - job:
65 name: jenkins-job-test-1
66 builders:
67 - shell: echo 'Hello world!'
68 '';
69 description = ''
70 Job descriptions for Jenkins Job Builder in YAML format.
71 '';
72 };
73
74 jsonJobs = mkOption {
75 default = [ ];
76 type = types.listOf types.str;
77 example = literalExpression ''
78 [
79 '''
80 [ { "job":
81 { "name": "jenkins-job-test-2",
82 "builders": [ "shell": "echo 'Hello world!'" ]
83 }
84 }
85 ]
86 '''
87 ]
88 '';
89 description = ''
90 Job descriptions for Jenkins Job Builder in JSON format.
91 '';
92 };
93
94 nixJobs = mkOption {
95 default = [ ];
96 type = types.listOf types.attrs;
97 example = literalExpression ''
98 [ { job =
99 { name = "jenkins-job-test-3";
100 builders = [
101 { shell = "echo 'Hello world!'"; }
102 ];
103 };
104 }
105 ]
106 '';
107 description = ''
108 Job descriptions for Jenkins Job Builder in Nix format.
109
110 This is a trivial wrapper around jsonJobs, using builtins.toJSON
111 behind the scene.
112 '';
113 };
114 };
115 };
116
117 config = mkIf (jenkinsCfg.enable && cfg.enable) {
118 assertions = [
119 { assertion =
120 if cfg.accessUser != ""
121 then (cfg.accessToken != "" && cfg.accessTokenFile == "") ||
122 (cfg.accessToken == "" && cfg.accessTokenFile != "")
123 else true;
124 message = ''
125 One of accessToken and accessTokenFile options must be non-empty
126 strings, but not both. Current values:
127 services.jenkins.jobBuilder.accessToken = "${cfg.accessToken}"
128 services.jenkins.jobBuilder.accessTokenFile = "${cfg.accessTokenFile}"
129 '';
130 }
131 ];
132
133 systemd.services.jenkins-job-builder = {
134 description = "Jenkins Job Builder Service";
135 # JJB can run either before or after jenkins. We chose after, so we can
136 # always use curl to notify (running) jenkins to reload its config.
137 after = [ "jenkins.service" ];
138 wantedBy = [ "multi-user.target" ];
139
140 path = with pkgs; [ jenkins-job-builder curl ];
141
142 # Q: Why manipulate files directly instead of using "jenkins-jobs upload [...]"?
143 # A: Because this module is for administering a local jenkins install,
144 # and using local file copy allows us to not worry about
145 # authentication.
146 script =
147 let
148 yamlJobsFile = builtins.toFile "jobs.yaml" cfg.yamlJobs;
149 jsonJobsFiles =
150 map (x: (builtins.toFile "jobs.json" x))
151 (cfg.jsonJobs ++ [(builtins.toJSON cfg.nixJobs)]);
152 jobBuilderOutputDir = "/run/jenkins-job-builder/output";
153 # Stamp file is placed in $JENKINS_HOME/jobs/$JOB_NAME/ to indicate
154 # ownership. Enables tracking and removal of stale jobs.
155 ownerStamp = ".config-xml-managed-by-nixos-jenkins-job-builder";
156 reloadScript = ''
157 echo "Asking Jenkins to reload config"
158 curl_opts="--silent --fail --show-error"
159 access_token=${if cfg.accessTokenFile != ""
160 then "$(cat '${cfg.accessTokenFile}')"
161 else cfg.accessToken}
162 jenkins_url="http://${cfg.accessUser}:$access_token@${jenkinsCfg.listenAddress}:${toString jenkinsCfg.port}${jenkinsCfg.prefix}"
163 crumb=$(curl $curl_opts "$jenkins_url"'/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)')
164 curl $curl_opts -X POST -H "$crumb" "$jenkins_url"/reload
165 '';
166 in
167 ''
168 joinByString()
169 {
170 local separator="$1"
171 shift
172 local first="$1"
173 shift
174 printf "%s" "$first" "''${@/#/$separator}"
175 }
176
177 # Map a relative directory path in the output from
178 # jenkins-job-builder (jobname) to the layout expected by jenkins:
179 # each directory level gets prepended "jobs/".
180 getJenkinsJobDir()
181 {
182 IFS='/' read -ra input_dirs <<< "$1"
183 printf "jobs/"
184 joinByString "/jobs/" "''${input_dirs[@]}"
185 }
186
187 # The inverse of getJenkinsJobDir (remove the "jobs/" prefixes)
188 getJobname()
189 {
190 IFS='/' read -ra input_dirs <<< "$1"
191 local i=0
192 local nelem=''${#input_dirs[@]}
193 for e in "''${input_dirs[@]}"; do
194 if [ $((i % 2)) -eq 1 ]; then
195 printf "$e"
196 if [ $i -lt $(( nelem - 1 )) ]; then
197 printf "/"
198 fi
199 fi
200 i=$((i + 1))
201 done
202 }
203
204 rm -rf ${jobBuilderOutputDir}
205 cur_decl_jobs=/run/jenkins-job-builder/declarative-jobs
206 rm -f "$cur_decl_jobs"
207
208 # Create / update jobs
209 mkdir -p ${jobBuilderOutputDir}
210 for inputFile in ${yamlJobsFile} ${concatStringsSep " " jsonJobsFiles}; do
211 HOME="${jenkinsCfg.home}" "${pkgs.jenkins-job-builder}/bin/jenkins-jobs" --ignore-cache test --config-xml -o "${jobBuilderOutputDir}" "$inputFile"
212 done
213
214 find "${jobBuilderOutputDir}" -type f -name config.xml | while read -r f; do echo "$(dirname "$f")"; done | sort | while read -r dir; do
215 jobname="$(realpath --relative-to="${jobBuilderOutputDir}" "$dir")"
216 jenkinsjobname=$(getJenkinsJobDir "$jobname")
217 jenkinsjobdir="${jenkinsCfg.home}/$jenkinsjobname"
218 echo "Creating / updating job \"$jobname\""
219 mkdir -p "$jenkinsjobdir"
220 touch "$jenkinsjobdir/${ownerStamp}"
221 cp "$dir"/config.xml "$jenkinsjobdir/config.xml"
222 echo "$jenkinsjobname" >> "$cur_decl_jobs"
223 done
224
225 # Remove stale jobs
226 find "${jenkinsCfg.home}" -type f -name "${ownerStamp}" | while read -r f; do echo "$(dirname "$f")"; done | sort --reverse | while read -r dir; do
227 jenkinsjobname="$(realpath --relative-to="${jenkinsCfg.home}" "$dir")"
228 grep --quiet --line-regexp "$jenkinsjobname" "$cur_decl_jobs" 2>/dev/null && continue
229 jobname=$(getJobname "$jenkinsjobname")
230 echo "Deleting stale job \"$jobname\""
231 jobdir="${jenkinsCfg.home}/$jenkinsjobname"
232 rm -rf "$jobdir"
233 done
234 '' + (if cfg.accessUser != "" then reloadScript else "");
235 serviceConfig = {
236 User = jenkinsCfg.user;
237 RuntimeDirectory = "jenkins-job-builder";
238 };
239 };
240 };
241}