6
0
mirror of https://github.com/ChristianLight/tutor.git synced 2025-02-15 07:01:39 +00:00

Progress on the plugins/k8s front

This commit introduces many changes:
- a fully functional minio plugin for local installation
- an almost-functional native k8s deployment
- a new way to process configuration, better suited to plugins

There are still many things to do:
- get rid of all the TODOs
- get a fully functional minio plugin for k8s
- add documentation for pluginso
- ...
This commit is contained in:
Régis Behmo 2019-06-05 15:43:51 +02:00
parent f5c225231f
commit 6a68c4cc20
39 changed files with 572 additions and 670 deletions

View File

@ -1,3 +1,2 @@
"FILE_UPLOAD_STORAGE_BUCKET_NAME": "{{ MINIO_FILE_UPLOAD_BUCKET_NAME }}",
"COURSE_IMPORT_EXPORT_BUCKET": "{{ MINIO_COURSE_IMPORT_EXPORT_BUCKET }}",
"AWS_S3_CUSTOM_DOMAIN": "{{ MINIO_HOST }}"
"COURSE_IMPORT_EXPORT_BUCKET": "{{ MINIO_COURSE_IMPORT_EXPORT_BUCKET }}"

View File

@ -0,0 +1,28 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: minio
labels:
app.kubernetes.io/name: minio
spec:
selector:
matchLabels:
app.kubernetes.io/name: minio
template:
metadata:
labels:
app.kubernetes.io/name: minio
spec:
containers:
- name: minio
image: {{ MINIO_DOCKER_REGISTRY }}{{ MINIO_DOCKER_IMAGE }}
ports:
- containerPort: 9000
volumeMounts:
- mountPath: /data
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: minio

View File

@ -1,22 +0,0 @@
---
apiVersion: batch/v1
kind: Job
metadata:
name: minio-client/init
labels:
app.kubernetes.io/component: script
app.kubernetes.io/name: minio-client/init
spec:
template:
metadata:
labels:
app.kubernetes.io/name: minio-client/init
spec:
restartPolicy: Never
containers:
- name: minio-client
image: {{ MINIO_DOCKER_REGISTRY }}{{ MINIO_DOCKER_IMAGE_CLIENT }}
command: ["/bin/sh", "-e", "-c"]
args:
- |
{{ include_plugin_script("minio", "minio-client", "init")|indent(12) }}

View File

@ -0,0 +1,2 @@
"FILE_UPLOAD_STORAGE_BUCKET_NAME": "{{ MINIO_FILE_UPLOAD_BUCKET_NAME }}",
"COURSE_IMPORT_EXPORT_BUCKET": "{{ MINIO_COURSE_IMPORT_EXPORT_BUCKET }}"

View File

@ -0,0 +1 @@
"{{ MINIO_HOST }}"

View File

@ -3,18 +3,15 @@ minio:
image: {{ MINIO_DOCKER_REGISTRY }}{{ MINIO_DOCKER_IMAGE_SERVER }}
volumes:
- ../../data/minio:/data
ports:
- "9000:9000"
environment:
MINIO_ACCESS_KEY: "{{ OPENEDX_AWS_ACCESS_KEY }}"
MINIO_SECRET_KEY: "{{ OPENEDX_AWS_SECRET_ACCESS_KEY }}"
command: server /data
command: server --address ":9000" /data
restart: unless-stopped
minio-client:
image: {{ MINIO_DOCKER_REGISTRY }}{{ MINIO_DOCKER_IMAGE_CLIENT }}
entrypoint: sh
command: "mc ls"
restart: "no"
entrypoint: sh
depends_on:
- minio

View File

@ -1,2 +1,2 @@
"AWS_STORAGE_BUCKET_NAME": "{{ MINIO_BUCKET_NAME }}",
"AWS_S3_CUSTOM_DOMAIN": "{{ MINIO_HOST }}"
"AWS_S3_CUSTOM_DOMAIN": ""

View File

@ -0,0 +1,16 @@
AWS_S3_HOST = "{{ MINIO_HOST }}"
AWS_S3_USE_SSL = {{ "True" if ACTIVATE_HTTPS else "False" }}
AWS_S3_SECURE_URLS = {{ "True" if ACTIVATE_HTTPS else "False" }}
AWS_S3_CALLING_FORMAT = "boto.s3.connection.OrdinaryCallingFormat"
# Configuring boto is required for ora2 because ora2 does not read
# host/port/ssl settings from django. Hence this hack.
# http://docs.pythonboto.org/en/latest/boto_config_tut.html
import os
os.environ["AWS_CREDENTIAL_FILE"] = "/tmp/boto.cfg"
with open("/tmp/boto.cfg", "w") as f:
f.write("""[Boto]
is_secure = {{ "True" if ACTIVATE_HTTPS else "False" }}
[s3]
host = {{ MINIO_HOST }}
calling_format = boto.s3.connection.OrdinaryCallingFormat""")

View File

@ -1 +0,0 @@
AWS_S3_USE_SSL = {{ "True" if ACTIVATE_HTTPS else "False" }}

View File

@ -0,0 +1 @@
ORA2_FILEUPLOAD_BACKEND = "s3"

View File

@ -19,22 +19,17 @@ config = {
},
}
templates = os.path.join(HERE, "templates")
scripts = {
"init": [
{
"service": "mysql-client",
"command": """mc config host add minio http://minio:9000 {{ OPENEDX_AWS_ACCESS_KEY }} {{ OPENEDX_AWS_SECRET_ACCESS_KEY }} --api s3v4
mc mb minio {{ FILE_UPLOAD_BUCKET_NAME }} {{ COURSE_IMPORT_EXPORT_BUCKET }}""",
}
]
"init": ["minio-client"]
}
def patches(*_args):
def patches():
all_patches = {}
for path in glob(os.path.join(HERE, "patches", "*.patch")):
for path in glob(os.path.join(HERE, "patches", "*")):
with open(path) as patch_file:
name = os.path.basename(path)[:-6]
name = os.path.basename(path)
content = patch_file.read()
all_patches[name] = content
return all_patches

View File

@ -0,0 +1,3 @@
# TODO we need to think long and hard whether we want to keep this init script or just set AWS_AUTO_CREATE_BUCKET=True
mc config host add minio http://minio:9000 {{ OPENEDX_AWS_ACCESS_KEY }} {{ OPENEDX_AWS_SECRET_ACCESS_KEY }} --api s3v4
mc mb --ignore-existing minio/{{ MINIO_BUCKET_NAME }} minio/{{ MINIO_FILE_UPLOAD_BUCKET_NAME }} minio/{{ MINIO_COURSE_IMPORT_EXPORT_BUCKET }}

