diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 70bae43b8d9a925bcbc011674e6001b545bdcc68..fa59dd27f480ba5ddf64cb5d7995eadc4b67c194 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -31,5 +31,7 @@ docker build: name: gcr.io/kaniko-project/executor:debug entrypoint: [""] script: - - echo "{\"auths\":{\"registry.kszk.bme.hu\":{\"username\":\"$CI_REG_USER\",\"password\":\"$CI_REG_PASS\"}}}" > /kaniko/.docker/config.json + - echo "{\"auths\":{\"registry.kszk.bme.hu\":{\"username\":\"$KSZK_NEXUS_USERNAME\",\"password\":\"$KSZK_NEXUS_PASSWORD\"}}}" > /kaniko/.docker/config.json - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination "$CONTAINER_IMAGE" + only: + - master diff --git a/build.py b/build.py index eca9a80fe89cddb620cc880e95f129be78f3c4ba..ef2e1e72e1de7869b49a30b53ff2a0cc388ad42c 100644 --- a/build.py +++ b/build.py @@ -1,12 +1,14 @@ """Fill YAML-jinja2 templates, prepare the final data-model""" import logging +import sys import jinja2 import yaml +from yaml.scanner import ScannerError from config import Config -from constants import APP_LOG_TAG, TEMPLATE_PREFIX -from helper import collect_snippets, check_true +from constants import APP_LOG_TAG, EXPORTER_PREFIX, ALERT_GROUP_PREFIX +from transformation import Transformation class ConfigBuilder: @@ -15,32 +17,85 @@ class ConfigBuilder: def __init__(self, cfg: Config): self.logger = logging.getLogger(APP_LOG_TAG) self.config = cfg + self.config.scrape_configs = [] + self.config.alert_groups = [] def build(self): """Fill YAML-jinja2 templates, prepare the final data-model""" - for service in self.config.service_definitions: - for job in service['scraping']: - self.logger.debug("Templating: " + service['service_name'] + ' -> ' + job['template']) - self.load_template(job) - output = self.substitute_template_field(job, service) - - # store generated prometheus job config - if 'ignore' in job and check_true(job['ignore']): - job['output_yaml'] = None - else: - job['output_yaml'] = yaml.safe_load(output) - - def load_template(self, job): - """Substitute 'template' field with its content""" - template_identifier = TEMPLATE_PREFIX + job['template'] - job['template'] = self.config.templates[template_identifier] - - def substitute_template_field(self, job, service): - """Fill template field with data""" - env = jinja2.Environment(undefined=jinja2.DebugUndefined) - template = env.from_string(job['template']) + self.logger.info("Build config structures") + for service_def in self.config.service_definitions: + self.logger.debug("Templating service=" + service_def['service_name']) + self.build_scraping_config(service_def) + self.build_alerting_config(service_def) + + def build_scraping_config(self, service_def): + """Prepare the final data-model for scraping""" + for exporter in service_def['scraping']: + self.logger.debug("Templating scraping service=" + service_def['service_name'] + + ', exporter=' + exporter['exporter']) + output_yaml = self.substitute_exporter_field(exporter, service_def) + transform = Transformation(self.config) + transform.transformate_scraping_job(exporter, service_def, output_yaml) + if transform.is_ignored_job(exporter): + self.logger.info("Ignore job: " + exporter['name'] + ' [' + exporter['ignore'] + ']') + continue + # store generated prometheus job config + self.config.scrape_configs.append(output_yaml) + + def build_alerting_config(self, service_def): + """Prepare the final data-model for alerting""" + if 'alerting' not in service_def or 'group' not in service_def['alerting']: + self.logger.warning("No alerting definition for service=" + service_def['service_name']) + return + else: + self.logger.debug("Templating alerting for service=" + service_def['service_name']) + + for alert_group in service_def['alerting']['group']: + self.substitute_alert_group(alert_group, service_def) + transform = Transformation(self.config) + transform.transformate_alerting_group(alert_group, service_def) + self.config.alert_groups.append(alert_group['alerts']) + + def substitute_exporter_field(self, job, service): + """Substitute 'exporter' field with its content""" + include_field_name = 'exporter' + template_identifier = EXPORTER_PREFIX + job[include_field_name] + job['exporter_name'] = job[include_field_name] + data = service.copy() data['job'] = job - data['snippet'] = collect_snippets(self.config.templates) + data['snippets'] = self.config.snippets + + yaml_data = self.substitute_field(template_identifier, include_field_name, job, data) + return yaml_data + + def substitute_alert_group(self, alert_group, service): + """Substitute 'alerts' field with its content""" + include_field_name = 'alerts' + template_identifier = ALERT_GROUP_PREFIX + alert_group[include_field_name] + alert_group['alert_name'] = alert_group[include_field_name] + + data = service.copy() + data['alert_group'] = alert_group + data['snippets'] = self.config.snippets + yaml_data = self.substitute_field(template_identifier, include_field_name, alert_group, data) + alert_group[include_field_name] = yaml_data['group'] + + def substitute_field(self, template_identifier, field_name, parent_object, data): + """Substitute field with jinja2 content""" + parent_object[field_name] = self.config.templates[template_identifier] + + """Fill template field with data""" + env = jinja2.Environment(undefined=jinja2.DebugUndefined) + template = env.from_string(parent_object[field_name]) + yaml_as_text = template.render(data) - return template.render(data) + service_name = data['service_name'] + try: + yaml_data = yaml.safe_load(yaml_as_text) + return yaml_data + except ScannerError as e: + self.logger.fatal("Cannot parse intermediate YAML data (service_name=" + service_name + "):") + self.logger.fatal(yaml_as_text) + self.logger.fatal(e) + sys.exit(42) diff --git a/config.py b/config.py index 12370595c7e23636ceefeb6f6f7aa7689c216934..e6e28f97d3ce10368ba1f7a58bea7391fc07fa3c 100644 --- a/config.py +++ b/config.py @@ -1,47 +1,58 @@ -"""Read service YAML files and preload templates""" +"""Read service, template and snippet files (without templating)""" import logging from pathlib import Path import yaml -from constants import APP_LOG_TAG -from helper import is_service_file, is_template_file +from constants import APP_LOG_TAG, SERVICE_PREFIX, TEMPLATE_PREFIX, SNIPPET_PREFIX class Config: - """Read service YAML files and preload templates""" + """Read service, template and snippet files (without templating)""" service_definitions = [] templates = {} + snippets = {} def __init__(self, path: Path): self.logger = logging.getLogger(APP_LOG_TAG) # YAML files in tha data directory recursively base_path_len = len(str(path.absolute())) + 1 - self.preload_service_files(path.rglob('*.yaml'), base_path_len) - self.preload_template_files(path.rglob('*.yaml.j2'), base_path_len) + self.read_service_files(path.rglob('*.yaml'), base_path_len) + self.read_template_files(path.rglob('*.yaml.j2'), base_path_len) + self.read_snippet_files(path.rglob('*.yaml.j2'), base_path_len) - def preload_service_files(self, path_glob, base_path_len: int): - """Read all (service)YAML files in tha data directory recursively""" + def read_service_files(self, path_glob, base_path_len: int): + """Read all service YAML files in tha data directory recursively""" + self.logger.info("Read service YAML files") for yaml_path in path_glob: file_identifier = str(yaml_path.absolute())[base_path_len:-len(".yaml")] - if is_service_file(file_identifier): - self.service_definitions.append(self.read_yaml_file(yaml_path)) - - def preload_template_files(self, path_glob, base_path_len: int): - """Read all (template)YAML files in tha data directory recursively""" + if file_identifier.startswith(SERVICE_PREFIX): + self.logger.debug("Load service file: " + str(yaml_path.absolute())) + with yaml_path.open('r') as stream: + try: + service_yaml = yaml.safe_load(stream) + self.service_definitions.append(service_yaml) + except yaml.YAMLError as exc: + self.logger.error("Cannot load service YAML file.") + self.logger.error(exc) + + def read_template_files(self, path_glob, base_path_len: int): + """Read all template files in tha data directory recursively""" + self.logger.info("Read template files (without parsing)") for yaml_path in path_glob: file_identifier = str(yaml_path.absolute())[base_path_len:-len(".yaml.j2")] - if is_template_file(file_identifier): + if file_identifier.startswith(TEMPLATE_PREFIX): data = open(str(yaml_path.absolute()), "r") self.logger.debug("Load template file: " + str(yaml_path.absolute())) self.templates[file_identifier] = data.read() - def read_yaml_file(self, yaml_path: Path): - """Reads a YAML file""" - self.logger.debug("Load YAML file: " + str(yaml_path.absolute())) - with yaml_path.open('r') as stream: - try: - return yaml.safe_load(stream) - except yaml.YAMLError as exc: - self.logger.error("Cannot load YAML file.") - self.logger.error(exc) + def read_snippet_files(self, path_glob, base_path_len: int): + """Read all snippet files in tha data directory recursively""" + self.logger.info("Read snippet files (without parsing)") + for yaml_path in path_glob: + file_identifier = str(yaml_path.absolute())[base_path_len:-len(".yaml.j2")] + if file_identifier.startswith(SNIPPET_PREFIX): + data = open(str(yaml_path.absolute()), "r") + self.logger.debug("Load snippet file: " + str(yaml_path.absolute())) + snippet_name = file_identifier[len(SNIPPET_PREFIX):] + self.snippets[snippet_name] = data.read() diff --git a/constants.py b/constants.py index 4b59614e2518ef7602eebbbd9ba00d9c8c5630af..906ac96929df53e5da5ce59a8d1cda55a3219865 100644 --- a/constants.py +++ b/constants.py @@ -4,4 +4,7 @@ GENERATOR_OUTPUT_FOLDER = 'generated/' OUTPUT_TEMPLATE_FOLDER = 'output-templates/' SERVICE_PREFIX = 'services/' TEMPLATE_PREFIX = 'service-templates/' -SNIPPET_PREFIX = 'service-templates/snippet/' +EXPORTER_PREFIX = 'service-templates/exporters/' +INDIVIDUAL_ALERT_PREFIX = 'service-templates/alerting/individual/' +ALERT_GROUP_PREFIX = 'service-templates/alerting/group/' +SNIPPET_PREFIX = 'service-templates/snippets/' diff --git a/generator.py b/generator.py index 873e12e9a5c5bc46a5adacde122f9836078034bc..cadd9aae51490d0fc7c77726ed8771fe06ae4363 100644 --- a/generator.py +++ b/generator.py @@ -10,7 +10,6 @@ import yaml from config import Config from constants import APP_LOG_TAG, OUTPUT_TEMPLATE_FOLDER, GENERATOR_OUTPUT_FOLDER -from helper import collect_snippets def autogen_warning(): @@ -35,9 +34,11 @@ class Generator: data = { 'autogen_warning': autogen_warning(), 'generation_info': self.generation_info(), - 'snippet': collect_snippets(self.config.templates), + 'snippets': self.config.snippets, } self.collect_scrape_configs(data) + self.collect_alert_groups(data) + self.collect_alertmanager_receivers(data) self.generate_files(data) def generation_info(self): @@ -56,13 +57,28 @@ class Generator: def collect_scrape_configs(self, output): """Build data model to the generation""" - data = [] - for service in self.config.service_definitions: - for job in service['scraping']: - if job['output_yaml'] is not None: - data.append(job['output_yaml']) output['scrape_configs'] = yaml.dump( - {'scrape_configs': data}, + {'scrape_configs': self.config.scrape_configs}, + explicit_start=False, + default_flow_style=False + ) + + def collect_alert_groups(self, output): + """Build data model to the generation""" + alert_groups = self.config.alert_groups + + output['alert_groups'] = yaml.dump( + {'groups': alert_groups}, + explicit_start=False, + default_flow_style=False + ) + + def collect_alertmanager_receivers(self, output): + """Build data model to the generation""" + alertmanager_receivers = 'alertmanager_receivers' + + output['alertmanager_receivers'] = yaml.dump( + {'receivers': alertmanager_receivers}, explicit_start=False, default_flow_style=False ) diff --git a/helper.py b/helper.py index 48c3064feb007488f8c05e76ffdf57276ae849aa..c7a9b2f83f790955af0f282cb3a2f8ef3f63cda9 100644 --- a/helper.py +++ b/helper.py @@ -1,22 +1,5 @@ """helper methods""" -from constants import SERVICE_PREFIX, TEMPLATE_PREFIX, SNIPPET_PREFIX - - -def is_service_file(file_identifier: str) -> bool: - """Hmm, is it a service definition file or not""" - return file_identifier.startswith(SERVICE_PREFIX) - - -def is_template_file(file_identifier: str) -> bool: - """Hmm, is it a service template file or not""" - return file_identifier.startswith(TEMPLATE_PREFIX) - - -def is_snippet_file(file_identifier: str) -> bool: - """Hmm, is it a snippet template file or not""" - return file_identifier.startswith(SNIPPET_PREFIX) - def check_true(s: str) -> bool: """Hmm, is it a true-ish string""" @@ -25,13 +8,3 @@ def check_true(s: str) -> bool: 'i', 'igen', 'aha', 'ja', 'jaja', 'nosza', 'mibajlehet', 'miƩrt ne', # Hungarian extension 'temp', 'temporary', 'ideiglenesen' # other extension ] - - -def collect_snippets(templates): - """Get snippets from templates""" - snippets = {} - for identifier in templates: - if is_snippet_file(identifier): - snippet_name = identifier[len(SNIPPET_PREFIX):] - snippets[snippet_name] = templates[identifier] - return snippets diff --git a/pupak.py b/pupak.py index c53c4833339256a54bd3ef4d723fc22ccbfdb441..16d5f80353819afcd298784fa8bb14db0df20064 100644 --- a/pupak.py +++ b/pupak.py @@ -21,9 +21,10 @@ if __name__ == "__main__": logger.error("Usage: data_folder") sys.exit(1) + # the base path (read YAML files from) data_folder = Path(sys.argv[1]) - # Read service YAML files and preload templates + # Read service, template and snippet files (without templating) config = Config(data_folder) # Fill YAML-jinja2 templates, diff --git a/transformation.py b/transformation.py new file mode 100644 index 0000000000000000000000000000000000000000..48f758eb0df9c1667c2d708cf4ab36258716996b --- /dev/null +++ b/transformation.py @@ -0,0 +1,53 @@ +"""Custom transformation of data-model""" +import logging + +from config import Config +from constants import APP_LOG_TAG +from helper import check_true + + +class Transformation: + """Custom transformation of data-model""" + + def __init__(self, cfg: Config): + self.logger = logging.getLogger(APP_LOG_TAG) + self.config = cfg + + def transformate_scraping_job(self, job, service, output_yaml): + """Modify job's data to be suitable for templating""" + self.add_job_name(job, service, output_yaml) + self.add_labels(job, service, output_yaml) + + def add_job_name(self, job, service, output_yaml): + """Add job_name property to prometheus job definitions""" + if 'job_name' not in job: + module_name = '' + if 'params' in output_yaml and 'module' in output_yaml['params']: + module_name = '#' + output_yaml['params']['module'][0] + job['name'] = service['service_name'] + '@' + job['exporter_name'] + module_name + output_yaml['job_name'] = job['name'] + + def add_labels(self, job, service, output_yaml): + """Add labels e.g. job_name to prometheus job definitions""" + for static_configs in output_yaml['static_configs']: + if 'labels' not in static_configs: + static_configs['labels'] = {} + static_configs['labels']['job'] = job['exporter_name'] # TODO check is this intended< + # static_configs['labels']['exporter'] = job['exporter_name'] + static_configs['labels']['service_name'] = service['service_name'] + + def is_ignored_job(self, job): + """Check ignored property""" + return 'ignore' in job and check_true(job['ignore']) + + def transformate_alerting_group(self, alert_group, service_def): + """Modify alert def's data to be suitable for templating""" + self.add_alert_group_name(alert_group, service_def) + + def add_alert_group_name(self, alert_group, service_def): + """Add name property to prometheus alert definition""" + if 'name' not in alert_group['alerts']: + name = service_def["service_name"] + name += "@" + name += alert_group["alert_name"].replace("/", "-") + alert_group['alerts']['name'] = name