1{
2 config,
3 lib,
4 options,
5 pkgs,
6 ...
7}:
8let
9 cfg = config.services.etcd;
10 opt = options.services.etcd;
11
12in
13{
14
15 options.services.etcd = {
16 enable = lib.mkOption {
17 description = "Whether to enable etcd.";
18 default = false;
19 type = lib.types.bool;
20 };
21
22 package = lib.mkPackageOption pkgs "etcd" { };
23
24 name = lib.mkOption {
25 description = "Etcd unique node name.";
26 default = config.networking.hostName;
27 defaultText = lib.literalExpression "config.networking.hostName";
28 type = lib.types.str;
29 };
30
31 advertiseClientUrls = lib.mkOption {
32 description = "Etcd list of this member's client URLs to advertise to the rest of the cluster.";
33 default = cfg.listenClientUrls;
34 defaultText = lib.literalExpression "config.${opt.listenClientUrls}";
35 type = lib.types.listOf lib.types.str;
36 };
37
38 listenClientUrls = lib.mkOption {
39 description = "Etcd list of URLs to listen on for client traffic.";
40 default = [ "http://127.0.0.1:2379" ];
41 type = lib.types.listOf lib.types.str;
42 };
43
44 listenPeerUrls = lib.mkOption {
45 description = "Etcd list of URLs to listen on for peer traffic.";
46 default = [ "http://127.0.0.1:2380" ];
47 type = lib.types.listOf lib.types.str;
48 };
49
50 initialAdvertisePeerUrls = lib.mkOption {
51 description = "Etcd list of this member's peer URLs to advertise to rest of the cluster.";
52 default = cfg.listenPeerUrls;
53 defaultText = lib.literalExpression "config.${opt.listenPeerUrls}";
54 type = lib.types.listOf lib.types.str;
55 };
56
57 initialCluster = lib.mkOption {
58 description = "Etcd initial cluster configuration for bootstrapping.";
59 default = [ "${cfg.name}=http://127.0.0.1:2380" ];
60 defaultText = lib.literalExpression ''["''${config.${opt.name}}=http://127.0.0.1:2380"]'';
61 type = lib.types.listOf lib.types.str;
62 };
63
64 initialClusterState = lib.mkOption {
65 description = "Etcd initial cluster configuration for bootstrapping.";
66 default = "new";
67 type = lib.types.enum [
68 "new"
69 "existing"
70 ];
71 };
72
73 initialClusterToken = lib.mkOption {
74 description = "Etcd initial cluster token for etcd cluster during bootstrap.";
75 default = "etcd-cluster";
76 type = lib.types.str;
77 };
78
79 discovery = lib.mkOption {
80 description = "Etcd discovery url";
81 default = "";
82 type = lib.types.str;
83 };
84
85 clientCertAuth = lib.mkOption {
86 description = "Whether to use certs for client authentication";
87 default = false;
88 type = lib.types.bool;
89 };
90
91 trustedCaFile = lib.mkOption {
92 description = "Certificate authority file to use for clients";
93 default = null;
94 type = lib.types.nullOr lib.types.path;
95 };
96
97 certFile = lib.mkOption {
98 description = "Cert file to use for clients";
99 default = null;
100 type = lib.types.nullOr lib.types.path;
101 };
102
103 keyFile = lib.mkOption {
104 description = "Key file to use for clients";
105 default = null;
106 type = lib.types.nullOr lib.types.path;
107 };
108
109 openFirewall = lib.mkOption {
110 type = lib.types.bool;
111 default = false;
112 description = ''
113 Open etcd ports in the firewall.
114 Ports opened:
115 - 2379/tcp for client requests
116 - 2380/tcp for peer communication
117 '';
118 };
119
120 peerCertFile = lib.mkOption {
121 description = "Cert file to use for peer to peer communication";
122 default = cfg.certFile;
123 defaultText = lib.literalExpression "config.${opt.certFile}";
124 type = lib.types.nullOr lib.types.path;
125 };
126
127 peerKeyFile = lib.mkOption {
128 description = "Key file to use for peer to peer communication";
129 default = cfg.keyFile;
130 defaultText = lib.literalExpression "config.${opt.keyFile}";
131 type = lib.types.nullOr lib.types.path;
132 };
133
134 peerTrustedCaFile = lib.mkOption {
135 description = "Certificate authority file to use for peer to peer communication";
136 default = cfg.trustedCaFile;
137 defaultText = lib.literalExpression "config.${opt.trustedCaFile}";
138 type = lib.types.nullOr lib.types.path;
139 };
140
141 peerClientCertAuth = lib.mkOption {
142 description = "Whether to check all incoming peer requests from the cluster for valid client certificates signed by the supplied CA";
143 default = false;
144 type = lib.types.bool;
145 };
146
147 extraConf = lib.mkOption {
148 description = ''
149 Etcd extra configuration. See
150 <https://github.com/coreos/etcd/blob/master/Documentation/op-guide/configuration.md#configuration-flags>
151 '';
152 type = lib.types.attrsOf lib.types.str;
153 default = { };
154 example = lib.literalExpression ''
155 {
156 "CORS" = "*";
157 "NAME" = "default-name";
158 "MAX_RESULT_BUFFER" = "1024";
159 "MAX_CLUSTER_SIZE" = "9";
160 "MAX_RETRY_ATTEMPTS" = "3";
161 }
162 '';
163 };
164
165 dataDir = lib.mkOption {
166 type = lib.types.path;
167 default = "/var/lib/etcd";
168 description = "Etcd data directory.";
169 };
170 };
171
172 config = lib.mkIf cfg.enable {
173 systemd.tmpfiles.settings."10-etcd".${cfg.dataDir}.d = {
174 user = "etcd";
175 mode = "0700";
176 };
177
178 systemd.services.etcd = {
179 description = "etcd key-value store";
180 wantedBy = [ "multi-user.target" ];
181 after = [
182 "network-online.target"
183 ]
184 ++ lib.optional config.networking.firewall.enable "firewall.service";
185 wants = [
186 "network-online.target"
187 ]
188 ++ lib.optional config.networking.firewall.enable "firewall.service";
189
190 environment =
191 (lib.filterAttrs (n: v: v != null) {
192 ETCD_NAME = cfg.name;
193 ETCD_DISCOVERY = cfg.discovery;
194 ETCD_DATA_DIR = cfg.dataDir;
195 ETCD_ADVERTISE_CLIENT_URLS = lib.concatStringsSep "," cfg.advertiseClientUrls;
196 ETCD_LISTEN_CLIENT_URLS = lib.concatStringsSep "," cfg.listenClientUrls;
197 ETCD_LISTEN_PEER_URLS = lib.concatStringsSep "," cfg.listenPeerUrls;
198 ETCD_INITIAL_ADVERTISE_PEER_URLS = lib.concatStringsSep "," cfg.initialAdvertisePeerUrls;
199 ETCD_PEER_CLIENT_CERT_AUTH = toString cfg.peerClientCertAuth;
200 ETCD_PEER_TRUSTED_CA_FILE = cfg.peerTrustedCaFile;
201 ETCD_PEER_CERT_FILE = cfg.peerCertFile;
202 ETCD_PEER_KEY_FILE = cfg.peerKeyFile;
203 ETCD_CLIENT_CERT_AUTH = toString cfg.clientCertAuth;
204 ETCD_TRUSTED_CA_FILE = cfg.trustedCaFile;
205 ETCD_CERT_FILE = cfg.certFile;
206 ETCD_KEY_FILE = cfg.keyFile;
207 })
208 // (lib.optionalAttrs (cfg.discovery == "") {
209 ETCD_INITIAL_CLUSTER = lib.concatStringsSep "," cfg.initialCluster;
210 ETCD_INITIAL_CLUSTER_STATE = cfg.initialClusterState;
211 ETCD_INITIAL_CLUSTER_TOKEN = cfg.initialClusterToken;
212 })
213 // (lib.mapAttrs' (n: v: lib.nameValuePair "ETCD_${n}" v) cfg.extraConf);
214
215 unitConfig = {
216 Documentation = "https://github.com/coreos/etcd";
217 };
218
219 serviceConfig = {
220 Type = "notify";
221 Restart = "always";
222 RestartSec = "30s";
223 ExecStart = "${cfg.package}/bin/etcd";
224 User = "etcd";
225 LimitNOFILE = 40000;
226 };
227 };
228
229 environment.systemPackages = [ cfg.package ];
230
231 networking.firewall = lib.mkIf cfg.openFirewall {
232 allowedTCPPorts = [
233 2379 # for client requests
234 2380 # for peer communication
235 ];
236 };
237
238 users.users.etcd = {
239 isSystemUser = true;
240 group = "etcd";
241 description = "Etcd daemon user";
242 home = cfg.dataDir;
243 };
244 users.groups.etcd = { };
245 };
246}