View File

@ -32,12 +32,12 @@ class ConfigTests(unittest.TestCase):
self.assertEqual("abcd", config["MYSQL_ROOT_PASSWORD"])
@unittest.mock.patch.object(tutor_config.fmt, "echo")
def test_save_twice(self, _):
def test_update_twice(self, _):
with tempfile.TemporaryDirectory() as root:
tutor_config.save(root, silent=True)
tutor_config.update(root)
config1 = tutor_config.load_user(root)
tutor_config.save(root, silent=True)
tutor_config.update(root)
config2 = tutor_config.load_user(root)
self.assertEqual(config1, config2)
@ -53,7 +53,7 @@ class ConfigTests(unittest.TestCase):
password1 = config1["MYSQL_ROOT_PASSWORD"]
config1.pop("MYSQL_ROOT_PASSWORD")
tutor_config.save_config(root, config1)
tutor_config.save(root, config1)
mock_random_string.return_value = "efgh"
config2, _ = tutor_config.load_all(root)

View File

@ -39,7 +39,7 @@ class EnvTests(unittest.TestCase):
config = {}
tutor_config.merge(config, tutor_config.load_defaults())
config["MYSQL_ROOT_PASSWORD"] = "testpassword"
rendered = env.render_file(config, "scripts", "mysql-client", "createdatabases")
rendered = env.render_file(config, "scripts", "mysql-client", "init")
self.assertIn("testpassword", rendered)
@unittest.mock.patch.object(tutor_config.fmt, "echo")

View File

@ -1,6 +1,7 @@
import unittest
import unittest.mock
from tutor import config as tutor_config
from tutor import exceptions
from tutor import plugins
@ -37,6 +38,11 @@ class PluginsTests(unittest.TestCase):
with unittest.mock.patch.object(plugins, "is_installed", return_value=False):
self.assertRaises(exceptions.TutorError, plugins.enable, config, "plugin1")
def test_disable(self):
config = {"PLUGINS": ["plugin1", "plugin2"]}
plugins.disable(config, "plugin1")
self.assertEqual(["plugin2"], config["PLUGINS"])
def test_patches(self):
class plugin1:
patches = {"patch1": "Hello {{ ID }}"}
@ -58,33 +64,72 @@ class PluginsTests(unittest.TestCase):
self.assertEqual([], patches)
def test_configure(self):
config = {"ID": "oldid"}
config = {"ID": "id"}
defaults = {}
class plugin1:
config = {
"add": {"PARAM1": "value1", "PARAM2": "value2"},
"set": {"ID": "newid"},
"defaults": {"PARAM3": "value3"},
"set": {"PARAM3": "value3"},
"defaults": {"PARAM4": "value4"},
}
with unittest.mock.patch.object(
plugins, "iter_enabled", return_value=[("plugin1", plugin1)]
):
add_config, set_config, defaults_config = plugins.load_config(config)
self.assertEqual(
{"PLUGIN1_PARAM1": "value1", "PLUGIN1_PARAM2": "value2"}, add_config
)
self.assertEqual({"ID": "newid"}, set_config)
self.assertEqual({"PLUGIN1_PARAM3": "value3"}, defaults_config)
tutor_config.load_plugins(config, defaults)
self.assertEqual(
{
"ID": "id",
"PARAM3": "value3",
"PLUGIN1_PARAM1": "value1",
"PLUGIN1_PARAM2": "value2",
},
config,
)
self.assertEqual({"PLUGIN1_PARAM4": "value4"}, defaults)
def test_configure_set_does_not_override(self):
config = {"ID": "oldid"}
def test_scripts(self):
class plugin1:
scripts = {"init": [{"service": "myclient", "command": "init command"}]}
config = {"set": {"ID": "newid"}}
with unittest.mock.patch.object(
plugins, "iter_enabled", return_value=[("plugin1", plugin1)]
):
tutor_config.load_plugins(config, {})
self.assertEqual({"ID": "oldid"}, config)
def test_configure_set_random_string(self):
config = {}
class plugin1:
config = {"set": {"PARAM1": "{{ 128|random_string }}"}}
with unittest.mock.patch.object(
plugins, "iter_enabled", return_value=[("plugin1", plugin1)]
):
tutor_config.load_plugins(config, {})
self.assertEqual(128, len(config["PARAM1"]))
def test_configure_default_value_with_previous_definition(self):
config = {}
defaults = {"PARAM1": "value"}
class plugin1:
config = {"defaults": {"PARAM2": "{{ PARAM1 }}"}}
with unittest.mock.patch.object(
plugins, "iter_enabled", return_value=[("plugin1", plugin1)]
):
tutor_config.load_plugins(config, defaults)
self.assertEqual("{{ PARAM1 }}", defaults["PLUGIN1_PARAM2"])
def test_scripts(self):
class plugin1:
scripts = {"init": ["myclient"]}
with unittest.mock.patch.object(
plugins, "iter_enabled", return_value=[("plugin1", plugin1)]
):
self.assertEqual(
[("plugin1", "myclient", "init command")],
list(plugins.iter_scripts({}, "init")),
[("plugin1", "myclient")], list(plugins.iter_scripts({}, "init"))
)

View File

@ -5,14 +5,10 @@ from tutor import config as tutor_config
from tutor import scripts
class DummyRunner(scripts.BaseRunner):
exec = unittest.mock.Mock()
class ScriptsTests(unittest.TestCase):
def test_run(self):
config = tutor_config.load_defaults()
runner = DummyRunner("/tmp", config)
rendered_script = runner.render("mysql-client", "createdatabases")
runner.run("mysql-client", "createdatabases")
runner.exec.assert_called_once_with("mysql-client", rendered_script)
def test_is_activated(self):
config = {"ACTIVATE_SERVICE1": True, "ACTIVATE_SERVICE2": False}
runner = scripts.BaseRunner("/tmp", config)
self.assertTrue(runner.is_activated("service1"))
self.assertFalse(runner.is_activated("service2"))

View File

