grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / databases / influxdb2.nix
blob20a94c03994b82bb71229b675b5422a5fffe213f
1 { config, lib, pkgs, ... }:
3 let
4   inherit
5     (lib)
6     any
7     attrNames
8     attrValues
9     count
10     escapeShellArg
11     filterAttrs
12     flatten
13     flip
14     getExe
15     hasAttr
16     hasInfix
17     listToAttrs
18     literalExpression
19     mapAttrsToList
20     mkEnableOption
21     mkPackageOption
22     mkIf
23     mkOption
24     nameValuePair
25     optional
26     subtractLists
27     types
28     unique
29     ;
31   format = pkgs.formats.json { };
32   cfg = config.services.influxdb2;
33   configFile = format.generate "config.json" cfg.settings;
35   validPermissions = [
36     "authorizations"
37     "buckets"
38     "dashboards"
39     "orgs"
40     "tasks"
41     "telegrafs"
42     "users"
43     "variables"
44     "secrets"
45     "labels"
46     "views"
47     "documents"
48     "notificationRules"
49     "notificationEndpoints"
50     "checks"
51     "dbrp"
52     "annotations"
53     "sources"
54     "scrapers"
55     "notebooks"
56     "remotes"
57     "replications"
58   ];
60   # Determines whether at least one active api token is defined
61   anyAuthDefined =
62     flip any (attrValues cfg.provision.organizations)
63     (o: o.present && flip any (attrValues o.auths)
64     (a: a.present && a.tokenFile != null));
66   provisionState = pkgs.writeText "provision_state.json" (builtins.toJSON {
67     inherit (cfg.provision) organizations users;
68   });
70   influxHost = "http://${escapeShellArg (
71       if ! hasAttr "http-bind-address" cfg.settings
72         || hasInfix "0.0.0.0" cfg.settings.http-bind-address
73       then "localhost:8086"
74       else cfg.settings.http-bind-address
75     )}";
77   waitUntilServiceIsReady = pkgs.writeShellScript "wait-until-service-is-ready" ''
78     set -euo pipefail
79     export INFLUX_HOST=${influxHost}
80     count=0
81     while ! influx ping &>/dev/null; do
82       if [ "$count" -eq 300 ]; then
83         echo "Tried for 30 seconds, giving up..."
84         exit 1
85       fi
87       if ! kill -0 "$MAINPID"; then
88         echo "Main server died, giving up..."
89         exit 1
90       fi
92       sleep 0.1
93       count=$((count++))
94     done
95   '';
97   provisioningScript = pkgs.writeShellScript "post-start-provision" ''
98     set -euo pipefail
99     export INFLUX_HOST=${influxHost}
101     # Do the initial database setup. Pass /dev/null as configs-path to
102     # avoid saving the token as the active config.
103     if test -e "$STATE_DIRECTORY/.first_startup"; then
104       influx setup \
105         --configs-path /dev/null \
106         --org ${escapeShellArg cfg.provision.initialSetup.organization} \
107         --bucket ${escapeShellArg cfg.provision.initialSetup.bucket} \
108         --username ${escapeShellArg cfg.provision.initialSetup.username} \
109         --password "$(< "$CREDENTIALS_DIRECTORY/admin-password")" \
110         --token "$(< "$CREDENTIALS_DIRECTORY/admin-token")" \
111         --retention ${toString cfg.provision.initialSetup.retention}s \
112         --force >/dev/null
114       rm -f "$STATE_DIRECTORY/.first_startup"
115     fi
117     provision_result=$(${getExe pkgs.influxdb2-provision} ${provisionState} "$INFLUX_HOST" "$(< "$CREDENTIALS_DIRECTORY/admin-token")")
118     if [[ "$(jq '[.auths[] | select(.action == "created")] | length' <<< "$provision_result")" -gt 0 ]]; then
119       echo "Created at least one new token, queueing service restart so we can manipulate secrets"
120       touch "$STATE_DIRECTORY/.needs_restart"
121     fi
122   '';
124   restarterScript = pkgs.writeShellScript "post-start-restarter" ''
125     set -euo pipefail
126     if test -e "$STATE_DIRECTORY/.needs_restart"; then
127       rm -f "$STATE_DIRECTORY/.needs_restart"
128       /run/current-system/systemd/bin/systemctl restart influxdb2
129     fi
130   '';
132   organizationSubmodule = types.submodule (organizationSubmod: let
133     org = organizationSubmod.config._module.args.name;
134   in {
135     options = {
136       present = mkOption {
137         description = "Whether to ensure that this organization is present or absent.";
138         type = types.bool;
139         default = true;
140       };
142       description = mkOption {
143         description = "Optional description for the organization.";
144         default = null;
145         type = types.nullOr types.str;
146       };
148       buckets = mkOption {
149         description = "Buckets to provision in this organization.";
150         default = {};
151         type = types.attrsOf (types.submodule (bucketSubmod: let
152           bucket = bucketSubmod.config._module.args.name;
153         in {
154           options = {
155             present = mkOption {
156               description = "Whether to ensure that this bucket is present or absent.";
157               type = types.bool;
158               default = true;
159             };
161             description = mkOption {
162               description = "Optional description for the bucket.";
163               default = null;
164               type = types.nullOr types.str;
165             };
167             retention = mkOption {
168               type = types.ints.unsigned;
169               default = 0;
170               description = "The duration in seconds for which the bucket will retain data (0 is infinite).";
171             };
172           };
173         }));
174       };
176       auths = mkOption {
177         description = "API tokens to provision for the user in this organization.";
178         default = {};
179         type = types.attrsOf (types.submodule (authSubmod: let
180           auth = authSubmod.config._module.args.name;
181         in {
182           options = {
183             id = mkOption {
184               description = "A unique identifier for this authentication token. Since influx doesn't store names for tokens, this will be hashed and appended to the description to identify the token.";
185               readOnly = true;
186               default = builtins.substring 0 32 (builtins.hashString "sha256" "${org}:${auth}");
187               defaultText = "<a hash derived from org and name>";
188               type = types.str;
189             };
191             present = mkOption {
192               description = "Whether to ensure that this user is present or absent.";
193               type = types.bool;
194               default = true;
195             };
197             description = mkOption {
198               description = ''
199                 Optional description for the API token.
200                 Note that the actual token will always be created with a descriptionregardless
201                 of whether this is given or not. The name is always added plus a unique suffix
202                 to later identify the token to track whether it has already been created.
203               '';
204               default = null;
205               type = types.nullOr types.str;
206             };
208             tokenFile = mkOption {
209               type = types.nullOr types.path;
210               default = null;
211               description = "The token value. If not given, influx will automatically generate one.";
212             };
214             operator = mkOption {
215               description = "Grants all permissions in all organizations.";
216               default = false;
217               type = types.bool;
218             };
220             allAccess = mkOption {
221               description = "Grants all permissions in the associated organization.";
222               default = false;
223               type = types.bool;
224             };
226             readPermissions = mkOption {
227               description = ''
228                 The read permissions to include for this token. Access is usually granted only
229                 for resources in the associated organization.
231                 Available permissions are `authorizations`, `buckets`, `dashboards`,
232                 `orgs`, `tasks`, `telegrafs`, `users`, `variables`, `secrets`, `labels`, `views`,
233                 `documents`, `notificationRules`, `notificationEndpoints`, `checks`, `dbrp`,
234                 `annotations`, `sources`, `scrapers`, `notebooks`, `remotes`, `replications`.
236                 Refer to `influx auth create --help` for a full list with descriptions.
238                 `buckets` grants read access to all associated buckets. Use `readBuckets` to define
239                 more granular access permissions.
240               '';
241               default = [];
242               type = types.listOf (types.enum validPermissions);
243             };
245             writePermissions = mkOption {
246               description = ''
247                 The read permissions to include for this token. Access is usually granted only
248                 for resources in the associated organization.
250                 Available permissions are `authorizations`, `buckets`, `dashboards`,
251                 `orgs`, `tasks`, `telegrafs`, `users`, `variables`, `secrets`, `labels`, `views`,
252                 `documents`, `notificationRules`, `notificationEndpoints`, `checks`, `dbrp`,
253                 `annotations`, `sources`, `scrapers`, `notebooks`, `remotes`, `replications`.
255                 Refer to `influx auth create --help` for a full list with descriptions.
257                 `buckets` grants write access to all associated buckets. Use `writeBuckets` to define
258                 more granular access permissions.
259               '';
260               default = [];
261               type = types.listOf (types.enum validPermissions);
262             };
264             readBuckets = mkOption {
265               description = "The organization's buckets which should be allowed to be read";
266               default = [];
267               type = types.listOf types.str;
268             };
270             writeBuckets = mkOption {
271               description = "The organization's buckets which should be allowed to be written";
272               default = [];
273               type = types.listOf types.str;
274             };
275           };
276         }));
277       };
278     };
279   });
282   options = {
283     services.influxdb2 = {
284       enable = mkEnableOption "the influxdb2 server";
286       package = mkPackageOption pkgs "influxdb2" { };
288       settings = mkOption {
289         default = { };
290         description = ''configuration options for influxdb2, see <https://docs.influxdata.com/influxdb/v2.0/reference/config-options> for details.'';
291         type = format.type;
292       };
294       provision = {
295         enable = mkEnableOption "initial database setup and provisioning";
297         initialSetup = {
298           organization = mkOption {
299             type = types.str;
300             example = "main";
301             description = "Primary organization name";
302           };
304           bucket = mkOption {
305             type = types.str;
306             example = "example";
307             description = "Primary bucket name";
308           };
310           username = mkOption {
311             type = types.str;
312             default = "admin";
313             description = "Primary username";
314           };
316           retention = mkOption {
317             type = types.ints.unsigned;
318             default = 0;
319             description = "The duration in seconds for which the bucket will retain data (0 is infinite).";
320           };
322           passwordFile = mkOption {
323             type = types.path;
324             description = "Password for primary user. Don't use a file from the nix store!";
325           };
327           tokenFile = mkOption {
328             type = types.path;
329             description = "API Token to set for the admin user. Don't use a file from the nix store!";
330           };
331         };
333         organizations = mkOption {
334           description = "Organizations to provision.";
335           example = literalExpression ''
336             {
337               myorg = {
338                 description = "My organization";
339                 buckets.mybucket = {
340                   description = "My bucket";
341                   retention = 31536000; # 1 year
342                 };
343                 auths.mytoken = {
344                   readBuckets = ["mybucket"];
345                   tokenFile = "/run/secrets/mytoken";
346                 };
347               };
348             }
349           '';
350           default = {};
351           type = types.attrsOf organizationSubmodule;
352         };
354         users = mkOption {
355           description = "Users to provision.";
356           default = {};
357           example = literalExpression ''
358             {
359               # admin = {}; /* The initialSetup.username will automatically be added. */
360               myuser.passwordFile = "/run/secrets/myuser_password";
361             }
362           '';
363           type = types.attrsOf (types.submodule (userSubmod: let
364             user = userSubmod.config._module.args.name;
365             org = userSubmod.config.org;
366           in {
367             options = {
368               present = mkOption {
369                 description = "Whether to ensure that this user is present or absent.";
370                 type = types.bool;
371                 default = true;
372               };
374               passwordFile = mkOption {
375                 description = "Password for the user. If unset, the user will not be able to log in until a password is set by an operator! Don't use a file from the nix store!";
376                 default = null;
377                 type = types.nullOr types.path;
378               };
379             };
380           }));
381         };
382       };
383     };
384   };
386   config = mkIf cfg.enable {
387     assertions =
388       [
389         {
390           assertion = !(hasAttr "bolt-path" cfg.settings) && !(hasAttr "engine-path" cfg.settings);
391           message = "services.influxdb2.config: bolt-path and engine-path should not be set as they are managed by systemd";
392         }
393       ]
394       ++ flatten (flip mapAttrsToList cfg.provision.organizations (orgName: org:
395         flip mapAttrsToList org.auths (authName: auth:
396           [
397             {
398               assertion = 1 == count (x: x) [
399                 auth.operator
400                 auth.allAccess
401                 (auth.readPermissions != []
402                   || auth.writePermissions != []
403                   || auth.readBuckets != []
404                   || auth.writeBuckets != [])
405               ];
406               message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: The `operator` and `allAccess` options are mutually exclusive with each other and the granular permission settings.";
407             }
408             (let unknownBuckets = subtractLists (attrNames org.buckets) auth.readBuckets; in {
409               assertion = unknownBuckets == [];
410               message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: Refers to invalid buckets in readBuckets: ${toString unknownBuckets}";
411             })
412             (let unknownBuckets = subtractLists (attrNames org.buckets) auth.writeBuckets; in {
413               assertion = unknownBuckets == [];
414               message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: Refers to invalid buckets in writeBuckets: ${toString unknownBuckets}";
415             })
416           ]
417         )
418       ));
420     services.influxdb2.provision = mkIf cfg.provision.enable {
421       organizations.${cfg.provision.initialSetup.organization} = {
422         buckets.${cfg.provision.initialSetup.bucket} = {
423           inherit (cfg.provision.initialSetup) retention;
424         };
425       };
426       users.${cfg.provision.initialSetup.username} = {
427         inherit (cfg.provision.initialSetup) passwordFile;
428       };
429     };
431     systemd.services.influxdb2 = {
432       description = "InfluxDB is an open-source, distributed, time series database";
433       documentation = [ "https://docs.influxdata.com/influxdb/" ];
434       wantedBy = [ "multi-user.target" ];
435       after = [ "network.target" ];
436       environment = {
437         INFLUXD_CONFIG_PATH = configFile;
438         ZONEINFO = "${pkgs.tzdata}/share/zoneinfo";
439       };
440       serviceConfig = {
441         Type = "exec"; # When credentials are used with systemd before v257 this is necessary to make the service start reliably (see systemd/systemd#33953)
442         ExecStart = "${cfg.package}/bin/influxd --bolt-path \${STATE_DIRECTORY}/influxd.bolt --engine-path \${STATE_DIRECTORY}/engine";
443         StateDirectory = "influxdb2";
444         User = "influxdb2";
445         Group = "influxdb2";
446         CapabilityBoundingSet = "";
447         SystemCallFilter = "@system-service";
448         LimitNOFILE = 65536;
449         KillMode = "control-group";
450         Restart = "on-failure";
451         LoadCredential = mkIf cfg.provision.enable [
452           "admin-password:${cfg.provision.initialSetup.passwordFile}"
453           "admin-token:${cfg.provision.initialSetup.tokenFile}"
454         ];
456         ExecStartPost = [
457           waitUntilServiceIsReady
458         ] ++ (lib.optionals cfg.provision.enable (
459           [provisioningScript] ++
460           # Only the restarter runs with elevated privileges
461           optional anyAuthDefined "+${restarterScript}"
462         ));
463       };
465       path = [
466         pkgs.influxdb2-cli
467         pkgs.jq
468       ];
470       # Mark if this is the first startup so postStart can do the initial setup.
471       # Also extract any token secret mappings and apply them if this isn't the first start.
472       preStart = let
473         tokenPaths = listToAttrs (flatten
474           # For all organizations
475           (flip mapAttrsToList cfg.provision.organizations
476             # For each contained token that has a token file
477             (_: org: flip mapAttrsToList (filterAttrs (_: x: x.tokenFile != null) org.auths)
478               # Collect id -> tokenFile for the mapping
479               (_: auth: nameValuePair auth.id auth.tokenFile))));
480         tokenMappings = pkgs.writeText "token_mappings.json" (builtins.toJSON tokenPaths);
481       in mkIf cfg.provision.enable ''
482         if ! test -e "$STATE_DIRECTORY/influxd.bolt"; then
483           touch "$STATE_DIRECTORY/.first_startup"
484         else
485           # Manipulate provisioned api tokens if necessary
486           ${getExe pkgs.influxdb2-token-manipulator} "$STATE_DIRECTORY/influxd.bolt" ${tokenMappings}
487         fi
488       '';
489     };
491     users.extraUsers.influxdb2 = {
492       isSystemUser = true;
493       group = "influxdb2";
494     };
496     users.extraGroups.influxdb2 = {};
497   };
499   meta.maintainers = with lib.maintainers; [ nickcao oddlama ];