Skip to content

Commit

Permalink
feat: add dev/local copyfrom commands
Browse files Browse the repository at this point in the history
`copyfrom` copies data from a container to the local filesystem. It's similar
to bindmount, but less clunky, and more intuitive. Also, it plays along great
with `--mount`. Eventually we'll just get rid of the `bindmount` command and
the `--volume` option.
  • Loading branch information
regisb committed Apr 24, 2022
1 parent fde20f0 commit 27449f4
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Note: Breaking changes between versions are indicated by "💥".

## Unreleased

- [Feature] Introduce `local/dev copyfrom` command to copy contents from a container.
- [Bugfix] Fix a race condition that could prevent a newly provisioned LMS container from starting due to a `FileExistsError` when creating data folders.
- [Deprecation] Mark `tutor dev runserver` as deprecated in favor of `tutor dev start`. Since `start` now supports bind-mounting and breakpoint debugging, `runserver` is redundant and will be removed in a future release.
- [Improvement] Allow breakpoint debugging when attached to a service via `tutor dev start SERVICE`.
Expand Down
16 changes: 15 additions & 1 deletion docs/dev.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,23 @@ So, when should you *not* be using the implicit form? That would be when Tutor d

.. note:: Remember to setup your edx-platform repository for development! See :ref:`edx_platform_dev_env`.

Copy files from containers to the local filesystem
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Sometimes, you may want to modify some of the files inside a container for which you don't have a copy on the host. A typical example is when you want to troubleshoot a Python dependency that is installed inside the application virtual environment. In such cases, you want to first copy the contents of the virtual environment from the container to the local filesystem. To that end, Tutor provides the ``tutor dev copyfrom`` command. First, copy the contents of the container folder to the local filesystem::

tutor dev copyfrom lms /openedx/venv ~

Then, bind-mount that folder back in the container with the ``--mount`` option (described :ref:`above <mount_option>`)::

tutor dev start --mount lms:~/venv:/openedx/venv lms

You can then edit the files in ``~/venv`` on your local filesystem and see the changes live in your container.

Bind-mount from the "volumes/" directory
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. warning:: Bind-mounting volumes with the ``bindmount`` command is no longer the default, recommended way of bind-mounting volumes from the host. Instead, see the :ref:`mount option <mount_option>`.
.. warning:: Bind-mounting volumes with the ``bindmount`` command is no longer the default, recommended way of bind-mounting volumes from the host. Instead, see the :ref:`mount option <mount_option>` and the ``tutor dev/local copyfrom`` commands.

Tutor makes it easy to create a bind-mount from an existing container. First, copy the contents of a container directory with the ``bindmount`` command. For instance, to copy the virtual environment of the "lms" container::

Expand Down Expand Up @@ -231,6 +244,7 @@ After running all these commands, your edx-platform repository will be ready for

If LMS isn't running, this will start it in your terminal. If an LMS container is already running background, this command will stop it, recreate it, and attach your terminal to it. Later, to detach your terminal without stopping the container, just hit ``Ctrl+z``.


XBlock and edx-platform plugin development
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
1 change: 1 addition & 0 deletions tests/commands/test_compose.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import unittest

from click.exceptions import ClickException

from tutor.commands import compose


Expand Down
48 changes: 48 additions & 0 deletions tests/commands/test_local.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import os
import tempfile
import unittest
from unittest.mock import patch

from tests.helpers import temporary_root

from .base import TestCommandMixin

Expand All @@ -18,3 +23,46 @@ def test_local_upgrade_help(self) -> None:
result = self.invoke(["local", "upgrade", "--help"])
self.assertIsNone(result.exception)
self.assertEqual(0, result.exit_code)

def test_copyfrom(self) -> None:
with temporary_root() as root:
with tempfile.TemporaryDirectory() as directory:
with patch("tutor.utils.docker_compose") as mock_docker_compose:
self.invoke_in_root(root, ["config", "save"])

# Copy to existing directory
result = self.invoke_in_root(
root, ["local", "copyfrom", "lms", "/openedx/venv", directory]
)
self.assertIsNone(result.exception)
self.assertEqual(0, result.exit_code)
self.assertIn(
f"--volume={directory}:/tmp/mount",
mock_docker_compose.call_args.args,
)
self.assertIn(
"cp --recursive --preserve /openedx/venv /tmp/mount",
mock_docker_compose.call_args.args,
)

