Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a47904e
Remove site url
arturo-seijas May 6, 2024
31789ba
Remove site url
arturo-seijas May 6, 2024
26f6e05
Remove site url
arturo-seijas May 6, 2024
5b435d2
Remove site url
arturo-seijas May 6, 2024
f3fd209
Remove site url
arturo-seijas May 6, 2024
cc93c49
Remove site url
arturo-seijas May 6, 2024
ecd1a01
fix some stuff
arturo-seijas May 6, 2024
4b185b5
fix unit tests
arturo-seijas May 7, 2024
687a408
Update docs
arturo-seijas May 7, 2024
2d50ea9
fix wfs
arturo-seijas May 7, 2024
1a2aa63
undo lib changes
arturo-seijas May 7, 2024
5b60f81
Fix integration tests
arturo-seijas May 7, 2024
7716d3c
update lib
arturo-seijas May 7, 2024
1dad35b
fix tests
arturo-seijas May 7, 2024
ccd1e3b
fix tests
arturo-seijas May 7, 2024
fe25d94
fix tests
arturo-seijas May 7, 2024
80d9bcd
fix tests
arturo-seijas May 7, 2024
8e528d1
fix tests
arturo-seijas May 7, 2024
1ae04e3
fix tests
arturo-seijas May 7, 2024
8c9a9c6
fix tests
arturo-seijas May 7, 2024
f79db7a
fix tests
arturo-seijas May 8, 2024
29f7a13
fix tests
arturo-seijas May 8, 2024
35dc3a3
fix zap config
arturo-seijas May 8, 2024
e97adfe
fix zap config
arturo-seijas May 8, 2024
ad3daa3
test stuff
arturo-seijas May 8, 2024
3195112
test stuff
arturo-seijas May 8, 2024
197afd3
Merge branch 'main' into remove-site-url
arturo-seijas May 10, 2024
4ddc7da
fix tests
arturo-seijas May 10, 2024
3ae5571
fix indico config
arturo-seijas May 20, 2024
cfa9eee
fix indico config
arturo-seijas May 20, 2024
b63e5f5
Remove site url
arturo-seijas May 6, 2024
284a69e
revert some stuff
arturo-seijas May 22, 2024
58c3eed
wait properly
arturo-seijas May 22, 2024
c18f7e3
Merge branch 'main' into remove-site-url
alithethird Feb 18, 2025
3f58153
Merge branch 'main' into remove-site-url
seb4stien May 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,3 @@ options:
type: string
description: Email address of the technical manager of the Indico instance.
default: 'support-tech@mydomain.local'
site_url:
type: string
description: URL through which Indico is accessed by users.
default: ''
10 changes: 3 additions & 7 deletions docs/how-to/configure-the-external-hostname.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
# How to configure the external hostname

This charm exposes the `site_url` configuration option to specify the external hostname of the application.

