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