@ -1,8 +1,10 @@
import click
from .. import config
from .. import config as tutor_config
from .. import env
from .. import exceptions
from .. import fmt
from .. import interactive
from .. import opts
@ -15,14 +17,19 @@ def config_command():
pass
@click.command(name="save", help="Create and save configuration interactively")
@click.command(help="Create and save configuration interactively")
@opts.root
@click.option("-y", "--yes", "silent1", is_flag=True, help="Run non-interactively")
@click.option("--silent", "silent2", is_flag=True, hidden=True)
@opts.key_value
def save_command(root, silent1, silent2, set_):
def save(root, silent1, silent2, set_):
silent = silent1 or silent2
config.save(root, silent=silent, keyvalues=set_)
config, defaults = interactive.load_all(root, silent=silent)
if set_:
tutor_config.merge(config, dict(set_), force=True)
tutor_config.save(root, config)
tutor_config.merge(config, defaults)
env.save(root, config)
@click.command(help="Print the project root")
@ -35,13 +42,13 @@ def printroot(root):
@opts.root
@click.argument("key")
def printvalue(root, key):
local = config.load(root)
config = tutor_config.load(root)
try:
fmt.echo(local[key])
fmt.echo(config[key])
except KeyError:
raise exceptions.TutorError("Missing configuration value: {}".format(key))
config_command.add_command(save_command)
config_command.add_command(save)
config_command.add_command(printroot)
config_command.add_command(printvalue)

View File

@ -2,9 +2,8 @@ import click
from .. import config as tutor_config
from .. import env as tutor_env
# from .. import exceptions
from .. import fmt
from .. import interactive
from .. import opts
from .. import scripts
from .. import utils
@ -20,7 +19,9 @@ def k8s():
@click.option("-y", "--yes", "silent", is_flag=True, help="Run non-interactively")
def quickstart(root, silent):
click.echo(fmt.title("Interactive platform configuration"))
tutor_config.save(root, silent=silent)
config = interactive.update(root, silent=silent)
click.echo(fmt.title("Updating the current environment"))
tutor_env.save(root, config)
click.echo(fmt.title("Starting the platform"))
start.callback(root)
click.echo(fmt.title("Database creation and migrations"))
@ -49,14 +50,8 @@ def start(root):
"--selector",
"app.kubernetes.io/component=volume",
)
# Create everything else (except Job objects)
utils.kubectl(
"apply",
"--selector",
"app.kubernetes.io/component!=script",
"--kustomize",
tutor_env.pathjoin(root),
)
# Create everything else
utils.kubectl("apply", "--kustomize", tutor_env.pathjoin(root))
@click.command(help="Stop a running platform")
@ -68,7 +63,7 @@ def stop(root):
"--namespace",
config["K8S_NAMESPACE"],
"--selector=app.kubernetes.io/instance=openedx-" + config["ID"],
"deployments,services,ingress,configmaps,jobs",
"deployments,services,ingress,configmaps",
)
@ -104,7 +99,6 @@ def databases(root):
def createuser(root, superuser, staff, name, email):
config = tutor_config.load(root)
runner = K8sScriptRunner(root, config)
# TODO this is not going to work
scripts.create_user(runner, superuser, staff, name, email)
@ -124,7 +118,6 @@ def importdemocourse(root):
def indexcourses(root):
config = tutor_config.load(root)
runner = K8sScriptRunner(root, config)
# TODO this is not going to work
scripts.index_courses(runner)
@ -159,32 +152,47 @@ def logs(root, follow, tail, service):
class K8sScriptRunner(scripts.BaseRunner):
def run(self, service, script, config=None):
job_name = "{}/{}".format(service, script)
selector = (
"--selector=app.kubernetes.io/component=script,app.kubernetes.io/name="
+ job_name
def exec(self, service, command):
# Find pod in runner deployment
fmt.echo_info("Finding pod name for {} deployment...".format(service))
pod = utils.check_output(
"kubectl",
"get",
"-n",
"openedx",
"pods",
"--selector=app.kubernetes.io/name={}".format(service),
"-o=jsonpath={.items[0].metadata.name}",
)
kustomization = tutor_env.pathjoin(self.root)
# Delete any previously run jobs (completed job objects still exist)
utils.kubectl("delete", "-k", kustomization, "--wait", selector)
# utils.kubectl("delete", "-k", kustomization, "--wait", selector)
# Run job
utils.kubectl("apply", "-k", kustomization, selector)
# Wait until complete
fmt.echo_info(
"Waiting for job to complete. To view logs, run: \n\n kubectl logs -n {} -l app.kubernetes.io/name={} --follow\n".format(
self.config["K8S_NAMESPACE"], job_name
)
)
utils.kubectl(
"wait",
"exec",
"--namespace",
self.config["K8S_NAMESPACE"],
"--for=condition=complete",
"--timeout=-1s",
selector,
"job",
pod.decode(),
"--",
"sh",
"-e",
"-c",
command,
)
# # Wait until complete
# fmt.echo_info(
# "Waiting for job to complete. To view logs, run: \n\n kubectl logs -n {} -l app.kubernetes.io/name={} --follow\n".format(
# self.config["K8S_NAMESPACE"], job_name
# )
# )
# utils.kubectl(
# "wait",
# "--namespace",
# self.config["K8S_NAMESPACE"],
# "--for=condition=complete",
# "--timeout=-1s",
# selector,
# "job",
# )
# TODO check failure?

View File

@ -5,6 +5,7 @@ import click
from .. import config as tutor_config
from .. import env as tutor_env
from .. import fmt
from .. import interactive
from .. import opts
from .. import scripts
from .. import utils
@ -19,14 +20,16 @@ def local():
@click.command(help="Configure and run Open edX from scratch")
@opts.root
@click.option("-y", "--yes", "silent", is_flag=True, help="Run non-interactively")
@click.option(
"-p", "--pullimages", "pullimages_", is_flag=True, help="Update docker images"
)
@opts.root
def quickstart(silent, pullimages_, root):
def quickstart(root, silent, pullimages_):
click.echo(fmt.title("Interactive platform configuration"))
tutor_config.save(root, silent=silent)
config = interactive.update(root, silent=silent)
click.echo(fmt.title("Updating the current environment"))
tutor_env.save(root, config)
click.echo(fmt.title("Stopping any existing platform"))
stop.callback(root)
if pullimages_:
@ -111,11 +114,15 @@ def restart(root, service):
context_settings={"ignore_unknown_options": True},
)
@opts.root
@click.option("--entrypoint", help="Override the entrypoint of the image")
@click.argument("service")
@click.argument("command", default=None, required=False)
@click.argument("args", nargs=-1, required=False)
def run(root, service, command, args):
run_command = ["run", "--rm", service]
def run(root, entrypoint, service, command, args):
run_command = ["run", "--rm"]
if entrypoint:
run_command += ["--entrypoint", entrypoint]
run_command.append(service)
if command:
run_command.append(command)
if args:
@ -169,6 +176,7 @@ def https_create(root):
fmt.echo_info("HTTPS is not activated: certificate generation skipped")
return
# TODO this is not going to work anymore
script = runner.render("certbot", "create")
if config["WEB_PROXY"]:
@ -297,9 +305,7 @@ def portainer(root, port):
class ScriptRunner(scripts.BaseRunner):
def exec(self, service, command):
docker_compose(
self.root, self.config, "run", "--rm", service, "sh", "-e", "-c", command
)
docker_compose(self.root, self.config, "run", "--rm", "--entrypoint", "sh -e -c", service, command)
def docker_compose(root, config, *command):

