diff options
Diffstat (limited to 'nixos/modules/services/continuous-integration/jenkins')
3 files changed, 558 insertions, 0 deletions
diff --git a/nixos/modules/services/continuous-integration/jenkins/default.nix b/nixos/modules/services/continuous-integration/jenkins/default.nix new file mode 100644 index 00000000000..d37dcb5519d --- /dev/null +++ b/nixos/modules/services/continuous-integration/jenkins/default.nix @@ -0,0 +1,249 @@ +{ config, lib, pkgs, ... }: +with lib; +let + cfg = config.services.jenkins; + jenkinsUrl = "http://${cfg.listenAddress}:${toString cfg.port}${cfg.prefix}"; +in { + options = { + services.jenkins = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable the jenkins continuous integration server. + ''; + }; + + user = mkOption { + default = "jenkins"; + type = types.str; + description = '' + User the jenkins server should execute under. + ''; + }; + + group = mkOption { + default = "jenkins"; + type = types.str; + description = '' + If the default user "jenkins" is configured then this is the primary + group of that user. + ''; + }; + + extraGroups = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "wheel" "dialout" ]; + description = '' + List of extra groups that the "jenkins" user should be a part of. + ''; + }; + + home = mkOption { + default = "/var/lib/jenkins"; + type = types.path; + description = '' + The path to use as JENKINS_HOME. If the default user "jenkins" is configured then + this is the home of the "jenkins" user. + ''; + }; + + listenAddress = mkOption { + default = "0.0.0.0"; + example = "localhost"; + type = types.str; + description = '' + Specifies the bind address on which the jenkins HTTP interface listens. + The default is the wildcard address. + ''; + }; + + port = mkOption { + default = 8080; + type = types.port; + description = '' + Specifies port number on which the jenkins HTTP interface listens. + The default is 8080. + ''; + }; + + prefix = mkOption { + default = ""; + example = "/jenkins"; + type = types.str; + description = '' + Specifies a urlPrefix to use with jenkins. + If the example /jenkins is given, the jenkins server will be + accessible using localhost:8080/jenkins. + ''; + }; + + package = mkOption { + default = pkgs.jenkins; + defaultText = literalExpression "pkgs.jenkins"; + type = types.package; + description = "Jenkins package to use."; + }; + + packages = mkOption { + default = [ pkgs.stdenv pkgs.git pkgs.jdk11 config.programs.ssh.package pkgs.nix ]; + defaultText = literalExpression "[ pkgs.stdenv pkgs.git pkgs.jdk11 config.programs.ssh.package pkgs.nix ]"; + type = types.listOf types.package; + description = '' + Packages to add to PATH for the jenkins process. + ''; + }; + + environment = mkOption { + default = { }; + type = with types; attrsOf str; + description = '' + Additional environment variables to be passed to the jenkins process. + As a base environment, jenkins receives NIX_PATH from + <option>environment.sessionVariables</option>, NIX_REMOTE is set to + "daemon" and JENKINS_HOME is set to the value of + <option>services.jenkins.home</option>. + This option has precedence and can be used to override those + mentioned variables. + ''; + }; + + plugins = mkOption { + default = null; + type = types.nullOr (types.attrsOf types.package); + description = '' + A set of plugins to activate. Note that this will completely + remove and replace any previously installed plugins. If you + have manually-installed plugins that you want to keep while + using this module, set this option to + <literal>null</literal>. You can generate this set with a + tool such as <literal>jenkinsPlugins2nix</literal>. + ''; + example = literalExpression '' + import path/to/jenkinsPlugins2nix-generated-plugins.nix { inherit (pkgs) fetchurl stdenv; } + ''; + }; + + extraOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--debug=9" ]; + description = '' + Additional command line arguments to pass to Jenkins. + ''; + }; + + extraJavaOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "-Xmx80m" ]; + description = '' + Additional command line arguments to pass to the Java run time (as opposed to Jenkins). + ''; + }; + + withCLI = mkOption { + type = types.bool; + default = false; + description = '' + Whether to make the CLI available. + + More info about the CLI available at + <link xlink:href="https://www.jenkins.io/doc/book/managing/cli"> + https://www.jenkins.io/doc/book/managing/cli</link> . + ''; + }; + }; + }; + + config = mkIf cfg.enable { + environment = { + # server references the dejavu fonts + systemPackages = [ + pkgs.dejavu_fonts + ] ++ optional cfg.withCLI cfg.package; + + variables = {} + // optionalAttrs cfg.withCLI { + # Make it more convenient to use the `jenkins-cli`. + JENKINS_URL = jenkinsUrl; + }; + }; + + users.groups = optionalAttrs (cfg.group == "jenkins") { + jenkins.gid = config.ids.gids.jenkins; + }; + + users.users = optionalAttrs (cfg.user == "jenkins") { + jenkins = { + description = "jenkins user"; + createHome = true; + home = cfg.home; + group = cfg.group; + extraGroups = cfg.extraGroups; + useDefaultShell = true; + uid = config.ids.uids.jenkins; + }; + }; + + systemd.services.jenkins = { + description = "Jenkins Continuous Integration Server"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment = + let + selectedSessionVars = + lib.filterAttrs (n: v: builtins.elem n [ "NIX_PATH" ]) + config.environment.sessionVariables; + in + selectedSessionVars // + { JENKINS_HOME = cfg.home; + NIX_REMOTE = "daemon"; + } // + cfg.environment; + + path = cfg.packages; + + # Force .war (re)extraction, or else we might run stale Jenkins. + + preStart = + let replacePlugins = + if cfg.plugins == null + then "" + else + let pluginCmds = lib.attrsets.mapAttrsToList + (n: v: "cp ${v} ${cfg.home}/plugins/${n}.jpi") + cfg.plugins; + in '' + rm -r ${cfg.home}/plugins || true + mkdir -p ${cfg.home}/plugins + ${lib.strings.concatStringsSep "\n" pluginCmds} + ''; + in '' + rm -rf ${cfg.home}/war + ${replacePlugins} + ''; + + # For reference: https://wiki.jenkins.io/display/JENKINS/JenkinsLinuxStartupScript + script = '' + ${pkgs.jdk11}/bin/java ${concatStringsSep " " cfg.extraJavaOptions} -jar ${cfg.package}/webapps/jenkins.war --httpListenAddress=${cfg.listenAddress} \ + --httpPort=${toString cfg.port} \ + --prefix=${cfg.prefix} \ + -Djava.awt.headless=true \ + ${concatStringsSep " " cfg.extraOptions} + ''; + + postStart = '' + until [[ $(${pkgs.curl.bin}/bin/curl -L -s --head -w '\n%{http_code}' ${jenkinsUrl} | tail -n1) =~ ^(200|403)$ ]]; do + sleep 1 + done + ''; + + serviceConfig = { + User = cfg.user; + }; + }; + }; +} diff --git a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix new file mode 100644 index 00000000000..3ca1542c18f --- /dev/null +++ b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix @@ -0,0 +1,241 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + jenkinsCfg = config.services.jenkins; + cfg = config.services.jenkins.jobBuilder; + +in { + options = { + services.jenkins.jobBuilder = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether or not to enable 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: + <link xlink:href="http://docs.openstack.org/infra/jenkins-job-builder/"> + http://docs.openstack.org/infra/jenkins-job-builder/</link> + ''; + }; + + accessUser = mkOption { + default = ""; + type = types.str; + description = '' + User id in Jenkins used to reload config. + ''; + }; + + accessToken = mkOption { + default = ""; + type = 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> option instead. + ''; + }; + + accessTokenFile = mkOption { + default = ""; + type = types.str; + example = "/run/keys/jenkins-job-builder-access-token"; + description = '' + File containing the API token for the <option>accessUser</option> + user. + ''; + }; + + yamlJobs = mkOption { + default = ""; + type = 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 = mkOption { + default = [ ]; + type = types.listOf types.str; + example = literalExpression '' + [ + ''' + [ { "job": + { "name": "jenkins-job-test-2", + "builders": [ "shell": "echo 'Hello world!'" ] + } + } + ] + ''' + ] + ''; + description = '' + Job descriptions for Jenkins Job Builder in JSON format. + ''; + }; + + nixJobs = mkOption { + default = [ ]; + type = types.listOf types.attrs; + example = 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 = 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=${if cfg.accessTokenFile != "" + then "$(cat '${cfg.accessTokenFile}')" + else cfg.accessToken} + jenkins_url="http://${cfg.accessUser}:$access_token@${jenkinsCfg.listenAddress}:${toString jenkinsCfg.port}${jenkinsCfg.prefix}" + crumb=$(curl $curl_opts "$jenkins_url"'/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)') + curl $curl_opts -X POST -H "$crumb" "$jenkins_url"/reload + ''; + 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} ${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 + '' + (if cfg.accessUser != "" then reloadScript else ""); + serviceConfig = { + User = jenkinsCfg.user; + RuntimeDirectory = "jenkins-job-builder"; + }; + }; + }; +} diff --git a/nixos/modules/services/continuous-integration/jenkins/slave.nix b/nixos/modules/services/continuous-integration/jenkins/slave.nix new file mode 100644 index 00000000000..3c0e6f78e74 --- /dev/null +++ b/nixos/modules/services/continuous-integration/jenkins/slave.nix @@ -0,0 +1,68 @@ +{ config, lib, ... }: +with lib; +let + cfg = config.services.jenkinsSlave; + masterCfg = config.services.jenkins; +in { + options = { + services.jenkinsSlave = { + # todo: + # * assure the profile of the jenkins user has a JRE and any specified packages. This would + # enable ssh slaves. + # * Optionally configure the node as a jenkins ad-hoc slave. This would imply configuration + # properties for the master node. + enable = mkOption { + type = types.bool; + default = false; + description = '' + If true the system will be configured to work as a jenkins slave. + If the system is also configured to work as a jenkins master then this has no effect. + In progress: Currently only assures the jenkins user is configured. + ''; + }; + + user = mkOption { + default = "jenkins"; + type = types.str; + description = '' + User the jenkins slave agent should execute under. + ''; + }; + + group = mkOption { + default = "jenkins"; + type = types.str; + description = '' + If the default slave agent user "jenkins" is configured then this is + the primary group of that user. + ''; + }; + + home = mkOption { + default = "/var/lib/jenkins"; + type = types.path; + description = '' + The path to use as JENKINS_HOME. If the default user "jenkins" is configured then + this is the home of the "jenkins" user. + ''; + }; + }; + }; + + config = mkIf (cfg.enable && !masterCfg.enable) { + users.groups = optionalAttrs (cfg.group == "jenkins") { + jenkins.gid = config.ids.gids.jenkins; + }; + + users.users = optionalAttrs (cfg.user == "jenkins") { + jenkins = { + description = "jenkins user"; + createHome = true; + home = cfg.home; + group = cfg.group; + useDefaultShell = true; + uid = config.ids.uids.jenkins; + }; + }; + }; +} |