operators/charts/kube-prometheus-stack/hack/sync_prometheus_rules.py
2025-05-23 07:43:19 +03:00

683 lines
29 KiB
Python

#!/usr/bin/env python3
"""Fetch alerting and aggregation rules from provided urls into this chart."""
import json
import os
import re
import shutil
import subprocess
import textwrap
import _jsonnet
import requests
import yaml
from yaml.representer import SafeRepresenter
# https://stackoverflow.com/a/20863889/961092
class LiteralStr(str):
pass
def change_style(style, representer):
def new_representer(dumper, data):
scalar = representer(dumper, data)
scalar.style = style
return scalar
return new_representer
refs = {
# renovate: git-refs=https://github.com/prometheus-operator/kube-prometheus branch=main
'ref.kube-prometheus': '1e4df581de8897f16108bc4881be26e2a98c02b8',
# renovate: git-refs=https://github.com/kubernetes-monitoring/kubernetes-mixin branch=master
'ref.kubernetes-mixin': '6c82d5abe587b4c1dda7f1b0013af7d81e84c9fe',
# renovate: git-refs=https://github.com/etcd-io/etcd branch=main
'ref.etcd': '592c195ae21e8d58b7e2fef355e7067499d70edd',
}
# Source files list
charts = [
{
'git': 'https://github.com/prometheus-operator/kube-prometheus.git',
'branch': refs['ref.kube-prometheus'],
'source': 'main.libsonnet',
'cwd': '',
'destination': '../templates/prometheus/rules-1.14',
'min_kubernetes': '1.14.0-0',
'mixin': """
local kp =
(import 'jsonnet/kube-prometheus/main.libsonnet') + {
values+:: {
nodeExporter+: {
mixin+: {
_config+: {
fsSelector: '$.Values.defaultRules.node.fsSelector',
},
},
},
common+: {
namespace: 'monitoring',
},
kubernetesControlPlane+: {
kubeProxy: true,
},
},
grafana: {},
};
{
groups: std.flattenArrays([
kp[component][resource].spec.groups
for component in std.objectFields(kp)
for resource in std.filter(
function(resource)
kp[component][resource].kind == 'PrometheusRule',
std.objectFields(kp[component])
)
]),
}
"""
},
{
'git': 'https://github.com/kubernetes-monitoring/kubernetes-mixin.git',
'branch': refs['ref.kubernetes-mixin'],
'source': 'windows.libsonnet',
'cwd': 'rules',
'destination': '../templates/prometheus/rules-1.14',
'min_kubernetes': '1.14.0-0',
'mixin': """
local kp =
{ prometheusAlerts+:: {}, prometheusRules+:: {}} +
(import "windows.libsonnet") +
{'_config': {
'clusterLabel': 'cluster',
'windowsExporterSelector': 'job="windows-exporter"',
'kubeStateMetricsSelector': 'job="kube-state-metrics"',
}};
kp.prometheusAlerts + kp.prometheusRules
"""
},
{
'git': 'https://github.com/etcd-io/etcd.git',
'branch': refs['ref.etcd'],
'source': 'mixin.libsonnet',
'cwd': 'contrib/mixin',
'destination': '../templates/prometheus/rules-1.14',
'min_kubernetes': '1.14.0-0',
# Override the default etcd_instance_labels to get proper aggregation for etcd instances in k8s clusters (#2720)
# see https://github.com/etcd-io/etcd/blob/1c22e7b36bc5d8543f1646212f2960f9fe503b8c/contrib/mixin/config.libsonnet#L13
'mixin': """
local kp =
{ prometheusAlerts+:: {}, prometheusRules+:: {}} +
(import "mixin.libsonnet") +
{'_config': {
'etcd_selector': 'job=~".*etcd.*"',
'etcd_instance_labels': 'instance, pod',
'scrape_interval_seconds': 30,
'clusterLabel': 'job',
}};
kp.prometheusAlerts + kp.prometheusRules
"""
},
]
# Additional conditions map
condition_map = {
'alertmanager.rules': ' .Values.defaultRules.rules.alertmanager',
'config-reloaders': ' .Values.defaultRules.rules.configReloaders',
'etcd': ' .Values.kubeEtcd.enabled .Values.defaultRules.rules.etcd',
'general.rules': ' .Values.defaultRules.rules.general',
'k8s.rules.container_cpu_limits': ' .Values.defaultRules.rules.k8sContainerCpuLimits',
'k8s.rules.container_cpu_requests': ' .Values.defaultRules.rules.k8sContainerCpuRequests',
'k8s.rules.container_cpu_usage_seconds_total': ' .Values.defaultRules.rules.k8sContainerCpuUsageSecondsTotal',
'k8s.rules.container_memory_cache': ' .Values.defaultRules.rules.k8sContainerMemoryCache',
'k8s.rules.container_memory_limits': ' .Values.defaultRules.rules.k8sContainerMemoryLimits',
'k8s.rules.container_memory_requests': ' .Values.defaultRules.rules.k8sContainerMemoryRequests',
'k8s.rules.container_memory_rss': ' .Values.defaultRules.rules.k8sContainerMemoryRss',
'k8s.rules.container_memory_swap': ' .Values.defaultRules.rules.k8sContainerMemorySwap',
'k8s.rules.container_memory_working_set_bytes': ' .Values.defaultRules.rules.k8sContainerMemoryWorkingSetBytes',
'k8s.rules.container_resource': ' .Values.defaultRules.rules.k8sContainerResource',
'k8s.rules.pod_owner': ' .Values.defaultRules.rules.k8sPodOwner',
'kube-apiserver-availability.rules': ' .Values.kubeApiServer.enabled .Values.defaultRules.rules.kubeApiserverAvailability',
'kube-apiserver-burnrate.rules': ' .Values.kubeApiServer.enabled .Values.defaultRules.rules.kubeApiserverBurnrate',
'kube-apiserver-histogram.rules': ' .Values.kubeApiServer.enabled .Values.defaultRules.rules.kubeApiserverHistogram',
'kube-apiserver-slos': ' .Values.kubeApiServer.enabled .Values.defaultRules.rules.kubeApiserverSlos',
'kube-prometheus-general.rules': ' .Values.defaultRules.rules.kubePrometheusGeneral',
'kube-prometheus-node-recording.rules': ' .Values.defaultRules.rules.kubePrometheusNodeRecording',
'kube-scheduler.rules': ' .Values.kubeScheduler.enabled .Values.defaultRules.rules.kubeSchedulerRecording',
'kube-state-metrics': ' .Values.defaultRules.rules.kubeStateMetrics',
'kubelet.rules': ' .Values.kubelet.enabled .Values.defaultRules.rules.kubelet',
'kubernetes-apps': ' .Values.defaultRules.rules.kubernetesApps',
'kubernetes-resources': ' .Values.defaultRules.rules.kubernetesResources',
'kubernetes-storage': ' .Values.defaultRules.rules.kubernetesStorage',
'kubernetes-system': ' .Values.defaultRules.rules.kubernetesSystem',
'kubernetes-system-kube-proxy': ' .Values.kubeProxy.enabled .Values.defaultRules.rules.kubeProxy',
'kubernetes-system-apiserver': ' .Values.defaultRules.rules.kubernetesSystem', # kubernetes-system was split into more groups in 1.14, one of them is kubernetes-system-apiserver
'kubernetes-system-kubelet': ' .Values.defaultRules.rules.kubernetesSystem', # kubernetes-system was split into more groups in 1.14, one of them is kubernetes-system-kubelet
'kubernetes-system-controller-manager': ' .Values.kubeControllerManager.enabled .Values.defaultRules.rules.kubeControllerManager',
'kubernetes-system-scheduler': ' .Values.kubeScheduler.enabled .Values.defaultRules.rules.kubeSchedulerAlerting',
'node-exporter.rules': ' .Values.defaultRules.rules.nodeExporterRecording',
'node-exporter': ' .Values.defaultRules.rules.nodeExporterAlerting',
'node.rules': ' .Values.defaultRules.rules.node',
'node-network': ' .Values.defaultRules.rules.network',
'prometheus-operator': ' .Values.defaultRules.rules.prometheusOperator',
'prometheus': ' .Values.defaultRules.rules.prometheus', # kube-prometheus >= 1.14 uses prometheus as group instead of prometheus.rules
'windows.node.rules': ' .Values.windowsMonitoring.enabled .Values.defaultRules.rules.windows',
'windows.pod.rules': ' .Values.windowsMonitoring.enabled .Values.defaultRules.rules.windows',
}
alert_condition_map = {
'AggregatedAPIDown': 'semverCompare ">=1.18.0-0" $kubeTargetVersion',
'AlertmanagerDown': '.Values.alertmanager.enabled',
'CoreDNSDown': '.Values.kubeDns.enabled',
'KubeAPIDown': '.Values.kubeApiServer.enabled', # there are more alerts which are left enabled, because they'll never fire without metrics
'KubeControllerManagerDown': '.Values.kubeControllerManager.enabled',
'KubeletDown': '.Values.prometheusOperator.kubeletService.enabled', # there are more alerts which are left enabled, because they'll never fire without metrics
'KubeSchedulerDown': '.Values.kubeScheduler.enabled',
'KubeStateMetricsDown': '.Values.kubeStateMetrics.enabled', # there are more alerts which are left enabled, because they'll never fire without metrics
'NodeExporterDown': '.Values.nodeExporter.enabled',
'PrometheusOperatorDown': '.Values.prometheusOperator.enabled',
}
replacement_map = {
'job="prometheus-operator"': {
'replacement': 'job="{{ $operatorJob }}"',
'init': '{{- $operatorJob := printf "%s-%s" (include "kube-prometheus-stack.fullname" .) "operator" }}'},
'job="prometheus-k8s"': {
'replacement': 'job="{{ $prometheusJob }}"',
'init': '{{- $prometheusJob := printf "%s-%s" (include "kube-prometheus-stack.fullname" .) "prometheus" }}'},
'job="alertmanager-main"': {
'replacement': 'job="{{ $alertmanagerJob }}"',
'init': '{{- $alertmanagerJob := printf "%s-%s" (include "kube-prometheus-stack.fullname" .) "alertmanager" }}'},
'namespace="monitoring"': {
'replacement': 'namespace="{{ $namespace }}"',
'init': '{{- $namespace := printf "%s" (include "kube-prometheus-stack.namespace" .) }}'},
'alertmanager-$1': {
'replacement': '$1',
'init': ''},
'job="kube-state-metrics"': {
'replacement': 'job="{{ $kubeStateMetricsJob }}"',
'init': '{{- $kubeStateMetricsJob := include "kube-prometheus-stack-kube-state-metrics.name" . }}'},
'job="{{ $kubeStateMetricsJob }}"': {
'replacement': 'job="{{ $kubeStateMetricsJob }}", namespace=~"{{ $targetNamespace }}"',
'limitGroup': ['kubernetes-apps'],
'init': '{{- $targetNamespace := .Values.defaultRules.appNamespacesTarget }}'},
'job="kubelet"': {
'replacement': 'job="kubelet", namespace=~"{{ $targetNamespace }}"',
'limitGroup': ['kubernetes-storage'],
'init': '{{- $targetNamespace := .Values.defaultRules.appNamespacesTarget }}'},
'runbook_url: https://runbooks.prometheus-operator.dev/runbooks/': {
'replacement': 'runbook_url: {{ .Values.defaultRules.runbookUrl }}/',
'init': ''},
'(namespace,service)': {
'replacement': '(namespace,service,cluster)',
'init': ''},
'(namespace, job, handler': {
'replacement': '(cluster, namespace, job, handler',
'init': ''},
'$.Values.defaultRules.node.fsSelector': {
'replacement': '{{ $.Values.defaultRules.node.fsSelector }}',
'init': ''},
}
# standard header
header = '''{{- /*
Generated from '%(name)s' group from %(url)s
Do not change in-place! In order to change this file first read following link:
https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack/hack
*/ -}}
{{- $kubeTargetVersion := default .Capabilities.KubeVersion.GitVersion .Values.kubeTargetVersionOverride }}
{{- if and (semverCompare ">=%(min_kubernetes)s" $kubeTargetVersion) (semverCompare "<%(max_kubernetes)s" $kubeTargetVersion) .Values.defaultRules.create%(condition)s }}%(init_line)s
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: {{ printf "%%s-%%s" (include "kube-prometheus-stack.fullname" .) "%(name)s" | trunc 63 | trimSuffix "-" }}
namespace: {{ template "kube-prometheus-stack.namespace" . }}
labels:
app: {{ template "kube-prometheus-stack.name" . }}
{{ include "kube-prometheus-stack.labels" . | indent 4 }}
{{- if .Values.defaultRules.labels }}
{{ toYaml .Values.defaultRules.labels | indent 4 }}
{{- end }}
{{- if .Values.defaultRules.annotations }}
annotations:
{{ toYaml .Values.defaultRules.annotations | indent 4 }}
{{- end }}
spec:
groups:
-'''
def init_yaml_styles():
represent_literal_str = change_style('|', SafeRepresenter.represent_str)
yaml.add_representer(LiteralStr, represent_literal_str)
def escape(s):
return s.replace("{{", "{{`{{").replace("}}", "}}`}}").replace("{{`{{", "{{`{{`}}").replace("}}`}}", "{{`}}`}}")
def fix_expr(rules):
"""Remove trailing whitespaces and line breaks, which happen to creep in
due to yaml import specifics;
convert multiline expressions to literal style, |-"""
for rule in rules:
rule['expr'] = rule['expr'].rstrip()
if '\n' in rule['expr']:
rule['expr'] = LiteralStr(rule['expr'])
def yaml_str_repr(struct, indent=4):
"""represent yaml as a string"""
text = yaml.dump(
struct,
width=1000, # to disable line wrapping
default_flow_style=False # to disable multiple items on single line
)
text = escape(text) # escape {{ and }} for helm
text = textwrap.indent(text, ' ' * indent)[indent - 1:] # indent everything, and remove very first line extra indentation
return text
def get_rule_group_condition(group_name, value_key):
if group_name == '':
return ''
if group_name.count(".Values") > 1:
group_name = group_name.split(' ')[-1]
return group_name.replace('Values.defaultRules.rules', f"Values.defaultRules.{value_key}").strip()
def add_rules_conditions(rules, rules_map, indent=4):
"""Add if wrapper for rules, listed in rules_map"""
rule_condition = '{{- if %s }}\n'
for alert_name in rules_map:
line_start = ' ' * indent + '- alert: '
if line_start + alert_name in rules:
rule_text = rule_condition % rules_map[alert_name]
start = 0
# to modify all alerts with same name
while True:
try:
# add if condition
index = rules.index(line_start + alert_name, start)
start = index + len(rule_text) + 1
rules = rules[:index] + rule_text + rules[index:]
# add end of if
try:
next_index = rules.index(line_start, index + len(rule_text) + 1)
except ValueError:
# we found the last alert in file if there are no alerts after it
next_index = len(rules)
# depending on the rule ordering in rules_map it's possible that an if statement from another rule is present at the end of this block.
found_block_end = False
last_line_index = next_index
while not found_block_end:
last_line_index = rules.rindex('\n', index, last_line_index - 1) # find the starting position of the last line
last_line = rules[last_line_index + 1:next_index]
if last_line.startswith('{{- if'):
next_index = last_line_index + 1 # move next_index back if the current block ends in an if statement
continue
found_block_end = True
rules = rules[:next_index] + '{{- end }}\n' + rules[next_index:]
except ValueError:
break
return rules
def add_rules_conditions_from_condition_map(rules, indent=4):
"""Add if wrapper for rules, listed in alert_condition_map"""
rules = add_rules_conditions(rules, alert_condition_map, indent)
return rules
def add_rules_per_rule_conditions(rules, group, indent=4):
"""Add if wrapper for rules, listed in alert_condition_map"""
rules_condition_map = {}
for rule in group['rules']:
if 'alert' in rule:
rules_condition_map[rule['alert']] = f"not (.Values.defaultRules.disabled.{rule['alert']} | default false)"
rules = add_rules_conditions(rules, rules_condition_map, indent)
return rules
def add_custom_labels(rules_str, group, indent=4, label_indent=2):
"""Add if wrapper for additional rules labels"""
rule_group_labels = get_rule_group_condition(condition_map.get(group['name'], ''), 'additionalRuleGroupLabels')
additional_rule_labels = textwrap.indent("""
{{- with .Values.defaultRules.additionalRuleLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with %s }}
{{- toYaml . | nindent 8 }}
{{- end }}""" % (rule_group_labels,), " " * (indent + label_indent * 2))
additional_rule_labels_condition_start = "\n" + " " * (indent + label_indent) + '{{- if or .Values.defaultRules.additionalRuleLabels %s }}' % (rule_group_labels,)
additional_rule_labels_condition_end = "\n" + " " * (indent + label_indent) + '{{- end }}'
# labels: cannot be null, if a rule does not have any labels by default, the labels block
# should only be added if there are .Values defaultRules.additionalRuleLabels defined
rule_seperator = "\n" + " " * indent + "-.*"
label_seperator = "\n" + " " * indent + " labels:"
section_seperator = "\n" + " " * indent + " \\S"
section_seperator_len = len(section_seperator)-1
rules_positions = re.finditer(rule_seperator,rules_str)
# fetch breakpoint between each set of rules
ruleStartingLine = [(rule_position.start(),rule_position.end()) for rule_position in rules_positions]
head = rules_str[:ruleStartingLine[0][0]]
# construct array of rules so they can be handled individually
rules = []
# pylint: disable=E1136
# See https://github.com/pylint-dev/pylint/issues/1498 for None Values
previousRule = None
for r in ruleStartingLine:
if previousRule != None:
rules.append(rules_str[previousRule[0]:r[0]])
previousRule = r
rules.append(rules_str[previousRule[0]:len(rules_str)-1])
for i, rule in enumerate(rules):
current_label = re.search(label_seperator,rule)
if current_label:
# `labels:` block exists
# determine if there are any existing entries
entries = re.search(section_seperator,rule[current_label.end():])
if entries:
entries_start = current_label.end()
entries_end = entries.end()+current_label.end()-section_seperator_len
rules[i] = rule[:entries_end] + additional_rule_labels_condition_start + additional_rule_labels + additional_rule_labels_condition_end + rule[entries_end:]
else:
# `labels:` does not contain any entries
# append template to label section
rules[i] += additional_rule_labels_condition_start + additional_rule_labels + additional_rule_labels_condition_end
else:
# `labels:` block does not exist
# create it and append template
rules[i] += additional_rule_labels_condition_start + "\n" + " " * indent + " labels:" + additional_rule_labels + additional_rule_labels_condition_end
return head + "".join(rules) + "\n"
def add_custom_annotations(rules, group, indent=4):
"""Add if wrapper for additional rules annotations"""
rule_condition = '{{- if .Values.defaultRules.additionalRuleAnnotations }}\n{{ toYaml .Values.defaultRules.additionalRuleAnnotations | indent 8 }}\n{{- end }}'
rule_group_labels = get_rule_group_condition(condition_map.get(group['name'], ''), 'additionalRuleGroupAnnotations')
rule_group_condition = '\n{{- if %s }}\n{{ toYaml %s | indent 8 }}\n{{- end }}' % (rule_group_labels, rule_group_labels)
annotations = " annotations:"
annotations_len = len(annotations) + 1
rule_condition_len = len(rule_condition) + 1
rule_group_condition_len = len(rule_group_condition)
separator = " " * indent + "- alert:.*"
alerts_positions = re.finditer(separator,rules)
alert = 0
for alert_position in alerts_positions:
# Add rule_condition after 'annotations:' statement
index = alert_position.end() + annotations_len + (rule_condition_len + rule_group_condition_len) * alert
rules = rules[:index] + "\n" + rule_condition + rule_group_condition + rules[index:]
alert += 1
return rules
def add_custom_keep_firing_for(rules, indent=4):
"""Add if wrapper for additional rules annotations"""
indent_spaces = " " * indent + " "
keep_firing_for = (indent_spaces + '{{- with .Values.defaultRules.keepFiringFor }}\n' +
indent_spaces + 'keep_firing_for: "{{ . }}"\n' +
indent_spaces + '{{- end }}')
keep_firing_for_len = len(keep_firing_for) + 1
separator = " " * indent + " for:.*"
alerts_positions = re.finditer(separator, rules)
alert = 0
for alert_position in alerts_positions:
# Add rule_condition after 'annotations:' statement
index = alert_position.end() + keep_firing_for_len * alert
rules = rules[:index] + "\n" + keep_firing_for + rules[index:]
alert += 1
return rules
def add_custom_for(rules, indent=4):
"""Add custom 'for:' condition in rules"""
replace_field = "for:"
rules = add_custom_alert_rules(rules, replace_field, indent)
return rules
def add_custom_severity(rules, indent=4):
"""Add custom 'severity:' condition in rules"""
replace_field = "severity:"
rules = add_custom_alert_rules(rules, replace_field, indent)
return rules
def add_custom_alert_rules(rules, key_to_replace, indent):
"""Extend alert field to allow custom values"""
key_to_replace_indented = ' ' * indent + key_to_replace
alertkey_field = '- alert:'
found_alert_key = False
alertname = None
updated_rules = ''
# pylint: disable=C0200
i = 0
while i < len(rules):
if rules[i:i + len(alertkey_field)] == alertkey_field:
found_alert_key = True
start_index_word_after = i + len(alertkey_field) + 1
end_index_alertkey_field = start_index_word_after
while end_index_alertkey_field < len(rules) and rules[end_index_alertkey_field].isalnum():
end_index_alertkey_field += 1
alertname = rules[start_index_word_after:end_index_alertkey_field]
if found_alert_key:
if rules[i:i + len(key_to_replace_indented)] == key_to_replace_indented:
found_alert_key = False
start_index_key_value = i + len(key_to_replace_indented) + 1
end_index_key_to_replace = start_index_key_value
while end_index_key_to_replace < len(rules) and rules[end_index_key_to_replace].isalnum():
end_index_key_to_replace += 1
word_after_key_to_replace = rules[start_index_key_value:end_index_key_to_replace]
new_key = key_to_replace_indented + ' {{ dig "' + alertname + \
'" "' + key_to_replace[:-1] + '" "' + \
word_after_key_to_replace + '" .Values.customRules }}'
updated_rules += new_key
i = end_index_key_to_replace
updated_rules += rules[i]
i += 1
return updated_rules
def write_group_to_file(group, url, destination, min_kubernetes, max_kubernetes):
fix_expr(group['rules'])
group_name = group['name']
# prepare rules string representation
rules = yaml_str_repr(group)
# add replacements of custom variables and include their initialisation in case it's needed
init_line = ''
for line in replacement_map:
if group_name in replacement_map[line].get('limitGroup', [group_name]) and line in rules:
rules = rules.replace(line, replacement_map[line]['replacement'])
if replacement_map[line]['init']:
init_line += '\n' + replacement_map[line]['init']
# append per-alert rules
rules = add_custom_labels(rules, group)
rules = add_custom_annotations(rules, group)
rules = add_custom_keep_firing_for(rules)
rules = add_custom_for(rules)
rules = add_custom_severity(rules)
rules = add_rules_conditions_from_condition_map(rules)
rules = add_rules_per_rule_conditions(rules, group)
# initialize header
lines = header % {
'name': sanitize_name(group['name']),
'url': url,
'condition': condition_map.get(group['name'], ''),
'init_line': init_line,
'min_kubernetes': min_kubernetes,
'max_kubernetes': max_kubernetes
}
# rules themselves
lines += re.sub(
r'\s(by|on) ?\(',
r' \1 ({{ range $.Values.defaultRules.additionalAggregationLabels }}{{ . }},{{ end }}',
rules,
flags=re.IGNORECASE
)
# footer
lines += '{{- end }}'
filename = group['name'] + '.yaml'
new_filename = "%s/%s" % (destination, filename)
# make sure directories to store the file exist
os.makedirs(destination, exist_ok=True)
# recreate the file
with open(new_filename, 'w') as f:
f.write(lines)
print("Generated %s" % new_filename)
def write_rules_names_template():
with open('../templates/prometheus/_rules.tpl', 'w') as f:
f.write('''{{- /*
Generated file. Do not change in-place! In order to change this file first read following link:
https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack/hack
*/ -}}\n''')
f.write('{{- define "rules.names" }}\n')
f.write('rules:\n')
for rule in condition_map:
f.write(' - "%s"\n' % sanitize_name(rule))
f.write('{{- end }}')
def main():
os.chdir(os.path.dirname(os.path.abspath(__file__)))
init_yaml_styles()
# read the rules, create a new template file per group
for chart in charts:
if 'git' in chart:
if 'source' not in chart:
chart['source'] = '_mixin.jsonnet'
url = chart['git']
print("Clone %s" % chart['git'])
checkout_dir = os.path.basename(chart['git'])
shutil.rmtree(checkout_dir, ignore_errors=True)
branch = "main"
if 'branch' in chart:
branch = chart['branch']
subprocess.run(["git", "init", "--initial-branch", "main", checkout_dir, "--quiet"])
subprocess.run(["git", "-C", checkout_dir, "remote", "add", "origin", chart['git']])
subprocess.run(["git", "-C", checkout_dir, "fetch", "--depth", "1", "origin", branch, "--quiet"])
subprocess.run(["git", "-c", "advice.detachedHead=false", "-C", checkout_dir, "checkout", "FETCH_HEAD", "--quiet"])
if chart.get('mixin'):
cwd = os.getcwd()
source_cwd = chart['cwd']
mixin_file = chart['source']
mixin_dir = cwd + '/' + checkout_dir + '/' + source_cwd + '/'
if os.path.exists(mixin_dir + "jsonnetfile.json"):
print("Running jsonnet-bundler, because jsonnetfile.json exists")
subprocess.run(["jb", "install"], cwd=mixin_dir)
if 'content' in chart:
f = open(mixin_dir + mixin_file, "w")
f.write(chart['content'])
f.close()
print("Generating rules from %s" % mixin_file)
print("Change cwd to %s" % checkout_dir + '/' + source_cwd)
os.chdir(mixin_dir)
alerts = json.loads(_jsonnet.evaluate_snippet(mixin_file, chart['mixin'], import_callback=jsonnet_import_callback))
os.chdir(cwd)
else:
with open(checkout_dir + '/' + chart['source'], "r") as f:
raw_text = f.read()
alerts = yaml.full_load(raw_text)
else:
url = chart['source']
print("Generating rules from %s" % url)
response = requests.get(url)
if response.status_code != 200:
print('Skipping the file, response code %s not equals 200' % response.status_code)
continue
raw_text = response.text
if chart.get('mixin'):
alerts = json.loads(_jsonnet.evaluate_snippet(url, raw_text + '.prometheusAlerts'))
else:
alerts = yaml.full_load(raw_text)
if ('max_kubernetes' not in chart):
chart['max_kubernetes']="9.9.9-9"
# etcd workaround, their file don't have spec level
groups = alerts['spec']['groups'] if alerts.get('spec') else alerts['groups']
for group in groups:
write_group_to_file(group, url, chart['destination'], chart['min_kubernetes'], chart['max_kubernetes'])
# write rules.names named template
write_rules_names_template()
print("Finished")
def sanitize_name(name):
return re.sub('[_]', '-', name).lower()
def jsonnet_import_callback(base, rel):
# rel_base is the path relative to the current cwd.
# see https://github.com/prometheus-community/helm-charts/issues/5283
# for more details.
rel_base = base
if rel_base.startswith(os.getcwd()):
rel_base = base[len(os.getcwd()):]
if "github.com" in rel:
base = os.getcwd() + '/vendor/'
elif "github.com" in rel_base:
base = os.getcwd() + '/vendor/' + rel_base[rel_base.find('github.com'):]
if os.path.isfile(base + rel):
return base + rel, open(base + rel).read().encode('utf-8')
raise RuntimeError('File not found')
if __name__ == '__main__':
main()