diff --git a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml index a84549bda1fb..933522591af4 100644 --- a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml +++ b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml @@ -873,6 +873,34 @@ has been hardened. + + + The services.grafana options were converted + to a + RFC + 0042 configuration. + + + + + The services.grafana.provision.datasources + and services.grafana.provision.dashboards + options were converted to a + RFC + 0042 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 diff --git a/nixos/doc/manual/release-notes/rl-2211.section.md b/nixos/doc/manual/release-notes/rl-2211.section.md index 53afe1ca4d68..b91014bdb21a 100644 --- a/nixos/doc/manual/release-notes/rl-2211.section.md +++ b/nixos/doc/manual/release-notes/rl-2211.section.md @@ -282,6 +282,12 @@ Available as [services.patroni](options.html#opt-services.patroni.enable). - 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. diff --git a/nixos/modules/services/monitoring/grafana-image-renderer.nix b/nixos/modules/services/monitoring/grafana-image-renderer.nix index 549da138fe23..60f6e84c63c7 100644 --- a/nixos/modules/services/monitoring/grafana-image-renderer.nix +++ b/nixos/modules/services/monitoring/grafana-image-renderer.nix @@ -106,9 +106,9 @@ in { } ]; - 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.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; diff --git a/nixos/modules/services/monitoring/grafana.nix b/nixos/modules/services/monitoring/grafana.nix index 15ca11a2e164..964602547e7d 100644 --- a/nixos/modules/services/monitoring/grafana.nix +++ b/nixos/modules/services/monitoring/grafana.nix @@ -5,86 +5,29 @@ with lib; 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.database.type == "mysql"; - usePostgresql = cfg.database.type == "postgres"; + useMysql = cfg.settings.database.type == "mysql"; + usePostgresql = cfg.settings.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; + settingsFormatIni = pkgs.formats.ini {}; + configFile = settingsFormatIni.generate "config.ini" cfg.settings; datasourceConfiguration = { apiVersion = 1; datasources = cfg.provision.datasources; }; - datasourceFile = pkgs.writeText "datasource.yaml" (builtins.toJSON datasourceConfiguration); + 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; }; - dashboardFile = pkgs.writeText "dashboard.yaml" (builtins.toJSON dashboardConfiguration); + 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; @@ -93,11 +36,25 @@ let 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} + 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: @@ -105,6 +62,8 @@ let # http://docs.grafana.org/administration/provisioning/#datasources grafanaTypes.datasourceConfig = types.submodule { + freeformType = provisioningSettingsFormat.type; + options = { name = mkOption { type = types.str; @@ -119,11 +78,6 @@ let 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; @@ -131,114 +85,68 @@ let }; url = mkOption { type = types.str; + default = "localhost"; 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."; }; + 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: + + ''; + }; + 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: + + ''; + }; + 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: + + ''; + }; }; }; # 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 "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."; + description = lib.mdDoc "A unique provider name."; }; 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."; - }; + options.path = mkOption { + type = types.path; + description = lib.mdDoc "Path grafana will watch for dashboards. Required when using the 'file' type."; }; }; }; @@ -296,76 +204,85 @@ let secure_settings = mkOption { type = types.nullOr types.attrs; default = null; - description = lib.mdDoc "Secure settings for the notifier type."; + 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: + + ''; }; }; }; 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"); - 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); default = null; @@ -377,396 +294,958 @@ in { 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; }; - database = { - type = mkOption { - description = lib.mdDoc "Database type."; - default = "sqlite3"; - type = types.enum ["mysql" "sqlite3" "postgres"]; - }; + settings = mkOption { + description = lib.mdDoc '' + Grafana settings. See + for available options. INI format is used. + ''; + type = types.submodule { + freeformType = settingsFormatIni.type; - host = mkOption { - description = lib.mdDoc "Database host."; - default = "127.0.0.1:3306"; - type = types.str; - }; + 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; + }; - name = mkOption { - description = lib.mdDoc "Database name."; - default = "grafana"; - type = types.str; - }; + 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; + }; + }; - user = mkOption { - description = lib.mdDoc "Database user."; - default = "root"; - type = types.str; - }; + server = { + protocol = mkOption { + description = lib.mdDoc "Which protocol to listen."; + default = "http"; + type = types.enum ["http" "https" "socket"]; + }; - password = mkOption { - description = lib.mdDoc '' - Database password. - This option is mutual exclusive with the passwordFile option. - ''; - default = ""; - type = types.str; - }; + http_addr = mkOption { + description = lib.mdDoc "Listening address."; + 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; - }; + http_port = mkOption { + description = lib.mdDoc "Listening port."; + default = 3000; + type = types.port; + }; - path = mkOption { - description = lib.mdDoc "Database path."; - default = "${cfg.dataDir}/data/grafana.db"; - defaultText = literalExpression ''"''${config.${opt.dataDir}}/data/grafana.db"''; - type = types.path; - }; + domain = mkOption { + description = lib.mdDoc "The public facing domain name used to access grafana from a browser."; + default = "localhost"; + type = types.str; + }; - 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" ]); + 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: + + ''; + 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: + + ''; + 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: + + ''; + 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: + + ''; + 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 "Grafana datasources configuration."; + description = lib.mdDoc '' + Deprecated option for Grafana datasource configuration. Use either + `services.grafana.provision.datasources.settings` or + `services.grafana.provision.datasources.path` instead. + ''; default = []; - type = types.listOf grafanaTypes.datasourceConfig; - apply = x: map _filter x; + 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 + + 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 "Grafana dashboard configuration."; + description = lib.mdDoc '' + Deprecated option for Grafana dashboard configuration. Use either + `services.grafana.provision.dashboards.settings` or + `services.grafana.provision.dashboards.path` instead. + ''; default = []; - type = types.listOf grafanaTypes.dashboardConfig; - apply = x: map _filter x; + 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 + + 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; }; - }; - 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. + 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 = "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 = 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 + + for supported options. ''; - 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; - }; - }; - }; + default = null; + type = types.nullOr (types.submodule { + options = { + apiVersion = mkOption { + description = lib.mdDoc "Config file version."; + default = 1; + type = types.int; + }; - analytics.reporting = { - enable = mkOption { - description = lib.mdDoc "Whether to allow anonymous usage reporting to stats.grafana.net."; - default = true; - type = types.bool; - }; - }; + groups = mkOption { + description = lib.mdDoc "List of rule groups to import or update."; + default = []; + type = types.listOf (types.submodule { + freeformType = provisioningSettingsFormat.type; - 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); + 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 + + 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 + + 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 + + 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 + + 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.database.password != opt.database.password.default || - cfg.security.adminPassword != opt.security.adminPassword.default - ) "Grafana passwords will be stored as plaintext in the Nix store!") + 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 ( - 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!") + 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!") + ) "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 = cfg.database.password != opt.database.password.default -> cfg.database.passwordFile == null; - message = "Cannot set both password and passwordFile"; + 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 = 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 + assertion = let + prometheusIsNotDirect = opt: all ({ type, access, ... }: type == "prometheus" -> access != "direct") - cfg.provision.datasources; + 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"; + } ]; systemd.services.grafana = { 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} + exec ${cfg.package}/bin/grafana-server -homepath ${cfg.dataDir} -config ${configFile} ''; serviceConfig = { WorkingDirectory = cfg.dataDir; diff --git a/nixos/modules/services/network-filesystems/litestream/litestream.xml b/nixos/modules/services/network-filesystems/litestream/litestream.xml index 598f9be8cf63..8f5597bb6891 100644 --- a/nixos/modules/services/network-filesystems/litestream/litestream.xml +++ b/nixos/modules/services/network-filesystems/litestream/litestream.xml @@ -15,7 +15,7 @@ Litestream service is managed by a dedicated user named litestream which needs permission to the database file. Here's an example config which gives - required permissions to access + required permissions to access grafana database: { pkgs, ... }: diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 0662c3ab08a7..78f90761d352 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -231,7 +231,7 @@ in { gollum = handleTest ./gollum.nix {}; google-oslogin = handleTest ./google-oslogin {}; gotify-server = handleTest ./gotify-server.nix {}; - grafana = handleTest ./grafana.nix {}; + grafana = handleTest ./grafana {}; grafana-agent = handleTest ./grafana-agent.nix {}; graphite = handleTest ./graphite.nix {}; graylog = handleTest ./graylog.nix {}; diff --git a/nixos/tests/grafana.nix b/nixos/tests/grafana/basic.nix similarity index 91% rename from nixos/tests/grafana.nix rename to nixos/tests/grafana/basic.nix index 5364f0ca8b0b..f91d649540e6 100644 --- a/nixos/tests/grafana.nix +++ b/nixos/tests/grafana/basic.nix @@ -1,4 +1,4 @@ -import ./make-test-python.nix ({ lib, pkgs, ... }: +import ../make-test-python.nix ({ lib, pkgs, ... }: let inherit (lib) mkMerge nameValuePair maintainers; @@ -17,6 +17,8 @@ let }; extraNodeConfs = { + sqlite = {}; + declarativePlugins = { services.grafana.declarativePlugins = [ pkgs.grafanaPlugins.grafana-clock-panel ]; }; @@ -52,14 +54,9 @@ let }; }; - nodes = builtins.listToAttrs (map (dbName: - nameValuePair dbName (mkMerge [ - baseGrafanaConf - (extraNodeConfs.${dbName} or {}) - ])) [ "sqlite" "declarativePlugins" "postgresql" "mysql" ]); - + nodes = builtins.mapAttrs (_: val: mkMerge [ val baseGrafanaConf ]) extraNodeConfs; in { - name = "grafana"; + name = "grafana-basic"; meta = with maintainers; { maintainers = [ willibutz ]; diff --git a/nixos/tests/grafana/default.nix b/nixos/tests/grafana/default.nix new file mode 100644 index 000000000000..9c2622571800 --- /dev/null +++ b/nixos/tests/grafana/default.nix @@ -0,0 +1,9 @@ +{ system ? builtins.currentSystem +, config ? { } +, pkgs ? import ../../.. { inherit system config; } +}: + +{ + basic = import ./basic.nix { inherit system pkgs; }; + provision = import ./provision { inherit system pkgs; }; +} diff --git a/nixos/tests/grafana/provision/contact-points.yaml b/nixos/tests/grafana/provision/contact-points.yaml new file mode 100644 index 000000000000..2a5f14e75e2d --- /dev/null +++ b/nixos/tests/grafana/provision/contact-points.yaml @@ -0,0 +1,9 @@ +apiVersion: 1 + +contactPoints: + - name: "Test Contact Point" + receivers: + - uid: "test_contact_point" + type: prometheus-alertmanager + settings: + url: http://localhost:9000 diff --git a/nixos/tests/grafana/provision/dashboards.yaml b/nixos/tests/grafana/provision/dashboards.yaml new file mode 100644 index 000000000000..dc83fe6b892d --- /dev/null +++ b/nixos/tests/grafana/provision/dashboards.yaml @@ -0,0 +1,6 @@ +apiVersion: 1 + +providers: + - name: 'default' + options: + path: /var/lib/grafana/dashboards diff --git a/nixos/tests/grafana/provision/datasources.yaml b/nixos/tests/grafana/provision/datasources.yaml new file mode 100644 index 000000000000..ccf9481db7f3 --- /dev/null +++ b/nixos/tests/grafana/provision/datasources.yaml @@ -0,0 +1,7 @@ +apiVersion: 1 + +datasources: + - name: 'Test Datasource' + type: 'testdata' + access: 'proxy' + uid: 'test_datasource' diff --git a/nixos/tests/grafana/provision/default.nix b/nixos/tests/grafana/provision/default.nix new file mode 100644 index 000000000000..331de35ed7fa --- /dev/null +++ b/nixos/tests/grafana/provision/default.nix @@ -0,0 +1,223 @@ +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" + ) + ''; +}) diff --git a/nixos/tests/grafana/provision/mute-timings.yaml b/nixos/tests/grafana/provision/mute-timings.yaml new file mode 100644 index 000000000000..1f47f7c18f0c --- /dev/null +++ b/nixos/tests/grafana/provision/mute-timings.yaml @@ -0,0 +1,4 @@ +apiVersion: 1 + +muteTimes: + - name: "Test Mute Timing" diff --git a/nixos/tests/grafana/provision/policies.yaml b/nixos/tests/grafana/provision/policies.yaml new file mode 100644 index 000000000000..eb31126c4ba5 --- /dev/null +++ b/nixos/tests/grafana/provision/policies.yaml @@ -0,0 +1,4 @@ +apiVersion: 1 + +policies: + - receiver: "Test Contact Point" diff --git a/nixos/tests/grafana/provision/rules.yaml b/nixos/tests/grafana/provision/rules.yaml new file mode 100644 index 000000000000..946539c8cb69 --- /dev/null +++ b/nixos/tests/grafana/provision/rules.yaml @@ -0,0 +1,36 @@ +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 diff --git a/nixos/tests/grafana/provision/templates.yaml b/nixos/tests/grafana/provision/templates.yaml new file mode 100644 index 000000000000..09df247b3451 --- /dev/null +++ b/nixos/tests/grafana/provision/templates.yaml @@ -0,0 +1,5 @@ +apiVersion: 1 + +templates: + - name: "Test Template" + template: "Test message" diff --git a/nixos/tests/grafana/provision/test_dashboard.json b/nixos/tests/grafana/provision/test_dashboard.json new file mode 100644 index 000000000000..6e7a5b37f22b --- /dev/null +++ b/nixos/tests/grafana/provision/test_dashboard.json @@ -0,0 +1,47 @@ +{ + "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": "" +}