nixpkgs/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
Silvan Mosberger 4f0dadbf38 treewide: format all inactive Nix files
After final improvements to the official formatter implementation,
this commit now performs the first treewide reformat of Nix files using it.
This is part of the implementation of RFC 166.

Only "inactive" files are reformatted, meaning only files that
aren't being touched by any PR with activity in the past 2 months.
This is to avoid conflicts for PRs that might soon be merged.
Later we can do a full treewide reformat to get the rest,
which should not cause as many conflicts.

A CI check has already been running for some time to ensure that new and
already-formatted files are formatted, so the files being reformatted here
should also stay formatted.

This commit was automatically created and can be verified using

    nix-build a08b3a4d19.tar.gz \
      --argstr baseRev b32a094368
    result/bin/apply-formatting $NIXPKGS_PATH
2024-12-10 20:26:33 +01:00

260 lines
9.1 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
jenkinsCfg = config.services.jenkins;
cfg = config.services.jenkins.jobBuilder;
in
{
options = {
services.jenkins.jobBuilder = {
enable = lib.mkEnableOption ''
the Jenkins Job Builder (JJB) service. It
allows defining jobs for Jenkins in a declarative manner.
Jobs managed through the Jenkins WebUI (or by other means) are left
unchanged.
Note that it really is declarative configuration; if you remove a
previously defined job, the corresponding job directory will be
deleted.
Please see the Jenkins Job Builder documentation for more info:
<https://jenkins-job-builder.readthedocs.io/>
'';
accessUser = lib.mkOption {
default = "admin";
type = lib.types.str;
description = ''
User id in Jenkins used to reload config.
'';
};
accessToken = lib.mkOption {
default = "";
type = lib.types.str;
description = ''
User token in Jenkins used to reload config.
WARNING: This token will be world readable in the Nix store. To keep
it secret, use the {option}`accessTokenFile` option instead.
'';
};
accessTokenFile = lib.mkOption {
default = "${config.services.jenkins.home}/secrets/initialAdminPassword";
defaultText = lib.literalExpression ''"''${config.services.jenkins.home}/secrets/initialAdminPassword"'';
type = lib.types.str;
example = "/run/keys/jenkins-job-builder-access-token";
description = ''
File containing the API token for the {option}`accessUser`
user.
'';
};
yamlJobs = lib.mkOption {
default = "";
type = lib.types.lines;
example = ''
- job:
name: jenkins-job-test-1
builders:
- shell: echo 'Hello world!'
'';
description = ''
Job descriptions for Jenkins Job Builder in YAML format.
'';
};
jsonJobs = lib.mkOption {
default = [ ];
type = lib.types.listOf lib.types.str;
example = lib.literalExpression ''
[
'''
[ { "job":
{ "name": "jenkins-job-test-2",
"builders": [ "shell": "echo 'Hello world!'" ]
}
}
]
'''
]
'';
description = ''
Job descriptions for Jenkins Job Builder in JSON format.
'';
};
nixJobs = lib.mkOption {
default = [ ];
type = lib.types.listOf lib.types.attrs;
example = lib.literalExpression ''
[ { job =
{ name = "jenkins-job-test-3";
builders = [
{ shell = "echo 'Hello world!'"; }
];
};
}
]
'';
description = ''
Job descriptions for Jenkins Job Builder in Nix format.
This is a trivial wrapper around jsonJobs, using builtins.toJSON
behind the scene.
'';
};
};
};
config = lib.mkIf (jenkinsCfg.enable && cfg.enable) {
assertions = [
{
assertion =
if cfg.accessUser != "" then
(cfg.accessToken != "" && cfg.accessTokenFile == "")
|| (cfg.accessToken == "" && cfg.accessTokenFile != "")
else
true;
message = ''
One of accessToken and accessTokenFile options must be non-empty
strings, but not both. Current values:
services.jenkins.jobBuilder.accessToken = "${cfg.accessToken}"
services.jenkins.jobBuilder.accessTokenFile = "${cfg.accessTokenFile}"
'';
}
];
systemd.services.jenkins-job-builder = {
description = "Jenkins Job Builder Service";
# JJB can run either before or after jenkins. We chose after, so we can
# always use curl to notify (running) jenkins to reload its config.
after = [ "jenkins.service" ];
wantedBy = [ "multi-user.target" ];
path = with pkgs; [
jenkins-job-builder
curl
];
# Q: Why manipulate files directly instead of using "jenkins-jobs upload [...]"?
# A: Because this module is for administering a local jenkins install,
# and using local file copy allows us to not worry about
# authentication.
script =
let
yamlJobsFile = builtins.toFile "jobs.yaml" cfg.yamlJobs;
jsonJobsFiles = map (x: (builtins.toFile "jobs.json" x)) (
cfg.jsonJobs ++ [ (builtins.toJSON cfg.nixJobs) ]
);
jobBuilderOutputDir = "/run/jenkins-job-builder/output";
# Stamp file is placed in $JENKINS_HOME/jobs/$JOB_NAME/ to indicate
# ownership. Enables tracking and removal of stale jobs.
ownerStamp = ".config-xml-managed-by-nixos-jenkins-job-builder";
reloadScript = ''
echo "Asking Jenkins to reload config"
curl_opts="--silent --fail --show-error"
access_token_file=${
if cfg.accessTokenFile != "" then
cfg.accessTokenFile
else
"$RUNTIME_DIRECTORY/jenkins_access_token.txt"
}
if [ "${cfg.accessToken}" != "" ]; then
(umask 0077; printf "${cfg.accessToken}" >"$access_token_file")
fi
jenkins_url="http://${jenkinsCfg.listenAddress}:${toString jenkinsCfg.port}${jenkinsCfg.prefix}"
auth_file="$RUNTIME_DIRECTORY/jenkins_auth_file.txt"
trap 'rm -f "$auth_file"' EXIT
(umask 0077; printf "${cfg.accessUser}:@password_placeholder@" >"$auth_file")
"${pkgs.replace-secret}/bin/replace-secret" "@password_placeholder@" "$access_token_file" "$auth_file"
if ! "${pkgs.jenkins}/bin/jenkins-cli" -s "$jenkins_url" -auth "@$auth_file" reload-configuration; then
echo "error: failed to reload configuration"
exit 1
fi
'';
in
''
joinByString()
{
local separator="$1"
shift
local first="$1"
shift
printf "%s" "$first" "''${@/#/$separator}"
}
# Map a relative directory path in the output from
# jenkins-job-builder (jobname) to the layout expected by jenkins:
# each directory level gets prepended "jobs/".
getJenkinsJobDir()
{
IFS='/' read -ra input_dirs <<< "$1"
printf "jobs/"
joinByString "/jobs/" "''${input_dirs[@]}"
}
# The inverse of getJenkinsJobDir (remove the "jobs/" prefixes)
getJobname()
{
IFS='/' read -ra input_dirs <<< "$1"
local i=0
local nelem=''${#input_dirs[@]}
for e in "''${input_dirs[@]}"; do
if [ $((i % 2)) -eq 1 ]; then
printf "$e"
if [ $i -lt $(( nelem - 1 )) ]; then
printf "/"
fi
fi
i=$((i + 1))
done
}
rm -rf ${jobBuilderOutputDir}
cur_decl_jobs=/run/jenkins-job-builder/declarative-jobs
rm -f "$cur_decl_jobs"
# Create / update jobs
mkdir -p ${jobBuilderOutputDir}
for inputFile in ${yamlJobsFile} ${lib.concatStringsSep " " jsonJobsFiles}; do
HOME="${jenkinsCfg.home}" "${pkgs.jenkins-job-builder}/bin/jenkins-jobs" --ignore-cache test --config-xml -o "${jobBuilderOutputDir}" "$inputFile"
done
find "${jobBuilderOutputDir}" -type f -name config.xml | while read -r f; do echo "$(dirname "$f")"; done | sort | while read -r dir; do
jobname="$(realpath --relative-to="${jobBuilderOutputDir}" "$dir")"
jenkinsjobname=$(getJenkinsJobDir "$jobname")
jenkinsjobdir="${jenkinsCfg.home}/$jenkinsjobname"
echo "Creating / updating job \"$jobname\""
mkdir -p "$jenkinsjobdir"
touch "$jenkinsjobdir/${ownerStamp}"
cp "$dir"/config.xml "$jenkinsjobdir/config.xml"
echo "$jenkinsjobname" >> "$cur_decl_jobs"
done
# Remove stale jobs
find "${jenkinsCfg.home}" -type f -name "${ownerStamp}" | while read -r f; do echo "$(dirname "$f")"; done | sort --reverse | while read -r dir; do
jenkinsjobname="$(realpath --relative-to="${jenkinsCfg.home}" "$dir")"
grep --quiet --line-regexp "$jenkinsjobname" "$cur_decl_jobs" 2>/dev/null && continue
jobname=$(getJobname "$jenkinsjobname")
echo "Deleting stale job \"$jobname\""
jobdir="${jenkinsCfg.home}/$jenkinsjobname"
rm -rf "$jobdir"
done
''
+ (lib.optionalString (cfg.accessUser != "") reloadScript);
serviceConfig = {
Type = "oneshot";
User = jenkinsCfg.user;
RuntimeDirectory = "jenkins-job-builder";
};
};
};
}