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