6
0
mirror of https://github.com/ChristianLight/tutor.git synced 2025-01-22 13:18:24 +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 from unittest.mock import Mock, patch
import tempfile import tempfile
import click
from tutor import config as tutor_config from tutor import config as tutor_config
from tutor import interactive from tutor import interactive
from tutor.types import get_typed, Config from tutor.types import get_typed, Config
@ -9,7 +11,7 @@ from tutor.types import get_typed, Config
class ConfigTests(unittest.TestCase): class ConfigTests(unittest.TestCase):
def test_version(self) -> None: def test_version(self) -> None:
defaults = tutor_config.load_defaults() defaults = tutor_config.get_defaults({})
self.assertNotIn("TUTOR_VERSION", defaults) self.assertNotIn("TUTOR_VERSION", defaults)
def test_merge(self) -> None: def test_merge(self) -> None:
@ -18,22 +20,21 @@ class ConfigTests(unittest.TestCase):
tutor_config.merge(config1, config2) tutor_config.merge(config1, config2)
self.assertEqual({"x": "y"}, config1) self.assertEqual({"x": "y"}, config1)
def test_merge_render(self) -> None: def test_merge_not_render(self) -> None:
config: Config = {} config: Config = {}
defaults = tutor_config.load_defaults() base = tutor_config.get_base({})
with patch.object(tutor_config.utils, "random_string", return_value="abcd"): 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") @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: with tempfile.TemporaryDirectory() as root:
tutor_config.update(root) config1 = tutor_config.load_minimal(root)
config1 = tutor_config.load_user(root) tutor_config.save_config_file(root, config1)
config2 = tutor_config.load_minimal(root)
tutor_config.update(root)
config2 = tutor_config.load_user(root)
self.assertEqual(config1, config2) self.assertEqual(config1, config2)
@ -44,28 +45,32 @@ class ConfigTests(unittest.TestCase):
tutor_config.utils, "random_string" tutor_config.utils, "random_string"
) as mock_random_string: ) as mock_random_string:
mock_random_string.return_value = "abcd" mock_random_string.return_value = "abcd"
config1, _defaults1 = tutor_config.load_all(root) config1 = tutor_config.load_full(root)
password1 = config1["MYSQL_ROOT_PASSWORD"] password1 = config1["MYSQL_ROOT_PASSWORD"]
config1.pop("MYSQL_ROOT_PASSWORD") config1.pop("MYSQL_ROOT_PASSWORD")
tutor_config.save_config_file(root, config1) tutor_config.save_config_file(root, config1)
mock_random_string.return_value = "efgh" mock_random_string.return_value = "efgh"
config2, _defaults2 = tutor_config.load_all(root) config2 = tutor_config.load_full(root)
password2 = config2["MYSQL_ROOT_PASSWORD"] password2 = config2["MYSQL_ROOT_PASSWORD"]
self.assertEqual("abcd", password1) self.assertEqual("abcd", password1)
self.assertEqual("efgh", password2) 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: 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.assertIn("MYSQL_ROOT_PASSWORD", config)
self.assertEqual(8, len(get_typed(config, "MYSQL_ROOT_PASSWORD", str))) self.assertEqual(8, len(get_typed(config, "MYSQL_ROOT_PASSWORD", str)))
self.assertNotIn("LMS_HOST", config) self.assertEqual("www.myopenedx.com", config["LMS_HOST"])
self.assertEqual("www.myopenedx.com", defaults["LMS_HOST"]) self.assertEqual("studio.www.myopenedx.com", config["CMS_HOST"])
self.assertEqual("studio.{{ LMS_HOST }}", defaults["CMS_HOST"])
def test_is_service_activated(self) -> None: def test_is_service_activated(self) -> None:
config: Config = {"RUN_SERVICE1": True, "RUN_SERVICE2": False} config: Config = {"RUN_SERVICE1": True, "RUN_SERVICE2": False}

View File

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

View File

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