# Copy to non-existing directory
result = self.invoke_in_root(
root,
[
"local",
"copyfrom",
"lms",
"/openedx/venv",
os.path.join(directory, "venv2"),
],
)
self.assertIsNone(result.exception)
self.assertEqual(0, result.exit_code)
self.assertIn(
f"--volume={directory}:/tmp/mount",
mock_docker_compose.call_args.args,
)
self.assertIn(
"cp --recursive --preserve /openedx/venv /tmp/mount/venv2",
mock_docker_compose.call_args.args,
)
59 changes: 52 additions & 7 deletions tutor/commands/compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@
from tutor import bindmounts
from tutor import config as tutor_config
from tutor import env as tutor_env
from tutor import fmt, jobs, utils
from tutor import serialize
from tutor import fmt, hooks, jobs, serialize, utils
from tutor.commands.context import BaseJobContext
from tutor.exceptions import TutorError
from tutor.types import Config
from tutor.commands.context import BaseJobContext
from tutor import hooks


class ComposeJobRunner(jobs.BaseComposeJobRunner):
Expand Down Expand Up @@ -326,15 +324,16 @@ def run(
name="bindmount",
help="Copy the contents of a container directory to a ready-to-bind-mount host directory",
)
@click.argument(
"service",
)
@click.argument("service")
@click.argument("path")
@click.pass_obj
def bindmount_command(context: BaseComposeContext, service: str, path: str) -> None:
"""
This command is made obsolete by the --mount arguments.
"""
fmt.echo_alert(
"The 'bindmount' command is deprecated and will be removed in a later release. Use 'copyfrom' instead."
)
config = tutor_config.load(context.root)
host_path = bindmounts.create(context.job_runner(config), service, path)
fmt.echo_info(
Expand All @@ -343,6 +342,51 @@ def bindmount_command(context: BaseComposeContext, service: str, path: str) -> N
)


@click.command(
name="copyfrom",
help="Copy files/folders from a container directory to the local filesystem.",
)
@click.argument("service")
@click.argument("container_path")
@click.argument(
"host_path",
type=click.Path(dir_okay=True, file_okay=False, resolve_path=True),
)
@click.pass_obj
def copyfrom(
context: BaseComposeContext, service: str, container_path: str, host_path: str
) -> None:
# Path management
container_root_path = "/tmp/mount"
container_dst_path = container_root_path
if not os.path.exists(host_path):
# Emulate cp semantics, where if the destination path does not exist
# then we copy to its parent and rename to the destination folder
container_dst_path += "/" + os.path.basename(host_path)
host_path = os.path.dirname(host_path)
if not os.path.exists(host_path):
raise TutorError(
f"Cannot create directory {host_path}. No such file or directory."
)

# cp/mv commands
command = f"cp --recursive --preserve {container_path} {container_dst_path}"
config = tutor_config.load(context.root)
runner = context.job_runner(config)
runner.docker_compose(
"run",
"--rm",
"--no-deps",
"--user=0",
f"--volume={host_path}:{container_root_path}",
service,
"sh",
"-e",
"-c",
command,
)


@click.command(
short_help="Run a command in a running container",
help=(
Expand Down Expand Up @@ -490,6 +534,7 @@ def add_commands(command_group: click.Group) -> None:
command_group.add_command(settheme)
command_group.add_command(dc_command)
command_group.add_command(run)
command_group.add_command(copyfrom)
command_group.add_command(bindmount_command)
command_group.add_command(execute)
command_group.add_command(logs)
Expand Down
3 changes: 2 additions & 1 deletion tutor/commands/k8s.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

from tutor import config as tutor_config
from tutor import env as tutor_env
from tutor import exceptions, fmt
from tutor import interactive as interactive_config
from tutor import exceptions, fmt, jobs, serialize, utils
from tutor import jobs, serialize, utils
from tutor.commands.config import save as config_save_command
from tutor.commands.context import BaseJobContext
from tutor.commands.upgrade.k8s import upgrade_from
Expand Down
3 changes: 2 additions & 1 deletion tutor/commands/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

from tutor import config as tutor_config
from tutor import env as tutor_env
from tutor import exceptions, fmt, utils
from tutor import exceptions, fmt
from tutor import interactive as interactive_config
from tutor import utils
from tutor.commands import compose
from tutor.commands.config import save as config_save_command
from tutor.commands.upgrade.local import upgrade_from
Expand Down
2 changes: 1 addition & 1 deletion tutor/commands/plugins.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
import urllib.request
import typing as t
import urllib.request

import click

Expand Down

0 comments on commit 27449f4

Please sign in to comment.