399 lines
17 KiB
Python
399 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""Fetch dashboards 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 = [
|
|
{
|
|
'source': '../files/dashboards/k8s-coredns.json',
|
|
'destination': '../templates/grafana/dashboards-1.14',
|
|
'type': 'dashboard_json',
|
|
'min_kubernetes': '1.14.0-0',
|
|
'multicluster_key': '.Values.grafana.sidecar.dashboards.multicluster.global.enabled',
|
|
},
|
|
{
|
|
'source': 'https://raw.githubusercontent.com/prometheus-operator/kube-prometheus/%s/manifests/grafana-dashboardDefinitions.yaml' % (refs['ref.kube-prometheus'],),
|
|
'destination': '../templates/grafana/dashboards-1.14',
|
|
'type': 'yaml',
|
|
'min_kubernetes': '1.14.0-0',
|
|
'multicluster_key': '.Values.grafana.sidecar.dashboards.multicluster.global.enabled',
|
|
},
|
|
{
|
|
'git': 'https://github.com/kubernetes-monitoring/kubernetes-mixin.git',
|
|
'branch': refs['ref.kubernetes-mixin'],
|
|
'content': "(import 'dashboards/windows.libsonnet') + (import 'config.libsonnet') + { _config+:: { windowsExporterSelector: 'job=\"windows-exporter\"', }}",
|
|
'cwd': '.',
|
|
'destination': '../templates/grafana/dashboards-1.14',
|
|
'min_kubernetes': '1.14.0-0',
|
|
'type': 'jsonnet_mixin',
|
|
'mixin_vars': {},
|
|
'multicluster_key': '.Values.grafana.sidecar.dashboards.multicluster.global.enabled',
|
|
},
|
|
{
|
|
'git': 'https://github.com/etcd-io/etcd.git',
|
|
'branch': refs['ref.etcd'],
|
|
'source': 'mixin.libsonnet',
|
|
'cwd': 'contrib/mixin',
|
|
'destination': '../templates/grafana/dashboards-1.14',
|
|
'min_kubernetes': '1.14.0-0',
|
|
'type': 'jsonnet_mixin',
|
|
'mixin_vars': {'_config+': {}},
|
|
'multicluster_key': '(or .Values.grafana.sidecar.dashboards.multicluster.global.enabled .Values.grafana.sidecar.dashboards.multicluster.etcd.enabled)'
|
|
},
|
|
]
|
|
|
|
# Additional conditions map
|
|
condition_map = {
|
|
'alertmanager-overview': ' (or .Values.alertmanager.enabled .Values.alertmanager.forceDeployDashboards)',
|
|
'grafana-coredns-k8s': ' .Values.coreDns.enabled',
|
|
'etcd': ' .Values.kubeEtcd.enabled',
|
|
'apiserver': ' .Values.kubeApiServer.enabled',
|
|
'controller-manager': ' .Values.kubeControllerManager.enabled',
|
|
'kubelet': ' .Values.kubelet.enabled',
|
|
'proxy': ' .Values.kubeProxy.enabled',
|
|
'scheduler': ' .Values.kubeScheduler.enabled',
|
|
'node-rsrc-use': ' (or .Values.nodeExporter.enabled .Values.nodeExporter.forceDeployDashboards)',
|
|
'node-cluster-rsrc-use': ' (or .Values.nodeExporter.enabled .Values.nodeExporter.forceDeployDashboards)',
|
|
'nodes': ' (and (or .Values.nodeExporter.enabled .Values.nodeExporter.forceDeployDashboards) .Values.nodeExporter.operatingSystems.linux.enabled)',
|
|
'nodes-aix': ' (and (or .Values.nodeExporter.enabled .Values.nodeExporter.forceDeployDashboards) .Values.nodeExporter.operatingSystems.aix.enabled)',
|
|
'nodes-darwin': ' (and (or .Values.nodeExporter.enabled .Values.nodeExporter.forceDeployDashboards) .Values.nodeExporter.operatingSystems.darwin.enabled)',
|
|
'prometheus-remote-write': ' .Values.prometheus.prometheusSpec.remoteWriteDashboards',
|
|
'k8s-coredns': ' .Values.coreDns.enabled',
|
|
'k8s-windows-cluster-rsrc-use': ' .Values.windowsMonitoring.enabled',
|
|
'k8s-windows-node-rsrc-use': ' .Values.windowsMonitoring.enabled',
|
|
'k8s-resources-windows-cluster': ' .Values.windowsMonitoring.enabled',
|
|
'k8s-resources-windows-namespace': ' .Values.windowsMonitoring.enabled',
|
|
'k8s-resources-windows-pod': ' .Values.windowsMonitoring.enabled',
|
|
}
|
|
|
|
replacement_map = {
|
|
'var-namespace=$__cell_1': {
|
|
'replacement': 'var-namespace=`}}{{ if .Values.grafana.sidecar.dashboards.enableNewTablePanelSyntax }}${__data.fields.namespace}{{ else }}$__cell_1{{ end }}{{`',
|
|
},
|
|
'var-type=$__cell_2': {
|
|
'replacement': 'var-type=`}}{{ if .Values.grafana.sidecar.dashboards.enableNewTablePanelSyntax }}${__data.fields.workload_type}{{ else }}$__cell_2{{ end }}{{`',
|
|
},
|
|
'=$__cell': {
|
|
'replacement': '=`}}{{ if .Values.grafana.sidecar.dashboards.enableNewTablePanelSyntax }}${__value.text}{{ else }}$__cell{{ end }}{{`',
|
|
},
|
|
'job=\\"prometheus-k8s\\",namespace=\\"monitoring\\"': {
|
|
'replacement': '',
|
|
},
|
|
}
|
|
|
|
# standard header
|
|
header = '''{{- /*
|
|
Generated from '%(name)s' 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 (or .Values.grafana.enabled .Values.grafana.forceDeployDashboards) (semverCompare ">=%(min_kubernetes)s" $kubeTargetVersion) (semverCompare "<%(max_kubernetes)s" $kubeTargetVersion) .Values.grafana.defaultDashboardsEnabled%(condition)s }}
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
namespace: {{ template "kube-prometheus-stack-grafana.namespace" . }}
|
|
name: {{ printf "%%s-%%s" (include "kube-prometheus-stack.fullname" $) "%(name)s" | trunc 63 | trimSuffix "-" }}
|
|
annotations:
|
|
{{ toYaml .Values.grafana.sidecar.dashboards.annotations | indent 4 }}
|
|
labels:
|
|
{{- if $.Values.grafana.sidecar.dashboards.label }}
|
|
{{ $.Values.grafana.sidecar.dashboards.label }}: {{ ternary $.Values.grafana.sidecar.dashboards.labelValue "1" (not (empty $.Values.grafana.sidecar.dashboards.labelValue)) | quote }}
|
|
{{- end }}
|
|
app: {{ template "kube-prometheus-stack.name" $ }}-grafana
|
|
{{ include "kube-prometheus-stack.labels" $ | indent 4 }}
|
|
data:
|
|
'''
|
|
|
|
# Add GrafanaDashboard custom resource
|
|
grafana_dashboard_operator = """
|
|
---
|
|
{{- if and .Values.grafana.operator.dashboardsConfigMapRefEnabled (or .Values.grafana.enabled .Values.grafana.forceDeployDashboards) (semverCompare ">=%(min_kubernetes)s" $kubeTargetVersion) (semverCompare "<%(max_kubernetes)s" $kubeTargetVersion) .Values.grafana.defaultDashboardsEnabled%(condition)s }}
|
|
apiVersion: grafana.integreatly.org/v1beta1
|
|
kind: GrafanaDashboard
|
|
metadata:
|
|
name: {{ printf "%%s-%%s" (include "kube-prometheus-stack.fullname" $) "%(name)s" | trunc 63 | trimSuffix "-" }}
|
|
namespace: {{ template "kube-prometheus-stack-grafana.namespace" . }}
|
|
{{ with .Values.grafana.operator.annotations }}
|
|
annotations:
|
|
{{- toYaml . | nindent 4 }}
|
|
{{ end }}
|
|
labels:
|
|
app: {{ template "kube-prometheus-stack.name" $ }}-grafana
|
|
spec:
|
|
allowCrossNamespaceImport: true
|
|
resyncPeriod: {{ .Values.grafana.operator.resyncPeriod | quote | default "10m" }}
|
|
folder: {{ .Values.grafana.operator.folder | quote }}
|
|
instanceSelector:
|
|
matchLabels:
|
|
{{- if .Values.grafana.operator.matchLabels }}
|
|
{{- toYaml .Values.grafana.operator.matchLabels | nindent 6 }}
|
|
{{- else }}
|
|
{{- fail "grafana.operator.matchLabels must be specified when grafana.operator.dashboardsConfigMapRefEnabled is true" }}
|
|
{{- end }}
|
|
configMapRef:
|
|
name: {{ printf "%%s-%%s" (include "kube-prometheus-stack.fullname" $) "%(name)s" | trunc 63 | trimSuffix "-" }}
|
|
key: %(name)s.json
|
|
{{- end }}
|
|
"""
|
|
|
|
def init_yaml_styles():
|
|
represent_literal_str = change_style('|', SafeRepresenter.represent_str)
|
|
yaml.add_representer(LiteralStr, represent_literal_str)
|
|
|
|
|
|
def yaml_str_repr(struct, indent=2):
|
|
"""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 = textwrap.indent(text, ' ' * indent)
|
|
return text
|
|
|
|
|
|
def replace_nested_key(data, key, value, replace):
|
|
if isinstance(data, dict):
|
|
return {
|
|
k: replace if k == key and v == value else replace_nested_key(v, key, value, replace)
|
|
for k, v in data.items()
|
|
}
|
|
elif isinstance(data, list):
|
|
return [replace_nested_key(v, key, value, replace) for v in data]
|
|
else:
|
|
return data
|
|
|
|
|
|
def patch_dashboards_json(content, multicluster_key):
|
|
try:
|
|
content_struct = json.loads(content)
|
|
|
|
# multicluster
|
|
overwrite_list = []
|
|
for variable in content_struct['templating']['list']:
|
|
if variable['name'] == 'cluster':
|
|
variable['allValue'] = '.*'
|
|
variable['hide'] = ':multicluster:'
|
|
overwrite_list.append(variable)
|
|
content_struct['templating']['list'] = overwrite_list
|
|
|
|
# Replace decimals=-1 with decimals= (nil value)
|
|
# ref: https://github.com/kubernetes-monitoring/kubernetes-mixin/pull/859
|
|
content_struct = replace_nested_key(content_struct, "decimals", -1, None)
|
|
|
|
content = json.dumps(content_struct, separators=(',', ':'))
|
|
content = content.replace('":multicluster:"', '`}}{{ if %s }}0{{ else }}2{{ end }}{{`' % multicluster_key,)
|
|
|
|
for line in replacement_map:
|
|
content = content.replace(line, replacement_map[line]['replacement'])
|
|
except (ValueError, KeyError):
|
|
pass
|
|
|
|
return "{{`" + content + "`}}"
|
|
|
|
|
|
def patch_json_set_timezone_as_variable(content):
|
|
# content is no more in json format, so we have to replace using regex
|
|
return re.sub(r'"timezone"\s*:\s*"(?:\\.|[^\"])*"', '"timezone": "`}}{{ .Values.grafana.defaultDashboardsTimezone }}{{`"', content, flags=re.IGNORECASE)
|
|
|
|
|
|
def patch_json_set_editable_as_variable(content):
|
|
# content is no more in json format, so we have to replace using regex
|
|
return re.sub(r'"editable"\s*:\s*(?:true|false)', '"editable":`}}{{ .Values.grafana.defaultDashboardsEditable }}{{`', content, flags=re.IGNORECASE)
|
|
|
|
|
|
def patch_json_set_interval_as_variable(content):
|
|
# content is no more in json format, so we have to replace using regex
|
|
return re.sub(r'"interval"\s*:\s*"(?:\\.|[^\"])*"', '"interval":"`}}{{ .Values.grafana.defaultDashboardsInterval }}{{`"', content, flags=re.IGNORECASE)
|
|
|
|
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')
|
|
|
|
|
|
def write_group_to_file(resource_name, content, url, destination, min_kubernetes, max_kubernetes, multicluster_key):
|
|
# initialize header
|
|
lines = header % {
|
|
'name': resource_name,
|
|
'url': url,
|
|
'condition': condition_map.get(resource_name, ''),
|
|
'min_kubernetes': min_kubernetes,
|
|
'max_kubernetes': max_kubernetes
|
|
}
|
|
|
|
content = patch_dashboards_json(content, multicluster_key)
|
|
content = patch_json_set_timezone_as_variable(content)
|
|
content = patch_json_set_editable_as_variable(content)
|
|
content = patch_json_set_interval_as_variable(content)
|
|
|
|
filename_struct = {resource_name + '.json': (LiteralStr(content))}
|
|
# rules themselves
|
|
lines += yaml_str_repr(filename_struct)
|
|
|
|
# footer
|
|
lines += '{{- end }}'
|
|
|
|
lines_grafana_operator = grafana_dashboard_operator % {
|
|
'name': resource_name,
|
|
'condition': condition_map.get(resource_name, ''),
|
|
'min_kubernetes': min_kubernetes,
|
|
'max_kubernetes': max_kubernetes
|
|
}
|
|
|
|
lines += lines_grafana_operator
|
|
|
|
filename = resource_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 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"])
|
|
print("Generating rules from %s" % chart['source'])
|
|
|
|
mixin_file = chart['source']
|
|
mixin_dir = checkout_dir + '/' + chart['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()
|
|
|
|
mixin_vars = json.dumps(chart['mixin_vars'])
|
|
|
|
cwd = os.getcwd()
|
|
os.chdir(mixin_dir)
|
|
raw_text = '((import "%s") + %s)' % (mixin_file, mixin_vars)
|
|
source = os.path.basename(mixin_file)
|
|
elif 'source' in chart and chart['source'].startswith('http'):
|
|
print("Generating rules from %s" % chart['source'])
|
|
response = requests.get(chart['source'])
|
|
if response.status_code != 200:
|
|
print('Skipping the file, response code %s not equals 200' % response.status_code)
|
|
continue
|
|
raw_text = response.text
|
|
source = chart['source']
|
|
url = chart['source']
|
|
else:
|
|
with open(chart['source']) as f:
|
|
raw_text = f.read()
|
|
|
|
source = chart['source']
|
|
url = chart['source']
|
|
|
|
if ('max_kubernetes' not in chart):
|
|
chart['max_kubernetes']="9.9.9-9"
|
|
|
|
if chart['type'] == 'yaml':
|
|
yaml_text = yaml.full_load(raw_text)
|
|
groups = yaml_text['items']
|
|
for group in groups:
|
|
for resource, content in group['data'].items():
|
|
write_group_to_file(resource.replace('.json', ''), content, url, chart['destination'], chart['min_kubernetes'], chart['max_kubernetes'], chart['multicluster_key'])
|
|
elif chart['type'] == 'jsonnet_mixin':
|
|
json_text = json.loads(_jsonnet.evaluate_snippet(source, raw_text + '.grafanaDashboards', import_callback=jsonnet_import_callback))
|
|
|
|
if 'git' in chart:
|
|
os.chdir(cwd)
|
|
# is it already a dashboard structure or is it nested (etcd case)?
|
|
flat_structure = bool(json_text.get('annotations'))
|
|
if flat_structure:
|
|
resource = os.path.basename(chart['source']).replace('.json', '')
|
|
write_group_to_file(resource, json.dumps(json_text, indent=4), url, chart['destination'], chart['min_kubernetes'], chart['max_kubernetes'], chart['multicluster_key'])
|
|
else:
|
|
for resource, content in json_text.items():
|
|
write_group_to_file(resource.replace('.json', ''), json.dumps(content, indent=4), url, chart['destination'], chart['min_kubernetes'], chart['max_kubernetes'], chart['multicluster_key'])
|
|
elif chart['type'] == 'dashboard_json':
|
|
write_group_to_file(os.path.basename(source).replace('.json', ''),
|
|
raw_text, url, chart['destination'], chart['min_kubernetes'],
|
|
chart['max_kubernetes'], chart['multicluster_key'])
|
|
|
|
|
|
print("Finished")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|