summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/module-list.nix2
-rw-r--r--modules/services/buildkite-agent.nix253
-rw-r--r--modules/services/buildkite-agents.nix293
-rw-r--r--tests/services-buildkite-agent.nix11
4 files changed, 299 insertions, 260 deletions
diff --git a/modules/module-list.nix b/modules/module-list.nix
index 1be337d..a226bba 100644
--- a/modules/module-list.nix
+++ b/modules/module-list.nix
@@ -44,7 +44,7 @@
./launchd
./services/activate-system
./services/autossh.nix
- ./services/buildkite-agent.nix
+ ./services/buildkite-agents.nix
./services/chunkwm.nix
./services/cachix-agent.nix
./services/dnsmasq.nix
diff --git a/modules/services/buildkite-agent.nix b/modules/services/buildkite-agent.nix
deleted file mode 100644
index ace89ec..0000000
--- a/modules/services/buildkite-agent.nix
+++ /dev/null
@@ -1,253 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
- cfg = config.services.buildkite-agent;
-
- mkHookOption = { name, description, example ? null }: {
- inherit name;
- value = mkOption {
- default = null;
- inherit description;
- type = types.nullOr types.lines;
- } // (if example == null then {} else { inherit example; });
- };
- mkHookOptions = hooks: listToAttrs (map mkHookOption hooks);
-
- hooksDir = let
- mkHookEntry = name: value: {
- inherit name;
- path = pkgs.writeScript "buildkite-agent-hook-${name}" ''
- #! ${pkgs.stdenv.shell}
- set -e
- ${value}
- '';
- };
- in pkgs.linkFarm "buildkite-agent-hooks"
- (mapAttrsToList mkHookEntry (filterAttrs (n: v: v != null) cfg.hooks));
-
-in
-
-{
- options = {
- services.buildkite-agent.enable = mkEnableOption "buildkite-agent";
-
- services.buildkite-agent.package = mkOption {
- default = pkgs.buildkite-agent;
- defaultText = "pkgs.buildkite-agent";
- description = "Which buildkite-agent derivation to use";
- type = types.package;
- };
-
- services.buildkite-agent.dataDir = mkOption {
- default = "/var/lib/buildkite-agent";
- description = "The workdir for the agent";
- type = types.str;
- };
-
- services.buildkite-agent.runtimePackages = mkOption {
- default = [ pkgs.bash pkgs.nix ];
- defaultText = "[ pkgs.bash pkgs.nix ]";
- description = "Add programs to the buildkite-agent environment";
- type = types.listOf types.package;
- };
-
- services.buildkite-agent.tokenPath = mkOption {
- type = types.path;
- description = ''
- The token from your Buildkite "Agents" page.
-
- A run-time path to the token file, which is supposed to be provisioned
- outside of Nix store.
- '';
- };
-
- services.buildkite-agent.name = mkOption {
- type = types.str;
- default = "%hostname-%n";
- description = ''
- The name of the agent.
- '';
- };
-
- services.buildkite-agent.meta-data = mkOption {
- type = types.str;
- default = "";
- example = "queue=default,docker=true,ruby2=true";
- description = ''
- Meta data for the agent. This is a comma-separated list of
- <code>key=value</code> pairs.
- '';
- };
-
- services.buildkite-agent.extraConfig = mkOption {
- type = types.lines;
- default = "";
- example = "debug=true";
- description = ''
- Extra lines to be added verbatim to the configuration file.
- '';
- };
- services.buildkite-agent.preCommands = mkOption {
- type = types.lines;
- default = "";
- description = ''
- Extra commands to run before starting buildkite.
- '';
- };
-
- services.buildkite-agent.openssh =
- { privateKeyPath = mkOption {
- type = types.path;
- description = ''
- Private agent key.
-
- A run-time path to the key file, which is supposed to be provisioned
- outside of Nix store.
- '';
- };
- publicKeyPath = mkOption {
- type = types.path;
- description = ''
- Public agent key.
-
- A run-time path to the key file, which is supposed to be provisioned
- outside of Nix store.
- '';
- };
- };
-
- services.buildkite-agent.hooks = mkHookOptions [
- { name = "checkout";
- description = ''
- The `checkout` hook script will replace the default checkout routine of the
- bootstrap.sh script. You can use this hook to do your own SCM checkout
- behaviour
- ''; }
- { name = "command";
- description = ''
- The `command` hook script will replace the default implementation of running
- the build command.
- ''; }
- { name = "environment";
- description = ''
- The `environment` hook will run before all other commands, and can be used
- to set up secrets, data, etc. Anything exported in hooks will be available
- to the build script.
-
- Note: the contents of this file will be copied to the world-readable
- Nix store.
- '';
- example = ''
- export SECRET_VAR=`head -1 /run/keys/secret`
- ''; }
- { name = "post-artifact";
- description = ''
- The `post-artifact` hook will run just after artifacts are uploaded
- ''; }
- { name = "post-checkout";
- description = ''
- The `post-checkout` hook will run after the bootstrap script has checked out
- your projects source code.
- ''; }
- { name = "post-command";
- description = ''
- The `post-command` hook will run after the bootstrap script has run your
- build commands
- ''; }
- { name = "pre-artifact";
- description = ''
- The `pre-artifact` hook will run just before artifacts are uploaded
- ''; }
- { name = "pre-checkout";
- description = ''
- The `pre-checkout` hook will run just before your projects source code is
- checked out from your SCM provider
- ''; }
- { name = "pre-command";
- description = ''
- The `pre-command` hook will run just before your build command runs
- ''; }
- { name = "pre-exit";
- description = ''
- The `pre-exit` hook will run just before your build job finishes
- ''; }
- ];
- };
-
- config = mkIf config.services.buildkite-agent.enable {
- users.users.buildkite-agent =
- { name = "buildkite-agent";
- home = cfg.dataDir;
- description = "Buildkite agent user";
- };
- users.groups.buildkite-agent =
- { name = "buildkite-agent";
- description = "Buildkite agent user group";
- };
-
- environment.systemPackages = [ cfg.package ];
-
- launchd.daemons.buildkite-agent =
- {
- path = cfg.runtimePackages ++ [ pkgs.coreutils cfg.package ]
- ++ (if pkgs.stdenv.isDarwin then [ pkgs.darwin.DarwinTools ] else []);
- environment = {
- HOME = cfg.dataDir;
- NIX_SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
- } // (if config.nix.useDaemon then { NIX_REMOTE = "daemon"; } else {});
-
- ## NB: maximum care is taken so that secrets (ssh keys and the CI token)
- ## don't end up in the Nix store.
- script = let
- sshDir = "${cfg.dataDir}/.ssh";
- in
- ''
- mkdir -m 0700 -p "${sshDir}"
- cp -f "${toString cfg.openssh.privateKeyPath}" "${sshDir}/id_rsa"
- cp -f "${toString cfg.openssh.publicKeyPath}" "${sshDir}/id_rsa.pub"
- chmod 600 "${sshDir}"/id_rsa*
-
- cat > "${cfg.dataDir}/buildkite-agent.cfg" <<EOF
- token="$(cat ${toString cfg.tokenPath})"
- name="${cfg.name}"
- meta-data="${cfg.meta-data}"
- build-path="${cfg.dataDir}/builds"
- hooks-path="${hooksDir}"
- ${cfg.extraConfig}
- EOF
-
- # Secrets exist in the buildkite-agent home directory
- chmod 750 "${cfg.dataDir}"
- chmod 640 "${cfg.dataDir}/buildkite-agent.cfg"
-
- ${cfg.preCommands}
-
- exec buildkite-agent start --config "${cfg.dataDir}/buildkite-agent.cfg"
- '';
-
- serviceConfig = {
- ProcessType = "Interactive";
- ThrottleInterval = 30;
-
- # The combination of KeepAlive.NetworkState and WatchPaths
- # will ensure that buildkite-agent is started on boot, but
- # after networking is available (so the hostname is
- # correct).
- RunAtLoad = true;
- KeepAlive.NetworkState = true;
- WatchPaths = [
- "/etc/resolv.conf"
- "/Library/Preferences/SystemConfiguration/NetworkInterfaces.plist"
- ];
-
- GroupName = "buildkite-agent";
- UserName = "buildkite-agent";
- WorkingDirectory = config.users.users.buildkite-agent.home;
- StandardErrorPath = "${cfg.dataDir}/buildkite-agent.log";
- StandardOutPath = "${cfg.dataDir}/buildkite-agent.log";
- };
- };
- };
-}
diff --git a/modules/services/buildkite-agents.nix b/modules/services/buildkite-agents.nix
new file mode 100644
index 0000000..a8931bd
--- /dev/null
+++ b/modules/services/buildkite-agents.nix
@@ -0,0 +1,293 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.buildkite-agents;
+ mdDoc = lib.mdDoc or (x: "Documentation not rendered. Please upgrade to a newer NixOS with markdown support.");
+ literalMD = lib.literalMD or (x: lib.literalDocBook "Documentation not rendered. Please upgrade to a newer NixOS with markdown support.");
+
+ mkHookOption = { name, description, example ? null }: {
+ inherit name;
+ value = mkOption {
+ default = null;
+ description = mdDoc description;
+ type = types.nullOr types.lines;
+ } // (if example == null then {} else { inherit example; });
+ };
+ mkHookOptions = hooks: listToAttrs (map mkHookOption hooks);
+
+ hooksDir = cfg: let
+ mkHookEntry = name: value: ''
+ cat > $out/${name} <<'EOF'
+ #! ${pkgs.runtimeShell}
+ set -e
+ ${value}
+ EOF
+ chmod 755 $out/${name}
+ '';
+ in pkgs.runCommand "buildkite-agent-hooks" { preferLocalBuild = true; } ''
+ mkdir $out
+ ${concatStringsSep "\n" (mapAttrsToList mkHookEntry (filterAttrs (n: v: v != null) cfg.hooks))}
+ '';
+
+ buildkiteOptions = { name ? "", config, ... }: {
+ options = {
+ enable = mkOption {
+ default = true;
+ type = types.bool;
+ description = mdDoc "Whether to enable this buildkite agent";
+ };
+
+ package = mkOption {
+ default = pkgs.buildkite-agent;
+ defaultText = literalExpression "pkgs.buildkite-agent";
+ description = mdDoc "Which buildkite-agent derivation to use";
+ type = types.package;
+ };
+
+ dataDir = mkOption {
+ default = "/var/lib/buildkite-agent-${name}";
+ description = mdDoc "The workdir for the agent";
+ type = types.str;
+ };
+
+ runtimePackages = mkOption {
+ default = [ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ];
+ defaultText = literalExpression "[ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ]";
+ description = mdDoc "Add programs to the buildkite-agent environment";
+ type = types.listOf types.package;
+ };
+
+ tokenPath = mkOption {
+ type = types.path;
+ description = mdDoc ''
+ The token from your Buildkite "Agents" page.
+
+ A run-time path to the token file, which is supposed to be provisioned
+ outside of Nix store.
+ '';
+ };
+
+ name = mkOption {
+ type = types.str;
+ default = "%hostname-${name}-%n";
+ description = mdDoc ''
+ The name of the agent as seen in the buildkite dashboard.
+ '';
+ };
+
+ tags = mkOption {
+ type = types.attrsOf (types.either types.str (types.listOf types.str));
+ default = {};
+ example = { queue = "default"; docker = "true"; ruby2 ="true"; };
+ description = mdDoc ''
+ Tags for the agent.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ example = "debug=true";
+ description = mdDoc ''
+ Extra lines to be added verbatim to the configuration file.
+ '';
+ };
+
+ preCommands = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Extra commands to run before starting buildkite.
+ '';
+ };
+
+ privateSshKeyPath = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ ## maximum care is taken so that secrets (ssh keys and the CI token)
+ ## don't end up in the Nix store.
+ apply = final: if final == null then null else toString final;
+
+ description = mdDoc ''
+ OpenSSH private key
+
+ A run-time path to the key file, which is supposed to be provisioned
+ outside of Nix store.
+ '';
+ };
+
+ hooks = mkHookOptions [
+ { name = "checkout";
+ description = ''
+ The `checkout` hook script will replace the default checkout routine of the
+ bootstrap.sh script. You can use this hook to do your own SCM checkout
+ behaviour
+ ''; }
+ { name = "command";
+ description = ''
+ The `command` hook script will replace the default implementation of running
+ the build command.
+ ''; }
+ { name = "environment";
+ description = ''
+ The `environment` hook will run before all other commands, and can be used
+ to set up secrets, data, etc. Anything exported in hooks will be available
+ to the build script.
+
+ Note: the contents of this file will be copied to the world-readable
+ Nix store.
+ '';
+ example = ''
+ export SECRET_VAR=`head -1 /run/keys/secret`
+ ''; }
+ { name = "post-artifact";
+ description = ''
+ The `post-artifact` hook will run just after artifacts are uploaded
+ ''; }
+ { name = "post-checkout";
+ description = ''
+ The `post-checkout` hook will run after the bootstrap script has checked out
+ your projects source code.
+ ''; }
+ { name = "post-command";
+ description = ''
+ The `post-command` hook will run after the bootstrap script has run your
+ build commands
+ ''; }
+ { name = "pre-artifact";
+ description = ''
+ The `pre-artifact` hook will run just before artifacts are uploaded
+ ''; }
+ { name = "pre-checkout";
+ description = ''
+ The `pre-checkout` hook will run just before your projects source code is
+ checked out from your SCM provider
+ ''; }
+ { name = "pre-command";
+ description = ''
+ The `pre-command` hook will run just before your build command runs
+ ''; }
+ { name = "pre-exit";
+ description = ''
+ The `pre-exit` hook will run just before your build job finishes
+ ''; }
+ ];
+
+ hooksPath = mkOption {
+ type = types.path;
+ default = hooksDir config;
+ defaultText = literalMD "generated from {option}`services.buildkite-agents.<name>.hooks`";
+ description = mdDoc ''
+ Path to the directory storing the hooks.
+ Consider using {option}`services.buildkite-agents.<name>.hooks.<name>`
+ instead.
+ '';
+ };
+
+ shell = mkOption {
+ type = types.str;
+ default = "${pkgs.bash}/bin/bash -e -c";
+ defaultText = literalExpression ''"''${pkgs.bash}/bin/bash -e -c"'';
+ description = mdDoc ''
+ Command that buildkite-agent 3 will execute when it spawns a shell.
+ '';
+ };
+ };
+ };
+ enabledAgents = lib.filterAttrs (n: v: v.enable) cfg;
+ mapAgents = function: lib.mkMerge (lib.mapAttrsToList function enabledAgents);
+in
+{
+ options.services.buildkite-agents = mkOption {
+ type = types.attrsOf (types.submodule buildkiteOptions);
+ default = {};
+ description = mdDoc ''
+ Attribute set of buildkite agents.
+ The attribute key is combined with the hostname and a unique integer to
+ create the final agent name. This can be overridden by setting the `name`
+ attribute.
+ '';
+ };
+
+ config.users.users = mapAgents (name: cfg: {
+ "buildkite-agent-${name}" = {
+ name = "buildkite-agent-${name}";
+ home = cfg.dataDir;
+ createHome = true;
+ description = "Buildkite agent user";
+ };
+ });
+ config.users.groups = mapAgents (name: cfg: {
+ "buildkite-agent-${name}" = {};
+ });
+
+ config.launchd.daemons = mapAgents (name: cfg: {
+ "buildkite-agent-${name}" =
+ { path = cfg.runtimePackages ++ [ cfg.package pkgs.coreutils pkgs.darwin.DarwinTools ];
+ environment = {
+ HOME = cfg.dataDir;
+ }// (if config.nix.useDaemon then { NIX_REMOTE = "daemon"; } else {});
+
+ ## NB: maximum care is taken so that secrets (ssh keys and the CI token)
+ ## don't end up in the Nix store.
+ script = let
+ sshDir = "${cfg.dataDir}/.ssh";
+ tagStr = lib.concatStringsSep "," (lib.mapAttrsToList (name: value: "${name}=${value}") cfg.tags);
+ in
+ optionalString (cfg.privateSshKeyPath != null) ''
+ mkdir -m 0700 -p "${sshDir}"
+ install -m600 "${toString cfg.privateSshKeyPath}" "${sshDir}/id_rsa"
+ '' + ''
+ cat > "${cfg.dataDir}/buildkite-agent.cfg" <<EOF
+ token="$(cat ${toString cfg.tokenPath})"
+ name="${cfg.name}"
+ shell="${cfg.shell}"
+ tags="${tagStr}"
+ build-path="${cfg.dataDir}/builds"
+ hooks-path="${cfg.hooksPath}"
+ ${cfg.extraConfig}
+ EOF
+
+ ${cfg.preCommands}
+
+ ${cfg.package}/bin/buildkite-agent start --config ${cfg.dataDir}/buildkite-agent.cfg
+ '';
+
+ serviceConfig = {
+ ProcessType = "Interactive";
+ ThrottleInterval = 30;
+
+ # The combination of KeepAlive.NetworkState and WatchPaths
+ # will ensure that buildkite-agent is started on boot, but
+ # after networking is available (so the hostname is
+ # correct).
+ RunAtLoad = true;
+ WatchPaths = [
+ "/etc/resolv.conf"
+ "/Library/Preferences/SystemConfiguration/NetworkInterfaces.plist"
+ ];
+
+ GroupName = "buildkite-agent-${name}";
+ UserName = "buildkite-agent-${name}";
+ WorkingDirectory = config.users.users."buildkite-agent-${name}".home;
+ StandardErrorPath = "${cfg.dataDir}/buildkite-agent.log";
+ StandardOutPath = "${cfg.dataDir}/buildkite-agent.log";
+ };
+ };
+ });
+
+ config.assertions = mapAgents (name: cfg: [
+ { assertion = cfg.hooksPath == (hooksDir cfg) || all (v: v == null) (attrValues cfg.hooks);
+ message = ''
+ Options `services.buildkite-agents.${name}.hooksPath' and
+ `services.buildkite-agents.${name}.hooks.<name>' are mutually exclusive.
+ '';
+ }
+ ]);
+
+ imports = [
+ (mkRemovedOptionModule [ "services" "buildkite-agent"] "services.buildkite-agent has been moved to an attribute set at services.buildkite-agents")
+ ];
+}
diff --git a/tests/services-buildkite-agent.nix b/tests/services-buildkite-agent.nix
index 4ca89b8..557aad2 100644
--- a/tests/services-buildkite-agent.nix
+++ b/tests/services-buildkite-agent.nix
@@ -6,22 +6,21 @@ let
in
{
- services.buildkite-agent = {
+ services.buildkite-agents.test = {
enable = true;
package = buildkite-agent;
extraConfig = "yolo=1";
- openssh.privateKeyPath = "/dev/null";
- openssh.publicKeyPath = "/dev/null";
+ privateSshKeyPath = "/dev/null";
hooks.command = "echo test hook";
inherit tokenPath;
};
test = ''
- echo "checking buildkite-agent service in /Library/LaunchDaemons" >&2
- grep "org.nixos.buildkite-agent" ${config.out}/Library/LaunchDaemons/org.nixos.buildkite-agent.plist
+ echo "checking buildkite-agent-test service in /Library/LaunchDaemons" >&2
+ grep "org.nixos.buildkite-agent-test" ${config.out}/Library/LaunchDaemons/org.nixos.buildkite-agent-test.plist
echo "checking creation of buildkite-agent service config" >&2
- script=$(cat ${config.out}/Library/LaunchDaemons/org.nixos.buildkite-agent.plist | awk -F'[< ]' '$3 ~ "^/nix/store/.*" {print $3}')
+ script=$(cat ${config.out}/Library/LaunchDaemons/org.nixos.buildkite-agent-test.plist | awk -F'[< ]' '$3 ~ "^/nix/store/.*" {print $3}')
grep "yolo=1" "$script"
grep "${tokenPath}" "$script"