Skip to content

Commit 44f50d4

Browse files
committed
use minio as storage for persistent packages
1 parent 08ea6ad commit 44f50d4

12 files changed

+131
-207
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
- [Feature] Add Xblocks at runtime without rebuilding image. (by @mlabeeb03)
1+
- [Feature] Install python packages at runtime without rebuilding image. (by @mlabeeb03)

tutor/commands/cli.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from tutor.commands.local import local
1717
from tutor.commands.mounts import mounts_command
1818
from tutor.commands.plugins import plugins_command
19-
from tutor.commands.packages import packages_command
2019

2120

2221
def main() -> None:
@@ -66,7 +65,7 @@ def ensure_plugins_enabled(self, ctx: click.Context) -> None:
6665
"""
6766
We enable plugins as soon as possible to have access to commands.
6867
"""
69-
if not "root" in ctx.params:
68+
if "root" not in ctx.params:
7069
# When generating docs, this function is called with empty args.
7170
# That's ok, we just ignore it.
7271
return
@@ -130,7 +129,6 @@ def help_command(context: click.Context) -> None:
130129
local,
131130
mounts_command,
132131
plugins_command,
133-
packages_command,
134132
]
135133
)
136134

tutor/commands/jobs.py

Lines changed: 44 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
from tutor import config as tutor_config
1515
from tutor import env, fmt, hooks
16-
from tutor.commands.config import save as config_save_command
1716
from tutor.commands.context import Context
1817
from tutor.commands.jobs_utils import (
1918
create_user_template,
@@ -541,68 +540,57 @@ def do_callback(service_commands: t.Iterable[tuple[str, str]]) -> None:
541540
runner.run_task_from_str(service, command)
542541

543542

544-
@click.command(help="Install a pip package at runtime")
545-
@click.pass_context
546-
@click.argument("package")
547-
def pip_install(context: click.Context, package: str) -> t.Iterable[tuple[str, str]]:
543+
@click.command(help="Build all persistent pip packages and upload to MinIO")
544+
@click.pass_obj
545+
def build_packages(context: Context) -> t.Iterable[tuple[str, str]]:
548546
"""
549-
Installs a pip package persistently in the lms and cms container and
550-
restarts with uwsgi server in both containers.
547+
Build the persistent pip packages and upload to MinIO.
548+
You need to update the `PERSISTENT_PIP_PACKAGES` variable
549+
in the config file to add/remove packages.
551550
"""
552-
553-
# TODO Only add package to config if pip install is successful
554-
fmt.echo_info(f"Adding {package} to config...")
555-
context.invoke(
556-
config_save_command,
557-
interactive=False,
558-
set_vars=[],
559-
append_vars=[("PERSISTENT_PIP_PACKAGES", package)],
560-
remove_vars=[],
561-
unset_vars=[],
562-
env_only=False,
563-
clean_env=False,
551+
config = tutor_config.load(context.root)
552+
all_packages = " ".join(
553+
package for package in t.cast(list[str], config["PERSISTENT_PIP_PACKAGES"])
564554
)
565555

566556
script = f"""
567557
pip install \
568-
--prefix=/mnt/persistent-python-packages \
569-
{package} \
570-
&& echo \"$(date)\" > /mnt/persistent-python-packages/.uwsgi_trigger
571-
"""
572-
573-
yield ("lms", script)
574-
558+
--prefix=/openedx/persistent-python-packages/deps \
559+
{all_packages} \
560+
&& python3 -c '
561+
import os, shutil, tempfile, boto3, botocore, datetime, zipfile
562+
563+
DEPS_DIR = "/openedx/persistent-python-packages/deps"
564+
MINIO_KEY = "deps.zip"
565+
DEPS_ZIP_PATH = DEPS_DIR[:-4] + MINIO_KEY
566+
MINIO_BUCKET = "tutor-deps"
567+
568+
s3 = boto3.client(
569+
"s3",
570+
endpoint_url="http://" + os.environ.get("MINIO_HOST"),
571+
aws_access_key_id=os.environ.get("OPENEDX_AWS_ACCESS_KEY"),
572+
aws_secret_access_key=os.environ.get("OPENEDX_AWS_SECRET_ACCESS_KEY"),
573+
)
575574
576-
@click.command(help="Remove a pip package at runtime")
577-
@click.pass_obj
578-
@click.pass_context
579-
@click.argument("package")
580-
def pip_uninstall(
581-
click_context: click.Context, context: Context, package: str
582-
) -> t.Iterable[tuple[str, str]]:
575+
def _upload_to_minio(local_path):
576+
try:
577+
s3.head_bucket(Bucket=MINIO_BUCKET)
578+
except botocore.exceptions.ClientError:
579+
s3.create_bucket(Bucket=MINIO_BUCKET)
580+
s3.upload_file(local_path, MINIO_BUCKET, MINIO_KEY)
581+
print(f"Uploaded {{local_path}} → MinIO:{{MINIO_BUCKET}}/{{MINIO_KEY}}")
582+
os.remove(local_path)
583+
584+
def _make_zip_archive(src_dir):
585+
with tempfile.TemporaryDirectory(prefix="tutor-depszip-") as zip_dir:
586+
path = os.path.join(zip_dir, "deps.zip")
587+
shutil.make_archive(path[:-4], format="zip", root_dir=src_dir)
588+
shutil.move(path, DEPS_ZIP_PATH)
589+
590+
_make_zip_archive(DEPS_DIR)
591+
_upload_to_minio(DEPS_ZIP_PATH)
592+
'
583593
"""
584-
Deletes the persistently installed pip package along with its dependencies
585-
"""
586-
587-
fmt.echo_info(f"Removing {package} from config...")
588-
click_context.invoke(
589-
config_save_command,
590-
interactive=False,
591-
set_vars=[],
592-
append_vars=[],
593-
remove_vars=[("PERSISTENT_PIP_PACKAGES", package)],
594-
unset_vars=[],
595-
env_only=False,
596-
clean_env=False,
597-
)
598-
599-
script = "rm -rf /mnt/persistent-python-packages/lib/"
600-
config = tutor_config.load(context.root)
601-
values = t.cast(list[str], config["PERSISTENT_PIP_PACKAGES"])
602-
remaining_packages = " ".join(values)
603-
if len(values) > 0:
604-
script += f" && pip install --prefix=/mnt/persistent-python-packages {remaining_packages}"
605-
script += ' && echo "$(date)" > /mnt/persistent-python-packages/.uwsgi_trigger'
606594

607595
yield ("lms", script)
608596

@@ -631,8 +619,7 @@ def run_migrations(package: str) -> t.Iterable[tuple[str, str]]:
631619
settheme,
632620
sqlshell,
633621
update_mysql_authentication_plugin,
634-
pip_install,
635-
pip_uninstall,
622+
build_packages,
636623
run_migrations,
637624
]
638625
)

