From 7410b790a0b3091da7cf42bf0367b4a65d43a21e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 18 Dec 2024 09:32:02 +0100 Subject: [PATCH] Support multi-tenant in simple mode --- .github/workflows/main.yaml | 1 + bin/build-l10n | 91 +++++++------------ bin/eval-templates | 11 +++ doc/integrator/create_application.rst | 4 +- doc/integrator/features.rst | 2 +- ...ulti_organization.rst => multi_tenant.rst} | 37 ++++++-- doc/integrator/ngeo.rst | 2 +- .../geoportal/Dockerfile | 3 +- .../__init__.py | 2 + .../multi_organization.py | 2 +- .../multi_tenant.py | 7 ++ .../{{cookiecutter.project}}/.dockerignore | 3 + .../{{cookiecutter.project}}/Dockerfile | 1 + .../create/{{cookiecutter.project}}/Makefile | 6 +- .../docker-compose-lib.yaml | 2 + .../{{cookiecutter.project}}/env.project | 3 + .../scripts/multi-tenant-update-po | 44 +++++++++ 17 files changed, 146 insertions(+), 75 deletions(-) rename doc/integrator/{multi_organization.rst => multi_tenant.rst} (88%) create mode 100644 geoportal/c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/multi_tenant.py create mode 100755 geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/multi-tenant-update-po diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 8bf02cb141..188391a1ff 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -97,6 +97,7 @@ jobs: # Tests - run: make preparetest + - run: docker compose logs --timestamps config - run: docker compose logs --timestamps if: failure() - run: make tests-commons diff --git a/bin/build-l10n b/bin/build-l10n index ce85daa550..85f1aa9c9a 100755 --- a/bin/build-l10n +++ b/bin/build-l10n @@ -3,7 +3,6 @@ import argparse import glob import os -import shutil import subprocess @@ -14,91 +13,71 @@ def main() -> None: parser.add_argument("--dry-run", action="store_true", help="run in dry-run mode") args = parser.parse_args() - all_suffix = [""] if args.suffix is None else args.suffix + all_suffix = [""] if args.suffix is None else ["", *args.suffix] package_base_path = f"/tmp/config/geoportal/{args.package}_geoportal" # nosec base_path = f"{package_base_path}/locale" + dest_package = "geomapfishapp" if os.environ.get("SIMPLE", "false").lower() == "true" else args.package for lang in os.listdir(base_path): for suffix in all_suffix: - if args.dry_run: - print( - f"{base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-client{suffix}.po => " - f"{base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-client{suffix}.mo" - ) - else: - subprocess.run( - [ - "msgfmt", - "-o", - f"{lang}/LC_MESSAGES/{args.package}_geoportal-client{suffix}.mo", - f"{lang}/LC_MESSAGES/{args.package}_geoportal-client{suffix}.po", - ], - cwd=base_path, - check=True, - ) - - if os.path.exists(f"{base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-server{suffix}.mo"): + if os.path.exists(f"{base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-client{suffix}.po"): if args.dry_run: print( - f"{base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-server{suffix}.po => " - f"{base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-server{suffix}.mo" + f"{base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-client{suffix}.po => " + f"{base_path}/{lang}/LC_MESSAGES/{dest_package}_geoportal-client{suffix}.mo" ) else: subprocess.run( [ "msgfmt", "-o", - f"{lang}/LC_MESSAGES/{args.package}_geoportal-server{suffix}.mo", - f"{lang}/LC_MESSAGES/{args.package}_geoportal-server{suffix}.po", + f"{lang}/LC_MESSAGES/{dest_package}_geoportal-client{suffix}.mo", + f"{lang}/LC_MESSAGES/{args.package}_geoportal-client{suffix}.po", ], cwd=base_path, check=True, ) - if args.dry_run: - print( - f"/opt/c2cgeoportal/geoportal/c2cgeoportal_geoportal/locale/{lang}/LC_MESSAGES/ngeo.po, " - f"{base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-client{suffix}.po => " - f"{package_base_path}/static/{lang}{suffix}.json" - ) - else: - with open(f"{package_base_path}/static/{lang}{suffix}.json", "w", encoding="utf-8") as out: + if os.path.exists(f"{base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-server{suffix}.mo"): + if args.dry_run: + print( + f"{base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-server{suffix}.po => " + f"{base_path}/{lang}/LC_MESSAGES/{dest_package}_geoportal-server{suffix}.mo" + ) + else: subprocess.run( [ - "compile-catalog", - f"/opt/c2cgeoportal/geoportal/c2cgeoportal_geoportal/locale/{lang}/LC_MESSAGES/ngeo.po", - f"{lang}/LC_MESSAGES/{args.package}_geoportal-client{suffix}.po", + "msgfmt", + "-o", + f"{lang}/LC_MESSAGES/{dest_package}_geoportal-server{suffix}.mo", + f"{lang}/LC_MESSAGES/{args.package}_geoportal-server{suffix}.po", ], - stdout=out, cwd=base_path, check=True, ) - if os.environ.get("SIMPLE", "false").lower() == "true": - if args.suffix is not None: - print("ERROR: simple mod is not compatible with suffix") - if args.dry_run: - print( - f"mv {base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-client.mo" - f"{base_path}/{lang}/LC_MESSAGES/geomapfishapp_geoportal-client.mo" - ) - else: - shutil.move( - f"{base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-client.mo", - f"{base_path}/{lang}/LC_MESSAGES/geomapfishapp_geoportal-client.mo", - ) - if os.path.exists(f"{base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-server.mo"): + if os.path.exists(f"{base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-client{suffix}.po"): if args.dry_run: print( - f"mv {base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-server.mo" - f"{base_path}/{lang}/LC_MESSAGES/geomapfishapp_geoportal-server.mo" + f"/opt/c2cgeoportal/geoportal/c2cgeoportal_geoportal/locale/{lang}/LC_MESSAGES/ngeo.po, " + f"{base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-client{suffix}.po => " + f"{package_base_path}/static/{lang}{suffix}.json" ) else: - shutil.move( - f"{base_path}/{lang}/LC_MESSAGES/{args.package}_geoportal-server.mo", - f"{base_path}/{lang}/LC_MESSAGES/geomapfishapp_geoportal-server.mo", - ) + with open( + f"{package_base_path}/static/{lang}{suffix}.json", "w", encoding="utf-8" + ) as out: + subprocess.run( + [ + "compile-catalog", + f"/opt/c2cgeoportal/geoportal/c2cgeoportal_geoportal/locale/{lang}/LC_MESSAGES/ngeo.po", + f"{lang}/LC_MESSAGES/{args.package}_geoportal-client{suffix}.po", + ], + stdout=out, + cwd=base_path, + check=True, + ) for po_file in glob.glob(f"{base_path}/*/LC_MESSAGES/*.po"): if args.dry_run: diff --git a/bin/eval-templates b/bin/eval-templates index af67d111ee..8a7e4a04ad 100755 --- a/bin/eval-templates +++ b/bin/eval-templates @@ -52,4 +52,15 @@ find /etc/static-ngeo/ \( -name '*.js' -or -name '*.css' -or -name '*.html' \) - sed --in-place --expression="s#\.__ENTRY_POINT__#${VISIBLE_ENTRY_POINT}#g" "${file}" done +if [ -d /app/geomapfishapp_geoportal/ ]; then + for name in authentication multi_tenant dev; do + if [ -f "/etc/geomapfish/${name}.py" ]; then + echo "Get: ${name}.py" + cp "/etc/geomapfish/${name}.py" /app/geomapfishapp_geoportal/ + fi + done +fi + +chmod go-w -R /app/*_geoportal/ + exec "$@" diff --git a/doc/integrator/create_application.rst b/doc/integrator/create_application.rst index d43aed5cc6..a58c6a8a12 100644 --- a/doc/integrator/create_application.rst +++ b/doc/integrator/create_application.rst @@ -221,7 +221,7 @@ We recommend instead that you use dynamic variables as described below. However, in some use cases extending ``vars.yaml`` may be needed: * Configuring highly specific environments -* Configuration of a multi-organization project +* Configuration of a multi-tenant project Use of dynamic variables ........................ @@ -271,7 +271,7 @@ Do not forget to add your changes to git: .. note:: - If you are using a multi-organization project, you should add all new children to + If you are using a multi-tenant project, you should add all new children to the parent site check_collector configuration. After creation and minimal setup the application is ready to be installed. diff --git a/doc/integrator/features.rst b/doc/integrator/features.rst index 4262914c8f..cc9b27448e 100644 --- a/doc/integrator/features.rst +++ b/doc/integrator/features.rst @@ -18,6 +18,6 @@ Features that require additional steps (most of the time): urllogin pdfreport routing - multi_organization + multi_tenant vector_tiles extend_application diff --git a/doc/integrator/multi_organization.rst b/doc/integrator/multi_tenant.rst similarity index 88% rename from doc/integrator/multi_organization.rst rename to doc/integrator/multi_tenant.rst index 01d794161c..df9fc29ca5 100644 --- a/doc/integrator/multi_organization.rst +++ b/doc/integrator/multi_tenant.rst @@ -1,7 +1,7 @@ -.. _integrator_multi_organization: +.. _integrator_multi_tenant: -Multi organization -================== +Multi-tenant +============ The geoportal can host multiple organizations, with configuration differences for each organization. In a multi-organization geoportal, each organization will have the same program code @@ -11,18 +11,21 @@ In this example we will have the came CSS but we can do some variations by using see ``cssVars`` in ``gmfOptions`` in the ngeo GMF constants definitions :ngeo_doc:`gmf constants `. -The following lines will provide a basic implementation for multi-organization. +The following lines will provide a basic implementation for multi-tenant. The code should be adapted, currently it handles the hostnames 'org1.camptocamp.com' and 'org2.camptocamp.com', and you probably want to put the hardcoded values in the config. -``__init__.py`` ---------------- +``multi_tenant.py`` +------------------- -In the file ``geoportal/_geoportal/__init__.py`` add the following lines: +You should have a ``geoportal/_geoportal/multi_tenant.py`` file like this one: .. code:: python + from pyramid.config import Configurator + + def get_instance_prefix(request): if request.host == "org1.camptocamp.com": return "org1" @@ -49,7 +52,9 @@ In the file ``geoportal/_geoportal/__init__.py`` add the following line return print_url - # In ``main`` function, after ``config.include("c2cgeoportal_geoportal")`` + def includeme(config: Configurator) -> None: + """Initialize the multi-tenant.""" + config.add_request_method( get_organization_role, name="get_organization_role") config.add_request_method( @@ -107,10 +112,22 @@ Internationalization For each organization, a set of localization file should be created. +First you should create a ``tenants.yaml`` file like that: + +.. code:: yaml + + tenants: + org1: + public_url: https://org1.camptocamp.com + suffix: -org1 + curl_args: + org2: + ... + The general workflow is: -- The integrator performs ``make update-client-po``. -- This will run one ``update-po`` script for each organization with different environment variables. +- run ``scripts/multi-tenant-update-po``. +- This will run one ``make update-po-from-url`` for each organization with different environment variables. - The result is one po file set for each organization with the defined suffix. - The integrator needs to complete the po files with translations. - When the config Docker image is built, all po files are automatically converted to JSON files diff --git a/doc/integrator/ngeo.rst b/doc/integrator/ngeo.rst index ef35cf8926..d0636f9a76 100644 --- a/doc/integrator/ngeo.rst +++ b/doc/integrator/ngeo.rst @@ -121,7 +121,7 @@ The sub section is the interface name, and after that we have: ``Request.route_url`` `documentation `_. -* ``lang_urls_suffix`` suffix used in l10n URL, see: :ref:`integrator_multi_organization`. +* ``lang_urls_suffix`` suffix used in l10n URL, see: :ref:`integrator_multi_tenant`. The dynamic values names are: diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Dockerfile b/geoportal/c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Dockerfile index faa7cd899d..3e4ae81fcf 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Dockerfile +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Dockerfile @@ -42,7 +42,8 @@ RUN --mount=type=cache,target=/root/.cache \ python3 -m pip install --disable-pip-version-check --editable=/app/ \ && python3 -m compileall -q /usr/local/lib/python3.* \ -x '/(ptvsd|.*pydev.*|networkx)/' \ - && python3 -m compileall -q /app/{{cookiecutter.package}}_geoportal -x /app/{{cookiecutter.package}}_geoportal/static.* + && python3 -m compileall -q "/app/{{cookiecutter.package}}_geoportal" -x "/app/{{cookiecutter.package}}_geoportal/static".* \ + && chmod go+w "/app/{{cookiecutter.package}}_geoportal" "/app/{{cookiecutter.package}}_geoportal/authentication.py" "/app/{{cookiecutter.package}}_geoportal/multi_tenant.py" "/app/{{cookiecutter.package}}_geoportal/dev.py" ARG GIT_HASH RUN c2cwsgiutils-genversion ${GIT_HASH} diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/__init__.py b/geoportal/c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/__init__.py index 44eac8c037..109c56a6da 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/__init__.py +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/__init__.py @@ -3,6 +3,7 @@ import {{cookiecutter.package}}_geoportal.authentication import {{cookiecutter.package}}_geoportal.dev import {{cookiecutter.package}}_geoportal.multi_organization +import {{cookiecutter.package}}_geoportal.multi_tenant from c2cgeoportal_geoportal import add_interface_config, locale_negotiator from c2cgeoportal_geoportal.lib.i18n import LOCALE_PATH from {{cookiecutter.package}}_geoportal.resources import Root @@ -29,6 +30,7 @@ def main(global_config, **settings): config.include("c2cgeoportal_geoportal") config.include({{cookiecutter.package}}_geoportal.multi_organization.includeme) + config.include({{cookiecutter.package}}_geoportal.multi_tenant.includeme) # Scan view decorator for adding routes config.scan() diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/multi_organization.py b/geoportal/c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/multi_organization.py index 25d9664418..8144d5d350 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/multi_organization.py +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/multi_organization.py @@ -2,6 +2,6 @@ def includeme(config: Configurator) -> None: - """Initialize the multi organization.""" + """Initialize the multi-tenant.""" del config # Unused diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/multi_tenant.py b/geoportal/c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/multi_tenant.py new file mode 100644 index 0000000000..8144d5d350 --- /dev/null +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/multi_tenant.py @@ -0,0 +1,7 @@ +from pyramid.config import Configurator + + +def includeme(config: Configurator) -> None: + """Initialize the multi-tenant.""" + + del config # Unused diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.dockerignore b/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.dockerignore index 42ee2380ee..7333d2724b 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.dockerignore +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.dockerignore @@ -12,3 +12,6 @@ !geoportal/{{cookiecutter.package}}_geoportal/static !geoportal/{{cookiecutter.package}}_geoportal/locale geoportal/{{cookiecutter.package}}_geoportal/locale/*.pot +!geoportal/{{cookiecutter.package}}_geoportal/authentication.py +!geoportal/{{cookiecutter.package}}_geoportal/multi_tenant.py +!geoportal/{{cookiecutter.package}}_geoportal/dev.py diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile b/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile index e0ae4eb2e6..39572700db 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile @@ -28,6 +28,7 @@ ENV PGSCHEMA=$PGSCHEMA RUN \ cd /tmp/config/geoportal/ \ + && [ "${SIMPLE}" == "TRUE" ] || rm -f {{cookiecutter.package}}_geoportal/*.py \ && c2c-template --vars ${VARS_FILE} \ --get-config {{cookiecutter.package}}_geoportal/config.yaml \ ${CONFIG_VARS} \ diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Makefile b/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Makefile index 3d4f784eed..371e42cbee 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Makefile +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Makefile @@ -1,4 +1,4 @@ -PROJECT_PUBLIC_URL=https://example.camptocamp.com/ +PROJECT_PUBLIC_URL?=https://example.camptocamp.com/ DUMP_FILE=dump.backup PACKAGE={{cookiecutter.package}} LANGUAGES=en fr de it @@ -13,10 +13,10 @@ help: ## Display this help message .PHONY: update-po-from-url update-po-from-url: ## Update the po files from the URL provide by PROJECT_PUBLIC_URL - curl --fail --retry 5 --retry-delay 1 \ + curl ${CURL_ARGS} --fail --retry 5 --retry-delay 1 \ $(PROJECT_PUBLIC_URL)locale.pot > geoportal/${PACKAGE}_geoportal/locale/${PACKAGE}_geoportal-client${SUFFIX}.pot sed -i '/^"POT-Creation-Date: /d' geoportal/${PACKAGE}_geoportal/locale/${PACKAGE}_geoportal-client${SUFFIX}.pot - docker-compose run --rm -T tools update-po-only `id --user` `id --group` $(LANGUAGES) + docker-compose run --rm -T --env=SUFFIX=${SUFFIX} tools update-po-only `id --user` `id --group` $(LANGUAGES) .PHONY: update-po update-po: ## Update the po files from the running composition diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-lib.yaml b/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-lib.yaml index ec215927e9..f42b6f21de 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-lib.yaml +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-lib.yaml @@ -332,6 +332,8 @@ services: - C2CWSGIUTILS_LOG_LEVEL - LOG_TYPE - C2CGEOPORTAL_THEME_TIMEOUT=300 + # For multi tenant + - DEFAULT_PREFIX geoportal-advance: image: ${DOCKER_BASE}-geoportal:${DOCKER_TAG} diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.project b/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.project index 2f1a15f17e..e65933d91c 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.project +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.project @@ -67,3 +67,6 @@ C2C_AUTH_GITHUB_SCOPE=repo #C2C_AUTH_GITHUB_SECRET= #C2C_AUTH_GITHUB_PROXY_URL=https://geoservicies.camptocamp.com/redirect C2C_USE_SESSION=true + +# For multi-tenant +DEFAULT_PREFIX=unknown diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/multi-tenant-update-po b/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/multi-tenant-update-po new file mode 100755 index 0000000000..550fc364c8 --- /dev/null +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/multi-tenant-update-po @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +import argparse +import os +import subprocess + +import yaml + + +def _main() -> None: + parser = argparse.ArgumentParser( + description="\n".join( + [ + "Update the po files in multi tenants mode", + "", + "Using the information available in the tenants.yaml file.", + ] + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.parse_args() + + with open("tenants.yaml", "r") as tenants_file: + tenants = yaml.safe_load(tenants_file.read()) + + for name, tenant in tenants.get("tenants", {}).items(): + print(f"Update localization for tenant {name}.") + subprocess.run( + [ + "make", + "update-po-from-url", + ], + check=True, + env={ + **os.environ, + "PROJECT_PUBLIC_URL": tenant["public_url"], + "SUFFIX": tenant["suffix"], + "CURL_ARGS": tenant.get("curl_args", ""), + }, + ) + + +if __name__ == "__main__": + _main()