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}