1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}:
8let
9
10 dataDir = "/var/lib/consul";
11 cfg = config.services.consul;
12
13 configOptions = {
14 data_dir = dataDir;
15 ui_config = {
16 enabled = cfg.webUi;
17 };
18 }
19 // cfg.extraConfig;
20
21 configFiles = [
22 "/etc/consul.json"
23 "/etc/consul-addrs.json"
24 ]
25 ++ cfg.extraConfigFiles;
26
27 devices = lib.attrValues (lib.filterAttrs (_: i: i != null) cfg.interface);
28 systemdDevices = lib.forEach devices (
29 i: "sys-subsystem-net-devices-${utils.escapeSystemdPath i}.device"
30 );
31in
32{
33 options = {
34
35 services.consul = {
36
37 enable = lib.mkOption {
38 type = lib.types.bool;
39 default = false;
40 description = ''
41 Enables the consul daemon.
42 '';
43 };
44
45 package = lib.mkPackageOption pkgs "consul" { };
46
47 webUi = lib.mkOption {
48 type = lib.types.bool;
49 default = false;
50 description = ''
51 Enables the web interface on the consul http port.
52 '';
53 };
54
55 leaveOnStop = lib.mkOption {
56 type = lib.types.bool;
57 default = false;
58 description = ''
59 If enabled, causes a leave action to be sent when closing consul.
60 This allows a clean termination of the node, but permanently removes
61 it from the cluster. You probably don't want this option unless you
62 are running a node which going offline in a permanent / semi-permanent
63 fashion.
64 '';
65 };
66
67 interface = {
68
69 advertise = lib.mkOption {
70 type = lib.types.nullOr lib.types.str;
71 default = null;
72 description = ''
73 The name of the interface to pull the advertise_addr from.
74 '';
75 };
76
77 bind = lib.mkOption {
78 type = lib.types.nullOr lib.types.str;
79 default = null;
80 description = ''
81 The name of the interface to pull the bind_addr from.
82 '';
83 };
84 };
85
86 forceAddrFamily = lib.mkOption {
87 type = lib.types.enum [
88 "any"
89 "ipv4"
90 "ipv6"
91 ];
92 default = "any";
93 description = ''
94 Whether to bind ipv4/ipv6 or both kind of addresses.
95 '';
96 };
97
98 forceIpv4 = lib.mkOption {
99 type = lib.types.nullOr lib.types.bool;
100 default = null;
101 description = ''
102 Deprecated: Use consul.forceAddrFamily instead.
103 Whether we should force the interfaces to only pull ipv4 addresses.
104 '';
105 };
106
107 dropPrivileges = lib.mkOption {
108 type = lib.types.bool;
109 default = true;
110 description = ''
111 Whether the consul agent should be run as a non-root consul user.
112 '';
113 };
114
115 extraConfig = lib.mkOption {
116 default = { };
117 type = lib.types.attrsOf lib.types.anything;
118 description = ''
119 Extra configuration options which are serialized to json and added
120 to the config.json file.
121 '';
122 };
123
124 extraConfigFiles = lib.mkOption {
125 default = [ ];
126 type = lib.types.listOf lib.types.str;
127 description = ''
128 Additional configuration files to pass to consul
129 NOTE: These will not trigger the service to be restarted when altered.
130 '';
131 };
132
133 alerts = {
134 enable = lib.mkEnableOption "consul-alerts";
135
136 package = lib.mkPackageOption pkgs "consul-alerts" { };
137
138 listenAddr = lib.mkOption {
139 description = "Api listening address.";
140 default = "localhost:9000";
141 type = lib.types.str;
142 };
143
144 consulAddr = lib.mkOption {
145 description = "Consul api listening address";
146 default = "localhost:8500";
147 type = lib.types.str;
148 };
149
150 watchChecks = lib.mkOption {
151 description = "Whether to enable check watcher.";
152 default = true;
153 type = lib.types.bool;
154 };
155
156 watchEvents = lib.mkOption {
157 description = "Whether to enable event watcher.";
158 default = true;
159 type = lib.types.bool;
160 };
161 };
162
163 };
164
165 };
166
167 config = lib.mkIf cfg.enable (
168 lib.mkMerge [
169 {
170
171 users.users.consul = {
172 description = "Consul agent daemon user";
173 isSystemUser = true;
174 group = "consul";
175 # The shell is needed for health checks
176 shell = "/run/current-system/sw/bin/bash";
177 };
178 users.groups.consul = { };
179
180 environment = {
181 etc."consul.json".text = builtins.toJSON configOptions;
182 # We need consul.d to exist for consul to start
183 etc."consul.d/dummy.json".text = "{ }";
184 systemPackages = [ cfg.package ];
185 };
186
187 warnings = lib.flatten [
188 (lib.optional (cfg.forceIpv4 != null) ''
189 The option consul.forceIpv4 is deprecated, please use
190 consul.forceAddrFamily instead.
191 '')
192 ];
193
194 systemd.services.consul = {
195 wantedBy = [ "multi-user.target" ];
196 after = [ "network.target" ] ++ systemdDevices;
197 bindsTo = systemdDevices;
198 restartTriggers = [
199 config.environment.etc."consul.json".source
200 ]
201 ++ lib.mapAttrsToList (_: d: d.source) (
202 lib.filterAttrs (n: _: lib.hasPrefix "consul.d/" n) config.environment.etc
203 );
204
205 serviceConfig = {
206 ExecStart =
207 "@${lib.getExe cfg.package} consul agent -config-dir /etc/consul.d"
208 + lib.concatMapStrings (n: " -config-file ${n}") configFiles;
209 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
210 PermissionsStartOnly = true;
211 User = if cfg.dropPrivileges then "consul" else null;
212 Restart = "on-failure";
213 TimeoutStartSec = "infinity";
214 }
215 // (lib.optionalAttrs (cfg.leaveOnStop) {
216 ExecStop = "${lib.getExe cfg.package} leave";
217 });
218
219 path = with pkgs; [
220 iproute2
221 gawk
222 cfg.package
223 ];
224 preStart =
225 let
226 family =
227 if cfg.forceAddrFamily == "ipv6" then
228 "-6"
229 else if cfg.forceAddrFamily == "ipv4" then
230 "-4"
231 else
232 "";
233 in
234 ''
235 mkdir -m 0700 -p ${dataDir}
236 chown -R consul ${dataDir}
237
238 # Determine interface addresses
239 getAddrOnce () {
240 ip ${family} addr show dev "$1" scope global \
241 | awk -F '[ /\t]*' '/inet/ {print $3}' | head -n 1
242 }
243 getAddr () {
244 ADDR="$(getAddrOnce $1)"
245 LEFT=60 # Die after 1 minute
246 while [ -z "$ADDR" ]; do
247 sleep 1
248 LEFT=$(expr $LEFT - 1)
249 if [ "$LEFT" -eq "0" ]; then
250 echo "Address lookup timed out"
251 exit 1
252 fi
253 ADDR="$(getAddrOnce $1)"
254 done
255 echo "$ADDR"
256 }
257 echo "{" > /etc/consul-addrs.json
258 delim=" "
259 ''
260 + lib.concatStrings (
261 lib.flip lib.mapAttrsToList cfg.interface (
262 name: i:
263 lib.optionalString (i != null) ''
264 echo "$delim \"${name}_addr\": \"$(getAddr "${i}")\"" >> /etc/consul-addrs.json
265 delim=","
266 ''
267 )
268 )
269 + ''
270 echo "}" >> /etc/consul-addrs.json
271 '';
272 };
273 }
274
275 # deprecated
276 (lib.mkIf (cfg.forceIpv4 != null && cfg.forceIpv4) {
277 services.consul.forceAddrFamily = "ipv4";
278 })
279
280 (lib.mkIf (cfg.alerts.enable) {
281 systemd.services.consul-alerts = {
282 wantedBy = [ "multi-user.target" ];
283 after = [ "consul.service" ];
284
285 path = [ cfg.package ];
286
287 serviceConfig = {
288 ExecStart = ''
289 ${lib.getExe cfg.alerts.package} start \
290 --alert-addr=${cfg.alerts.listenAddr} \
291 --consul-addr=${cfg.alerts.consulAddr} \
292 ${lib.optionalString cfg.alerts.watchChecks "--watch-checks"} \
293 ${lib.optionalString cfg.alerts.watchEvents "--watch-events"}
294 '';
295 User = if cfg.dropPrivileges then "consul" else null;
296 Restart = "on-failure";
297 };
298 };
299 })
300
301 ]
302 );
303}