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