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