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