View File

@ -50,16 +50,16 @@ def save(
unset_vars: List[str], unset_vars: List[str],
env_only: bool, env_only: bool,
) -> None: ) -> None:
config, defaults = interactive_config.load_all( config = interactive_config.load_user_config(context.root, interactive=interactive)
context.root, interactive=interactive
)
if set_vars: 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: for key in unset_vars:
config.pop(key, None) config.pop(key, None)
if not env_only: if not env_only:
tutor_config.save_config_file(context.root, config) tutor_config.save_config_file(context.root, config)
tutor_config.merge(config, defaults)
tutor_config.render_full(config)
env.save(context.root, 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: def render(context: Context, extra_configs: List[str], src: str, dst: str) -> None:
config = tutor_config.load(context.root) config = tutor_config.load(context.root)
for extra_config in extra_configs: for extra_config in extra_configs:
tutor_config.merge( config.update(
config, tutor_config.load_config_file(extra_config), force=True env.render_unknown(config, tutor_config.get_yaml_file(extra_config))
) )
renderer = env.Renderer(config, [src]) 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 env as tutor_env
from .. import exceptions from .. import exceptions
from .. import fmt from .. import fmt
from .. import interactive as interactive_config
from .. import jobs from .. import jobs
from .. import serialize from .. import serialize
from ..types import Config, get_typed from ..types import Config, get_typed
from .. import utils from .. import utils
from .config import save as config_save_command
from .context import Context from .context import Context
@ -162,9 +162,13 @@ def k8s() -> None:
@click.pass_context @click.pass_context
def quickstart(context: click.Context, non_interactive: bool) -> None: def quickstart(context: click.Context, non_interactive: bool) -> None:
click.echo(fmt.title("Interactive platform configuration")) click.echo(fmt.title("Interactive platform configuration"))
config = interactive_config.update( context.invoke(
context.obj.root, interactive=(not non_interactive) config_save_command,
interactive=(not non_interactive),
set_vars=[],
unset_vars=[],
) )
config = tutor_config.load(context.obj.root)
if not config["ENABLE_WEB_PROXY"]: if not config["ENABLE_WEB_PROXY"]:
fmt.echo_alert( fmt.echo_alert(
"Potentially invalid configuration: ENABLE_WEB_PROXY=false\n" "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.option("-I", "--non-interactive", is_flag=True, help="Run non-interactively")
@click.pass_context @click.pass_context
def upgrade(context: click.Context, from_version: str, non_interactive: bool) -> None: 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: 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: 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.command(name="list", help="List installed plugins")
@click.pass_obj @click.pass_obj
def list_command(context: Context) -> None: 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(): for plugin in plugins.iter_installed():
status = "" if plugins.is_enabled(config, plugin.name) else " (disabled)" status = "" if plugins.is_enabled(config, plugin.name) else " (disabled)"
print( print(
@ -42,7 +42,7 @@ def list_command(context: Context) -> None:
@click.argument("plugin_names", metavar="plugin", nargs=-1) @click.argument("plugin_names", metavar="plugin", nargs=-1)
@click.pass_obj @click.pass_obj
def enable(context: Context, plugin_names: List[str]) -> None: 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: for plugin in plugin_names:
plugins.enable(config, plugin) plugins.enable(config, plugin)
fmt.echo_info("Plugin {} enabled".format(plugin)) fmt.echo_info("Plugin {} enabled".format(plugin))
@ -59,7 +59,7 @@ def enable(context: Context, plugin_names: List[str]) -> None:
@click.argument("plugin_names", metavar="plugin", nargs=-1) @click.argument("plugin_names", metavar="plugin", nargs=-1)
@click.pass_obj @click.pass_obj
def disable(context: Context, plugin_names: List[str]) -> None: 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 disable_all = "all" in plugin_names
for plugin in plugins.iter_enabled(config): for plugin in plugins.iter_enabled(config):
if disable_all or plugin.name in plugin_names: if disable_all or plugin.name in plugin_names:

View File

@ -1,134 +1,178 @@
import os import os
from typing import Tuple
from . import env, exceptions, fmt, plugins, serialize, utils from . import env, exceptions, fmt, plugins, serialize, utils
from .types import Config, cast_config 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: def load(root: str) -> Config:
""" """
Load full configuration. This will raise an exception if there is no current Load full configuration.
configuration in the project root.
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) if not os.path.exists(config_path(root)):
return load_no_check(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: def load_minimal(root: str) -> Config:
config, defaults = load_all(root) """
merge(config, defaults) 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 return config
def load_all(root: str) -> Tuple[Config, Config]: def load_full(root: str) -> Config:
""" """
Return: Load a full configuration, with user, base and defaults.
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() config = get_user(root)
current = load_current(root, defaults) update_with_base(config)
return current, defaults 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 "{{...}}" Add base configuration to the config object.
values.
Note that configuration entries are unrendered at this point.
""" """
for key, value in defaults.items(): base = get_base(config)
if force or key not in config: merge(config, base)
config[key] = env.render_unknown(config, value)
def load_defaults() -> Config: def update_with_defaults(config: Config) -> None:
config = serialize.load(env.read_template_file("config.yml")) """
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) 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: with open(path) as f:
config = serialize.load(f.read()) config = serialize.load(f.read())
return cast_config(config) 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. Merge base values with user configuration. Values are only added if not
Note: this modifies the defaults with the plugin default values. already present.
Note that this function does not perform the rendering step of the
configuration entries.
""" """
convert_json2yml(root) for key, value in base.items():
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",
]:
if key not in config: 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): for key, value in config.items():
# Add new config key/values config[key] = env.render_unknown(config, value)
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)
def is_service_activated(config: Config, service: str) -> bool: def is_service_activated(config: Config, service: str) -> bool:
@ -194,7 +238,7 @@ def convert_json2yml(root: str) -> None:
root root
) )
) )
config = load_config_file(json_path) config = get_yaml_file(json_path)
save_config_file(root, config) save_config_file(root, config)
os.remove(json_path) os.remove(json_path)
fmt.echo_info( fmt.echo_info(
@ -210,18 +254,5 @@ def save_config_file(root: str, config: Config) -> None:
fmt.echo_info("Configuration saved to {}".format(path)) 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: def config_path(root: str) -> str:
return os.path.join(root, "config.yml") return os.path.join(root, "config.yml")

View File

@ -1,34 +1,24 @@
from typing import List, Tuple from typing import List
import click import click
from . import config as tutor_config from . import config as tutor_config
from . import env, exceptions, fmt from . import env, exceptions, fmt
from .__about__ import __version__
from .types import Config, get_typed from .types import Config, get_typed
def update(root: str, interactive: bool = True) -> Config: def load_user_config(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]:
""" """
Load configuration and interactively ask questions to collect param values from the user. 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: if interactive:
ask_questions(config, defaults) ask_questions(config)
return config, defaults 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 = config.get("LMS_HOST") != "local.overhang.io"
run_for_prod = click.confirm( run_for_prod = click.confirm(
fmt.question( 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. # This file includes all Tutor setting defaults. Settings that do not have a
MYSQL_ROOT_PASSWORD: "{{ 8|random_string }}" # default value, such as passwords, should be stored in base.yml.
OPENEDX_MYSQL_PASSWORD: "{{ 8|random_string }}"
OPENEDX_SECRET_KEY: "{{ 24|random_string }}"
ID: "{{ 24|random_string }}"
# This must be defined early # 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 CADDY_HTTP_PORT: 80
CMS_HOST: "studio.{{ LMS_HOST }}" CMS_HOST: "studio.{{ LMS_HOST }}"
PREVIEW_LMS_HOST: "preview.{{ LMS_HOST }}"
CONTACT_EMAIL: "contact@{{ LMS_HOST }}" CONTACT_EMAIL: "contact@{{ LMS_HOST }}"
OPENEDX_AWS_ACCESS_KEY: ""
OPENEDX_AWS_SECRET_ACCESS_KEY: ""
DEV_PROJECT_NAME: "{{ TUTOR_APP }}_dev" DEV_PROJECT_NAME: "{{ TUTOR_APP }}_dev"
DOCKER_REGISTRY: "docker.io/" DOCKER_REGISTRY: "docker.io/"
DOCKER_IMAGE_OPENEDX: "{{ DOCKER_REGISTRY }}overhangio/openedx:{{ TUTOR_VERSION }}" 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_PERMISSIONS: "{{ DOCKER_REGISTRY }}overhangio/openedx-permissions:{{ TUTOR_VERSION }}"
DOCKER_IMAGE_REDIS: "{{ DOCKER_REGISTRY }}redis:6.2.6" DOCKER_IMAGE_REDIS: "{{ DOCKER_REGISTRY }}redis:6.2.6"
DOCKER_IMAGE_SMTP: "{{ DOCKER_REGISTRY }}devture/exim-relay:4.94.2-r0-4" DOCKER_IMAGE_SMTP: "{{ DOCKER_REGISTRY }}devture/exim-relay:4.94.2-r0-4"
LOCAL_PROJECT_NAME: "{{ TUTOR_APP }}_local"
ELASTICSEARCH_HOST: "elasticsearch" ELASTICSEARCH_HOST: "elasticsearch"
ELASTICSEARCH_PORT: 9200 ELASTICSEARCH_PORT: 9200
ELASTICSEARCH_SCHEME: "http" ELASTICSEARCH_SCHEME: "http"
@ -45,14 +27,17 @@ ENABLE_WEB_PROXY: true
JWT_COMMON_AUDIENCE: "openedx" JWT_COMMON_AUDIENCE: "openedx"
JWT_COMMON_ISSUER: "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/oauth2" JWT_COMMON_ISSUER: "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}/oauth2"
JWT_COMMON_SECRET_KEY: "{{ OPENEDX_SECRET_KEY }}" JWT_COMMON_SECRET_KEY: "{{ OPENEDX_SECRET_KEY }}"
JWT_RSA_PRIVATE_KEY: "{{ 2048|rsa_private_key }}"
K8S_NAMESPACE: "openedx" K8S_NAMESPACE: "openedx"
LANGUAGE_CODE: "en" LANGUAGE_CODE: "en"
LMS_HOST: "www.myopenedx.com"
LOCAL_PROJECT_NAME: "{{ TUTOR_APP }}_local"
MONGODB_HOST: "mongodb" MONGODB_HOST: "mongodb"
MONGODB_DATABASE: "openedx" MONGODB_DATABASE: "openedx"
MONGODB_PORT: 27017 MONGODB_PORT: 27017
MONGODB_USERNAME: "" MONGODB_USERNAME: ""
MONGODB_PASSWORD: "" MONGODB_PASSWORD: ""
OPENEDX_AWS_ACCESS_KEY: ""
OPENEDX_AWS_SECRET_ACCESS_KEY: ""
OPENEDX_CACHE_REDIS_DB: 1 OPENEDX_CACHE_REDIS_DB: 1
OPENEDX_CELERY_REDIS_DB: 0 OPENEDX_CELERY_REDIS_DB: 0
OPENEDX_CMS_UWSGI_WORKERS: 2 OPENEDX_CMS_UWSGI_WORKERS: 2
@ -73,6 +58,13 @@ REDIS_HOST: "redis"
REDIS_PORT: 6379 REDIS_PORT: 6379
REDIS_USERNAME: "" REDIS_USERNAME: ""
REDIS_PASSWORD: "" 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_HOST: "smtp"
SMTP_PORT: 8025 SMTP_PORT: 8025
SMTP_USERNAME: "" SMTP_USERNAME: ""