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