1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 jenkinsCfg = config.services.jenkins; 9 cfg = config.services.jenkins.jobBuilder; 10 11in 12{ 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. 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 <https://jenkins-job-builder.readthedocs.io/> 28 ''; 29 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 }; 37 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 }; 47 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 }; 58 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 }; 72 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 }; 92 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. 108 109 This is a trivial wrapper around jsonJobs, using builtins.toJSON 110 behind the scene. 111 ''; 112 }; 113 }; 114 }; 115 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 ]; 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; [ 142 jenkins-job-builder 143 curl 144 ]; 145 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" 177 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 } 193 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 } 203 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 } 220 221 rm -rf ${jobBuilderOutputDir} 222 cur_decl_jobs=/run/jenkins-job-builder/declarative-jobs 223 rm -f "$cur_decl_jobs" 224 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 230 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 241 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 }; 260}