Gateway API Integrator

Platform:

Ubuntu
24.04 22.04
Channel Revision Published Runs on
latest/stable 151 08 Apr 2026
Ubuntu 24.04 Ubuntu 22.04
latest/stable 14 01 Jul 2024
Ubuntu 24.04 Ubuntu 22.04
latest/edge 157 02 Jun 2026
Ubuntu 24.04 Ubuntu 22.04
latest/edge 14 27 Jun 2024
Ubuntu 24.04 Ubuntu 22.04
juju deploy gateway-api-integrator

# Copyright 2026 Canonical Ltd.
# See LICENSE file for licensing details.

"""gateway-route interface library v1.

This library implements the gateway-route interface.

The requirer publishes hostname information. The provider publishes
gateway resource details (gateway name, model/namespace, HTTPS mode)
so the requirer can reference the correct Gateway resource.

With the v1 interface, the requirer is expected to create and manage
its own HTTP/TCP/UDP route resources referencing the provider's Gateway.

## Getting Started

Fetch the library:

```shell
charmcraft fetch-lib charms.gateway_api_integrator.v1.gateway_route
```

### Requirer usage

In the `charmcraft.yaml` of the charm, add the following:

```yaml
requires:
    gateway-route:
        interface: gateway-route
```

```python
from charms.gateway_api_integrator.v1.gateway_route import (
    GATEWAY_ROUTE_RELATION_NAME,
    GatewayRouteRequirer,
    GatewayRouteProviderAppData,
)

class IngressConfiguratorCharm(CharmBase):
    def __init__(self, *args):
        super().__init__(*args)
        self.gateway_route = GatewayRouteRequirer(self)
        self.framework.observe(
            self.on[GATEWAY_ROUTE_RELATION_NAME].relation_changed, self._on_gateway_route_changed
        )
        self.framework.observe(
            self.on[GATEWAY_ROUTE_RELATION_NAME].relation_broken, self._on_gateway_route_changed
        )

    def _on_gateway_route_changed(self, event):
        provider_data = self.gateway_route.get_provider_data()
        if provider_data:
            # Use provider_data.gateway_name, provider_data.gateway_model, provider_data.https_mode
            ...
```

### Provider usage

In the `charmcraft.yaml` of the charm, add the following:

```yaml
provides:
    gateway-route:
        interface: gateway-route
```

```python
from charms.gateway_api_integrator.v1.gateway_route import (
    GATEWAY_ROUTE_RELATION_NAME,
    GatewayRouteProvider,
    HttpsMode,
    GatewayRouteRequirerAppData,
)

class GatewayAPIIntegratorCharm(CharmBase):
    def __init__(self, *args):
        super().__init__(*args)
        self.gateway_route = GatewayRouteProvider(self)
        self.framework.observe(
            self.on[GATEWAY_ROUTE_RELATION_NAME].relation_changed, self._on_gateway_route_changed
        )
        self.framework.observe(
            self.on[GATEWAY_ROUTE_RELATION_NAME].relation_broken, self._on_gateway_route_changed
        )

    def _on_gateway_route_changed(self, event):
        requirer_data = self.gateway_route.get_requirer_data()
        # requirer_data is a dict[int, GatewayRouteRequirerAppData]
        # Use data.hostname, data.additional_hostnames
        # Publish provider data to all valid relations
        self.gateway_route.publish_provider_data(
            gateway_name=self.app.name,
            gateway_model=self.model.name,
            https_mode=HttpsMode.ENFORCED,
        )
```
"""

import logging
from enum import StrEnum
from typing import Annotated

from ops import CharmBase
from ops.framework import Object
from ops.model import (
    Relation,
    RelationDataTypeError,
)
from pydantic import BeforeValidator, Field, ValidationError
from pydantic.dataclasses import dataclass
from validators import domain

# The unique Charmhub library identifier, never change it
LIBID = "53fdf90019a7406695064ed1e3d2708f"

# Increment this major API version when introducing breaking changes
LIBAPI = 1

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 0

logger = logging.getLogger(__name__)
GATEWAY_ROUTE_RELATION_NAME = "gateway-route"


class GatewayRouteInvalidRelationDataError(Exception):
    """Raised when relation data validation for gateway-route fails."""


def valid_fqdn(value: str) -> str:
    """Validate if value is a valid FQDN (TLDs alone are not allowed).

    Args:
        value: The value to validate.

    Raises:
        ValueError: When value is not a valid domain.
    """
    if not bool(domain(value)):
        raise ValueError(f"Invalid domain: {value}")
    return value


# --- Data models ---


class HttpsMode(StrEnum):
    """HTTPS mode for the gateway.

    Attrs:
        DISABLED: TLS is not configured; only HTTP listener exists.
        ENABLED: TLS is configured; both HTTP and HTTPS listeners exist.
        ENFORCED: TLS is configured and enforce-https is true; HTTP redirects to HTTPS.
    """

    DISABLED = "disabled"
    ENABLED = "enabled"
    ENFORCED = "enforced"


@dataclass
class GatewayRouteRequirerAppData:
    """Requirer application databag schema.

    The requirer provides hostname information so the provider can
    configure TLS certificates and DNS records.

    Attributes:
        hostname: The hostname for the service.
        additional_hostnames: Additional hostnames for the service.
    """

    hostname: Annotated[str, BeforeValidator(valid_fqdn)] | None = Field(
        description="The hostname for the service.", default=None
    )
    additional_hostnames: list[Annotated[str, BeforeValidator(valid_fqdn)]] = Field(
        description="Additional hostnames for the service.", default_factory=list
    )


