From 72132aa5ce4f6c913ccff4dc8c56cbb324f1b996 Mon Sep 17 00:00:00 2001
From: Haoyu Sun <hasun@redhat.com>
Date: Fri, 27 Aug 2021 17:55:25 +0200
Subject: [PATCH] add rule patching function

---
 examples/rule-patcher.jsonnet                 |  31 ++++
 examples/rule-patches.libsonnet               |  26 +++
 .../lib/rule-sanitizer.libsonnet              | 156 ++++++++++++++++++
 3 files changed, 213 insertions(+)
 create mode 100644 examples/rule-patcher.jsonnet
 create mode 100644 examples/rule-patches.libsonnet
 create mode 100644 jsonnet/kube-prometheus/lib/rule-sanitizer.libsonnet

diff --git a/examples/rule-patcher.jsonnet b/examples/rule-patcher.jsonnet
new file mode 100644
index 00000000..59d18ddf
--- /dev/null
+++ b/examples/rule-patcher.jsonnet
@@ -0,0 +1,31 @@
+local kp =
+  (import 'kube-prometheus/main.libsonnet') +
+  {
+    values+:: {
+      common+: {
+        namespace: 'monitoring',
+      },
+    },
+  };
+
+local rulePatches = import 'rule-patches.libsonnet';
+
+local sanitizePrometheusRules = (import 'kube-prometheus/lib/rule-sanitizer.libsonnet')(rulePatches).sanitizePrometheusRules;
+
+sanitizePrometheusRules({ 'setup/0namespace-namespace': kp.kubePrometheus.namespace } +
+                        {
+                          ['setup/prometheus-operator-' + name]: kp.prometheusOperator[name]
+                          for name in std.filter((function(name) name != 'serviceMonitor' && name != 'prometheusRule'), std.objectFields(kp.prometheusOperator))
+                        } +
+                        // serviceMonitor and prometheusRule are separated so that they can be created after the CRDs are ready
+                        { 'prometheus-operator-serviceMonitor': kp.prometheusOperator.serviceMonitor } +
+                        { 'prometheus-operator-prometheusRule': kp.prometheusOperator.prometheusRule } +
+                        { 'kube-prometheus-prometheusRule': kp.kubePrometheus.prometheusRule } +
+                        { ['alertmanager-' + name]: kp.alertmanager[name] for name in std.objectFields(kp.alertmanager) } +
+                        { ['blackbox-exporter-' + name]: kp.blackboxExporter[name] for name in std.objectFields(kp.blackboxExporter) } +
+                        { ['grafana-' + name]: kp.grafana[name] for name in std.objectFields(kp.grafana) } +
+                        { ['kube-state-metrics-' + name]: kp.kubeStateMetrics[name] for name in std.objectFields(kp.kubeStateMetrics) } +
+                        { ['kubernetes-' + name]: kp.kubernetesControlPlane[name] for name in std.objectFields(kp.kubernetesControlPlane) }
+                        { ['node-exporter-' + name]: kp.nodeExporter[name] for name in std.objectFields(kp.nodeExporter) } +
+                        { ['prometheus-' + name]: kp.prometheus[name] for name in std.objectFields(kp.prometheus) } +
+                        { ['prometheus-adapter-' + name]: kp.prometheusAdapter[name] for name in std.objectFields(kp.prometheusAdapter) })
diff --git a/examples/rule-patches.libsonnet b/examples/rule-patches.libsonnet
new file mode 100644
index 00000000..65bd863f
--- /dev/null
+++ b/examples/rule-patches.libsonnet
@@ -0,0 +1,26 @@
+{
+  excludedRuleGroups: [
+    'alertmanager.rules',
+  ],
+  excludedRules: [
+    {
+      name: 'prometheus-operator',
+      rules: [
+        { alert: 'PrometheusOperatorListErrors' },
+      ],
+    },
+  ],
+  patchedRules: [
+    {
+      name: 'prometheus-operator',
+      rules: [
+        {
+          alert: 'PrometheusOperatorWatchErrors',
+          labels: {
+            severity: 'info',
+          },
+        },
+      ],
+    },
+  ],
+}
diff --git a/jsonnet/kube-prometheus/lib/rule-sanitizer.libsonnet b/jsonnet/kube-prometheus/lib/rule-sanitizer.libsonnet
new file mode 100644
index 00000000..00cc27ce
--- /dev/null
+++ b/jsonnet/kube-prometheus/lib/rule-sanitizer.libsonnet
@@ -0,0 +1,156 @@
+local defaults = {
+  /* name of rule groups to exclude */
+  excludedRuleGroups: [],
+  /* Rule match is based on field "alert" or "record" for excludedRules and patchedRules.
+   * When multiple match is found, we can use a "index" field to distingush each rule,
+   * which represents their order of appearance. For example, if we have two rules:
+   * [
+   *  {
+   *    name: 'alertmanager.rules',
+   *    rules: [
+   *      {
+   *        alert: 'A',
+   *        field: 'A0 rule',
+   *        labels: {
+   *          severity: 'warning',
+   *        },
+   *      },
+   *      {
+   *        alert: 'A',
+   *        field: 'A1 rule',
+   *        labels: {
+   *          severity: 'warning',
+   *        },
+   *      },
+   *    ],
+   *  },
+   * ]
+   * We can use index 1 to choose "A1 rule" for patching, as shown in the example below:
+   * [
+   *   {
+   *     name: 'alertmanager.rules',
+   *     rules: [
+   *       {
+   *         alert: 'A',
+   *         index: 1,
+   *         patch: 'A1',
+   *         labels: {
+   *           severity: 'warning',
+   *         },
+   *       },
+   *     ],
+   *   },
+   * ]
+   */
+  excludedRules: [],
+  patchedRules: [],
+};
+
+
+local deleteIndex(rule) = {
+  [k]: rule[k]
+  for k in std.objectFields(rule)
+  if k != 'index'
+};
+
+
+local patchOrExcludeRule(rule, ruleSet, operation) =
+  if std.length(ruleSet) == 0 then
+    [deleteIndex(rule)]
+  /* 2 rules match when the name of the patch is a prefix of the name of the rule to patch. */
+  else if ((('alert' in rule && 'alert' in ruleSet[0]) && std.startsWith(rule.alert, ruleSet[0].alert)) ||
+           (('record' in rule && 'record' in ruleSet[0]) && std.startsWith(rule.record, ruleSet[0].record))) &&
+          (!('index' in ruleSet[0]) || (('index' in ruleSet[0]) && (ruleSet[0].index == rule.index))) then
+    if operation == 'patch' then
+      local patch = {
+        [k]: ruleSet[0][k]
+        for k in std.objectFields(ruleSet[0])
+        if k != 'alert' && k != 'record' && k != 'index'
+      };
+      [deleteIndex(std.mergePatch(rule, patch))]
+    else  // equivalnt to operation == 'exclude'
+      []
+
+  else
+    [] + patchOrExcludeRule(rule, ruleSet[1:], operation);
+
+
+local sameRuleName(rule1, rule2) =
+  if ('alert' in rule1 && 'alert' in rule2) then
+    rule1.alert == rule2.alert
+  else if ('record' in rule1 && 'record' in rule2) then
+    rule1.record == rule2.record
+  else
+    false;
+
+local indexRules(lastRule, ruleSet) =
+  if std.length(ruleSet) == 0 then
+    []
+  else if (lastRule != null) && sameRuleName(lastRule, ruleSet[0]) then
+    local updatedRule = std.mergePatch(ruleSet[0], { index: lastRule.index + 1 });
+    [updatedRule] + indexRules(updatedRule, ruleSet[1:])
+  else
+    local updatedRule = std.mergePatch(ruleSet[0], { index: 0 });
+    [updatedRule] + indexRules(updatedRule, ruleSet[1:]);
+
+local ruleName(rule) =
+  if ('alert' in rule) then
+    rule.alert
+  else if ('record' in rule) then
+    rule.record
+  else
+    assert false : 'rule should have either "alert" or "record" field' + std.toString(rule);
+    '';
+
+local patchOrExcludeRuleGroup(group, groupSet, operation) =
+  if std.length(groupSet) == 0 then
+    [group.rules]
+  else if (group.name == groupSet[0].name) then
+    local indexedRules = indexRules(null, std.sort(
+      group.rules, keyF=ruleName
+    ));
+    [patchOrExcludeRule(rule, groupSet[0].rules, operation) for rule in indexedRules]
+  else
+    [] + patchOrExcludeRuleGroup(group, groupSet[1:], operation);
+
+function(params) {
+  local ruleModifications = defaults + params,
+  assert std.isArray(ruleModifications.excludedRuleGroups) : 'rule-patcher: excludedRuleGroups should be an array',
+  assert std.isArray(ruleModifications.excludedRules) : 'rule-patcher: excludedRules should be an array',
+  assert std.isArray(ruleModifications.patchedRules) : 'rule-patcher: patchedRules should be an array',
+
+  local excludeRule(o) = o {
+    [if (o.kind == 'PrometheusRule') then 'spec']+: {
+      groups: std.filterMap(
+        function(group) !std.member(ruleModifications.excludedRuleGroups, group.name),
+        function(group)
+          group {
+            rules: std.flattenArrays(
+              patchOrExcludeRuleGroup(group, ruleModifications.excludedRules, 'exclude')
+            ),
+          },
+        super.groups,
+      ),
+    },
+  },
+
+  local patchRule(o) = o {
+    [if (o.kind == 'PrometheusRule') then 'spec']+: {
+      groups: std.map(
+        function(group)
+          group {
+            rules: std.flattenArrays(
+              patchOrExcludeRuleGroup(group, ruleModifications.patchedRules, 'patch')
+            ),
+          },
+        super.groups,
+      ),
+    },
+  },
+
+  // shorthand for rule patching, rule excluding
+  sanitizePrometheusRules(o): {
+    [k]: patchRule(excludeRule(o[k]))
+    for k in std.objectFields(o)
+  },
+}
-- 
GitLab