Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

draft: feat: move plugin CLIs under dev/local/k8s; let plugins run jobs #567

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions tests/commands/test_context.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import os
import unittest

from tests.helpers import TestContext, TestJobRunner, temporary_root
from tests.helpers import TestJobContext, TestJobRunner, temporary_root
from tutor import config as tutor_config


class TestContextTests(unittest.TestCase):
def test_create_testcontext(self) -> None:
class TestJobContextTests(unittest.TestCase):
def test_create_testjobcontext(self) -> None:
with temporary_root() as root:
context = TestContext(root)
config = tutor_config.load_full(root)
runner = context.job_runner(config)
context = TestJobContext(root, {})
runner = context.job_runner()
self.assertTrue(os.path.exists(context.root))
self.assertFalse(
os.path.exists(os.path.join(context.root, tutor_config.CONFIG_FILENAME))
Expand Down
14 changes: 10 additions & 4 deletions tests/helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import tempfile

from tutor.commands.context import BaseJobContext
from tutor.commands.context import BaseJobContext, Context
from tutor.jobs import BaseJobRunner
from tutor.types import Config

Expand Down Expand Up @@ -36,10 +36,16 @@ def temporary_root() -> "tempfile.TemporaryDirectory[str]":
return tempfile.TemporaryDirectory(prefix="tutor-test-root-")


class TestContext(BaseJobContext):
class TestContext(Context):
"""
Barebones click test context.
"""


class TestJobContext(TestContext, BaseJobContext):
"""
Click context that will use only test job runners.
"""

def job_runner(self, config: Config) -> TestJobRunner:
return TestJobRunner(self.root, config)
def job_runner(self) -> TestJobRunner:
return TestJobRunner(self.root, self.config)
15 changes: 8 additions & 7 deletions tests/test_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from io import StringIO
from unittest.mock import patch

from tests.helpers import TestContext, temporary_root
from tests.helpers import TestJobContext, temporary_root
from tutor import config as tutor_config
from tutor import jobs

Expand All @@ -12,9 +12,9 @@ class JobsTests(unittest.TestCase):
@patch("sys.stdout", new_callable=StringIO)
def test_initialise(self, mock_stdout: StringIO) -> None:
with temporary_root() as root:
context = TestContext(root)
config = tutor_config.load_full(root)
runner = context.job_runner(config)
context = TestJobContext(root, config)
runner = context.job_runner()
jobs.initialise(runner)

output = mock_stdout.getvalue().strip()
Expand Down Expand Up @@ -44,9 +44,9 @@ def test_create_user_command_with_staff_with_password(self) -> None:
@patch("sys.stdout", new_callable=StringIO)
def test_import_demo_course(self, mock_stdout: StringIO) -> None:
with temporary_root() as root:
context = TestContext(root)
config = tutor_config.load_full(root)
runner = context.job_runner(config)
context = TestJobContext(root, config)
runner = context.job_runner()
jobs.import_demo_course(runner)

output = mock_stdout.getvalue()
Expand All @@ -64,9 +64,10 @@ def test_import_demo_course(self, mock_stdout: StringIO) -> None:
@patch("sys.stdout", new_callable=StringIO)
def test_set_theme(self, mock_stdout: StringIO) -> None:
with temporary_root() as root:
context = TestContext(root)
config = tutor_config.load_full(root)
runner = context.job_runner(config)
context = TestJobContext(root, config)
config = tutor_config.load_full(root)
runner = context.job_runner()
jobs.set_theme("sample_theme", ["domain1", "domain2"], runner)

output = mock_stdout.getvalue()
Expand Down
36 changes: 13 additions & 23 deletions tutor/commands/compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import click

from .. import bindmounts
from .. import config as tutor_config
from .. import env as tutor_env
from .. import fmt, jobs, utils
from ..exceptions import TutorError
Expand Down Expand Up @@ -56,7 +55,7 @@ def run_job(self, service: str, command: str) -> int:


class BaseComposeContext(BaseJobContext):
def job_runner(self, config: Config) -> ComposeJobRunner:
def job_runner(self) -> ComposeJobRunner:
raise NotImplementedError


Expand All @@ -78,16 +77,14 @@ def start(
command.append("-d")

# Start services
config = tutor_config.load(context.root)
context.job_runner(config).docker_compose(*command, *services)
context.job_runner().docker_compose(*command, *services)


@click.command(help="Stop a running platform")
@click.argument("services", metavar="service", nargs=-1)
@click.pass_obj
def stop(context: BaseComposeContext, services: List[str]) -> None:
config = tutor_config.load(context.root)
context.job_runner(config).docker_compose("stop", *services)
context.job_runner().docker_compose("stop", *services)


@click.command(
Expand All @@ -112,28 +109,26 @@ def reboot(context: click.Context, detach: bool, services: List[str]) -> None:
@click.argument("services", metavar="service", nargs=-1)
@click.pass_obj
def restart(context: BaseComposeContext, services: List[str]) -> None:
config = tutor_config.load(context.root)
command = ["restart"]
if "all" in services:
pass
else:
for service in services:
if service == "openedx":
if config["RUN_LMS"]:
if context.config["RUN_LMS"]:
command += ["lms", "lms-worker"]
if config["RUN_CMS"]:
if context.config["RUN_CMS"]:
command += ["cms", "cms-worker"]
else:
command.append(service)
context.job_runner(config).docker_compose(*command)
context.job_runner().docker_compose(*command)


@click.command(help="Initialise all applications")
@click.option("-l", "--limit", help="Limit initialisation to this service or plugin")
@click.pass_obj
def init(context: BaseComposeContext, limit: str) -> None:
config = tutor_config.load(context.root)
runner = context.job_runner(config)
runner = context.job_runner()
jobs.initialise(runner, limit_to=limit)


Expand All @@ -156,8 +151,7 @@ def createuser(
name: str,
email: str,
) -> None:
config = tutor_config.load(context.root)
runner = context.job_runner(config)
runner = context.job_runner()
command = jobs.create_user_command(superuser, staff, name, email, password=password)
runner.run_job("lms", command)

Expand All @@ -178,17 +172,15 @@ def createuser(
@click.argument("theme_name")
@click.pass_obj
def settheme(context: BaseComposeContext, domains: List[str], theme_name: str) -> None:
config = tutor_config.load(context.root)
runner = context.job_runner(config)
domains = domains or jobs.get_all_openedx_domains(config)
runner = context.job_runner()
domains = domains or jobs.get_all_openedx_domains(context.config)
jobs.set_theme(theme_name, domains, runner)


@click.command(help="Import the demo course")
@click.pass_obj
def importdemocourse(context: BaseComposeContext) -> None:
config = tutor_config.load(context.root)
runner = context.job_runner(config)
runner = context.job_runner()
fmt.echo_info("Importing demo course")
jobs.import_demo_course(runner)

Expand Down Expand Up @@ -221,8 +213,7 @@ def run(context: click.Context, args: List[str]) -> None:
@click.argument("path")
@click.pass_obj
def bindmount_command(context: BaseComposeContext, service: str, path: str) -> None:
config = tutor_config.load(context.root)
host_path = bindmounts.create(context.job_runner(config), service, path)
host_path = bindmounts.create(context.job_runner(), service, path)
fmt.echo_info(
"Bind-mount volume created at {}. You can now use it in all `local` and `dev` commands with the `--volume={}` option.".format(
host_path, path
Expand Down Expand Up @@ -277,7 +268,6 @@ def logs(context: click.Context, follow: bool, tail: bool, service: str) -> None
@click.argument("args", nargs=-1)
@click.pass_obj
def dc_command(context: BaseComposeContext, command: str, args: List[str]) -> None:
config = tutor_config.load(context.root)
volumes, non_volume_args = bindmounts.parse_volumes(args)
volume_args = []
for volume_arg in volumes:
Expand All @@ -293,7 +283,7 @@ def dc_command(context: BaseComposeContext, command: str, args: List[str]) -> No
)
volume_arg = "{}:{}".format(host_bind_path, volume_arg)
volume_args += ["--volume", volume_arg]
context.job_runner(config).docker_compose(command, *volume_args, *non_volume_args)
context.job_runner().docker_compose(command, *volume_args, *non_volume_args)


def add_commands(command_group: click.Group) -> None:
Expand Down
41 changes: 38 additions & 3 deletions tutor/commands/context.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
from ..jobs import BaseJobRunner
from ..types import Config
from ..exceptions import TutorError


class Context:
"""
Context object that is passed to all subcommands.

The project `root` is passed to all subcommands of `tutor`; that's because
it is defined as an argument of the top-level command. For instance:
The project `root` and its loaded `config` are passed to all subcommands of `tutor`;
that's because it is defined as an argument of the top-level command. For instance:

$ tutor --root=... local run ...
"""

def __init__(self, root: str) -> None:
self.root = root

def job_runner(self) -> BaseJobRunner:
"""
We cannot run jobs in this context. Raise an error.
"""
raise TutorError(
"Jobs may not be run under the root context. "
+ "Please specify dev, local, or k8s context.\n"
+ "\n"
+ "For example, if you just ran:\n"
+ " tutor <command>\n"
+ "\n"
+ "then you should instead run one of:\n"
+ " tutor dev <command>\n"
+ " tutor local <command>\n"
+ " tutor k8s <command>"
)


class BaseJobContext(Context):
"""
Expand All @@ -23,8 +41,25 @@ class BaseJobContext(Context):
For instance `dev`, `local` and `k8s` define custom runners to run jobs.
"""

def job_runner(self, config: Config) -> BaseJobRunner:
def __init__(self, root: str, config: Config) -> None:
super().__init__(root)
self._config = config

@property
def config(self) -> Config:
"""
Return this context's configuration dictionary.

Mutations to the dictionary will not affect the context's underlying config.
"""
return self._config.copy()

def job_runner(self) -> BaseJobRunner:
"""
Return a runner capable of running docker-compose/kubectl commands.

All concrete subclasses of BaseJobContext should define this method,
so we raise a `NotImplementedError` here instead of falling back to the
`TutorError` that Context.job_runner raises.
"""
raise NotImplementedError
15 changes: 10 additions & 5 deletions tutor/commands/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .. import fmt
from ..types import Config, get_typed
from . import compose
from .plugins import add_plugin_commands


class DevJobRunner(compose.ComposeJobRunner):
Expand All @@ -31,14 +32,14 @@ def __init__(self, root: str, config: Config):


class DevContext(compose.BaseComposeContext):
def job_runner(self, config: Config) -> DevJobRunner:
return DevJobRunner(self.root, config)
def job_runner(self) -> DevJobRunner:
return DevJobRunner(self.root, self.config)


@click.group(help="Run Open edX locally with development settings")
@click.pass_context
def dev(context: click.Context) -> None:
context.obj = DevContext(context.obj.root)
context.obj = DevContext(context.obj.root, tutor_config.load(context.obj.root))


@click.command(
Expand All @@ -49,10 +50,13 @@ def dev(context: click.Context) -> None:
@click.argument("service")
@click.pass_context
def runserver(context: click.Context, options: List[str], service: str) -> None:
config = tutor_config.load(context.obj.root)
if service in ["lms", "cms"]:
port = 8000 if service == "lms" else 8001
host = config["LMS_HOST"] if service == "lms" else config["CMS_HOST"]
host = (
context.obj.config["LMS_HOST"]
if service == "lms"
else context.obj.config["CMS_HOST"]
)
fmt.echo_info(
"The {} service will be available at http://{}:{}".format(
service, host, port
Expand All @@ -63,4 +67,5 @@ def runserver(context: click.Context, options: List[str], service: str) -> None:


dev.add_command(runserver)
add_plugin_commands(dev)
compose.add_commands(dev)
2 changes: 1 addition & 1 deletion tutor/commands/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def build(
target: str,
docker_args: List[str],
) -> None:
config = tutor_config.load(context.root)
config = tutor_config.load_full(context.root)
command_args = []
if no_cache:
command_args.append("--no-cache")
Expand Down
Loading