View File

@ -1,6 +1,7 @@
import click
from .. import config as tutor_config
from .. import fmt
from .. import opts
from .. import plugins
@ -11,32 +12,43 @@ from .. import plugins
help="Manage Tutor plugins to add new features and customize your Open edX platform",
)
def plugins_command():
pass
"""
All plugin commands should work even if there is no existing config file. This is
because users might enable plugins prior to configuration or environment generation.
"""
@click.command(name="list", help="List installed plugins")
@opts.root
def list_command(root):
config = tutor_config.load(root)
config = tutor_config.load_user(root)
for name, _ in plugins.iter_installed():
status = "" if plugins.is_enabled(config, name) else " (disabled)"
print("{plugin}{status}".format(plugin=name, status=status))
@click.command(help="Enable a plugin")
@click.argument("plugin")
@opts.root
def enable(plugin, root):
config = tutor_config.load(root)
@click.argument("plugin")
def enable(root, plugin):
config = tutor_config.load_user(root)
plugins.enable(config, plugin)
tutor_config.save_config(root, config)
tutor_config.save(root, silent=True)
tutor_config.save(root, config)
fmt.echo_info(
"You should now re-generate your environment with `tutor config save`."
)
@click.command(help="Disable a plugin")
@opts.root
def disable(root):
pass
@click.argument("plugin")
def disable(root, plugin):
config = tutor_config.load_user(root)
plugins.disable(config, plugin)
tutor_config.save(root, config)
fmt.echo_info(
"You should now re-generate your environment with `tutor config save`."
)
plugins_command.add_command(list_command)

View File

@ -1,8 +1,5 @@
import json
import os
import sys
import click
from . import exceptions
from . import env
@ -10,103 +7,59 @@ from . import fmt
from . import plugins
from . import serialize
from . import utils
from .__about__ import __version__
def save(root, silent=False, keyvalues=None):
keyvalues = keyvalues or []
def update(root):
"""
Load and save the configuration.
"""
config, defaults = load_all(root)
for k, v in keyvalues:
config[k] = v
if not silent:
load_interactive(config, defaults)
save_config(root, config)
merge(config, defaults)
save_env(root, config)
def load(root):
"""
Load configuration, and generate it interactively if the file does not
exist.
"""
defaults = load_defaults()
config = load_current(root, defaults)
should_update_env = False
if not os.path.exists(config_path(root)):
load_interactive(config, defaults)
should_update_env = True
save_config(root, config)
if not env.is_up_to_date(root):
should_update_env = True
pre_upgrade_announcement(root)
if should_update_env:
save_env(root, config)
save(root, config)
merge(config, defaults)
return config
def merge(config, defaults):
def load(root):
"""
Merge default values with user configuration.
Load full configuration. This will raise an exception if there is no current
configuration in the project root.
"""
for key, value in defaults.items():
if key not in config:
if isinstance(value, str):
config[key] = env.render_str(config, value)
else:
config[key] = value
def pre_upgrade_announcement(root):
"""
Inform the user that the current environment is not up-to-date. Crash if running in
non-interactive mode.
"""
fmt.echo_alert(
"The current environment stored at {} is not up-to-date: it is at "
"v{} while the 'tutor' binary is at v{}.".format(
env.base_dir(root), env.version(root), __version__
)
)
if os.isatty(sys.stdin.fileno()):
# Interactive mode: ask the user permission to proceed
click.confirm(
fmt.question(
# every patch you take, every change you make, I'll be watching you
"Would you like to upgrade the environment? If you do, any change you"
" might have made will be overwritten."
),
default=True,
abort=True,
)
else:
# Non-interactive mode with no authorization: abort
raise exceptions.TutorError(
"Running in non-interactive mode, the environment will not be upgraded"
" automatically. To upgrade the environment manually, run:\n"
"\n"
" tutor config save -y"
)
check_existing_config(root)
config, defaults = load_all(root)
merge(config, defaults)
return config
def load_all(root):
"""
Return:
current (dict): params currently saved in config.yml
defaults (dict): default values of params which might be missing from the
current config
"""
defaults = load_defaults()
current = load_current(root, defaults)
return current, defaults
def merge(config, defaults, force=False):
"""
Merge default values with user configuration and perform rendering of "{{...}}"
values.
"""
for key, value in defaults.items():
if force or key not in config:
config[key] = env.render_unknown(config, value)
def load_defaults():
return serialize.load(env.read("config.yml"))
def load_current(root, defaults):
"""
Note: this modifies the defaults. TODO this is not that great.
Load the configuration currently stored on disk.
Note: this modifies the defaults with the plugin default values.
"""
convert_json2yml(root)
config = load_user(root)
@ -156,16 +109,28 @@ def load_required(config, defaults):
"ID",
]:
if key not in config:
config[key] = env.render_str(config, defaults[key])
config[key] = env.render_unknown(config, defaults[key])
def load_plugins(config, defaults):
add_config, set_config, defaults_config = plugins.load_config(config)
merge(config, add_config)
# TODO this might have unexpected consequences if plugins have conflicting configurations. Maybe we should print warning messages.
merge(config, set_config)
# TODO this modifies defaults, which is ugly.
merge(defaults, defaults_config)
"""
Add, override and set new defaults from plugins.
"""
for plugin_name, plugin in plugins.iter_enabled(config):
plugin_prefix = plugin_name.upper() + "_"
plugin_config = plugins.get_callable_attr(plugin, "config")
# Add new config key/values
for key, value in plugin_config.get("add", {}).items():
config[plugin_prefix + key] = env.render_unknown(config, value)
# Set existing config key/values: here, we do not override existing values
for key, value in plugin_config.get("set", {}).items():
if key not in config:
config[key] = env.render_unknown(config, value)
# Create new defaults
for key, value in plugin_config.get("defaults", {}).items():
defaults[plugin_prefix + key] = value
def upgrade_obsolete(config):
@ -180,146 +145,10 @@ def upgrade_obsolete(config):
config["OPENEDX_MYSQL_USERNAME"] = config.pop("MYSQL_USERNAME")
def load_interactive(config, defaults):
ask("Your website domain name for students (LMS)", "LMS_HOST", config, defaults)
ask("Your website domain name for teachers (CMS)", "CMS_HOST", config, defaults)
ask("Your platform name/title", "PLATFORM_NAME", config, defaults)
ask("Your public contact email address", "CONTACT_EMAIL", config, defaults)
ask_choice(
"The default language code for the platform",
"LANGUAGE_CODE",
config,
defaults,
[
"en",
"am",
"ar",
"az",
"bg-bg",
"bn-bd",
"bn-in",
"bs",
"ca",
"ca@valencia",
"cs",
"cy",
"da",
"de-de",
"el",
"en-uk",
"en@lolcat",
"en@pirate",
"es-419",
"es-ar",
"es-ec",
"es-es",
"es-mx",
"es-pe",
"et-ee",
"eu-es",
"fa",
"fa-ir",
"fi-fi",
"fil",
"fr",
"gl",
"gu",
"he",
"hi",
"hr",
"hu",
"hy-am",
"id",
"it-it",
"ja-jp",
"kk-kz",
"km-kh",
"kn",
"ko-kr",
"lt-lt",
"ml",
"mn",
"mr",
"ms",
"nb",
"ne",
"nl-nl",
"or",
"pl",
"pt-br",
"pt-pt",
"ro",
"ru",
"si",
"sk",
"sl",
"sq",
"sr",
"sv",
"sw",
"ta",
"te",
"th",
"tr-tr",
"uk",
"ur",
"vi",
"uz",
"zh-cn",
"zh-hk",
"zh-tw",
],
)
ask_bool(
(
"Activate SSL/TLS certificates for HTTPS access? Important note:"
" this will NOT work in a development environment."
),
"ACTIVATE_HTTPS",
config,
defaults,
)
ask_bool(
"Activate Student Notes service (https://open.edx.org/features/student-notes)?",
"ACTIVATE_NOTES",
config,
defaults,
)
ask_bool(
"Activate Xqueue for external grader services (https://github.com/edx/xqueue)?",
"ACTIVATE_XQUEUE",
config,
defaults,
)
def ask(question, key, config, defaults):
default = env.render_str(config, config.get(key, defaults[key]))
config[key] = click.prompt(
fmt.question(question), prompt_suffix=" ", default=default, show_default=True
)
def ask_bool(question, key, config, defaults):
default = config.get(key, defaults[key])
config[key] = click.confirm(
fmt.question(question), prompt_suffix=" ", default=default
)
def ask_choice(question, key, config, defaults, choices):
default = config.get(key, defaults[key])
answer = click.prompt(
fmt.question(question),
type=click.Choice(choices),
prompt_suffix=" ",
default=default,
show_choices=False,
)
config[key] = answer
def convert_json2yml(root):
"""
Older versions of tutor used to have json config files.
"""
json_path = os.path.join(root, "config.json")
if not os.path.exists(json_path):
return
@ -331,14 +160,14 @@ def convert_json2yml(root):
)
with open(json_path) as fi:
config = json.load(fi)
save_config(root, config)
save(root, config)
os.remove(json_path)
fmt.echo_info(
"File config.json detected in {} and converted to config.yml".format(root)
)
def save_config(root, config):
def save(root, config):
path = config_path(root)
utils.ensure_file_directory_exists(path)
with open(path, "w") as of:
@ -346,9 +175,15 @@ def save_config(root, config):
fmt.echo_info("Configuration saved to {}".format(path))
def save_env(root, config):
env.render_full(root, config)
fmt.echo_info("Environment generated in {}".format(env.base_dir(root)))
def check_existing_config(root):
"""
Check there is a configuration on disk and the current environment is up-to-date.
"""
if not os.path.exists(config_path(root)):
raise exceptions.TutorError(
"Project root does not exist. Make sure to generate the initial configuration with `tutor config save` or `tutor config quickstart` prior to running other commands."
)
env.check_is_up_to_date(root)
def config_path(root):

