2019-01-22 20:25:04 +00:00
|
|
|
import codecs
|
2019-07-10 08:20:43 +00:00
|
|
|
from copy import deepcopy
|
2019-01-22 20:25:04 +00:00
|
|
|
import os
|
|
|
|
import shutil
|
|
|
|
|
|
|
|
import jinja2
|
2019-08-20 15:04:18 +00:00
|
|
|
import pkg_resources
|
2019-01-22 20:25:04 +00:00
|
|
|
|
|
|
|
from . import exceptions
|
2019-05-11 22:10:14 +00:00
|
|
|
from . import fmt
|
2019-05-29 09:14:06 +00:00
|
|
|
from . import plugins
|
2019-01-22 20:25:04 +00:00
|
|
|
from . import utils
|
2019-03-18 16:26:37 +00:00
|
|
|
from .__about__ import __version__
|
2019-01-22 20:25:04 +00:00
|
|
|
|
|
|
|
|
2019-08-20 15:04:18 +00:00
|
|
|
TEMPLATES_ROOT = pkg_resources.resource_filename("tutor", "templates")
|
2019-03-18 16:26:37 +00:00
|
|
|
VERSION_FILENAME = "version"
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-05-11 17:31:18 +00:00
|
|
|
class Renderer:
|
|
|
|
ENVIRONMENT = None
|
2019-07-10 07:19:20 +00:00
|
|
|
ENVIRONMENT_CONFIG = None
|
2019-05-11 17:31:18 +00:00
|
|
|
|
|
|
|
@classmethod
|
2019-06-05 13:43:51 +00:00
|
|
|
def environment(cls, config):
|
2020-01-08 11:20:24 +00:00
|
|
|
def patch(name, separator="\n", suffix=""):
|
|
|
|
return cls.__render_patch(config, name, separator=separator, suffix=suffix)
|
|
|
|
|
2019-07-10 07:19:20 +00:00
|
|
|
if cls.ENVIRONMENT_CONFIG != config:
|
2020-01-08 11:20:24 +00:00
|
|
|
# Load template roots
|
2019-06-05 13:43:51 +00:00
|
|
|
template_roots = [TEMPLATES_ROOT]
|
2020-01-14 09:30:41 +00:00
|
|
|
for _, plugin_template_root in plugins.iter_template_roots(config):
|
|
|
|
template_roots.append(plugin_template_root)
|
2020-01-08 11:20:24 +00:00
|
|
|
|
|
|
|
# Create environment
|
2019-05-11 22:11:44 +00:00
|
|
|
environment = jinja2.Environment(
|
2019-06-05 13:43:51 +00:00
|
|
|
loader=jinja2.FileSystemLoader(template_roots),
|
2019-05-11 17:31:18 +00:00
|
|
|
undefined=jinja2.StrictUndefined,
|
|
|
|
)
|
2019-05-11 22:11:44 +00:00
|
|
|
environment.filters["random_string"] = utils.random_string
|
2019-05-21 10:34:29 +00:00
|
|
|
environment.filters["common_domain"] = utils.common_domain
|
2019-11-22 08:20:17 +00:00
|
|
|
environment.filters["list_if"] = utils.list_if
|
2019-05-11 22:11:44 +00:00
|
|
|
environment.filters["reverse_host"] = utils.reverse_host
|
2019-05-09 07:51:06 +00:00
|
|
|
environment.filters["walk_templates"] = walk_templates
|
2020-01-08 11:20:24 +00:00
|
|
|
environment.globals["patch"] = patch
|
2019-05-20 17:09:58 +00:00
|
|
|
environment.globals["TUTOR_VERSION"] = __version__
|
2019-07-10 08:20:43 +00:00
|
|
|
|
2020-01-08 11:20:24 +00:00
|
|
|
# Update environment singleton
|
2019-07-10 08:20:43 +00:00
|
|
|
cls.ENVIRONMENT_CONFIG = deepcopy(config)
|
2019-05-11 22:11:44 +00:00
|
|
|
cls.ENVIRONMENT = environment
|
|
|
|
|
2019-05-11 17:31:18 +00:00
|
|
|
return cls.ENVIRONMENT
|
|
|
|
|
2019-05-11 22:11:44 +00:00
|
|
|
@classmethod
|
|
|
|
def reset(cls):
|
|
|
|
cls.ENVIRONMENT = None
|
2019-07-10 08:20:43 +00:00
|
|
|
cls.ENVIRONMENT_CONFIG = None
|
2019-05-11 22:11:44 +00:00
|
|
|
|
2019-05-11 17:31:18 +00:00
|
|
|
@classmethod
|
|
|
|
def render_str(cls, config, text):
|
2019-06-05 13:43:51 +00:00
|
|
|
template = cls.environment(config).from_string(text)
|
2019-05-29 09:14:06 +00:00
|
|
|
return cls.__render(config, template)
|
2019-05-11 22:11:44 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def render_file(cls, config, path):
|
|
|
|
try:
|
2019-06-05 13:43:51 +00:00
|
|
|
template = cls.environment(config).get_template(path)
|
2020-01-08 11:20:24 +00:00
|
|
|
except Exception:
|
2019-05-29 09:14:06 +00:00
|
|
|
fmt.echo_error("Error loading template " + path)
|
|
|
|
raise
|
|
|
|
try:
|
|
|
|
return cls.__render(config, template)
|
2019-05-11 22:11:44 +00:00
|
|
|
except (jinja2.exceptions.TemplateError, exceptions.TutorError):
|
2019-05-11 22:10:14 +00:00
|
|
|
fmt.echo_error("Error rendering template " + path)
|
2019-05-11 22:11:44 +00:00
|
|
|
raise
|
|
|
|
except Exception:
|
2019-05-11 22:10:14 +00:00
|
|
|
fmt.echo_error("Unknown error rendering template " + path)
|
2019-05-11 22:11:44 +00:00
|
|
|
raise
|
|
|
|
|
|
|
|
@classmethod
|
2019-05-29 09:14:06 +00:00
|
|
|
def __render(cls, config, template):
|
2019-05-11 17:31:18 +00:00
|
|
|
try:
|
2020-01-08 11:20:24 +00:00
|
|
|
return template.render(**config)
|
2019-05-11 17:31:18 +00:00
|
|
|
except jinja2.exceptions.UndefinedError as e:
|
|
|
|
raise exceptions.TutorError(
|
|
|
|
"Missing configuration value: {}".format(e.args[0])
|
|
|
|
)
|
|
|
|
|
2019-05-29 09:14:06 +00:00
|
|
|
@classmethod
|
|
|
|
def __render_patch(cls, config, name, separator="\n", suffix=""):
|
2020-01-08 11:20:24 +00:00
|
|
|
"""
|
|
|
|
Render calls to {{ patch("...") }} in environment templates from plugin patches.
|
|
|
|
"""
|
2019-05-29 09:14:06 +00:00
|
|
|
patches = []
|
|
|
|
for plugin, patch in plugins.iter_patches(config, name):
|
2019-06-05 13:43:51 +00:00
|
|
|
patch_template = cls.environment(config).from_string(patch)
|
2019-05-29 09:14:06 +00:00
|
|
|
try:
|
|
|
|
patches.append(patch_template.render(**config))
|
|
|
|
except jinja2.exceptions.UndefinedError as e:
|
|
|
|
raise exceptions.TutorError(
|
|
|
|
"Missing configuration value: {} in patch '{}' from plugin {}".format(
|
|
|
|
e.args[0], name, plugin
|
|
|
|
)
|
|
|
|
)
|
|
|
|
rendered = separator.join(patches)
|
|
|
|
if rendered:
|
|
|
|
rendered += suffix
|
|
|
|
return rendered
|
|
|
|
|
2019-05-11 17:31:18 +00:00
|
|
|
|
2019-06-05 13:43:51 +00:00
|
|
|
def save(root, config):
|
|
|
|
render_full(root, config)
|
|
|
|
fmt.echo_info("Environment generated in {}".format(base_dir(root)))
|
|
|
|
|
|
|
|
|
2019-03-18 16:26:37 +00:00
|
|
|
def render_full(root, config):
|
|
|
|
"""
|
|
|
|
Render the full environment, including version information.
|
|
|
|
"""
|
2019-10-22 14:13:50 +00:00
|
|
|
for subdir in ["android", "apps", "build", "dev", "k8s", "local", "webui"]:
|
2019-05-21 10:34:29 +00:00
|
|
|
save_subdir(subdir, root, config)
|
2020-01-14 09:30:41 +00:00
|
|
|
for plugin, path in plugins.iter_template_roots(config):
|
2019-07-02 20:16:44 +00:00
|
|
|
save_plugin_templates(plugin, path, root, config)
|
2019-05-21 10:34:29 +00:00
|
|
|
save_file(VERSION_FILENAME, root, config)
|
2019-05-09 07:51:06 +00:00
|
|
|
save_file("kustomization.yml", root, config)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-07-02 20:16:44 +00:00
|
|
|
def save_plugin_templates(plugin, plugin_path, root, config):
|
|
|
|
"""
|
|
|
|
Save plugin templates to plugins/<plugin name>/*.
|
|
|
|
Only the "apps" and "build" subfolders are rendered.
|
|
|
|
"""
|
|
|
|
for subdir in ["apps", "build"]:
|
|
|
|
path = os.path.join(plugin_path, plugin, subdir)
|
|
|
|
for src in walk_templates(path, root=plugin_path):
|
|
|
|
dst = pathjoin(root, "plugins", src)
|
|
|
|
rendered = render_file(config, src)
|
|
|
|
write_to(rendered, dst)
|
|
|
|
|
|
|
|
|
2019-05-21 10:34:29 +00:00
|
|
|
def save_subdir(subdir, root, config):
|
2019-01-22 20:25:04 +00:00
|
|
|
"""
|
2019-05-11 17:31:18 +00:00
|
|
|
Render the templates located in `subdir` and store them with the same
|
2019-01-22 20:25:04 +00:00
|
|
|
hierarchy at `root`.
|
|
|
|
"""
|
2019-05-11 17:31:18 +00:00
|
|
|
for path in walk_templates(subdir):
|
2019-05-21 10:34:29 +00:00
|
|
|
save_file(path, root, config)
|
|
|
|
|
|
|
|
|
|
|
|
def save_file(path, root, config):
|
|
|
|
"""
|
|
|
|
Render the template located in `path` and store it with the same hierarchy at `root`.
|
|
|
|
"""
|
|
|
|
dst = pathjoin(root, path)
|
|
|
|
rendered = render_file(config, path)
|
2019-07-02 20:16:44 +00:00
|
|
|
write_to(rendered, dst)
|
|
|
|
|
|
|
|
|
|
|
|
def write_to(content, path):
|
|
|
|
utils.ensure_file_directory_exists(path)
|
|
|
|
with open(path, "w") as of:
|
|
|
|
of.write(content)
|
2019-03-22 17:50:16 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-05-11 19:20:09 +00:00
|
|
|
def render_file(config, *path):
|
2019-05-11 17:31:18 +00:00
|
|
|
"""
|
|
|
|
Return the rendered contents of a template.
|
|
|
|
"""
|
2019-05-11 22:11:44 +00:00
|
|
|
return Renderer.render_file(config, os.path.join(*path))
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-01-22 20:25:04 +00:00
|
|
|
def render_dict(config):
|
|
|
|
"""
|
|
|
|
Render the values from the dict. This is useful for rendering the default
|
|
|
|
values from config.yml.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
config (dict)
|
|
|
|
"""
|
|
|
|
rendered = {}
|
|
|
|
for key, value in config.items():
|
|
|
|
if isinstance(value, str):
|
2019-03-22 17:50:16 +00:00
|
|
|
rendered[key] = render_str(config, value)
|
2019-01-22 20:25:04 +00:00
|
|
|
else:
|
|
|
|
rendered[key] = value
|
|
|
|
for k, v in rendered.items():
|
|
|
|
config[k] = v
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-06-05 13:43:51 +00:00
|
|
|
def render_unknown(config, value):
|
|
|
|
if isinstance(value, str):
|
|
|
|
return render_str(config, value)
|
|
|
|
return value
|
|
|
|
|
2019-06-05 17:57:30 +00:00
|
|
|
|
2019-03-22 17:50:16 +00:00
|
|
|
def render_str(config, text):
|
2019-01-22 20:25:04 +00:00
|
|
|
"""
|
|
|
|
Args:
|
|
|
|
text (str)
|
|
|
|
config (dict)
|
|
|
|
|
|
|
|
Return:
|
|
|
|
substituted (str)
|
|
|
|
"""
|
2019-05-11 17:31:18 +00:00
|
|
|
return Renderer.render_str(config, text)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-05-11 17:31:18 +00:00
|
|
|
def copy_subdir(subdir, root):
|
2019-01-22 20:25:04 +00:00
|
|
|
"""
|
2019-05-11 17:31:18 +00:00
|
|
|
Copy the templates located in `subdir` and store them with the same hierarchy
|
|
|
|
at `root`. No rendering is done here.
|
2019-01-22 20:25:04 +00:00
|
|
|
"""
|
2019-05-11 17:31:18 +00:00
|
|
|
for path in walk_templates(subdir):
|
|
|
|
src = os.path.join(TEMPLATES_ROOT, path)
|
|
|
|
dst = pathjoin(root, path)
|
2019-05-11 22:11:44 +00:00
|
|
|
utils.ensure_file_directory_exists(dst)
|
2019-03-29 13:24:59 +00:00
|
|
|
shutil.copy(src, dst)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-06-05 13:43:51 +00:00
|
|
|
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"
|
2019-06-05 17:45:22 +00:00
|
|
|
" tutor config save"
|
2019-06-05 13:43:51 +00:00
|
|
|
)
|
|
|
|
fmt.echo_alert(message.format(base_dir(root), version(root), __version__))
|
|
|
|
|
|
|
|
|
2019-03-18 16:26:37 +00:00
|
|
|
def is_up_to_date(root):
|
2019-05-11 17:31:18 +00:00
|
|
|
"""
|
|
|
|
Check if the currently rendered version is equal to the current tutor version.
|
|
|
|
"""
|
2019-03-18 16:26:37 +00:00
|
|
|
return version(root) == __version__
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-03-18 16:26:37 +00:00
|
|
|
def version(root):
|
|
|
|
"""
|
2019-05-11 17:31:18 +00:00
|
|
|
Return the current environment version. If the current environment has no version,
|
|
|
|
return "0".
|
2019-03-18 16:26:37 +00:00
|
|
|
"""
|
|
|
|
path = pathjoin(root, VERSION_FILENAME)
|
|
|
|
if not os.path.exists(path):
|
|
|
|
return "0"
|
|
|
|
return open(path).read().strip()
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-01-22 20:25:04 +00:00
|
|
|
def read(*path):
|
|
|
|
"""
|
2019-05-11 17:31:18 +00:00
|
|
|
Read raw content of template located at `path`.
|
2019-01-22 20:25:04 +00:00
|
|
|
"""
|
2019-03-22 17:50:16 +00:00
|
|
|
src = template_path(*path)
|
2019-05-05 09:45:24 +00:00
|
|
|
with codecs.open(src, encoding="utf-8") as fi:
|
2019-01-22 20:25:04 +00:00
|
|
|
return fi.read()
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-07-02 20:16:44 +00:00
|
|
|
def walk_templates(subdir, root=TEMPLATES_ROOT):
|
2019-01-22 20:25:04 +00:00
|
|
|
"""
|
2019-05-11 17:31:18 +00:00
|
|
|
Iterate on the template files from `templates/<subdir>`.
|
2019-01-22 20:25:04 +00:00
|
|
|
|
|
|
|
Yield:
|
2019-05-11 17:31:18 +00:00
|
|
|
path: template path relative to the template root
|
2019-01-22 20:25:04 +00:00
|
|
|
"""
|
2019-05-11 17:31:18 +00:00
|
|
|
for dirpath, _, filenames in os.walk(template_path(subdir)):
|
2019-03-29 13:24:59 +00:00
|
|
|
if not is_part_of_env(dirpath):
|
|
|
|
continue
|
2019-01-22 20:25:04 +00:00
|
|
|
for filename in filenames:
|
2019-07-02 20:16:44 +00:00
|
|
|
path = os.path.join(os.path.relpath(dirpath, root), filename)
|
2019-05-11 17:31:18 +00:00
|
|
|
if is_part_of_env(path):
|
|
|
|
yield path
|
2019-03-29 13:24:59 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-03-29 13:24:59 +00:00
|
|
|
def is_part_of_env(path):
|
2019-05-11 17:31:18 +00:00
|
|
|
"""
|
|
|
|
Determines whether a file should be rendered or not.
|
|
|
|
"""
|
2019-03-29 13:24:59 +00:00
|
|
|
basename = os.path.basename(path)
|
2020-01-14 09:30:41 +00:00
|
|
|
is_excluded = False
|
|
|
|
is_excluded = is_excluded or basename.startswith(".") or basename.endswith(".pyc")
|
|
|
|
is_excluded = is_excluded or basename == "__pycache__" or basename == "partials"
|
|
|
|
return not is_excluded
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-03-22 17:50:16 +00:00
|
|
|
def template_path(*path):
|
2019-05-11 17:31:18 +00:00
|
|
|
"""
|
|
|
|
Return the template file's absolute path.
|
|
|
|
"""
|
2019-03-22 17:50:16 +00:00
|
|
|
return os.path.join(TEMPLATES_ROOT, *path)
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-01-22 20:25:04 +00:00
|
|
|
def data_path(root, *path):
|
2019-05-11 17:31:18 +00:00
|
|
|
"""
|
|
|
|
Return the file's absolute path inside the data directory.
|
|
|
|
"""
|
|
|
|
return os.path.join(root_dir(root), "data", *path)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-05-11 17:31:18 +00:00
|
|
|
def pathjoin(root, *path):
|
|
|
|
"""
|
|
|
|
Return the file's absolute path inside the environment.
|
|
|
|
"""
|
|
|
|
return os.path.join(base_dir(root), *path)
|
2019-03-18 16:26:37 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-03-18 16:26:37 +00:00
|
|
|
def base_dir(root):
|
2019-05-11 17:31:18 +00:00
|
|
|
"""
|
|
|
|
Return the environment base directory.
|
|
|
|
"""
|
|
|
|
return os.path.join(root_dir(root), "env")
|
|
|
|
|
|
|
|
|
|
|
|
def root_dir(root):
|
|
|
|
"""
|
|
|
|
Return the project root directory.
|
|
|
|
"""
|
|
|
|
return os.path.abspath(root)
|