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