7
0
mirror of https://github.com/ChristianLight/tutor.git synced 2024-06-01 05:40:48 +00:00

refactor: clarify configuration management

Previously, configuration management was very confusing because we kept mixing
"base" and "defaults" configuration:

- It was difficult to make the difference between core settings that were
  necessary (e.g: passwords) as opposed to others that could simply be
  defaulted to.
- The order of settings in config.yml mattered: config entries that depended on
  other needed to be defined later. As a consequence, Tutor was not compatible
  with Python 3.5, where dict entries are not sorted.
This commit is contained in:
Régis Behmo 2021-11-08 14:46:38 +01:00 committed by Régis Behmo
parent b18c9dc4f8
commit c40e682f5d
11 changed files with 233 additions and 221 deletions

View File

@ -2,6 +2,8 @@ import unittest
from unittest.mock import Mock, patch
import tempfile
import click
from tutor import config as tutor_config
from tutor import interactive
from tutor.types import get_typed, Config
@ -9,7 +11,7 @@ from tutor.types import get_typed, Config
class ConfigTests(unittest.TestCase):
def test_version(self) -> None:
defaults = tutor_config.load_defaults()
defaults = tutor_config.get_defaults({})
self.assertNotIn("TUTOR_VERSION", defaults)
def test_merge(self) -> None:
@ -18,22 +20,21 @@ class ConfigTests(unittest.TestCase):
tutor_config.merge(config1, config2)
self.assertEqual({"x": "y"}, config1)
def test_merge_render(self) -> None:
def test_merge_not_render(self) -> None:
config: Config = {}
defaults = tutor_config.load_defaults()
base = tutor_config.get_base({})
with patch.object(tutor_config.utils, "random_string", return_value="abcd"):
tutor_config.merge(config, defaults)
tutor_config.merge(config, base)
self.assertEqual("abcd", config["MYSQL_ROOT_PASSWORD"])
# Check that merge does not perform a rendering
self.assertNotEqual("abcd", config["MYSQL_ROOT_PASSWORD"])
@patch.object(tutor_config.fmt, "echo")
def test_update_twice(self, _: Mock) -> None:
def test_save_load(self, _: Mock) -> None:
with tempfile.TemporaryDirectory() as root:
tutor_config.update(root)
config1 = tutor_config.load_user(root)
tutor_config.update(root)
config2 = tutor_config.load_user(root)
config1 = tutor_config.load_minimal(root)
tutor_config.save_config_file(root, config1)
config2 = tutor_config.load_minimal(root)
self.assertEqual(config1, config2)
@ -44,28 +45,32 @@ class ConfigTests(unittest.TestCase):
tutor_config.utils, "random_string"
) as mock_random_string:
mock_random_string.return_value = "abcd"
config1, _defaults1 = tutor_config.load_all(root)
config1 = tutor_config.load_full(root)
password1 = config1["MYSQL_ROOT_PASSWORD"]
config1.pop("MYSQL_ROOT_PASSWORD")
tutor_config.save_config_file(root, config1)
mock_random_string.return_value = "efgh"
config2, _defaults2 = tutor_config.load_all(root)
config2 = tutor_config.load_full(root)
password2 = config2["MYSQL_ROOT_PASSWORD"]
self.assertEqual("abcd", password1)
self.assertEqual("efgh", password2)
def test_interactive_load_all(self) -> None:
def test_interactive(self) -> None:
def mock_prompt(*_args: None, **kwargs: str) -> str:
return kwargs["default"]
with tempfile.TemporaryDirectory() as rootdir:
config, defaults = interactive.load_all(rootdir, interactive=False)
with patch.object(click, "prompt", new=mock_prompt):
with patch.object(click, "confirm", new=mock_prompt):
config = interactive.load_user_config(rootdir, interactive=True)
self.assertIn("MYSQL_ROOT_PASSWORD", config)
self.assertEqual(8, len(get_typed(config, "MYSQL_ROOT_PASSWORD", str)))
self.assertNotIn("LMS_HOST", config)
self.assertEqual("www.myopenedx.com", defaults["LMS_HOST"])
self.assertEqual("studio.{{ LMS_HOST }}", defaults["CMS_HOST"])
self.assertEqual("www.myopenedx.com", config["LMS_HOST"])
self.assertEqual("studio.www.myopenedx.com", config["CMS_HOST"])
def test_is_service_activated(self) -> None:
config: Config = {"RUN_SERVICE1": True, "RUN_SERVICE2": False}