View File

@ -19,10 +19,14 @@ class Renderer:
ENVIRONMENT = None
@classmethod
def environment(cls):
def environment(cls, config):
if not cls.ENVIRONMENT:
template_roots = [TEMPLATES_ROOT]
for plugin_name, plugin in plugins.iter_enabled(config):
# TODO move this to plugins.iter_templates and add tests
template_roots.append(plugin.templates)
environment = jinja2.Environment(
loader=jinja2.FileSystemLoader(TEMPLATES_ROOT),
loader=jinja2.FileSystemLoader(template_roots),
undefined=jinja2.StrictUndefined,
)
environment.filters["random_string"] = utils.random_string
@ -40,13 +44,13 @@ class Renderer:
@classmethod
def render_str(cls, config, text):
template = cls.environment().from_string(text)
template = cls.environment(config).from_string(text)
return cls.__render(config, template)
@classmethod
def render_file(cls, config, path):
try:
template = cls.environment().get_template(path)
template = cls.environment(config).get_template(path)
except:
fmt.echo_error("Error loading template " + path)
raise
@ -75,7 +79,7 @@ class Renderer:
def __render_patch(cls, config, name, separator="\n", suffix=""):
patches = []
for plugin, patch in plugins.iter_patches(config, name):
patch_template = cls.environment().from_string(patch)
patch_template = cls.environment(config).from_string(patch)
try:
patches.append(patch_template.render(**config))
except jinja2.exceptions.UndefinedError as e:
@ -90,6 +94,11 @@ class Renderer:
return rendered
def save(root, config):
render_full(root, config)
fmt.echo_info("Environment generated in {}".format(base_dir(root)))
def render_full(root, config):
"""
Render the full environment, including version information.
@ -146,6 +155,11 @@ def render_dict(config):
config[k] = v
def render_unknown(config, value):
if isinstance(value, str):
return render_str(config, value)
return value
def render_str(config, text):
"""
Args:
@ -170,6 +184,18 @@ def copy_subdir(subdir, root):
shutil.copy(src, dst)
def check_is_up_to_date(root):
if not is_up_to_date(root):
message = (
"The current environment stored at {} is not up-to-date: it is at "
"v{} while the 'tutor' binary is at v{}. You should upgrade "
"the environment by running:\n"
"\n"
" tutor config save -y"
)
fmt.echo_alert(message.format(base_dir(root), version(root), __version__))
def is_up_to_date(root):
"""
Check if the currently rendered version is equal to the current tutor version.

172
tutor/interactive.py Normal file
View File

@ -0,0 +1,172 @@
import os
import click
from . import config as tutor_config
from . import env
from . import fmt
from .__about__ import __version__
def update(root, silent=False):
"""
Load and save the configuration.
"""
config, defaults = load_all(root, silent=silent)
tutor_config.save(root, config)
tutor_config.merge(config, defaults)
return config
def load_all(root, silent=False):
"""
Load configuration and interactively ask questions to collect param values from the user.
"""
defaults = tutor_config.load_defaults()
config = {}
if os.path.exists(tutor_config.config_path(root)):
config = tutor_config.load_current(root, defaults)
if not silent:
ask_questions(config, defaults)
return config, defaults
def ask_questions(config, defaults):
ask("Your website domain name for students (LMS)", "LMS_HOST", config, defaults)
ask("Your website domain name for teachers (CMS)", "CMS_HOST", config, defaults)
ask("Your platform name/title", "PLATFORM_NAME", config, defaults)
ask("Your public contact email address", "CONTACT_EMAIL", config, defaults)
ask_choice(
"The default language code for the platform",
"LANGUAGE_CODE",
config,
defaults,
[
"en",
"am",
"ar",
"az",
"bg-bg",
"bn-bd",
"bn-in",
"bs",
"ca",
"ca@valencia",
"cs",
"cy",
"da",
"de-de",
"el",
"en-uk",
"en@lolcat",
"en@pirate",
"es-419",
"es-ar",
"es-ec",
"es-es",
"es-mx",
"es-pe",
"et-ee",
"eu-es",
"fa",
"fa-ir",
"fi-fi",
"fil",
"fr",
"gl",
"gu",
"he",
"hi",
"hr",
"hu",
"hy-am",
"id",
"it-it",
"ja-jp",
"kk-kz",
"km-kh",
"kn",
"ko-kr",
"lt-lt",
"ml",
"mn",
"mr",
"ms",
"nb",
"ne",
"nl-nl",
"or",
"pl",
"pt-br",
"pt-pt",
"ro",
"ru",
"si",
"sk",
"sl",
"sq",
"sr",
"sv",
"sw",
"ta",
"te",
"th",
"tr-tr",
"uk",
"ur",
"vi",
"uz",
"zh-cn",
"zh-hk",
"zh-tw",
],
)
ask_bool(
(
"Activate SSL/TLS certificates for HTTPS access? Important note:"
" this will NOT work in a development environment."
),
"ACTIVATE_HTTPS",
config,
defaults,
)
ask_bool(
"Activate Student Notes service (https://open.edx.org/features/student-notes)?",
"ACTIVATE_NOTES",
config,
defaults,
)
ask_bool(
"Activate Xqueue for external grader services (https://github.com/edx/xqueue)?",
"ACTIVATE_XQUEUE",
config,
defaults,
)
def ask(question, key, config, defaults):
default = env.render_str(config, config.get(key, defaults[key]))
config[key] = click.prompt(
fmt.question(question), prompt_suffix=" ", default=default, show_default=True
)
def ask_bool(question, key, config, defaults):
default = config.get(key, defaults[key])
config[key] = click.confirm(
fmt.question(question), prompt_suffix=" ", default=default
)
def ask_choice(question, key, config, defaults, choices):
default = config.get(key, defaults[key])
answer = click.prompt(
fmt.question(question),
type=click.Choice(choices),
prompt_suffix=" ",
default=default,
show_choices=False,
)
config[key] = answer

View File

@ -1,6 +1,7 @@
import pkg_resources
from . import exceptions
from . import fmt
"""
Tutor plugins are regular python packages that have a 'tutor.plugin.v1' entrypoint. This
@ -85,6 +86,11 @@ def enable(config, name):
config[CONFIG_KEY].sort()
def disable(config, name):
while name in config[CONFIG_KEY]:
config[CONFIG_KEY].remove(name)
def iter_enabled(config):
for name, plugin in iter_installed():
if is_enabled(config, name):
@ -100,46 +106,14 @@ def iter_patches(config, name):
yield plugin, patch
def load_config(config):
"""
TODO Return:
add_config (dict): config key/values to add, if they are not already present
set_config (dict): config key/values to set and override existing values
"""
add_config = {}
set_config = {}
defaults_config = {}
for plugin_name, plugin in iter_enabled(config):
plugin_prefix = plugin_name.upper() + "_"
plugin_config = get_callable_attr(plugin, "config")
# Add new config key/values
for key, value in plugin_config.get("add", {}).items():
add_config[plugin_prefix + key] = value
# Set existing config key/values
for key, value in plugin_config.get("set", {}).items():
# TODO crash in case of conflicting keys between plugins
# If key already exists
set_config[key] = value
# Create new defaults
for key, value in plugin_config.get("defaults", {}).items():
defaults_config[plugin_prefix + key] = value
return add_config, set_config, defaults_config
def iter_scripts(config, script_name):
"""
Scripts are of the form:
scripts = {
"script-name": [
{
"service": "service-name",
"command": "...",
},
"service-name1",
"service-name2",
...
],
...
@ -147,10 +121,5 @@ def iter_scripts(config, script_name):
"""
for plugin_name, plugin in iter_enabled(config):
scripts = get_callable_attr(plugin, "scripts")
for script in scripts.get(script_name, []):
try:
yield plugin_name, script["service"], script["command"]
except KeyError as e:
raise exceptions.TutorError(
"plugin script configuration requires key '{}'".format(e.args[0])
)
for service in scripts.get(script_name, []):
yield plugin_name, service

