Skip to content

Commit 2dbce5e

Browse files
authored
feat: support for redwood (#9)
* 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.
1 parent 6811d69 commit 2dbce5e

File tree

8 files changed

+108
-185
lines changed

8 files changed

+108
-185
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ for MFEs.
1212
| Olive | `>=15.0, <16` | `edunext/[email protected]` | 15.x.x |
1313
| Palm | `>=16.0, <17` | `edunext/[email protected]` | 16.x.x |
1414
| Quince | `>=17.0, <18` | `openedx/[email protected]` | 17.x.x |
15+
| Redwood | `>=18.0, <19` | `tutor-mfe>17` | 18.x.x |
1516

1617
## Installation
1718

1819
```bash
19-
pip install git+https://github.com/edunext/[email protected]
20-
pip install git+https://github.com/edunext/[email protected]
20+
pip install git+https://github.com/edunext/[email protected]
2121
```
2222

2323
## Plugin Configuration

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def load_about():
3939
packages=find_packages(exclude=["tests*"]),
4040
include_package_data=True,
4141
python_requires=">=3.8",
42-
install_requires=["tutor>=17.0.2", "tutor-mfe>=17.0.1"],
42+
install_requires=["tutor>=18.0.0", "tutor-mfe>=18.0.0"],
4343
entry_points={
4444
"tutor.plugin.v1": [
4545
"mfe_extensions = tutormfe_extensions.plugin"

tutormfe_extensions/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "17.0.0"
1+
__version__ = "18.0.0"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{%- if MFE_EXTENSIONS_BY_PATH %}
2+
reverse_proxy /api/mfe_config/v1* lms:8000 {
3+
header_up Host {{ LMS_HOST }}
4+
}
5+
{%- for app_name in iter_mfes_per_service("cms") %}
6+
@mfe_{{ app_name }} {
7+
path /{{ app_name }} /{{ app_name }}/*
8+
}
9+
handle @mfe_{{ app_name }} {
10+
redir /{{ app_name }} /{{ app_name }}/
11+
reverse_proxy mfe:8002 {
12+
header_up Host {host}
13+
}
14+
}
15+
{%- endfor %}
16+
{%- endif %}
Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
{%- if MFE_EXTENSIONS_BY_PATH %}
2-
{% for app_name, app in iter_mfes() %}
3-
route /{{app_name}}/* {
2+
{%- for app_name in iter_mfes_per_service("lms") %}
3+
@mfe_{{ app_name }} {
4+
path /{{ app_name }} /{{ app_name }}/*
5+
}
6+
handle @mfe_{{ app_name }} {
7+
redir /{{ app_name }} /{{ app_name }}/
8+
{%- if app_name == "account" %}
9+
handle /account/settings {
10+
reverse_proxy lms:8000
11+
}
12+
handle /account/password {
13+
reverse_proxy lms:8000
14+
}
15+
{%- endif %}
416
reverse_proxy mfe:8002 {
517
header_up Host {host}
618
}
719
}
8-
{% endfor %}
20+
{%- endfor %}
921
{%- endif %}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{%- if MFE_EXTENSIONS_BY_PATH %}
2+
{%- if get_mfe("course-authoring") %}
3+
COURSE_AUTHORING_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ CMS_HOST }}/course-authoring"
4+
{%- endif %}
5+
{%- endif %}

tutormfe_extensions/patches/openedx-lms-production-settings

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,46 @@
11
{% if MFE_EXTENSIONS_BY_PATH %}
22
LEARNING_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/learning"
3+
MFE_CONFIG["LEARNING_BASE_URL"] = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/learning"
4+
5+
{%- if get_mfe("authn") %}
6+
AUTHN_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/authn"
7+
AUTHN_MICROFRONTEND_DOMAIN = "{{ LMS_HOST }}/authn"
8+
{%- endif %}
9+
10+
{% if get_mfe("account") %}
11+
ACCOUNT_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/account/"
12+
MFE_CONFIG["ACCOUNT_SETTINGS_URL"] = ACCOUNT_MICROFRONTEND_URL
13+
{%- endif %}
14+
15+
{% if get_mfe("discussions") %}
16+
DISCUSSIONS_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/discussions"
17+
MFE_CONFIG["DISCUSSIONS_MFE_BASE_URL"] = DISCUSSIONS_MICROFRONTEND_URL
18+
{% endif %}
19+
20+
{% if get_mfe("gradebook") %}
21+
WRITABLE_GRADEBOOK_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/gradebook"
22+
{% endif %}
23+
24+
{% if get_mfe("learner-dashboard") %}
25+
LEARNER_HOME_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/learner-dashboard/"
26+
{% endif %}
27+
28+
{% if get_mfe("ora-grading") %}
29+
ORA_GRADING_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/ora-grading"
30+
{% endif %}
31+
32+
{% if get_mfe("profile") %}
33+
PROFILE_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/profile/u/"
34+
MFE_CONFIG["ACCOUNT_PROFILE_URL"] = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/profile"
35+
{% endif %}
36+
37+
{% if get_mfe("communications") %}
38+
COMMUNICATIONS_MICROFRONTEND_URL = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/communications"
39+
{% endif %}
40+
41+
{% if get_mfe("course-authoring") %}
42+
MFE_CONFIG["COURSE_AUTHORING_MICROFRONTEND_URL"] = "{% if ENABLE_HTTPS %}https://{% else %}http://{% endif %}{{ LMS_HOST }}/course-authoring"
43+
{% endif %}
44+
45+
CORS_ORIGIN_WHITELIST.append("{% if ENABLE_HTTPS %}https://{% else %}http://{% endif %}{{ CMS_HOST }}")
346
{% endif %}
4-
MFE_CONFIG["PARAGON_THEME_URLS"] = {
5-
"core": {
6-
"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"
7-
},
8-
"defaults": {
9-
"light": "light"
10-
},
11-
"variants": {
12-
"light": {
13-
"url": "https://css-varsify.s3.amazonaws.com/public/a9959998-0bab-4447-ada5-6819866195f3.css"
14-
}
15-
}
16-
}

tutormfe_extensions/plugin.py

Lines changed: 25 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
import os
44
import os.path
55
from glob import glob
6+
from typing import Iterable
67

7-
import click
88
import importlib_resources
99
from tutor import config as tutor_config
1010
from tutor import hooks
1111
from tutormfe.hooks import MFE_APPS
12+
from tutormfe.plugin import CORE_MFE_APPS
1213

1314

1415
from .__about__ import __version__
@@ -23,17 +24,17 @@
2324
def validate_mfe_config(mfe_setting_name: str):
2425
if mfe_setting_name.startswith("MFE_") and mfe_setting_name.endswith("_MFE_APP"):
2526
return (
26-
mfe_setting_name
27-
.replace("_MFE_APP", "")
27+
mfe_setting_name.replace("_MFE_APP", "")
2828
.replace("MFE_", "")
2929
.replace("_", "-")
3030
.lower()
3131
)
3232
return None
3333

34+
3435
@MFE_APPS.add()
3536
def _manage_mfes_from_config(mfe_list):
36-
config = tutor_config.load('.')
37+
config = tutor_config.load(".")
3738
for setting, value in config.items():
3839
mfe_name = validate_mfe_config(setting)
3940
if not mfe_name:
@@ -54,6 +55,7 @@ def _manage_mfes_from_config(mfe_list):
5455

5556
return mfe_list
5657

58+
5759
hooks.Filters.CONFIG_DEFAULTS.add_items(
5860
[
5961
# Add your new settings that have default values here.
@@ -65,104 +67,22 @@ def _manage_mfes_from_config(mfe_list):
6567
]
6668
)
6769

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

79-
hooks.Filters.CONFIG_OVERRIDES.add_items(
80-
[
81-
# Danger zone!
82-
# Add values to override settings from Tutor core or other plugins here.
83-
# Each override is a pair: (setting_name, new_value). For example:
84-
(k,v) for k,v in CORE_MFES_CONFIG.items()
85-
]
86-
)
71+
def iter_mfes_per_service(service: str = "") -> Iterable[str]:
72+
"""
73+
Return the list of MFEs that should be hosted via path in the
74+
same domain as each service.
8775
88-
########################################
89-
# INITIALIZATION TASKS
90-
########################################
76+
"""
77+
active_mfes = MFE_APPS.apply({})
78+
cms_mfes = {"course-authoring"}
79+
lms_mfes = set(CORE_MFE_APPS) - cms_mfes
9180

92-
# To add a custom initialization task, create a bash script template under:
93-
# tutormfe_extensions/templates/mfe_extensions/jobs/init/
94-
# and then add it to the MY_INIT_TASKS list. Each task is in the format:
95-
# ("<service>", ("<path>", "<to>", "<script>", "<template>"))
96-
MY_INIT_TASKS: list[tuple[str, tuple[str, ...]]] = [
97-
# For example, to add LMS initialization steps, you could add the script template at:
98-
# tutormfe_extensions/templates/mfe_extensions/jobs/init/lms.sh
99-
# And then add the line:
100-
### ("lms", ("mfe_extensions", "jobs", "init", "lms.sh")),
101-
]
102-
103-
104-
# For each task added to MY_INIT_TASKS, we load the task template
105-
# and add it to the CLI_DO_INIT_TASKS filter, which tells Tutor to
106-
# run it as part of the `init` job.
107-
for service, template_path in MY_INIT_TASKS:
108-
full_path: str = str(
109-
importlib_resources.files("tutormfe_extensions")
110-
/ os.path.join("templates", *template_path)
111-
)
112-
with open(full_path, encoding="utf-8") as init_task_file:
113-
init_task: str = init_task_file.read()
114-
hooks.Filters.CLI_DO_INIT_TASKS.add_item((service, init_task))
115-
116-
117-
########################################
118-
# DOCKER IMAGE MANAGEMENT
119-
########################################
120-
121-
122-
# Images to be built by `tutor images build`.
123-
# Each item is a quadruple in the form:
124-
# ("<tutor_image_name>", ("path", "to", "build", "dir"), "<docker_image_tag>", "<build_args>")
125-
hooks.Filters.IMAGES_BUILD.add_items(
126-
[
127-
# To build `myimage` with `tutor images build myimage`,
128-
# you would add a Dockerfile to templates/mfe_extensions/build/myimage,
129-
# and then write:
130-
### (
131-
### "myimage",
132-
### ("plugins", "mfe_extensions", "build", "myimage"),
133-
### "docker.io/myimage:{{ MFE_EXTENSIONS_VERSION }}",
134-
### (),
135-
### ),
136-
]
137-
)
138-
139-
140-
# Images to be pulled as part of `tutor images pull`.
141-
# Each item is a pair in the form:
142-
# ("<tutor_image_name>", "<docker_image_tag>")
143-
hooks.Filters.IMAGES_PULL.add_items(
144-
[
145-
# To pull `myimage` with `tutor images pull myimage`, you would write:
146-
### (
147-
### "myimage",
148-
### "docker.io/myimage:{{ MFE_EXTENSIONS_VERSION }}",
149-
### ),
150-
]
151-
)
152-
153-
154-
# Images to be pushed as part of `tutor images push`.
155-
# Each item is a pair in the form:
156-
# ("<tutor_image_name>", "<docker_image_tag>")
157-
hooks.Filters.IMAGES_PUSH.add_items(
158-
[
159-
# To push `myimage` with `tutor images push myimage`, you would write:
160-
### (
161-
### "myimage",
162-
### "docker.io/myimage:{{ MFE_EXTENSIONS_VERSION }}",
163-
### ),
164-
]
165-
)
81+
for mfe in active_mfes:
82+
if service == "lms" and mfe in lms_mfes:
83+
yield mfe
84+
if service == "cms" and mfe in cms_mfes:
85+
yield mfe
16686

16787

16888
########################################
@@ -191,6 +111,12 @@ def _manage_mfes_from_config(mfe_list):
191111
],
192112
)
193113

114+
# Make the mfe_extensions functions available within templates
115+
hooks.Filters.ENV_TEMPLATE_VARIABLES.add_items(
116+
[
117+
("iter_mfes_per_service", iter_mfes_per_service),
118+
],
119+
)
194120

195121
########################################
196122
# PATCH LOADING
@@ -208,69 +134,3 @@ def _manage_mfes_from_config(mfe_list):
208134
):
209135
with open(path, encoding="utf-8") as patch_file:
210136
hooks.Filters.ENV_PATCHES.add_item((os.path.basename(path), patch_file.read()))
211-
212-
213-
########################################
214-
# CUSTOM JOBS (a.k.a. "do-commands")
215-
########################################
216-
217-
# A job is a set of tasks, each of which run inside a certain container.
218-
# Jobs are invoked using the `do` command, for example: `tutor local do importdemocourse`.
219-
# A few jobs are built in to Tutor, such as `init` and `createuser`.
220-
# You can also add your own custom jobs:
221-
222-
# To add a custom job, define a Click command that returns a list of tasks,
223-
# where each task is a pair in the form ("<service>", "<shell_command>").
224-
# For example:
225-
### @click.command()
226-
### @click.option("-n", "--name", default="plugin developer")
227-
### def say_hi(name: str) -> list[tuple[str, str]]:
228-
### """
229-
### An example job that just prints 'hello' from within both LMS and CMS.
230-
### """
231-
### return [
232-
### ("lms", f"echo 'Hello from LMS, {name}!'"),
233-
### ("cms", f"echo 'Hello from CMS, {name}!'"),
234-
### ]
235-
236-
237-
# Then, add the command function to CLI_DO_COMMANDS:
238-
## hooks.Filters.CLI_DO_COMMANDS.add_item(say_hi)
239-
240-
# Now, you can run your job like this:
241-
# $ tutor local do say-hi --name="Moisés González"
242-
243-
244-
#######################################
245-
# CUSTOM CLI COMMANDS
246-
#######################################
247-
248-
# Your plugin can also add custom commands directly to the Tutor CLI.
249-
# These commands are run directly on the user's host computer
250-
# (unlike jobs, which are run in containers).
251-
252-
# To define a command group for your plugin, you would define a Click
253-
# group and then add it to CLI_COMMANDS:
254-
255-
256-
### @click.group()
257-
### def mfe_extensions() -> None:
258-
### pass
259-
260-
261-
### hooks.Filters.CLI_COMMANDS.add_item(mfe_extensions)
262-
263-
264-
# Then, you would add subcommands directly to the Click group, for example:
265-
266-
267-
### @mfe_extensions.command()
268-
### def example_command() -> None:
269-
### """
270-
### This is helptext for an example command.
271-
### """
272-
### print("You've run an example command.")
273-
274-
275-
# This would allow you to run:
276-
# $ tutor mfe_extensions example-command

0 commit comments

Comments
 (0)