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