tutor/commands/packages.py

Lines changed: 0 additions & 133 deletions
This file was deleted.

tutor/templates/build/openedx/Dockerfile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ ENV VIRTUAL_ENV=/openedx/venv/
179179
ENV COMPREHENSIVE_THEME_DIRS=/openedx/themes
180180
ENV STATIC_ROOT_LMS=/openedx/staticfiles
181181
ENV STATIC_ROOT_CMS=/openedx/staticfiles/studio
182-
ENV PYTHONPATH=/mnt/persistent-python-packages/lib/python3.11/site-packages
182+
ENV PYTHONPATH=/openedx/persistent-python-packages/deps/lib/python3.11/site-packages
183183

184184
WORKDIR /openedx/edx-platform
185185

@@ -314,8 +314,14 @@ ENV UWSGI_WORKERS=2
314314
# Copy the default uWSGI configuration
315315
COPY --chown=app:app settings/uwsgi.ini /openedx
316316

317+
# Copy the download script that fetches dependencies from MinIO
318+
COPY --chown=app:app settings/download_packages_from_minio.py /openedx
319+
317320
# Run server
318-
CMD ["uwsgi", "/openedx/uwsgi.ini"]
321+
CMD sh -c "python /openedx/download_packages_from_minio.py & \
322+
while true; do sleep 30; python /openedx/download_packages_from_minio.py; done & \
323+
uwsgi /openedx/uwsgi.ini"
324+
319325

