1 { config, lib, pkgs, ... }:
4 cfg = config.services.buildkite-agents;
8 mkHookEntry = name: text: ''
9 ln --symbolic ${pkgs.writeShellApplication { inherit name text; }}/bin/${name} $out/${name}
12 pkgs.runCommand "buildkite-agent-hooks" {
13 preferLocalBuild = true;
16 ${lib.concatStringsSep "\n" (lib.mapAttrsToList mkHookEntry hooks)}
19 buildkiteOptions = { name ? "", config, ... }: {
21 enable = lib.mkOption {
23 type = lib.types.bool;
24 description = "Whether to enable this buildkite agent";
27 package = lib.mkOption {
28 default = pkgs.buildkite-agent;
29 defaultText = lib.literalExpression "pkgs.buildkite-agent";
30 description = "Which buildkite-agent derivation to use";
31 type = lib.types.package;
34 dataDir = lib.mkOption {
35 default = "/var/lib/buildkite-agent-${name}";
36 description = "The workdir for the agent";
40 extraGroups = lib.mkOption {
42 description = "Groups the user for this buildkite agent should belong to";
43 type = lib.types.listOf lib.types.str;
46 runtimePackages = lib.mkOption {
47 default = [ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ];
48 defaultText = lib.literalExpression "[ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ]";
49 description = "Add programs to the buildkite-agent environment";
50 type = lib.types.listOf lib.types.package;
53 tokenPath = lib.mkOption {
54 type = lib.types.path;
56 The token from your Buildkite "Agents" page.
58 A run-time path to the token file, which is supposed to be provisioned
65 default = "%hostname-${name}-%n";
67 The name of the agent as seen in the buildkite dashboard.
72 type = lib.types.attrsOf (lib.types.either lib.types.str (lib.types.listOf lib.types.str));
74 example = { queue = "default"; docker = "true"; ruby2 = "true"; };
80 extraConfig = lib.mkOption {
81 type = lib.types.lines;
83 example = "debug=true";
85 Extra lines to be added verbatim to the configuration file.
89 privateSshKeyPath = lib.mkOption {
90 type = lib.types.nullOr lib.types.path;
92 ## maximum care is taken so that secrets (ssh keys and the CI token)
93 ## don't end up in the Nix store.
94 apply = final: if final == null then null else toString final;
99 A run-time path to the key file, which is supposed to be provisioned
100 outside of Nix store.
104 hooks = lib.mkOption {
105 type = lib.types.attrsOf lib.types.lines;
107 example = lib.literalExpression ''
110 export SECRET_VAR=`head -1 /run/keys/secret`
114 "Agent" hooks to install.
115 See <https://buildkite.com/docs/agent/v3/hooks> for possible options.
119 hooksPath = lib.mkOption {
120 type = lib.types.path;
121 default = hooksDir config.hooks;
122 defaultText = lib.literalMD "generated from {option}`services.buildkite-agents.<name>.hooks`";
124 Path to the directory storing the hooks.
125 Consider using {option}`services.buildkite-agents.<name>.hooks.<name>`
130 shell = lib.mkOption {
131 type = lib.types.str;
132 default = "${pkgs.bash}/bin/bash -e -c";
133 defaultText = lib.literalExpression ''"''${pkgs.bash}/bin/bash -e -c"'';
135 Command that buildkite-agent 3 will execute when it spawns a shell.
140 enabledAgents = lib.filterAttrs (n: v: v.enable) cfg;
141 mapAgents = function: lib.mkMerge (lib.mapAttrsToList function enabledAgents);
144 options.services.buildkite-agents = lib.mkOption {
145 type = lib.types.attrsOf (lib.types.submodule buildkiteOptions);
148 Attribute set of buildkite agents.
149 The attribute key is combined with the hostname and a unique integer to
150 create the final agent name. This can be overridden by setting the `name`
155 config.users.users = mapAgents (name: cfg: {
156 "buildkite-agent-${name}" = {
157 name = "buildkite-agent-${name}";
160 description = "Buildkite agent user";
161 extraGroups = cfg.extraGroups;
163 group = "buildkite-agent-${name}";
166 config.users.groups = mapAgents (name: cfg: {
167 "buildkite-agent-${name}" = { };
170 config.systemd.services = mapAgents (name: cfg: {
171 "buildkite-agent-${name}" = {
172 description = "Buildkite Agent";
173 wantedBy = [ "multi-user.target" ];
174 after = [ "network.target" ];
175 path = cfg.runtimePackages ++ [ cfg.package pkgs.coreutils ];
176 environment = config.networking.proxy.envVars // {
178 NIX_REMOTE = "daemon";
181 ## NB: maximum care is taken so that secrets (ssh keys and the CI token)
182 ## don't end up in the Nix store.
185 sshDir = "${cfg.dataDir}/.ssh";
186 tagStr = name: value:
188 then lib.concatStringsSep "," (builtins.map (v: "${name}=${v}") value)
189 else "${name}=${value}";
190 tagsStr = lib.concatStringsSep "," (lib.mapAttrsToList tagStr cfg.tags);
192 lib.optionalString (cfg.privateSshKeyPath != null) ''
193 mkdir -m 0700 -p "${sshDir}"
194 install -m600 "${toString cfg.privateSshKeyPath}" "${sshDir}/id_rsa"
196 cat > "${cfg.dataDir}/buildkite-agent.cfg" <<EOF
197 token="$(cat ${toString cfg.tokenPath})"
201 build-path="${cfg.dataDir}/builds"
202 hooks-path="${cfg.hooksPath}"
208 ExecStart = "${cfg.package}/bin/buildkite-agent start --config ${cfg.dataDir}/buildkite-agent.cfg";
209 User = "buildkite-agent-${name}";
211 Restart = "on-failure";
213 # set a long timeout to give buildkite-agent a chance to finish current builds
214 TimeoutStopSec = "2 min";
220 config.assertions = mapAgents (name: cfg: [{
221 assertion = cfg.hooksPath != hooksDir cfg.hooks -> cfg.hooks == { };
223 Options `services.buildkite-agents.${name}.hooksPath' and
224 `services.buildkite-agents.${name}.hooks.<name>' are mutually exclusive.