7e47f3658e
GitOrigin-RevId: 1925c603f17fc89f4c8f6bf6f631a802ad85d784
499 lines
17 KiB
Nix
499 lines
17 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
let
|
|
inherit
|
|
(lib)
|
|
any
|
|
attrNames
|
|
attrValues
|
|
count
|
|
escapeShellArg
|
|
filterAttrs
|
|
flatten
|
|
flip
|
|
getExe
|
|
hasAttr
|
|
hasInfix
|
|
listToAttrs
|
|
literalExpression
|
|
mapAttrsToList
|
|
mkEnableOption
|
|
mkPackageOption
|
|
mkIf
|
|
mkOption
|
|
nameValuePair
|
|
optional
|
|
subtractLists
|
|
types
|
|
unique
|
|
;
|
|
|
|
format = pkgs.formats.json { };
|
|
cfg = config.services.influxdb2;
|
|
configFile = format.generate "config.json" cfg.settings;
|
|
|
|
validPermissions = [
|
|
"authorizations"
|
|
"buckets"
|
|
"dashboards"
|
|
"orgs"
|
|
"tasks"
|
|
"telegrafs"
|
|
"users"
|
|
"variables"
|
|
"secrets"
|
|
"labels"
|
|
"views"
|
|
"documents"
|
|
"notificationRules"
|
|
"notificationEndpoints"
|
|
"checks"
|
|
"dbrp"
|
|
"annotations"
|
|
"sources"
|
|
"scrapers"
|
|
"notebooks"
|
|
"remotes"
|
|
"replications"
|
|
];
|
|
|
|
# Determines whether at least one active api token is defined
|
|
anyAuthDefined =
|
|
flip any (attrValues cfg.provision.organizations)
|
|
(o: o.present && flip any (attrValues o.auths)
|
|
(a: a.present && a.tokenFile != null));
|
|
|
|
provisionState = pkgs.writeText "provision_state.json" (builtins.toJSON {
|
|
inherit (cfg.provision) organizations users;
|
|
});
|
|
|
|
influxHost = "http://${escapeShellArg (
|
|
if ! hasAttr "http-bind-address" cfg.settings
|
|
|| hasInfix "0.0.0.0" cfg.settings.http-bind-address
|
|
then "localhost:8086"
|
|
else cfg.settings.http-bind-address
|
|
)}";
|
|
|
|
waitUntilServiceIsReady = pkgs.writeShellScript "wait-until-service-is-ready" ''
|
|
set -euo pipefail
|
|
export INFLUX_HOST=${influxHost}
|
|
count=0
|
|
while ! influx ping &>/dev/null; do
|
|
if [ "$count" -eq 300 ]; then
|
|
echo "Tried for 30 seconds, giving up..."
|
|
exit 1
|
|
fi
|
|
|
|
if ! kill -0 "$MAINPID"; then
|
|
echo "Main server died, giving up..."
|
|
exit 1
|
|
fi
|
|
|
|
sleep 0.1
|
|
count=$((count++))
|
|
done
|
|
'';
|
|
|
|
provisioningScript = pkgs.writeShellScript "post-start-provision" ''
|
|
set -euo pipefail
|
|
export INFLUX_HOST=${influxHost}
|
|
|
|
# Do the initial database setup. Pass /dev/null as configs-path to
|
|
# avoid saving the token as the active config.
|
|
if test -e "$STATE_DIRECTORY/.first_startup"; then
|
|
influx setup \
|
|
--configs-path /dev/null \
|
|
--org ${escapeShellArg cfg.provision.initialSetup.organization} \
|
|
--bucket ${escapeShellArg cfg.provision.initialSetup.bucket} \
|
|
--username ${escapeShellArg cfg.provision.initialSetup.username} \
|
|
--password "$(< "$CREDENTIALS_DIRECTORY/admin-password")" \
|
|
--token "$(< "$CREDENTIALS_DIRECTORY/admin-token")" \
|
|
--retention ${toString cfg.provision.initialSetup.retention}s \
|
|
--force >/dev/null
|
|
|
|
rm -f "$STATE_DIRECTORY/.first_startup"
|
|
fi
|
|
|
|
provision_result=$(${getExe pkgs.influxdb2-provision} ${provisionState} "$INFLUX_HOST" "$(< "$CREDENTIALS_DIRECTORY/admin-token")")
|
|
if [[ "$(jq '[.auths[] | select(.action == "created")] | length' <<< "$provision_result")" -gt 0 ]]; then
|
|
echo "Created at least one new token, queueing service restart so we can manipulate secrets"
|
|
touch "$STATE_DIRECTORY/.needs_restart"
|
|
fi
|
|
'';
|
|
|
|
restarterScript = pkgs.writeShellScript "post-start-restarter" ''
|
|
set -euo pipefail
|
|
if test -e "$STATE_DIRECTORY/.needs_restart"; then
|
|
rm -f "$STATE_DIRECTORY/.needs_restart"
|
|
/run/current-system/systemd/bin/systemctl restart influxdb2
|
|
fi
|
|
'';
|
|
|
|
organizationSubmodule = types.submodule (organizationSubmod: let
|
|
org = organizationSubmod.config._module.args.name;
|
|
in {
|
|
options = {
|
|
present = mkOption {
|
|
description = "Whether to ensure that this organization is present or absent.";
|
|
type = types.bool;
|
|
default = true;
|
|
};
|
|
|
|
description = mkOption {
|
|
description = "Optional description for the organization.";
|
|
default = null;
|
|
type = types.nullOr types.str;
|
|
};
|
|
|
|
buckets = mkOption {
|
|
description = "Buckets to provision in this organization.";
|
|
default = {};
|
|
type = types.attrsOf (types.submodule (bucketSubmod: let
|
|
bucket = bucketSubmod.config._module.args.name;
|
|
in {
|
|
options = {
|
|
present = mkOption {
|
|
description = "Whether to ensure that this bucket is present or absent.";
|
|
type = types.bool;
|
|
default = true;
|
|
};
|
|
|
|
description = mkOption {
|
|
description = "Optional description for the bucket.";
|
|
default = null;
|
|
type = types.nullOr types.str;
|
|
};
|
|
|
|
retention = mkOption {
|
|
type = types.ints.unsigned;
|
|
default = 0;
|
|
description = "The duration in seconds for which the bucket will retain data (0 is infinite).";
|
|
};
|
|
};
|
|
}));
|
|
};
|
|
|
|
auths = mkOption {
|
|
description = "API tokens to provision for the user in this organization.";
|
|
default = {};
|
|
type = types.attrsOf (types.submodule (authSubmod: let
|
|
auth = authSubmod.config._module.args.name;
|
|
in {
|
|
options = {
|
|
id = mkOption {
|
|
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.";
|
|
readOnly = true;
|
|
default = builtins.substring 0 32 (builtins.hashString "sha256" "${org}:${auth}");
|
|
defaultText = "<a hash derived from org and name>";
|
|
type = types.str;
|
|
};
|
|
|
|
present = mkOption {
|
|
description = "Whether to ensure that this user is present or absent.";
|
|
type = types.bool;
|
|
default = true;
|
|
};
|
|
|
|
description = mkOption {
|
|
description = ''
|
|
Optional description for the API token.
|
|
Note that the actual token will always be created with a descriptionregardless
|
|
of whether this is given or not. The name is always added plus a unique suffix
|
|
to later identify the token to track whether it has already been created.
|
|
'';
|
|
default = null;
|
|
type = types.nullOr types.str;
|
|
};
|
|
|
|
tokenFile = mkOption {
|
|
type = types.nullOr types.path;
|
|
default = null;
|
|
description = "The token value. If not given, influx will automatically generate one.";
|
|
};
|
|
|
|
operator = mkOption {
|
|
description = "Grants all permissions in all organizations.";
|
|
default = false;
|
|
type = types.bool;
|
|
};
|
|
|
|
allAccess = mkOption {
|
|
description = "Grants all permissions in the associated organization.";
|
|
default = false;
|
|
type = types.bool;
|
|
};
|
|
|
|
readPermissions = mkOption {
|
|
description = ''
|
|
The read permissions to include for this token. Access is usually granted only
|
|
for resources in the associated organization.
|
|
|
|
Available permissions are `authorizations`, `buckets`, `dashboards`,
|
|
`orgs`, `tasks`, `telegrafs`, `users`, `variables`, `secrets`, `labels`, `views`,
|
|
`documents`, `notificationRules`, `notificationEndpoints`, `checks`, `dbrp`,
|
|
`annotations`, `sources`, `scrapers`, `notebooks`, `remotes`, `replications`.
|
|
|
|
Refer to `influx auth create --help` for a full list with descriptions.
|
|
|
|
`buckets` grants read access to all associated buckets. Use `readBuckets` to define
|
|
more granular access permissions.
|
|
'';
|
|
default = [];
|
|
type = types.listOf (types.enum validPermissions);
|
|
};
|
|
|
|
writePermissions = mkOption {
|
|
description = ''
|
|
The read permissions to include for this token. Access is usually granted only
|
|
for resources in the associated organization.
|
|
|
|
Available permissions are `authorizations`, `buckets`, `dashboards`,
|
|
`orgs`, `tasks`, `telegrafs`, `users`, `variables`, `secrets`, `labels`, `views`,
|
|
`documents`, `notificationRules`, `notificationEndpoints`, `checks`, `dbrp`,
|
|
`annotations`, `sources`, `scrapers`, `notebooks`, `remotes`, `replications`.
|
|
|
|
Refer to `influx auth create --help` for a full list with descriptions.
|
|
|
|
`buckets` grants write access to all associated buckets. Use `writeBuckets` to define
|
|
more granular access permissions.
|
|
'';
|
|
default = [];
|
|
type = types.listOf (types.enum validPermissions);
|
|
};
|
|
|
|
readBuckets = mkOption {
|
|
description = "The organization's buckets which should be allowed to be read";
|
|
default = [];
|
|
type = types.listOf types.str;
|
|
};
|
|
|
|
writeBuckets = mkOption {
|
|
description = "The organization's buckets which should be allowed to be written";
|
|
default = [];
|
|
type = types.listOf types.str;
|
|
};
|
|
};
|
|
}));
|
|
};
|
|
};
|
|
});
|
|
in
|
|
{
|
|
options = {
|
|
services.influxdb2 = {
|
|
enable = mkEnableOption "the influxdb2 server";
|
|
|
|
package = mkPackageOption pkgs "influxdb2" { };
|
|
|
|
settings = mkOption {
|
|
default = { };
|
|
description = ''configuration options for influxdb2, see <https://docs.influxdata.com/influxdb/v2.0/reference/config-options> for details.'';
|
|
type = format.type;
|
|
};
|
|
|
|
provision = {
|
|
enable = mkEnableOption "initial database setup and provisioning";
|
|
|
|
initialSetup = {
|
|
organization = mkOption {
|
|
type = types.str;
|
|
example = "main";
|
|
description = "Primary organization name";
|
|
};
|
|
|
|
bucket = mkOption {
|
|
type = types.str;
|
|
example = "example";
|
|
description = "Primary bucket name";
|
|
};
|
|
|
|
username = mkOption {
|
|
type = types.str;
|
|
default = "admin";
|
|
description = "Primary username";
|
|
};
|
|
|
|
retention = mkOption {
|
|
type = types.ints.unsigned;
|
|
default = 0;
|
|
description = "The duration in seconds for which the bucket will retain data (0 is infinite).";
|
|
};
|
|
|
|
passwordFile = mkOption {
|
|
type = types.path;
|
|
description = "Password for primary user. Don't use a file from the nix store!";
|
|
};
|
|
|
|
tokenFile = mkOption {
|
|
type = types.path;
|
|
description = "API Token to set for the admin user. Don't use a file from the nix store!";
|
|
};
|
|
};
|
|
|
|
organizations = mkOption {
|
|
description = "Organizations to provision.";
|
|
example = literalExpression ''
|
|
{
|
|
myorg = {
|
|
description = "My organization";
|
|
buckets.mybucket = {
|
|
description = "My bucket";
|
|
retention = 31536000; # 1 year
|
|
};
|
|
auths.mytoken = {
|
|
readBuckets = ["mybucket"];
|
|
tokenFile = "/run/secrets/mytoken";
|
|
};
|
|
};
|
|
}
|
|
'';
|
|
default = {};
|
|
type = types.attrsOf organizationSubmodule;
|
|
};
|
|
|
|
users = mkOption {
|
|
description = "Users to provision.";
|
|
default = {};
|
|
example = literalExpression ''
|
|
{
|
|
# admin = {}; /* The initialSetup.username will automatically be added. */
|
|
myuser.passwordFile = "/run/secrets/myuser_password";
|
|
}
|
|
'';
|
|
type = types.attrsOf (types.submodule (userSubmod: let
|
|
user = userSubmod.config._module.args.name;
|
|
org = userSubmod.config.org;
|
|
in {
|
|
options = {
|
|
present = mkOption {
|
|
description = "Whether to ensure that this user is present or absent.";
|
|
type = types.bool;
|
|
default = true;
|
|
};
|
|
|
|
passwordFile = mkOption {
|
|
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!";
|
|
default = null;
|
|
type = types.nullOr types.path;
|
|
};
|
|
};
|
|
}));
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
assertions =
|
|
[
|
|
{
|
|
assertion = !(hasAttr "bolt-path" cfg.settings) && !(hasAttr "engine-path" cfg.settings);
|
|
message = "services.influxdb2.config: bolt-path and engine-path should not be set as they are managed by systemd";
|
|
}
|
|
]
|
|
++ flatten (flip mapAttrsToList cfg.provision.organizations (orgName: org:
|
|
flip mapAttrsToList org.auths (authName: auth:
|
|
[
|
|
{
|
|
assertion = 1 == count (x: x) [
|
|
auth.operator
|
|
auth.allAccess
|
|
(auth.readPermissions != []
|
|
|| auth.writePermissions != []
|
|
|| auth.readBuckets != []
|
|
|| auth.writeBuckets != [])
|
|
];
|
|
message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: The `operator` and `allAccess` options are mutually exclusive with each other and the granular permission settings.";
|
|
}
|
|
(let unknownBuckets = subtractLists (attrNames org.buckets) auth.readBuckets; in {
|
|
assertion = unknownBuckets == [];
|
|
message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: Refers to invalid buckets in readBuckets: ${toString unknownBuckets}";
|
|
})
|
|
(let unknownBuckets = subtractLists (attrNames org.buckets) auth.writeBuckets; in {
|
|
assertion = unknownBuckets == [];
|
|
message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: Refers to invalid buckets in writeBuckets: ${toString unknownBuckets}";
|
|
})
|
|
]
|
|
)
|
|
));
|
|
|
|
services.influxdb2.provision = mkIf cfg.provision.enable {
|
|
organizations.${cfg.provision.initialSetup.organization} = {
|
|
buckets.${cfg.provision.initialSetup.bucket} = {
|
|
inherit (cfg.provision.initialSetup) retention;
|
|
};
|
|
};
|
|
users.${cfg.provision.initialSetup.username} = {
|
|
inherit (cfg.provision.initialSetup) passwordFile;
|
|
};
|
|
};
|
|
|
|
systemd.services.influxdb2 = {
|
|
description = "InfluxDB is an open-source, distributed, time series database";
|
|
documentation = [ "https://docs.influxdata.com/influxdb/" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network.target" ];
|
|
environment = {
|
|
INFLUXD_CONFIG_PATH = configFile;
|
|
ZONEINFO = "${pkgs.tzdata}/share/zoneinfo";
|
|
};
|
|
serviceConfig = {
|
|
ExecStart = "${cfg.package}/bin/influxd --bolt-path \${STATE_DIRECTORY}/influxd.bolt --engine-path \${STATE_DIRECTORY}/engine";
|
|
StateDirectory = "influxdb2";
|
|
User = "influxdb2";
|
|
Group = "influxdb2";
|
|
CapabilityBoundingSet = "";
|
|
SystemCallFilter = "@system-service";
|
|
LimitNOFILE = 65536;
|
|
KillMode = "control-group";
|
|
Restart = "on-failure";
|
|
LoadCredential = mkIf cfg.provision.enable [
|
|
"admin-password:${cfg.provision.initialSetup.passwordFile}"
|
|
"admin-token:${cfg.provision.initialSetup.tokenFile}"
|
|
];
|
|
|
|
ExecStartPost = [
|
|
waitUntilServiceIsReady
|
|
] ++ (lib.optionals cfg.provision.enable (
|
|
[provisioningScript] ++
|
|
# Only the restarter runs with elevated privileges
|
|
optional anyAuthDefined "+${restarterScript}"
|
|
));
|
|
};
|
|
|
|
path = [
|
|
pkgs.influxdb2-cli
|
|
pkgs.jq
|
|
];
|
|
|
|
# Mark if this is the first startup so postStart can do the initial setup.
|
|
# Also extract any token secret mappings and apply them if this isn't the first start.
|
|
preStart = let
|
|
tokenPaths = listToAttrs (flatten
|
|
# For all organizations
|
|
(flip mapAttrsToList cfg.provision.organizations
|
|
# For each contained token that has a token file
|
|
(_: org: flip mapAttrsToList (filterAttrs (_: x: x.tokenFile != null) org.auths)
|
|
# Collect id -> tokenFile for the mapping
|
|
(_: auth: nameValuePair auth.id auth.tokenFile))));
|
|
tokenMappings = pkgs.writeText "token_mappings.json" (builtins.toJSON tokenPaths);
|
|
in mkIf cfg.provision.enable ''
|
|
if ! test -e "$STATE_DIRECTORY/influxd.bolt"; then
|
|
touch "$STATE_DIRECTORY/.first_startup"
|
|
else
|
|
# Manipulate provisioned api tokens if necessary
|
|
${getExe pkgs.influxdb2-token-manipulator} "$STATE_DIRECTORY/influxd.bolt" ${tokenMappings}
|
|
fi
|
|
'';
|
|
};
|
|
|
|
users.extraUsers.influxdb2 = {
|
|
isSystemUser = true;
|
|
group = "influxdb2";
|
|
};
|
|
|
|
users.extraGroups.influxdb2 = {};
|
|
};
|
|
|
|
meta.maintainers = with lib.maintainers; [ nickcao oddlama ];
|
|
}
|