mirror of
https://github.com/ChristianLight/tutor.git
synced 2025-01-06 07:30:40 +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:
parent
b18c9dc4f8
commit
c40e682f5d
@ -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}
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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])
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
247
tutor/config.py
247
tutor/config.py
@ -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")
|
||||
|
@ -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(
|
||||
|
6
tutor/templates/config/base.yml
Normal file
6
tutor/templates/config/base.yml
Normal 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 }}"
|
@ -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: ""
|
Loading…
Reference in New Issue
Block a user