1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 cfg = config.services.buildkite-agents;
10
11 hooksDir =
12 hooks:
13 let
14 mkHookEntry = name: text: ''
15 ln --symbolic ${pkgs.writeShellApplication { inherit name text; }}/bin/${name} $out/${name}
16 '';
17 in
18 pkgs.runCommand "buildkite-agent-hooks"
19 {
20 preferLocalBuild = true;
21 }
22 ''
23 mkdir $out
24 ${lib.concatStringsSep "\n" (lib.mapAttrsToList mkHookEntry hooks)}
25 '';
26
27 buildkiteOptions =
28 {
29 name ? "",
30 config,
31 ...
32 }:
33 {
34 options = {
35 enable = lib.mkOption {
36 default = true;
37 type = lib.types.bool;
38 description = "Whether to enable this buildkite agent";
39 };
40
41 package = lib.mkOption {
42 default = pkgs.buildkite-agent;
43 defaultText = lib.literalExpression "pkgs.buildkite-agent";
44 description = "Which buildkite-agent derivation to use";
45 type = lib.types.package;
46 };
47
48 dataDir = lib.mkOption {
49 default = "/var/lib/buildkite-agent-${name}";
50 description = "The workdir for the agent";
51 type = lib.types.str;
52 };
53
54 extraGroups = lib.mkOption {
55 default = [ "keys" ];
56 description = "Groups the user for this buildkite agent should belong to";
57 type = lib.types.listOf lib.types.str;
58 };
59
60 runtimePackages = lib.mkOption {
61 default = [
62 pkgs.bash
63 pkgs.gnutar
64 pkgs.gzip
65 pkgs.git
66 pkgs.nix
67 ];
68 defaultText = lib.literalExpression "[ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ]";
69 description = "Add programs to the buildkite-agent environment";
70 type = lib.types.listOf lib.types.package;
71 };
72
73 tokenPath = lib.mkOption {
74 type = lib.types.path;
75 description = ''
76 The token from your Buildkite "Agents" page.
77
78 A run-time path to the token file, which is supposed to be provisioned
79 outside of Nix store.
80 '';
81 };
82
83 name = lib.mkOption {
84 type = lib.types.str;
85 default = "%hostname-${name}-%n";
86 description = ''
87 The name of the agent as seen in the buildkite dashboard.
88 '';
89 };
90
91 tags = lib.mkOption {
92 type = lib.types.attrsOf (lib.types.either lib.types.str (lib.types.listOf lib.types.str));
93 default = { };
94 example = {
95 queue = "default";
96 docker = "true";
97 ruby2 = "true";
98 };
99 description = ''
100 Tags for the agent.
101 '';
102 };
103
104 extraConfig = lib.mkOption {
105 type = lib.types.lines;
106 default = "";
107 example = "debug=true";
108 description = ''
109 Extra lines to be added verbatim to the configuration file.
110 '';
111 };
112
113 privateSshKeyPath = lib.mkOption {
114 type = lib.types.nullOr lib.types.path;
115 default = null;
116 ## maximum care is taken so that secrets (ssh keys and the CI token)
117 ## don't end up in the Nix store.
118 apply = final: if final == null then null else toString final;
119
120 description = ''
121 OpenSSH private key
122
123 A run-time path to the key file, which is supposed to be provisioned
124 outside of Nix store.
125 '';
126 };
127
128 hooks = lib.mkOption {
129 type = lib.types.attrsOf lib.types.lines;
130 default = { };
131 example = lib.literalExpression ''
132 {
133 environment = '''
134 export SECRET_VAR=`head -1 /run/keys/secret`
135 ''';
136 }'';
137 description = ''
138 "Agent" hooks to install.
139 See <https://buildkite.com/docs/agent/v3/hooks> for possible options.
140 '';
141 };
142
143 hooksPath = lib.mkOption {
144 type = lib.types.path;
145 default = hooksDir config.hooks;
146 defaultText = lib.literalMD "generated from {option}`services.buildkite-agents.<name>.hooks`";
147 description = ''
148 Path to the directory storing the hooks.
149 Consider using {option}`services.buildkite-agents.<name>.hooks.<name>`
150 instead.
151 '';
152 };
153
154 shell = lib.mkOption {
155 type = lib.types.str;
156 default = "${pkgs.bash}/bin/bash -e -c";
157 defaultText = lib.literalExpression ''"''${pkgs.bash}/bin/bash -e -c"'';
158 description = ''
159 Command that buildkite-agent 3 will execute when it spawns a shell.
160 '';
161 };
162 };
163 };
164 enabledAgents = lib.filterAttrs (n: v: v.enable) cfg;
165 mapAgents = function: lib.mkMerge (lib.mapAttrsToList function enabledAgents);
166in
167{
168 options.services.buildkite-agents = lib.mkOption {
169 type = lib.types.attrsOf (lib.types.submodule buildkiteOptions);
170 default = { };
171 description = ''
172 Attribute set of buildkite agents.
173 The attribute key is combined with the hostname and a unique integer to
174 create the final agent name. This can be overridden by setting the `name`
175 attribute.
176 '';
177 };
178
179 config.users.users = mapAgents (
180 name: cfg: {
181 "buildkite-agent-${name}" = {
182 name = "buildkite-agent-${name}";
183 home = cfg.dataDir;
184 createHome = true;
185 description = "Buildkite agent user";
186 extraGroups = cfg.extraGroups;
187 isSystemUser = true;
188 group = "buildkite-agent-${name}";
189 };
190 }
191 );
192 config.users.groups = mapAgents (
193 name: cfg: {
194 "buildkite-agent-${name}" = { };
195 }
196 );
197
198 config.systemd.services = mapAgents (
199 name: cfg: {
200 "buildkite-agent-${name}" = {
201 description = "Buildkite Agent";
202 wantedBy = [ "multi-user.target" ];
203 after = [ "network.target" ];
204 path = cfg.runtimePackages ++ [
205 cfg.package
206 pkgs.coreutils
207 ];
208 environment = config.networking.proxy.envVars // {
209 HOME = cfg.dataDir;
210 NIX_REMOTE = "daemon";
211 };
212
213 ## NB: maximum care is taken so that secrets (ssh keys and the CI token)
214 ## don't end up in the Nix store.
215 preStart =
216 let
217 sshDir = "${cfg.dataDir}/.ssh";
218 tagStr =
219 name: value:
220 if lib.isList value then
221 lib.concatStringsSep "," (builtins.map (v: "${name}=${v}") value)
222 else
223 "${name}=${value}";
224 tagsStr = lib.concatStringsSep "," (lib.mapAttrsToList tagStr cfg.tags);
225 in
226 lib.optionalString (cfg.privateSshKeyPath != null) ''
227 mkdir -m 0700 -p "${sshDir}"
228 install -m600 "${toString cfg.privateSshKeyPath}" "${sshDir}/id_rsa"
229 ''
230 + ''
231 cat > "${cfg.dataDir}/buildkite-agent.cfg" <<EOF
232 token="$(cat ${toString cfg.tokenPath})"
233 name="${cfg.name}"
234 shell="${cfg.shell}"
235 tags="${tagsStr}"
236 build-path="${cfg.dataDir}/builds"
237 hooks-path="${cfg.hooksPath}"
238 ${cfg.extraConfig}
239 EOF
240 '';
241
242 serviceConfig = {
243 ExecStart = "${cfg.package}/bin/buildkite-agent start --config ${cfg.dataDir}/buildkite-agent.cfg";
244 User = "buildkite-agent-${name}";
245 RestartSec = 5;
246 Restart = "on-failure";
247 TimeoutSec = 10;
248 # set a long timeout to give buildkite-agent a chance to finish current builds
249 TimeoutStopSec = "2 min";
250 KillMode = "mixed";
251 };
252 };
253 }
254 );
255
256 config.assertions = mapAgents (
257 name: cfg: [
258 {
259 assertion = cfg.hooksPath != hooksDir cfg.hooks -> cfg.hooks == { };
260 message = ''
261 Options `services.buildkite-agents.${name}.hooksPath' and
262 `services.buildkite-agents.${name}.hooks.<name>' are mutually exclusive.
263 '';
264 }
265 ]
266 );
267}