diff --git a/.env-devel b/.env-devel index 94abfdde6a6..3c743a086b1 100644 --- a/.env-devel +++ b/.env-devel @@ -269,11 +269,15 @@ VENDOR_DEV_MANUAL_IMAGE=containous/whoami VENDOR_DEV_MANUAL_REPLICAS=1 VENDOR_DEV_MANUAL_SUBDOMAIN=manual -## VENDOR DEVELOPMENT SERVICES --- +## WEBSERVER SERVICES VARIANTS --- WB_API_WEBSERVER_HOST=wb-api-server WB_API_WEBSERVER_PORT=8080 +WB_AUTH_WEBSERVER_HOST=wb-auth +WB_AUTH_WEBSERVER_PORT=8080 +WB_AUTH_LOGLEVEL=INFO + WB_GC_ACTIVITY=null WB_GC_ANNOUNCEMENTS=0 WB_GC_CATALOG=null diff --git a/packages/pytest-simcore/src/pytest_simcore/environment_configs.py b/packages/pytest-simcore/src/pytest_simcore/environment_configs.py index d953465d96a..6a87e3536e9 100644 --- a/packages/pytest-simcore/src/pytest_simcore/environment_configs.py +++ b/packages/pytest-simcore/src/pytest_simcore/environment_configs.py @@ -9,6 +9,7 @@ from typing import Any import pytest +from faker import Faker from .helpers.monkeypatch_envs import load_dotenv, setenvs_from_dict from .helpers.typing_env import EnvVarsDict @@ -112,7 +113,7 @@ def service_name(project_slug_dir: Path) -> str: @pytest.fixture(scope="session") -def services_docker_compose_dict(services_docker_compose_file: Path) -> EnvVarsDict: +def docker_compose_services_dict(services_docker_compose_file: Path) -> EnvVarsDict: # NOTE: By keeping import here, this library is ONLY required when the fixture is used import yaml @@ -121,11 +122,30 @@ def services_docker_compose_dict(services_docker_compose_file: Path) -> EnvVarsD return content +@pytest.fixture +def docker_compose_service_hostname( + faker: Faker, service_name: str, docker_compose_services_dict: dict[str, Any] +) -> str: + """Evaluates `hostname` from docker-compose service""" + hostname_template = docker_compose_services_dict["services"][service_name][ + "hostname" + ] + + # Generate fake values to replace Docker Swarm template variables + node_hostname = faker.hostname(levels=1) + task_slot = faker.random_int(min=0, max=10) + + # Replace the Docker Swarm template variables with faker values + return hostname_template.replace("{{.Node.Hostname}}", node_hostname).replace( + "{{.Task.Slot}}", str(task_slot) + ) + + @pytest.fixture def docker_compose_service_environment_dict( - services_docker_compose_dict: dict[str, Any], - env_devel_dict: EnvVarsDict, + docker_compose_services_dict: dict[str, Any], service_name: str, + env_devel_dict: EnvVarsDict, env_devel_file: Path, ) -> EnvVarsDict: """Returns env vars dict from the docker-compose `environment` section @@ -133,10 +153,10 @@ def docker_compose_service_environment_dict( - env_devel_dict in environment_configs plugin - service_name needs to be defined """ - service = services_docker_compose_dict["services"][service_name] + service = docker_compose_services_dict["services"][service_name] def _substitute(key, value) -> tuple[str, str]: - if m := re.match(r"\${([^{}:-]\w+)", value): + if m := re.match(r"\${([^{}:-]\w+)", f"{value}"): expected_env_var = m.group(1) try: # NOTE: if this raises, then the RHS env-vars in the docker-compose are diff --git a/packages/pytest-simcore/src/pytest_simcore/repository_paths.py b/packages/pytest-simcore/src/pytest_simcore/repository_paths.py index 6112cef627b..0f52de8f844 100644 --- a/packages/pytest-simcore/src/pytest_simcore/repository_paths.py +++ b/packages/pytest-simcore/src/pytest_simcore/repository_paths.py @@ -85,6 +85,14 @@ def services_docker_compose_file(services_dir: Path) -> Path: return dcpath +@pytest.fixture(scope="session") +def services_docker_compose_dev_vendors_file(osparc_simcore_services_dir: Path) -> Path: + """Path to osparc-simcore/services/docker-compose-dev-vendors.yml file""" + dcpath = osparc_simcore_services_dir / "docker-compose-dev-vendors.yml" + assert dcpath.exists() + return dcpath + + @pytest.fixture(scope="session") def pylintrc(osparc_simcore_root_dir: Path) -> Path: pylintrc = osparc_simcore_root_dir / ".pylintrc" diff --git a/services/docker-compose-deploy.yml b/services/docker-compose-deploy.yml index e6c21da36db..2ad1ca974b9 100644 --- a/services/docker-compose-deploy.yml +++ b/services/docker-compose-deploy.yml @@ -17,8 +17,12 @@ services: image: ${DOCKER_REGISTRY:-itisfoundation}/director:${DOCKER_IMAGE_TAG:-latest} director-v2: image: ${DOCKER_REGISTRY:-itisfoundation}/director-v2:${DOCKER_IMAGE_TAG:-latest} + docker-api-proxy: + image: ${DOCKER_REGISTRY:-itisfoundation}/docker-api-proxy:${DOCKER_IMAGE_TAG:-latest} dynamic-sidecar: image: ${DOCKER_REGISTRY:-itisfoundation}/dynamic-sidecar:${DOCKER_IMAGE_TAG:-latest} + dynamic-scheduler: + image: ${DOCKER_REGISTRY:-itisfoundation}/dynamic-scheduler:${DOCKER_IMAGE_TAG:-latest} efs-guardian: image: ${DOCKER_REGISTRY:-itisfoundation}/efs-guardian:${DOCKER_IMAGE_TAG:-latest} invitations: @@ -29,10 +33,6 @@ services: image: ${DOCKER_REGISTRY:-itisfoundation}/notifications:${DOCKER_IMAGE_TAG:-latest} payments: image: ${DOCKER_REGISTRY:-itisfoundation}/payments:${DOCKER_IMAGE_TAG:-latest} - dynamic-scheduler: - image: ${DOCKER_REGISTRY:-itisfoundation}/dynamic-scheduler:${DOCKER_IMAGE_TAG:-latest} - docker-api-proxy: - image: ${DOCKER_REGISTRY:-itisfoundation}/docker-api-proxy:${DOCKER_IMAGE_TAG:-latest} resource-usage-tracker: image: ${DOCKER_REGISTRY:-itisfoundation}/resource-usage-tracker:${DOCKER_IMAGE_TAG:-latest} service-integration: diff --git a/services/docker-compose-dev-vendors.yml b/services/docker-compose-dev-vendors.yml index 2c885c0ea95..338bccfd4aa 100644 --- a/services/docker-compose-dev-vendors.yml +++ b/services/docker-compose-dev-vendors.yml @@ -15,7 +15,7 @@ services: - traefik.enable=true - traefik.swarm.network=${SWARM_STACK_NAME}_default # auth: https://doc.traefik.io/traefik/middlewares/http/forwardauth - - traefik.http.middlewares.${SWARM_STACK_NAME}_manual-auth.forwardauth.address=http://${WEBSERVER_HOST}:${WEBSERVER_PORT}/v0/auth:check + - traefik.http.middlewares.${SWARM_STACK_NAME}_manual-auth.forwardauth.address=http://${WB_AUTH_WEBSERVER_HOST}:${WB_AUTH_WEBSERVER_PORT}/v0/auth:check - traefik.http.middlewares.${SWARM_STACK_NAME}_manual-auth.forwardauth.trustForwardHeader=true - traefik.http.middlewares.${SWARM_STACK_NAME}_manual-auth.forwardauth.authResponseHeaders=Set-Cookie,osparc-sc2 # routing diff --git a/services/docker-compose.devel.yml b/services/docker-compose.devel.yml index 2b994b99ffd..085a78ef0c7 100644 --- a/services/docker-compose.devel.yml +++ b/services/docker-compose.devel.yml @@ -150,9 +150,13 @@ services: WEBSERVER_LOGLEVEL: DEBUG WEBSERVER_PROFILING: ${WEBSERVER_PROFILING} WEBSERVER_REMOTE_DEBUGGING_PORT: 3000 - WEBSERVER_FUNCTIONS: ${WEBSERVER_FUNCTIONS} + wb-auth: + volumes: *webserver_volumes_devel + environment: + <<: *webserver_environment_devel + wb-api-server: volumes: *webserver_volumes_devel environment: diff --git a/services/docker-compose.local.yml b/services/docker-compose.local.yml index 4f5d11e6b90..5f84ba9bf10 100644 --- a/services/docker-compose.local.yml +++ b/services/docker-compose.local.yml @@ -166,6 +166,13 @@ services: - traefik.http.routers.${SWARM_STACK_NAME}_webserver_local.priority=9 - traefik.http.routers.${SWARM_STACK_NAME}_webserver_local.middlewares=${SWARM_STACK_NAME}_gzip@swarm, ${SWARM_STACK_NAME_NO_HYPHEN}_sslheader@swarm, ${SWARM_STACK_NAME}_webserver_retry + wb-auth: + environment: + <<: *webserver_environment_local + ports: + - "8080" + - "3024:3000" + wb-api-server: environment: <<: *webserver_environment_local diff --git a/services/docker-compose.yml b/services/docker-compose.yml index e7cf3774ca7..9596a6e35ed 100644 --- a/services/docker-compose.yml +++ b/services/docker-compose.yml @@ -964,7 +964,7 @@ services: WEBSERVER_GARBAGE_COLLECTOR: ${WB_DB_EL_GARBAGE_COLLECTOR} WEBSERVER_GROUPS: ${WB_DB_EL_GROUPS} WEBSERVER_INVITATIONS: ${WB_DB_EL_INVITATIONS} - WEBSERVER_LICENSES: null + WEBSERVER_LICENSES: "null" WEBSERVER_LOGIN: ${WB_DB_EL_LOGIN} WEBSERVER_PAYMENTS: ${WB_DB_EL_PAYMENTS} WEBSERVER_NOTIFICATIONS: ${WB_DB_EL_NOTIFICATIONS} @@ -1077,7 +1077,7 @@ services: WEBSERVER_GROUPS: ${WB_GC_GROUPS} WEBSERVER_HOST: ${WEBSERVER_HOST} WEBSERVER_INVITATIONS: ${WB_GC_INVITATIONS} - WEBSERVER_LICENSES: null + WEBSERVER_LICENSES: "null" WEBSERVER_LOGIN: ${WB_GC_LOGIN} WEBSERVER_LOGLEVEL: ${WB_GC_LOGLEVEL} WEBSERVER_NOTIFICATIONS: ${WB_GC_NOTIFICATIONS} @@ -1099,6 +1099,70 @@ services: - default - interactive_services_subnet + wb-auth: + image: ${DOCKER_REGISTRY:-itisfoundation}/webserver:${DOCKER_IMAGE_TAG:-latest} + init: true + hostname: "auth-{{.Node.Hostname}}-{{.Task.Slot}}" # the hostname is used in conjonction with other services and must be unique see https://github.com/ITISFoundation/osparc-simcore/pull/5931 + environment: + WEBSERVER_APP_FACTORY_NAME: WEBSERVER_AUTHZ_APP_FACTORY + WEBSERVER_LOGLEVEL: ${WB_AUTH_LOGLEVEL} + GUNICORN_CMD_ARGS: ${WEBSERVER_GUNICORN_CMD_ARGS} + + # WEBSERVER_DB + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_ENDPOINT: ${POSTGRES_ENDPOINT} + POSTGRES_HOST: ${POSTGRES_HOST} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_PORT: ${POSTGRES_PORT} + POSTGRES_USER: ${POSTGRES_USER} + + # WEBSERVER_REST + REST_SWAGGER_API_DOC_ENABLED: 0 + + # WEBSERVER_SERVER_HOST + WEBSERVER_HOST: ${WB_AUTH_WEBSERVER_HOST} + WEBSERVER_PORT: ${WB_AUTH_WEBSERVER_PORT} + + # WEBSERVER_SESSION Enabled + SESSION_SECRET_KEY: ${WEBSERVER_SESSION_SECRET_KEY} + SESSION_COOKIE_MAX_AGE: ${SESSION_COOKIE_MAX_AGE} + SESSION_COOKIE_SAMESITE: ${SESSION_COOKIE_SAMESITE} + SESSION_COOKIE_SECURE: ${SESSION_COOKIE_SECURE} + SESSION_COOKIE_HTTPONLY: ${SESSION_COOKIE_HTTPONLY} + + + WEBSERVER_ACTIVITY: "null" + WEBSERVER_ANNOUNCEMENTS: 0 + WEBSERVER_CATALOG: "null" + WEBSERVER_DB_LISTENER: 0 + WEBSERVER_DIRECTOR_V2: "null" + WEBSERVER_EMAIL: "null" + WEBSERVER_EXPORTER: "null" + WEBSERVER_FOLDERS: 0 + WEBSERVER_FRONTEND: "null" + WEBSERVER_FUNCTIONS: 0 + WEBSERVER_GARBAGE_COLLECTOR: "null" + WEBSERVER_GROUPS: 0 + WEBSERVER_INVITATIONS: "null" + WEBSERVER_LICENSES: "null" + WEBSERVER_LOGIN: "null" + WEBSERVER_NOTIFICATIONS: 0 + WEBSERVER_PAYMENTS: "null" + WEBSERVER_PRODUCTS: 1 + WEBSERVER_PROJECTS: "null" + WEBSERVER_PUBLICATIONS: 0 + WEBSERVER_RABBITMQ: "null" + WEBSERVER_REALTIME_COLLABORATION: "null" + WEBSERVER_REDIS: "null" # TODO: cache? + WEBSERVER_RESOURCE_USAGE_TRACKER: "null" + WEBSERVER_SCICRUNCH: "null" + WEBSERVER_SOCKETIO: 0 + WEBSERVER_STATICWEB: "null" + WEBSERVER_STORAGE: "null" + WEBSERVER_STUDIES_DISPATCHER: "null" + WEBSERVER_TAGS: 0 + WEBSERVER_USERS: "null" + agent: image: ${DOCKER_REGISTRY:-itisfoundation}/agent:${DOCKER_IMAGE_TAG:-latest} init: true diff --git a/services/web/server/src/simcore_service_webserver/_meta.py b/services/web/server/src/simcore_service_webserver/_meta.py index 2b6861a05b5..33eab823fb2 100644 --- a/services/web/server/src/simcore_service_webserver/_meta.py +++ b/services/web/server/src/simcore_service_webserver/_meta.py @@ -23,8 +23,6 @@ api_version_prefix: str = API_VTAG -# kids drawings :-) - WELCOME_MSG = r""" _ _ _ | | | | | | @@ -45,6 +43,7 @@ (_)) __((/ __| | (_ || (__ \___| \___| + """ WELCOME_DB_LISTENER_MSG = r""" @@ -54,5 +53,17 @@ | | | _ <___| |--| |___ \- -| __| | | __| _ < |_____\_____/ \_____\___<_____/|__|\_____\__|__\_____\__|\_/ - """ + +# SEE https://patorjk.com/software/taag/#p=display&f=BlurVision%20ASCII&t=Auth%0A +WELCOME_AUTH_APP_MSG = r""" + ░▒▓██████▓▒░░▒▓█▓▒░░▒▓█▓▒░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░ +░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ +░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ +░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓████████▓▒░ +░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ +░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ +░▒▓█▓▒░░▒▓█▓▒░░▒▓██████▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ {} +""".format( + f"v{__version__}" +) diff --git a/services/web/server/src/simcore_service_webserver/application.py b/services/web/server/src/simcore_service_webserver/application.py index 96056e14c4a..9c5b1bbfbd3 100644 --- a/services/web/server/src/simcore_service_webserver/application.py +++ b/services/web/server/src/simcore_service_webserver/application.py @@ -2,6 +2,7 @@ """Main application""" import logging +from collections.abc import Callable from pprint import pformat from typing import Any @@ -11,7 +12,13 @@ setup_realtime_collaboration, ) -from ._meta import WELCOME_DB_LISTENER_MSG, WELCOME_GC_MSG, WELCOME_MSG, info +from ._meta import ( + WELCOME_AUTH_APP_MSG, + WELCOME_DB_LISTENER_MSG, + WELCOME_GC_MSG, + WELCOME_MSG, + info, +) from .activity.plugin import setup_activity from .announcements.plugin import setup_announcements from .api_keys.plugin import setup_api_keys @@ -62,18 +69,29 @@ _logger = logging.getLogger(__name__) -async def _welcome_banner(app: web.Application): - settings = get_application_settings(app) - print(WELCOME_MSG, flush=True) # noqa: T201 - if settings.WEBSERVER_GARBAGE_COLLECTOR: - print("with", WELCOME_GC_MSG, flush=True) # noqa: T201 - if settings.WEBSERVER_DB_LISTENER: - print("with", WELCOME_DB_LISTENER_MSG, flush=True) # noqa: T201 +def _create_welcome_banner(banner_msg: str) -> Callable: + """Creates a welcome banner function with optional GC and DB listener messages""" + + async def _welcome_banner(app: web.Application): + settings = get_application_settings(app) + + print(banner_msg, flush=True) # noqa: T201 + if settings.WEBSERVER_GARBAGE_COLLECTOR: + print("with", WELCOME_GC_MSG, flush=True) # noqa: T201 + if settings.WEBSERVER_DB_LISTENER: + print("with", WELCOME_DB_LISTENER_MSG, flush=True) # noqa: T201 + + return _welcome_banner + + +def _create_finished_banner() -> Callable: + """Creates a finished banner function""" + async def _finished_banner(app: web.Application): + assert app # nosec + print(info.get_finished_banner(), flush=True) # noqa: T201 -async def _finished_banner(app: web.Application): - assert app # nosec - print(info.get_finished_banner(), flush=True) # noqa: T201 + return _finished_banner def create_application() -> web.Application: @@ -166,8 +184,8 @@ def create_application() -> web.Application: setup_realtime_collaboration(app) # NOTE: *last* events - app.on_startup.append(_welcome_banner) - app.on_shutdown.append(_finished_banner) + app.on_startup.append(_create_welcome_banner(WELCOME_MSG)) + app.on_shutdown.append(_create_finished_banner()) _logger.debug("Routes in app: \n %s", pformat(app.router.named_resources())) @@ -183,8 +201,8 @@ def create_application_auth() -> web.Application: setup_login_auth(app) # NOTE: *last* events - app.on_startup.append(_welcome_banner) - app.on_shutdown.append(_finished_banner) + app.on_startup.append(_create_welcome_banner(WELCOME_AUTH_APP_MSG)) + app.on_shutdown.append(_create_finished_banner()) _logger.debug( "Routes in application-auth: \n %s", pformat(app.router.named_resources()) diff --git a/services/web/server/tests/unit/with_dbs/03/test_login_auth_app.py b/services/web/server/tests/unit/with_dbs/03/test_login_auth_app.py index 9a69ba26b28..193cdca0de1 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_login_auth_app.py +++ b/services/web/server/tests/unit/with_dbs/03/test_login_auth_app.py @@ -4,27 +4,94 @@ # pylint: disable=unused-argument # pylint: disable=unused-variable +import logging from collections.abc import Callable +from pathlib import Path import pytest import pytest_asyncio import sqlalchemy as sa +import yaml from aiohttp import web from aiohttp.test_utils import TestClient, TestServer from pytest_simcore.helpers.assert_checks import assert_status +from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict from pytest_simcore.helpers.typing_env import EnvVarsDict from pytest_simcore.helpers.webserver_login import UserInfoDict from servicelib.aiohttp import status from simcore_service_webserver.application import create_application_auth +from simcore_service_webserver.application_settings import ApplicationSettings +from simcore_service_webserver.application_settings_utils import AppConfigDict from simcore_service_webserver.security import security_web +@pytest.fixture +def service_name() -> str: + return "wb-auth" + + +@pytest.fixture +def app_environment_for_wb_authz_service_dict( + docker_compose_service_environment_dict: EnvVarsDict, + docker_compose_service_hostname: str, + default_app_cfg: AppConfigDict, +) -> EnvVarsDict: + + postgres_cfg = default_app_cfg["db"]["postgres"] + + assert ( + docker_compose_service_environment_dict["WEBSERVER_APP_FACTORY_NAME"] + == "WEBSERVER_AUTHZ_APP_FACTORY" + ) + + return { + **docker_compose_service_environment_dict, + # NOTE: TEST-stack uses different env-vars + # this is temporary here until we get rid of config files + # SEE https://github.com/ITISFoundation/osparc-simcore/issues/8129 + "POSTGRES_DB": postgres_cfg["database"], + "POSTGRES_HOST": postgres_cfg["host"], + "POSTGRES_PORT": postgres_cfg["port"], + "POSTGRES_USER": postgres_cfg["user"], + "POSTGRES_PASSWORD": postgres_cfg["password"], + "HOSTNAME": docker_compose_service_hostname, + # TODO: add everything coming from Dockerfile? + } + + +@pytest.fixture +def app_environment_for_wb_authz_service( + monkeypatch: pytest.MonkeyPatch, + app_environment_for_wb_authz_service_dict: EnvVarsDict, + service_name: str, +) -> EnvVarsDict: + """Mocks the environment variables for the auth app service (considering docker-compose's environment).""" + + mocked_envs = setenvs_from_dict( + monkeypatch, {**app_environment_for_wb_authz_service_dict} + ) + + # test how service will load + settings = ApplicationSettings.create_from_envs() + + logging.info( + "Application settings:\n%s", + settings.model_dump_json(indent=2), + ) + + assert service_name == settings.WEBSERVER_HOST + assert settings.WEBSERVER_DB is not None + assert settings.WEBSERVER_SESSION is not None + assert settings.WEBSERVER_SECURITY is not None + + return mocked_envs + + @pytest.fixture async def auth_app( - app_environment: EnvVarsDict, - disable_static_webserver: Callable, + app_environment_for_wb_authz_service: EnvVarsDict, ) -> web.Application: - assert app_environment + assert app_environment_for_wb_authz_service # creates auth application instead app = create_application_auth() @@ -33,18 +100,16 @@ async def auth_app( url = app.router["check_auth"].url_for() assert url.path == "/v0/auth:check" - disable_static_webserver(app) return app @pytest_asyncio.fixture(loop_scope="function", scope="function") async def web_server( - postgres_db: sa.engine.Engine, + postgres_db: sa.engine.Engine, # sets up postgres database auth_app: web.Application, webserver_test_server_port: int, # tools aiohttp_server: Callable, - mocked_send_email: None, ) -> TestServer: # Overrides tests/unit/with_dbs/context.py:web_server fixture @@ -88,3 +153,68 @@ async def test_check_endpoint_in_auth_app(client: TestClient, user: UserInfoDict response = await client.get("/v0/auth:check") await assert_status(response, status.HTTP_401_UNAUTHORIZED) + + +def test_docker_compose_dev_vendors_forwardauth_configuration( + services_docker_compose_dev_vendors_file: Path, + env_devel_dict: EnvVarsDict, +): + """Test that manual service forwardauth.address points to correct WB_AUTH_WEBSERVER_HOST and port.""" + + # Load docker-compose file + compose_config = yaml.safe_load( + services_docker_compose_dev_vendors_file.read_text() + ) + + # Get the manual service configuration + manual_service = compose_config.get("services", {}).get("manual") + assert ( + manual_service is not None + ), "Manual service not found in docker-compose-dev-vendors.yml" + + # Extract forwardauth.address from deploy labels + deploy_labels = manual_service.get("deploy", {}).get("labels", []) + forwardauth_address_label = None + + for label in deploy_labels: + if "forwardauth.address=" in label: + forwardauth_address_label = label + break + + assert ( + forwardauth_address_label is not None + ), "forwardauth.address label not found in manual service" + + # Parse the forwardauth address + # Expected format: traefik.http.middlewares.${SWARM_STACK_NAME}_manual-auth.forwardauth.address=http://${WB_AUTH_WEBSERVER_HOST}:${WB_AUTH_WEBSERVER_PORT}/v0/auth:check + address_part = forwardauth_address_label.split("forwardauth.address=")[1] + + # Verify it contains the expected pattern + assert ( + "${WB_AUTH_WEBSERVER_HOST}" in address_part + ), "forwardauth.address should reference WB_AUTH_WEBSERVER_HOST" + assert ( + "${WB_AUTH_WEBSERVER_PORT}" in address_part + ), "forwardauth.address should reference WB_AUTH_WEBSERVER_PORT" + assert ( + "/v0/auth:check" in address_part + ), "forwardauth.address should point to /v0/auth:check endpoint" + + # Verify the full expected pattern + expected_pattern = ( + "http://${WB_AUTH_WEBSERVER_HOST}:${WB_AUTH_WEBSERVER_PORT}/v0/auth:check" + ) + assert ( + address_part == expected_pattern + ), f"forwardauth.address should be '{expected_pattern}', got '{address_part}'" + + # Verify that WB_AUTH_WEBSERVER_HOST and WB_AUTH_WEBSERVER_PORT are configured in the .env-devel file! + wb_auth_host = env_devel_dict.get("WB_AUTH_WEBSERVER_HOST") + wb_auth_port = env_devel_dict.get("WB_AUTH_WEBSERVER_PORT") + + assert ( + wb_auth_host is not None + ), "WB_AUTH_WEBSERVER_HOST should be configured in test environment" + assert ( + wb_auth_port is not None + ), "WB_AUTH_WEBSERVER_PORT should be configured in test environment"