View File

@ -63,7 +63,10 @@ class EnvTests(unittest.TestCase):
def test_render_file(self) -> None:
config: Config = {}
tutor_config.merge(config, tutor_config.load_defaults())
tutor_config.update_with_base(config)
tutor_config.update_with_defaults(config)
tutor_config.render_full(config)
config["MYSQL_ROOT_PASSWORD"] = "testpassword"
rendered = env.render_file(config, "hooks", "mysql", "init")
self.assertIn("testpassword", rendered)
@ -75,10 +78,8 @@ class EnvTests(unittest.TestCase):
)
def test_save_full(self) -> None:
defaults = tutor_config.load_defaults()
with tempfile.TemporaryDirectory() as root:
config = tutor_config.load_current(root, defaults)
tutor_config.merge(config, defaults)
config = tutor_config.load_full(root)
with patch.object(fmt, "STDOUT"):
env.save(root, config)
self.assertTrue(
@ -86,10 +87,8 @@ class EnvTests(unittest.TestCase):
)
def test_save_full_with_https(self) -> None:
defaults = tutor_config.load_defaults()
with tempfile.TemporaryDirectory() as root:
config = tutor_config.load_current(root, defaults)
tutor_config.merge(config, defaults)
config = tutor_config.load_full(root)
config["ENABLE_HTTPS"] = True
with patch.object(fmt, "STDOUT"):
env.save(root, config)

View File

@ -128,9 +128,6 @@ class PluginsTests(unittest.TestCase):
self.assertEqual([], patches)
def test_configure(self) -> None:
config: Config = {"ID": "id"}
defaults: Config = {}
class plugin1:
config: Config = {
"add": {"PARAM1": "value1", "PARAM2": "value2"},
@ -143,37 +140,31 @@ class PluginsTests(unittest.TestCase):
"iter_enabled",
return_value=[plugins.BasePlugin("plugin1", plugin1)],
):
tutor_config.load_plugins(config, defaults)
base = tutor_config.get_base({})
defaults = tutor_config.get_defaults({})
self.assertEqual(
{
"ID": "id",
"PARAM3": "value3",
"PLUGIN1_PARAM1": "value1",
"PLUGIN1_PARAM2": "value2",
},
config,
)
self.assertEqual({"PLUGIN1_PARAM4": "value4"}, defaults)
self.assertEqual(base["PARAM3"], "value3")
self.assertEqual(base["PLUGIN1_PARAM1"], "value1")
self.assertEqual(base["PLUGIN1_PARAM2"], "value2")
self.assertEqual(defaults["PLUGIN1_PARAM4"], "value4")
def test_configure_set_does_not_override(self) -> None:
config: Config = {"ID": "oldid"}
config: Config = {"ID1": "oldid"}
class plugin1:
config: Config = {"set": {"ID": "newid"}}
config: Config = {"set": {"ID1": "newid", "ID2": "id2"}}
with patch.object(
plugins.Plugins,
"iter_enabled",
return_value=[plugins.BasePlugin("plugin1", plugin1)],
):
tutor_config.load_plugins(config, {})
tutor_config.update_with_base(config)
self.assertEqual({"ID": "oldid"}, config)
self.assertEqual("oldid", config["ID1"])
self.assertEqual("id2", config["ID2"])
def test_configure_set_random_string(self) -> None:
config: Config = {}
class plugin1:
config: Config = {"set": {"PARAM1": "{{ 128|random_string }}"}}
@ -182,12 +173,13 @@ class PluginsTests(unittest.TestCase):
"iter_enabled",
return_value=[plugins.BasePlugin("plugin1", plugin1)],
):
tutor_config.load_plugins(config, {})
config = tutor_config.get_base({})
tutor_config.render_full(config)
self.assertEqual(128, len(get_typed(config, "PARAM1", str)))
def test_configure_default_value_with_previous_definition(self) -> None:
config: Config = {}
defaults: Config = {"PARAM1": "value"}
config: Config = {"PARAM1": "value"}
class plugin1:
config: Config = {"defaults": {"PARAM2": "{{ PARAM1 }}"}}
@ -197,10 +189,10 @@ class PluginsTests(unittest.TestCase):
"iter_enabled",
return_value=[plugins.BasePlugin("plugin1", plugin1)],
):
tutor_config.load_plugins(config, defaults)
self.assertEqual("{{ PARAM1 }}", defaults["PLUGIN1_PARAM2"])
tutor_config.update_with_defaults(config)
self.assertEqual("{{ PARAM1 }}", config["PLUGIN1_PARAM2"])
def test_configure_add_twice(self) -> None:
def test_config_load_from_plugins(self) -> None:
config: Config = {}
class plugin1:
@ -211,19 +203,12 @@ class PluginsTests(unittest.TestCase):
"iter_enabled",
return_value=[plugins.BasePlugin("plugin1", plugin1)],
):
tutor_config.load_plugins(config, {})
tutor_config.update_with_base(config)
tutor_config.update_with_defaults(config)
tutor_config.render_full(config)
value1 = get_typed(config, "PLUGIN1_PARAM1", str)
with patch.object(
plugins.Plugins,
"iter_enabled",
return_value=[plugins.BasePlugin("plugin1", plugin1)],
):
tutor_config.load_plugins(config, {})
value2 = get_typed(config, "PLUGIN1_PARAM1", str)
self.assertEqual(10, len(value1))
self.assertEqual(10, len(value2))
self.assertEqual(value1, value2)
def test_hooks(self) -> None:
class plugin1:

