From 8277e31d5808b813e8d8378f66072f3a05a38be6 Mon Sep 17 00:00:00 2001 From: "Kyle D. McCormick" Date: Thu, 31 Aug 2023 16:17:08 -0400 Subject: [PATCH] feat: `tutor dev/local populate-mounts` --- tutor/commands/compose.py | 76 +++++++++++++++++++++++++++++++++++++++ tutor/hooks/catalog.py | 5 +++ 2 files changed, 81 insertions(+) diff --git a/tutor/commands/compose.py b/tutor/commands/compose.py index 0f19e3a5e3d..1fe9e790113 100644 --- a/tutor/commands/compose.py +++ b/tutor/commands/compose.py @@ -20,6 +20,7 @@ from tutor.exceptions import TutorError from tutor.tasks import BaseComposeTaskRunner from tutor.types import Config +from tutor.utils import execute as execute_shell class ComposeTaskRunner(BaseComposeTaskRunner): @@ -365,6 +366,59 @@ def copyfrom( ) +@click.command( + help="TODO describe" +) +@click.argument( + "mount_paths", + metavar="mount_path", + nargs=-1, + type=click.Path(dir_okay=True, file_okay=False, resolve_path=True), +) +@click.pass_obj +def populate_mounts(context: BaseComposeContext, mount_paths: list[Path]) -> None: + """ + TODO write docstring + """ + config = tutor_config.load(context.root) + host_mount_paths: list[str] = [ + os.path.abspath(os.path.expanduser(mount_path)) + for mount_path + in mount_paths or bindmount.get_mounts(config) + ] + copies_by_service: list[tuple[str, str]] = {} + for host_mount_path in host_mount_paths: + mount_name = os.path.basename(host_mount_path) + for service, container_mount_path in hooks.Filters.COMPOSE_MOUNTS.iterate(mount_name): + for path_in_mount in hooks.Filters.COMPOSE_MOUNT_POPULATORS.iterate(mount_name, service): + source = f"{container_mount_path}/{path_in_mount}" + target = f"{host_mount_path}/{path_in_mount}" + copies_by_service.setdefault(service, []).append((source, target)) + container_name = "tutor_mounts_populate_temp" # TODO: improve name? + runner = context.job_runner(config) + for service, copies in copies_by_service.items(): + execute_shell("docker", "rm", "-f", container_name) + runner.docker_compose( + "run", + "--detach", + "--rm", + "--no-deps", + "--user=0", + # TODO: Explain this next bit + *(f"--volume={source}" for source, target in copies), + "--name", + container_name, + service, + "sleep", + "infinity", + ) + for source, target in copies: + execute_shell("rm", "-rf", target) + execute_shell("sh", "-c", f'mkdir -p "$(dirname "{target}")"') + execute_shell("docker", "cp", f"{container_name}:{source}", target) + execute_shell("sh", "-c", f"docker kill '{container_name}' || true") + + @click.command( short_help="Run a command in a running container", help=( @@ -447,6 +501,27 @@ def _mount_edx_platform( return volumes +@hooks.Filters.COMPOSE_MOUNT_POPULATORS.add() +def _populate_edx_platform( + paths_to_copy: list[str], mount_name: str, service: str +) -> list[tuple[str, str]]: + """ + TODO describe + """ + if mount_name == "edx-platform" and service == "lms": + paths_to_copy += [ + "Open_edX.egg-info", + "node_modules", + "lms/static/css", + "lms/static/certificates/css", + "cms/static/css", + "common/static/bundles", + "common/static/common/js/vendor", + "common/static/common/css/vendor", + ] + return paths_to_copy + + @hooks.Filters.APP_PUBLIC_HOSTS.add() def _edx_platform_public_hosts( hosts: list[str], context_name: t.Literal["local", "dev"] @@ -471,6 +546,7 @@ def add_commands(command_group: click.Group) -> None: command_group.add_command(dc_command) command_group.add_command(run) command_group.add_command(copyfrom) + command_group.add_command(populate_mounts) command_group.add_command(execute) command_group.add_command(logs) command_group.add_command(status) diff --git a/tutor/hooks/catalog.py b/tutor/hooks/catalog.py index dde0f4a4cf5..d529101d3a1 100644 --- a/tutor/hooks/catalog.py +++ b/tutor/hooks/catalog.py @@ -222,6 +222,11 @@ def your_filter_callback(some_data): #: conditionally add mounts. COMPOSE_MOUNTS: Filter[list[tuple[str, str]], [str]] = Filter() + #: TODO describe + #: + #: :parameter populators: each item ``(service, mount_name, path_in_mount)`` tuple. + COMPOSE_MOUNT_POPULATORS: Filter[list[tuple[str, str, str]]] = Filter() + #: Declare new default configuration settings that don't necessarily have to be saved in the user #: ``config.yml`` file. Default settings may be overridden with ``tutor config save --set=...``, in which #: case they will automatically be added to ``config.yml``.