320326
{{ patch("openedx-dockerfile-final") }}
321327

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import datetime
2+
import os
3+
import shutil
4+
import zipfile
5+
6+
import boto3
7+
8+
DEPS_DIR = "/openedx/persistent-python-packages/deps"
9+
MINIO_KEY = "deps.zip"
10+
DEPS_ZIP_PATH = DEPS_DIR[:-4] + MINIO_KEY
11+
MINIO_BUCKET = "tutor-deps"
12+
TRIGGER_FILE = "/openedx/persistent-python-packages/.uwsgi_trigger"
13+
14+
s3 = boto3.client(
15+
"s3",
16+
endpoint_url="http://" + os.environ.get("MINIO_HOST"),
17+
aws_access_key_id=os.environ.get("OPENEDX_AWS_ACCESS_KEY"),
18+
aws_secret_access_key=os.environ.get("OPENEDX_AWS_SECRET_ACCESS_KEY"),
19+
)
20+
21+
22+
def _download_from_minio():
23+
head = s3.head_object(Bucket=MINIO_BUCKET, Key=MINIO_KEY)
24+
remote_ts = head["LastModified"].astimezone(datetime.timezone.utc)
25+
26+
if os.path.exists(DEPS_ZIP_PATH):
27+
local_ts = os.path.getmtime(DEPS_ZIP_PATH)
28+
local_ts = datetime.datetime.fromtimestamp(local_ts, tz=datetime.timezone.utc)
29+
30+
if local_ts >= remote_ts:
31+
return
32+
33+
if os.path.exists(DEPS_DIR):
34+
shutil.rmtree(DEPS_DIR)
35+
os.makedirs(DEPS_DIR, exist_ok=True)
36+
37+
s3.download_file(MINIO_BUCKET, MINIO_KEY, DEPS_ZIP_PATH)
38+
39+
with zipfile.ZipFile(DEPS_ZIP_PATH, "r") as zip_ref:
40+
zip_ref.extractall(DEPS_DIR)
41+
42+
with open(TRIGGER_FILE, "a"):
43+
os.utime(TRIGGER_FILE, None)
44+
45+
46+
_download_from_minio()

tutor/templates/build/openedx/settings/uwsgi.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ master = true
2222
py-call-osafterfork = true
2323
vacuum = true
2424
# Touch this file to initiate a reload
25-
touch-reload = /mnt/persistent-python-packages/.uwsgi_trigger
25+
touch-reload = /openedx/persistent-python-packages/.uwsgi_trigger

tutor/templates/dev/docker-compose.jobs.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,24 @@ x-openedx-job-service:
1414
- ../apps/openedx/config:/openedx/config:ro
1515
# theme files
1616
- ../build/openedx/themes:/openedx/themes
17-
# third party xblocks and their dependencies
18-
- ../../data/persistent-python-packages:/mnt/persistent-python-packages
1917

2018
services:
2119

2220
lms-job:
2321
<<: *openedx-job-service
2422
environment:
2523
DJANGO_SETTINGS_MODULE: lms.envs.tutor.development
24+
MINIO_HOST: {{ MINIO_HOST | default("minio:9000") }}
25+
OPENEDX_AWS_ACCESS_KEY: {{ OPENEDX_AWS_ACCESS_KEY}}
26+
OPENEDX_AWS_SECRET_ACCESS_KEY: {{ OPENEDX_AWS_SECRET_ACCESS_KEY }}
2627

2728

2829
cms-job:
2930
<<: *openedx-job-service
3031
environment:
3132
DJANGO_SETTINGS_MODULE: cms.envs.tutor.development
33+
MINIO_HOST: {{ MINIO_HOST | default("minio:9000") }}
34+
OPENEDX_AWS_ACCESS_KEY: {{ OPENEDX_AWS_ACCESS_KEY}}
35+
OPENEDX_AWS_SECRET_ACCESS_KEY: {{ OPENEDX_AWS_SECRET_ACCESS_KEY }}
3236

3337
{{ patch("dev-docker-compose-jobs-services")|indent(2) }}

0 commit comments

Comments
 (0)