View File

@ -9,12 +9,8 @@ class BaseRunner:
self.root = root
self.config = config
def render(self, service, script, config=None):
config = config or self.config
return env.render_file(config, "scripts", service, script).strip()
def run(self, service, script, config=None):
command = self.render(service, script, config=config)
def run(self, service, *path):
command = env.render_file(self.config, *path).strip()
self.exec(service, command)
def exec(self, service, command):
@ -31,32 +27,37 @@ class BaseRunner:
def is_activated(self, service):
return self.config["ACTIVATE_" + service.upper()]
def iter_plugin_scripts(self, script):
yield from plugins.iter_scripts(self.config, script)
def migrate(runner):
fmt.echo_info("Creating all databases...")
runner.run("mysql-client", "createdatabases")
runner.run("mysql-client", "scripts", "mysql-client", "init")
for service in ["lms", "cms", "forum", "notes", "xqueue"]:
if runner.is_activated(service):
fmt.echo_info("Running {} migrations...".format(service))
runner.run(service, "init")
# TODO it's really ugly to load the config from the runner
for plugin_name, service, _command in plugins.iter_scripts(runner.config, "init"):
runner.run(service, "scripts", service, "init")
for plugin_name, service in runner.iter_plugin_scripts("init"):
fmt.echo_info(
"Plugin {}: running init for service {}...".format(plugin_name, service)
)
runner.run(service, "init")
runner.run(service, plugin_name, "scripts", service, "init")
fmt.echo_info("Databases ready.")
def create_user(runner, superuser, staff, name, email):
def create_user(runner, superuser, staff, username, email):
runner.check_service_is_activated("lms")
config = {"OPTS": "", "USERNAME": name, "EMAIL": email}
opts = ""
if superuser:
config["OPTS"] += " --superuser"
opts += " --superuser"
if staff:
config["OPTS"] += " --staff"
runner.run("lms", "createuser", config=config)
opts += " --staff"
command = (
"./manage.py lms --settings=tutor.production manage_user {opts} {username} {email}\n"
"./manage.py lms --settings=tutor.production changepassword {username}"
).format(opts=opts, username=username, email=email)
runner.exec("lms", command)
def import_demo_course(runner):

