7
0
mirror of https://github.com/ChristianLight/tutor.git synced 2024-06-17 12:52:21 +00:00
tutor/tutor/commands/compose.py

578 lines
20 KiB
Python
Raw Normal View History

import os
import re
import typing as t
from copy import deepcopy
import click
from typing_extensions import TypeAlias
feat: migrate to plugins.v1 with filters & actions This is a very large refactoring which aims at making Tutor both more extendable and more generic. Historically, the Tutor plugin system was designed as an ad-hoc solution to allow developers to modify their own Open edX platforms without having to fork Tutor. The plugin API was simple, but limited, because of its ad-hoc nature. As a consequence, there were many things that plugin developers could not do, such as extending different parts of the CLI or adding custom template filters. Here, we refactor the whole codebase to make use of a generic plugin system. This system was inspired by the Wordpress plugin API and the Open edX "hooks and filters" API. The various components are added to a small core thanks to a set of actions and filters. Actions are callback functions that can be triggered at different points of the application lifecycle. Filters are functions that modify some data. Both actions and filters are collectively named as "hooks". Hooks can optionally be created within a certain context, which makes it easier to keep track of which application created which callback. This new hooks system allows us to provide a Python API that developers can use to extend their applications. The API reference is added to the documentation, along with a new plugin development tutorial. The plugin v0 API remains supported for backward compatibility of existing plugins. Done: - Do not load commands from plugins which are not enabled. - Load enabled plugins once on start. - Implement contexts for actions and filters, which allow us to keep track of the source of every hook. - Migrate patches - Migrate commands - Migrate plugin detection - Migrate templates_root - Migrate config - Migrate template environment globals and filters - Migrate hooks to tasks - Generate hook documentation - Generate patch reference documentation - Add the concept of action priority Close #499.
2022-02-07 17:11:43 +00:00
from tutor import bindmounts
from tutor import config as tutor_config
from tutor import env as tutor_env
from tutor import fmt, hooks, jobs, serialize, utils
from tutor.commands.context import BaseJobContext
feat: migrate to plugins.v1 with filters & actions This is a very large refactoring which aims at making Tutor both more extendable and more generic. Historically, the Tutor plugin system was designed as an ad-hoc solution to allow developers to modify their own Open edX platforms without having to fork Tutor. The plugin API was simple, but limited, because of its ad-hoc nature. As a consequence, there were many things that plugin developers could not do, such as extending different parts of the CLI or adding custom template filters. Here, we refactor the whole codebase to make use of a generic plugin system. This system was inspired by the Wordpress plugin API and the Open edX "hooks and filters" API. The various components are added to a small core thanks to a set of actions and filters. Actions are callback functions that can be triggered at different points of the application lifecycle. Filters are functions that modify some data. Both actions and filters are collectively named as "hooks". Hooks can optionally be created within a certain context, which makes it easier to keep track of which application created which callback. This new hooks system allows us to provide a Python API that developers can use to extend their applications. The API reference is added to the documentation, along with a new plugin development tutorial. The plugin v0 API remains supported for backward compatibility of existing plugins. Done: - Do not load commands from plugins which are not enabled. - Load enabled plugins once on start. - Implement contexts for actions and filters, which allow us to keep track of the source of every hook. - Migrate patches - Migrate commands - Migrate plugin detection - Migrate templates_root - Migrate config - Migrate template environment globals and filters - Migrate hooks to tasks - Generate hook documentation - Generate patch reference documentation - Add the concept of action priority Close #499.
2022-02-07 17:11:43 +00:00
from tutor.exceptions import TutorError
from tutor.types import Config
COMPOSE_FILTER_TYPE: TypeAlias = "hooks.filters.Filter[t.Dict[str, t.Any], []]"
class ComposeJobRunner(jobs.BaseComposeJobRunner):
def __init__(self, root: str, config: Config):
super().__init__(root, config)
self.project_name = ""
self.docker_compose_files: t.List[str] = []
self.docker_compose_job_files: t.List[str] = []
def docker_compose(self, *command: str) -> int:
"""
Run docker-compose with the right yml files.
"""
if "start" in command or "up" in command or "restart" in command:
# Note that we don't trigger the action on "run". That's because we
# don't want to trigger the action for every initialization script.
hooks.Actions.COMPOSE_PROJECT_STARTED.do(
self.root, self.config, self.project_name
)
args = []
for docker_compose_path in self.docker_compose_files:
if os.path.exists(docker_compose_path):
args += ["-f", docker_compose_path]
return utils.docker_compose(
*args, "--project-name", self.project_name, *command
)
def update_docker_compose_tmp(
self,
compose_tmp_filter: COMPOSE_FILTER_TYPE,
compose_jobs_tmp_filter: COMPOSE_FILTER_TYPE,
docker_compose_tmp_path: str,
docker_compose_jobs_tmp_path: str,
) -> None:
"""
Update the contents of the docker-compose.tmp.yml and
docker-compose.jobs.tmp.yml files, which are generated at runtime.
"""
compose_base: t.Dict[str, t.Any] = {
"version": "{{ DOCKER_COMPOSE_VERSION }}",
"services": {},
}
# 1. Apply compose_tmp filter
# 2. Render the resulting dict
# 3. Serialize to yaml
# 4. Save to disk
docker_compose_tmp: str = serialize.dumps(
tutor_env.render_unknown(
self.config, compose_tmp_filter.apply(deepcopy(compose_base))
)
)
tutor_env.write_to(
docker_compose_tmp,
docker_compose_tmp_path,
)
# Same thing but with tmp jobs
docker_compose_jobs_tmp: str = serialize.dumps(
tutor_env.render_unknown(
self.config, compose_jobs_tmp_filter.apply(deepcopy(compose_base))
)
)
tutor_env.write_to(
docker_compose_jobs_tmp,
docker_compose_jobs_tmp_path,
)
def run_job(self, service: str, command: str) -> int:
"""
Run the "{{ service }}-job" service from local/docker-compose.jobs.yml with the
2021-11-02 17:24:38 +00:00
specified command.
"""
run_command = []
for docker_compose_path in self.docker_compose_job_files:
path = tutor_env.pathjoin(self.root, docker_compose_path)
if os.path.exists(path):
run_command += ["-f", path]
run_command += ["run", "--rm"]
if not utils.is_a_tty():
run_command += ["-T"]
job_service_name = f"{service}-job"
return self.docker_compose(
*run_command,
job_service_name,
"sh",
"-e",
"-c",
command,
)
class BaseComposeContext(BaseJobContext):
COMPOSE_TMP_FILTER: COMPOSE_FILTER_TYPE = NotImplemented
COMPOSE_JOBS_TMP_FILTER: COMPOSE_FILTER_TYPE = NotImplemented
def job_runner(self, config: Config) -> ComposeJobRunner:
raise NotImplementedError
class MountParam(click.ParamType):
"""
Parser for --mount arguments of the form "service1[,service2,...]:/host/path:/container/path".
"""
name = "mount"
MountType = t.Tuple[str, str, str]
# Note that this syntax does not allow us to include colon ':' characters in paths
PARAM_REGEXP = (
r"(?P<services>[a-zA-Z0-9-_, ]+):(?P<host_path>[^:]+):(?P<container_path>[^:]+)"
)
def convert(
self,
value: str,
param: t.Optional["click.Parameter"],
ctx: t.Optional[click.Context],
) -> t.List["MountType"]:
mounts = self.convert_explicit_form(value) or self.convert_implicit_form(value)
return mounts
def convert_explicit_form(self, value: str) -> t.List["MountParam.MountType"]:
"""
Argument is of the form "containers:/host/path:/container/path".
"""
match = re.match(self.PARAM_REGEXP, value)
if not match:
return []
mounts: t.List["MountParam.MountType"] = []
services: t.List[str] = [
service.strip() for service in match["services"].split(",")
]
host_path = os.path.abspath(os.path.expanduser(match["host_path"]))
host_path = host_path.replace(os.path.sep, "/")
container_path = match["container_path"]
for service in services:
if not service:
self.fail(f"incorrect services syntax: '{match['services']}'")
mounts.append((service, host_path, container_path))
return mounts
def convert_implicit_form(self, value: str) -> t.List["MountParam.MountType"]:
"""
Argument is of the form "/host/path"
"""
mounts: t.List["MountParam.MountType"] = []
host_path = os.path.abspath(os.path.expanduser(value))
for service, container_path in hooks.Filters.COMPOSE_MOUNTS.iterate(
os.path.basename(host_path)
):
mounts.append((service, host_path, container_path))
if not mounts:
raise self.fail(f"no mount found for {value}")
return mounts
mount_option = click.option(
"-m",
"--mount",
"mounts",
help="""Bind-mount a folder from the host in the right containers. This option can take two different forms. The first one is explicit: 'service1[,service2...]:/host/path:/container/path'. The other is implicit: '/host/path'. Arguments passed in the implicit form will be parsed by plugins to define the right folders to bind-mount from the host.""",
type=MountParam(),
multiple=True,
)
def mount_tmp_volumes(
all_mounts: t.Tuple[t.List[MountParam.MountType], ...],
context: BaseComposeContext,
) -> None:
for mounts in all_mounts:
for service, host_path, container_path in mounts:
mount_tmp_volume(service, host_path, container_path, context)
def mount_tmp_volume(
service: str,
host_path: str,
container_path: str,
context: BaseComposeContext,
) -> None:
"""
Append user-defined bind-mounted volumes to the docker-compose.tmp file(s).
The service/host path/container path values are appended to the docker-compose
files by mean of two filters. Each dev/local environment is then responsible for
generating the files based on the output of these filters.
Bind-mounts that are associated to "*-job" services will be added to the
docker-compose jobs file.
"""
fmt.echo_info(f"Bind-mount: {host_path} -> {container_path} in {service}")
compose_tmp_filter: COMPOSE_FILTER_TYPE = (
context.COMPOSE_JOBS_TMP_FILTER
if service.endswith("-job")
else context.COMPOSE_TMP_FILTER
)
@compose_tmp_filter.add()
def _add_mounts_to_docker_compose_tmp(
docker_compose: t.Dict[str, t.Any],
) -> t.Dict[str, t.Any]:
services = docker_compose.setdefault("services", {})
services.setdefault(service, {"volumes": []})
services[service]["volumes"].append(f"{host_path}:{container_path}")
return docker_compose
@click.command(
short_help="Run all or a selection of services.",
help="Run all or a selection of services. Docker images will be rebuilt where necessary.",
)
feat: upgrade to Maple - A shared cookie domain between lms and cms is no longer recommended: https://github.com/edx/edx-platform/blob/master/docs/guides/studio_oauth.rst - refactor: clean mounted data folder in lms/cms. In Lilac, the bind-mounted lms/data and cms/data folders are a mess because new folders are created there for every new course organisation. These folders are empty. As far as we know they are useless... With this change we move these folders to a dedicated "modulestore" subdirectory; which corresponds better to the initial intent of the fs_root setting. - fix: frontend failure during login to the lms. See: https://github.com/openedx/build-test-release-wg/issues/104 - feat: move all forum-related code to a dedicated plugin. Forum is an optional feature, and as such it deserves its own plugin. Starting from Maple, users will be able to install the forum from https://github.com/overhangio/tutor-forum/ - migrate from DCS_* session cookie settings to SESSION_*. That's because edx-platform no longer depends on django-cookies-samesite. Close https://github.com/openedx/build-test-release-wg/issues/110 - get rid of tons of deprecation warnings in the lms/cms - feat: make it possible to point to themed assets. Cherry-picking this change makes it possible to point to themed assets with a theme-agnostic url, notably from MFEs. - Install all official plugins as part of the `tutor[full]` package. - Don't print error messages about loading plugins during autocompletion. - Prompt for image building when upgrading from one release to the next. - Add `tutor local start --skip-build` option to skip building Docker images. Close #450. Close #545.
2021-10-18 09:43:40 +00:00
@click.option("--skip-build", is_flag=True, help="Skip image building")
@click.option("-d", "--detach", is_flag=True, help="Start in daemon mode")
@mount_option
@click.argument("services", metavar="service", nargs=-1)
@click.pass_obj
feat: upgrade to Maple - A shared cookie domain between lms and cms is no longer recommended: https://github.com/edx/edx-platform/blob/master/docs/guides/studio_oauth.rst - refactor: clean mounted data folder in lms/cms. In Lilac, the bind-mounted lms/data and cms/data folders are a mess because new folders are created there for every new course organisation. These folders are empty. As far as we know they are useless... With this change we move these folders to a dedicated "modulestore" subdirectory; which corresponds better to the initial intent of the fs_root setting. - fix: frontend failure during login to the lms. See: https://github.com/openedx/build-test-release-wg/issues/104 - feat: move all forum-related code to a dedicated plugin. Forum is an optional feature, and as such it deserves its own plugin. Starting from Maple, users will be able to install the forum from https://github.com/overhangio/tutor-forum/ - migrate from DCS_* session cookie settings to SESSION_*. That's because edx-platform no longer depends on django-cookies-samesite. Close https://github.com/openedx/build-test-release-wg/issues/110 - get rid of tons of deprecation warnings in the lms/cms - feat: make it possible to point to themed assets. Cherry-picking this change makes it possible to point to themed assets with a theme-agnostic url, notably from MFEs. - Install all official plugins as part of the `tutor[full]` package. - Don't print error messages about loading plugins during autocompletion. - Prompt for image building when upgrading from one release to the next. - Add `tutor local start --skip-build` option to skip building Docker images. Close #450. Close #545.
2021-10-18 09:43:40 +00:00
def start(
context: BaseComposeContext,
skip_build: bool,
detach: bool,
mounts: t.Tuple[t.List[MountParam.MountType]],
services: t.List[str],
feat: upgrade to Maple - A shared cookie domain between lms and cms is no longer recommended: https://github.com/edx/edx-platform/blob/master/docs/guides/studio_oauth.rst - refactor: clean mounted data folder in lms/cms. In Lilac, the bind-mounted lms/data and cms/data folders are a mess because new folders are created there for every new course organisation. These folders are empty. As far as we know they are useless... With this change we move these folders to a dedicated "modulestore" subdirectory; which corresponds better to the initial intent of the fs_root setting. - fix: frontend failure during login to the lms. See: https://github.com/openedx/build-test-release-wg/issues/104 - feat: move all forum-related code to a dedicated plugin. Forum is an optional feature, and as such it deserves its own plugin. Starting from Maple, users will be able to install the forum from https://github.com/overhangio/tutor-forum/ - migrate from DCS_* session cookie settings to SESSION_*. That's because edx-platform no longer depends on django-cookies-samesite. Close https://github.com/openedx/build-test-release-wg/issues/110 - get rid of tons of deprecation warnings in the lms/cms - feat: make it possible to point to themed assets. Cherry-picking this change makes it possible to point to themed assets with a theme-agnostic url, notably from MFEs. - Install all official plugins as part of the `tutor[full]` package. - Don't print error messages about loading plugins during autocompletion. - Prompt for image building when upgrading from one release to the next. - Add `tutor local start --skip-build` option to skip building Docker images. Close #450. Close #545.
2021-10-18 09:43:40 +00:00
) -> None:
command = ["up", "--remove-orphans"]
if not skip_build:
command.append("--build")
if detach:
command.append("-d")
# Start services
mount_tmp_volumes(mounts, context)
config = tutor_config.load(context.root)
context.job_runner(config).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: t.List[str]) -> None:
config = tutor_config.load(context.root)
context.job_runner(config).docker_compose("stop", *services)
@click.command(
short_help="Reboot an existing platform",
help="This is more than just a restart: with reboot, the platform is fully stopped before being restarted again",
)
@click.option("-d", "--detach", is_flag=True, help="Start in daemon mode")
@click.argument("services", metavar="service", nargs=-1)
@click.pass_context
def reboot(context: click.Context, detach: bool, services: t.List[str]) -> None:
context.invoke(stop, services=services)
context.invoke(start, detach=detach, services=services)
@click.command(
short_help="Restart some components from a running platform.",
help="""Specify 'openedx' to restart the lms, cms and workers, or 'all' to
restart all services. Note that this performs a 'docker-compose restart', so new images
may not be taken into account. It is useful for reloading settings, for instance. To
fully stop the platform, use the 'reboot' command.""",
)
@click.argument("services", metavar="service", nargs=-1)
@click.pass_obj
def restart(context: BaseComposeContext, services: t.List[str]) -> None:
config = tutor_config.load(context.root)
command = ["restart"]
if "all" in services:
pass
else:
for service in services:
if service == "openedx":
v11.0.0 (2020-12-09) - 💥[Improvement] Upgrade Open edX to Koa - 💥 Setting changes: - The ``ACTIVATE_HTTPS`` setting was renamed to ``ENABLE_HTTPS``. - Other ``ACTIVATE_*`` variables were all renamed to ``RUN_*``. - The ``WEB_PROXY`` setting was removed and ``RUN_CADDY`` was added. - The ``NGINX_HTTPS_PORT`` setting is deprecated. - Architectural changes: - Use Caddy as a web proxy for automated SSL/TLS certificate generation: - Nginx no longer listens to port 443 for https traffic - The Caddy configuration file comes with a new ``caddyfile`` patch for much simpler SSL/TLS management. - Configuration files for web proxies are no longer provided. - Kubernetes deployment no longer requires setting up a custom Ingress resource or custom manager. - Gunicorn and Whitenoise are replaced by uwsgi: this increases boostrap performance and makes it no longer necessary to mount media folders in the Nginx container. - Replace memcached and rabbitmq by redis. - Additional features: - Make it possible to disable all plugins at once with ``plugins disable all``. - Add ``tutor k8s wait`` command to wait for a pod to become ready - Faster, more reliable static assets with local memory caching - Deprecation: proxy files for Apache and Nginx are no longer provided out of the box. - Removed plugin `{{ patch (...) }}` statements: - "https-create", "k8s-ingress-rules", "k8s-ingress-tls-hosts": these are no longer necessary. Instead, declare your app in the "caddyfile" patch. - "local-docker-compose-nginx-volumes": this patch was primarily used to serve media assets. The recommended is now to serve assets with uwsgi.
2020-09-17 10:53:14 +00:00
if config["RUN_LMS"]:
command += ["lms", "lms-worker"]
v11.0.0 (2020-12-09) - 💥[Improvement] Upgrade Open edX to Koa - 💥 Setting changes: - The ``ACTIVATE_HTTPS`` setting was renamed to ``ENABLE_HTTPS``. - Other ``ACTIVATE_*`` variables were all renamed to ``RUN_*``. - The ``WEB_PROXY`` setting was removed and ``RUN_CADDY`` was added. - The ``NGINX_HTTPS_PORT`` setting is deprecated. - Architectural changes: - Use Caddy as a web proxy for automated SSL/TLS certificate generation: - Nginx no longer listens to port 443 for https traffic - The Caddy configuration file comes with a new ``caddyfile`` patch for much simpler SSL/TLS management. - Configuration files for web proxies are no longer provided. - Kubernetes deployment no longer requires setting up a custom Ingress resource or custom manager. - Gunicorn and Whitenoise are replaced by uwsgi: this increases boostrap performance and makes it no longer necessary to mount media folders in the Nginx container. - Replace memcached and rabbitmq by redis. - Additional features: - Make it possible to disable all plugins at once with ``plugins disable all``. - Add ``tutor k8s wait`` command to wait for a pod to become ready - Faster, more reliable static assets with local memory caching - Deprecation: proxy files for Apache and Nginx are no longer provided out of the box. - Removed plugin `{{ patch (...) }}` statements: - "https-create", "k8s-ingress-rules", "k8s-ingress-tls-hosts": these are no longer necessary. Instead, declare your app in the "caddyfile" patch. - "local-docker-compose-nginx-volumes": this patch was primarily used to serve media assets. The recommended is now to serve assets with uwsgi.
2020-09-17 10:53:14 +00:00
if config["RUN_CMS"]:
command += ["cms", "cms-worker"]
else:
command.append(service)
context.job_runner(config).docker_compose(*command)
@click.command(help="Initialise all applications")
@click.option("-l", "--limit", help="Limit initialisation to this service or plugin")
@mount_option
@click.pass_obj
def init(
context: BaseComposeContext,
limit: str,
mounts: t.Tuple[t.List[MountParam.MountType]],
) -> None:
mount_tmp_volumes(mounts, context)
config = tutor_config.load(context.root)
runner = context.job_runner(config)
jobs.initialise(runner, limit_to=limit)
@click.command(help="Create an Open edX user and interactively set their password")
@click.option("--superuser", is_flag=True, help="Make superuser")
@click.option("--staff", is_flag=True, help="Make staff user")
@click.option(
"-p",
"--password",
help="Specify password from the command line. If undefined, you will be prompted to input a password",
)
@click.argument("name")
@click.argument("email")
@click.pass_obj
def createuser(
context: BaseComposeContext,
superuser: str,
staff: bool,
password: str,
name: str,
email: str,
) -> None:
config = tutor_config.load(context.root)
runner = context.job_runner(config)
command = jobs.create_user_command(superuser, staff, name, email, password=password)
runner.run_job("lms", command)
@click.command(
help="Assign a theme to the LMS and the CMS. To reset to the default theme , use 'default' as the theme name."
)
@click.option(
"-d",
"--domain",
"domains",
multiple=True,
help=(
"Limit the theme to these domain names. By default, the theme is "
"applied to the LMS and the CMS, both in development and production mode"
),
)
@click.argument("theme_name")
@click.pass_obj
def settheme(
context: BaseComposeContext, domains: t.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)
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)
fmt.echo_info("Importing demo course")
jobs.import_demo_course(runner)
@click.command(
short_help="Run a command in a new container",
help=(
"Run a command in a new container. This is a wrapper around `docker-compose run`. Any option or argument passed"
" to this command will be forwarded to docker-compose. Thus, you may use `-v` or `-p` to mount volumes and"
" expose ports."
),
context_settings={"ignore_unknown_options": True},
)
@mount_option
@click.argument("args", nargs=-1, required=True)
@click.pass_context
def run(
context: click.Context,
mounts: t.Tuple[t.List[MountParam.MountType]],
args: t.List[str],
) -> None:
extra_args = ["--rm"]
if not utils.is_a_tty():
extra_args.append("-T")
context.invoke(dc_command, mounts=mounts, command="run", args=[*extra_args, *args])
@click.command(
name="bindmount",
help="Copy the contents of a container directory to a ready-to-bind-mount host directory",
)
@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(
f"Bind-mount volume created at {host_path}. You can now use it in all `local` and `dev` "
f"commands with the `--volume={path}` option."
)
@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=(
"Run a command in a running container. This is a wrapper around `docker-compose exec`. Any option or argument"
" passed to this command will be forwarded to docker-compose. Thus, you may use `-e` to manually define"
" environment variables."
),
context_settings={"ignore_unknown_options": True},
name="exec",
)
@click.argument("args", nargs=-1, required=True)
@click.pass_context
def execute(context: click.Context, args: t.List[str]) -> None:
context.invoke(dc_command, command="exec", args=args)
@click.command(
short_help="View output from containers",
help="View output from containers. This is a wrapper around `docker-compose logs`.",
)
@click.option("-f", "--follow", is_flag=True, help="Follow log output")
@click.option("--tail", type=int, help="Number of lines to show from each container")
@click.argument("service", nargs=-1)
@click.pass_context
def logs(context: click.Context, follow: bool, tail: bool, service: str) -> None:
args = []
if follow:
args.append("--follow")
if tail is not None:
args += ["--tail", str(tail)]
args += service
context.invoke(dc_command, command="logs", args=args)
@click.command(help="Print status information for containers")
@click.pass_context
def status(context: click.Context) -> None:
context.invoke(dc_command, command="ps")
@click.command(
short_help="Direct interface to docker-compose.",
help=(
"Direct interface to docker-compose. This is a wrapper around `docker-compose`. Most commands, options and"
" arguments passed to this command will be forwarded as-is to docker-compose."
),
context_settings={"ignore_unknown_options": True},
name="dc",
)
@mount_option
@click.argument("command")
@click.argument("args", nargs=-1)
@click.pass_obj
def dc_command(
context: BaseComposeContext,
mounts: t.Tuple[t.List[MountParam.MountType]],
command: str,
args: t.List[str],
) -> None:
mount_tmp_volumes(mounts, context)
config = tutor_config.load(context.root)
volumes, non_volume_args = bindmounts.parse_volumes(args)
volume_args = []
for volume_arg in volumes:
if ":" not in volume_arg:
# This is a bind-mounted volume from the "volumes/" folder.
host_bind_path = bindmounts.get_path(context.root, volume_arg)
if not os.path.exists(host_bind_path):
raise TutorError(
f"Bind-mount volume directory {host_bind_path} does not exist. It must first be created "
f"with the '{bindmount_command.name}' command."
)
volume_arg = f"{host_bind_path}:{volume_arg}"
volume_args += ["--volume", volume_arg]
context.job_runner(config).docker_compose(command, *volume_args, *non_volume_args)
@hooks.Filters.COMPOSE_MOUNTS.add()
def _mount_edx_platform(
volumes: t.List[t.Tuple[str, str]], name: str
) -> t.List[t.Tuple[str, str]]:
"""
When mounting edx-platform with `--mount=/path/to/edx-platform`, bind-mount the host
repo in the lms/cms containers.
"""
if name == "edx-platform":
path = "/openedx/edx-platform"
volumes += [
("lms", path),
("cms", path),
("lms-worker", path),
("cms-worker", path),
("lms-job", path),
("cms-job", path),
]
return volumes
def add_commands(command_group: click.Group) -> None:
command_group.add_command(start)
command_group.add_command(stop)
command_group.add_command(restart)
command_group.add_command(reboot)
command_group.add_command(init)
command_group.add_command(createuser)
command_group.add_command(importdemocourse)
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)
command_group.add_command(status)