Fix Collection Service Helm charts package
[demo.git] / vnfs / DAaaS / prometheus-operator / hack / sync_prometheus_rules.py
1 #!/usr/bin/env python3
2 """Fetch alerting and aggregation rules from provided urls into this chart."""
3 import textwrap
4 from os import makedirs
5
6 import requests
7 import yaml
8 from yaml.representer import SafeRepresenter
9
10
11 # https://stackoverflow.com/a/20863889/961092
12 class LiteralStr(str):
13     pass
14
15
16 def change_style(style, representer):
17     def new_representer(dumper, data):
18         scalar = representer(dumper, data)
19         scalar.style = style
20         return scalar
21
22     return new_representer
23
24
25 # Source files list
26 charts = [
27     {
28         'source': 'https://raw.githubusercontent.com/coreos/prometheus-operator/master/contrib/kube-prometheus/manifests/prometheus-rules.yaml',
29         'destination': '../templates/alertmanager/rules'
30     },
31     # don't uncomment until https://github.com/etcd-io/etcd/pull/10244 is merged
32     # {
33     #     'source': 'https://raw.githubusercontent.com/etcd-io/etcd/master/Documentation/op-guide/etcd3_alert.rules.yml',
34     #     'destination': '../templates/alertmanager/rules'
35     # },
36 ]
37
38 # Additional conditions map
39 condition_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',
45 }
46
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',
56 }
57
58 replacement_map = {
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" }}'},
68 }
69
70 # standard header
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") }}
74 kind: PrometheusRule
75 metadata:
76   name: {{ printf "%%s-%%s" (include "prometheus-operator.fullname" .) "%(name)s" | trunc 63 | trimSuffix "-" }}
77   labels:
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 }}
82 {{- end }}
83 {{- if .Values.defaultRules.annotations }}
84   annotations:
85 {{ toYaml .Values.defaultRules.annotations | indent 4 }}
86 {{- end }}
87 spec:
88   groups:
89   -'''
90
91
92 def init_yaml_styles():
93     represent_literal_str = change_style('|', SafeRepresenter.represent_str)
94     yaml.add_representer(LiteralStr, represent_literal_str)
95
96
97 def escape(s):
98     return s.replace("{{", "{{`{{").replace("}}", "}}`}}")
99
100
101 def fix_expr(rules):
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, |-"""
105     for rule in rules:
106         rule['expr'] = rule['expr'].rstrip()
107         if '\n' in rule['expr']:
108             rule['expr'] = LiteralStr(rule['expr'])
109
110
111 def yaml_str_repr(struct, indent=4):
112     """represent yaml as a string"""
113     text = yaml.dump(
114         struct,
115         width=1000,  # to disable line wrapping
116         default_flow_style=False  # to disable multiple items on single line
117     )
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
120     return text
121
122
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]
130             # add if condition
131             index = rules.index(line_start + alert_name)
132             rules = rules[:index] + rule_text + rules[index:]
133             # add end of if
134             try:
135                 next_index = rules.index(line_start, index + len(rule_text) + 1)
136             except ValueError:
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:]
140     return rules
141
142
143 def write_group_to_file(group, url, destination):
144     fix_expr(group['rules'])
145
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
149     init_line = ''
150     for line in replacement_map:
151         if line in rules:
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)
156     # initialize header
157     lines = header % {
158         'name': group['name'],
159         'url': url,
160         'condition': condition_map.get(group['name'], ''),
161         'init_line': init_line,
162     }
163
164     # rules themselves
165     lines += rules
166
167     # footer
168     lines += '{{- end }}'
169
170     filename = group['name'] + '.yaml'
171     new_filename = "%s/%s" % (destination, filename)
172
173     # make sure directories to store the file exist
174     makedirs(destination, exist_ok=True)
175
176     # recreate the file
177     with open(new_filename, 'w') as f:
178         f.write(lines)
179
180     print("Generated %s" % new_filename)
181
182
183 def main():
184     init_yaml_styles()
185     # read the rules, create a new template file per group
186     for chart in charts:
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']
192         for group in groups:
193             write_group_to_file(group, chart['source'], chart['destination'])
194     print("Finished")
195
196
197 if __name__ == '__main__':
198     main()