Temporal UI Charm
[osm/devops.git] / installers / charm / osm-temporal-ui / lib / charms / nginx_ingress_integrator / v0 / ingress.py
diff --git a/installers/charm/osm-temporal-ui/lib/charms/nginx_ingress_integrator/v0/ingress.py b/installers/charm/osm-temporal-ui/lib/charms/nginx_ingress_integrator/v0/ingress.py
new file mode 100644 (file)
index 0000000..be2d762
--- /dev/null
@@ -0,0 +1,229 @@
+# See LICENSE file for licensing details.
+#   http://www.apache.org/licenses/LICENSE-2.0
+"""Library for the ingress relation.
+
+This library contains the Requires and Provides classes for handling
+the ingress interface.
+
+Import `IngressRequires` in your charm, with two required options:
+    - "self" (the charm itself)
+    - config_dict
+
+`config_dict` accepts the following keys:
+    - service-hostname (required)
+    - service-name (required)
+    - service-port (required)
+    - additional-hostnames
+    - limit-rps
+    - limit-whitelist
+    - max-body-size
+    - owasp-modsecurity-crs
+    - path-routes
+    - retry-errors
+    - rewrite-enabled
+    - rewrite-target
+    - service-namespace
+    - session-cookie-max-age
+    - tls-secret-name
+
+See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions
+of each, along with the required type.
+
+As an example, add the following to `src/charm.py`:
+```
+from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
+
+# In your charm's `__init__` method.
+self.ingress = IngressRequires(self, {"service-hostname": self.config["external_hostname"],
+                                      "service-name": self.app.name,
+                                      "service-port": 80})
+
+# In your charm's `config-changed` handler.
+self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
+```
+And then add the following to `metadata.yaml`:
+```
+requires:
+  ingress:
+    interface: ingress
+```
+You _must_ register the IngressRequires class as part of the `__init__` method
+rather than, for instance, a config-changed event handler. This is because
+doing so won't get the current relation changed event, because it wasn't
+registered to handle the event (because it wasn't created in `__init__` when
+the event was fired).
+"""
+
+import logging
+
+from ops.charm import CharmEvents
+from ops.framework import EventBase, EventSource, Object
+from ops.model import BlockedStatus
+
+# The unique Charmhub library identifier, never change it
+LIBID = "db0af4367506491c91663468fb5caa4c"
+
+# Increment this major API version when introducing breaking changes
+LIBAPI = 0
+
+# Increment this PATCH version before using `charmcraft publish-lib` or reset
+# to 0 if you are raising the major API version
+LIBPATCH = 10
+
+logger = logging.getLogger(__name__)
+
+REQUIRED_INGRESS_RELATION_FIELDS = {
+    "service-hostname",
+    "service-name",
+    "service-port",
+}
+
+OPTIONAL_INGRESS_RELATION_FIELDS = {
+    "additional-hostnames",
+    "limit-rps",
+    "limit-whitelist",
+    "max-body-size",
+    "owasp-modsecurity-crs",
+    "path-routes",
+    "retry-errors",
+    "rewrite-target",
+    "rewrite-enabled",
+    "service-namespace",
+    "session-cookie-max-age",
+    "tls-secret-name",
+}
+
+
+class IngressAvailableEvent(EventBase):
+    pass
+
+
+class IngressBrokenEvent(EventBase):
+    pass
+
+
+class IngressCharmEvents(CharmEvents):
+    """Custom charm events."""
+
+    ingress_available = EventSource(IngressAvailableEvent)
+    ingress_broken = EventSource(IngressBrokenEvent)
+
+
+class IngressRequires(Object):
+    """This class defines the functionality for the 'requires' side of the 'ingress' relation.
+
+    Hook events observed:
+        - relation-changed
+    """
+
+    def __init__(self, charm, config_dict):
+        super().__init__(charm, "ingress")
+
+        self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
+
+        self.config_dict = config_dict
+
+    def _config_dict_errors(self, update_only=False):
+        """Check our config dict for errors."""
+        blocked_message = "Error in ingress relation, check `juju debug-log`"
+        unknown = [
+            x
+            for x in self.config_dict
+            if x not in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
+        ]
+        if unknown:
+            logger.error(
+                "Ingress relation error, unknown key(s) in config dictionary found: %s",
+                ", ".join(unknown),
+            )
+            self.model.unit.status = BlockedStatus(blocked_message)
+            return True
+        if not update_only:
+            missing = [x for x in REQUIRED_INGRESS_RELATION_FIELDS if x not in self.config_dict]
+            if missing:
+                logger.error(
+                    "Ingress relation error, missing required key(s) in config dictionary: %s",
+                    ", ".join(sorted(missing)),
+                )
+                self.model.unit.status = BlockedStatus(blocked_message)
+                return True
+        return False
+
+    def _on_relation_changed(self, event):
+        """Handle the relation-changed event."""
+        # `self.unit` isn't available here, so use `self.model.unit`.
+        if self.model.unit.is_leader():
+            if self._config_dict_errors():
+                return
+            for key in self.config_dict:
+                event.relation.data[self.model.app][key] = str(self.config_dict[key])
+
+    def update_config(self, config_dict):
+        """Allow for updates to relation."""
+        if self.model.unit.is_leader():
+            self.config_dict = config_dict
+            if self._config_dict_errors(update_only=True):
+                return
+            relation = self.model.get_relation("ingress")
+            if relation:
+                for key in self.config_dict:
+                    relation.data[self.model.app][key] = str(self.config_dict[key])
+
+
+class IngressProvides(Object):
+    """This class defines the functionality for the 'provides' side of the 'ingress' relation.
+
+    Hook events observed:
+        - relation-changed
+    """
+
+    def __init__(self, charm):
+        super().__init__(charm, "ingress")
+        # Observe the relation-changed hook event and bind
+        # self.on_relation_changed() to handle the event.
+        self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
+        self.framework.observe(charm.on["ingress"].relation_broken, self._on_relation_broken)
+        self.charm = charm
+
+    def _on_relation_changed(self, event):
+        """Handle a change to the ingress relation.
+
+        Confirm we have the fields we expect to receive."""
+        # `self.unit` isn't available here, so use `self.model.unit`.
+        if not self.model.unit.is_leader():
+            return
+
+        ingress_data = {
+            field: event.relation.data[event.app].get(field)
+            for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
+        }
+
+        missing_fields = sorted(
+            [
+                field
+                for field in REQUIRED_INGRESS_RELATION_FIELDS
+                if ingress_data.get(field) is None
+            ]
+        )
+
+        if missing_fields:
+            logger.error(
+                "Missing required data fields for ingress relation: {}".format(
+                    ", ".join(missing_fields)
+                )
+            )
+            self.model.unit.status = BlockedStatus(
+                "Missing fields for ingress: {}".format(", ".join(missing_fields))
+            )
+
+        # Create an event that our charm can use to decide it's okay to
+        # configure the ingress.
+        self.charm.on.ingress_available.emit()
+
+    def _on_relation_broken(self, _):
+        """Handle a relation-broken event in the ingress relation."""
+        if not self.model.unit.is_leader():
+            return
+
+        # Create an event that our charm can use to remove the ingress resource.
+        self.charm.on.ingress_broken.emit()