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