@dataclass
class GatewayRouteProviderAppData:
    """Provider application databag schema.

    The provider publishes gateway resource details so the requirer
    can construct HTTPRoute resources referencing the correct Gateway.

    Attributes:
        gateway_name: The name of the Gateway resource managed by the provider.
        gateway_model: The Juju model (K8s namespace) where the Gateway resource lives.
        https_mode: The HTTPS mode indicating how the provider handles TLS.
    """

    gateway_name: str = Field(description="The name of the Gateway resource.")
    gateway_model: str = Field(
        description="The Juju model (K8s namespace) where the Gateway resource lives."
    )
    https_mode: HttpsMode = Field(
        description="The HTTPS mode: disabled, enabled, or enforced."
    )


# --- Provider ---


class GatewayRouteProvider(Object):
    """Gateway-route interface provider implementation.

    The provider reads hostname data from requirers and publishes
    gateway resource information back.
    """

    def __init__(
        self,
        charm: CharmBase,
        relation_name: str = GATEWAY_ROUTE_RELATION_NAME,
    ) -> None:
        """Initialize the GatewayRouteProvider.

        Args:
            charm: The charm instance using this library.
            relation_name: The name of the relation endpoint.
        """
        super().__init__(charm, relation_name)
        self.charm = charm
        self.relation_name = relation_name
        self._valid_relations: list[Relation] = []

    @property
    def relations(self) -> list[Relation]:
        """All relations for this endpoint."""
        return self.charm.model.relations.get(self.relation_name, [])

    def get_requirer_data(self) -> dict[int, GatewayRouteRequirerAppData]:
        """Fetch requirer data from all relations.

        Also stores the valid relations for use by publish_provider_data.

        Returns:
            Dictionary mapping relation ID to validated requirer data.
        """
        results: dict[int, GatewayRouteRequirerAppData] = {}
        self._valid_relations = []
        for relation in self.relations:
            try:
                data = relation.load(GatewayRouteRequirerAppData, relation.app)
            except ValidationError:
                logger.error(
                    "Skipping relation %s: invalid data", relation.id
                )
                continue
            results[relation.id] = data
            self._valid_relations.append(relation)
        return results

    def publish_provider_data(
        self,
        gateway_name: str,
        gateway_model: str,
        https_mode: HttpsMode,
    ) -> None:
        """Publish gateway information to requirers with valid data.

        Only publishes to relations previously validated by get_requirer_data.

        Args:
            gateway_name: The name of the Gateway resource.
            gateway_model: The Juju model (K8s namespace) of the Gateway resource.
            https_mode: The HTTPS mode for the gateway.

        Raises:
            GatewayRouteInvalidRelationDataError: When publishing fails for any relation.
        """
        if not self.charm.unit.is_leader():
            return

        failed_relations: list[int] = []
        for relation in self._valid_relations:
            try:
                app_data = GatewayRouteProviderAppData(
                    gateway_name=gateway_name,
                    gateway_model=gateway_model,
                    https_mode=https_mode,
                )
                relation.save(app_data, self.charm.app)
            except (ValidationError, RelationDataTypeError):
                logger.error(
                    "Failed to publish provider data to relation %s", relation.id
                )
                failed_relations.append(relation.id)

        if failed_relations:
            raise GatewayRouteInvalidRelationDataError(
                f"Failed to publish provider data to relations: {failed_relations}"
            )


# --- Requirer ---


class GatewayRouteRequirer(Object):
    """Gateway-route interface requirer implementation.

    The requirer provides hostname information and reads back
    gateway resource details from the provider.
    """

    def __init__(
        self,
        charm: CharmBase,
        relation_name: str = GATEWAY_ROUTE_RELATION_NAME,
    ) -> None:
        """Initialize the GatewayRouteRequirer.

        Args:
            charm: The charm instance using this library.
            relation_name: The name of the relation endpoint.
        """
        super().__init__(charm, relation_name)
        self.charm = charm
        self.relation_name = relation_name

    @property
    def relation(self) -> Relation | None:
        """The relation instance for this endpoint."""
        return self.charm.model.get_relation(self.relation_name)

    def publish_requirer_data(
        self,
        hostname: str,
        additional_hostnames: list[str] | None = None,
    ) -> None:
        """Publish hostname data to the provider.

        Args:
            hostname: The hostname for the service.
            additional_hostnames: Additional hostnames for the service.

        Raises:
            GatewayRouteInvalidRelationDataError: When data validation fails.
        """
        if (relation := self.relation) and (not self.charm.unit.is_leader()):
            return

        try:
            app_data = GatewayRouteRequirerAppData(
                hostname=hostname,
                additional_hostnames=additional_hostnames or [],
            )
            relation.save(app_data, self.charm.app)
        except (ValidationError, RelationDataTypeError) as exc:
            raise GatewayRouteInvalidRelationDataError(
                "Failed to publish requirer relation data."
            ) from exc

    def get_provider_data(self) -> GatewayRouteProviderAppData | None:
        """Fetch provider application data from the relation.

        Returns:
            Validated provider data or None if not yet available.

        Raises:
            GatewayRouteInvalidRelationDataError: When data validation fails.
        """
        if not (relation := self.relation) or not relation.app:
            return None

        try:
            return relation.load(GatewayRouteProviderAppData, relation.app)
        except ValidationError as exc:
            raise GatewayRouteInvalidRelationDataError(
                "gateway-route provider data validation failed."
            ) from exc