vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / continuous-integration / jenkins / job-builder.nix
blobccad25cb92a7360832cc38dfa94e438ce4025c4d
1 { config, lib, pkgs, ... }:
2 let
3   jenkinsCfg = config.services.jenkins;
4   cfg = config.services.jenkins.jobBuilder;
6 in {
7   options = {
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
14         unchanged.
16         Note that it really is declarative configuration; if you remove a
17         previously defined job, the corresponding job directory will be
18         deleted.
20         Please see the Jenkins Job Builder documentation for more info:
21         <https://jenkins-job-builder.readthedocs.io/>
22       '';
24       accessUser = lib.mkOption {
25         default = "admin";
26         type = lib.types.str;
27         description = ''
28           User id in Jenkins used to reload config.
29         '';
30       };
32       accessToken = lib.mkOption {
33         default = "";
34         type = lib.types.str;
35         description = ''
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.
39         '';
40       };
42       accessTokenFile = lib.mkOption {
43         default = "${config.services.jenkins.home}/secrets/initialAdminPassword";
44         defaultText = lib.literalExpression ''"''${config.services.jenkins.home}/secrets/initialAdminPassword"'';
45         type = lib.types.str;
46         example = "/run/keys/jenkins-job-builder-access-token";
47         description = ''
48           File containing the API token for the {option}`accessUser`
49           user.
50         '';
51       };
53       yamlJobs = lib.mkOption {
54         default = "";
55         type = lib.types.lines;
56         example = ''
57           - job:
58               name: jenkins-job-test-1
59               builders:
60                 - shell: echo 'Hello world!'
61         '';
62         description = ''
63           Job descriptions for Jenkins Job Builder in YAML format.
64         '';
65       };
67       jsonJobs = lib.mkOption {
68         default = [ ];
69         type = lib.types.listOf lib.types.str;
70         example = lib.literalExpression ''
71           [
72             '''
73               [ { "job":
74                   { "name": "jenkins-job-test-2",
75                     "builders": [ "shell": "echo 'Hello world!'" ]
76                   }
77                 }
78               ]
79             '''
80           ]
81         '';
82         description = ''
83           Job descriptions for Jenkins Job Builder in JSON format.
84         '';
85       };
87       nixJobs = lib.mkOption {
88         default = [ ];
89         type = lib.types.listOf lib.types.attrs;
90         example = lib.literalExpression ''
91           [ { job =
92               { name = "jenkins-job-test-3";
93                 builders = [
94                   { shell = "echo 'Hello world!'"; }
95                 ];
96               };
97             }
98           ]
99         '';
100         description = ''
101           Job descriptions for Jenkins Job Builder in Nix format.
103           This is a trivial wrapper around jsonJobs, using builtins.toJSON
104           behind the scene.
105         '';
106       };
107     };
108   };
110   config = lib.mkIf (jenkinsCfg.enable && cfg.enable) {
111     assertions = [
112       { assertion =
113           if cfg.accessUser != ""
114           then (cfg.accessToken != "" && cfg.accessTokenFile == "") ||
115                (cfg.accessToken == "" && cfg.accessTokenFile != "")
116           else true;
117         message = ''
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}"
122         '';
123       }
124     ];
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
138       #    authentication.
139       script =
140         let
141           yamlJobsFile = builtins.toFile "jobs.yaml" cfg.yamlJobs;
142           jsonJobsFiles =
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";
149           reloadScript = ''
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")
157             fi
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"
166                 exit 1
167             fi
168           '';
169         in
170           ''
171             joinByString()
172             {
173                 local separator="$1"
174                 shift
175                 local first="$1"
176                 shift
177                 printf "%s" "$first" "''${@/#/$separator}"
178             }
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/".
183             getJenkinsJobDir()
184             {
185                 IFS='/' read -ra input_dirs <<< "$1"
186                 printf "jobs/"
187                 joinByString "/jobs/" "''${input_dirs[@]}"
188             }
190             # The inverse of getJenkinsJobDir (remove the "jobs/" prefixes)
191             getJobname()
192             {
193                 IFS='/' read -ra input_dirs <<< "$1"
194                 local i=0
195                 local nelem=''${#input_dirs[@]}
196                 for e in "''${input_dirs[@]}"; do
197                     if [ $((i % 2)) -eq 1 ]; then
198                         printf "$e"
199                         if [ $i -lt $(( nelem - 1 )) ]; then
200                             printf "/"
201                         fi
202                     fi
203                     i=$((i + 1))
204                 done
205             }
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"
215             done
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"
226             done
228             # Remove stale 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"
235                 rm -rf "$jobdir"
236             done
237           '' + (lib.optionalString (cfg.accessUser != "") reloadScript);
238       serviceConfig = {
239         Type = "oneshot";
240         User = jenkinsCfg.user;
241         RuntimeDirectory = "jenkins-job-builder";
242       };
243     };
244   };