X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=installers%2Fcharm%2Fprometheus%2Fsrc%2Fcharm.py;h=3dcb5d412cfef1d4f8c979ea4cad4c9f7cd543c8;hb=3ddbbd1f6c70306d13db0976e1e6b3bda0c69abd;hp=4371d47ab2cca53058b80a7d1fc0eb2160cdb9a7;hpb=2459af695441e460e64daea40157c0fd5a78e2c3;p=osm%2Fdevops.git diff --git a/installers/charm/prometheus/src/charm.py b/installers/charm/prometheus/src/charm.py index 4371d47a..3dcb5d41 100755 --- a/installers/charm/prometheus/src/charm.py +++ b/installers/charm/prometheus/src/charm.py @@ -20,175 +20,225 @@ # osm-charmers@lists.launchpad.net ## +# pylint: disable=E0213 + +from ipaddress import ip_network import logging -from typing import Dict, List, NoReturn +from typing import NoReturn, Optional +from urllib.parse import urlparse -from ops.charm import CharmBase -from ops.framework import EventBase, StoredState +from oci_image import OCIImageResource +from ops.framework import EventBase from ops.main import main -from ops.model import ActiveStatus, Application, BlockedStatus, MaintenanceStatus, Unit -from oci_image import OCIImageResource, OCIImageResourceError +from opslib.osm.charm import CharmedOsmBase +from opslib.osm.interfaces.prometheus import PrometheusServer +from opslib.osm.pod import ( + ContainerV3Builder, + FilesV3Builder, + IngressResourceV3Builder, + PodSpecV3Builder, +) +from opslib.osm.validator import ( + ModelValidator, + validator, +) +import requests -from pod_spec import make_pod_spec logger = logging.getLogger(__name__) -PROMETHEUS_PORT = 9090 - - -class RelationsMissing(Exception): - def __init__(self, missing_relations: List): - self.message = "" - if missing_relations and isinstance(missing_relations, list): - self.message += f'Waiting for {", ".join(missing_relations)} relation' - if "," in self.message: - self.message += "s" +PORT = 9090 + + +class ConfigModel(ModelValidator): + web_subpath: str + default_target: str + max_file_size: int + site_url: Optional[str] + cluster_issuer: Optional[str] + ingress_class: Optional[str] + ingress_whitelist_source_range: Optional[str] + tls_secret_name: Optional[str] + enable_web_admin_api: bool + image_pull_policy: Optional[str] + + @validator("web_subpath") + def validate_web_subpath(cls, v): + if len(v) < 1: + raise ValueError("web-subpath must be a non-empty string") + return v + + @validator("max_file_size") + def validate_max_file_size(cls, v): + if v < 0: + raise ValueError("value must be equal or greater than 0") + return v + + @validator("site_url") + def validate_site_url(cls, v): + if v: + parsed = urlparse(v) + if not parsed.scheme.startswith("http"): + raise ValueError("value must start with http") + return v + + @validator("ingress_whitelist_source_range") + def validate_ingress_whitelist_source_range(cls, v): + if v: + ip_network(v) + return v + + @validator("image_pull_policy") + def validate_image_pull_policy(cls, v): + values = { + "always": "Always", + "ifnotpresent": "IfNotPresent", + "never": "Never", + } + v = v.lower() + if v not in values.keys(): + raise ValueError("value must be always, ifnotpresent or never") + return values[v] + + +class PrometheusCharm(CharmedOsmBase): - -class RelationDefinition: - def __init__(self, relation_name: str, keys: List, source_type): - if source_type != Application and source_type != Unit: - raise TypeError( - "source_type should be ops.model.Application or ops.model.Unit" - ) - self.relation_name = relation_name - self.keys = keys - self.source_type = source_type - - -def check_missing_relation_data( - data: Dict, - expected_relations_data: List[RelationDefinition], -): - missing_relations = [] - for relation_data in expected_relations_data: - if not all( - f"{relation_data.relation_name}_{k}" in data for k in relation_data.keys - ): - missing_relations.append(relation_data.relation_name) - if missing_relations: - raise RelationsMissing(missing_relations) - - -def get_relation_data( - charm: CharmBase, - relation_data: RelationDefinition, -) -> Dict: - data = {} - relation = charm.model.get_relation(relation_data.relation_name) - if relation: - self_app_unit = ( - charm.app if relation_data.source_type == Application else charm.unit - ) - expected_type = relation_data.source_type - for app_unit in relation.data: - if app_unit != self_app_unit and isinstance(app_unit, expected_type): - if all(k in relation.data[app_unit] for k in relation_data.keys): - for k in relation_data.keys: - data[f"{relation_data.relation_name}_{k}"] = relation.data[ - app_unit - ].get(k) - break - return data - - -class PrometheusCharm(CharmBase): """Prometheus Charm.""" - state = StoredState() - def __init__(self, *args) -> NoReturn: """Prometheus Charm constructor.""" - super().__init__(*args) - - # Internal state initialization - self.state.set_default(pod_spec=None) - - self.port = PROMETHEUS_PORT - self.image = OCIImageResource(self, "image") - - # Registering regular events - self.framework.observe(self.on.start, self.configure_pod) - self.framework.observe(self.on.config_changed, self.configure_pod) + super().__init__(*args, oci_image="image") # Registering provided relation events + self.prometheus = PrometheusServer(self, "prometheus") self.framework.observe( - self.on.prometheus_relation_joined, self._publish_prometheus_info + self.on.prometheus_relation_joined, # pylint: disable=E1101 + self._publish_prometheus_info, + ) + + # Registering actions + self.framework.observe( + self.on.backup_action, # pylint: disable=E1101 + self._on_backup_action, ) def _publish_prometheus_info(self, event: EventBase) -> NoReturn: - """Publishes Prometheus information. - - Args: - event (EventBase): Prometheus relation event. - """ - if self.unit.is_leader(): - rel_data = { - "host": self.model.app.name, - "port": str(PROMETHEUS_PORT), + self.prometheus.publish_info(self.app.name, PORT) + + def _on_backup_action(self, event: EventBase) -> NoReturn: + url = f"http://{self.model.app.name}:{PORT}/api/v1/admin/tsdb/snapshot" + result = requests.post(url) + + if result.status_code == 200: + event.set_results({"backup-name": result.json()["name"]}) + else: + event.fail(f"status-code: {result.status_code}") + + def _build_files(self, config: ConfigModel): + files_builder = FilesV3Builder() + files_builder.add_file( + "prometheus.yml", + ( + "global:\n" + " scrape_interval: 15s\n" + " evaluation_interval: 15s\n" + "alerting:\n" + " alertmanagers:\n" + " - static_configs:\n" + " - targets:\n" + "rule_files:\n" + "scrape_configs:\n" + " - job_name: 'prometheus'\n" + " static_configs:\n" + f" - targets: [{config.default_target}]\n" + ), + ) + return files_builder.build() + + def build_pod_spec(self, image_info): + # Validate config + config = ConfigModel(**dict(self.config)) + # Create Builder for the PodSpec + pod_spec_builder = PodSpecV3Builder() + + # Build Backup Container + backup_image = OCIImageResource(self, "backup-image") + backup_image_info = backup_image.fetch() + backup_container_builder = ContainerV3Builder("prom-backup", backup_image_info) + backup_container = backup_container_builder.build() + # Add backup container to pod spec + pod_spec_builder.add_container(backup_container) + + # Build Container + container_builder = ContainerV3Builder( + self.app.name, image_info, config.image_pull_policy + ) + container_builder.add_port(name=self.app.name, port=PORT) + container_builder.add_http_readiness_probe( + "/-/ready", + PORT, + initial_delay_seconds=10, + timeout_seconds=30, + ) + container_builder.add_http_liveness_probe( + "/-/healthy", + PORT, + initial_delay_seconds=30, + period_seconds=30, + ) + command = [ + "/bin/prometheus", + "--config.file=/etc/prometheus/prometheus.yml", + "--storage.tsdb.path=/prometheus", + "--web.console.libraries=/usr/share/prometheus/console_libraries", + "--web.console.templates=/usr/share/prometheus/consoles", + f"--web.route-prefix={config.web_subpath}", + f"--web.external-url=http://localhost:{PORT}{config.web_subpath}", + ] + if config.enable_web_admin_api: + command.append("--web.enable-admin-api") + container_builder.add_command(command) + container_builder.add_volume_config( + "config", "/etc/prometheus", self._build_files(config) + ) + container = container_builder.build() + # Add container to pod spec + pod_spec_builder.add_container(container) + # Add ingress resources to pod spec if site url exists + if config.site_url: + parsed = urlparse(config.site_url) + annotations = { + "nginx.ingress.kubernetes.io/proxy-body-size": "{}".format( + str(config.max_file_size) + "m" + if config.max_file_size > 0 + else config.max_file_size + ) } - for k, v in rel_data.items(): - event.relation.data[self.app][k] = v - - @property - def relations_requirements(self): - return [] - - def get_relation_state(self): - relation_state = {} - for relation_requirements in self.relations_requirements: - data = get_relation_data(self, relation_requirements) - relation_state = {**relation_state, **data} - check_missing_relation_data(relation_state, self.relations_requirements) - return relation_state - - def configure_pod(self, _=None) -> NoReturn: - """Assemble the pod spec and apply it, if possible. - - Args: - event (EventBase): Hook or Relation event that started the - function. - """ - if not self.unit.is_leader(): - self.unit.status = ActiveStatus("ready") - return - - relation_state = None - try: - relation_state = self.get_relation_state() - except RelationsMissing as exc: - logger.exception("Relation missing error") - self.unit.status = BlockedStatus(exc.message) - return - - self.unit.status = MaintenanceStatus("Assembling pod spec") - - # Fetch image information - try: - self.unit.status = MaintenanceStatus("Fetching image information") - image_info = self.image.fetch() - except OCIImageResourceError: - self.unit.status = BlockedStatus("Error fetching image information") - return - - try: - pod_spec = make_pod_spec( - image_info, - self.model.config, - relation_state, - self.model.app.name, - self.port, + if config.ingress_class: + annotations["kubernetes.io/ingress.class"] = config.ingress_class + ingress_resource_builder = IngressResourceV3Builder( + f"{self.app.name}-ingress", annotations ) - except ValueError as exc: - logger.exception("Config/Relation data validation error") - self.unit.status = BlockedStatus(str(exc)) - return - - if self.state.pod_spec != pod_spec: - self.model.pod.set_spec(pod_spec) - self.state.pod_spec = pod_spec - self.unit.status = ActiveStatus("ready") + if config.ingress_whitelist_source_range: + annotations[ + "nginx.ingress.kubernetes.io/whitelist-source-range" + ] = config.ingress_whitelist_source_range + + if config.cluster_issuer: + annotations["cert-manager.io/cluster-issuer"] = config.cluster_issuer + + if parsed.scheme == "https": + ingress_resource_builder.add_tls( + [parsed.hostname], config.tls_secret_name + ) + else: + annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false" + + ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT) + ingress_resource = ingress_resource_builder.build() + pod_spec_builder.add_ingress_resource(ingress_resource) + return pod_spec_builder.build() if __name__ == "__main__":