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.runCommandLocal "buildkite-agent-hooks" { } ''
14 ${lib.concatStringsSep "\n" (lib.mapAttrsToList mkHookEntry hooks)}
17 buildkiteOptions = { name ? "", config, ... }: {
19 enable = lib.mkOption {
21 type = lib.types.bool;
22 description = "Whether to enable this buildkite agent";
25 package = lib.mkOption {
26 default = pkgs.buildkite-agent;
27 defaultText = lib.literalExpression "pkgs.buildkite-agent";
28 description = "Which buildkite-agent derivation to use";
29 type = lib.types.package;
32 dataDir = lib.mkOption {
33 default = "/var/lib/buildkite-agent-${name}";
34 description = "The workdir for the agent";
38 extraGroups = lib.mkOption {
40 description = "Groups the user for this buildkite agent should belong to";
41 type = lib.types.listOf lib.types.str;
44 runtimePackages = lib.mkOption {
45 default = [ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ];
46 defaultText = lib.literalExpression "[ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ]";
47 description = "Add programs to the buildkite-agent environment";
48 type = lib.types.listOf lib.types.package;
51 tokenPath = lib.mkOption {
52 type = lib.types.path;
54 The token from your Buildkite "Agents" page.
56 A run-time path to the token file, which is supposed to be provisioned
63 default = "%hostname-${name}-%n";
65 The name of the agent as seen in the buildkite dashboard.
70 type = lib.types.attrsOf (lib.types.either lib.types.str (lib.types.listOf lib.types.str));
72 example = { queue = "default"; docker = "true"; ruby2 = "true"; };
78 extraConfig = lib.mkOption {
79 type = lib.types.lines;
81 example = "debug=true";
83 Extra lines to be added verbatim to the configuration file.
87 privateSshKeyPath = lib.mkOption {
88 type = lib.types.nullOr lib.types.path;
90 ## maximum care is taken so that secrets (ssh keys and the CI token)
91 ## don't end up in the Nix store.
92 apply = final: if final == null then null else toString final;
97 A run-time path to the key file, which is supposed to be provisioned
102 hooks = lib.mkOption {
103 type = lib.types.attrsOf lib.types.lines;
105 example = lib.literalExpression ''
108 export SECRET_VAR=`head -1 /run/keys/secret`
112 "Agent" hooks to install.
113 See <https://buildkite.com/docs/agent/v3/hooks> for possible options.
117 hooksPath = lib.mkOption {
118 type = lib.types.path;
119 default = hooksDir config.hooks;
120 defaultText = lib.literalMD "generated from {option}`services.buildkite-agents.<name>.hooks`";
122 Path to the directory storing the hooks.
123 Consider using {option}`services.buildkite-agents.<name>.hooks.<name>`
128 shell = lib.mkOption {
129 type = lib.types.str;
130 default = "${pkgs.bash}/bin/bash -e -c";
131 defaultText = lib.literalExpression ''"''${pkgs.bash}/bin/bash -e -c"'';
133 Command that buildkite-agent 3 will execute when it spawns a shell.
138 enabledAgents = lib.filterAttrs (n: v: v.enable) cfg;
139 mapAgents = function: lib.mkMerge (lib.mapAttrsToList function enabledAgents);
142 options.services.buildkite-agents = lib.mkOption {
143 type = lib.types.attrsOf (lib.types.submodule buildkiteOptions);
146 Attribute set of buildkite agents.
147 The attribute key is combined with the hostname and a unique integer to
148 create the final agent name. This can be overridden by setting the `name`
153 config.users.users = mapAgents (name: cfg: {
154 "buildkite-agent-${name}" = {
155 name = "buildkite-agent-${name}";
158 description = "Buildkite agent user";
159 extraGroups = cfg.extraGroups;
161 group = "buildkite-agent-${name}";
164 config.users.groups = mapAgents (name: cfg: {
165 "buildkite-agent-${name}" = { };
168 config.systemd.services = mapAgents (name: cfg: {
169 "buildkite-agent-${name}" = {
170 description = "Buildkite Agent";
171 wantedBy = [ "multi-user.target" ];
172 after = [ "network.target" ];
173 path = cfg.runtimePackages ++ [ cfg.package pkgs.coreutils ];
174 environment = config.networking.proxy.envVars // {
176 NIX_REMOTE = "daemon";
179 ## NB: maximum care is taken so that secrets (ssh keys and the CI token)
180 ## don't end up in the Nix store.
183 sshDir = "${cfg.dataDir}/.ssh";
184 tagStr = name: value:
186 then lib.concatStringsSep "," (builtins.map (v: "${name}=${v}") value)
187 else "${name}=${value}";
188 tagsStr = lib.concatStringsSep "," (lib.mapAttrsToList tagStr cfg.tags);
190 lib.optionalString (cfg.privateSshKeyPath != null) ''
191 mkdir -m 0700 -p "${sshDir}"
192 install -m600 "${toString cfg.privateSshKeyPath}" "${sshDir}/id_rsa"
194 cat > "${cfg.dataDir}/buildkite-agent.cfg" <<EOF
195 token="$(cat ${toString cfg.tokenPath})"
199 build-path="${cfg.dataDir}/builds"
200 hooks-path="${cfg.hooksPath}"
206 ExecStart = "${cfg.package}/bin/buildkite-agent start --config ${cfg.dataDir}/buildkite-agent.cfg";
207 User = "buildkite-agent-${name}";
209 Restart = "on-failure";
211 # set a long timeout to give buildkite-agent a chance to finish current builds
212 TimeoutStopSec = "2 min";
218 config.assertions = mapAgents (name: cfg: [{
219 assertion = cfg.hooksPath != hooksDir cfg.hooks -> cfg.hooks == { };
221 Options `services.buildkite-agents.${name}.hooksPath' and
222 `services.buildkite-agents.${name}.hooks.<name>' are mutually exclusive.