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