1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.buildkite-agent;
7
8 mkHookOption = { name, description, example ? null }: {
9 inherit name;
10 value = mkOption {
11 default = null;
12 inherit 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 = 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" {} ''
28 mkdir $out
29 ${concatStringsSep "\n" (mapAttrsToList mkHookEntry (filterAttrs (n: v: v != null) cfg.hooks))}
30 '';
31
32in
33
34{
35 options = {
36 services.buildkite-agent = {
37 enable = mkEnableOption "buildkite-agent";
38
39 package = mkOption {
40 default = pkgs.buildkite-agent;
41 defaultText = "pkgs.buildkite-agent";
42 description = "Which buildkite-agent derivation to use";
43 type = types.package;
44 };
45
46 dataDir = mkOption {
47 default = "/var/lib/buildkite-agent";
48 description = "The workdir for the agent";
49 type = types.str;
50 };
51
52 runtimePackages = mkOption {
53 default = [ pkgs.bash pkgs.nix ];
54 defaultText = "[ pkgs.bash pkgs.nix ]";
55 description = "Add programs to the buildkite-agent environment";
56 type = types.listOf types.package;
57 };
58
59 tokenPath = mkOption {
60 type = types.path;
61 description = ''
62 The token from your Buildkite "Agents" page.
63
64 A run-time path to the token file, which is supposed to be provisioned
65 outside of Nix store.
66 '';
67 };
68
69 name = mkOption {
70 type = types.str;
71 default = "%hostname-%n";
72 description = ''
73 The name of the agent.
74 '';
75 };
76
77 meta-data = mkOption {
78 type = types.str;
79 default = "";
80 example = "queue=default,docker=true,ruby2=true";
81 description = ''
82 Meta data for the agent. This is a comma-separated list of
83 <code>key=value</code> pairs.
84 '';
85 };
86
87 extraConfig = mkOption {
88 type = types.lines;
89 default = "";
90 example = "debug=true";
91 description = ''
92 Extra lines to be added verbatim to the configuration file.
93 '';
94 };
95
96 openssh =
97 { privateKeyPath = mkOption {
98 type = types.path;
99 description = ''
100 Private agent key.
101
102 A run-time path to the key file, which is supposed to be provisioned
103 outside of Nix store.
104 '';
105 };
106 publicKeyPath = mkOption {
107 type = types.path;
108 description = ''
109 Public agent key.
110
111 A run-time path to the key file, which is supposed to be provisioned
112 outside of Nix store.
113 '';
114 };
115 };
116
117 hooks = mkHookOptions [
118 { name = "checkout";
119 description = ''
120 The `checkout` hook script will replace the default checkout routine of the
121 bootstrap.sh script. You can use this hook to do your own SCM checkout
122 behaviour
123 ''; }
124 { name = "command";
125 description = ''
126 The `command` hook script will replace the default implementation of running
127 the build command.
128 ''; }
129 { name = "environment";
130 description = ''
131 The `environment` hook will run before all other commands, and can be used
132 to set up secrets, data, etc. Anything exported in hooks will be available
133 to the build script.
134
135 Note: the contents of this file will be copied to the world-readable
136 Nix store.
137 '';
138 example = ''
139 export SECRET_VAR=`head -1 /run/keys/secret`
140 ''; }
141 { name = "post-artifact";
142 description = ''
143 The `post-artifact` hook will run just after artifacts are uploaded
144 ''; }
145 { name = "post-checkout";
146 description = ''
147 The `post-checkout` hook will run after the bootstrap script has checked out
148 your projects source code.
149 ''; }
150 { name = "post-command";
151 description = ''
152 The `post-command` hook will run after the bootstrap script has run your
153 build commands
154 ''; }
155 { name = "pre-artifact";
156 description = ''
157 The `pre-artifact` hook will run just before artifacts are uploaded
158 ''; }
159 { name = "pre-checkout";
160 description = ''
161 The `pre-checkout` hook will run just before your projects source code is
162 checked out from your SCM provider
163 ''; }
164 { name = "pre-command";
165 description = ''
166 The `pre-command` hook will run just before your build command runs
167 ''; }
168 { name = "pre-exit";
169 description = ''
170 The `pre-exit` hook will run just before your build job finishes
171 ''; }
172 ];
173
174 hooksPath = mkOption {
175 type = types.path;
176 default = hooksDir;
177 defaultText = "generated from services.buildkite-agent.hooks";
178 description = ''
179 Path to the directory storing the hooks.
180 Consider using <option>services.buildkite-agent.hooks.<name></option>
181 instead.
182 '';
183 };
184 };
185 };
186
187 config = mkIf config.services.buildkite-agent.enable {
188 users.users.buildkite-agent =
189 { name = "buildkite-agent";
190 home = cfg.dataDir;
191 createHome = true;
192 description = "Buildkite agent user";
193 extraGroups = [ "keys" ];
194 };
195
196 environment.systemPackages = [ cfg.package ];
197
198 systemd.services.buildkite-agent =
199 { description = "Buildkite Agent";
200 wantedBy = [ "multi-user.target" ];
201 after = [ "network.target" ];
202 path = cfg.runtimePackages ++ [ pkgs.coreutils ];
203 environment = config.networking.proxy.envVars // {
204 HOME = cfg.dataDir;
205 NIX_REMOTE = "daemon";
206 };
207
208 ## NB: maximum care is taken so that secrets (ssh keys and the CI token)
209 ## don't end up in the Nix store.
210 preStart = let
211 sshDir = "${cfg.dataDir}/.ssh";
212 in
213 ''
214 mkdir -m 0700 -p "${sshDir}"
215 cp -f "${toString cfg.openssh.privateKeyPath}" "${sshDir}/id_rsa"
216 cp -f "${toString cfg.openssh.publicKeyPath}" "${sshDir}/id_rsa.pub"
217 chmod 600 "${sshDir}"/id_rsa*
218
219 cat > "${cfg.dataDir}/buildkite-agent.cfg" <<EOF
220 token="$(cat ${toString cfg.tokenPath})"
221 name="${cfg.name}"
222 meta-data="${cfg.meta-data}"
223 build-path="${cfg.dataDir}/builds"
224 hooks-path="${cfg.hooksPath}"
225 ${cfg.extraConfig}
226 EOF
227 '';
228
229 serviceConfig =
230 { ExecStart = "${pkgs.buildkite-agent}/bin/buildkite-agent start --config /var/lib/buildkite-agent/buildkite-agent.cfg";
231 User = "buildkite-agent";
232 RestartSec = 5;
233 Restart = "on-failure";
234 TimeoutSec = 10;
235 };
236 };
237
238 assertions = [
239 { assertion = cfg.hooksPath == hooksDir || all isNull (attrValues cfg.hooks);
240 message = ''
241 Options `services.buildkite-agent.hooksPath' and
242 `services.buildkite-agent.hooks.<name>' are mutually exclusive.
243 '';
244 }
245 ];
246 };
247 imports = [
248 (mkRenamedOptionModule [ "services" "buildkite-agent" "token" ] [ "services" "buildkite-agent" "tokenPath" ])
249 (mkRenamedOptionModule [ "services" "buildkite-agent" "openssh" "privateKey" ] [ "services" "buildkite-agent" "openssh" "privateKeyPath" ])
250 (mkRenamedOptionModule [ "services" "buildkite-agent" "openssh" "publicKey" ] [ "services" "buildkite-agent" "openssh" "publicKeyPath" ])
251 ];
252}