View File

@ -50,16 +50,16 @@ def save(
unset_vars: List[str],
env_only: bool,
) -> None:
config, defaults = interactive_config.load_all(
context.root, interactive=interactive
)
config = interactive_config.load_user_config(context.root, interactive=interactive)
if set_vars:
tutor_config.merge(config, dict(set_vars), force=True)
for key, value in dict(set_vars).items():
config[key] = env.render_unknown(config, value)
for key in unset_vars:
config.pop(key, None)
if not env_only:
tutor_config.save_config_file(context.root, config)
tutor_config.merge(config, defaults)
tutor_config.render_full(config)
env.save(context.root, config)
@ -78,8 +78,8 @@ def save(
def render(context: Context, extra_configs: List[str], src: str, dst: str) -> None:
config = tutor_config.load(context.root)
for extra_config in extra_configs:
tutor_config.merge(
config, tutor_config.load_config_file(extra_config), force=True
config.update(
env.render_unknown(config, tutor_config.get_yaml_file(extra_config))
)
renderer = env.Renderer(config, [src])

View File

@ -8,11 +8,11 @@ from .. import config as tutor_config
from .. import env as tutor_env
from .. import exceptions
from .. import fmt
from .. import interactive as interactive_config
from .. import jobs
from .. import serialize
from ..types import Config, get_typed
from .. import utils
from .config import save as config_save_command
from .context import Context
@ -162,9 +162,13 @@ def k8s() -> None:
@click.pass_context
def quickstart(context: click.Context, non_interactive: bool) -> None:
click.echo(fmt.title("Interactive platform configuration"))
config = interactive_config.update(
context.obj.root, interactive=(not non_interactive)
context.invoke(
config_save_command,
interactive=(not non_interactive),
set_vars=[],
unset_vars=[],
)
config = tutor_config.load(context.obj.root)
if not config["ENABLE_WEB_PROXY"]:
fmt.echo_alert(
"Potentially invalid configuration: ENABLE_WEB_PROXY=false\n"

View File

@ -109,7 +109,7 @@ Your Open edX platform is ready and can be accessed at the following urls:
@click.option("-I", "--non-interactive", is_flag=True, help="Run non-interactively")
@click.pass_context
def upgrade(context: click.Context, from_version: str, non_interactive: bool) -> None:
config = tutor_config.load_no_check(context.obj.root)
config = tutor_config.load_full(context.obj.root)
if not non_interactive:
question = """You are about to upgrade your Open edX platform. It is strongly recommended to make a backup before upgrading. To do so, run:

View File

@ -28,7 +28,7 @@ def plugins_command() -> None:
@click.command(name="list", help="List installed plugins")
@click.pass_obj
def list_command(context: Context) -> None:
config = tutor_config.load_user(context.root)
config = tutor_config.load_full(context.root)
for plugin in plugins.iter_installed():
status = "" if plugins.is_enabled(config, plugin.name) else " (disabled)"
print(
@ -42,7 +42,7 @@ def list_command(context: Context) -> None:
@click.argument("plugin_names", metavar="plugin", nargs=-1)
@click.pass_obj
def enable(context: Context, plugin_names: List[str]) -> None:
config = tutor_config.load_user(context.root)
config = tutor_config.load_full(context.root)
for plugin in plugin_names:
plugins.enable(config, plugin)
fmt.echo_info("Plugin {} enabled".format(plugin))
@ -59,7 +59,7 @@ def enable(context: Context, plugin_names: List[str]) -> None:
@click.argument("plugin_names", metavar="plugin", nargs=-1)
@click.pass_obj
def disable(context: Context, plugin_names: List[str]) -> None:
config = tutor_config.load_user(context.root)
config = tutor_config.load_full(context.root)
disable_all = "all" in plugin_names
for plugin in plugins.iter_enabled(config):
if disable_all or plugin.name in plugin_names:

View File

@ -1,134 +1,178 @@
import os
from typing import Tuple
from . import env, exceptions, fmt, plugins, serialize, utils
from .types import Config, cast_config
def update(root: str) -> Config:
"""
Load and save the configuration.
"""
config, defaults = load_all(root)
save_config_file(root, config)
merge(config, defaults)
return config
def load(root: str) -> Config:
"""
Load full configuration. This will raise an exception if there is no current
configuration in the project root.
Load full configuration.
This will raise an exception if there is no current configuration in the
project root. A warning will also be printed if the version from disk
differs from the package version.
"""
check_existing_config(root)
return load_no_check(root)
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 --interactive` or `tutor local "
"quickstart` prior to running other commands."
)
env.check_is_up_to_date(root)
convert_json2yml(root)
return load_full(root)
def load_no_check(root: str) -> Config:
config, defaults = load_all(root)
merge(config, defaults)
def load_minimal(root: str) -> Config:
"""
Load a minimal configuration composed of the user and the base config.
This configuration is not suitable for rendering templates, as it is incomplete.
"""
config = get_user(root)
update_with_base(config)
render_full(config)
return config
def load_all(root: str) -> Tuple[Config, Config]:
def load_full(root: str) -> Config:
"""
Return:
current (dict): params currently saved in config.yml
defaults (dict): default values of params which might be missing from the
current config
Load a full configuration, with user, base and defaults.
"""
defaults = load_defaults()
current = load_current(root, defaults)
return current, defaults
config = get_user(root)
update_with_base(config)
update_with_defaults(config)
render_full(config)
return config
def merge(config: Config, defaults: Config, force: bool = False) -> None:
def update_with_base(config: Config) -> None:
"""
Merge default values with user configuration and perform rendering of "{{...}}"
values.
Add base configuration to the config object.
Note that configuration entries are unrendered at this point.
"""
for key, value in defaults.items():
if force or key not in config:
config[key] = env.render_unknown(config, value)
base = get_base(config)
merge(config, base)
def load_defaults() -> Config:
config = serialize.load(env.read_template_file("config.yml"))
def update_with_defaults(config: Config) -> None:
"""
Add default configuration to the config object.
Note that configuration entries are unrendered at this point.
"""
defaults = get_defaults(config)
merge(config, defaults)
def update_with_env(config: Config) -> None:
"""
Override config values from environment variables.
"""
overrides = {}
for k in config.keys():
env_var = "TUTOR_" + k
if env_var in os.environ:
overrides[k] = serialize.parse(os.environ[env_var])
config.update(overrides)
def get_user(root: str) -> Config:
"""
Get the user configuration from the tutor root.
Overrides from environment variables are loaded as well.
"""
path = config_path(root)
config = {}
if os.path.exists(path):
config = get_yaml_file(path)
upgrade_obsolete(config)
update_with_env(config)
return config
def get_base(config: Config) -> Config:
"""
Load the base configuration.
Entries in this configuration are unrendered.
"""
base = get_template("base.yml")
# Load base values from plugins
for plugin in plugins.iter_enabled(config):
# Add new config key/values
for key, value in plugin.config_add.items():
new_key = plugin.config_key(key)
base[new_key] = value
# Set existing config key/values
for key, value in plugin.config_set.items():
base[key] = value
return base
def get_defaults(config: Config) -> Config:
"""
Get default configuration, including from plugins.
Entries in this configuration are unrendered.
"""
defaults = get_template("defaults.yml")
for plugin in plugins.iter_enabled(config):
# Create new defaults
for key, value in plugin.config_defaults.items():
defaults[plugin.config_key(key)] = value
update_with_env(defaults)
return defaults
def get_template(filename: str) -> Config:
"""
Get one of the configuration templates.
Entries in this configuration are unrendered.
"""
config = serialize.load(env.read_template_file("config", filename))
return cast_config(config)
def load_config_file(path: str) -> Config:
def get_yaml_file(path: str) -> Config:
"""
Load config from yaml file.
"""
with open(path) as f:
config = serialize.load(f.read())
return cast_config(config)
def load_current(root: str, defaults: Config) -> Config:
def merge(config: Config, base: Config) -> None:
"""
Load the configuration currently stored on disk.
Note: this modifies the defaults with the plugin default values.
Merge base values with user configuration. Values are only added if not
already present.
Note that this function does not perform the rendering step of the
configuration entries.
"""
convert_json2yml(root)
config = load_user(root)
load_env(config, defaults)
load_required(config, defaults)
load_plugins(config, defaults)
return config
def load_user(root: str) -> Config:
path = config_path(root)
if not os.path.exists(path):
return {}
config = load_config_file(path)
upgrade_obsolete(config)
return config
def load_env(config: Config, defaults: Config) -> None:
for k in defaults.keys():
env_var = "TUTOR_" + k
if env_var in os.environ:
config[k] = serialize.parse(os.environ[env_var])
def load_required(config: Config, defaults: Config) -> None:
"""
All these keys must be present in the user's config.yml. This includes all values
that are generated once and must be kept after that, such as passwords.
"""
for key in [
"OPENEDX_SECRET_KEY",
"MYSQL_ROOT_PASSWORD",
"OPENEDX_MYSQL_PASSWORD",
"ID",
"JWT_RSA_PRIVATE_KEY",
]:
for key, value in base.items():
if key not in config:
config[key] = env.render_unknown(config, defaults[key])
config[key] = value
def load_plugins(config: Config, defaults: Config) -> None:
def render_full(config: Config) -> None:
"""
Add, override and set new defaults from plugins.
Fill and render an existing configuration with defaults.
It is generally necessary to apply this function before rendering templates,
otherwise configuration entries may not be rendered.
"""
for plugin in plugins.iter_enabled(config):
# Add new config key/values
for key, value in plugin.config_add.items():
new_key = plugin.config_key(key)
if new_key not in config:
config[new_key] = env.render_unknown(config, value)
# Create new defaults
for key, value in plugin.config_defaults.items():
defaults[plugin.config_key(key)] = value
# Set existing config key/values: here, we do not override existing values
# This must come last, as overridden values may depend on plugin defaults
for key, value in plugin.config_set.items():
if key not in config:
config[key] = env.render_unknown(config, value)
for key, value in config.items():
config[key] = env.render_unknown(config, value)
def is_service_activated(config: Config, service: str) -> bool:
@ -194,7 +238,7 @@ def convert_json2yml(root: str) -> None:
root
)
)
config = load_config_file(json_path)
config = get_yaml_file(json_path)
save_config_file(root, config)
os.remove(json_path)
fmt.echo_info(
@ -210,18 +254,5 @@ def save_config_file(root: str, config: Config) -> None:
fmt.echo_info("Configuration saved to {}".format(path))
def check_existing_config(root: str) -> None:
"""
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 --interactive` or `tutor local "
"quickstart` prior to running other commands."
)
env.check_is_up_to_date(root)
def config_path(root: str) -> str:
return os.path.join(root, "config.yml")

View File

@ -1,34 +1,24 @@
from typing import List, Tuple
from typing import List
import click
from . import config as tutor_config
from . import env, exceptions, fmt
from .__about__ import __version__
from .types import Config, get_typed
def update(root: str, interactive: bool = True) -> Config:
"""
Load and save the configuration.
"""
config, defaults = load_all(root, interactive=interactive)
tutor_config.save_config_file(root, config)
tutor_config.merge(config, defaults)
return config
def load_all(root: str, interactive: bool = True) -> Tuple[Config, Config]:
def load_user_config(root: str, interactive: bool = True) -> Config:
"""
Load configuration and interactively ask questions to collect param values from the user.
"""
config, defaults = tutor_config.load_all(root)
config = tutor_config.load_minimal(root)
if interactive:
ask_questions(config, defaults)
return config, defaults
ask_questions(config)
return config
def ask_questions(config: Config, defaults: Config) -> None:
def ask_questions(config: Config) -> None:
defaults = tutor_config.get_defaults(config)
run_for_prod = config.get("LMS_HOST") != "local.overhang.io"
run_for_prod = click.confirm(
fmt.question(

View File

@ -0,0 +1,6 @@
---
ID: "{{ 24|random_string }}"
JWT_RSA_PRIVATE_KEY: "{{ 2048|rsa_private_key }}"
MYSQL_ROOT_PASSWORD: "{{ 8|random_string }}"
OPENEDX_MYSQL_PASSWORD: "{{ 8|random_string }}"
OPENEDX_SECRET_KEY: "{{ 24|random_string }}"

View File

@ -1,27 +1,10 @@
---
# These configuration values must be stored in the user's config.yml.
MYSQL_ROOT_PASSWORD: "{{ 8|random_string }}"
OPENEDX_MYSQL_PASSWORD: "{{ 8|random_string }}"
OPENEDX_SECRET_KEY: "{{ 24|random_string }}"
ID: "{{ 24|random_string }}"
# This file includes all Tutor setting defaults. Settings that do not have a
# default value, such as passwords, should be stored in base.yml.
# This must be defined early
LMS_HOST: "www.myopenedx.com"
# The following are default values
RUN_LMS: true
RUN_CMS: true
RUN_ELASTICSEARCH: true
RUN_MONGODB: true
RUN_MYSQL: true
RUN_REDIS: true
RUN_SMTP: true
CADDY_HTTP_PORT: 80
CMS_HOST: "studio.{{ LMS_HOST }}"
PREVIEW_LMS_HOST: "preview.{{ LMS_HOST }}"
CONTACT_EMAIL: "contact@{{ LMS_HOST }}"
OPENEDX_AWS_ACCESS_KEY: ""
OPENEDX_AWS_SECRET_ACCESS_KEY: ""
DEV_PROJECT_NAME: "{{ TUTOR_APP }}_dev"
DOCKER_REGISTRY: "docker.io/"
DOCKER_IMAGE_OPENEDX: "{{ DOCKER_REGISTRY }}overhangio/openedx:{{ TUTOR_VERSION }}"
@ -35,7 +18,6 @@ DOCKER_IMAGE_NGINX: "{{ DOCKER_REGISTRY }}nginx:1.21.1"
DOCKER_IMAGE_PERMISSIONS: "{{ DOCKER_REGISTRY }}overhangio/openedx-permissions:{{ TUTOR_VERSION }}"
DOCKER_IMAGE_REDIS: "{{ DOCKER_REGISTRY }}redis:6.2.6"
DOCKER_IMAGE_SMTP: "{{ DOCKER_REGISTRY }}devture/exim-relay:4.94.2-r0-4"
LOCAL_PROJECT_NAME: "{{ TUTOR_APP }}_local"
ELASTICSEARCH_HOST: "elasticsearch"
ELASTICSEARCH_PORT: 9200
ELASTICSEARCH_SCHEME: "http"
@ -45,14 +27,17 @@ ENABLE_WEB_PROXY: true
JWT_COMMON_AUDIENCE: "openedx"
JWT_COMMON_ISSUER: "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/oauth2"
JWT_COMMON_SECRET_KEY: "{{ OPENEDX_SECRET_KEY }}"
JWT_RSA_PRIVATE_KEY: "{{ 2048|rsa_private_key }}"
K8S_NAMESPACE: "openedx"
LANGUAGE_CODE: "en"
LMS_HOST: "www.myopenedx.com"
LOCAL_PROJECT_NAME: "{{ TUTOR_APP }}_local"
MONGODB_HOST: "mongodb"
MONGODB_DATABASE: "openedx"
MONGODB_PORT: 27017
MONGODB_USERNAME: ""
MONGODB_PASSWORD: ""
OPENEDX_AWS_ACCESS_KEY: ""
OPENEDX_AWS_SECRET_ACCESS_KEY: ""
OPENEDX_CACHE_REDIS_DB: 1
OPENEDX_CELERY_REDIS_DB: 0
OPENEDX_CMS_UWSGI_WORKERS: 2
@ -73,6 +58,13 @@ REDIS_HOST: "redis"
REDIS_PORT: 6379
REDIS_USERNAME: ""
REDIS_PASSWORD: ""
RUN_CMS: true
RUN_ELASTICSEARCH: true
RUN_LMS: true
RUN_MONGODB: true
RUN_MYSQL: true
RUN_REDIS: true
RUN_SMTP: true
SMTP_HOST: "smtp"
SMTP_PORT: 8025
SMTP_USERNAME: ""