Merge pull request #191768 from KFearsoff/grafana-rfc42

nixos/grafana: refactor for RFC42

Changed files
+1392 -532
nixos
+28
nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
···
</listitem>
<listitem>
<para>
Matrix Synapse now requires entries in the
<literal>state_group_edges</literal> table to be unique, in
order to prevent accidentally introducing duplicate
···
</listitem>
<listitem>
<para>
+
The <literal>services.grafana</literal> options were converted
+
to a
+
<link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">RFC
+
0042</link> configuration.
+
</para>
+
</listitem>
+
<listitem>
+
<para>
+
The <literal>services.grafana.provision.datasources</literal>
+
and <literal>services.grafana.provision.dashboards</literal>
+
options were converted to a
+
<link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">RFC
+
0042</link> configuration. They also now support specifying
+
the provisioning YAML file with <literal>path</literal>
+
option.
+
</para>
+
</listitem>
+
<listitem>
+
<para>
+
The <literal>services.grafana.provision.alerting</literal>
+
option was added. It includes suboptions for every
+
alerting-related objects (with the exception of
+
<literal>notifiers</literal>), which means it’s now possible
+
to configure modern Grafana alerting declaratively.
+
</para>
+
</listitem>
+
<listitem>
+
<para>
Matrix Synapse now requires entries in the
<literal>state_group_edges</literal> table to be unique, in
order to prevent accidentally introducing duplicate
+6
nixos/doc/manual/release-notes/rl-2211.section.md
···
- The `services.matrix-synapse` systemd unit has been hardened.
- Matrix Synapse now requires entries in the `state_group_edges` table to be unique, in order to prevent accidentally introducing duplicate information (for example, because a database backup was restored multiple times). If your Synapse database already has duplicate rows in this table, this could fail with an error and require manual remediation.
- The `diamond` package has been update from 0.8.36 to 2.0.15. See the [upstream release notes](https://github.com/bbuchfink/diamond/releases) for more details.
···
- The `services.matrix-synapse` systemd unit has been hardened.
+
- The `services.grafana` options were converted to a [RFC 0042](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md) configuration.
+
+
- The `services.grafana.provision.datasources` and `services.grafana.provision.dashboards` options were converted to a [RFC 0042](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md) configuration. They also now support specifying the provisioning YAML file with `path` option.
+
+
- The `services.grafana.provision.alerting` option was added. It includes suboptions for every alerting-related objects (with the exception of `notifiers`), which means it's now possible to configure modern Grafana alerting declaratively.
+
- Matrix Synapse now requires entries in the `state_group_edges` table to be unique, in order to prevent accidentally introducing duplicate information (for example, because a database backup was restored multiple times). If your Synapse database already has duplicate rows in this table, this could fail with an error and require manual remediation.
- The `diamond` package has been update from 0.8.36 to 2.0.15. See the [upstream release notes](https://github.com/bbuchfink/diamond/releases) for more details.
+3 -3
nixos/modules/services/monitoring/grafana-image-renderer.nix
···
}
];
-
services.grafana.extraOptions = mkIf cfg.provisionGrafana {
-
RENDERING_SERVER_URL = "http://localhost:${toString cfg.settings.service.port}/render";
-
RENDERING_CALLBACK_URL = "http://localhost:${toString config.services.grafana.port}";
};
services.grafana-image-renderer.chromium = mkDefault pkgs.chromium;
···
}
];
+
services.grafana.settings.rendering = mkIf cfg.provisionGrafana {
+
url = "http://localhost:${toString cfg.settings.service.port}/render";
+
callback_url = "http://localhost:${toString config.services.grafana.port}";
};
services.grafana-image-renderer.chromium = mkDefault pkgs.chromium;
+998 -519
nixos/modules/services/monitoring/grafana.nix
···
let
cfg = config.services.grafana;
opt = options.services.grafana;
declarativePlugins = pkgs.linkFarm "grafana-plugins" (builtins.map (pkg: { name = pkg.pname; path = pkg; }) cfg.declarativePlugins);
-
useMysql = cfg.database.type == "mysql";
-
usePostgresql = cfg.database.type == "postgres";
-
-
envOptions = {
-
PATHS_DATA = cfg.dataDir;
-
PATHS_PLUGINS = if builtins.isNull cfg.declarativePlugins then "${cfg.dataDir}/plugins" else declarativePlugins;
-
PATHS_LOGS = "${cfg.dataDir}/log";
-
-
SERVER_SERVE_FROM_SUBPATH = boolToString cfg.server.serveFromSubPath;
-
SERVER_PROTOCOL = cfg.protocol;
-
SERVER_HTTP_ADDR = cfg.addr;
-
SERVER_HTTP_PORT = cfg.port;
-
SERVER_SOCKET = cfg.socket;
-
SERVER_DOMAIN = cfg.domain;
-
SERVER_ROOT_URL = cfg.rootUrl;
-
SERVER_STATIC_ROOT_PATH = cfg.staticRootPath;
-
SERVER_CERT_FILE = cfg.certFile;
-
SERVER_CERT_KEY = cfg.certKey;
-
-
DATABASE_TYPE = cfg.database.type;
-
DATABASE_HOST = cfg.database.host;
-
DATABASE_NAME = cfg.database.name;
-
DATABASE_USER = cfg.database.user;
-
DATABASE_PASSWORD = cfg.database.password;
-
DATABASE_PATH = cfg.database.path;
-
DATABASE_CONN_MAX_LIFETIME = cfg.database.connMaxLifetime;
-
-
SECURITY_ADMIN_USER = cfg.security.adminUser;
-
SECURITY_ADMIN_PASSWORD = cfg.security.adminPassword;
-
SECURITY_SECRET_KEY = cfg.security.secretKey;
-
-
USERS_ALLOW_SIGN_UP = boolToString cfg.users.allowSignUp;
-
USERS_ALLOW_ORG_CREATE = boolToString cfg.users.allowOrgCreate;
-
USERS_AUTO_ASSIGN_ORG = boolToString cfg.users.autoAssignOrg;
-
USERS_AUTO_ASSIGN_ORG_ROLE = cfg.users.autoAssignOrgRole;
-
-
AUTH_DISABLE_LOGIN_FORM = boolToString cfg.auth.disableLoginForm;
-
AUTH_ANONYMOUS_ENABLED = boolToString cfg.auth.anonymous.enable;
-
AUTH_ANONYMOUS_ORG_NAME = cfg.auth.anonymous.org_name;
-
AUTH_ANONYMOUS_ORG_ROLE = cfg.auth.anonymous.org_role;
-
-
AUTH_AZUREAD_NAME = "Azure AD";
-
AUTH_AZUREAD_ENABLED = boolToString cfg.auth.azuread.enable;
-
AUTH_AZUREAD_ALLOW_SIGN_UP = boolToString cfg.auth.azuread.allowSignUp;
-
AUTH_AZUREAD_CLIENT_ID = cfg.auth.azuread.clientId;
-
AUTH_AZUREAD_SCOPES = "openid email profile";
-
AUTH_AZUREAD_AUTH_URL = "https://login.microsoftonline.com/${cfg.auth.azuread.tenantId}/oauth2/v2.0/authorize";
-
AUTH_AZUREAD_TOKEN_URL = "https://login.microsoftonline.com/${cfg.auth.azuread.tenantId}/oauth2/v2.0/token";
-
AUTH_AZUREAD_ALLOWED_DOMAINS = cfg.auth.azuread.allowedDomains;
-
AUTH_AZUREAD_ALLOWED_GROUPS = cfg.auth.azuread.allowedGroups;
-
AUTH_AZUREAD_ROLE_ATTRIBUTE_STRICT = false;
-
-
AUTH_GOOGLE_ENABLED = boolToString cfg.auth.google.enable;
-
AUTH_GOOGLE_ALLOW_SIGN_UP = boolToString cfg.auth.google.allowSignUp;
-
AUTH_GOOGLE_CLIENT_ID = cfg.auth.google.clientId;
-
-
ANALYTICS_REPORTING_ENABLED = boolToString cfg.analytics.reporting.enable;
-
-
SMTP_ENABLED = boolToString cfg.smtp.enable;
-
SMTP_HOST = cfg.smtp.host;
-
SMTP_USER = cfg.smtp.user;
-
SMTP_PASSWORD = cfg.smtp.password;
-
SMTP_FROM_ADDRESS = cfg.smtp.fromAddress;
-
} // cfg.extraOptions;
datasourceConfiguration = {
apiVersion = 1;
datasources = cfg.provision.datasources;
};
-
datasourceFile = pkgs.writeText "datasource.yaml" (builtins.toJSON datasourceConfiguration);
dashboardConfiguration = {
apiVersion = 1;
providers = cfg.provision.dashboards;
};
-
dashboardFile = pkgs.writeText "dashboard.yaml" (builtins.toJSON dashboardConfiguration);
notifierConfiguration = {
apiVersion = 1;
···
};
notifierFile = pkgs.writeText "notifier.yaml" (builtins.toJSON notifierConfiguration);
provisionConfDir = pkgs.runCommand "grafana-provisioning" { } ''
-
mkdir -p $out/{datasources,dashboards,notifiers}
ln -sf ${datasourceFile} $out/datasources/datasource.yaml
ln -sf ${dashboardFile} $out/dashboards/dashboard.yaml
ln -sf ${notifierFile} $out/notifiers/notifier.yaml
'';
# Get a submodule without any embedded metadata:
···
# http://docs.grafana.org/administration/provisioning/#datasources
grafanaTypes.datasourceConfig = types.submodule {
options = {
name = mkOption {
type = types.str;
···
default = "proxy";
description = lib.mdDoc "Access mode. proxy or direct (Server or Browser in the UI). Required.";
};
-
orgId = mkOption {
-
type = types.int;
-
default = 1;
-
description = lib.mdDoc "Org id. will default to orgId 1 if not specified.";
-
};
uid = mkOption {
type = types.nullOr types.str;
default = null;
···
};
url = mkOption {
type = types.str;
description = lib.mdDoc "Url of the datasource.";
};
-
password = mkOption {
-
type = types.nullOr types.str;
-
default = null;
-
description = lib.mdDoc "Database password, if used.";
-
};
-
user = mkOption {
-
type = types.nullOr types.str;
-
default = null;
-
description = lib.mdDoc "Database user, if used.";
-
};
-
database = mkOption {
-
type = types.nullOr types.str;
-
default = null;
-
description = lib.mdDoc "Database name, if used.";
-
};
-
basicAuth = mkOption {
-
type = types.nullOr types.bool;
-
default = null;
-
description = lib.mdDoc "Enable/disable basic auth.";
};
-
basicAuthUser = mkOption {
type = types.nullOr types.str;
default = null;
-
description = lib.mdDoc "Basic auth username.";
};
basicAuthPassword = mkOption {
type = types.nullOr types.str;
default = null;
-
description = lib.mdDoc "Basic auth password.";
-
};
-
withCredentials = mkOption {
-
type = types.bool;
-
default = false;
-
description = lib.mdDoc "Enable/disable with credentials headers.";
-
};
-
isDefault = mkOption {
-
type = types.bool;
-
default = false;
-
description = lib.mdDoc "Mark as default datasource. Max one per org.";
-
};
-
jsonData = mkOption {
-
type = types.nullOr types.attrs;
-
default = null;
-
description = lib.mdDoc "Datasource specific configuration.";
};
secureJsonData = mkOption {
type = types.nullOr types.attrs;
default = null;
-
description = lib.mdDoc "Datasource specific secure configuration.";
-
};
-
version = mkOption {
-
type = types.int;
-
default = 1;
-
description = lib.mdDoc "Version.";
-
};
-
editable = mkOption {
-
type = types.bool;
-
default = false;
-
description = lib.mdDoc "Allow users to edit datasources from the UI.";
};
};
};
# http://docs.grafana.org/administration/provisioning/#dashboards
grafanaTypes.dashboardConfig = types.submodule {
options = {
name = mkOption {
type = types.str;
default = "default";
-
description = lib.mdDoc "Provider name.";
-
};
-
orgId = mkOption {
-
type = types.int;
-
default = 1;
-
description = lib.mdDoc "Organization ID.";
-
};
-
folder = mkOption {
-
type = types.str;
-
default = "";
-
description = lib.mdDoc "Add dashboards to the specified folder.";
};
type = mkOption {
type = types.str;
default = "file";
description = lib.mdDoc "Dashboard provider type.";
};
-
disableDeletion = mkOption {
-
type = types.bool;
-
default = false;
-
description = lib.mdDoc "Disable deletion when JSON file is removed.";
-
};
-
updateIntervalSeconds = mkOption {
-
type = types.int;
-
default = 10;
-
description = lib.mdDoc "How often Grafana will scan for changed dashboards.";
-
};
-
options = {
-
path = mkOption {
-
type = types.path;
-
description = lib.mdDoc "Path grafana will watch for dashboards.";
-
};
-
foldersFromFilesStructure = mkOption {
-
type = types.bool;
-
default = false;
-
description = lib.mdDoc "Use folder names from filesystem to create folders in Grafana.";
-
};
};
};
};
···
secure_settings = mkOption {
type = types.nullOr types.attrs;
default = null;
-
description = lib.mdDoc "Secure settings for the notifier type.";
};
};
};
in {
-
options.services.grafana = {
-
enable = mkEnableOption (lib.mdDoc "grafana");
-
-
protocol = mkOption {
-
description = lib.mdDoc "Which protocol to listen.";
-
default = "http";
-
type = types.enum ["http" "https" "socket"];
-
};
-
-
addr = mkOption {
-
description = lib.mdDoc "Listening address.";
-
default = "127.0.0.1";
-
type = types.str;
-
};
-
-
port = mkOption {
-
description = lib.mdDoc "Listening port.";
-
default = 3000;
-
type = types.port;
-
};
-
-
socket = mkOption {
-
description = lib.mdDoc "Listening socket.";
-
default = "/run/grafana/grafana.sock";
-
type = types.str;
-
};
-
-
domain = mkOption {
-
description = lib.mdDoc "The public facing domain name used to access grafana from a browser.";
-
default = "localhost";
-
type = types.str;
-
};
-
-
rootUrl = mkOption {
-
description = lib.mdDoc "Full public facing url.";
-
default = "%(protocol)s://%(domain)s:%(http_port)s/";
-
type = types.str;
-
};
-
-
certFile = mkOption {
-
description = lib.mdDoc "Cert file for ssl.";
-
default = "";
-
type = types.str;
-
};
-
certKey = mkOption {
-
description = lib.mdDoc "Cert key for ssl.";
-
default = "";
-
type = types.str;
-
};
-
staticRootPath = mkOption {
-
description = lib.mdDoc "Root path for static assets.";
-
default = "${cfg.package}/share/grafana/public";
-
defaultText = literalExpression ''"''${package}/share/grafana/public"'';
-
type = types.str;
-
};
-
package = mkOption {
-
description = lib.mdDoc "Package to use.";
-
default = pkgs.grafana;
-
defaultText = literalExpression "pkgs.grafana";
-
type = types.package;
-
};
declarativePlugins = mkOption {
type = with types; nullOr (listOf path);
···
apply = x: if isList x then lib.unique x else x;
};
dataDir = mkOption {
description = lib.mdDoc "Data directory.";
default = "/var/lib/grafana";
type = types.path;
};
-
database = {
-
type = mkOption {
-
description = lib.mdDoc "Database type.";
-
default = "sqlite3";
-
type = types.enum ["mysql" "sqlite3" "postgres"];
-
};
-
host = mkOption {
-
description = lib.mdDoc "Database host.";
-
default = "127.0.0.1:3306";
-
type = types.str;
-
};
-
name = mkOption {
-
description = lib.mdDoc "Database name.";
-
default = "grafana";
-
type = types.str;
-
};
-
user = mkOption {
-
description = lib.mdDoc "Database user.";
-
default = "root";
-
type = types.str;
-
};
-
password = mkOption {
-
description = lib.mdDoc ''
-
Database password.
-
This option is mutual exclusive with the passwordFile option.
-
'';
-
default = "";
-
type = types.str;
-
};
-
passwordFile = mkOption {
-
description = lib.mdDoc ''
-
File that containts the database password.
-
This option is mutual exclusive with the password option.
-
'';
-
default = null;
-
type = types.nullOr types.path;
-
};
-
path = mkOption {
-
description = lib.mdDoc "Database path.";
-
default = "${cfg.dataDir}/data/grafana.db";
-
defaultText = literalExpression ''"''${config.${opt.dataDir}}/data/grafana.db"'';
-
type = types.path;
-
};
-
connMaxLifetime = mkOption {
-
description = lib.mdDoc ''
-
Sets the maximum amount of time (in seconds) a connection may be reused.
-
For MySQL this setting should be shorter than the `wait_timeout' variable.
-
'';
-
default = "unlimited";
-
example = 14400;
-
type = types.either types.int (types.enum [ "unlimited" ]);
};
};
provision = {
enable = mkEnableOption (lib.mdDoc "provision");
datasources = mkOption {
-
description = lib.mdDoc "Grafana datasources configuration.";
default = [];
-
type = types.listOf grafanaTypes.datasourceConfig;
-
apply = x: map _filter x;
};
dashboards = mkOption {
-
description = lib.mdDoc "Grafana dashboard configuration.";
default = [];
-
type = types.listOf grafanaTypes.dashboardConfig;
-
apply = x: map _filter x;
};
notifiers = mkOption {
description = lib.mdDoc "Grafana notifier configuration.";
default = [];
type = types.listOf grafanaTypes.notifierConfig;
apply = x: map _filter x;
};
-
};
-
security = {
-
adminUser = mkOption {
-
description = lib.mdDoc "Default admin username.";
-
default = "admin";
-
type = types.str;
-
};
-
adminPassword = mkOption {
-
description = lib.mdDoc ''
-
Default admin password.
-
This option is mutual exclusive with the adminPasswordFile option.
-
'';
-
default = "admin";
-
type = types.str;
-
};
-
adminPasswordFile = mkOption {
-
description = lib.mdDoc ''
-
Default admin password.
-
This option is mutual exclusive with the `adminPassword` option.
-
'';
-
default = null;
-
type = types.nullOr types.path;
-
};
-
secretKey = mkOption {
-
description = lib.mdDoc "Secret key used for signing.";
-
default = "SW2YcwTIb9zpOOhoPsMm";
-
type = types.str;
-
};
-
secretKeyFile = mkOption {
-
description = lib.mdDoc "Secret key used for signing.";
-
default = null;
-
type = types.nullOr types.path;
-
};
-
};
-
server = {
-
serveFromSubPath = mkOption {
-
description = lib.mdDoc "Serve Grafana from subpath specified in rootUrl setting";
-
default = false;
-
type = types.bool;
-
};
-
};
-
smtp = {
-
enable = mkEnableOption (lib.mdDoc "smtp");
-
host = mkOption {
-
description = lib.mdDoc "Host to connect to.";
-
default = "localhost:25";
-
type = types.str;
-
};
-
user = mkOption {
-
description = lib.mdDoc "User used for authentication.";
-
default = "";
-
type = types.str;
-
};
-
password = mkOption {
-
description = lib.mdDoc ''
-
Password used for authentication.
-
This option is mutual exclusive with the passwordFile option.
-
'';
-
default = "";
-
type = types.str;
-
};
-
passwordFile = mkOption {
-
description = lib.mdDoc ''
-
Password used for authentication.
-
This option is mutual exclusive with the password option.
-
'';
-
default = null;
-
type = types.nullOr types.path;
-
};
-
fromAddress = mkOption {
-
description = lib.mdDoc "Email address used for sending.";
-
default = "admin@grafana.localhost";
-
type = types.str;
-
};
-
};
-
users = {
-
allowSignUp = mkOption {
-
description = lib.mdDoc "Disable user signup / registration.";
-
default = false;
-
type = types.bool;
-
};
-
allowOrgCreate = mkOption {
-
description = lib.mdDoc "Whether user is allowed to create organizations.";
-
default = false;
-
type = types.bool;
-
};
-
autoAssignOrg = mkOption {
-
description = lib.mdDoc "Whether to automatically assign new users to default org.";
-
default = true;
-
type = types.bool;
-
};
-
autoAssignOrgRole = mkOption {
-
description = lib.mdDoc "Default role new users will be auto assigned.";
-
default = "Viewer";
-
type = types.enum ["Viewer" "Editor"];
-
};
-
};
-
auth = {
-
disableLoginForm = mkOption {
-
description = lib.mdDoc "Set to true to disable (hide) the login form, useful if you use OAuth";
-
default = false;
-
type = types.bool;
-
};
-
anonymous = {
-
enable = mkOption {
-
description = lib.mdDoc "Whether to allow anonymous access.";
-
default = false;
-
type = types.bool;
-
};
-
org_name = mkOption {
-
description = lib.mdDoc "Which organization to allow anonymous access to.";
-
default = "Main Org.";
-
type = types.str;
-
};
-
org_role = mkOption {
-
description = lib.mdDoc "Which role anonymous users have in the organization.";
-
default = "Viewer";
-
type = types.str;
-
};
-
};
-
azuread = {
-
enable = mkOption {
-
description = lib.mdDoc "Whether to allow Azure AD OAuth.";
-
default = false;
-
type = types.bool;
-
};
-
allowSignUp = mkOption {
-
description = lib.mdDoc "Whether to allow sign up with Azure AD OAuth.";
-
default = false;
-
type = types.bool;
-
};
-
clientId = mkOption {
-
description = lib.mdDoc "Azure AD OAuth client ID.";
-
default = "";
-
type = types.str;
-
};
-
clientSecretFile = mkOption {
-
description = lib.mdDoc "Azure AD OAuth client secret.";
-
default = null;
-
type = types.nullOr types.path;
-
};
-
tenantId = mkOption {
-
description = lib.mdDoc ''
-
Tenant id used to create auth and token url. Default to "common"
-
, let user sign in with any tenant.
'';
-
default = "common";
-
type = types.str;
-
};
-
allowedDomains = mkOption {
-
description = lib.mdDoc ''
-
Limits access to users who belong to specific domains.
-
Separate domains with space or comma.
-
'';
-
default = "";
-
type = types.str;
-
};
-
allowedGroups = mkOption {
-
description = lib.mdDoc ''
-
To limit access to authenticated users who are members of one or more groups,
-
set allowedGroups to a comma- or space-separated list of group object IDs.
-
You can find object IDs for a specific group on the Azure portal.
'';
-
default = "";
-
type = types.str;
-
};
-
};
-
google = {
-
enable = mkOption {
-
description = lib.mdDoc "Whether to allow Google OAuth2.";
-
default = false;
-
type = types.bool;
-
};
-
allowSignUp = mkOption {
-
description = lib.mdDoc "Whether to allow sign up with Google OAuth2.";
-
default = false;
-
type = types.bool;
};
-
clientId = mkOption {
-
description = lib.mdDoc "Google OAuth2 client ID.";
-
default = "";
-
type = types.str;
};
-
clientSecretFile = mkOption {
-
description = lib.mdDoc "Google OAuth2 client secret.";
-
default = null;
-
type = types.nullOr types.path;
};
-
};
-
};
-
analytics.reporting = {
-
enable = mkOption {
-
description = lib.mdDoc "Whether to allow anonymous usage reporting to stats.grafana.net.";
-
default = true;
-
type = types.bool;
};
};
-
-
extraOptions = mkOption {
-
description = lib.mdDoc ''
-
Extra configuration options passed as env variables as specified in
-
[documentation](http://docs.grafana.org/installation/configuration/),
-
but without GF_ prefix
-
'';
-
default = {};
-
type = with types; attrsOf (either str path);
-
};
};
config = mkIf cfg.enable {
warnings = flatten [
(optional (
-
cfg.database.password != opt.database.password.default ||
-
cfg.security.adminPassword != opt.security.adminPassword.default
-
) "Grafana passwords will be stored as plaintext in the Nix store!")
(optional (
-
any (x: x.password != null || x.basicAuthPassword != null || x.secureJsonData != null) cfg.provision.datasources
-
) "Datasource passwords will be stored as plaintext in the Nix store!")
(optional (
any (x: x.secure_settings != null) cfg.provision.notifiers
-
) "Notifier secure settings will be stored as plaintext in the Nix store!")
];
environment.systemPackages = [ cfg.package ];
assertions = [
{
-
assertion = cfg.database.password != opt.database.password.default -> cfg.database.passwordFile == null;
-
message = "Cannot set both password and passwordFile";
}
{
-
assertion = cfg.security.adminPassword != opt.security.adminPassword.default -> cfg.security.adminPasswordFile == null;
-
message = "Cannot set both adminPassword and adminPasswordFile";
}
{
-
assertion = cfg.security.secretKey != opt.security.secretKey.default -> cfg.security.secretKeyFile == null;
-
message = "Cannot set both secretKey and secretKeyFile";
}
{
-
assertion = cfg.smtp.password != opt.smtp.password.default -> cfg.smtp.passwordFile == null;
-
message = "Cannot set both password and passwordFile";
}
{
-
assertion = all
-
({ type, access, ... }: type == "prometheus" -> access != "direct")
-
cfg.provision.datasources;
-
message = "For datasources of type `prometheus`, the `direct` access mode is not supported anymore (since Grafana 9.2.0)";
}
];
···
description = "Grafana Service Daemon";
wantedBy = ["multi-user.target"];
after = ["networking.target"] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
-
environment = {
-
QT_QPA_PLATFORM = "offscreen";
-
} // mapAttrs' (n: v: nameValuePair "GF_${n}" (toString v)) envOptions;
script = ''
set -o errexit -o pipefail -o nounset -o errtrace
shopt -s inherit_errexit
-
${optionalString (cfg.auth.azuread.clientSecretFile != null) ''
-
GF_AUTH_AZUREAD_CLIENT_SECRET="$(<${escapeShellArg cfg.auth.azuread.clientSecretFile})"
-
export GF_AUTH_AZUREAD_CLIENT_SECRET
-
''}
-
${optionalString (cfg.auth.google.clientSecretFile != null) ''
-
GF_AUTH_GOOGLE_CLIENT_SECRET="$(<${escapeShellArg cfg.auth.google.clientSecretFile})"
-
export GF_AUTH_GOOGLE_CLIENT_SECRET
-
''}
-
${optionalString (cfg.database.passwordFile != null) ''
-
GF_DATABASE_PASSWORD="$(<${escapeShellArg cfg.database.passwordFile})"
-
export GF_DATABASE_PASSWORD
-
''}
-
${optionalString (cfg.security.adminPasswordFile != null) ''
-
GF_SECURITY_ADMIN_PASSWORD="$(<${escapeShellArg cfg.security.adminPasswordFile})"
-
export GF_SECURITY_ADMIN_PASSWORD
-
''}
-
${optionalString (cfg.security.secretKeyFile != null) ''
-
GF_SECURITY_SECRET_KEY="$(<${escapeShellArg cfg.security.secretKeyFile})"
-
export GF_SECURITY_SECRET_KEY
-
''}
-
${optionalString (cfg.smtp.passwordFile != null) ''
-
GF_SMTP_PASSWORD="$(<${escapeShellArg cfg.smtp.passwordFile})"
-
export GF_SMTP_PASSWORD
-
''}
-
${optionalString cfg.provision.enable ''
-
export GF_PATHS_PROVISIONING=${provisionConfDir};
-
''}
-
exec ${cfg.package}/bin/grafana-server -homepath ${cfg.dataDir}
'';
serviceConfig = {
WorkingDirectory = cfg.dataDir;
···
let
cfg = config.services.grafana;
opt = options.services.grafana;
+
provisioningSettingsFormat = pkgs.formats.yaml {};
declarativePlugins = pkgs.linkFarm "grafana-plugins" (builtins.map (pkg: { name = pkg.pname; path = pkg; }) cfg.declarativePlugins);
+
useMysql = cfg.settings.database.type == "mysql";
+
usePostgresql = cfg.settings.database.type == "postgres";
+
settingsFormatIni = pkgs.formats.ini {};
+
configFile = settingsFormatIni.generate "config.ini" cfg.settings;
datasourceConfiguration = {
apiVersion = 1;
datasources = cfg.provision.datasources;
};
+
datasourceFileNew = if (cfg.provision.datasources.path == null) then provisioningSettingsFormat.generate "datasource.yaml" cfg.provision.datasources.settings else cfg.provision.datasources.path;
+
datasourceFile = if (builtins.isList cfg.provision.datasources) then provisioningSettingsFormat.generate "datasource.yaml" datasourceConfiguration else datasourceFileNew;
dashboardConfiguration = {
apiVersion = 1;
providers = cfg.provision.dashboards;
};
+
dashboardFileNew = if (cfg.provision.dashboards.path == null) then provisioningSettingsFormat.generate "dashboard.yaml" cfg.provision.dashboards.settings else cfg.provision.dashboards.path;
+
dashboardFile = if (builtins.isList cfg.provision.dashboards) then provisioningSettingsFormat.generate "dashboard.yaml" dashboardConfiguration else dashboardFileNew;
notifierConfiguration = {
apiVersion = 1;
···
};
notifierFile = pkgs.writeText "notifier.yaml" (builtins.toJSON notifierConfiguration);
+
+
generateAlertingProvisioningYaml = x: if (cfg.provision.alerting."${x}".path == null)
+
then provisioningSettingsFormat.generate "${x}.yaml" cfg.provision.alerting."${x}".settings
+
else cfg.provision.alerting."${x}".path;
+
rulesFile = generateAlertingProvisioningYaml "rules";
+
contactPointsFile = generateAlertingProvisioningYaml "contactPoints";
+
policiesFile = generateAlertingProvisioningYaml "policies";
+
templatesFile = generateAlertingProvisioningYaml "templates";
+
muteTimingsFile = generateAlertingProvisioningYaml "muteTimings";
provisionConfDir = pkgs.runCommand "grafana-provisioning" { } ''
+
mkdir -p $out/{datasources,dashboards,notifiers,alerting}
ln -sf ${datasourceFile} $out/datasources/datasource.yaml
ln -sf ${dashboardFile} $out/dashboards/dashboard.yaml
ln -sf ${notifierFile} $out/notifiers/notifier.yaml
+
ln -sf ${rulesFile} $out/alerting/rules.yaml
+
ln -sf ${contactPointsFile} $out/alerting/contactPoints.yaml
+
ln -sf ${policiesFile} $out/alerting/policies.yaml
+
ln -sf ${templatesFile} $out/alerting/templates.yaml
+
ln -sf ${muteTimingsFile} $out/alerting/muteTimings.yaml
'';
# Get a submodule without any embedded metadata:
···
# http://docs.grafana.org/administration/provisioning/#datasources
grafanaTypes.datasourceConfig = types.submodule {
+
freeformType = provisioningSettingsFormat.type;
+
options = {
name = mkOption {
type = types.str;
···
default = "proxy";
description = lib.mdDoc "Access mode. proxy or direct (Server or Browser in the UI). Required.";
};
uid = mkOption {
type = types.nullOr types.str;
default = null;
···
};
url = mkOption {
type = types.str;
+
default = "localhost";
description = lib.mdDoc "Url of the datasource.";
};
+
editable = mkOption {
+
type = types.bool;
+
default = false;
+
description = lib.mdDoc "Allow users to edit datasources from the UI.";
};
+
password = mkOption {
type = types.nullOr types.str;
default = null;
+
description = lib.mdDoc ''
+
Database password, if used. Please note that the contents of this option
+
will end up in a world-readable Nix store. Use the file provider
+
pointing at a reasonably secured file in the local filesystem
+
to work around that. Look at the documentation for details:
+
<https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
+
'';
};
basicAuthPassword = mkOption {
type = types.nullOr types.str;
default = null;
+
description = lib.mdDoc ''
+
Basic auth password. Please note that the contents of this option
+
will end up in a world-readable Nix store. Use the file provider
+
pointing at a reasonably secured file in the local filesystem
+
to work around that. Look at the documentation for details:
+
<https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
+
'';
};
secureJsonData = mkOption {
type = types.nullOr types.attrs;
default = null;
+
description = lib.mdDoc ''
+
Datasource specific secure configuration. Please note that the contents of this option
+
will end up in a world-readable Nix store. Use the file provider
+
pointing at a reasonably secured file in the local filesystem
+
to work around that. Look at the documentation for details:
+
<https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
+
'';
};
};
};
# http://docs.grafana.org/administration/provisioning/#dashboards
grafanaTypes.dashboardConfig = types.submodule {
+
freeformType = provisioningSettingsFormat.type;
+
options = {
name = mkOption {
type = types.str;
default = "default";
+
description = lib.mdDoc "A unique provider name.";
};
type = mkOption {
type = types.str;
default = "file";
description = lib.mdDoc "Dashboard provider type.";
};
+
options.path = mkOption {
+
type = types.path;
+
description = lib.mdDoc "Path grafana will watch for dashboards. Required when using the 'file' type.";
};
};
};
···
secure_settings = mkOption {
type = types.nullOr types.attrs;
default = null;
+
description = lib.mdDoc ''
+
Secure settings for the notifier type. Please note that the contents of this option
+
will end up in a world-readable Nix store. Use the file provider
+
pointing at a reasonably secured file in the local filesystem
+
to work around that. Look at the documentation for details:
+
<https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
+
'';
};
};
};
in {
+
imports = [
+
(mkRenamedOptionModule [ "services" "grafana" "protocol" ] [ "services" "grafana" "settings" "server" "protocol" ])
+
(mkRenamedOptionModule [ "services" "grafana" "addr" ] [ "services" "grafana" "settings" "server" "http_addr" ])
+
(mkRenamedOptionModule [ "services" "grafana" "port" ] [ "services" "grafana" "settings" "server" "http_port" ])
+
(mkRenamedOptionModule [ "services" "grafana" "domain" ] [ "services" "grafana" "settings" "server" "domain" ])
+
(mkRenamedOptionModule [ "services" "grafana" "rootUrl" ] [ "services" "grafana" "settings" "server" "root_url" ])
+
(mkRenamedOptionModule [ "services" "grafana" "staticRootPath" ] [ "services" "grafana" "settings" "server" "static_root_path" ])
+
(mkRenamedOptionModule [ "services" "grafana" "certFile" ] [ "services" "grafana" "settings" "server" "cert_file" ])
+
(mkRenamedOptionModule [ "services" "grafana" "certKey" ] [ "services" "grafana" "settings" "server" "cert_key" ])
+
(mkRenamedOptionModule [ "services" "grafana" "socket" ] [ "services" "grafana" "settings" "server" "socket" ])
+
(mkRenamedOptionModule [ "services" "grafana" "database" "type" ] [ "services" "grafana" "settings" "database" "type" ])
+
(mkRenamedOptionModule [ "services" "grafana" "database" "host" ] [ "services" "grafana" "settings" "database" "host" ])
+
(mkRenamedOptionModule [ "services" "grafana" "database" "name" ] [ "services" "grafana" "settings" "database" "name" ])
+
(mkRenamedOptionModule [ "services" "grafana" "database" "user" ] [ "services" "grafana" "settings" "database" "user" ])
+
(mkRenamedOptionModule [ "services" "grafana" "database" "password" ] [ "services" "grafana" "settings" "database" "password" ])
+
(mkRenamedOptionModule [ "services" "grafana" "database" "path" ] [ "services" "grafana" "settings" "database" "path" ])
+
(mkRenamedOptionModule [ "services" "grafana" "database" "connMaxLifetime" ] [ "services" "grafana" "settings" "database" "conn_max_lifetime" ])
+
(mkRenamedOptionModule [ "services" "grafana" "security" "adminUser" ] [ "services" "grafana" "settings" "security" "admin_user" ])
+
(mkRenamedOptionModule [ "services" "grafana" "security" "adminPassword" ] [ "services" "grafana" "settings" "security" "admin_password" ])
+
(mkRenamedOptionModule [ "services" "grafana" "security" "secretKey" ] [ "services" "grafana" "settings" "security" "secret_key" ])
+
(mkRenamedOptionModule [ "services" "grafana" "server" "serveFromSubPath" ] [ "services" "grafana" "settings" "server" "serve_from_sub_path" ])
+
(mkRenamedOptionModule [ "services" "grafana" "smtp" "enable" ] [ "services" "grafana" "settings" "smtp" "enabled" ])
+
(mkRenamedOptionModule [ "services" "grafana" "smtp" "user" ] [ "services" "grafana" "settings" "smtp" "user" ])
+
(mkRenamedOptionModule [ "services" "grafana" "smtp" "password" ] [ "services" "grafana" "settings" "smtp" "password" ])
+
(mkRenamedOptionModule [ "services" "grafana" "smtp" "fromAddress" ] [ "services" "grafana" "settings" "smtp" "from_address" ])
+
(mkRenamedOptionModule [ "services" "grafana" "users" "allowSignUp" ] [ "services" "grafana" "settings" "users" "allow_sign_up" ])
+
(mkRenamedOptionModule [ "services" "grafana" "users" "allowOrgCreate" ] [ "services" "grafana" "settings" "users" "allow_org_create" ])
+
(mkRenamedOptionModule [ "services" "grafana" "users" "autoAssignOrg" ] [ "services" "grafana" "settings" "users" "auto_assign_org" ])
+
(mkRenamedOptionModule [ "services" "grafana" "users" "autoAssignOrgRole" ] [ "services" "grafana" "settings" "users" "auto_assign_org_role" ])
+
(mkRenamedOptionModule [ "services" "grafana" "auth" "disableLoginForm" ] [ "services" "grafana" "settings" "auth" "disable_login_form" ])
+
(mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "enable" ] [ "services" "grafana" "settings" "auth" "anonymous" "enable" ])
+
(mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "org_name" ] [ "services" "grafana" "settings" "auth" "anonymous" "org_name" ])
+
(mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "org_role" ] [ "services" "grafana" "settings" "auth" "anonymous" "org_role" ])
+
(mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "enable" ] [ "services" "grafana" "settings" "auth" "azuread" "enable" ])
+
(mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowSignUp" ] [ "services" "grafana" "settings" "auth" "azuread" "allow_sign_up" ])
+
(mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "clientId" ] [ "services" "grafana" "settings" "auth" "azuread" "client_id" ])
+
(mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowedDomains" ] [ "services" "grafana" "settings" "auth" "azuread" "allowed_domains" ])
+
(mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowedGroups" ] [ "services" "grafana" "settings" "auth" "azuread" "allowed_groups" ])
+
(mkRenamedOptionModule [ "services" "grafana" "auth" "google" "enable" ] [ "services" "grafana" "settings" "auth" "google" "enable" ])
+
(mkRenamedOptionModule [ "services" "grafana" "auth" "google" "allowSignUp" ] [ "services" "grafana" "settings" "auth" "google" "allow_sign_up" ])
+
(mkRenamedOptionModule [ "services" "grafana" "auth" "google" "clientId" ] [ "services" "grafana" "settings" "auth" "google" "client_id" ])
+
(mkRenamedOptionModule [ "services" "grafana" "analytics" "reporting" "enable" ] [ "services" "grafana" "settings" "analytics" "reporting_enabled" ])
+
(mkRemovedOptionModule [ "services" "grafana" "database" "passwordFile" ] ''
+
This option has been removed. Use 'services.grafana.settings.database.password' with file provider instead.
+
'')
+
(mkRemovedOptionModule [ "services" "grafana" "security" "adminPasswordFile" ] ''
+
This option has been removed. Use 'services.grafana.settings.security.admin_password' with file provider instead.
+
'')
+
(mkRemovedOptionModule [ "services" "grafana" "security" "secretKeyFile" ] ''
+
This option has been removed. Use 'services.grafana.settings.security.secret_key' with file provider instead.
+
'')
+
(mkRemovedOptionModule [ "services" "grafana" "smtp" "passwordFile" ] ''
+
This option has been removed. Use 'services.grafana.settings.smtp.password' with file provider instead.
+
'')
+
(mkRemovedOptionModule [ "services" "grafana" "auth" "azuread" "clientSecretFile" ] ''
+
This option has been removed. Use 'services.grafana.settings.azuread.client_secret' with file provider instead.
+
'')
+
(mkRemovedOptionModule [ "services" "grafana" "auth" "google" "clientSecretFile" ] ''
+
This option has been removed. Use 'services.grafana.settings.google.client_secret' with file provider instead.
+
'')
+
(mkRemovedOptionModule [ "services" "grafana" "auth" "azuread" "tenantId" ] "This option has been deprecated upstream.")
+
];
+
options.services.grafana = {
+
enable = mkEnableOption (lib.mdDoc "grafana");
declarativePlugins = mkOption {
type = with types; nullOr (listOf path);
···
apply = x: if isList x then lib.unique x else x;
};
+
package = mkOption {
+
description = lib.mdDoc "Package to use.";
+
default = pkgs.grafana;
+
defaultText = literalExpression "pkgs.grafana";
+
type = types.package;
+
};
+
dataDir = mkOption {
description = lib.mdDoc "Data directory.";
default = "/var/lib/grafana";
type = types.path;
};
+
settings = mkOption {
+
description = lib.mdDoc ''
+
Grafana settings. See <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/>
+
for available options. INI format is used.
+
'';
+
type = types.submodule {
+
freeformType = settingsFormatIni.type;
+
+
options = {
+
paths = {
+
plugins = mkOption {
+
description = lib.mdDoc "Directory where grafana will automatically scan and look for plugins";
+
default = if (cfg.declarativePlugins == null) then "${cfg.dataDir}/plugins" else declarativePlugins;
+
defaultText = literalExpression "if (cfg.declarativePlugins == null) then \"\${cfg.dataDir}/plugins\" else declarativePlugins";
+
type = types.path;
+
};
+
+
provisioning = mkOption {
+
description = lib.mdDoc ''
+
Folder that contains provisioning config files that grafana will apply on startup and while running.
+
Don't change the value of this option if you are planning to use `services.grafana.provision` options.
+
'';
+
default = provisionConfDir;
+
defaultText = literalExpression ''
+
pkgs.runCommand "grafana-provisioning" { } \'\'
+
mkdir -p $out/{datasources,dashboards,notifiers,alerting}
+
ln -sf ''${datasourceFile} $out/datasources/datasource.yaml
+
ln -sf ''${dashboardFile} $out/dashboards/dashboard.yaml
+
ln -sf ''${notifierFile} $out/notifiers/notifier.yaml
+
ln -sf ''${rulesFile} $out/alerting/rules.yaml
+
ln -sf ''${contactPointsFile} $out/alerting/contactPoints.yaml
+
ln -sf ''${policiesFile} $out/alerting/policies.yaml
+
ln -sf ''${templatesFile} $out/alerting/templates.yaml
+
ln -sf ''${muteTimingsFile} $out/alerting/muteTimings.yaml
+
\'\'
+
'';
+
type = types.path;
+
};
+
};
+
+
server = {
+
protocol = mkOption {
+
description = lib.mdDoc "Which protocol to listen.";
+
default = "http";
+
type = types.enum ["http" "https" "socket"];
+
};
+
+
http_addr = mkOption {
+
description = lib.mdDoc "Listening address.";
+
default = "";
+
type = types.str;
+
};
+
+
http_port = mkOption {
+
description = lib.mdDoc "Listening port.";
+
default = 3000;
+
type = types.port;
+
};
+
+
domain = mkOption {
+
description = lib.mdDoc "The public facing domain name used to access grafana from a browser.";
+
default = "localhost";
+
type = types.str;
+
};
+
+
root_url = mkOption {
+
description = lib.mdDoc "Full public facing url.";
+
default = "%(protocol)s://%(domain)s:%(http_port)s/";
+
type = types.str;
+
};
+
+
static_root_path = mkOption {
+
description = lib.mdDoc "Root path for static assets.";
+
default = "${cfg.package}/share/grafana/public";
+
defaultText = literalExpression ''"''${package}/share/grafana/public"'';
+
type = types.str;
+
};
+
+
enable_gzip = mkOption {
+
description = lib.mdDoc ''
+
Set this option to true to enable HTTP compression, this can improve transfer speed and bandwidth utilization.
+
It is recommended that most users set it to true. By default it is set to false for compatibility reasons.
+
'';
+
default = false;
+
type = types.bool;
+
};
+
+
cert_file = mkOption {
+
description = lib.mdDoc "Cert file for ssl.";
+
default = "";
+
type = types.str;
+
};
+
+
cert_key = mkOption {
+
description = lib.mdDoc "Cert key for ssl.";
+
default = "";
+
type = types.str;
+
};
+
+
socket = mkOption {
+
description = lib.mdDoc "Path where the socket should be created when protocol=socket. Make sure that Grafana has appropriate permissions before you change this setting.";
+
default = "";
+
type = types.str;
+
};
+
};
+
database = {
+
type = mkOption {
+
description = lib.mdDoc "Database type.";
+
default = "sqlite3";
+
type = types.enum ["mysql" "sqlite3" "postgres"];
+
};
+
host = mkOption {
+
description = lib.mdDoc "Database host.";
+
default = "127.0.0.1:3306";
+
type = types.str;
+
};
+
+
name = mkOption {
+
description = lib.mdDoc "Database name.";
+
default = "grafana";
+
type = types.str;
+
};
+
+
user = mkOption {
+
description = lib.mdDoc "Database user.";
+
default = "root";
+
type = types.str;
+
};
+
+
password = mkOption {
+
description = lib.mdDoc ''
+
Database password. Please note that the contents of this option
+
will end up in a world-readable Nix store. Use the file provider
+
pointing at a reasonably secured file in the local filesystem
+
to work around that. Look at the documentation for details:
+
<https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
+
'';
+
default = "";
+
type = types.str;
+
};
+
+
path = mkOption {
+
description = lib.mdDoc "Only applicable to sqlite3 database. The file path where the database will be stored.";
+
default = "${cfg.dataDir}/data/grafana.db";
+
defaultText = literalExpression ''"''${config.${opt.dataDir}}/data/grafana.db"'';
+
type = types.path;
+
};
+
};
+
+
security = {
+
admin_user = mkOption {
+
description = lib.mdDoc "Default admin username.";
+
default = "admin";
+
type = types.str;
+
};
+
+
admin_password = mkOption {
+
description = lib.mdDoc ''
+
Default admin password. Please note that the contents of this option
+
will end up in a world-readable Nix store. Use the file provider
+
pointing at a reasonably secured file in the local filesystem
+
to work around that. Look at the documentation for details:
+
<https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
+
'';
+
default = "admin";
+
type = types.str;
+
};
+
+
secret_key = mkOption {
+
description = lib.mdDoc ''
+
Secret key used for signing. Please note that the contents of this option
+
will end up in a world-readable Nix store. Use the file provider
+
pointing at a reasonably secured file in the local filesystem
+
to work around that. Look at the documentation for details:
+
<https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
+
'';
+
default = "SW2YcwTIb9zpOOhoPsMm";
+
type = types.str;
+
};
+
};
+
+
smtp = {
+
enabled = mkOption {
+
description = lib.mdDoc "Whether to enable SMTP.";
+
default = false;
+
type = types.bool;
+
};
+
host = mkOption {
+
description = lib.mdDoc "Host to connect to.";
+
default = "localhost:25";
+
type = types.str;
+
};
+
user = mkOption {
+
description = lib.mdDoc "User used for authentication.";
+
default = "";
+
type = types.str;
+
};
+
password = mkOption {
+
description = lib.mdDoc ''
+
Password used for authentication. Please note that the contents of this option
+
will end up in a world-readable Nix store. Use the file provider
+
pointing at a reasonably secured file in the local filesystem
+
to work around that. Look at the documentation for details:
+
<https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
+
'';
+
default = "";
+
type = types.str;
+
};
+
from_address = mkOption {
+
description = lib.mdDoc "Email address used for sending.";
+
default = "admin@grafana.localhost";
+
type = types.str;
+
};
+
};
+
users = {
+
allow_sign_up = mkOption {
+
description = lib.mdDoc "Disable user signup / registration.";
+
default = false;
+
type = types.bool;
+
};
+
allow_org_create = mkOption {
+
description = lib.mdDoc "Whether user is allowed to create organizations.";
+
default = false;
+
type = types.bool;
+
};
+
auto_assign_org = mkOption {
+
description = lib.mdDoc "Whether to automatically assign new users to default org.";
+
default = true;
+
type = types.bool;
+
};
+
auto_assign_org_role = mkOption {
+
description = lib.mdDoc "Default role new users will be auto assigned.";
+
default = "Viewer";
+
type = types.enum ["Viewer" "Editor"];
+
};
+
};
+
analytics.reporting_enabled = mkOption {
+
description = lib.mdDoc "Whether to allow anonymous usage reporting to stats.grafana.net.";
+
default = true;
+
type = types.bool;
+
};
+
};
};
};
provision = {
enable = mkEnableOption (lib.mdDoc "provision");
+
datasources = mkOption {
+
description = lib.mdDoc ''
+
Deprecated option for Grafana datasource configuration. Use either
+
`services.grafana.provision.datasources.settings` or
+
`services.grafana.provision.datasources.path` instead.
+
'';
default = [];
+
apply = x: if (builtins.isList x) then map _filter x else x;
+
type = with types; either (listOf grafanaTypes.datasourceConfig) (submodule {
+
options.settings = mkOption {
+
description = lib.mdDoc ''
+
Grafana datasource configuration in Nix. Can't be used with
+
`services.grafana.provision.datasources.path` simultaneously. See
+
<https://grafana.com/docs/grafana/latest/administration/provisioning/#data-sources>
+
for supported options.
+
'';
+
default = null;
+
type = types.nullOr (types.submodule {
+
options = {
+
apiVersion = mkOption {
+
description = lib.mdDoc "Config file version.";
+
default = 1;
+
type = types.int;
+
};
+
+
datasources = mkOption {
+
description = lib.mdDoc "List of datasources to insert/update.";
+
default = [];
+
type = types.listOf grafanaTypes.datasourceConfig;
+
};
+
+
deleteDatasources = mkOption {
+
description = lib.mdDoc "List of datasources that should be deleted from the database.";
+
default = [];
+
type = types.listOf (types.submodule {
+
options.name = mkOption {
+
description = lib.mdDoc "Name of the datasource to delete.";
+
type = types.str;
+
};
+
+
options.orgId = mkOption {
+
description = lib.mdDoc "Organization ID of the datasource to delete.";
+
type = types.int;
+
};
+
});
+
};
+
};
+
});
+
example = literalExpression ''
+
{
+
apiVersion = 1;
+
+
datasources = [{
+
name = "Graphite";
+
type = "graphite";
+
}];
+
+
deleteDatasources = [{
+
name = "Graphite";
+
orgId = 1;
+
}];
+
}
+
'';
+
};
+
+
options.path = mkOption {
+
description = lib.mdDoc ''
+
Path to YAML datasource configuration. Can't be used with
+
`services.grafana.provision.datasources.settings` simultaneously.
+
'';
+
default = null;
+
type = types.nullOr types.path;
+
};
+
});
};
+
+
dashboards = mkOption {
+
description = lib.mdDoc ''
+
Deprecated option for Grafana dashboard configuration. Use either
+
`services.grafana.provision.dashboards.settings` or
+
`services.grafana.provision.dashboards.path` instead.
+
'';
default = [];
+
apply = x: if (builtins.isList x) then map _filter x else x;
+
type = with types; either (listOf grafanaTypes.dashboardConfig) (submodule {
+
options.settings = mkOption {
+
description = lib.mdDoc ''
+
Grafana dashboard configuration in Nix. Can't be used with
+
`services.grafana.provision.dashboards.path` simultaneously. See
+
<https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards>
+
for supported options.
+
'';
+
default = null;
+
type = types.nullOr (types.submodule {
+
options.apiVersion = mkOption {
+
description = lib.mdDoc "Config file version.";
+
default = 1;
+
type = types.int;
+
};
+
+
options.providers = mkOption {
+
description = lib.mdDoc "List of dashboards to insert/update.";
+
default = [];
+
type = types.listOf grafanaTypes.dashboardConfig;
+
};
+
});
+
example = literalExpression ''
+
{
+
apiVersion = 1;
+
+
providers = [{
+
name = "default";
+
options.path = "/var/lib/grafana/dashboards";
+
}];
+
}
+
'';
+
};
+
+
options.path = mkOption {
+
description = lib.mdDoc ''
+
Path to YAML dashboard configuration. Can't be used with
+
`services.grafana.provision.dashboards.settings` simultaneously.
+
'';
+
default = null;
+
type = types.nullOr types.path;
+
};
+
});
};
+
+
notifiers = mkOption {
description = lib.mdDoc "Grafana notifier configuration.";
default = [];
type = types.listOf grafanaTypes.notifierConfig;
apply = x: map _filter x;
};
+
alerting = {
+
rules = {
+
path = mkOption {
+
description = lib.mdDoc ''
+
Path to YAML rules configuration. Can't be used with
+
`services.grafana.provision.alerting.rules.settings` simultaneously.
+
'';
+
default = null;
+
type = types.nullOr types.path;
+
};
+
settings = mkOption {
+
description = lib.mdDoc ''
+
Grafana rules configuration in Nix. Can't be used with
+
`services.grafana.provision.alerting.rules.path` simultaneously. See
+
<https://grafana.com/docs/grafana/latest/administration/provisioning/#rules>
+
for supported options.
+
'';
+
default = null;
+
type = types.nullOr (types.submodule {
+
options = {
+
apiVersion = mkOption {
+
description = lib.mdDoc "Config file version.";
+
default = 1;
+
type = types.int;
+
};
+
groups = mkOption {
+
description = lib.mdDoc "List of rule groups to import or update.";
+
default = [];
+
type = types.listOf (types.submodule {
+
freeformType = provisioningSettingsFormat.type;
+
options.name = mkOption {
+
description = lib.mdDoc "Name of the rule group. Required.";
+
type = types.str;
+
};
+
options.folder = mkOption {
+
description = lib.mdDoc "Name of the folder the rule group will be stored in. Required.";
+
type = types.str;
+
};
+
options.interval = mkOption {
+
description = lib.mdDoc "Interval that the rule group should be evaluated at. Required.";
+
type = types.str;
+
};
+
});
+
};
+
deleteRules = mkOption {
+
description = lib.mdDoc "List of alert rule UIDs that should be deleted.";
+
default = [];
+
type = types.listOf (types.submodule {
+
options.orgId = mkOption {
+
description = lib.mdDoc "Organization ID, default = 1";
+
default = 1;
+
type = types.int;
+
};
+
options.uid = mkOption {
+
description = lib.mdDoc "Unique identifier for the rule. Required.";
+
type = types.str;
+
};
+
});
+
};
+
};
+
});
+
example = literalExpression ''
+
{
+
apiVersion = 1;
+
groups = [{
+
orgId = 1;
+
name = "my_rule_group";
+
folder = "my_first_folder";
+
interval = "60s";
+
rules = [{
+
uid = "my_id_1";
+
title = "my_first_rule";
+
condition = "A";
+
data = [{
+
refId = "A";
+
datasourceUid = "-100";
+
model = {
+
conditions = [{
+
evaluator = {
+
params = [ 3 ];
+
type = "git";
+
};
+
operator.type = "and";
+
query.params = [ "A" ];
+
reducer.type = "last";
+
type = "query";
+
}];
+
datasource = {
+
type = "__expr__";
+
uid = "-100";
+
};
+
expression = "1==0";
+
intervalMs = 1000;
+
maxDataPoints = 43200;
+
refId = "A";
+
type = "math";
+
};
+
}];
+
dashboardUid = "my_dashboard";
+
panelId = 123;
+
noDataState = "Alerting";
+
for = "60s";
+
annotations.some_key = "some_value";
+
labels.team = "sre_team1";
+
}];
+
}];
+
deleteRules = [{
+
orgId = 1;
+
uid = "my_id_1";
+
}];
+
}
+
'';
+
};
+
};
+
contactPoints = {
+
path = mkOption {
+
description = lib.mdDoc ''
+
Path to YAML contact points configuration. Can't be used with
+
`services.grafana.provision.alerting.contactPoints.settings` simultaneously.
+
'';
+
default = null;
+
type = types.nullOr types.path;
+
};
+
settings = mkOption {
+
description = lib.mdDoc ''
+
Grafana contact points configuration in Nix. Can't be used with
+
`services.grafana.provision.alerting.contactPoints.path` simultaneously. See
+
<https://grafana.com/docs/grafana/latest/administration/provisioning/#contact-points>
+
for supported options.
'';
+
default = null;
+
type = types.nullOr (types.submodule {
+
options = {
+
apiVersion = mkOption {
+
description = lib.mdDoc "Config file version.";
+
default = 1;
+
type = types.int;
+
};
+
+
contactPoints = mkOption {
+
description = lib.mdDoc "List of contact points to import or update.";
+
default = [];
+
type = types.listOf (types.submodule {
+
freeformType = provisioningSettingsFormat.type;
+
+
options.name = mkOption {
+
description = lib.mdDoc "Name of the contact point. Required.";
+
type = types.str;
+
};
+
});
+
};
+
+
deleteContactPoints = mkOption {
+
description = lib.mdDoc "List of receivers that should be deleted.";
+
default = [];
+
type = types.listOf (types.submodule {
+
options.orgId = mkOption {
+
description = lib.mdDoc "Organization ID, default = 1.";
+
default = 1;
+
type = types.int;
+
};
+
+
options.uid = mkOption {
+
description = lib.mdDoc "Unique identifier for the receiver. Required.";
+
type = types.str;
+
};
+
});
+
};
+
};
+
});
+
example = literalExpression ''
+
{
+
apiVersion = 1;
+
+
contactPoints = [{
+
orgId = 1;
+
name = "cp_1";
+
receivers = [{
+
uid = "first_uid";
+
type = "prometheus-alertmanager";
+
settings.url = "http://test:9000";
+
}];
+
}];
+
+
deleteContactPoints = [{
+
orgId = 1;
+
uid = "first_uid";
+
}];
+
}
'';
+
};
};
+
+
policies = {
+
path = mkOption {
+
description = lib.mdDoc ''
+
Path to YAML notification policies configuration. Can't be used with
+
`services.grafana.provision.alerting.policies.settings` simultaneously.
+
'';
+
default = null;
+
type = types.nullOr types.path;
+
};
+
+
settings = mkOption {
+
description = lib.mdDoc ''
+
Grafana notification policies configuration in Nix. Can't be used with
+
`services.grafana.provision.alerting.policies.path` simultaneously. See
+
<https://grafana.com/docs/grafana/latest/administration/provisioning/#notification-policies>
+
for supported options.
+
'';
+
default = null;
+
type = types.nullOr (types.submodule {
+
options = {
+
apiVersion = mkOption {
+
description = lib.mdDoc "Config file version.";
+
default = 1;
+
type = types.int;
+
};
+
+
policies = mkOption {
+
description = lib.mdDoc "List of contact points to import or update.";
+
default = [];
+
type = types.listOf (types.submodule {
+
freeformType = provisioningSettingsFormat.type;
+
});
+
};
+
+
resetPolicies = mkOption {
+
description = lib.mdDoc "List of orgIds that should be reset to the default policy.";
+
default = [];
+
type = types.listOf types.int;
+
};
+
};
+
});
+
example = literalExpression ''
+
{
+
apiVersion = 1;
+
+
policies = [{
+
orgId = 1;
+
receiver = "grafana-default-email";
+
group_by = [ "..." ];
+
matchers = [
+
"alertname = Watchdog"
+
"severity =~ \"warning|critical\""
+
];
+
mute_time_intervals = [
+
"abc"
+
];
+
group_wait = "30s";
+
group_interval = "5m";
+
repeat_interval = "4h";
+
}];
+
+
resetPolicies = [
+
1
+
];
+
}
+
'';
+
};
};
+
+
templates = {
+
path = mkOption {
+
description = lib.mdDoc ''
+
Path to YAML templates configuration. Can't be used with
+
`services.grafana.provision.alerting.templates.settings` simultaneously.
+
'';
+
default = null;
+
type = types.nullOr types.path;
+
};
+
+
settings = mkOption {
+
description = lib.mdDoc ''
+
Grafana templates configuration in Nix. Can't be used with
+
`services.grafana.provision.alerting.templates.path` simultaneously. See
+
<https://grafana.com/docs/grafana/latest/administration/provisioning/#templates>
+
for supported options.
+
'';
+
default = null;
+
type = types.nullOr (types.submodule {
+
options = {
+
apiVersion = mkOption {
+
description = lib.mdDoc "Config file version.";
+
default = 1;
+
type = types.int;
+
};
+
+
templates = mkOption {
+
description = lib.mdDoc "List of templates to import or update.";
+
default = [];
+
type = types.listOf (types.submodule {
+
freeformType = provisioningSettingsFormat.type;
+
+
options.name = mkOption {
+
description = lib.mdDoc "Name of the template, must be unique. Required.";
+
type = types.str;
+
};
+
+
options.template = mkOption {
+
description = lib.mdDoc "Alerting with a custom text template";
+
type = types.str;
+
};
+
});
+
};
+
+
deleteTemplates = mkOption {
+
description = lib.mdDoc "List of alert rule UIDs that should be deleted.";
+
default = [];
+
type = types.listOf (types.submodule {
+
options.orgId = mkOption {
+
description = lib.mdDoc "Organization ID, default = 1.";
+
default = 1;
+
type = types.int;
+
};
+
+
options.name = mkOption {
+
description = lib.mdDoc "Name of the template, must be unique. Required.";
+
type = types.str;
+
};
+
});
+
};
+
};
+
});
+
example = literalExpression ''
+
{
+
apiVersion = 1;
+
+
templates = [{
+
orgId = 1;
+
name = "my_first_template";
+
template = "Alerting with a custom text template";
+
}];
+
+
deleteTemplates = [{
+
orgId = 1;
+
name = "my_first_template";
+
}];
+
}
+
'';
+
};
};
+
+
muteTimings = {
+
path = mkOption {
+
description = lib.mdDoc ''
+
Path to YAML mute timings configuration. Can't be used with
+
`services.grafana.provision.alerting.muteTimings.settings` simultaneously.
+
'';
+
default = null;
+
type = types.nullOr types.path;
+
};
+
+
settings = mkOption {
+
description = lib.mdDoc ''
+
Grafana mute timings configuration in Nix. Can't be used with
+
`services.grafana.provision.alerting.muteTimings.path` simultaneously. See
+
<https://grafana.com/docs/grafana/latest/administration/provisioning/#mute-timings>
+
for supported options.
+
'';
+
default = null;
+
type = types.nullOr (types.submodule {
+
options = {
+
apiVersion = mkOption {
+
description = lib.mdDoc "Config file version.";
+
default = 1;
+
type = types.int;
+
};
+
+
muteTimes = mkOption {
+
description = lib.mdDoc "List of mute time intervals to import or update.";
+
default = [];
+
type = types.listOf (types.submodule {
+
freeformType = provisioningSettingsFormat.type;
+
+
options.name = mkOption {
+
description = lib.mdDoc "Name of the mute time interval, must be unique. Required.";
+
type = types.str;
+
};
+
});
+
};
+
+
deleteMuteTimes = mkOption {
+
description = lib.mdDoc "List of mute time intervals that should be deleted.";
+
default = [];
+
type = types.listOf (types.submodule {
+
options.orgId = mkOption {
+
description = lib.mdDoc "Organization ID, default = 1.";
+
default = 1;
+
type = types.int;
+
};
+
+
options.name = mkOption {
+
description = lib.mdDoc "Name of the mute time interval, must be unique. Required.";
+
type = types.str;
+
};
+
});
+
};
+
};
+
});
+
example = literalExpression ''
+
{
+
apiVersion = 1;
+
+
muteTimes = [{
+
orgId = 1;
+
name = "mti_1";
+
time_intervals = [{
+
times = [{
+
start_time = "06:00";
+
end_time = "23:59";
+
}];
+
weekdays = [
+
"monday:wednesday"
+
"saturday"
+
"sunday"
+
];
+
months = [
+
"1:3"
+
"may:august"
+
"december"
+
];
+
years = [
+
"2020:2022"
+
"2030"
+
];
+
days_of_month = [
+
"1:5"
+
"-3:-1"
+
];
+
}];
+
}];
+
deleteMuteTimes = [{
+
orgId = 1;
+
name = "mti_1";
+
}];
+
}
+
'';
+
};
+
};
};
};
};
config = mkIf cfg.enable {
warnings = flatten [
(optional (
+
cfg.settings.database.password != "" ||
+
cfg.settings.security.admin_password != "admin"
+
) "Grafana passwords will be stored as plaintext in the Nix store! Use file provider instead.")
(optional (
+
let
+
checkOpts = opt: any (x: x.password != null || x.basicAuthPassword != null || x.secureJsonData != null) opt;
+
datasourcesUsed = if (cfg.provision.datasources.settings == null) then [] else cfg.provision.datasources.settings.datasources;
+
in if (builtins.isList cfg.provision.datasources) then checkOpts cfg.provision.datasources else checkOpts datasourcesUsed
+
) "Datasource passwords will be stored as plaintext in the Nix store! Use file provider instead.")
(optional (
any (x: x.secure_settings != null) cfg.provision.notifiers
+
) "Notifier secure settings will be stored as plaintext in the Nix store! Use file provider instead.")
+
(optional (
+
builtins.isList cfg.provision.datasources
+
) ''
+
Provisioning Grafana datasources with options has been deprecated.
+
Use `services.grafana.provision.datasources.settings` or
+
`services.grafana.provision.datasources.path` instead.
+
'')
+
(optional (
+
builtins.isList cfg.provision.dashboards
+
) ''
+
Provisioning Grafana dashboards with options has been deprecated.
+
Use `services.grafana.provision.dashboards.settings` or
+
`services.grafana.provision.dashboards.path` instead.
+
'')
+
(optional (
+
cfg.provision.notifiers != []
+
) ''
+
Notifiers are deprecated upstream and will be removed in Grafana 10.
+
Use `services.grafana.provision.alerting.contactPoints` instead.
+
'')
];
environment.systemPackages = [ cfg.package ];
assertions = [
{
+
assertion = if (builtins.isList cfg.provision.datasources) then true else cfg.provision.datasources.settings == null || cfg.provision.datasources.path == null;
+
message = "Cannot set both datasources settings and datasources path";
}
{
+
assertion = let
+
prometheusIsNotDirect = opt: all
+
({ type, access, ... }: type == "prometheus" -> access != "direct")
+
opt;
+
in
+
if (builtins.isList cfg.provision.datasources) then prometheusIsNotDirect cfg.provision.datasources
+
else cfg.provision.datasources.settings == null || prometheusIsNotDirect cfg.provision.datasources.settings.datasources;
+
message = "For datasources of type `prometheus`, the `direct` access mode is not supported anymore (since Grafana 9.2.0)";
}
{
+
assertion = if (builtins.isList cfg.provision.dashboards) then true else cfg.provision.dashboards.settings == null || cfg.provision.dashboards.path == null;
+
message = "Cannot set both dashboards settings and dashboards path";
}
{
+
assertion = cfg.provision.alerting.rules.settings == null || cfg.provision.alerting.rules.path == null;
+
message = "Cannot set both rules settings and rules path";
}
{
+
assertion = cfg.provision.alerting.contactPoints.settings == null || cfg.provision.alerting.contactPoints.path == null;
+
message = "Cannot set both contact points settings and contact points path";
+
}
+
{
+
assertion = cfg.provision.alerting.policies.settings == null || cfg.provision.alerting.policies.path == null;
+
message = "Cannot set both policies settings and policies path";
+
}
+
{
+
assertion = cfg.provision.alerting.templates.settings == null || cfg.provision.alerting.templates.path == null;
+
message = "Cannot set both templates settings and templates path";
+
}
+
{
+
assertion = cfg.provision.alerting.muteTimings.settings == null || cfg.provision.alerting.muteTimings.path == null;
+
message = "Cannot set both mute timings settings and mute timings path";
}
];
···
description = "Grafana Service Daemon";
wantedBy = ["multi-user.target"];
after = ["networking.target"] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
script = ''
set -o errexit -o pipefail -o nounset -o errtrace
shopt -s inherit_errexit
+
exec ${cfg.package}/bin/grafana-server -homepath ${cfg.dataDir} -config ${configFile}
'';
serviceConfig = {
WorkingDirectory = cfg.dataDir;
+1 -1
nixos/modules/services/network-filesystems/litestream/litestream.xml
···
<para>
Litestream service is managed by a dedicated user named <literal>litestream</literal>
which needs permission to the database file. Here's an example config which gives
-
required permissions to access <link linkend="opt-services.grafana.database.path">
grafana database</link>:
<programlisting>
{ pkgs, ... }:
···
<para>
Litestream service is managed by a dedicated user named <literal>litestream</literal>
which needs permission to the database file. Here's an example config which gives
+
required permissions to access <link linkend="opt-services.grafana.settings.database.path">
grafana database</link>:
<programlisting>
{ pkgs, ... }:
+1 -1
nixos/tests/all-tests.nix
···
gollum = handleTest ./gollum.nix {};
google-oslogin = handleTest ./google-oslogin {};
gotify-server = handleTest ./gotify-server.nix {};
-
grafana = handleTest ./grafana.nix {};
grafana-agent = handleTest ./grafana-agent.nix {};
graphite = handleTest ./graphite.nix {};
graylog = handleTest ./graylog.nix {};
···
gollum = handleTest ./gollum.nix {};
google-oslogin = handleTest ./google-oslogin {};
gotify-server = handleTest ./gotify-server.nix {};
+
grafana = handleTest ./grafana {};
grafana-agent = handleTest ./grafana-agent.nix {};
graphite = handleTest ./graphite.nix {};
graylog = handleTest ./graylog.nix {};
+5 -8
nixos/tests/grafana.nix nixos/tests/grafana/basic.nix
···
-
import ./make-test-python.nix ({ lib, pkgs, ... }:
let
inherit (lib) mkMerge nameValuePair maintainers;
···
};
extraNodeConfs = {
declarativePlugins = {
services.grafana.declarativePlugins = [ pkgs.grafanaPlugins.grafana-clock-panel ];
};
···
};
};
-
nodes = builtins.listToAttrs (map (dbName:
-
nameValuePair dbName (mkMerge [
-
baseGrafanaConf
-
(extraNodeConfs.${dbName} or {})
-
])) [ "sqlite" "declarativePlugins" "postgresql" "mysql" ]);
-
in {
-
name = "grafana";
meta = with maintainers; {
maintainers = [ willibutz ];
···
+
import ../make-test-python.nix ({ lib, pkgs, ... }:
let
inherit (lib) mkMerge nameValuePair maintainers;
···
};
extraNodeConfs = {
+
sqlite = {};
+
declarativePlugins = {
services.grafana.declarativePlugins = [ pkgs.grafanaPlugins.grafana-clock-panel ];
};
···
};
};
+
nodes = builtins.mapAttrs (_: val: mkMerge [ val baseGrafanaConf ]) extraNodeConfs;
in {
+
name = "grafana-basic";
meta = with maintainers; {
maintainers = [ willibutz ];
+9
nixos/tests/grafana/default.nix
···
···
+
{ system ? builtins.currentSystem
+
, config ? { }
+
, pkgs ? import ../../.. { inherit system config; }
+
}:
+
+
{
+
basic = import ./basic.nix { inherit system pkgs; };
+
provision = import ./provision { inherit system pkgs; };
+
}
+9
nixos/tests/grafana/provision/contact-points.yaml
···
···
+
apiVersion: 1
+
+
contactPoints:
+
- name: "Test Contact Point"
+
receivers:
+
- uid: "test_contact_point"
+
type: prometheus-alertmanager
+
settings:
+
url: http://localhost:9000
+6
nixos/tests/grafana/provision/dashboards.yaml
···
···
+
apiVersion: 1
+
+
providers:
+
- name: 'default'
+
options:
+
path: /var/lib/grafana/dashboards
+7
nixos/tests/grafana/provision/datasources.yaml
···
···
+
apiVersion: 1
+
+
datasources:
+
- name: 'Test Datasource'
+
type: 'testdata'
+
access: 'proxy'
+
uid: 'test_datasource'
+223
nixos/tests/grafana/provision/default.nix
···
···
+
import ../../make-test-python.nix ({ lib, pkgs, ... }:
+
+
let
+
inherit (lib) mkMerge nameValuePair maintainers;
+
+
baseGrafanaConf = {
+
services.grafana = {
+
enable = true;
+
addr = "localhost";
+
analytics.reporting.enable = false;
+
domain = "localhost";
+
security = {
+
adminUser = "testadmin";
+
adminPassword = "snakeoilpwd";
+
};
+
provision.enable = true;
+
};
+
+
systemd.tmpfiles.rules = [
+
"L /var/lib/grafana/dashboards/test.json 0700 grafana grafana - ${pkgs.writeText "test.json" (builtins.readFile ./test_dashboard.json)}"
+
];
+
};
+
+
extraNodeConfs = {
+
provisionOld = {
+
services.grafana.provision = {
+
datasources = [{
+
name = "Test Datasource";
+
type = "testdata";
+
access = "proxy";
+
uid = "test_datasource";
+
}];
+
+
dashboards = [{ options.path = "/var/lib/grafana/dashboards"; }];
+
+
notifiers = [{
+
uid = "test_notifiers";
+
name = "Test Notifiers";
+
type = "email";
+
settings = {
+
singleEmail = true;
+
addresses = "test@test.com";
+
};
+
}];
+
};
+
};
+
+
provisionNix = {
+
services.grafana.provision = {
+
datasources.settings = {
+
apiVersion = 1;
+
datasources = [{
+
name = "Test Datasource";
+
type = "testdata";
+
access = "proxy";
+
uid = "test_datasource";
+
}];
+
};
+
+
dashboards.settings = {
+
apiVersion = 1;
+
providers = [{
+
name = "default";
+
options.path = "/var/lib/grafana/dashboards";
+
}];
+
};
+
+
alerting = {
+
rules.settings = {
+
groups = [{
+
name = "test_rule_group";
+
folder = "test_folder";
+
interval = "60s";
+
rules = [{
+
uid = "test_rule";
+
title = "Test Rule";
+
condition = "A";
+
data = [{
+
refId = "A";
+
datasourceUid = "-100";
+
model = {
+
conditions = [{
+
evaluator = {
+
params = [ 3 ];
+
type = "git";
+
};
+
operator.type = "and";
+
query.params = [ "A" ];
+
reducer.type = "last";
+
type = "query";
+
}];
+
datasource = {
+
type = "__expr__";
+
uid = "-100";
+
};
+
expression = "1==0";
+
intervalMs = 1000;
+
maxDataPoints = 43200;
+
refId = "A";
+
type = "math";
+
};
+
}];
+
for = "60s";
+
}];
+
}];
+
};
+
+
contactPoints.settings = {
+
contactPoints = [{
+
name = "Test Contact Point";
+
receivers = [{
+
uid = "test_contact_point";
+
type = "prometheus-alertmanager";
+
settings.url = "http://localhost:9000";
+
}];
+
}];
+
};
+
+
policies.settings = {
+
policies = [{
+
receiver = "Test Contact Point";
+
}];
+
};
+
+
templates.settings = {
+
templates = [{
+
name = "Test Template";
+
template = "Test message";
+
}];
+
};
+
+
muteTimings.settings = {
+
muteTimes = [{
+
name = "Test Mute Timing";
+
}];
+
};
+
};
+
};
+
};
+
+
provisionYaml = {
+
services.grafana.provision = {
+
datasources.path = ./datasources.yaml;
+
dashboards.path = ./dashboards.yaml;
+
alerting = {
+
rules.path = ./rules.yaml;
+
contactPoints.path = ./contact-points.yaml;
+
policies.path = ./policies.yaml;
+
templates.path = ./templates.yaml;
+
muteTimings.path = ./mute-timings.yaml;
+
};
+
};
+
};
+
};
+
+
nodes = builtins.mapAttrs (_: val: mkMerge [ val baseGrafanaConf ]) extraNodeConfs;
+
in {
+
name = "grafana-provision";
+
+
meta = with maintainers; {
+
maintainers = [ kfears willibutz ];
+
};
+
+
inherit nodes;
+
+
testScript = ''
+
start_all()
+
+
nodeOld = ("Nix (old format)", provisionOld)
+
nodeNix = ("Nix (new format)", provisionNix)
+
nodeYaml = ("Nix (YAML)", provisionYaml)
+
+
for nodeInfo in [nodeOld, nodeNix, nodeYaml]:
+
with subtest(f"Should start provision node: {nodeInfo[0]}"):
+
nodeInfo[1].wait_for_unit("grafana.service")
+
nodeInfo[1].wait_for_open_port(3000)
+
+
with subtest(f"Successful datasource provision with {nodeInfo[0]}"):
+
nodeInfo[1].succeed(
+
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/datasources/uid/test_datasource | grep Test\ Datasource"
+
)
+
+
with subtest(f"Successful dashboard provision with {nodeInfo[0]}"):
+
nodeInfo[1].succeed(
+
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/dashboards/uid/test_dashboard | grep Test\ Dashboard"
+
)
+
+
+
+
with subtest(f"Successful notifiers provision with {nodeOld[0]}"):
+
nodeOld[1].succeed(
+
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/alert-notifications/uid/test_notifiers | grep Test\ Notifiers"
+
)
+
+
+
+
for nodeInfo in [nodeNix, nodeYaml]:
+
with subtest(f"Successful rule provision with {nodeInfo[0]}"):
+
nodeInfo[1].succeed(
+
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/alert-rules/test_rule | grep Test\ Rule"
+
)
+
+
with subtest(f"Successful contact point provision with {nodeInfo[0]}"):
+
nodeInfo[1].succeed(
+
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/contact-points | grep Test\ Contact\ Point"
+
)
+
+
with subtest(f"Successful policy provision with {nodeInfo[0]}"):
+
nodeInfo[1].succeed(
+
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/policies | grep Test\ Contact\ Point"
+
)
+
+
with subtest(f"Successful template provision with {nodeInfo[0]}"):
+
nodeInfo[1].succeed(
+
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/templates | grep Test\ Template"
+
)
+
+
with subtest("Successful mute timings provision with {nodeInfo[0]}"):
+
nodeInfo[1].succeed(
+
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/mute-timings | grep Test\ Mute\ Timing"
+
)
+
'';
+
})
+4
nixos/tests/grafana/provision/mute-timings.yaml
···
···
+
apiVersion: 1
+
+
muteTimes:
+
- name: "Test Mute Timing"
+4
nixos/tests/grafana/provision/policies.yaml
···
···
+
apiVersion: 1
+
+
policies:
+
- receiver: "Test Contact Point"
+36
nixos/tests/grafana/provision/rules.yaml
···
···
+
apiVersion: 1
+
+
groups:
+
- name: "test_rule_group"
+
folder: "test_group"
+
interval: 60s
+
rules:
+
- uid: "test_rule"
+
title: "Test Rule"
+
condition: A
+
data:
+
- refId: A
+
datasourceUid: '-100'
+
model:
+
conditions:
+
- evaluator:
+
params:
+
- 3
+
type: gt
+
operator:
+
type: and
+
query:
+
params:
+
- A
+
reducer:
+
type: last
+
type: query
+
datasource:
+
type: __expr__
+
uid: '-100'
+
expression: 1==0
+
intervalMs: 1000
+
maxDataPoints: 43200
+
refId: A
+
type: math
+
for: 60s
+5
nixos/tests/grafana/provision/templates.yaml
···
···
+
apiVersion: 1
+
+
templates:
+
- name: "Test Template"
+
template: "Test message"
+47
nixos/tests/grafana/provision/test_dashboard.json
···
···
+
{
+
"annotations": {
+
"list": [
+
{
+
"builtIn": 1,
+
"datasource": {
+
"type": "grafana",
+
"uid": "-- Grafana --"
+
},
+
"enable": true,
+
"hide": true,
+
"iconColor": "rgba(0, 211, 255, 1)",
+
"name": "Annotations & Alerts",
+
"target": {
+
"limit": 100,
+
"matchAny": false,
+
"tags": [],
+
"type": "dashboard"
+
},
+
"type": "dashboard"
+
}
+
]
+
},
+
"editable": true,
+
"fiscalYearStartMonth": 0,
+
"graphTooltip": 0,
+
"id": 28,
+
"links": [],
+
"liveNow": false,
+
"panels": [],
+
"schemaVersion": 37,
+
"style": "dark",
+
"tags": [],
+
"templating": {
+
"list": []
+
},
+
"time": {
+
"from": "now-6h",
+
"to": "now"
+
},
+
"timepicker": {},
+
"timezone": "",
+
"title": "Test Dashboard",
+
"uid": "test_dashboard",
+
"version": 1,
+
"weekStart": ""
+
}