6
0
mirror of https://github.com/ChristianLight/tutor.git synced 2024-12-12 14:17:46 +00:00

Move "-r/--root" option to parent command level

This commit is contained in:
Régis Behmo 2019-12-12 17:05:56 +01:00
parent e9f200a102
commit 13de3c8adc
14 changed files with 266 additions and 233 deletions

View File

@ -4,6 +4,7 @@ Note: Breaking changes between versions are indicated by "💥".
## Unreleased ## Unreleased
- [Improvement] Move ``-r/--root`` option to parent command level
- [Bugfix] Fix course about page visibility - [Bugfix] Fix course about page visibility
- [Improvement] Print gunicorn access logs in the console - [Improvement] Print gunicorn access logs in the console
- 💥[Improvement] Get rid of the `indexcourses` and `portainer` command (#269) - 💥[Improvement] Get rid of the `indexcourses` and `portainer` command (#269)

View File

@ -5,7 +5,12 @@ Local deployment
This method is for deploying Open edX locally on a single server, where docker images are orchestrated with `docker-compose <https://docs.docker.com/compose/overview/>`_. This method is for deploying Open edX locally on a single server, where docker images are orchestrated with `docker-compose <https://docs.docker.com/compose/overview/>`_.
In the following, environment and data files will be generated in a user-specific project folder which will be referred to as the "**project root**". On Linux, the default project root is ``~/.local/share/tutor``. An alternative project root can be defined by passing the ``--root=...`` option to most commands, or define the ``TUTOR_ROOT=...`` environment variable. In the following, environment and data files will be generated in a user-specific project folder which will be referred to as the "**project root**". On Linux, the default project root is ``~/.local/share/tutor``. An alternative project root can be defined by passing the ``--root=...`` option to the ``tutor`` command, or define the ``TUTOR_ROOT=...`` environment variable::
tutor --root=/path/to/tutorroot run ...
# Or equivalently:
export TUTOR_ROOT=/path/to/tutorroot
tutor run ...
Main commands Main commands
------------- -------------

View File

@ -3,7 +3,6 @@ import click
from .. import config as tutor_config from .. import config as tutor_config
from .. import env as tutor_env from .. import env as tutor_env
from .. import fmt from .. import fmt
from .. import opts
from .. import utils from .. import utils
@ -18,31 +17,31 @@ def build():
@click.command(help="Build the application in debug mode") @click.command(help="Build the application in debug mode")
@opts.root @click.pass_obj
def debug(root): def debug(context):
docker_run(root) docker_run(context.root)
fmt.echo_info( fmt.echo_info(
"The debuggable APK file is available in {}".format( "The debuggable APK file is available in {}".format(
tutor_env.data_path(root, "android") tutor_env.data_path(context.root, "android")
) )
) )
@click.command(help="Build the application in release mode") @click.command(help="Build the application in release mode")
@opts.root @click.pass_obj
def release(root): def release(context):
docker_run(root, "./gradlew", "assembleProdRelease") docker_run(context.root, "./gradlew", "assembleProdRelease")
fmt.echo_info( fmt.echo_info(
"The production APK file is available in {}".format( "The production APK file is available in {}".format(
tutor_env.data_path(root, "android") tutor_env.data_path(context.root, "android")
) )
) )
@click.command(help="Pull the docker image") @click.command(help="Pull the docker image")
@opts.root @click.pass_obj
def pullimage(root): def pullimage(context):
config = tutor_config.load(root) config = tutor_config.load(context.root)
utils.execute("docker", "pull", config["DOCKER_IMAGE_ANDROID"]) utils.execute("docker", "pull", config["DOCKER_IMAGE_ANDROID"])

View File

@ -1,6 +1,7 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
import sys import sys
import appdirs
import click import click
import click_repl import click_repl
@ -18,9 +19,15 @@ from .. import exceptions
from .. import fmt from .. import fmt
# pylint: disable=too-few-public-methods
class Context:
def __init__(self, root):
self.root = root
def main(): def main():
try: try:
cli() cli() # pylint: disable=no-value-for-parameter
except exceptions.TutorError as e: except exceptions.TutorError as e:
fmt.echo_error("Error: {}".format(e.args[0])) fmt.echo_error("Error: {}".format(e.args[0]))
sys.exit(1) sys.exit(1)
@ -28,8 +35,18 @@ def main():
@click.group(context_settings={"help_option_names": ["-h", "--help", "help"]}) @click.group(context_settings={"help_option_names": ["-h", "--help", "help"]})
@click.version_option(version=__version__) @click.version_option(version=__version__)
def cli(): @click.option(
pass "-r",
"--root",
envvar="TUTOR_ROOT",
default=appdirs.user_data_dir(appname="tutor"),
show_default=True,
type=click.Path(resolve_path=True),
help="Root project directory (environment variable: TUTOR_ROOT)",
)
@click.pass_context
def cli(context, root):
context.obj = Context(root)
@click.command(help="Print this help", name="help") @click.command(help="Print this help", name="help")

View File

@ -5,7 +5,7 @@ from .. import env
from .. import exceptions from .. import exceptions
from .. import fmt from .. import fmt
from .. import interactive as interactive_config from .. import interactive as interactive_config
from .. import opts from .. import serialize
@click.group( @click.group(
@ -17,14 +17,24 @@ def config_command():
pass pass
class YamlParamType(click.ParamType):
name = "yaml"
def convert(self, value, param, ctx):
try:
k, v = value.split("=")
except ValueError:
self.fail("'{}' is not of the form 'key=value'.".format(value), param, ctx)
return k, serialize.parse(v)
@click.command(help="Create and save configuration interactively") @click.command(help="Create and save configuration interactively")
@opts.root
@click.option("-i", "--interactive", is_flag=True, help="Run interactively") @click.option("-i", "--interactive", is_flag=True, help="Run interactively")
@click.option( @click.option(
"-s", "-s",
"--set", "--set",
"set_", "set_",
type=opts.YamlParamType(), type=YamlParamType(),
multiple=True, multiple=True,
metavar="KEY=VAL", metavar="KEY=VAL",
help="Set a configuration value (can be used multiple times)", help="Set a configuration value (can be used multiple times)",
@ -35,28 +45,31 @@ def config_command():
multiple=True, multiple=True,
help="Remove a configuration value (can be used multiple times)", help="Remove a configuration value (can be used multiple times)",
) )
def save(root, interactive, set_, unset): @click.pass_obj
config, defaults = interactive_config.load_all(root, interactive=interactive) def save(context, interactive, set_, unset):
config, defaults = interactive_config.load_all(
context.root, interactive=interactive
)
if set_: if set_:
tutor_config.merge(config, dict(set_), force=True) tutor_config.merge(config, dict(set_), force=True)
for key in unset: for key in unset:
config.pop(key, None) config.pop(key, None)
tutor_config.save(root, config) tutor_config.save(context.root, config)
tutor_config.merge(config, defaults) tutor_config.merge(config, defaults)
env.save(root, config) env.save(context.root, config)
@click.command(help="Print the project root") @click.command(help="Print the project root")
@opts.root @click.pass_obj
def printroot(root): def printroot(context):
click.echo(root) click.echo(context.root)
@click.command(help="Print a configuration value") @click.command(help="Print a configuration value")
@opts.root
@click.argument("key") @click.argument("key")
def printvalue(root, key): @click.pass_obj
config = tutor_config.load(root) def printvalue(context, key):
config = tutor_config.load(context.root)
try: try:
fmt.echo(config[key]) fmt.echo(config[key])
except KeyError: except KeyError:

View File

@ -2,7 +2,6 @@ import click
from .. import env as tutor_env from .. import env as tutor_env
from .. import fmt from .. import fmt
from .. import opts
from .. import utils from .. import utils
@ -11,53 +10,72 @@ def dev():
pass pass
edx_platform_path_option = click.option(
"-P",
"--edx-platform-path",
envvar="TUTOR_EDX_PLATFORM_PATH",
type=click.Path(exists=True, dir_okay=True, resolve_path=True),
help="Mount a local edx-platform from the host (environment variable: TUTOR_EDX_PLATFORM_PATH)",
)
edx_platform_development_settings_option = click.option(
"-S",
"--edx-platform-settings",
envvar="TUTOR_EDX_PLATFORM_SETTINGS",
default="tutor.development",
help="Mount a local edx-platform from the host (environment variable: TUTOR_EDX_PLATFORM_PATH)",
)
@click.command( @click.command(
help="Run a command in one of the containers", help="Run a command in one of the containers",
context_settings={"ignore_unknown_options": True}, context_settings={"ignore_unknown_options": True},
) )
@opts.root @edx_platform_path_option
@opts.edx_platform_path @edx_platform_development_settings_option
@opts.edx_platform_development_settings
@click.argument("service") @click.argument("service")
@click.argument("command", default=None, required=False) @click.argument("command", default=None, required=False)
@click.argument("args", nargs=-1) @click.argument("args", nargs=-1)
def run(root, edx_platform_path, edx_platform_settings, service, command, args): @click.pass_obj
def run(context, edx_platform_path, edx_platform_settings, service, command, args):
run_command = [service] run_command = [service]
if command: if command:
run_command.append(command) run_command.append(command)
if args: if args:
run_command += args run_command += args
docker_compose_run(root, edx_platform_path, edx_platform_settings, *run_command) docker_compose_run(
context.root, edx_platform_path, edx_platform_settings, *run_command
)
@click.command( @click.command(
help="Exec a command in a running container", help="Exec a command in a running container",
context_settings={"ignore_unknown_options": True}, context_settings={"ignore_unknown_options": True},
) )
@opts.root
@click.argument("service") @click.argument("service")
@click.argument("command") @click.argument("command")
@click.argument("args", nargs=-1) @click.argument("args", nargs=-1)
def execute(root, service, command, args): @click.pass_obj
def execute(context, service, command, args):
exec_command = ["exec", service, command] exec_command = ["exec", service, command]
if args: if args:
exec_command += args exec_command += args
docker_compose(root, *exec_command) docker_compose(context.root, *exec_command)
@click.command(help="Run a development server") @click.command(help="Run a development server")
@opts.root @edx_platform_path_option
@opts.edx_platform_path @edx_platform_development_settings_option
@opts.edx_platform_development_settings
@click.argument("service", type=click.Choice(["lms", "cms"])) @click.argument("service", type=click.Choice(["lms", "cms"]))
def runserver(root, edx_platform_path, edx_platform_settings, service): @click.pass_obj
def runserver(context, edx_platform_path, edx_platform_settings, service):
port = service_port(service) port = service_port(service)
fmt.echo_info( fmt.echo_info(
"The {} service will be available at http://localhost:{}".format(service, port) "The {} service will be available at http://localhost:{}".format(service, port)
) )
docker_compose_run( docker_compose_run(
root, context.root,
edx_platform_path, edx_platform_path,
edx_platform_settings, edx_platform_settings,
"-p", "-p",
@ -71,9 +89,9 @@ def runserver(root, edx_platform_path, edx_platform_settings, service):
@click.command(help="Stop a running development platform") @click.command(help="Stop a running development platform")
@opts.root @click.pass_obj
def stop(root): def stop(context):
docker_compose(root, "rm", "--stop", "--force") docker_compose(context.root, "rm", "--stop", "--force")
def docker_compose_run(root, edx_platform_path, edx_platform_settings, *command): def docker_compose_run(root, edx_platform_path, edx_platform_settings, *command):

View File

@ -5,7 +5,6 @@ import click
from .. import config as tutor_config from .. import config as tutor_config
from .. import env as tutor_env from .. import env as tutor_env
from .. import images from .. import images
from .. import opts
from .. import plugins from .. import plugins
BASE_IMAGE_NAMES = ["openedx", "forum", "android"] BASE_IMAGE_NAMES = ["openedx", "forum", "android"]
@ -21,7 +20,6 @@ def images_command():
short_help="Build docker images", short_help="Build docker images",
help="Build the docker images necessary for an Open edX platform.", help="Build the docker images necessary for an Open edX platform.",
) )
@opts.root
@click.argument("image", nargs=-1) @click.argument("image", nargs=-1)
@click.option( @click.option(
"--no-cache", is_flag=True, help="Do not use cache when building the image" "--no-cache", is_flag=True, help="Do not use cache when building the image"
@ -32,10 +30,11 @@ def images_command():
multiple=True, multiple=True,
help="Set build-time docker ARGS in the form 'myarg=value'. This option may be specified multiple times.", help="Set build-time docker ARGS in the form 'myarg=value'. This option may be specified multiple times.",
) )
def build(root, image, no_cache, build_arg): @click.pass_obj
config = tutor_config.load(root) def build(context, image, no_cache, build_arg):
config = tutor_config.load(context.root)
for i in image: for i in image:
build_image(root, config, i, no_cache, build_arg) build_image(context.root, config, i, no_cache, build_arg)
def build_image(root, config, image, no_cache, build_arg): def build_image(root, config, image, no_cache, build_arg):
@ -77,10 +76,10 @@ def build_image(root, config, image, no_cache, build_arg):
@click.command(short_help="Pull images from the Docker registry") @click.command(short_help="Pull images from the Docker registry")
@opts.root
@click.argument("image", nargs=-1) @click.argument("image", nargs=-1)
def pull(root, image): @click.pass_obj
config = tutor_config.load(root) def pull(context, image):
config = tutor_config.load(context.root)
for i in image: for i in image:
pull_image(config, i) pull_image(config, i)
@ -101,15 +100,15 @@ def pull_image(config, image):
@click.command(short_help="Push images to the Docker registry") @click.command(short_help="Push images to the Docker registry")
@opts.root
@click.argument("image", nargs=-1) @click.argument("image", nargs=-1)
def push(root, image): @click.pass_obj
config = tutor_config.load(root) def push(context, image):
config = tutor_config.load(context.root)
for i in image: for i in image:
push_image(root, config, i) push_image(config, i)
def push_image(root, config, image): def push_image(config, image):
# Push base images # Push base images
for img in BASE_IMAGE_NAMES: for img in BASE_IMAGE_NAMES:
if image in [img, "all"]: if image in [img, "all"]:

View File

@ -4,7 +4,6 @@ from .. import config as tutor_config
from .. import env as tutor_env from .. import env as tutor_env
from .. import fmt from .. import fmt
from .. import interactive as interactive_config from .. import interactive as interactive_config
from .. import opts
from .. import scripts from .. import scripts
from .. import utils from .. import utils
@ -15,11 +14,11 @@ def k8s():
@click.command(help="Configure and run Open edX from scratch") @click.command(help="Configure and run Open edX from scratch")
@opts.root
@click.option("-I", "--non-interactive", is_flag=True, help="Run non-interactively") @click.option("-I", "--non-interactive", is_flag=True, help="Run non-interactively")
def quickstart(root, non_interactive): @click.pass_obj
def quickstart(context, non_interactive):
click.echo(fmt.title("Interactive platform configuration")) click.echo(fmt.title("Interactive platform configuration"))
config = interactive_config.update(root, interactive=(not non_interactive)) config = interactive_config.update(context.root, interactive=(not non_interactive))
if config["ACTIVATE_HTTPS"] and not config["WEB_PROXY"]: if config["ACTIVATE_HTTPS"] and not config["WEB_PROXY"]:
fmt.echo_alert( fmt.echo_alert(
"Potentially invalid configuration: ACTIVATE_HTTPS=true WEB_PROXY=false\n" "Potentially invalid configuration: ACTIVATE_HTTPS=true WEB_PROXY=false\n"
@ -28,21 +27,21 @@ def quickstart(root, non_interactive):
" more information." " more information."
) )
click.echo(fmt.title("Updating the current environment")) click.echo(fmt.title("Updating the current environment"))
tutor_env.save(root, config) tutor_env.save(context, config)
click.echo(fmt.title("Starting the platform")) click.echo(fmt.title("Starting the platform"))
start.callback(root) start.callback()
click.echo(fmt.title("Database creation and migrations")) click.echo(fmt.title("Database creation and migrations"))
init.callback(root) init.callback()
@click.command(help="Run all configured Open edX services") @click.command(help="Run all configured Open edX services")
@opts.root @click.pass_obj
def start(root): def start(context):
# Create namespace # Create namespace
utils.kubectl( utils.kubectl(
"apply", "apply",
"--kustomize", "--kustomize",
tutor_env.pathjoin(root), tutor_env.pathjoin(context.root),
"--wait", "--wait",
"--selector", "--selector",
"app.kubernetes.io/component=namespace", "app.kubernetes.io/component=namespace",
@ -51,29 +50,29 @@ def start(root):
utils.kubectl( utils.kubectl(
"apply", "apply",
"--kustomize", "--kustomize",
tutor_env.pathjoin(root), tutor_env.pathjoin(context.root),
"--wait", "--wait",
"--selector", "--selector",
"app.kubernetes.io/component=volume", "app.kubernetes.io/component=volume",
) )
# Create everything else # Create everything else
utils.kubectl("apply", "--kustomize", tutor_env.pathjoin(root)) utils.kubectl("apply", "--kustomize", tutor_env.pathjoin(context.root))
@click.command(help="Stop a running platform") @click.command(help="Stop a running platform")
@opts.root @click.pass_obj
def stop(root): def stop(context):
config = tutor_config.load(root) config = tutor_config.load(context.root)
utils.kubectl( utils.kubectl(
"delete", *resource_selector(config), "deployments,services,ingress,configmaps" "delete", *resource_selector(config), "deployments,services,ingress,configmaps"
) )
@click.command(help="Reboot an existing platform") @click.command(help="Reboot an existing platform")
@opts.root @click.pass_obj
def reboot(root): def reboot(context):
stop.callback(root) stop.callback()
start.callback(root) start.callback()
def resource_selector(config, *selectors): def resource_selector(config, *selectors):
@ -87,24 +86,28 @@ def resource_selector(config, *selectors):
@click.command(help="Completely delete an existing platform") @click.command(help="Completely delete an existing platform")
@opts.root
@click.option("-y", "--yes", is_flag=True, help="Do not ask for confirmation") @click.option("-y", "--yes", is_flag=True, help="Do not ask for confirmation")
def delete(root, yes): @click.pass_obj
def delete(context, yes):
if not yes: if not yes:
click.confirm( click.confirm(
"Are you sure you want to delete the platform? All data will be removed.", "Are you sure you want to delete the platform? All data will be removed.",
abort=True, abort=True,
) )
utils.kubectl( utils.kubectl(
"delete", "-k", tutor_env.pathjoin(root), "--ignore-not-found=true", "--wait" "delete",
"-k",
tutor_env.pathjoin(context.root),
"--ignore-not-found=true",
"--wait",
) )
@click.command(help="Initialise all applications") @click.command(help="Initialise all applications")
@opts.root @click.pass_obj
def init(root): def init(context):
config = tutor_config.load(root) config = tutor_config.load(context.root)
runner = K8sScriptRunner(root, config) runner = K8sScriptRunner(context.root, config)
for service in ["mysql", "elasticsearch", "mongodb"]: for service in ["mysql", "elasticsearch", "mongodb"]:
if runner.is_activated(service): if runner.is_activated(service):
wait_for_pod_ready(config, service) wait_for_pod_ready(config, service)
@ -112,7 +115,6 @@ def init(root):
@click.command(help="Create an Open edX user and interactively set their password") @click.command(help="Create an Open edX user and interactively set their password")
@opts.root
@click.option("--superuser", is_flag=True, help="Make superuser") @click.option("--superuser", is_flag=True, help="Make superuser")
@click.option("--staff", is_flag=True, help="Make staff user") @click.option("--staff", is_flag=True, help="Make staff user")
@click.option( @click.option(
@ -122,9 +124,10 @@ def init(root):
) )
@click.argument("name") @click.argument("name")
@click.argument("email") @click.argument("email")
def createuser(root, superuser, staff, password, name, email): @click.pass_obj
config = tutor_config.load(root) def createuser(context, superuser, staff, password, name, email):
runner = K8sScriptRunner(root, config) config = tutor_config.load(context.root)
runner = K8sScriptRunner(context.root, config)
runner.check_service_is_activated("lms") runner.check_service_is_activated("lms")
command = scripts.create_user_command( command = scripts.create_user_command(
superuser, staff, name, email, password=password superuser, staff, name, email, password=password
@ -133,31 +136,31 @@ def createuser(root, superuser, staff, password, name, email):
@click.command(help="Import the demo course") @click.command(help="Import the demo course")
@opts.root @click.pass_obj
def importdemocourse(root): def importdemocourse(context):
fmt.echo_info("Importing demo course") fmt.echo_info("Importing demo course")
config = tutor_config.load(root) config = tutor_config.load(context.root)
runner = K8sScriptRunner(root, config) runner = K8sScriptRunner(context.root, config)
scripts.import_demo_course(runner) scripts.import_demo_course(runner)
@click.command(name="exec", help="Execute a command in a pod of the given application") @click.command(name="exec", help="Execute a command in a pod of the given application")
@opts.root
@click.argument("service") @click.argument("service")
@click.argument("command") @click.argument("command")
def exec_command(root, service, command): @click.pass_obj
config = tutor_config.load(root) def exec_command(context, service, command):
config = tutor_config.load(context.root)
kubectl_exec(config, service, command, attach=True) kubectl_exec(config, service, command, attach=True)
@click.command(help="View output from containers") @click.command(help="View output from containers")
@opts.root
@click.option("-c", "--container", help="Print the logs of this specific container") @click.option("-c", "--container", help="Print the logs of this specific container")
@click.option("-f", "--follow", is_flag=True, help="Follow log output") @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.option("--tail", type=int, help="Number of lines to show from each container")
@click.argument("service") @click.argument("service")
def logs(root, container, follow, tail, service): @click.pass_obj
config = tutor_config.load(root) def logs(context, container, follow, tail, service):
config = tutor_config.load(context.root)
command = ["logs"] command = ["logs"]
selectors = ["app.kubernetes.io/name=" + service] if service else [] selectors = ["app.kubernetes.io/name=" + service] if service else []

View File

@ -6,7 +6,6 @@ from .. import config as tutor_config
from .. import env as tutor_env from .. import env as tutor_env
from .. import fmt from .. import fmt
from .. import interactive as interactive_config from .. import interactive as interactive_config
from .. import opts
from .. import scripts from .. import scripts
from .. import utils from .. import utils
@ -20,48 +19,48 @@ def local():
@click.command(help="Configure and run Open edX from scratch") @click.command(help="Configure and run Open edX from scratch")
@opts.root
@click.option("-I", "--non-interactive", is_flag=True, help="Run non-interactively") @click.option("-I", "--non-interactive", is_flag=True, help="Run non-interactively")
@click.option( @click.option(
"-p", "--pullimages", "pullimages_", is_flag=True, help="Update docker images" "-p", "--pullimages", "pullimages_", is_flag=True, help="Update docker images"
) )
def quickstart(root, non_interactive, pullimages_): @click.pass_obj
def quickstart(context, non_interactive, pullimages_):
click.echo(fmt.title("Interactive platform configuration")) click.echo(fmt.title("Interactive platform configuration"))
config = interactive_config.update(root, interactive=(not non_interactive)) config = interactive_config.update(context.root, interactive=(not non_interactive))
click.echo(fmt.title("Updating the current environment")) click.echo(fmt.title("Updating the current environment"))
tutor_env.save(root, config) tutor_env.save(context.root, config)
click.echo(fmt.title("Stopping any existing platform")) click.echo(fmt.title("Stopping any existing platform"))
stop.callback(root, []) stop.callback([])
click.echo(fmt.title("HTTPS certificates generation")) click.echo(fmt.title("HTTPS certificates generation"))
https_create.callback(root) https_create.callback()
if pullimages_: if pullimages_:
click.echo(fmt.title("Docker image updates")) click.echo(fmt.title("Docker image updates"))
pullimages.callback(root) pullimages.callback()
click.echo(fmt.title("Starting the platform in detached mode")) click.echo(fmt.title("Starting the platform in detached mode"))
start.callback(root, True, []) start.callback(True, [])
click.echo(fmt.title("Database creation and migrations")) click.echo(fmt.title("Database creation and migrations"))
init.callback(root) init.callback()
echo_platform_info(config) echo_platform_info(config)
@click.command(help="Update docker images") @click.command(help="Update docker images")
@opts.root @click.pass_obj
def pullimages(root): def pullimages(context):
config = tutor_config.load(root) config = tutor_config.load(context.root)
docker_compose(root, config, "pull") docker_compose(context.root, config, "pull")
@click.command(help="Run all or a selection of configured Open edX services") @click.command(help="Run all or a selection of configured Open edX services")
@opts.root
@click.option("-d", "--detach", is_flag=True, help="Start in daemon mode") @click.option("-d", "--detach", is_flag=True, help="Start in daemon mode")
@click.argument("services", metavar="service", nargs=-1) @click.argument("services", metavar="service", nargs=-1)
def start(root, detach, services): @click.pass_obj
def start(context, detach, services):
command = ["up", "--remove-orphans"] command = ["up", "--remove-orphans"]
if detach: if detach:
command.append("-d") command.append("-d")
config = tutor_config.load(root) config = tutor_config.load(context.root)
docker_compose(root, config, *command, *services) docker_compose(context.root, config, *command, *services)
def echo_platform_info(config): def echo_platform_info(config):
@ -82,23 +81,22 @@ def echo_platform_info(config):
@click.command(help="Stop a running platform") @click.command(help="Stop a running platform")
@opts.root
@click.argument("services", metavar="service", nargs=-1) @click.argument("services", metavar="service", nargs=-1)
def stop(root, services): @click.pass_obj
config = tutor_config.load(root) def stop(context, services):
docker_compose(root, config, "rm", "--stop", "--force", *services) config = tutor_config.load(context.root)
docker_compose(context.root, config, "rm", "--stop", "--force", *services)
@click.command( @click.command(
short_help="Reboot an existing platform", 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", help="This is more than just a restart: with reboot, the platform is fully stopped before being restarted again",
) )
@opts.root
@click.option("-d", "--detach", is_flag=True, help="Start in daemon mode") @click.option("-d", "--detach", is_flag=True, help="Start in daemon mode")
@click.argument("services", metavar="service", nargs=-1) @click.argument("services", metavar="service", nargs=-1)
def reboot(root, detach, services): def reboot(detach, services):
stop.callback(root, services) stop.callback(services)
start.callback(root, detach, services) start.callback(detach, services)
@click.command( @click.command(
@ -108,10 +106,10 @@ restart all services. Note that this performs a 'docker-compose restart', so new
may not be taken into account. It is useful for reloading settings, for instance. To may not be taken into account. It is useful for reloading settings, for instance. To
fully stop the platform, use the 'reboot' command.""", fully stop the platform, use the 'reboot' command.""",
) )
@opts.root
@click.argument("service") @click.argument("service")
def restart(root, service): @click.pass_obj
config = tutor_config.load(root) def restart(context, service):
config = tutor_config.load(context.root)
command = ["restart"] command = ["restart"]
if service == "openedx": if service == "openedx":
if config["ACTIVATE_LMS"]: if config["ACTIVATE_LMS"]:
@ -120,19 +118,19 @@ def restart(root, service):
command += ["cms", "cms_worker"] command += ["cms", "cms_worker"]
elif service != "all": elif service != "all":
command += [service] command += [service]
docker_compose(root, config, *command) docker_compose(context.root, config, *command)
@click.command( @click.command(
help="Run a command in one of the containers", help="Run a command in one of the containers",
context_settings={"ignore_unknown_options": True}, context_settings={"ignore_unknown_options": True},
) )
@opts.root
@click.option("--entrypoint", help="Override the entrypoint of the image") @click.option("--entrypoint", help="Override the entrypoint of the image")
@click.argument("service") @click.argument("service")
@click.argument("command", default=None, required=False) @click.argument("command", default=None, required=False)
@click.argument("args", nargs=-1) @click.argument("args", nargs=-1)
def run(root, entrypoint, service, command, args): @click.pass_obj
def run(context, entrypoint, service, command, args):
run_command = ["run", "--rm"] run_command = ["run", "--rm"]
if entrypoint: if entrypoint:
run_command += ["--entrypoint", entrypoint] run_command += ["--entrypoint", entrypoint]
@ -141,31 +139,31 @@ def run(root, entrypoint, service, command, args):
run_command.append(command) run_command.append(command)
if args: if args:
run_command += args run_command += args
config = tutor_config.load(root) config = tutor_config.load(context.root)
docker_compose(root, config, *run_command) docker_compose(context.root, config, *run_command)
@click.command( @click.command(
help="Exec a command in a running container", help="Exec a command in a running container",
context_settings={"ignore_unknown_options": True}, context_settings={"ignore_unknown_options": True},
) )
@opts.root
@click.argument("service") @click.argument("service")
@click.argument("command") @click.argument("command")
@click.argument("args", nargs=-1) @click.argument("args", nargs=-1)
def execute(root, service, command, args): @click.pass_obj
def execute(context, service, command, args):
exec_command = ["exec", service, command] exec_command = ["exec", service, command]
if args: if args:
exec_command += args exec_command += args
config = tutor_config.load(root) config = tutor_config.load(context.root)
docker_compose(root, config, *exec_command) docker_compose(context.root, config, *exec_command)
@click.command(help="Initialise all applications") @click.command(help="Initialise all applications")
@opts.root @click.pass_obj
def init(root): def init(context):
config = tutor_config.load(root) config = tutor_config.load(context.root)
runner = ScriptRunner(root, config) runner = ScriptRunner(context.root, config)
scripts.initialise(runner) scripts.initialise(runner)
@ -179,12 +177,12 @@ that is convenient when developing new plugins. Ex:
tutor local hook discovery discovery hooks discovery init""", tutor local hook discovery discovery hooks discovery init""",
name="hook", name="hook",
) )
@opts.root
@click.argument("service") @click.argument("service")
@click.argument("path", nargs=-1) @click.argument("path", nargs=-1)
def run_hook(root, service, path): @click.pass_obj
config = tutor_config.load(root) def run_hook(context, service, path):
runner = ScriptRunner(root, config) config = tutor_config.load(context.root)
runner = ScriptRunner(context.root, config)
fmt.echo_info( fmt.echo_info(
"Running '{}' hook in '{}' container...".format(".".join(path), service) "Running '{}' hook in '{}' container...".format(".".join(path), service)
) )
@ -197,8 +195,8 @@ def https():
@click.command(help="Create https certificates", name="create") @click.command(help="Create https certificates", name="create")
@opts.root @click.pass_obj
def https_create(root): def https_create(context):
""" """
Note: there are a couple issues with https certificate generation. Note: there are a couple issues with https certificate generation.
1. Certificates are generated and renewed by using port 80, which is not necessarily open. 1. Certificates are generated and renewed by using port 80, which is not necessarily open.
@ -206,8 +204,8 @@ def https_create(root):
b. It may be occupied by an external web server b. It may be occupied by an external web server
2. On certificate renewal, nginx is not reloaded 2. On certificate renewal, nginx is not reloaded
""" """
config = tutor_config.load(root) config = tutor_config.load(context.root)
runner = ScriptRunner(root, config) runner = ScriptRunner(context.root, config)
if not config["ACTIVATE_HTTPS"]: if not config["ACTIVATE_HTTPS"]:
fmt.echo_info("HTTPS is not activated: certificate generation skipped") fmt.echo_info("HTTPS is not activated: certificate generation skipped")
return return
@ -230,7 +228,7 @@ See the official certbot documentation for your platform: https://certbot.eff.or
utils.docker_run( utils.docker_run(
"--volume", "--volume",
"{}:/etc/letsencrypt/".format(tutor_env.data_path(root, "letsencrypt")), "{}:/etc/letsencrypt/".format(tutor_env.data_path(context.root, "letsencrypt")),
"-p", "-p",
"80:80", "80:80",
"--entrypoint=sh", "--entrypoint=sh",
@ -242,9 +240,9 @@ See the official certbot documentation for your platform: https://certbot.eff.or
@click.command(help="Renew https certificates", name="renew") @click.command(help="Renew https certificates", name="renew")
@opts.root @click.pass_obj
def https_renew(root): def https_renew(context):
config = tutor_config.load(root) config = tutor_config.load(context.root)
if not config["ACTIVATE_HTTPS"]: if not config["ACTIVATE_HTTPS"]:
fmt.echo_info("HTTPS is not activated: certificate renewal skipped") fmt.echo_info("HTTPS is not activated: certificate renewal skipped")
return return
@ -261,7 +259,7 @@ See the official certbot documentation for your platform: https://certbot.eff.or
return return
docker_run = [ docker_run = [
"--volume", "--volume",
"{}:/etc/letsencrypt/".format(tutor_env.data_path(root, "letsencrypt")), "{}:/etc/letsencrypt/".format(tutor_env.data_path(context.root, "letsencrypt")),
"-p", "-p",
"80:80", "80:80",
"certbot/certbot:latest", "certbot/certbot:latest",
@ -271,23 +269,22 @@ See the official certbot documentation for your platform: https://certbot.eff.or
@click.command(help="View output from containers") @click.command(help="View output from containers")
@opts.root
@click.option("-f", "--follow", is_flag=True, help="Follow log output") @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.option("--tail", type=int, help="Number of lines to show from each container")
@click.argument("service", nargs=-1) @click.argument("service", nargs=-1)
def logs(root, follow, tail, service): @click.pass_obj
def logs(context, follow, tail, service):
command = ["logs"] command = ["logs"]
if follow: if follow:
command += ["--follow"] command += ["--follow"]
if tail is not None: if tail is not None:
command += ["--tail", str(tail)] command += ["--tail", str(tail)]
command += service command += service
config = tutor_config.load(root) config = tutor_config.load(context.root)
docker_compose(root, config, *command) docker_compose(context.root, config, *command)
@click.command(help="Create an Open edX user and interactively set their password") @click.command(help="Create an Open edX user and interactively set their password")
@opts.root
@click.option("--superuser", is_flag=True, help="Make superuser") @click.option("--superuser", is_flag=True, help="Make superuser")
@click.option("--staff", is_flag=True, help="Make staff user") @click.option("--staff", is_flag=True, help="Make staff user")
@click.option( @click.option(
@ -297,9 +294,10 @@ def logs(root, follow, tail, service):
) )
@click.argument("name") @click.argument("name")
@click.argument("email") @click.argument("email")
def createuser(root, superuser, staff, password, name, email): @click.pass_obj
config = tutor_config.load(root) def createuser(context, superuser, staff, password, name, email):
runner = ScriptRunner(root, config) config = tutor_config.load(context.root)
runner = ScriptRunner(context.root, config)
runner.check_service_is_activated("lms") runner.check_service_is_activated("lms")
command = scripts.create_user_command( command = scripts.create_user_command(
superuser, staff, name, email, password=password superuser, staff, name, email, password=password
@ -308,10 +306,10 @@ def createuser(root, superuser, staff, password, name, email):
@click.command(help="Import the demo course") @click.command(help="Import the demo course")
@opts.root @click.pass_obj
def importdemocourse(root): def importdemocourse(context):
config = tutor_config.load(root) config = tutor_config.load(context.root)
runner = ScriptRunner(root, config) runner = ScriptRunner(context.root, config)
fmt.echo_info("Importing demo course") fmt.echo_info("Importing demo course")
scripts.import_demo_course(runner) scripts.import_demo_course(runner)

View File

@ -5,7 +5,6 @@ import click
from .. import config as tutor_config from .. import config as tutor_config
from .. import env as tutor_env from .. import env as tutor_env
from .. import fmt from .. import fmt
from .. import opts
from .. import plugins from .. import plugins
@ -22,47 +21,57 @@ def plugins_command():
@click.command(name="list", help="List installed plugins") @click.command(name="list", help="List installed plugins")
@opts.root @click.pass_obj
def list_command(root): def list_command(context):
config = tutor_config.load_user(root) config = tutor_config.load_user(context.root)
for name, _ in plugins.iter_installed(): for name, _ in plugins.iter_installed():
status = "" if plugins.is_enabled(config, name) else " (disabled)" status = "" if plugins.is_enabled(config, name) else " (disabled)"
print("{plugin}{status}".format(plugin=name, status=status)) print("{plugin}{status}".format(plugin=name, status=status))
@click.command(help="Enable a plugin") @click.command(help="Enable a plugin")
@opts.root
@click.argument("plugin_names", metavar="plugin", nargs=-1) @click.argument("plugin_names", metavar="plugin", nargs=-1)
def enable(root, plugin_names): @click.pass_obj
config = tutor_config.load_user(root) def enable(context, plugin_names):
config = tutor_config.load_user(context.root)
for plugin in plugin_names: for plugin in plugin_names:
plugins.enable(config, plugin) plugins.enable(config, plugin)
fmt.echo_info("Plugin {} enabled".format(plugin)) fmt.echo_info("Plugin {} enabled".format(plugin))
tutor_config.save(root, config) tutor_config.save(context.root, config)
fmt.echo_info( fmt.echo_info(
"You should now re-generate your environment with `tutor config save`." "You should now re-generate your environment with `tutor config save`."
) )
@click.command(help="Disable a plugin") @click.command(help="Disable a plugin")
@opts.root
@click.argument("plugin_names", metavar="plugin", nargs=-1) @click.argument("plugin_names", metavar="plugin", nargs=-1)
def disable(root, plugin_names): @click.pass_obj
config = tutor_config.load_user(root) def disable(context, plugin_names):
config = tutor_config.load_user(context.root)
for plugin in plugin_names: for plugin in plugin_names:
plugins.disable(config, plugin) plugins.disable(config, plugin)
plugin_dir = tutor_env.pathjoin(root, "plugins", plugin) plugin_dir = tutor_env.pathjoin(context.root, "plugins", plugin)
if os.path.exists(plugin_dir): if os.path.exists(plugin_dir):
shutil.rmtree(plugin_dir) shutil.rmtree(plugin_dir)
fmt.echo_info("Plugin {} disabled".format(plugin)) fmt.echo_info("Plugin {} disabled".format(plugin))
tutor_config.save(root, config) tutor_config.save(context.root, config)
fmt.echo_info( fmt.echo_info(
"You should now re-generate your environment with `tutor config save`." "You should now re-generate your environment with `tutor config save`."
) )
def iter_extra_plugin_commands():
"""
TODO document this. Merge with plugins.iter_commands? It's good to keepo
click-related stuff outside of the plugins module.
"""
for plugin_name, command in plugins.iter_commands():
command.name = plugin_name
yield command
plugins_command.add_command(list_command) plugins_command.add_command(list_command)
plugins_command.add_command(enable) plugins_command.add_command(enable)
plugins_command.add_command(disable) plugins_command.add_command(disable)

View File

@ -11,7 +11,6 @@ import click
# Note: it is important that this module does not depend on config, such that # Note: it is important that this module does not depend on config, such that
# the web ui can be launched even where there is no configuration. # the web ui can be launched even where there is no configuration.
from .. import fmt from .. import fmt
from .. import opts
from .. import env as tutor_env from .. import env as tutor_env
from .. import serialize from .. import serialize
@ -24,7 +23,6 @@ def webui():
@click.command(help="Start the web UI") @click.command(help="Start the web UI")
@opts.root
@click.option( @click.option(
"-p", "-p",
"--port", "--port",
@ -36,15 +34,16 @@ def webui():
@click.option( @click.option(
"-h", "--host", default="0.0.0.0", show_default=True, help="Host address to listen" "-h", "--host", default="0.0.0.0", show_default=True, help="Host address to listen"
) )
def start(root, port, host): @click.pass_obj
check_gotty_binary(root) def start(context, port, host):
check_gotty_binary(context.root)
fmt.echo_info("Access the Tutor web UI at http://{}:{}".format(host, port)) fmt.echo_info("Access the Tutor web UI at http://{}:{}".format(host, port))
while True: while True:
config = load_config(root) config = load_config(context.root)
user = config["user"] user = config["user"]
password = config["password"] password = config["password"]
command = [ command = [
gotty_path(root), gotty_path(context.root),
"--permit-write", "--permit-write",
"--address", "--address",
host, host,
@ -66,7 +65,7 @@ def start(root, port, host):
try: try:
p.wait(timeout=2) p.wait(timeout=2)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
new_config = load_config(root) new_config = load_config(context.root)
if new_config != config: if new_config != config:
click.echo( click.echo(
"WARNING configuration changed. Tutor web UI is now going to restart. Reload this page to continue." "WARNING configuration changed. Tutor web UI is now going to restart. Reload this page to continue."
@ -77,7 +76,6 @@ def start(root, port, host):
@click.command(help="Configure authentication") @click.command(help="Configure authentication")
@opts.root
@click.option("-u", "--user", prompt="User name", help="Authentication user name") @click.option("-u", "--user", prompt="User name", help="Authentication user name")
@click.option( @click.option(
"-p", "-p",
@ -87,12 +85,13 @@ def start(root, port, host):
confirmation_prompt=True, confirmation_prompt=True,
help="Authentication password", help="Authentication password",
) )
def configure(root, user, password): @click.pass_obj
save_config(root, {"user": user, "password": password}) def configure(context, user, password):
save_config(context.root, {"user": user, "password": password})
fmt.echo_info( fmt.echo_info(
"The web UI configuration has been updated. " "The web UI configuration has been updated. "
"If at any point you wish to reset your username and password, " "If at any point you wish to reset your username and password, "
"just delete the following file:\n\n {}".format(config_path(root)) "just delete the following file:\n\n {}".format(config_path(context.root))
) )

View File

@ -185,7 +185,9 @@ def check_existing_config(root):
""" """
if not os.path.exists(config_path(root)): if not os.path.exists(config_path(root)):
raise exceptions.TutorError( raise exceptions.TutorError(
"Project root does not exist. Make sure to generate the initial configuration with `tutor config save --interactive` or `tutor config quickstart` prior to running other commands." "Project root does not exist. Make sure to generate the initial "
"configuration with `tutor config save --interactive` or `tutor config "
"quickstart` prior to running other commands."
) )
env.check_is_up_to_date(root) env.check_is_up_to_date(root)

View File

@ -1,42 +0,0 @@
import appdirs
import click
from . import serialize
root = click.option(
"-r",
"--root",
envvar="TUTOR_ROOT",
default=appdirs.user_data_dir(appname="tutor"),
show_default=True,
type=click.Path(resolve_path=True),
help="Root project directory (environment variable: TUTOR_ROOT)",
)
edx_platform_path = click.option(
"-P",
"--edx-platform-path",
envvar="TUTOR_EDX_PLATFORM_PATH",
type=click.Path(exists=True, dir_okay=True, resolve_path=True),
help="Mount a local edx-platform from the host (environment variable: TUTOR_EDX_PLATFORM_PATH)",
)
edx_platform_development_settings = click.option(
"-S",
"--edx-platform-settings",
envvar="TUTOR_EDX_PLATFORM_SETTINGS",
default="tutor.development",
help="Mount a local edx-platform from the host (environment variable: TUTOR_EDX_PLATFORM_PATH)",
)
class YamlParamType(click.ParamType):
name = "yaml"
def convert(self, value, param, ctx):
try:
k, v = value.split("=")
except ValueError:
self.fail("'{}' is not of the form 'key=value'.".format(value), param, ctx)
return k, serialize.parse(v)

View File

@ -156,3 +156,15 @@ def iter_hooks(config, hook_name):
def iter_templates(config): def iter_templates(config):
yield from Plugins.instance(config).iter_templates() yield from Plugins.instance(config).iter_templates()
def iter_commands():
"""
TODO doesn't this slow down the cli? (we need to import all plugins)
Also, do we really need the `config` argument? Do we want to make it possible for disabled plugins to be loaded?
TODO document this
"""
for plugin_name, plugin in iter_installed():
command = getattr(plugin, "command", None)
if command:
yield plugin_name, command