at master 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 assertion = !(hasAttr "bolt-path" cfg.settings) && !(hasAttr "engine-path" cfg.settings); 421 message = "services.influxdb2.config: bolt-path and engine-path should not be set as they are managed by systemd"; 422 } 423 ] 424 ++ flatten ( 425 flip mapAttrsToList cfg.provision.organizations ( 426 orgName: org: 427 flip mapAttrsToList org.auths ( 428 authName: auth: [ 429 { 430 assertion = 431 1 == count (x: x) [ 432 auth.operator 433 auth.allAccess 434 ( 435 auth.readPermissions != [ ] 436 || auth.writePermissions != [ ] 437 || auth.readBuckets != [ ] 438 || auth.writeBuckets != [ ] 439 ) 440 ]; 441 message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: The `operator` and `allAccess` options are mutually exclusive with each other and the granular permission settings."; 442 } 443 ( 444 let 445 unknownBuckets = subtractLists (attrNames org.buckets) auth.readBuckets; 446 in 447 { 448 assertion = unknownBuckets == [ ]; 449 message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: Refers to invalid buckets in readBuckets: ${toString unknownBuckets}"; 450 } 451 ) 452 ( 453 let 454 unknownBuckets = subtractLists (attrNames org.buckets) auth.writeBuckets; 455 in 456 { 457 assertion = unknownBuckets == [ ]; 458 message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: Refers to invalid buckets in writeBuckets: ${toString unknownBuckets}"; 459 } 460 ) 461 ] 462 ) 463 ) 464 ); 465 466 services.influxdb2.provision = mkIf cfg.provision.enable { 467 organizations.${cfg.provision.initialSetup.organization} = { 468 buckets.${cfg.provision.initialSetup.bucket} = { 469 inherit (cfg.provision.initialSetup) retention; 470 }; 471 }; 472 users.${cfg.provision.initialSetup.username} = { 473 inherit (cfg.provision.initialSetup) passwordFile; 474 }; 475 }; 476 477 systemd.services.influxdb2 = { 478 description = "InfluxDB is an open-source, distributed, time series database"; 479 documentation = [ "https://docs.influxdata.com/influxdb/" ]; 480 wantedBy = [ "multi-user.target" ]; 481 after = [ "network.target" ]; 482 environment = { 483 INFLUXD_CONFIG_PATH = configFile; 484 ZONEINFO = "${pkgs.tzdata}/share/zoneinfo"; 485 }; 486 serviceConfig = { 487 Type = "exec"; # When credentials are used with systemd before v257 this is necessary to make the service start reliably (see systemd/systemd#33953) 488 ExecStart = "${cfg.package}/bin/influxd --bolt-path \${STATE_DIRECTORY}/influxd.bolt --engine-path \${STATE_DIRECTORY}/engine"; 489 StateDirectory = "influxdb2"; 490 User = "influxdb2"; 491 Group = "influxdb2"; 492 CapabilityBoundingSet = ""; 493 SystemCallFilter = "@system-service"; 494 LimitNOFILE = 65536; 495 KillMode = "control-group"; 496 Restart = "on-failure"; 497 LoadCredential = mkIf cfg.provision.enable [ 498 "admin-password:${cfg.provision.initialSetup.passwordFile}" 499 "admin-token:${cfg.provision.initialSetup.tokenFile}" 500 ]; 501 502 ExecStartPost = [ 503 waitUntilServiceIsReady 504 ] 505 ++ (lib.optionals cfg.provision.enable ( 506 [ provisioningScript ] 507 ++ 508 # Only the restarter runs with elevated privileges 509 optional anyAuthDefined "+${restarterScript}" 510 )); 511 }; 512 513 path = [ 514 pkgs.influxdb2-cli 515 pkgs.jq 516 ]; 517 518 # Mark if this is the first startup so postStart can do the initial setup. 519 # Also extract any token secret mappings and apply them if this isn't the first start. 520 preStart = 521 let 522 tokenPaths = listToAttrs ( 523 flatten 524 # For all organizations 525 ( 526 flip mapAttrsToList cfg.provision.organizations 527 # For each contained token that has a token file 528 ( 529 _: org: 530 flip mapAttrsToList (filterAttrs (_: x: x.tokenFile != null) org.auths) 531 # Collect id -> tokenFile for the mapping 532 (_: auth: nameValuePair auth.id auth.tokenFile) 533 ) 534 ) 535 ); 536 tokenMappings = pkgs.writeText "token_mappings.json" (builtins.toJSON tokenPaths); 537 in 538 mkIf cfg.provision.enable '' 539 if ! test -e "$STATE_DIRECTORY/influxd.bolt"; then 540 touch "$STATE_DIRECTORY/.first_startup" 541 else 542 # Manipulate provisioned api tokens if necessary 543 ${getExe pkgs.influxdb2-token-manipulator} "$STATE_DIRECTORY/influxd.bolt" ${tokenMappings} 544 fi 545 ''; 546 }; 547 548 users.extraUsers.influxdb2 = { 549 isSystemUser = true; 550 group = "influxdb2"; 551 }; 552 553 users.extraGroups.influxdb2 = { }; 554 }; 555 556 meta.maintainers = with lib.maintainers; [ 557 nickcao 558 oddlama 559 ]; 560}