mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-07-12 05:16:25 +03:00
nixos/grafana: add alerting
This commit is contained in:
parent
34c2ea6750
commit
7908ef062f
15 changed files with 978 additions and 1 deletions
|
@ -844,6 +844,15 @@
|
||||||
option.
|
option.
|
||||||
</para>
|
</para>
|
||||||
</listitem>
|
</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>
|
<listitem>
|
||||||
<para>
|
<para>
|
||||||
Matrix Synapse now requires entries in the
|
Matrix Synapse now requires entries in the
|
||||||
|
|
|
@ -274,6 +274,8 @@ Available as [services.patroni](options.html#opt-services.patroni.enable).
|
||||||
|
|
||||||
- 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.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.
|
- 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 `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.
|
||||||
|
|
|
@ -96,11 +96,25 @@ let
|
||||||
|
|
||||||
notifierFile = pkgs.writeText "notifier.yaml" (builtins.toJSON notifierConfiguration);
|
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" { } ''
|
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 ${datasourceFile} $out/datasources/datasource.yaml
|
||||||
ln -sf ${dashboardFile} $out/dashboards/dashboard.yaml
|
ln -sf ${dashboardFile} $out/dashboards/dashboard.yaml
|
||||||
ln -sf ${notifierFile} $out/notifiers/notifier.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:
|
# Get a submodule without any embedded metadata:
|
||||||
|
@ -544,6 +558,461 @@ in {
|
||||||
type = types.listOf grafanaTypes.notifierConfig;
|
type = types.listOf grafanaTypes.notifierConfig;
|
||||||
apply = x: map _filter x;
|
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
|
||||||
|
<link xlink:href="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
|
||||||
|
<link xlink:href="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
|
||||||
|
<link xlink:href="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
|
||||||
|
<link xlink:href="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
|
||||||
|
<link xlink:href="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";
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
security = {
|
security = {
|
||||||
|
@ -841,6 +1310,26 @@ in {
|
||||||
assertion = if (builtins.isList cfg.provision.dashboards) then true else cfg.provision.dashboards.settings == null || cfg.provision.dashboards.path == null;
|
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";
|
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 = {
|
systemd.services.grafana = {
|
||||||
|
|
|
@ -8,4 +8,9 @@
|
||||||
provision-datasources = import ./provision-datasources { inherit system pkgs; };
|
provision-datasources = import ./provision-datasources { inherit system pkgs; };
|
||||||
provision-dashboards = import ./provision-dashboards { inherit system pkgs; };
|
provision-dashboards = import ./provision-dashboards { inherit system pkgs; };
|
||||||
provision-notifiers = import ./provision-notifiers.nix { inherit system pkgs; };
|
provision-notifiers = import ./provision-notifiers.nix { inherit system pkgs; };
|
||||||
|
provision-rules = import ./provision-rules { inherit system pkgs; };
|
||||||
|
provision-contact-points = import ./provision-contact-points { inherit system pkgs; };
|
||||||
|
provision-policies = import ./provision-policies { inherit system pkgs; };
|
||||||
|
provision-templates = import ./provision-templates { inherit system pkgs; };
|
||||||
|
provision-mute-timings = import ./provision-mute-timings { inherit system pkgs; };
|
||||||
}
|
}
|
||||||
|
|
76
nixos/tests/grafana/provision-contact-points/default.nix
Normal file
76
nixos/tests/grafana/provision-contact-points/default.nix
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
args@{ pkgs, ... }:
|
||||||
|
|
||||||
|
(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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
extraNodeConfs = {
|
||||||
|
provisionContactPointsNix = {
|
||||||
|
services.grafana.provision = {
|
||||||
|
alerting.contactPoints.settings = {
|
||||||
|
contactPoints = [{
|
||||||
|
name = "Test Contact Point";
|
||||||
|
receivers = [{
|
||||||
|
uid = "test_contact_point";
|
||||||
|
type = "prometheus-alertmanager";
|
||||||
|
settings.url = "http://localhost:9000";
|
||||||
|
}];
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
provisionContactPointsYaml = {
|
||||||
|
services.grafana.provision.alerting.contactPoints.path = ./provision-contact-points.yaml;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes = builtins.listToAttrs (map (provisionType:
|
||||||
|
nameValuePair provisionType (mkMerge [
|
||||||
|
baseGrafanaConf
|
||||||
|
(extraNodeConfs.${provisionType} or {})
|
||||||
|
])) [ "provisionContactPointsNix" "provisionContactPointsYaml" ]);
|
||||||
|
|
||||||
|
in {
|
||||||
|
name = "grafana-provision-contact-points";
|
||||||
|
|
||||||
|
meta = with maintainers; {
|
||||||
|
maintainers = [ kfears willibutz ];
|
||||||
|
};
|
||||||
|
|
||||||
|
inherit nodes;
|
||||||
|
|
||||||
|
testScript = ''
|
||||||
|
start_all()
|
||||||
|
with subtest("Successful contact point provision with Nix"):
|
||||||
|
provisionContactPointsNix.wait_for_unit("grafana.service")
|
||||||
|
provisionContactPointsNix.wait_for_open_port(3000)
|
||||||
|
provisionContactPointsNix.succeed(
|
||||||
|
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/contact-points | grep Test\ Contact\ Point"
|
||||||
|
)
|
||||||
|
provisionContactPointsNix.shutdown()
|
||||||
|
|
||||||
|
with subtest("Successful contact point provision with YAML"):
|
||||||
|
provisionContactPointsYaml.wait_for_unit("grafana.service")
|
||||||
|
provisionContactPointsYaml.wait_for_open_port(3000)
|
||||||
|
provisionContactPointsYaml.succeed(
|
||||||
|
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/contact-points | grep Test\ Contact\ Point"
|
||||||
|
)
|
||||||
|
provisionContactPointsYaml.shutdown()
|
||||||
|
'';
|
||||||
|
})) args
|
|
@ -0,0 +1,9 @@
|
||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
contactPoints:
|
||||||
|
- name: "Test Contact Point"
|
||||||
|
receivers:
|
||||||
|
- uid: "test_contact_point"
|
||||||
|
type: prometheus-alertmanager
|
||||||
|
settings:
|
||||||
|
url: http://localhost:9000
|
71
nixos/tests/grafana/provision-mute-timings/default.nix
Normal file
71
nixos/tests/grafana/provision-mute-timings/default.nix
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
args@{ pkgs, ... }:
|
||||||
|
|
||||||
|
(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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
extraNodeConfs = {
|
||||||
|
provisionMuteTimingsNix = {
|
||||||
|
services.grafana.provision = {
|
||||||
|
alerting.muteTimings.settings = {
|
||||||
|
muteTimes = [{
|
||||||
|
name = "Test Mute Timing";
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
provisionMuteTimingsYaml = {
|
||||||
|
services.grafana.provision.alerting.muteTimings.path = ./provision-mute-timings.yaml;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes = builtins.listToAttrs (map (provisionType:
|
||||||
|
nameValuePair provisionType (mkMerge [
|
||||||
|
baseGrafanaConf
|
||||||
|
(extraNodeConfs.${provisionType} or {})
|
||||||
|
])) [ "provisionMuteTimingsNix" "provisionMuteTimingsYaml" ]);
|
||||||
|
|
||||||
|
in {
|
||||||
|
name = "grafana-provision-mute-timings";
|
||||||
|
|
||||||
|
meta = with maintainers; {
|
||||||
|
maintainers = [ kfears willibutz ];
|
||||||
|
};
|
||||||
|
|
||||||
|
inherit nodes;
|
||||||
|
|
||||||
|
testScript = ''
|
||||||
|
start_all()
|
||||||
|
with subtest("Successful mute timings provision with Nix"):
|
||||||
|
provisionMuteTimingsNix.wait_for_unit("grafana.service")
|
||||||
|
provisionMuteTimingsNix.wait_for_open_port(3000)
|
||||||
|
provisionMuteTimingsNix.succeed(
|
||||||
|
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/mute-timings | grep Test\ Mute\ Timing"
|
||||||
|
)
|
||||||
|
provisionMuteTimingsNix.shutdown()
|
||||||
|
|
||||||
|
with subtest("Successful mute timings provision with YAML"):
|
||||||
|
provisionMuteTimingsYaml.wait_for_unit("grafana.service")
|
||||||
|
provisionMuteTimingsYaml.wait_for_open_port(3000)
|
||||||
|
provisionMuteTimingsYaml.succeed(
|
||||||
|
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/mute-timings | grep Test\ Mute\ Timing"
|
||||||
|
)
|
||||||
|
provisionMuteTimingsYaml.shutdown()
|
||||||
|
'';
|
||||||
|
})) args
|
|
@ -0,0 +1,4 @@
|
||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
muteTimes:
|
||||||
|
- name: "Test Mute Timing"
|
82
nixos/tests/grafana/provision-policies/default.nix
Normal file
82
nixos/tests/grafana/provision-policies/default.nix
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
args@{ pkgs, ... }:
|
||||||
|
|
||||||
|
(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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
extraNodeConfs = {
|
||||||
|
provisionPoliciesNix = {
|
||||||
|
services.grafana.provision = {
|
||||||
|
alerting.policies.settings = {
|
||||||
|
policies = [{
|
||||||
|
receiver = "Test Contact Point";
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
alerting.contactPoints.settings = {
|
||||||
|
contactPoints = [{
|
||||||
|
name = "Test Contact Point";
|
||||||
|
receivers = [{
|
||||||
|
uid = "test_contact_point";
|
||||||
|
type = "prometheus-alertmanager";
|
||||||
|
settings.url = "http://localhost:9000";
|
||||||
|
}];
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
provisionPoliciesYaml = {
|
||||||
|
services.grafana.provision.alerting.policies.path = ./provision-policies.yaml;
|
||||||
|
services.grafana.provision.alerting.contactPoints.path = ./provision-contact-points.yaml;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes = builtins.listToAttrs (map (provisionType:
|
||||||
|
nameValuePair provisionType (mkMerge [
|
||||||
|
baseGrafanaConf
|
||||||
|
(extraNodeConfs.${provisionType} or {})
|
||||||
|
])) [ "provisionPoliciesNix" "provisionPoliciesYaml" ]);
|
||||||
|
|
||||||
|
in {
|
||||||
|
name = "grafana-provision-policies";
|
||||||
|
|
||||||
|
meta = with maintainers; {
|
||||||
|
maintainers = [ kfears willibutz ];
|
||||||
|
};
|
||||||
|
|
||||||
|
inherit nodes;
|
||||||
|
|
||||||
|
testScript = ''
|
||||||
|
start_all()
|
||||||
|
with subtest("Successful policy provision with Nix"):
|
||||||
|
provisionPoliciesNix.wait_for_unit("grafana.service")
|
||||||
|
provisionPoliciesNix.wait_for_open_port(3000)
|
||||||
|
provisionPoliciesNix.succeed(
|
||||||
|
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/contact-points | grep Test\ Contact\ Point"
|
||||||
|
)
|
||||||
|
provisionPoliciesNix.shutdown()
|
||||||
|
|
||||||
|
with subtest("Successful policy provision with YAML"):
|
||||||
|
provisionPoliciesYaml.wait_for_unit("grafana.service")
|
||||||
|
provisionPoliciesYaml.wait_for_open_port(3000)
|
||||||
|
provisionPoliciesYaml.succeed(
|
||||||
|
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/contact-points | grep Test\ Contact\ Point"
|
||||||
|
)
|
||||||
|
provisionPoliciesYaml.shutdown()
|
||||||
|
'';
|
||||||
|
})) args
|
|
@ -0,0 +1,9 @@
|
||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
contactPoints:
|
||||||
|
- name: "Test Contact Point"
|
||||||
|
receivers:
|
||||||
|
- uid: "test_contact_point"
|
||||||
|
type: prometheus-alertmanager
|
||||||
|
settings:
|
||||||
|
url: http://localhost:9000
|
|
@ -0,0 +1,4 @@
|
||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
policies:
|
||||||
|
- receiver: "Test Contact Point"
|
104
nixos/tests/grafana/provision-rules/default.nix
Normal file
104
nixos/tests/grafana/provision-rules/default.nix
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
args@{ pkgs, ... }:
|
||||||
|
|
||||||
|
(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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
extraNodeConfs = {
|
||||||
|
provisionRulesNix = {
|
||||||
|
services.grafana.provision = {
|
||||||
|
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";
|
||||||
|
}];
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
provisionRulesYaml = {
|
||||||
|
services.grafana.provision.alerting.rules.path = ./provision-rules.yaml;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes = builtins.listToAttrs (map (provisionType:
|
||||||
|
nameValuePair provisionType (mkMerge [
|
||||||
|
baseGrafanaConf
|
||||||
|
(extraNodeConfs.${provisionType} or {})
|
||||||
|
])) [ "provisionRulesNix" "provisionRulesYaml" ]);
|
||||||
|
|
||||||
|
in {
|
||||||
|
name = "grafana-provision-rules";
|
||||||
|
|
||||||
|
meta = with maintainers; {
|
||||||
|
maintainers = [ kfears willibutz ];
|
||||||
|
};
|
||||||
|
|
||||||
|
inherit nodes;
|
||||||
|
|
||||||
|
testScript = ''
|
||||||
|
start_all()
|
||||||
|
with subtest("Successful rule provision with Nix"):
|
||||||
|
provisionRulesNix.wait_for_unit("grafana.service")
|
||||||
|
provisionRulesNix.wait_for_open_port(3000)
|
||||||
|
provisionRulesNix.succeed(
|
||||||
|
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/alert-rules/test_rule | grep Test\ Rule"
|
||||||
|
)
|
||||||
|
provisionRulesNix.shutdown()
|
||||||
|
|
||||||
|
with subtest("Successful rule provision with YAML"):
|
||||||
|
provisionRulesYaml.wait_for_unit("grafana.service")
|
||||||
|
provisionRulesYaml.wait_for_open_port(3000)
|
||||||
|
provisionRulesYaml.succeed(
|
||||||
|
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/alert-rules/test_rule | grep Test\ Rule"
|
||||||
|
)
|
||||||
|
provisionRulesYaml.shutdown()
|
||||||
|
'';
|
||||||
|
})) args
|
36
nixos/tests/grafana/provision-rules/provision-rules.yaml
Normal file
36
nixos/tests/grafana/provision-rules/provision-rules.yaml
Normal file
|
@ -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
|
72
nixos/tests/grafana/provision-templates/default.nix
Normal file
72
nixos/tests/grafana/provision-templates/default.nix
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
args@{ pkgs, ... }:
|
||||||
|
|
||||||
|
(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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
extraNodeConfs = {
|
||||||
|
provisionTemplatesNix = {
|
||||||
|
services.grafana.provision = {
|
||||||
|
alerting.templates.settings = {
|
||||||
|
templates = [{
|
||||||
|
name = "Test Template";
|
||||||
|
template = "Test message";
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
provisionTemplatesYaml = {
|
||||||
|
services.grafana.provision.alerting.templates.path = ./provision-templates.yaml;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes = builtins.listToAttrs (map (provisionType:
|
||||||
|
nameValuePair provisionType (mkMerge [
|
||||||
|
baseGrafanaConf
|
||||||
|
(extraNodeConfs.${provisionType} or {})
|
||||||
|
])) [ "provisionTemplatesNix" "provisionTemplatesYaml" ]);
|
||||||
|
|
||||||
|
in {
|
||||||
|
name = "grafana-provision-rules";
|
||||||
|
|
||||||
|
meta = with maintainers; {
|
||||||
|
maintainers = [ kfears willibutz ];
|
||||||
|
};
|
||||||
|
|
||||||
|
inherit nodes;
|
||||||
|
|
||||||
|
testScript = ''
|
||||||
|
start_all()
|
||||||
|
with subtest("Successful template provision with Nix"):
|
||||||
|
provisionTemplatesNix.wait_for_unit("grafana.service")
|
||||||
|
provisionTemplatesNix.wait_for_open_port(3000)
|
||||||
|
provisionTemplatesNix.succeed(
|
||||||
|
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/templates | grep Test\ Template"
|
||||||
|
)
|
||||||
|
provisionTemplatesNix.shutdown()
|
||||||
|
|
||||||
|
with subtest("Successful template provision with YAML"):
|
||||||
|
provisionTemplatesYaml.wait_for_unit("grafana.service")
|
||||||
|
provisionTemplatesYaml.wait_for_open_port(3000)
|
||||||
|
provisionTemplatesYaml.succeed(
|
||||||
|
"curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/templates | grep Test\ Template"
|
||||||
|
)
|
||||||
|
provisionTemplatesYaml.shutdown()
|
||||||
|
'';
|
||||||
|
})) args
|
|
@ -0,0 +1,5 @@
|
||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
templates:
|
||||||
|
- name: "Test Template"
|
||||||
|
template: "Test message"
|
Loading…
Add table
Add a link
Reference in a new issue