diff --git a/src/paas_charm/django/charm.py b/src/paas_charm/django/charm.py index 310291d..e2e4583 100644 --- a/src/paas_charm/django/charm.py +++ b/src/paas_charm/django/charm.py @@ -6,9 +6,10 @@ import pathlib import secrets import typing +from urllib.parse import urlsplit import ops -from pydantic import ConfigDict, Field, validator +from pydantic import BaseModel, ConfigDict, Field, validator from paas_charm._gunicorn.charm import GunicornBase from paas_charm.framework import FrameworkConfig @@ -75,6 +76,21 @@ def get_cos_dir(self) -> str: """ return str((pathlib.Path(__file__).parent / "cos").absolute()) + def get_framework_config(self) -> BaseModel: + """Return the framework related configurations. + + The method is overridden to inject the base_url, that can be an ingress URL or a k8s svc + url, to the list of allowed hosts. + + Returns: + Framework related configurations. + """ + base_model = super().get_framework_config() + url = urlsplit(self._base_url) + # base_model can be downcasted to a DjangoConfig, and allowed_hosts is really a list. + base_model.allowed_hosts.append(url.hostname) # type: ignore + return base_model + def is_ready(self) -> bool: """Check if the charm is ready to start the workload application. diff --git a/tests/integration/django/test_django.py b/tests/integration/django/test_django.py index 7479bb8..7048d66 100644 --- a/tests/integration/django/test_django.py +++ b/tests/integration/django/test_django.py @@ -23,7 +23,7 @@ async def test_django_webserver_timeout(django_app, get_unit_ips, timeout): """ arrange: build and deploy the django charm, and change the gunicorn timeout configuration. - act: send long-running requests to the django application managed by the flask charm. + act: send long-running requests to the django application managed by the django charm. assert: the gunicorn should restart the worker if the request duration exceeds the timeout. """ safety_timeout = timeout + 3 @@ -50,7 +50,9 @@ async def test_django_database_migration(django_app, get_unit_ips): "update_config, expected_settings", [ pytest.param( - {"django-allowed-hosts": "*,test"}, {"ALLOWED_HOSTS": ["*", "test"]}, id="allowed-host" + {"django-allowed-hosts": "test"}, + {"ALLOWED_HOSTS": ["test", "django-k8s.testing"]}, + id="allowed-host", ), pytest.param({"django-secret-key": "test"}, {"SECRET_KEY": "test"}, id="secret-key"), ], @@ -66,7 +68,11 @@ async def test_django_charm_config(django_app, expected_settings, get_unit_ips): for unit_ip in await get_unit_ips(django_app.name): for setting, value in expected_settings.items(): url = f"http://{unit_ip}:8000/settings/{setting}" - assert value == requests.get(url, timeout=5).json() + # it is necessary to specify a host header if the IP or '*' is not in ALLOWED_HOSTS + assert ( + value + == requests.get(url, headers={"Host": "django-k8s.testing"}, timeout=5).json() + ) async def test_django_create_superuser(django_app, get_unit_ips, run_action): diff --git a/tests/unit/django/test_charm.py b/tests/unit/django/test_charm.py index bdacdc4..8d66364 100644 --- a/tests/unit/django/test_charm.py +++ b/tests/unit/django/test_charm.py @@ -21,20 +21,31 @@ from .constants import DEFAULT_LAYER TEST_DJANGO_CONFIG_PARAMS = [ - pytest.param({}, {"DJANGO_SECRET_KEY": "test", "DJANGO_ALLOWED_HOSTS": "[]"}, id="default"), + pytest.param( + {}, + {"DJANGO_SECRET_KEY": "test", "DJANGO_ALLOWED_HOSTS": '["django-k8s.none"]'}, + id="default", + ), pytest.param( {"django-allowed-hosts": "test.local"}, - {"DJANGO_SECRET_KEY": "test", "DJANGO_ALLOWED_HOSTS": '["test.local"]'}, + { + "DJANGO_SECRET_KEY": "test", + "DJANGO_ALLOWED_HOSTS": '["test.local", "django-k8s.none"]', + }, id="allowed-hosts", ), pytest.param( {"django-debug": True}, - {"DJANGO_SECRET_KEY": "test", "DJANGO_ALLOWED_HOSTS": "[]", "DJANGO_DEBUG": "true"}, + { + "DJANGO_SECRET_KEY": "test", + "DJANGO_ALLOWED_HOSTS": '["django-k8s.none"]', + "DJANGO_DEBUG": "true", + }, id="debug", ), pytest.param( {"django-secret-key": "foobar"}, - {"DJANGO_SECRET_KEY": "foobar", "DJANGO_ALLOWED_HOSTS": "[]"}, + {"DJANGO_SECRET_KEY": "foobar", "DJANGO_ALLOWED_HOSTS": '["django-k8s.none"]'}, id="secret-key", ), ] @@ -142,3 +153,52 @@ def test_required_database_integration(harness_no_integrations: Harness): assert harness.model.unit.status == ops.BlockedStatus( "Django requires a database integration to work" ) + + +def test_allowed_hosts_base_hostname_updates_correctly(harness: Harness): + """ + arrange: Deploy a Django charm without an ingress integration + act: Add a new ingress integration + assert: The allowed hosts env var should match the url of the ingress integration + act: Update the url in the ingress integration + assert: The allowed hosts env var should match the new url of the ingress integration + """ + postgresql_relation_data = { + "database": "test-database", + "endpoints": "test-postgresql:5432,test-postgresql-2:5432", + "password": "test-password", + "username": "test-username", + } + harness.add_relation("postgresql", "postgresql-k8s", app_data=postgresql_relation_data) + container = harness.model.unit.get_container("django-app") + container.add_layer("a_layer", DEFAULT_LAYER) + harness.set_model_name("flask-model") + harness.begin_with_initial_hooks() + + # The initial allowed hosts matches the k8s service name. + plan = container.get_plan() + env = plan.to_dict()["services"]["django"]["environment"] + assert env["DJANGO_ALLOWED_HOSTS"] == '["django-k8s.flask-model"]' + + # Add a relation and the allowed hosts should be updated to the ingress url + harness.add_network("10.0.0.10", endpoint="ingress") + relation_id = harness.add_relation( + "ingress", + "nginx-ingress-integrator", + app_data={"ingress": '{"url": "http://oldjuju.test/"}'}, + ) + + plan = container.get_plan() + env = plan.to_dict()["services"]["django"]["environment"] + assert env["DJANGO_ALLOWED_HOSTS"] == '["oldjuju.test"]' + + # Updating the ingress url to a new url should update the allowed hosts. + harness.update_relation_data( + relation_id, + app_or_unit="nginx-ingress-integrator", + key_values={"ingress": '{"url": "http://newjuju.test/"}'}, + ) + + plan = container.get_plan() + env = plan.to_dict()["services"]["django"]["environment"] + assert env["DJANGO_ALLOWED_HOSTS"] == '["newjuju.test"]'