To expose the application it is recommended to set that configuration option and deploy and integrate with the [Nginx Ingress Integrator Operator](https://charmhub.io/nginx-ingress-integrator), that will be automatically configured with the values provided by the charm.
To expose the application, deploy and integrate with the [Nginx Ingress Integrator Operator](https://charmhub.io/nginx-ingress-integrator). The charm will be automatically exposed at `[application name].local`, being `[application name]` the charm name. To provide a different hostname, set the [service-hostname](https://charmhub.io/nginx-ingress-integrator/configuration#service-hostname) configuration for the Nginx Ingress Integrator Operator.

Assuming Indico is already up and running as `indico`, you'll need to run the following commands:
```
# Configure the external hostname
juju config indico site_url=indico.local
# Deploy and integrate with the Nginx Ingress Integrator charm
juju deploy nginx-ingress-integrator
juju trust nginx-ingress-integrator --scope cluster # if RBAC is enabled
juju integrate nginx-ingress-integrator indico
# Configure the external hostname
juju config nginx-ingress-integrator service-hostname=indico.example
```

For more details on the configuration options and their default values see the [configuration reference](https://charmhub.io/indico/configure).
2 changes: 1 addition & 1 deletion src-docs/charm.py.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Charm for Indico on kubernetes.

Attrs: on: Redis relation charm events.

<a href="../src/charm.py#L61"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/charm.py#L56"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `__init__`

Expand Down
70 changes: 18 additions & 52 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,19 @@
import os
import typing
from re import findall
from typing import Any, Dict, Iterator, List, Optional, Tuple
from urllib.parse import urlparse
from typing import Any, Dict, Iterator, List, Optional

import charms.loki_k8s.v0.loki_push_api
import ops
from charms.grafana_k8s.v0.grafana_dashboard import GrafanaDashboardProvider
from charms.nginx_ingress_integrator.v0.nginx_route import NginxRouteRequirer, require_nginx_route
from charms.loki_k8s.v0.loki_push_api import LogProxyConsumer
from charms.nginx_ingress_integrator.v0.nginx_route import require_nginx_route
from charms.prometheus_k8s.v0.prometheus_scrape import MetricsEndpointProvider
from charms.redis_k8s.v0.redis import RedisRelationCharmEvents, RedisRequires
from ops.charm import ActionEvent, CharmBase, HookEvent, PebbleReadyEvent, RelationDepartedEvent
from ops.jujuversion import JujuVersion
from ops.main import main
from ops.model import ActiveStatus, BlockedStatus, Container, MaintenanceStatus, WaitingStatus
from ops.model import ActiveStatus, Container, MaintenanceStatus, WaitingStatus
from ops.pebble import ExecError

from database_observer import DatabaseObserver
Expand Down Expand Up @@ -100,7 +99,7 @@ def __init__(self, *args):
self.framework.observe(
self.on["indico-peers"].relation_departed, self._on_peer_relation_departed
)
self._require_nginx_route()
self.nginx_route = self._require_nginx_route()

self._metrics_endpoint = MetricsEndpointProvider(
self,
Expand Down Expand Up @@ -128,11 +127,15 @@ def __init__(self, *args):
container_name="indico",
)

def _require_nginx_route(self) -> None:
"""Require nginx ingress."""
require_nginx_route(
def _require_nginx_route(self) -> NginxRouteRequirer:
"""Require nginx ingress.

Returns:
The NginxRouteRequirer.
"""
return require_nginx_route(
charm=self,
service_hostname=self._get_external_hostname(),
service_hostname=f"{self.app.name}.local",
service_name=self.app.name,
service_port=8080,
)
Expand All @@ -148,46 +151,13 @@ def _are_pebble_instances_ready(self) -> bool:
for container_name in self.model.unit.containers
)

def _is_configuration_valid(self) -> Tuple[bool, str]:
"""Validate charm configuration.

Returns:
Tuple containing as first element whether the configuration is valid.
and a string with the error, if any, as second element.
"""
site_url = typing.cast(str, self.config["site_url"])
if site_url and not urlparse(site_url).hostname:
return False, "Configuration option site_url is not valid"
return True, ""

def _get_external_hostname(self) -> str:
"""Extract and return hostname from site_url or default to [application name].local.

Returns:
The site URL defined as part of the site_url configuration or a default value.
"""
site_url = typing.cast(str, self.config["site_url"])
if not site_url or not (hostname := urlparse(site_url).hostname):
return f"{self.app.name}.local"
return hostname

def _get_external_scheme(self) -> str:
"""Extract and return schema from site_url.

Returns:
The HTTP schema.
"""
site_url = typing.cast(str, self.config["site_url"])
return urlparse(site_url).scheme if site_url else "http"

def _get_external_port(self) -> Optional[int]:
"""Extract and return port from site_url.
"""Extract and return hostname from the nginx-route relation data.

Returns:
The port number.
The hostname configured in the NGINX ingress integrator.
"""
site_url = typing.cast(str, self.config["site_url"])
return urlparse(site_url).port
return self.nginx_route.config.get("service-hostname")

def _are_relations_ready(self, _) -> bool:
"""Check if the needed relations are established.
Expand Down Expand Up @@ -572,8 +542,8 @@ def _get_indico_env_config(self, container: Container) -> Dict:
"REDIS_CACHE_URL": self._get_redis_url("redis-cache"),
"SECRET_KEY": self._get_indico_secret_key_from_relation(),
"SERVICE_HOSTNAME": self._get_external_hostname(),
"SERVICE_PORT": self._get_external_port(),
"SERVICE_SCHEME": self._get_external_scheme(),
"SERVICE_PORT": "",
"SERVICE_SCHEME": "https",
"STORAGE_DICT": {
"default": "fs:/srv/indico/archive",
},
Expand Down Expand Up @@ -604,7 +574,7 @@ def _get_indico_env_config(self, container: Container) -> Dict:
saml_config: Dict[str, Any] = {
"strict": True,
"sp": {
"entityId": self.config["site_url"],
"entityId": f"https://{self._get_external_hostname()}",
},
"idp": {
"entityId": self.state.saml_config.entity_id,
Expand Down Expand Up @@ -685,10 +655,6 @@ def _on_config_changed(self, event: HookEvent) -> None:
self.unit.status = WaitingStatus("Waiting for pebble")
return
self.model.unit.status = MaintenanceStatus("Configuring pod")
is_valid, error = self._is_configuration_valid()
if not is_valid:
self.model.unit.status = BlockedStatus(error)
return
for container_name in self.model.unit.containers:
self._config_pebble(self.unit.get_container(container_name))

Expand Down
17 changes: 8 additions & 9 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
from pytest_operator.plugin import OpsTest


@fixture(scope="module", name="external_url")
def external_url_fixture():
@fixture(scope="module", name="hostname")
def hostname_fixture():
"""Provides the external URL for Indico."""
return "https://events.staging.canonical.com"
return "events.staging.canonical.com"


@fixture(scope="module")
Expand Down Expand Up @@ -60,6 +60,7 @@ def requests_timeout():
async def app_fixture(
ops_test: OpsTest,
app_name: str,
hostname: str,
pytestconfig: Config,
):
"""Indico charm used for integration testing.
Expand All @@ -80,11 +81,9 @@ async def app_fixture(
ops_test.model.deploy("redis-k8s", "redis-broker", channel="latest/edge"),
ops_test.model.deploy("redis-k8s", "redis-cache", channel="latest/edge"),
ops_test.model.deploy(
"nginx-ingress-integrator",
channel="latest/edge",
revision=133,
series="focal",
trust=True,
"nginx-ingress-integrator", channel="latest/edge", series="focal", config={
"service-hostname": hostname,
}, trust=True
),
)
await ops_test.model.wait_for_idle(
Expand Down Expand Up @@ -117,14 +116,14 @@ async def app_fixture(
ops_test.model.add_relation(f"{app_name}:redis-cache", "redis-cache"),
ops_test.model.add_relation(app_name, "nginx-ingress-integrator"),
)
await ops_test.model.wait_for_idle(status="active", raise_on_error=False)
# Install saml_groups plugin
# Disabling line-too-long lint since we are installing the plugin via gh release
await application.set_config(
{
"external_plugins": "https://github.com/canonical/flask-multipass-saml-groups/releases/download/1.2.1/flask_multipass_saml_groups-1.2.1-py3-none-any.whl" # noqa: E501 pylint: disable=line-too-long
}
)
await ops_test.model.wait_for_idle(status="active", raise_on_error=False)
yield application


Expand Down
9 changes: 4 additions & 5 deletions tests/integration/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,13 @@ async def test_indico_is_up(ops_test: OpsTest, app: Application):
Assume that the charm has already been built and is running.
"""
assert ops_test.model
# Read the IP address of indico
status = await ops_test.model.get_status()
unit = list(status.applications[app.name].units)[0]
address = status["applications"][app.name]["units"][unit]["address"]
# Send request to bootstrap page and set Host header to app_name (which the application
# expects)
response = requests.get(
f"http://{address}:8080/bootstrap", headers={"Host": f"{app.name}.local"}, timeout=10
"https://127.0.0.1/bootstrap",
headers={"Host": f"{app.name}.local"},
timeout=10,
verify=False, # nosec
)
assert response.status_code == 200

Expand Down
9 changes: 8 additions & 1 deletion tests/integration/test_s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@
@pytest.mark.asyncio
@pytest.mark.abort_on_fail
@pytest.mark.usefixtures("s3_integrator")
async def test_s3(app: Application, s3_integrator: Application, ops_test: OpsTest):
async def test_s3(app: Application, s3_integrator: Application, ops_test: OpsTest, hostname: str):
"""
arrange: given charm integrated with S3.
act: do nothing.
assert: the pebble plan matches the S3 values as configured by the integrator.
"""
assert ops_test.model
await ops_test.model.applications["nginx-ingress-integrator"].set_config(
{"service-hostname": hostname}
)
# The linter does not recognize wait_for_idle as a method,
# since ops_test has a model as Optional, so this error must be ignored.
await ops_test.model.wait_for_idle(status="active") # type: ignore[union-attr]
# Application actually does have units
return_code, stdout, _ = await ops_test.juju(
"ssh", "--container", app.name, app.units[0].name, "pebble", "plan" # type: ignore
Expand Down
34 changes: 13 additions & 21 deletions tests/integration/test_saml.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@
import re
import socket
from unittest.mock import patch
from urllib.parse import urlparse

import pytest
import requests
import urllib3.exceptions
from ops import Application
from pytest_operator.plugin import OpsTest


Expand All @@ -21,27 +19,22 @@
@pytest.mark.usefixtures("saml_integrator")
async def test_saml_auth( # pylint: disable=too-many-arguments, too-many-positional-arguments
ops_test: OpsTest,
app: Application,
saml_email: str,
saml_password: str,
requests_timeout: float,
external_url: str,
hostname: str,
):
"""
arrange: given charm in its initial state
act: configure a SAML target url and fire SAML authentication
assert: The SAML authentication process is executed successfully.
"""
# The linter does not recognize set_config as a method, so this errors must be ignored.
await app.set_config( # type: ignore[attr-defined] # pylint: disable=W0106
{"site_url": external_url}
)
# The linter does not recognize wait_for_idle as a method,
# since ops_test has a model as Optional, so this error must be ignored.
await ops_test.model.wait_for_idle(status="active") # type: ignore[union-attr]
assert ops_test.model
nginx_ingress_integrator_app = ops_test.model.applications["nginx-ingress-integrator"]
await nginx_ingress_integrator_app.set_config({"service-hostname": hostname})
await ops_test.model.wait_for_idle(status="active")
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

host = urlparse(external_url).netloc
original_getaddrinfo = socket.getaddrinfo

def patched_getaddrinfo(*args):
Expand All @@ -53,14 +46,14 @@ def patched_getaddrinfo(*args):
Returns:
Address information with localhost as the patched IP.
"""
if args[0] == host:
if args[0] == hostname:
return original_getaddrinfo("127.0.0.1", *args[1:])
return original_getaddrinfo(*args)

with patch.multiple(socket, getaddrinfo=patched_getaddrinfo), requests.session() as session:
session.get(f"https://{host}", verify=False)
session.get(f"https://{hostname}", verify=False)
login_page = session.get(
f"https://{host}/login",
f"https://{hostname}/login",
verify=False,
timeout=requests_timeout,
)
Expand Down Expand Up @@ -88,7 +81,7 @@ def patched_getaddrinfo(*args):
)
assert len(saml_response_matches), saml_callback.text
session.post(
f"https://{host}/multipass/saml/ubuntu/acs",
f"https://{hostname}/multipass/saml/ubuntu/acs",
data={
"RelayState": "None",
"SAMLResponse": saml_response_matches[0],
Expand All @@ -98,19 +91,18 @@ def patched_getaddrinfo(*args):
timeout=requests_timeout,
)
session.post(
f"https://{host}/multipass/saml/ubuntu/acs",
f"https://{hostname}/multipass/saml/ubuntu/acs",
data={"SAMLResponse": saml_response_matches[0], "SameSite": "1"},
verify=False,
timeout=requests_timeout,
)

dashboard_page = session.get(
f"https://{host}/register/ubuntu",
f"https://{hostname}/register/ubuntu",
verify=False,
timeout=requests_timeout,
)
assert dashboard_page.status_code == 200
# Revert SAML config for zap to be able to run
await app.set_config( # type: ignore[attr-defined] # pylint: disable=W0106
{"site_url": ""}
)
# await ops_test.model.remove_relation("indico", "saml_integrator")
# await nginx_ingress_integrator_app.reset_config(["service-hostname"])
4 changes: 2 additions & 2 deletions tests/unit/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def set_up_all_relations(self):
)

self.nginx_route_relation_id = self.harness.add_relation( # pylint: disable=W0201
"nginx-route", "ingress"
"nginx-route", "ingress", app_data={"service-hostname": "example.local"}
)

def is_ready(self, apps: List[str]):
Expand All @@ -77,8 +77,8 @@ def is_ready(self, apps: List[str]):

def set_relations_and_leader(self):
"""Set Indico relations, the leader and check container readiness."""
self.set_up_all_relations()
self.harness.set_leader(True)
self.set_up_all_relations()
self.is_ready(
[
"indico",
Expand Down
Loading