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