View File

@ -34,6 +34,7 @@
"HTTPS": "{{ "on" if ACTIVATE_HTTPS else "off" }}",
"LANGUAGE_CODE": "{{ LANGUAGE_CODE }}",
{% if ACTIVATE_HTTPS %}"SESSION_COOKIE_DOMAIN": ".{{ LMS_HOST|common_domain(CMS_HOST) }}",{% endif %}
{{ patch("cms-env", separator=",\n", suffix=",")|indent(2) }}
"CACHES": {
"default": {
"KEY_PREFIX": "default",

View File

@ -24,9 +24,10 @@ LOGGING["loggers"]["tracking"]["handlers"] = ["console", "local", "tracking"]
LOCALE_PATHS.append("/openedx/locale")
{{ patch("openedx-common-settings") }}
# Create folders if necessary
for folder in [LOG_DIR, MEDIA_ROOT, STATIC_ROOT_BASE]:
if not os.path.exists(folder):
os.makedirs(folder)
{{ patch("openedx-common-settings") }}
{{ patch("openedx-cms-common-settings") }}

View File

@ -50,3 +50,6 @@ LOCALE_PATHS.append("/openedx/locale")
for folder in [LOG_DIR, MEDIA_ROOT, STATIC_ROOT_BASE, ORA2_FILEUPLOAD_ROOT]:
if not os.path.exists(folder):
os.makedirs(folder)
{{ patch("openedx-common-settings") }}
{{ patch("openedx-lms-common-settings") }}

View File

@ -41,6 +41,10 @@ ENV VIRTUAL_ENV /openedx/venv/
RUN pip install setuptools==39.0.1 pip==9.0.3
RUN pip install -r requirements/edx/development.txt
# Install patched version of ora2
RUN pip uninstall -y ora2 && \
pip install git+https://github.com/regisb/edx-ora2.git@2.2.0-patched#egg=ora2==2.2.0
# Install a recent version of nodejs
RUN nodeenv /openedx/nodeenv --node=8.9.3 --prebuilt
ENV PATH /openedx/nodeenv/bin:${PATH}

View File

@ -247,6 +247,27 @@ spec:
persistentVolumeClaim:
claimName: mysql
{% endif %}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-client
labels:
app.kubernetes.io/name: mysql-client
spec:
selector:
matchLabels:
app.kubernetes.io/name: mysql-client
template:
metadata:
labels:
app.kubernetes.io/name: mysql-client
spec:
containers:
- name: mysql
image: {{ DOCKER_REGISTRY }}{{ DOCKER_IMAGE_MYSQL }}
command: ["sh", "-e", "-c"]
args: ["while true; do echo 'ready'; sleep 10; done"]
{% if ACTIVATE_NOTES %}
---
apiVersion: apps/v1
@ -399,3 +420,5 @@ spec:
persistentVolumeClaim:
claimName: rabbitmq
{% endif %}
{{ patch("k8s-deployments") }}

View File

@ -1,237 +0,0 @@
{% macro include_script(script) %}{% include "scripts/" + script %}{% endmacro %}---
apiVersion: batch/v1
kind: Job
metadata:
name: mysql-client/init
labels:
app.kubernetes.io/component: script
app.kubernetes.io/name: mysql-client/init
spec:
template:
metadata:
labels:
app.kubernetes.io/name: mysql-client/init
spec:
restartPolicy: Never
containers:
- name: mysql-client
image: {{ DOCKER_REGISTRY }}{{ DOCKER_IMAGE_MYSQL }}
command: ["/bin/sh", "-e", "-c"]
args:
- |
{{ include_script("mysql-client/createdatabases")|indent(12) }}
---
apiVersion: batch/v1
kind: Job
metadata:
name: cms/init
labels:
app.kubernetes.io/component: script
app.kubernetes.io/name: cms/init
spec:
template:
metadata:
labels:
app.kubernetes.io/name: cms/init
spec:
restartPolicy: Never
containers:
- name: cms
image: {{ DOCKER_REGISTRY }}{{ DOCKER_IMAGE_OPENEDX }}
command: ["/bin/sh", "-e", "-c"]
args:
- |
{{ include_script("cms/init")|indent(12) }}
volumeMounts:
- mountPath: /openedx/edx-platform/lms/envs/tutor/
name: settings-lms
- mountPath: /openedx/edx-platform/cms/envs/tutor/
name: settings-cms
- mountPath: /openedx/config
name: config
volumes:
- name: settings-lms
configMap:
name: openedx-settings-lms
- name: settings-cms
configMap:
name: openedx-settings-cms
- name: config
configMap:
name: openedx-config
---
apiVersion: batch/v1
kind: Job
metadata:
name: lms/init
labels:
app.kubernetes.io/component: script
app.kubernetes.io/name: lms/init
spec:
template:
metadata:
labels:
app.kubernetes.io/name: lms/init
spec:
restartPolicy: Never
containers:
- name: lms
image: {{ DOCKER_REGISTRY }}{{ DOCKER_IMAGE_OPENEDX }}
command: ["/bin/sh", "-e", "-c"]
args:
- |
{{ include_script("lms/init")|indent(12) }}
volumeMounts:
- mountPath: /openedx/edx-platform/lms/envs/tutor/
name: settings-lms
- mountPath: /openedx/edx-platform/cms/envs/tutor/
name: settings-cms
- mountPath: /openedx/config
name: config
volumes:
- name: settings-lms
configMap:
name: openedx-settings-lms
- name: settings-cms
configMap:
name: openedx-settings-cms
- name: config
configMap:
name: openedx-config
---
apiVersion: batch/v1
kind: Job
metadata:
name: cms/importdemocourse
labels:
app.kubernetes.io/component: script
app.kubernetes.io/name: cms/importdemocourse
spec:
template:
metadata:
labels:
app.kubernetes.io/name: cms/importdemocourse
spec:
restartPolicy: Never
containers:
- name: lms
image: {{ DOCKER_REGISTRY }}{{ DOCKER_IMAGE_OPENEDX }}
command: ["/bin/sh", "-e", "-c"]
args:
- |
{{ include_script("cms/importdemocourse")|indent(12) }}
volumeMounts:
- mountPath: /openedx/edx-platform/lms/envs/tutor/
name: settings-lms
- mountPath: /openedx/edx-platform/cms/envs/tutor/
name: settings-cms
- mountPath: /openedx/config
name: config
- mountPath: /openedx/data
name: data
volumes:
- name: settings-lms
configMap:
name: openedx-settings-lms
- name: settings-cms
configMap:
name: openedx-settings-cms
- name: config
configMap:
name: openedx-config
- name: data
persistentVolumeClaim:
claimName: lms-data
{% if ACTIVATE_FORUM %}
---
apiVersion: batch/v1
kind: Job
metadata:
name: forum/init
labels:
app.kubernetes.io/component: script
app.kubernetes.io/name: forum/init
spec:
template:
metadata:
labels:
app.kubernetes.io/name: forum/init
spec:
restartPolicy: Never
containers:
- name: forum
image: {{ DOCKER_REGISTRY }}{{ DOCKER_IMAGE_FORUM }}
command: ["/bin/sh", "-e", "-c"]
args:
- |
{{ include_script("forum/init")|indent(12) }}
env:
- name: SEARCH_SERVER
value: "http://{{ ELASTICSEARCH_HOST }}:{{ ELASTICSEARCH_PORT }}"
- name: MONGOHQ_URL
value: "mongodb://{% if MONGODB_USERNAME and MONGODB_PASSWORD %}{{ MONGODB_USERNAME}}:{{ MONGODB_PASSWORD }}@{% endif %}{{ MONGODB_HOST }}:{{ MONGODB_PORT }}/cs_comments_service"
{% endif %}
{% if ACTIVATE_NOTES %}
---
apiVersion: batch/v1
kind: Job
metadata:
name: notes/init
labels:
app.kubernetes.io/component: script
app.kubernetes.io/name: notes/init
spec:
template:
metadata:
labels:
app.kubernetes.io/name: notes/init
spec:
restartPolicy: Never
containers:
- name: notes
image: {{ DOCKER_REGISTRY }}{{ DOCKER_IMAGE_NOTES }}
command: ["/bin/sh", "-e", "-c"]
args:
- |
{{ include_script("notes/init")|indent(12) }}
volumeMounts:
- mountPath: /openedx/edx-notes-api/notesserver/settings/tutor.py
name: settings
volumes:
- name: settings
configMap:
name: notes-settings
{% endif %}
{% if ACTIVATE_XQUEUE %}
---
apiVersion: batch/v1
kind: Job
metadata:
name: xqueue/init
labels:
app.kubernetes.io/component: script
app.kubernetes.io/name: xqueue/init
spec:
template:
metadata:
labels:
app.kubernetes.io/name: xqueue/init
spec:
restartPolicy: Never
containers:
- name: xqueue
image: {{ DOCKER_REGISTRY }}{{ DOCKER_IMAGE_NOTES }}
command: ["/bin/sh", "-e", "-c"]
args:
- |
{{ include_script("xqueue/init")|indent(12) }}
volumeMounts:
- mountPath: /openedx/xqueue/xqueue/tutor.py
name: settings
volumes:
- name: settings
configMap:
name: notes-settings
{% endif %}
{{ patch("k8s-jobs") }}

View File

@ -5,7 +5,6 @@ resources:
- k8s/namespace.yml
- k8s/deployments.yml
- k8s/ingress.yml
- k8s/jobs.yml
- k8s/services.yml
- k8s/volumes.yml

View File

@ -30,9 +30,9 @@ services:
{% endif %}
mysql-client:
image: {{ DOCKER_REGISTRY }}{{ DOCKER_IMAGE_MYSQL }}
command: echo "mysql client ready"
command: sh
restart: "no"
{% if ACTIVATE_MYSQL %}depends_on:
{% if ACTIVATE_MYSQL%}depends_on:
- mysql{% endif %}
{% if ACTIVATE_ELASTICSEARCH %}
@ -63,6 +63,9 @@ services:
ports:
- "{{ NGINX_HTTP_PORT }}:80"
- "{{ NGINX_HTTPS_PORT }}:443"
networks:
default:
aliases: [{{ patch("local-docker-compose-nginx-aliases", separator=", ") }}]
volumes:
- ../apps/nginx:/etc/nginx/conf.d/:ro
- ../../data/openedx:/var/www/openedx:ro
@ -201,8 +204,8 @@ services:
- ../apps/notes/settings/tutor.py:/openedx/edx-notes-api/notesserver/settings/tutor.py
- ../../data/notes:/openedx/data
restart: unless-stopped
depends_on:
{% if ACTIVATE_MYSQL %}- mysql{% endif %}
{% if ACTIVATE_MYSQL %}depends_on:
- mysql{% endif %}
{% endif %}
{% if ACTIVATE_XQUEUE %}
@ -215,8 +218,8 @@ services:
environment:
DJANGO_SETTINGS_MODULE: xqueue.tutor
restart: unless-stopped
depends_on:
{% if ACTIVATE_MYSQL %}- mysql{% endif %}
{% if ACTIVATE_MYSQL %}depends_on:
- mysql{% endif %}
xqueue_consumer:
image: {{ DOCKER_REGISTRY }}{{ DOCKER_IMAGE_XQUEUE }}
@ -227,8 +230,8 @@ services:
DJANGO_SETTINGS_MODULE: xqueue.tutor
restart: unless-stopped
command: ./manage.py run_consumer
depends_on:
{% if ACTIVATE_MYSQL %}- mysql{% endif %}
{% if ACTIVATE_MYSQL %}depends_on:
- mysql{% endif %}
{% endif %}
{{ patch("local-docker-compose-services")|indent(2) }}

View File

@ -1,4 +1,5 @@
dockerize -wait tcp://{{ MYSQL_HOST }}:{{ MYSQL_PORT }} -timeout 20s && ./manage.py lms migrate
dockerize -wait tcp://{{ MYSQL_HOST }}:{{ MYSQL_PORT }} -timeout 20s
./manage.py lms migrate
./manage.py lms create_oauth2_client \
"http://androidapp.com" "http://androidapp.com/redirect" public \

View File

@ -1 +0,0 @@
./manage.py migrate

View File

@ -105,3 +105,12 @@ def execute(*command):
raise exceptions.TutorError(
"Command failed with status {}: {}".format(result, " ".join(command))
)
def check_output(*command):
click.echo(fmt.command(" ".join(command)))
try:
return subprocess.check_output(command)
except:
fmt.echo_error("Command failed: {}".format(" ".join(command)))
raise