X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=installers%2Fcharm%2Fng-ui%2Fsrc%2Fcharm.py;h=4d2bb85d6b6043319cdaea26a359989fcd5db14e;hb=49379ced23b5e344a773ce77ac9cb59c1864e19b;hp=7510a6cbb7c87230a74eb4d2173dcbd6820b670d;hpb=ef349d9224f93fcc3eeb7a26f71c6a128ffbf96a;p=osm%2Fdevops.git diff --git a/installers/charm/ng-ui/src/charm.py b/installers/charm/ng-ui/src/charm.py index 7510a6cb..4d2bb85d 100755 --- a/installers/charm/ng-ui/src/charm.py +++ b/installers/charm/ng-ui/src/charm.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2020 Canonical Ltd. +# Copyright 2021 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -20,174 +20,164 @@ # osm-charmers@lists.launchpad.net ## -import logging -from typing import Any, Dict, NoReturn -from pydantic import ValidationError - -from ops.charm import CharmBase, CharmEvents -from ops.framework import EventBase, EventSource, StoredState -from ops.main import main -from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus -from oci_image import OCIImageResource, OCIImageResourceError - -from pod_spec import make_pod_spec - -logger = logging.getLogger(__name__) +# pylint: disable=E0213 -NGUI_PORT = 80 +import logging +from typing import Optional, NoReturn +from ipaddress import ip_network +from urllib.parse import urlparse -class ConfigurePodEvent(EventBase): - """Configure Pod event""" +from ops.main import main - pass +from opslib.osm.charm import CharmedOsmBase, RelationsMissing +from opslib.osm.pod import ( + ContainerV3Builder, + PodSpecV3Builder, + FilesV3Builder, + IngressResourceV3Builder, +) -class NgUiEvents(CharmEvents): - """NGUI Events""" - configure_pod = EventSource(ConfigurePodEvent) +from opslib.osm.validator import ( + ModelValidator, + validator, +) +from opslib.osm.interfaces.http import HttpClient +from string import Template +from pathlib import Path -class NgUiCharm(CharmBase): - """NGUI Charm.""" +logger = logging.getLogger(__name__) - state = StoredState() - on = NgUiEvents() +class ConfigModel(ModelValidator): + port: int + server_name: str + max_file_size: int + site_url: Optional[str] + ingress_whitelist_source_range: Optional[str] + tls_secret_name: Optional[str] + + @validator("port") + def validate_port(cls, v): + if v <= 0: + raise ValueError("value must be greater than 0") + 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 + + +class NgUiCharm(CharmedOsmBase): def __init__(self, *args) -> NoReturn: - """NGUI Charm constructor.""" - super().__init__(*args) - - # Internal state initialization - self.state.set_default(pod_spec=None) - - # North bound interface initialization - self.state.set_default(nbi_host=None) - self.state.set_default(nbi_port=None) - - self.http_port = NGUI_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) - # self.framework.observe(self.on.upgrade_charm, self.configure_pod) - - # Registering custom internal events - self.framework.observe(self.on.configure_pod, self.configure_pod) - - # Registering required relation changed events - self.framework.observe( - self.on.nbi_relation_changed, self._on_nbi_relation_changed + super().__init__(*args, oci_image="image") + + self.nbi_client = HttpClient(self, "nbi") + self.framework.observe(self.on["nbi"].relation_changed, self.configure_pod) + self.framework.observe(self.on["nbi"].relation_broken, self.configure_pod) + + def _check_missing_dependencies(self, config: ConfigModel): + missing_relations = [] + + if self.nbi_client.is_missing_data_in_app(): + missing_relations.append("nbi") + + if missing_relations: + raise RelationsMissing(missing_relations) + + def _build_files(self, config: ConfigModel): + files_builder = FilesV3Builder() + files_builder.add_file( + "default", + Template(Path("files/default").read_text()).substitute( + port=config.port, + server_name=config.server_name, + max_file_size=config.max_file_size, + nbi_host=self.nbi_client.host, + nbi_port=self.nbi_client.port, + ), ) - - # Registering required relation departed events - self.framework.observe( - self.on.nbi_relation_departed, self._on_nbi_relation_departed + return files_builder.build() + + def build_pod_spec(self, image_info): + # Validate config + config = ConfigModel(**dict(self.config)) + # Check relations + self._check_missing_dependencies(config) + # Create Builder for the PodSpec + pod_spec_builder = PodSpecV3Builder() + # Build Container + container_builder = ContainerV3Builder(self.app.name, image_info) + container_builder.add_port(name=self.app.name, port=config.port) + container = container_builder.build() + container_builder.add_tcpsocket_readiness_probe( + config.port, + initial_delay_seconds=45, + timeout_seconds=5, ) - - def _on_nbi_relation_changed(self, event: EventBase) -> NoReturn: - """Reads information about the nbi relation. - - Args: - event (EventBase): NBI relation event. - """ - data_loc = event.unit if event.unit else event.app - logger.error(dict(event.relation.data)) - nbi_host = event.relation.data[data_loc].get("host") - nbi_port = event.relation.data[data_loc].get("port") - - if ( - nbi_host - and nbi_port - and (self.state.nbi_host != nbi_host or self.state.nbi_port != nbi_port) - ): - self.state.nbi_host = nbi_host - self.state.nbi_port = nbi_port - self.on.configure_pod.emit() - - def _on_nbi_relation_departed(self, event: EventBase) -> NoReturn: - """Clears data from nbi relation. - - Args: - event (EventBase): NBI relation event. - """ - self.state.nbi_host = None - self.state.nbi_port = None - self.on.configure_pod.emit() - - def _missing_relations(self) -> str: - """Checks if there missing relations. - - Returns: - str: string with missing relations - """ - data_status = { - "nbi": self.state.nbi_host, - } - - missing_relations = [k for k, v in data_status.items() if not v] - - return ", ".join(missing_relations) - - @property - def relation_state(self) -> Dict[str, Any]: - """Collects relation state configuration for pod spec assembly. - - Returns: - Dict[str, Any]: relation state information. - """ - relation_state = { - "nbi_host": self.state.nbi_host, - "nbi_port": self.state.nbi_port, - } - return relation_state - - def configure_pod(self, event: EventBase) -> NoReturn: - """Assemble the pod spec and apply it, if possible. - - Args: - event (EventBase): Hook or Relation event that started the - function. - """ - if missing := self._missing_relations(): - self.unit.status = BlockedStatus( - f"Waiting for {missing} relation{'s' if ',' in missing else ''}" - ) - return - - if not self.unit.is_leader(): - self.unit.status = ActiveStatus("ready") - 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.config, - self.relation_state, - self.model.app.name, + container_builder.add_tcpsocket_liveness_probe( + config.port, + initial_delay_seconds=45, + timeout_seconds=15, + ) + container_builder.add_volume_config( + "configuration", + "/etc/nginx/sites-available/", + self._build_files(config), + ) + # 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 + ), + } + ingress_resource_builder = IngressResourceV3Builder( + f"{self.app.name}-ingress", annotations ) - except ValidationError 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 + if config.ingress_whitelist_source_range: + annotations[ + "nginx.ingress.kubernetes.io/whitelist-source-range" + ] = config.ingress_whitelist_source_range - self.unit.status = ActiveStatus("ready") + 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, config.port + ) + ingress_resource = ingress_resource_builder.build() + pod_spec_builder.add_ingress_resource(ingress_resource) + return pod_spec_builder.build() if __name__ == "__main__":