2 """Fetch alerting and aggregation rules from provided urls into this chart."""
4 from os import makedirs
8 from yaml.representer import SafeRepresenter
11 # https://stackoverflow.com/a/20863889/961092
12 class LiteralStr(str):
16 def change_style(style, representer):
17 def new_representer(dumper, data):
18 scalar = representer(dumper, data)
22 return new_representer
28 'source': 'https://raw.githubusercontent.com/coreos/prometheus-operator/master/contrib/kube-prometheus/manifests/prometheus-rules.yaml',
29 'destination': '../templates/alertmanager/rules'
31 # don't uncomment until https://github.com/etcd-io/etcd/pull/10244 is merged
33 # 'source': 'https://raw.githubusercontent.com/etcd-io/etcd/master/Documentation/op-guide/etcd3_alert.rules.yml',
34 # 'destination': '../templates/alertmanager/rules'
38 # Additional conditions map
40 'kube-apiserver.rules': ' .Values.kubeApiServer.enabled',
41 'kube-scheduler.rules': ' .Values.kubeScheduler.enabled',
42 'node.rules': ' .Values.nodeExporter.enabled',
43 'kubernetes-apps': ' .Values.kubeStateMetrics.enabled',
44 'etcd': ' .Values.kubeEtcd.enabled',
47 alert_condition_map = {
48 'KubeAPIDown': '.Values.kubeApiServer.enabled', # there are more alerts which are left enabled, because they'll never fire without metrics
49 'KubeControllerManagerDown': '.Values.kubeControllerManager.enabled',
50 'KubeSchedulerDown': '.Values.kubeScheduler.enabled',
51 'KubeStateMetricsDown': '.Values.kubeStateMetrics.enabled', # there are more alerts which are left enabled, because they'll never fire without metrics
52 'KubeletDown': '.Values.prometheusOperator.kubeletService.enabled', # there are more alerts which are left enabled, because they'll never fire without metrics
53 'PrometheusOperatorDown': '.Values.prometheusOperator.enabled',
54 'NodeExporterDown': '.Values.nodeExporter.enabled',
55 'CoreDNSDown': '.Values.kubeDns.enabled',
59 'job="prometheus-operator"': {
60 'replacement': 'job="{{ $operatorJob }}"',
61 'init': '{{- $operatorJob := printf "%s-%s" (include "prometheus-operator.fullname" .) "operator" }}'},
62 'job="prometheus-k8s"': {
63 'replacement': 'job="{{ $prometheusJob }}"',
64 'init': '{{- $prometheusJob := printf "%s-%s" (include "prometheus-operator.fullname" .) "prometheus" }}'},
65 'job="alertmanager-main"': {
66 'replacement': 'job="{{ $alertmanagerJob }}"',
67 'init': '{{- $alertmanagerJob := printf "%s-%s" (include "prometheus-operator.fullname" .) "alertmanager" }}'},
71 header = '''# Generated from '%(name)s' group from %(url)s
72 {{- if and .Values.defaultRules.create%(condition)s }}%(init_line)s
73 apiVersion: {{ printf "%%s/v1" (.Values.prometheusOperator.crdApiGroup | default "monitoring.coreos.com") }}
76 name: {{ printf "%%s-%%s" (include "prometheus-operator.fullname" .) "%(name)s" | trunc 63 | trimSuffix "-" }}
78 app: {{ template "prometheus-operator.name" . }}
79 {{ include "prometheus-operator.labels" . | indent 4 }}
80 {{- if .Values.defaultRules.labels }}
81 {{ toYaml .Values.defaultRules.labels | indent 4 }}
83 {{- if .Values.defaultRules.annotations }}
85 {{ toYaml .Values.defaultRules.annotations | indent 4 }}
92 def init_yaml_styles():
93 represent_literal_str = change_style('|', SafeRepresenter.represent_str)
94 yaml.add_representer(LiteralStr, represent_literal_str)
98 return s.replace("{{", "{{`{{").replace("}}", "}}`}}")
102 """Remove trailing whitespaces and line breaks, which happen to creep in
103 due to yaml import specifics;
104 convert multiline expressions to literal style, |-"""
106 rule['expr'] = rule['expr'].rstrip()
107 if '\n' in rule['expr']:
108 rule['expr'] = LiteralStr(rule['expr'])
111 def yaml_str_repr(struct, indent=4):
112 """represent yaml as a string"""
115 width=1000, # to disable line wrapping
116 default_flow_style=False # to disable multiple items on single line
118 text = escape(text) # escape {{ and }} for helm
119 text = textwrap.indent(text, ' ' * indent)[indent - 1:] # indent everything, and remove very first line extra indentation
123 def add_rules_conditions(rules, indent=4):
124 """Add if wrapper for rules, listed in alert_condition_map"""
125 rule_condition = '{{- if %s }}\n'
126 for alert_name in alert_condition_map:
127 line_start = ' ' * indent + '- alert: '
128 if line_start + alert_name in rules:
129 rule_text = rule_condition % alert_condition_map[alert_name]
131 index = rules.index(line_start + alert_name)
132 rules = rules[:index] + rule_text + rules[index:]
135 next_index = rules.index(line_start, index + len(rule_text) + 1)
137 # we found the last alert in file if there are no alerts after it
138 next_index = len(rules)
139 rules = rules[:next_index] + '{{- end }}\n' + rules[next_index:]
143 def write_group_to_file(group, url, destination):
144 fix_expr(group['rules'])
146 # prepare rules string representation
147 rules = yaml_str_repr(group)
148 # add replacements of custom variables and include their initialisation in case it's needed
150 for line in replacement_map:
152 rules = rules.replace(line, replacement_map[line]['replacement'])
153 init_line += '\n' + replacement_map[line]['init']
154 # append per-alert rules
155 rules = add_rules_conditions(rules)
158 'name': group['name'],
160 'condition': condition_map.get(group['name'], ''),
161 'init_line': init_line,
168 lines += '{{- end }}'
170 filename = group['name'] + '.yaml'
171 new_filename = "%s/%s" % (destination, filename)
173 # make sure directories to store the file exist
174 makedirs(destination, exist_ok=True)
177 with open(new_filename, 'w') as f:
180 print("Generated %s" % new_filename)
185 # read the rules, create a new template file per group
187 print("Generating rules from %s" % chart['source'])
188 raw_text = requests.get(chart['source']).text
189 yaml_text = yaml.load(raw_text)
190 # etcd workaround, their file don't have spec level
191 groups = yaml_text['spec']['groups'] if yaml_text.get('spec') else yaml_text['groups']
193 write_group_to_file(group, chart['source'], chart['destination'])
197 if __name__ == '__main__':