--- /dev/null
+# 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()