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:
parent
f5c225231f
commit
6a68c4cc20
@ -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 }}"
|
28
plugins/minio/tutorminio/patches/k8s-deployments
Normal file
28
plugins/minio/tutorminio/patches/k8s-deployments
Normal 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
|
@ -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) }}
|
2
plugins/minio/tutorminio/patches/lms-env
Normal file
2
plugins/minio/tutorminio/patches/lms-env
Normal file
@ -0,0 +1,2 @@
|
||||
"FILE_UPLOAD_STORAGE_BUCKET_NAME": "{{ MINIO_FILE_UPLOAD_BUCKET_NAME }}",
|
||||
"COURSE_IMPORT_EXPORT_BUCKET": "{{ MINIO_COURSE_IMPORT_EXPORT_BUCKET }}"
|
@ -0,0 +1 @@
|
||||
"{{ MINIO_HOST }}"
|
@ -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
|
@ -1,2 +1,2 @@
|
||||
"AWS_STORAGE_BUCKET_NAME": "{{ MINIO_BUCKET_NAME }}",
|
||||
"AWS_S3_CUSTOM_DOMAIN": "{{ MINIO_HOST }}"
|
||||
"AWS_S3_CUSTOM_DOMAIN": ""
|
16
plugins/minio/tutorminio/patches/openedx-common-settings
Normal file
16
plugins/minio/tutorminio/patches/openedx-common-settings
Normal 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""")
|
@ -1 +0,0 @@
|
||||
AWS_S3_USE_SSL = {{ "True" if ACTIVATE_HTTPS else "False" }}
|
@ -0,0 +1 @@
|
||||
ORA2_FILEUPLOAD_BACKEND = "s3"
|
@ -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
|
||||
|
@ -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 }}
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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"))
|
||||
)
|
||||
|
@ -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"))
|
||||
|
@ -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)
|
||||
|
@ -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?
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
293
tutor/config.py
293
tutor/config.py
@ -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):
|
||||
|
36
tutor/env.py
36
tutor/env.py
@ -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
172
tutor/interactive.py
Normal 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
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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",
|
||||
|
@ -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") }}
|
||||
|
@ -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") }}
|
||||
|
@ -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}
|
||||
|
@ -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") }}
|
||||
|
@ -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") }}
|
@ -5,7 +5,6 @@ resources:
|
||||
- k8s/namespace.yml
|
||||
- k8s/deployments.yml
|
||||
- k8s/ingress.yml
|
||||
- k8s/jobs.yml
|
||||
- k8s/services.yml
|
||||
- k8s/volumes.yml
|
||||
|
||||
|
@ -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) }}
|
@ -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 \
|
||||
|
@ -1 +0,0 @@
|
||||
./manage.py migrate
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user