Skip to content

Commit

Permalink
Support multi-tenant in simple mode
Browse files Browse the repository at this point in the history
  • Loading branch information
sbrunner committed Jan 10, 2025
1 parent 9f0c808 commit e60a376
Show file tree
Hide file tree
Showing 16 changed files with 145 additions and 75 deletions.
91 changes: 35 additions & 56 deletions bin/build-l10n
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import argparse
import glob
import os
import shutil
import subprocess


Expand All @@ -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:
Expand Down
11 changes: 11 additions & 0 deletions bin/eval-templates
Original file line number Diff line number Diff line change
Expand Up @@ -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 "$@"
4 changes: 2 additions & 2 deletions doc/integrator/create_application.rst
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,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
........................
Expand Down Expand Up @@ -272,7 +272,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.
Expand Down
2 changes: 1 addition & 1 deletion doc/integrator/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ Features that require additional steps (most of the time):
urllogin
pdfreport
routing
multi_organization
multi_tenant
vector_tiles
extend_application
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 </jsdoc/module-contribs_gmf_src_options.html>`.

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/<package>_geoportal/__init__.py`` add the following lines:
You should have a ``geoportal/<package>_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"
Expand All @@ -49,7 +52,9 @@ In the file ``geoportal/<package>_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(
Expand Down Expand Up @@ -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: <optional>
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
Expand Down
2 changes: 1 addition & 1 deletion doc/integrator/ngeo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ The sub section is the interface name, and after that we have:
``Request.route_url`` `documentation
<https://docs.pylonsproject.org/projects/pyramid/en/latest/api/request.html#pyramid.request.Request.route_url>`_.

* ``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:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@


def includeme(config: Configurator) -> None:
"""Initialize the multi organization."""
"""Initialize the multi-tenant."""

del config # Unused
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pyramid.config import Configurator


def includeme(config: Configurator) -> None:
"""Initialize the multi-tenant."""

del config # Unused
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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} \
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,6 @@ C2C_AUTH_GITHUB_SCOPE=repo
#C2C_AUTH_GITHUB_SECRET=<secret>
#C2C_AUTH_GITHUB_PROXY_URL=https://geoservicies.camptocamp.com/redirect
C2C_USE_SESSION=true

# For multi-tenant
DEFAULT_PREFIX=unknown
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit e60a376

Please sign in to comment.