Skip to content

Commit

Permalink
feat: support for redwood (#9)
Browse files Browse the repository at this point in the history
* allow MFEs to be hosted on either the LMS or the CMS

Before all the MFEs that were hosted by path were hosted in the LMS
domain. For certain MFEs (course-authoring) the CMS domain is more
appropriate.

* point the MFEs URLs to either the LMS or the CMS

* handle special cases in the $LMS_HOST/account route.

The `/account/password` endpoint is used in the 'forgot password' flow,
for that reason we must forward those requests to the LMS.

The `/account/settings` is more specific to the multitenant use
case. Some sites may be configured to use the legacy pages but the
current configuration would send every subpath of `/account/` to the
MFE, we make an exception for the legacy URL.
  • Loading branch information
MoisesGSalas authored Jul 15, 2024
1 parent 6811d69 commit 2dbce5e
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 185 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ for MFEs.
| Olive | `>=15.0, <16` | `edunext/[email protected]` | 15.x.x |
| Palm | `>=16.0, <17` | `edunext/[email protected]` | 16.x.x |
| Quince | `>=17.0, <18` | `openedx/[email protected]` | 17.x.x |
| Redwood | `>=18.0, <19` | `tutor-mfe>17` | 18.x.x |

## Installation

```bash
pip install git+https://github.com/edunext/[email protected]
pip install git+https://github.com/edunext/[email protected]
pip install git+https://github.com/edunext/[email protected]
```

## Plugin Configuration
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def load_about():
packages=find_packages(exclude=["tests*"]),
include_package_data=True,
python_requires=">=3.8",
install_requires=["tutor>=17.0.2", "tutor-mfe>=17.0.1"],
install_requires=["tutor>=18.0.0", "tutor-mfe>=18.0.0"],
entry_points={
"tutor.plugin.v1": [
"mfe_extensions = tutormfe_extensions.plugin"
Expand Down
2 changes: 1 addition & 1 deletion tutormfe_extensions/__about__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "17.0.0"
__version__ = "18.0.0"
16 changes: 16 additions & 0 deletions tutormfe_extensions/patches/caddyfile-cms
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{%- if MFE_EXTENSIONS_BY_PATH %}
reverse_proxy /api/mfe_config/v1* lms:8000 {
header_up Host {{ LMS_HOST }}
}
{%- for app_name in iter_mfes_per_service("cms") %}
@mfe_{{ app_name }} {
path /{{ app_name }} /{{ app_name }}/*
}
handle @mfe_{{ app_name }} {
redir /{{ app_name }} /{{ app_name }}/
reverse_proxy mfe:8002 {
header_up Host {host}
}
}
{%- endfor %}
{%- endif %}
18 changes: 15 additions & 3 deletions tutormfe_extensions/patches/caddyfile-mfe-by-path
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
{%- if MFE_EXTENSIONS_BY_PATH %}
{% for app_name, app in iter_mfes() %}
route /{{app_name}}/* {
{%- for app_name in iter_mfes_per_service("lms") %}
@mfe_{{ app_name }} {
path /{{ app_name }} /{{ app_name }}/*
}
handle @mfe_{{ app_name }} {
redir /{{ app_name }} /{{ app_name }}/
{%- if app_name == "account" %}
handle /account/settings {
reverse_proxy lms:8000
}
handle /account/password {
reverse_proxy lms:8000
}
{%- endif %}
reverse_proxy mfe:8002 {
header_up Host {host}
}
}
{% endfor %}
{%- endfor %}
{%- endif %}
5 changes: 5 additions & 0 deletions tutormfe_extensions/patches/openedx-cms-production-settings
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{%- if MFE_EXTENSIONS_BY_PATH %}
{%- if get_mfe("course-authoring") %}
COURSE_AUTHORING_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ CMS_HOST }}/course-authoring"
{%- endif %}
{%- endif %}
56 changes: 43 additions & 13 deletions tutormfe_extensions/patches/openedx-lms-production-settings
Original file line number Diff line number Diff line change
@@ -1,16 +1,46 @@
{% if MFE_EXTENSIONS_BY_PATH %}
LEARNING_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/learning"
MFE_CONFIG["LEARNING_BASE_URL"] = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/learning"

{%- if get_mfe("authn") %}
AUTHN_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/authn"
AUTHN_MICROFRONTEND_DOMAIN = "{{ LMS_HOST }}/authn"
{%- endif %}

{% if get_mfe("account") %}
ACCOUNT_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/account/"
MFE_CONFIG["ACCOUNT_SETTINGS_URL"] = ACCOUNT_MICROFRONTEND_URL
{%- endif %}

{% if get_mfe("discussions") %}
DISCUSSIONS_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/discussions"
MFE_CONFIG["DISCUSSIONS_MFE_BASE_URL"] = DISCUSSIONS_MICROFRONTEND_URL
{% endif %}

{% if get_mfe("gradebook") %}
WRITABLE_GRADEBOOK_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/gradebook"
{% endif %}

{% if get_mfe("learner-dashboard") %}
LEARNER_HOME_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/learner-dashboard/"
{% endif %}

{% if get_mfe("ora-grading") %}
ORA_GRADING_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/ora-grading"
{% endif %}

{% if get_mfe("profile") %}
PROFILE_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/profile/u/"
MFE_CONFIG["ACCOUNT_PROFILE_URL"] = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/profile"
{% endif %}

{% if get_mfe("communications") %}
COMMUNICATIONS_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/communications"
{% endif %}

{% if get_mfe("course-authoring") %}
MFE_CONFIG["COURSE_AUTHORING_MICROFRONTEND_URL"] = "{% if ENABLE_HTTPS %}https://{% else %}http://{% endif %}{{ LMS_HOST }}/course-authoring"
{% endif %}

CORS_ORIGIN_WHITELIST.append("{% if ENABLE_HTTPS %}https://{% else %}http://{% endif %}{{ CMS_HOST }}")
{% endif %}
MFE_CONFIG["PARAGON_THEME_URLS"] = {
"core": {
"url": "https://cdn.jsdelivr.net/combine/npm/@edx/[email protected]/styles/css/themes/light/utility-classes.min.css,npm/@edx/[email protected]/dist/core.min.css"
},
"defaults": {
"light": "light"
},
"variants": {
"light": {
"url": "https://css-varsify.s3.amazonaws.com/public/a9959998-0bab-4447-ada5-6819866195f3.css"
}
}
}
190 changes: 25 additions & 165 deletions tutormfe_extensions/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import os
import os.path
from glob import glob
from typing import Iterable

import click
import importlib_resources
from tutor import config as tutor_config
from tutor import hooks
from tutormfe.hooks import MFE_APPS
from tutormfe.plugin import CORE_MFE_APPS


from .__about__ import __version__
Expand All @@ -23,17 +24,17 @@
def validate_mfe_config(mfe_setting_name: str):
if mfe_setting_name.startswith("MFE_") and mfe_setting_name.endswith("_MFE_APP"):
return (
mfe_setting_name
.replace("_MFE_APP", "")
mfe_setting_name.replace("_MFE_APP", "")
.replace("MFE_", "")
.replace("_", "-")
.lower()
)
return None


@MFE_APPS.add()
def _manage_mfes_from_config(mfe_list):
config = tutor_config.load('.')
config = tutor_config.load(".")
for setting, value in config.items():
mfe_name = validate_mfe_config(setting)
if not mfe_name:
Expand All @@ -54,6 +55,7 @@ def _manage_mfes_from_config(mfe_list):

return mfe_list


hooks.Filters.CONFIG_DEFAULTS.add_items(
[
# Add your new settings that have default values here.
Expand All @@ -65,104 +67,22 @@ def _manage_mfes_from_config(mfe_list):
]
)

hooks.Filters.CONFIG_UNIQUE.add_items(
[
# Add settings that don't have a reasonable default for all users here.
# For instance: passwords, secret keys, etc.
# Each new setting is a pair: (setting_name, unique_generated_value).
# Prefix your setting names with 'MFE_EXTENSIONS_'.
# For example:
### ("MFE_EXTENSIONS_SECRET_KEY", "{{ 24|random_string }}"),
]
)

hooks.Filters.CONFIG_OVERRIDES.add_items(
[
# Danger zone!
# Add values to override settings from Tutor core or other plugins here.
# Each override is a pair: (setting_name, new_value). For example:
(k,v) for k,v in CORE_MFES_CONFIG.items()
]
)
def iter_mfes_per_service(service: str = "") -> Iterable[str]:
"""
Return the list of MFEs that should be hosted via path in the
same domain as each service.
########################################
# INITIALIZATION TASKS
########################################
"""
active_mfes = MFE_APPS.apply({})
cms_mfes = {"course-authoring"}
lms_mfes = set(CORE_MFE_APPS) - cms_mfes

# To add a custom initialization task, create a bash script template under:
# tutormfe_extensions/templates/mfe_extensions/jobs/init/
# and then add it to the MY_INIT_TASKS list. Each task is in the format:
# ("<service>", ("<path>", "<to>", "<script>", "<template>"))
MY_INIT_TASKS: list[tuple[str, tuple[str, ...]]] = [
# For example, to add LMS initialization steps, you could add the script template at:
# tutormfe_extensions/templates/mfe_extensions/jobs/init/lms.sh
# And then add the line:
### ("lms", ("mfe_extensions", "jobs", "init", "lms.sh")),
]


# For each task added to MY_INIT_TASKS, we load the task template
# and add it to the CLI_DO_INIT_TASKS filter, which tells Tutor to
# run it as part of the `init` job.
for service, template_path in MY_INIT_TASKS:
full_path: str = str(
importlib_resources.files("tutormfe_extensions")
/ os.path.join("templates", *template_path)
)
with open(full_path, encoding="utf-8") as init_task_file:
init_task: str = init_task_file.read()
hooks.Filters.CLI_DO_INIT_TASKS.add_item((service, init_task))


########################################
# DOCKER IMAGE MANAGEMENT
########################################


# Images to be built by `tutor images build`.
# Each item is a quadruple in the form:
# ("<tutor_image_name>", ("path", "to", "build", "dir"), "<docker_image_tag>", "<build_args>")
hooks.Filters.IMAGES_BUILD.add_items(
[
# To build `myimage` with `tutor images build myimage`,
# you would add a Dockerfile to templates/mfe_extensions/build/myimage,
# and then write:
### (
### "myimage",
### ("plugins", "mfe_extensions", "build", "myimage"),
### "docker.io/myimage:{{ MFE_EXTENSIONS_VERSION }}",
### (),
### ),
]
)


# Images to be pulled as part of `tutor images pull`.
# Each item is a pair in the form:
# ("<tutor_image_name>", "<docker_image_tag>")
hooks.Filters.IMAGES_PULL.add_items(
[
# To pull `myimage` with `tutor images pull myimage`, you would write:
### (
### "myimage",
### "docker.io/myimage:{{ MFE_EXTENSIONS_VERSION }}",
### ),
]
)


# Images to be pushed as part of `tutor images push`.
# Each item is a pair in the form:
# ("<tutor_image_name>", "<docker_image_tag>")
hooks.Filters.IMAGES_PUSH.add_items(
[
# To push `myimage` with `tutor images push myimage`, you would write:
### (
### "myimage",
### "docker.io/myimage:{{ MFE_EXTENSIONS_VERSION }}",
### ),
]
)
for mfe in active_mfes:
if service == "lms" and mfe in lms_mfes:
yield mfe
if service == "cms" and mfe in cms_mfes:
yield mfe


########################################
Expand Down Expand Up @@ -191,6 +111,12 @@ def _manage_mfes_from_config(mfe_list):
],
)

# Make the mfe_extensions functions available within templates
hooks.Filters.ENV_TEMPLATE_VARIABLES.add_items(
[
("iter_mfes_per_service", iter_mfes_per_service),
],
)

########################################
# PATCH LOADING
Expand All @@ -208,69 +134,3 @@ def _manage_mfes_from_config(mfe_list):
):
with open(path, encoding="utf-8") as patch_file:
hooks.Filters.ENV_PATCHES.add_item((os.path.basename(path), patch_file.read()))


########################################
# CUSTOM JOBS (a.k.a. "do-commands")
########################################

# A job is a set of tasks, each of which run inside a certain container.
# Jobs are invoked using the `do` command, for example: `tutor local do importdemocourse`.
# A few jobs are built in to Tutor, such as `init` and `createuser`.
# You can also add your own custom jobs:

# To add a custom job, define a Click command that returns a list of tasks,
# where each task is a pair in the form ("<service>", "<shell_command>").
# For example:
### @click.command()
### @click.option("-n", "--name", default="plugin developer")
### def say_hi(name: str) -> list[tuple[str, str]]:
### """
### An example job that just prints 'hello' from within both LMS and CMS.
### """
### return [
### ("lms", f"echo 'Hello from LMS, {name}!'"),
### ("cms", f"echo 'Hello from CMS, {name}!'"),
### ]


# Then, add the command function to CLI_DO_COMMANDS:
## hooks.Filters.CLI_DO_COMMANDS.add_item(say_hi)

# Now, you can run your job like this:
# $ tutor local do say-hi --name="Moisés González"


#######################################
# CUSTOM CLI COMMANDS
#######################################

# Your plugin can also add custom commands directly to the Tutor CLI.
# These commands are run directly on the user's host computer
# (unlike jobs, which are run in containers).

# To define a command group for your plugin, you would define a Click
# group and then add it to CLI_COMMANDS:


### @click.group()
### def mfe_extensions() -> None:
### pass


### hooks.Filters.CLI_COMMANDS.add_item(mfe_extensions)


# Then, you would add subcommands directly to the Click group, for example:


### @mfe_extensions.command()
### def example_command() -> None:
### """
### This is helptext for an example command.
### """
### print("You've run an example command.")


# This would allow you to run:
# $ tutor mfe_extensions example-command

0 comments on commit 2dbce5e

Please sign in to comment.