diff --git a/README.rst b/README.rst
index 7b0ba278..425904d6 100644
--- a/README.rst
+++ b/README.rst
@@ -1,20 +1,51 @@
drydock: a flexible manifest builder for Open edX
=================================================
+**⚠️ Warning**: drydock is currently in an alpha stage and may suffer substantial changes
+while it transitions in to a stable stage.
+
+
Installation
------------
::
tvm plugins install -e git+https://github.com/edunext/drydock#egg=drydock
+ tutor plugins enable drydock
-Usage
------
+Getting started
+---------------
-::
+Drydock aims to offer flexibility on the kind of environment you want to generate, for that reason
+you can chose different kind of implementations (of renderers?) depending on your needs. At the
+moment Drydock ships with a basic manifest builder that wraps the Kubernetes generated files by tutor
+in a Kustomize application with some useful extra resources.
- tutor plugins enable drydock
- tutor drydock save -r drydock/references/tutor_v13.yml
+To use drydock just use the reference that defines the implementation you want drydock to use and run:
+
+.. code:: bash
+
+ tutor drydock save -r reference.yml
+
+An example reference would look something like this:
+
+.. code:: yaml
+
+ drydock:
+ builder_class: drydock.manifest_builder.application.manifest_builder.ManifestBuilder
+ config_class: drydock.manifest_builder.infrastructure.tutor_config.TutorConfig
+ manifest_class: drydock.manifest_builder.infrastructure.tutor_based_manifests.BaseManifests
+ manifest_options:
+ output: "drydock-environment"
+
+This will render the default drydock environment on the `drydock-environment` directory, allowing
+you to check the files onto version control and use tools for continuous deployment such as
+Flux or ArgoCD. The default environment is generated using Tutor and its templates, as a result
+it should be compatible with all the plugins, variables and patches.
+
+**Note:** If you are using module or yaml plugins in tutor and set ``manifest_options.output=env``
+you will have to define your ``TUTOR_PLUGINS_ROOT`` outside of your ``TUTOR_ROOT`` because
+drydock will override your tutor env and erase your plugins.
Rationale
diff --git a/drydock/manifest_builder/domain/config.py b/drydock/manifest_builder/domain/config.py
index 809d05ec..af6f17ca 100644
--- a/drydock/manifest_builder/domain/config.py
+++ b/drydock/manifest_builder/domain/config.py
@@ -4,5 +4,9 @@
class DrydockConfig(ABC):
@abstractmethod
- def get_data() -> dict:
+ def get_data(self) -> dict:
+ pass
+
+ @abstractmethod
+ def get_root(self) -> str:
pass
diff --git a/drydock/manifest_builder/domain/manifest_repository.py b/drydock/manifest_builder/domain/manifest_repository.py
index 493c28b7..09662c6b 100644
--- a/drydock/manifest_builder/domain/manifest_repository.py
+++ b/drydock/manifest_builder/domain/manifest_repository.py
@@ -5,5 +5,5 @@
class ManifestRepository(ABC):
@abstractmethod
- def save(config: DrydockConfig) -> None:
+ def save(self, config: DrydockConfig) -> None:
pass
diff --git a/drydock/manifest_builder/infrastructure/tutor_based_manifests.py b/drydock/manifest_builder/infrastructure/tutor_based_manifests.py
new file mode 100644
index 00000000..effdb15e
--- /dev/null
+++ b/drydock/manifest_builder/infrastructure/tutor_based_manifests.py
@@ -0,0 +1,97 @@
+"""
+Collection of tutor based renderers for Kubernetes manifests.
+"""
+import os
+import shutil
+import tempfile
+from os.path import join as path_join
+
+import pkg_resources
+from tutor import env as tutor_env
+from tutor import fmt, hooks
+
+from drydock.manifest_builder.domain.config import DrydockConfig
+from drydock.manifest_builder.domain.manifest_repository import ManifestRepository
+
+
+class BaseManifests(ManifestRepository):
+ """Baseline of Kubernetes configuration repository.
+
+ Generates an environment based on Tutor with the relevant Kubernetes configuration
+ to be tracked by version control.
+ """
+ TEMPLATE_ROOT = "kustomized/tutor13"
+ TEMPLATE_TARGETS = [
+ f"{TEMPLATE_ROOT}/base",
+ f"{TEMPLATE_ROOT}/extensions",
+ f"{TEMPLATE_ROOT}/kustomization.yml",
+ ]
+
+ def __init__(self, options: dict) -> None:
+ """Initialize the class based on the `manifest_options` from the reference.
+
+ Parameters
+ ----------
+ options: dict
+ Defines additional configuration options for the generations of the
+ configuration repository.
+
+ - ["output"]: Name of the directory to store the manifests.
+ """
+ self.output_dir = options.get("output", "drydock-env")
+
+ def render(self, root: str, config: DrydockConfig) -> None:
+ """Register drydock custom templates and render a tutor env."""
+ with hooks.Contexts.APP("drydock-base").enter():
+ hooks.Filters.ENV_TEMPLATE_ROOTS.add_item(pkg_resources.resource_filename("drydock", "templates"))
+ hooks.Filters.ENV_TEMPLATE_TARGETS.add_items(
+ [
+ (target, "drydock") for target in self.TEMPLATE_TARGETS
+ ],
+ )
+ tutor_env.save(root, config.get_data())
+ hooks.Filters.ENV_TEMPLATE_ROOTS.clear(context=hooks.Contexts.APP("drydock-base").name)
+ hooks.Filters.ENV_TEMPLATE_TARGETS.clear(context=hooks.Contexts.APP("drydock-base").name)
+
+ def relocate_env(self, src: str, dst: str) -> None:
+ """Moves the drydock rendered templates and tutor plugins to src.
+
+ At the moment we render a full tutor env with our templates under
+ 'env/drydock/default'. The final drydock env is the one in 'env/drydock'
+ **plus** the plugin directory that has to be manually relocated to
+ 'env/drydock/base/plugins'.
+
+ Parameters
+ ----------
+ src: str
+ The inital path where the full tutor env was rendered.
+ dst: str
+ The path to save the final drydock env.
+ """
+ base_dir = path_join(src, "env/drydock", f"{self.TEMPLATE_ROOT}")
+ plugins_dir = path_join(src, "env/plugins")
+ shutil.move(base_dir, dst)
+ if os.path.exists(plugins_dir):
+ shutil.move(plugins_dir, path_join(dst, "base"))
+
+ def save(self, config: DrydockConfig) -> None:
+ """Creates an alternative tutor env with the base openedx installation.
+
+ The generated env consists of a Kustomize application that uses the original
+ tutor env as a base in addition to an `extensions` overlay to include additional
+ resources.
+
+ Parameters
+ ----------
+ config: DrydockConfig
+ A Tutor configuration extension.
+ """
+ tutor_root = config.get_root()
+ dst = path_join(tutor_root, self.output_dir)
+ with tempfile.TemporaryDirectory() as tmpdir:
+ src = path_join(tmpdir)
+ self.render(src, config)
+ shutil.rmtree(path=dst, ignore_errors=True)
+ self.relocate_env(src, dst)
+
+ fmt.echo_info(f"Drydock environment generated in {dst}")
diff --git a/drydock/manifest_builder/infrastructure/tutor_config.py b/drydock/manifest_builder/infrastructure/tutor_config.py
index 56bf40f6..2328a19f 100644
--- a/drydock/manifest_builder/infrastructure/tutor_config.py
+++ b/drydock/manifest_builder/infrastructure/tutor_config.py
@@ -1,14 +1,65 @@
-from abc import abstractmethod
-from tutor import config as tutor_config
+import pkg_resources
+import tutor
+import yaml
+
from drydock.manifest_builder.domain.config import DrydockConfig
class TutorConfig(DrydockConfig):
-
def __init__(self, context, options: dict):
self.context = context
self.options = options
def get_data(self) -> dict:
- config = tutor_config.load_full(self.context.obj.root)
+ config = tutor.config.load_full(self.context.obj.root)
return config
+
+ def get_root(self) -> dict:
+ return self.context.obj.root
+
+
+class TutorExtendedConfig(DrydockConfig):
+ """Drydock configuration based on tutor."""
+ DEFAULT_TEMPLATE_SET = "kustomized/tutor13"
+
+ def __init__(self, context, options: dict):
+ """Initialize the class based on the `config_options` from the manifest.
+
+ Parameters
+ ----------
+ context: clic.core.Context
+ context of the current Tutor command.
+ options: dict
+
+ - ["template_set"]: template set to render default values from.
+ """
+ self.context = context
+ self.options = options
+
+ def get_data(self) -> dict:
+ """Return tutor config values using a template set defaults as fallback.
+
+ Retrieve the Tutor configuration values for the current TUTOR_ROOT and
+ use the the values in `defaults.yml` of the chosen template set
+
+ Returns
+ -------
+ base: dict
+ Tutor configuration values taken from the config.yml at the TUTOR_ROOT
+ using the defaults from the template set as a fallback.
+ """
+ template_set = self.options.get("template_set", self.DEFAULT_TEMPLATE_SET)
+
+ try:
+ defaults_path = pkg_resources.resource_filename("drydock", f"templates/{template_set}/defaults.yml")
+ with open(defaults_path, encoding="utf-8") as file:
+ base = yaml.safe_load(file)
+ except FileNotFoundError:
+ base = {}
+
+ config = tutor.config.load_full(self.get_root())
+ base.update(config)
+ return base
+
+ def get_root(self) -> str:
+ return self.context.obj.root
diff --git a/drydock/references/kustomized_v13.yml b/drydock/references/kustomized_v13.yml
new file mode 100644
index 00000000..2306f1c1
--- /dev/null
+++ b/drydock/references/kustomized_v13.yml
@@ -0,0 +1,10 @@
+# The kustomized_v13 reference is a Kustomize based instalation that uses the environment
+# generated by tutor v13.3.1 as a Base with additional resources such as an ingress and
+# a flower deployment used as overlays.
+#
+drydock:
+ builder_class: drydock.manifest_builder.application.manifest_builder.ManifestBuilder
+ config_class: drydock.manifest_builder.infrastructure.tutor_config.TutorConfig
+ manifest_class: drydock.manifest_builder.infrastructure.tutor_based_manifests.BaseManifests
+ manifest_options:
+ output: "drydock-environment"
diff --git a/drydock/templates/README.md b/drydock/templates/README.md
new file mode 100644
index 00000000..544f0e0a
--- /dev/null
+++ b/drydock/templates/README.md
@@ -0,0 +1,32 @@
+# Drydock Templates
+
+The inventory of template sets used by drydock.
+
+## Template list
+
+1. `kustomized/tutor13`: Kubernetes focused template set. Uses the templates from Tutor v13 as a Kustomize
+ base (rendered in the `base` directory) and adds additional resources as an overlay in the
+ `extensions` directory.
+ The additional resources include:
+
+ ```yaml
+ - flowers.yml # A flowers deployment to track celery tasks.
+ - hpa.yml # An Horizontal Pod Autoscaler for lms, cms and workers.
+ - ingress.yml # An ingress with certmanager to use instead of caddy as web proxy.
+ ```
+
+ The list of variables with their default values can be found in [defaults.yml](kustomized/tutor13/defaults.yml)
+
+
+ List of patches
+
+ ```
+ {{ patch("drydock-kustomization-resources")}}
+ {{ patch("drydock-kustomization-patches")}}
+ {{ patch("drydock-overrides") }}
+ ```
+
+
+2. `tutor/v13`: Small subset of templates from Tutor v13. Mostly as a proof con concept.
+
+3. `tutor/v13`: Small subset of templates from Tutor v12. Mostly as a proof con concept.
diff --git a/drydock/templates/kustomized/tutor13/base/apps/caddy/Caddyfile b/drydock/templates/kustomized/tutor13/base/apps/caddy/Caddyfile
new file mode 100644
index 00000000..cfb82fd0
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/apps/caddy/Caddyfile
@@ -0,0 +1,62 @@
+# Global configuration
+{
+ {{ patch("caddyfile-global")|indent(4) }}
+}
+
+# proxy directive snippet (with logging) to be used as follows:
+#
+# import proxy "containername:port"
+(proxy) {
+ log {
+ output stdout
+ format filter {
+ wrap json
+ fields {
+ common_log delete
+ request>headers delete
+ resp_headers delete
+ tls delete
+ }
+ }
+ }
+
+ reverse_proxy {args.0} {
+ header_up X-Forwarded-Port {{ 443 if ENABLE_HTTPS else 80 }}
+ }
+}
+
+{{ LMS_HOST }}{$default_site_port}, {{ PREVIEW_LMS_HOST }}{$default_site_port} {
+ @favicon_matcher {
+ path_regexp ^/favicon.ico$
+ }
+ rewrite @favicon_matcher /theming/asset/images/favicon.ico
+
+ # Limit profile image upload size
+ request_body /api/profile_images/*/*/upload {
+ max_size 1MB
+ }
+ request_body {
+ max_size 4MB
+ }
+
+ import proxy "lms:8000"
+
+ {{ patch("caddyfile-lms")|indent(4) }}
+}
+
+{{ CMS_HOST }}{$default_site_port} {
+ @favicon_matcher {
+ path_regexp ^/favicon.ico$
+ }
+ rewrite @favicon_matcher /theming/asset/images/favicon.ico
+
+ request_body {
+ max_size 250MB
+ }
+
+ import proxy "cms:8000"
+
+ {{ patch("caddyfile-cms")|indent(4) }}
+}
+
+{{ patch("caddyfile") }}
diff --git a/drydock/templates/kustomized/tutor13/base/apps/openedx/config/cms.env.json b/drydock/templates/kustomized/tutor13/base/apps/openedx/config/cms.env.json
new file mode 100644
index 00000000..68aca49f
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/apps/openedx/config/cms.env.json
@@ -0,0 +1,84 @@
+{
+ "SITE_NAME": "{{ CMS_HOST }}",
+ "BOOK_URL": "",
+ "LOG_DIR": "/openedx/data/logs",
+ "LOGGING_ENV": "sandbox",
+ "OAUTH_OIDC_ISSUER": "{{ JWT_COMMON_ISSUER }}",
+ "PLATFORM_NAME": "{{ PLATFORM_NAME }}",
+ "FEATURES": {
+ {{ patch("common-env-features", separator=",\n", suffix=",")|indent(4) }}
+ {{ patch("cms-env-features", separator=",\n", suffix=",")|indent(4) }}
+ "CERTIFICATES_HTML_VIEW": true,
+ "PREVIEW_LMS_BASE": "{{ PREVIEW_LMS_HOST }}",
+ "ENABLE_COURSEWARE_INDEX": true,
+ "ENABLE_CSMH_EXTENDED": false,
+ "ENABLE_LEARNER_RECORDS": false,
+ "ENABLE_LIBRARY_INDEX": true,
+ "MILESTONES_APP": true,
+ "ENABLE_PREREQUISITE_COURSES": true
+ },
+ "LMS_ROOT_URL": "{{ "https" if ENABLE_HTTPS else "http" }}://{{ LMS_HOST }}",
+ "CMS_ROOT_URL": "{{ "https" if ENABLE_HTTPS else "http" }}://{{ CMS_HOST }}",
+ "CMS_BASE": "{{ CMS_HOST }}",
+ "LMS_BASE": "{{ LMS_HOST }}",
+ "CONTACT_EMAIL": "{{ CONTACT_EMAIL }}",
+ "CELERY_BROKER_TRANSPORT": "redis",
+ "CELERY_BROKER_HOSTNAME": "{{ REDIS_HOST }}:{{ REDIS_PORT }}",
+ "CELERY_BROKER_VHOST": "{{ OPENEDX_CELERY_REDIS_DB }}",
+ "CELERY_BROKER_USER": "{{ REDIS_USERNAME }}",
+ "CELERY_BROKER_PASSWORD": "{{ REDIS_PASSWORD }}",
+ "ALTERNATE_WORKER_QUEUES": "lms",
+ "ENABLE_COMPREHENSIVE_THEMING": true,
+ "COMPREHENSIVE_THEME_DIRS": ["/openedx/themes"],
+ "STATIC_ROOT_BASE": "/openedx/staticfiles",
+ "EMAIL_BACKEND": "django.core.mail.backends.smtp.EmailBackend",
+ "EMAIL_HOST": "{{ SMTP_HOST }}",
+ "EMAIL_PORT": {{ SMTP_PORT }},
+ "EMAIL_USE_TLS": {{ "true" if SMTP_USE_TLS else "false" }},
+ "HTTPS": "{{ "on" if ENABLE_HTTPS else "off" }}",
+ "LANGUAGE_CODE": "{{ LANGUAGE_CODE }}",
+ "SESSION_COOKIE_DOMAIN": "{{ CMS_HOST }}",
+ {{ patch("cms-env", separator=",\n", suffix=",")|indent(2) }}
+ "CACHES": {
+ "default": {
+ "KEY_PREFIX": "default",
+ "VERSION": "1",
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://{% if REDIS_USERNAME and REDIS_PASSWORD %}{{ REDIS_USERNAME }}:{{ REDIS_PASSWORD }}{% endif %}@{{ REDIS_HOST }}:{{ REDIS_PORT }}/{{ OPENEDX_CACHE_REDIS_DB }}"
+ },
+ "general": {
+ "KEY_PREFIX": "general",
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://{% if REDIS_USERNAME and REDIS_PASSWORD %}{{ REDIS_USERNAME }}:{{ REDIS_PASSWORD }}{% endif %}@{{ REDIS_HOST }}:{{ REDIS_PORT }}/{{ OPENEDX_CACHE_REDIS_DB }}"
+ },
+ "mongo_metadata_inheritance": {
+ "KEY_PREFIX": "mongo_metadata_inheritance",
+ "TIMEOUT": 300,
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://{% if REDIS_USERNAME and REDIS_PASSWORD %}{{ REDIS_USERNAME }}:{{ REDIS_PASSWORD }}{% endif %}@{{ REDIS_HOST }}:{{ REDIS_PORT }}/{{ OPENEDX_CACHE_REDIS_DB }}"
+ },
+ "staticfiles": {
+ "KEY_PREFIX": "staticfiles_cms",
+ "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
+ "LOCATION": "staticfiles_cms"
+ },
+ "configuration": {
+ "KEY_PREFIX": "configuration",
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://{% if REDIS_USERNAME and REDIS_PASSWORD %}{{ REDIS_USERNAME }}:{{ REDIS_PASSWORD }}{% endif %}@{{ REDIS_HOST }}:{{ REDIS_PORT }}/{{ OPENEDX_CACHE_REDIS_DB }}"
+ },
+ "celery": {
+ "KEY_PREFIX": "celery",
+ "TIMEOUT": "7200",
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://{% if REDIS_USERNAME and REDIS_PASSWORD %}{{ REDIS_USERNAME }}:{{ REDIS_PASSWORD }}{% endif %}@{{ REDIS_HOST }}:{{ REDIS_PORT }}/{{ OPENEDX_CACHE_REDIS_DB }}"
+ },
+ "course_structure_cache": {
+ "KEY_PREFIX": "course_structure",
+ "TIMEOUT": "7200",
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://{% if REDIS_USERNAME and REDIS_PASSWORD %}{{ REDIS_USERNAME }}:{{ REDIS_PASSWORD }}{% endif %}@{{ REDIS_HOST }}:{{ REDIS_PORT }}/{{ OPENEDX_CACHE_REDIS_DB }}"
+ }
+ },
+{% include "apps/openedx/config/partials/auth.json" %}
+}
diff --git a/drydock/templates/kustomized/tutor13/base/apps/openedx/config/lms.env.json b/drydock/templates/kustomized/tutor13/base/apps/openedx/config/lms.env.json
new file mode 100644
index 00000000..b3188f94
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/apps/openedx/config/lms.env.json
@@ -0,0 +1,97 @@
+{
+ "SITE_NAME": "{{ LMS_HOST }}",
+ "BOOK_URL": "",
+ "LOG_DIR": "/openedx/data/logs",
+ "LOGGING_ENV": "sandbox",
+ "OAUTH_OIDC_ISSUER": "{{ JWT_COMMON_ISSUER }}",
+ "PLATFORM_NAME": "{{ PLATFORM_NAME }}",
+ "FEATURES": {
+ {{ patch("common-env-features", separator=",\n", suffix=",")|indent(4) }}
+ {{ patch("lms-env-features", separator=",\n", suffix=",")|indent(4) }}
+ "CERTIFICATES_HTML_VIEW": true,
+ "PREVIEW_LMS_BASE": "{{ PREVIEW_LMS_HOST }}",
+ "ENABLE_CORS_HEADERS": true,
+ "ENABLE_COURSE_DISCOVERY": true,
+ "ENABLE_COURSEWARE_SEARCH": true,
+ "ENABLE_CSMH_EXTENDED": false,
+ "ENABLE_DASHBOARD_SEARCH": true,
+ "ENABLE_COMBINED_LOGIN_REGISTRATION": true,
+ "ENABLE_GRADE_DOWNLOADS": true,
+ "ENABLE_LEARNER_RECORDS": false,
+ "ENABLE_MOBILE_REST_API": true,
+ "ENABLE_OAUTH2_PROVIDER": true,
+ "ENABLE_THIRD_PARTY_AUTH": true,
+ "MILESTONES_APP": true,
+ "ENABLE_PREREQUISITE_COURSES": true
+ },
+ "LMS_ROOT_URL": "{{ "https" if ENABLE_HTTPS else "http" }}://{{ LMS_HOST }}",
+ "CMS_ROOT_URL": "{{ "https" if ENABLE_HTTPS else "http" }}://{{ CMS_HOST }}",
+ "CMS_BASE": "{{ CMS_HOST }}",
+ "LMS_BASE": "{{ LMS_HOST }}",
+ "CONTACT_EMAIL": "{{ CONTACT_EMAIL }}",
+ "CELERY_BROKER_TRANSPORT": "redis",
+ "CELERY_BROKER_HOSTNAME": "{{ REDIS_HOST }}:{{ REDIS_PORT }}",
+ "CELERY_BROKER_VHOST": "{{ OPENEDX_CELERY_REDIS_DB }}",
+ "CELERY_BROKER_USER": "{{ REDIS_USERNAME }}",
+ "CELERY_BROKER_PASSWORD": "{{ REDIS_PASSWORD }}",
+ "ALTERNATE_WORKER_QUEUES": "cms",
+ "ENABLE_COMPREHENSIVE_THEMING": true,
+ "COMPREHENSIVE_THEME_DIRS": ["/openedx/themes"],
+ "STATIC_ROOT_BASE": "/openedx/staticfiles",
+ "EMAIL_BACKEND": "django.core.mail.backends.smtp.EmailBackend",
+ "EMAIL_HOST": "{{ SMTP_HOST }}",
+ "EMAIL_PORT": {{ SMTP_PORT }},
+ "EMAIL_USE_TLS": {{ "true" if SMTP_USE_TLS else "false" }},
+ "ACE_ROUTING_KEY": "edx.lms.core.default",
+ "HTTPS": "{{ "on" if ENABLE_HTTPS else "off" }}",
+ "LANGUAGE_CODE": "{{ LANGUAGE_CODE }}",
+ "SESSION_COOKIE_DOMAIN": "{{ LMS_HOST }}",
+ {{ patch("lms-env", separator=",\n", suffix=",")|indent(2) }}
+ "CACHES": {
+ "default": {
+ "KEY_PREFIX": "default",
+ "VERSION": "1",
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://{% if REDIS_USERNAME and REDIS_PASSWORD %}{{ REDIS_USERNAME }}:{{ REDIS_PASSWORD }}{% endif %}@{{ REDIS_HOST }}:{{ REDIS_PORT }}/{{ OPENEDX_CACHE_REDIS_DB }}"
+ },
+ "general": {
+ "KEY_PREFIX": "general",
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://{% if REDIS_USERNAME and REDIS_PASSWORD %}{{ REDIS_USERNAME }}:{{ REDIS_PASSWORD }}{% endif %}@{{ REDIS_HOST }}:{{ REDIS_PORT }}/{{ OPENEDX_CACHE_REDIS_DB }}"
+ },
+ "mongo_metadata_inheritance": {
+ "KEY_PREFIX": "mongo_metadata_inheritance",
+ "TIMEOUT": 300,
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://{% if REDIS_USERNAME and REDIS_PASSWORD %}{{ REDIS_USERNAME }}:{{ REDIS_PASSWORD }}{% endif %}@{{ REDIS_HOST }}:{{ REDIS_PORT }}/{{ OPENEDX_CACHE_REDIS_DB }}"
+ },
+ "staticfiles": {
+ "KEY_PREFIX": "staticfiles_lms",
+ "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
+ "LOCATION": "staticfiles_lms"
+ },
+ "configuration": {
+ "KEY_PREFIX": "configuration",
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://{% if REDIS_USERNAME and REDIS_PASSWORD %}{{ REDIS_USERNAME }}:{{ REDIS_PASSWORD }}{% endif %}@{{ REDIS_HOST }}:{{ REDIS_PORT }}/{{ OPENEDX_CACHE_REDIS_DB }}"
+ },
+ "celery": {
+ "KEY_PREFIX": "celery",
+ "TIMEOUT": "7200",
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://{% if REDIS_USERNAME and REDIS_PASSWORD %}{{ REDIS_USERNAME }}:{{ REDIS_PASSWORD }}{% endif %}@{{ REDIS_HOST }}:{{ REDIS_PORT }}/{{ OPENEDX_CACHE_REDIS_DB }}"
+ },
+ "course_structure_cache": {
+ "KEY_PREFIX": "course_structure",
+ "TIMEOUT": "7200",
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://{% if REDIS_USERNAME and REDIS_PASSWORD %}{{ REDIS_USERNAME }}:{{ REDIS_PASSWORD }}{% endif %}@{{ REDIS_HOST }}:{{ REDIS_PORT }}/{{ OPENEDX_CACHE_REDIS_DB }}"
+ },
+ "ora2-storage": {
+ "KEY_PREFIX": "ora2-storage",
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://{% if REDIS_USERNAME and REDIS_PASSWORD %}{{ REDIS_USERNAME }}:{{ REDIS_PASSWORD }}{% endif %}@{{ REDIS_HOST }}:{{ REDIS_PORT }}/{{ OPENEDX_CACHE_REDIS_DB }}"
+ }
+ },
+{% include "apps/openedx/config/partials/auth.json" %}
+}
diff --git a/drydock/templates/kustomized/tutor13/base/apps/openedx/config/partials/auth.json b/drydock/templates/kustomized/tutor13/base/apps/openedx/config/partials/auth.json
new file mode 100644
index 00000000..66e9422b
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/apps/openedx/config/partials/auth.json
@@ -0,0 +1,26 @@
+ "SECRET_KEY": "{{ OPENEDX_SECRET_KEY }}",
+ "AWS_ACCESS_KEY_ID": "{{ OPENEDX_AWS_ACCESS_KEY }}",
+ "AWS_SECRET_ACCESS_KEY": "{{ OPENEDX_AWS_SECRET_ACCESS_KEY }}",
+ "CONTENTSTORE": null,
+ "DOC_STORE_CONFIG": null,
+ {{ patch("openedx-auth", separator=",\n", suffix=",")|indent(2) }}
+ "XQUEUE_INTERFACE": {
+ "django_auth": null,
+ "url": null
+ },
+ "DATABASES": {
+ "default": {
+ "ENGINE": "django.db.backends.mysql",
+ "HOST": "{{ MYSQL_HOST }}",
+ "PORT": {{ MYSQL_PORT }},
+ "NAME": "{{ OPENEDX_MYSQL_DATABASE }}",
+ "USER": "{{ OPENEDX_MYSQL_USERNAME }}",
+ "PASSWORD": "{{ OPENEDX_MYSQL_PASSWORD }}",
+ "ATOMIC_REQUESTS": true,
+ "OPTIONS": {
+ "init_command": "SET sql_mode='STRICT_TRANS_TABLES'"
+ }
+ }
+ },
+ "EMAIL_HOST_USER": "{{ SMTP_USERNAME }}",
+ "EMAIL_HOST_PASSWORD": "{{ SMTP_PASSWORD }}"
\ No newline at end of file
diff --git a/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/cms/__init__.py b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/cms/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/cms/development.py b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/cms/development.py
new file mode 100644
index 00000000..5ba365da
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/cms/development.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+import os
+from cms.envs.devstack import *
+
+LMS_BASE = "{{ LMS_HOST }}:8000"
+LMS_ROOT_URL = "http://" + LMS_BASE
+
+# Authentication
+SOCIAL_AUTH_EDX_OAUTH2_KEY = "{{ CMS_OAUTH2_KEY_SSO_DEV }}"
+SOCIAL_AUTH_EDX_OAUTH2_PUBLIC_URL_ROOT = LMS_ROOT_URL
+
+FEATURES["PREVIEW_LMS_BASE"] = "{{ PREVIEW_LMS_HOST }}:8000"
+
+{% include "apps/openedx/settings/partials/common_cms.py" %}
+
+# Setup correct webpack configuration file for development
+WEBPACK_CONFIG_PATH = "webpack.dev.config.js"
+
+{{ patch("openedx-development-settings") }}
+{{ patch("openedx-cms-development-settings") }}
diff --git a/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/cms/production.py b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/cms/production.py
new file mode 100644
index 00000000..d09456e3
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/cms/production.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+import os
+from cms.envs.production import *
+
+{% include "apps/openedx/settings/partials/common_cms.py" %}
+
+ALLOWED_HOSTS = [
+ ENV_TOKENS.get("CMS_BASE"),
+ "cms",
+]
+
+# Authentication
+SOCIAL_AUTH_EDX_OAUTH2_KEY = "{{ CMS_OAUTH2_KEY_SSO }}"
+SOCIAL_AUTH_EDX_OAUTH2_PUBLIC_URL_ROOT = "{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}"
+
+{{ patch("openedx-cms-production-settings") }}
diff --git a/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/cms/test.py b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/cms/test.py
new file mode 100644
index 00000000..14a3e3db
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/cms/test.py
@@ -0,0 +1,3 @@
+from cms.envs.test import *
+
+{% include "apps/openedx/settings/partials/common_test.py" %}
diff --git a/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/lms/__init__.py b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/lms/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/lms/development.py b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/lms/development.py
new file mode 100644
index 00000000..e30aabb6
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/lms/development.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+import os
+from lms.envs.devstack import *
+
+{% include "apps/openedx/settings/partials/common_lms.py" %}
+
+# Setup correct webpack configuration file for development
+WEBPACK_CONFIG_PATH = "webpack.dev.config.js"
+
+LMS_BASE = "{{ LMS_HOST}}:8000"
+LMS_ROOT_URL = "http://{}".format(LMS_BASE)
+LMS_INTERNAL_ROOT_URL = LMS_ROOT_URL
+SITE_NAME = LMS_BASE
+CMS_BASE = "{{ CMS_HOST}}:8001"
+CMS_ROOT_URL = "http://{}".format(CMS_BASE)
+LOGIN_REDIRECT_WHITELIST.append(CMS_BASE)
+
+# Session cookie
+SESSION_COOKIE_DOMAIN = "{{ LMS_HOST }}"
+SESSION_COOKIE_SECURE = False
+CSRF_COOKIE_SECURE = False
+SESSION_COOKIE_SAMESITE = "Lax"
+
+# CMS authentication
+IDA_LOGOUT_URI_LIST.append("http://{{ CMS_HOST }}:8001/logout/")
+
+FEATURES['ENABLE_COURSEWARE_MICROFRONTEND'] = False
+
+LOGGING["loggers"]["oauth2_provider"] = {
+ "handlers": ["console"],
+ "level": "DEBUG"
+}
+
+{{ patch("openedx-development-settings") }}
+{{ patch("openedx-lms-development-settings") }}
diff --git a/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/lms/production.py b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/lms/production.py
new file mode 100644
index 00000000..b859b5fe
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/lms/production.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+import os
+from lms.envs.production import *
+
+{% include "apps/openedx/settings/partials/common_lms.py" %}
+
+ALLOWED_HOSTS = [
+ ENV_TOKENS.get("LMS_BASE"),
+ FEATURES["PREVIEW_LMS_BASE"],
+ "lms",
+]
+
+{% if ENABLE_HTTPS %}
+# Properly set the "secure" attribute on session/csrf cookies. This is required in
+# Chrome to support samesite=none cookies.
+SESSION_COOKIE_SECURE = True
+CSRF_COOKIE_SECURE = True
+SESSION_COOKIE_SAMESITE = "None"
+{% else %}
+# When we cannot provide secure session/csrf cookies, we must disable samesite=none
+SESSION_COOKIE_SECURE = False
+CSRF_COOKIE_SECURE = False
+SESSION_COOKIE_SAMESITE = "Lax"
+{% endif %}
+
+# CMS authentication
+IDA_LOGOUT_URI_LIST.append("{% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ CMS_HOST }}/logout/")
+
+# Required to display all courses on start page
+SEARCH_SKIP_ENROLLMENT_START_DATE_FILTERING = True
+
+{{ patch("openedx-lms-production-settings") }}
diff --git a/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/lms/test.py b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/lms/test.py
new file mode 100644
index 00000000..a5fc3518
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/lms/test.py
@@ -0,0 +1,3 @@
+from lms.envs.test import *
+
+{% include "apps/openedx/settings/partials/common_test.py" %}
diff --git a/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/partials/common_all.py b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/partials/common_all.py
new file mode 100644
index 00000000..d6a92914
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/partials/common_all.py
@@ -0,0 +1,174 @@
+####### Settings common to LMS and CMS
+import json
+import os
+
+from xmodule.modulestore.modulestore_settings import update_module_store_settings
+
+# Mongodb connection parameters: simply modify `mongodb_parameters` to affect all connections to MongoDb.
+mongodb_parameters = {
+ "host": "{{ MONGODB_HOST }}",
+ "port": {{ MONGODB_PORT }},
+ {% if MONGODB_USERNAME and MONGODB_PASSWORD %}
+ "user": "{{ MONGODB_USERNAME }}",
+ "password": "{{ MONGODB_PASSWORD }}",
+ {% else %}
+ "user": None,
+ "password": None,
+ {% endif %}
+ "db": "{{ MONGODB_DATABASE }}",
+}
+DOC_STORE_CONFIG = mongodb_parameters
+CONTENTSTORE = {
+ "ENGINE": "xmodule.contentstore.mongo.MongoContentStore",
+ "ADDITIONAL_OPTIONS": {},
+ "DOC_STORE_CONFIG": DOC_STORE_CONFIG
+}
+# Load module store settings from config files
+update_module_store_settings(MODULESTORE, doc_store_settings=DOC_STORE_CONFIG)
+DATA_DIR = "/openedx/data/modulestore"
+
+for store in MODULESTORE["default"]["OPTIONS"]["stores"]:
+ store["OPTIONS"]["fs_root"] = DATA_DIR
+
+# Behave like memcache when it comes to connection errors
+DJANGO_REDIS_IGNORE_EXCEPTIONS = True
+
+# Elasticsearch connection parameters
+ELASTIC_SEARCH_CONFIG = [{
+ {% if ELASTICSEARCH_SCHEME == "https" %}"use_ssl": True,{% endif %}
+ "host": "{{ ELASTICSEARCH_HOST }}",
+ "port": {{ ELASTICSEARCH_PORT }},
+}]
+
+CONTACT_MAILING_ADDRESS = "{{ PLATFORM_NAME }} - {% if ENABLE_HTTPS %}https{% else %}http{% endif %}://{{ LMS_HOST }}"
+
+DEFAULT_FROM_EMAIL = ENV_TOKENS.get("DEFAULT_FROM_EMAIL", ENV_TOKENS["CONTACT_EMAIL"])
+DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get("DEFAULT_FEEDBACK_EMAIL", ENV_TOKENS["CONTACT_EMAIL"])
+SERVER_EMAIL = ENV_TOKENS.get("SERVER_EMAIL", ENV_TOKENS["CONTACT_EMAIL"])
+TECH_SUPPORT_EMAIL = ENV_TOKENS.get("TECH_SUPPORT_EMAIL", ENV_TOKENS["CONTACT_EMAIL"])
+CONTACT_EMAIL = ENV_TOKENS.get("CONTACT_EMAIL", ENV_TOKENS["CONTACT_EMAIL"])
+BUGS_EMAIL = ENV_TOKENS.get("BUGS_EMAIL", ENV_TOKENS["CONTACT_EMAIL"])
+UNIVERSITY_EMAIL = ENV_TOKENS.get("UNIVERSITY_EMAIL", ENV_TOKENS["CONTACT_EMAIL"])
+PRESS_EMAIL = ENV_TOKENS.get("PRESS_EMAIL", ENV_TOKENS["CONTACT_EMAIL"])
+PAYMENT_SUPPORT_EMAIL = ENV_TOKENS.get("PAYMENT_SUPPORT_EMAIL", ENV_TOKENS["CONTACT_EMAIL"])
+BULK_EMAIL_DEFAULT_FROM_EMAIL = ENV_TOKENS.get("BULK_EMAIL_DEFAULT_FROM_EMAIL", ENV_TOKENS["CONTACT_EMAIL"])
+API_ACCESS_MANAGER_EMAIL = ENV_TOKENS.get("API_ACCESS_MANAGER_EMAIL", ENV_TOKENS["CONTACT_EMAIL"])
+API_ACCESS_FROM_EMAIL = ENV_TOKENS.get("API_ACCESS_FROM_EMAIL", ENV_TOKENS["CONTACT_EMAIL"])
+
+# Get rid completely of coursewarehistoryextended, as we do not use the CSMH database
+INSTALLED_APPS.remove("lms.djangoapps.coursewarehistoryextended")
+DATABASE_ROUTERS.remove(
+ "openedx.core.lib.django_courseware_routers.StudentModuleHistoryExtendedRouter"
+)
+
+# Set uploaded media file path
+MEDIA_ROOT = "/openedx/media/"
+
+# Add your MFE and third-party app domains here
+CORS_ORIGIN_WHITELIST = []
+
+# Video settings
+VIDEO_IMAGE_SETTINGS["STORAGE_KWARGS"]["location"] = MEDIA_ROOT
+VIDEO_TRANSCRIPTS_SETTINGS["STORAGE_KWARGS"]["location"] = MEDIA_ROOT
+
+GRADES_DOWNLOAD = {
+ "STORAGE_TYPE": "",
+ "STORAGE_KWARGS": {
+ "base_url": "/media/grades/",
+ "location": "/openedx/media/grades",
+ },
+}
+
+ORA2_FILEUPLOAD_BACKEND = "filesystem"
+ORA2_FILEUPLOAD_ROOT = "/openedx/data/ora2"
+ORA2_FILEUPLOAD_CACHE_NAME = "ora2-storage"
+
+# Change syslog-based loggers which don't work inside docker containers
+LOGGING["handlers"]["local"] = {
+ "class": "logging.handlers.WatchedFileHandler",
+ "filename": os.path.join(LOG_DIR, "all.log"),
+ "formatter": "standard",
+}
+LOGGING["handlers"]["tracking"] = {
+ "level": "DEBUG",
+ "class": "logging.handlers.WatchedFileHandler",
+ "filename": os.path.join(LOG_DIR, "tracking.log"),
+ "formatter": "standard",
+}
+LOGGING["loggers"]["tracking"]["handlers"] = ["console", "local", "tracking"]
+# Silence some loggers (note: we must attempt to get rid of these when upgrading from one release to the next)
+
+import warnings
+from django.utils.deprecation import RemovedInDjango40Warning, RemovedInDjango41Warning
+warnings.filterwarnings("ignore", category=RemovedInDjango40Warning)
+warnings.filterwarnings("ignore", category=RemovedInDjango41Warning)
+warnings.filterwarnings("ignore", category=DeprecationWarning, module="lms.djangoapps.course_wiki.plugins.markdownedx.wiki_plugin")
+warnings.filterwarnings("ignore", category=DeprecationWarning, module="wiki.plugins.links.wiki_plugin")
+
+# Email
+EMAIL_USE_SSL = {{ SMTP_USE_SSL }}
+# Forward all emails from edX's Automated Communication Engine (ACE) to django.
+ACE_ENABLED_CHANNELS = ["django_email"]
+ACE_CHANNEL_DEFAULT_EMAIL = "django_email"
+ACE_CHANNEL_TRANSACTIONAL_EMAIL = "django_email"
+EMAIL_FILE_PATH = "/tmp/openedx/emails"
+
+# Language/locales
+LOCALE_PATHS.append("/openedx/locale/contrib/locale")
+LOCALE_PATHS.append("/openedx/locale/user/locale")
+LANGUAGE_COOKIE_NAME = "openedx-language-preference"
+
+# Allow the platform to include itself in an iframe
+X_FRAME_OPTIONS = "SAMEORIGIN"
+
+{% set jwt_rsa_key = rsa_import_key(JWT_RSA_PRIVATE_KEY) %}
+JWT_AUTH["JWT_ISSUER"] = "{{ JWT_COMMON_ISSUER }}"
+JWT_AUTH["JWT_AUDIENCE"] = "{{ JWT_COMMON_AUDIENCE }}"
+JWT_AUTH["JWT_SECRET_KEY"] = "{{ JWT_COMMON_SECRET_KEY }}"
+JWT_AUTH["JWT_PRIVATE_SIGNING_JWK"] = json.dumps(
+ {
+ "kid": "openedx",
+ "kty": "RSA",
+ "e": "{{ jwt_rsa_key.e|long_to_base64 }}",
+ "d": "{{ jwt_rsa_key.d|long_to_base64 }}",
+ "n": "{{ jwt_rsa_key.n|long_to_base64 }}",
+ "p": "{{ jwt_rsa_key.p|long_to_base64 }}",
+ "q": "{{ jwt_rsa_key.q|long_to_base64 }}",
+ }
+)
+JWT_AUTH["JWT_PUBLIC_SIGNING_JWK_SET"] = json.dumps(
+ {
+ "keys": [
+ {
+ "kid": "openedx",
+ "kty": "RSA",
+ "e": "{{ jwt_rsa_key.e|long_to_base64 }}",
+ "n": "{{ jwt_rsa_key.n|long_to_base64 }}",
+ }
+ ]
+ }
+)
+JWT_AUTH["JWT_ISSUERS"] = [
+ {
+ "ISSUER": "{{ JWT_COMMON_ISSUER }}",
+ "AUDIENCE": "{{ JWT_COMMON_AUDIENCE }}",
+ "SECRET_KEY": "{{ OPENEDX_SECRET_KEY }}"
+ }
+]
+
+# Enable/Disable some features globally
+FEATURES["ENABLE_DISCUSSION_SERVICE"] = False
+FEATURES["PREVENT_CONCURRENT_LOGINS"] = False
+
+# Disable codejail support
+# explicitely configuring python is necessary to prevent unsafe calls
+import codejail.jail_code
+codejail.jail_code.configure("python", "nonexistingpythonbinary", user=None)
+# another configuration entry is required to override prod/dev settings
+CODE_JAIL = {
+ "python_bin": "nonexistingpythonbinary",
+ "user": None,
+}
+
+{{ patch("openedx-common-settings") }}
+######## End of settings common to LMS and CMS
diff --git a/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/partials/common_cms.py b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/partials/common_cms.py
new file mode 100644
index 00000000..d6093459
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/partials/common_cms.py
@@ -0,0 +1,25 @@
+{% include "apps/openedx/settings/partials/common_all.py" %}
+
+######## Common CMS settings
+
+STUDIO_NAME = u"{{ PLATFORM_NAME }} - Studio"
+
+# Authentication
+SOCIAL_AUTH_EDX_OAUTH2_SECRET = "{{ CMS_OAUTH2_SECRET }}"
+SOCIAL_AUTH_EDX_OAUTH2_URL_ROOT = "http://lms:8000"
+SOCIAL_AUTH_REDIRECT_IS_HTTPS = False # scheme is correctly included in redirect_uri
+SESSION_COOKIE_NAME = "studio_session_id"
+
+MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB = 100
+
+FRONTEND_LOGIN_URL = LMS_ROOT_URL + '/login'
+FRONTEND_REGISTER_URL = LMS_ROOT_URL + '/register'
+
+# Create folders if necessary
+for folder in [LOG_DIR, MEDIA_ROOT, STATIC_ROOT_BASE]:
+ if not os.path.exists(folder):
+ os.makedirs(folder)
+
+{{ patch("openedx-cms-common-settings") }}
+
+######## End of common CMS settings
diff --git a/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/partials/common_lms.py b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/partials/common_lms.py
new file mode 100644
index 00000000..2398a7e9
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/partials/common_lms.py
@@ -0,0 +1,31 @@
+{% include "apps/openedx/settings/partials/common_all.py" %}
+
+######## Common LMS settings
+LOGIN_REDIRECT_WHITELIST = ["{{ CMS_HOST }}"]
+
+# Better layout of honor code/tos links during registration
+REGISTRATION_EXTRA_FIELDS["terms_of_service"] = "required"
+REGISTRATION_EXTRA_FIELDS["honor_code"] = "hidden"
+
+# Fix media files paths
+PROFILE_IMAGE_BACKEND["options"]["location"] = os.path.join(
+ MEDIA_ROOT, "profile-images/"
+)
+
+COURSE_CATALOG_VISIBILITY_PERMISSION = "see_in_catalog"
+COURSE_ABOUT_VISIBILITY_PERMISSION = "see_about_page"
+
+# Allow insecure oauth2 for local interaction with local containers
+OAUTH_ENFORCE_SECURE = False
+
+# Email settings
+DEFAULT_EMAIL_LOGO_URL = LMS_ROOT_URL + "/theming/asset/images/logo.png"
+
+# Create folders if necessary
+for folder in [DATA_DIR, LOG_DIR, MEDIA_ROOT, STATIC_ROOT_BASE, ORA2_FILEUPLOAD_ROOT]:
+ if not os.path.exists(folder):
+ os.makedirs(folder, exist_ok=True)
+
+{{ patch("openedx-lms-common-settings") }}
+
+######## End of common LMS settings
diff --git a/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/partials/common_test.py b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/partials/common_test.py
new file mode 100644
index 00000000..290ac292
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/apps/openedx/settings/partials/common_test.py
@@ -0,0 +1,3 @@
+# Fix MongoDb connection credentials
+DOC_STORE_CONFIG["user"] = None
+DOC_STORE_CONFIG["password"] = None
diff --git a/drydock/templates/kustomized/tutor13/base/apps/redis/redis.conf b/drydock/templates/kustomized/tutor13/base/apps/redis/redis.conf
new file mode 100644
index 00000000..cfdf709d
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/apps/redis/redis.conf
@@ -0,0 +1,41 @@
+# https://raw.githubusercontent.com/redis/redis/6.0/redis.conf
+port 6379
+
+tcp-backlog 511
+timeout 0
+tcp-keepalive 300
+daemonize no
+supervised no
+pidfile /var/run/redis_6379.pid
+loglevel notice
+logfile ""
+databases 16
+
+################################ SNAPSHOTTING ################################
+#
+# Save the DB on disk:
+#
+# save
+
+save 900 1
+save 300 10
+save 60 10000
+
+stop-writes-on-bgsave-error yes
+rdbcompression yes
+rdbchecksum yes
+dir /openedx/redis/data/
+dbfilename dump.rdb
+rdb-del-sync-files no
+
+############################## APPEND ONLY MODE ###############################
+
+# http://redis.io/topics/persistence
+appendonly yes
+appendfilename "appendonly.aof"
+appendfsync everysec
+no-appendfsync-on-rewrite no
+auto-aof-rewrite-percentage 100
+auto-aof-rewrite-min-size 64mb
+aof-load-truncated yes
+aof-use-rdb-preamble yes
diff --git a/drydock/templates/kustomized/tutor13/base/k8s/deployments.yml b/drydock/templates/kustomized/tutor13/base/k8s/deployments.yml
new file mode 100644
index 00000000..6f8098c2
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/k8s/deployments.yml
@@ -0,0 +1,467 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: caddy
+ labels:
+ app.kubernetes.io/name: caddy
+spec:
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: caddy
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: caddy
+ spec:
+ {%- if ENABLE_WEB_PROXY %}
+ # This Deployment uses a persistent volume claim. This requires
+ # that in order to enable rolling updates (i.e. use a deployment
+ # strategy other than Replace), we schedule the new Pod to the
+ # same node as the original Pod.
+ affinity:
+ podAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ - labelSelector:
+ matchExpressions:
+ - key: app.kubernetes.io/name
+ operator: In
+ values:
+ - caddy
+ topologyKey: "kubernetes.io/hostname"
+ {%- endif %}
+ containers:
+ - name: caddy
+ image: {{ DOCKER_IMAGE_CADDY }}
+ env:
+ - name: default_site_port
+ value: "{% if not ENABLE_HTTPS or not ENABLE_WEB_PROXY %}:80{% endif %}"
+ volumeMounts:
+ - mountPath: /etc/caddy/
+ name: config
+ {%- if ENABLE_WEB_PROXY %}
+ - mountPath: /data/
+ name: data
+ {%- endif %}
+ ports:
+ - containerPort: 80
+ {%- if ENABLE_WEB_PROXY %}
+ - containerPort: 443
+ {%- endif %}
+ volumes:
+ - name: config
+ configMap:
+ name: caddy-config
+ {%- if ENABLE_WEB_PROXY %}
+ - name: data
+ persistentVolumeClaim:
+ claimName: caddy
+ {%- endif %}
+{% if RUN_CMS %}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: cms
+ labels:
+ app.kubernetes.io/name: cms
+spec:
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: cms
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: cms
+ spec:
+ securityContext:
+ runAsUser: 1000
+ runAsGroup: 1000
+ containers:
+ - name: cms
+ image: {{ DOCKER_IMAGE_OPENEDX }}
+ env:
+ - name: SERVICE_VARIANT
+ value: cms
+ - name: DJANGO_SETTINGS_MODULE
+ value: cms.envs.tutor.production
+ ports:
+ - containerPort: 8000
+ volumeMounts:
+ - mountPath: /openedx/edx-platform/lms/envs/tutor/
+ name: settings-lms
+ - mountPath: /openedx/edx-platform/cms/envs/tutor/
+ name: settings-cms
+ - mountPath: /openedx/config
+ name: config
+ resources:
+ requests:
+ memory: 2Gi
+ securityContext:
+ allowPrivilegeEscalation: false
+ volumes:
+ - name: settings-lms
+ configMap:
+ name: openedx-settings-lms
+ - name: settings-cms
+ configMap:
+ name: openedx-settings-cms
+ - name: config
+ configMap:
+ name: openedx-config
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: cms-worker
+ labels:
+ app.kubernetes.io/name: cms-worker
+spec:
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: cms-worker
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: cms-worker
+ spec:
+ securityContext:
+ runAsUser: 1000
+ runAsGroup: 1000
+ containers:
+ - name: cms-worker
+ image: {{ DOCKER_IMAGE_OPENEDX }}
+ args: ["celery", "worker", "--app=cms.celery", "--loglevel=info", "--hostname=edx.cms.core.default.%%h", "--maxtasksperchild", "100", "--exclude-queues=edx.lms.core.default"]
+ env:
+ - name: SERVICE_VARIANT
+ value: cms
+ - name: DJANGO_SETTINGS_MODULE
+ value: cms.envs.tutor.production
+ volumeMounts:
+ - mountPath: /openedx/edx-platform/lms/envs/tutor/
+ name: settings-lms
+ - mountPath: /openedx/edx-platform/cms/envs/tutor/
+ name: settings-cms
+ - mountPath: /openedx/config
+ name: config
+ securityContext:
+ allowPrivilegeEscalation: false
+ volumes:
+ - name: settings-lms
+ configMap:
+ name: openedx-settings-lms
+ - name: settings-cms
+ configMap:
+ name: openedx-settings-cms
+ - name: config
+ configMap:
+ name: openedx-config
+{% endif %}
+{% if RUN_LMS %}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: lms
+ labels:
+ app.kubernetes.io/name: lms
+spec:
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: lms
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: lms
+ spec:
+ securityContext:
+ runAsUser: 1000
+ runAsGroup: 1000
+ containers:
+ - name: lms
+ image: {{ DOCKER_IMAGE_OPENEDX }}
+ env:
+ - name: SERVICE_VARIANT
+ value: lms
+ - name: DJANGO_SETTINGS_MODULE
+ value: lms.envs.tutor.production
+ ports:
+ - containerPort: 8000
+ volumeMounts:
+ - mountPath: /openedx/edx-platform/lms/envs/tutor/
+ name: settings-lms
+ - mountPath: /openedx/edx-platform/cms/envs/tutor/
+ name: settings-cms
+ - mountPath: /openedx/config
+ name: config
+ resources:
+ requests:
+ memory: 2Gi
+ securityContext:
+ allowPrivilegeEscalation: false
+ volumes:
+ - name: settings-lms
+ configMap:
+ name: openedx-settings-lms
+ - name: settings-cms
+ configMap:
+ name: openedx-settings-cms
+ - name: config
+ configMap:
+ name: openedx-config
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: lms-worker
+ labels:
+ app.kubernetes.io/name: lms-worker
+spec:
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: lms-worker
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: lms-worker
+ spec:
+ securityContext:
+ runAsUser: 1000
+ runAsGroup: 1000
+ containers:
+ - name: lms-worker
+ image: {{ DOCKER_IMAGE_OPENEDX }}
+ args: ["celery", "worker", "--app=lms.celery", "--loglevel=info", "--hostname=edx.lms.core.default.%%h", "--maxtasksperchild=100", "--exclude-queues=edx.cms.core.default"]
+ env:
+ - name: SERVICE_VARIANT
+ value: lms
+ - name: DJANGO_SETTINGS_MODULE
+ value: lms.envs.tutor.production
+ volumeMounts:
+ - mountPath: /openedx/edx-platform/lms/envs/tutor/
+ name: settings-lms
+ - mountPath: /openedx/edx-platform/cms/envs/tutor/
+ name: settings-cms
+ - mountPath: /openedx/config
+ name: config
+ securityContext:
+ allowPrivilegeEscalation: false
+ volumes:
+ - name: settings-lms
+ configMap:
+ name: openedx-settings-lms
+ - name: settings-cms
+ configMap:
+ name: openedx-settings-cms
+ - name: config
+ configMap:
+ name: openedx-config
+{% endif %}
+{% if RUN_ELASTICSEARCH %}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: elasticsearch
+ labels:
+ app.kubernetes.io/name: elasticsearch
+spec:
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: elasticsearch
+ strategy:
+ type: Recreate
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: elasticsearch
+ spec:
+ securityContext:
+ runAsUser: 1000
+ runAsGroup: 1000
+ fsGroup: 1000
+ fsGroupChangePolicy: "OnRootMismatch"
+ containers:
+ - name: elasticsearch
+ image: {{ DOCKER_IMAGE_ELASTICSEARCH }}
+ env:
+ - name: cluster.name
+ value: "openedx"
+ - name: bootstrap.memory_lock
+ value: "true"
+ - name: discovery.type
+ value: "single-node"
+ - name: ES_JAVA_OPTS
+ value: "-Xms{{ ELASTICSEARCH_HEAP_SIZE }} -Xmx{{ ELASTICSEARCH_HEAP_SIZE }}"
+ - name: TAKE_FILE_OWNERSHIP
+ value: "1"
+ ports:
+ - containerPort: 9200
+ securityContext:
+ allowPrivilegeEscalation: false
+ volumeMounts:
+ - mountPath: /usr/share/elasticsearch/data
+ name: data
+ volumes:
+ - name: data
+ persistentVolumeClaim:
+ claimName: elasticsearch
+{% endif %}
+{% if RUN_MONGODB %}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mongodb
+ labels:
+ app.kubernetes.io/name: mongodb
+spec:
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: mongodb
+ strategy:
+ type: Recreate
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: mongodb
+ spec:
+ securityContext:
+ runAsUser: 999
+ runAsGroup: 999
+ fsGroup: 999
+ fsGroupChangePolicy: "OnRootMismatch"
+ containers:
+ - name: mongodb
+ image: {{ DOCKER_IMAGE_MONGODB }}
+ args: ["mongod", "--nojournal", "--storageEngine", "wiredTiger"]
+ ports:
+ - containerPort: 27017
+ volumeMounts:
+ - mountPath: /data/db
+ name: data
+ securityContext:
+ allowPrivilegeEscalation: false
+ volumes:
+ - name: data
+ persistentVolumeClaim:
+ claimName: mongodb
+{% endif %}
+{% if RUN_MYSQL %}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mysql
+ labels:
+ app.kubernetes.io/name: mysql
+spec:
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: mysql
+ strategy:
+ type: Recreate
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: mysql
+ spec:
+ securityContext:
+ runAsUser: 999
+ runAsGroup: 999
+ fsGroup: 999
+ fsGroupChangePolicy: "OnRootMismatch"
+ containers:
+ - name: mysql
+ image: {{ DOCKER_IMAGE_MYSQL }}
+ # Note the ignore-db-dir: this is because ext4 volumes are created with a lost+found directory in them, which causes mysql
+ # initialisation to fail
+ args: ["mysqld", "--character-set-server=utf8", "--collation-server=utf8_general_ci", "--ignore-db-dir=lost+found"]
+ env:
+ - name: MYSQL_ROOT_PASSWORD
+ value: "{{ MYSQL_ROOT_PASSWORD }}"
+ ports:
+ - containerPort: 3306
+ volumeMounts:
+ - mountPath: /var/lib/mysql
+ name: data
+ securityContext:
+ allowPrivilegeEscalation: false
+ volumes:
+ - name: data
+ persistentVolumeClaim:
+ claimName: mysql
+{% endif %}
+{% if RUN_SMTP %}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: smtp
+ labels:
+ app.kubernetes.io/name: smtp
+spec:
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: smtp
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: smtp
+ spec:
+ securityContext:
+ runAsUser: 100
+ runAsGroup: 101
+ containers:
+ - name: smtp
+ image: {{ DOCKER_IMAGE_SMTP }}
+ ports:
+ - containerPort: 8025
+{% endif %}
+{% if RUN_REDIS %}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: redis
+ labels:
+ app.kubernetes.io/name: redis
+spec:
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: redis
+ strategy:
+ type: Recreate
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: redis
+ spec:
+ securityContext:
+ runAsUser: 1000
+ runAsGroup: 1000
+ fsGroup: 1000
+ fsGroupChangePolicy: "OnRootMismatch"
+ containers:
+ - name: redis
+ image: {{ DOCKER_IMAGE_REDIS }}
+ args: ["redis-server", "/openedx/redis/config/redis.conf"]
+ workingDir: /openedx/redis/data
+ ports:
+ - containerPort: {{ REDIS_PORT }}
+ volumeMounts:
+ - mountPath: /openedx/redis/config/
+ name: config
+ - mountPath: /openedx/redis/data
+ name: data
+ securityContext:
+ allowPrivilegeEscalation: false
+ volumes:
+ - name: config
+ configMap:
+ name: redis-config
+ - name: data
+ persistentVolumeClaim:
+ claimName: redis
+{% endif %}
+{{ patch("k8s-deployments") }}
diff --git a/drydock/templates/kustomized/tutor13/base/k8s/jobs.yml b/drydock/templates/kustomized/tutor13/base/k8s/jobs.yml
new file mode 100644
index 00000000..7b751360
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/k8s/jobs.yml
@@ -0,0 +1,88 @@
+---
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: lms-job
+ labels:
+ app.kubernetes.io/component: job
+spec:
+ template:
+ spec:
+ restartPolicy: Never
+ containers:
+ - name: lms
+ image: {{ DOCKER_IMAGE_OPENEDX }}
+ env:
+ - name: SERVICE_VARIANT
+ value: lms
+ - name: DJANGO_SETTINGS_MODULE
+ value: lms.envs.tutor.production
+ volumeMounts:
+ - mountPath: /openedx/edx-platform/lms/envs/tutor/
+ name: settings-lms
+ - mountPath: /openedx/edx-platform/cms/envs/tutor/
+ name: settings-cms
+ - mountPath: /openedx/config
+ name: config
+ volumes:
+ - name: settings-lms
+ configMap:
+ name: openedx-settings-lms
+ - name: settings-cms
+ configMap:
+ name: openedx-settings-cms
+ - name: config
+ configMap:
+ name: openedx-config
+---
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: cms-job
+ labels:
+ app.kubernetes.io/component: job
+spec:
+ template:
+ spec:
+ restartPolicy: Never
+ containers:
+ - name: cms
+ image: {{ DOCKER_IMAGE_OPENEDX }}
+ env:
+ - name: SERVICE_VARIANT
+ value: cms
+ - name: DJANGO_SETTINGS_MODULE
+ value: cms.envs.tutor.production
+ volumeMounts:
+ - mountPath: /openedx/edx-platform/lms/envs/tutor/
+ name: settings-lms
+ - mountPath: /openedx/edx-platform/cms/envs/tutor/
+ name: settings-cms
+ - mountPath: /openedx/config
+ name: config
+ volumes:
+ - name: settings-lms
+ configMap:
+ name: openedx-settings-lms
+ - name: settings-cms
+ configMap:
+ name: openedx-settings-cms
+ - name: config
+ configMap:
+ name: openedx-config
+---
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: mysql-job
+ labels:
+ app.kubernetes.io/component: job
+spec:
+ template:
+ spec:
+ restartPolicy: Never
+ containers:
+ - name: mysql
+ image: {{ DOCKER_IMAGE_MYSQL }}
+
+{{ patch("k8s-jobs") }}
diff --git a/drydock/templates/kustomized/tutor13/base/k8s/namespace.yml b/drydock/templates/kustomized/tutor13/base/k8s/namespace.yml
new file mode 100644
index 00000000..d1b35d97
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/k8s/namespace.yml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: {{ K8S_NAMESPACE }}
+ labels:
+ app.kubernetes.io/component: namespace
\ No newline at end of file
diff --git a/drydock/templates/kustomized/tutor13/base/k8s/services.yml b/drydock/templates/kustomized/tutor13/base/k8s/services.yml
new file mode 100644
index 00000000..180032ba
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/k8s/services.yml
@@ -0,0 +1,149 @@
+{% if ENABLE_WEB_PROXY %}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: caddy
+ labels:
+ app.kubernetes.io/name: caddy
+ app.kubernetes.io/component: loadbalancer
+spec:
+ type: LoadBalancer
+ ports:
+ - port: 80
+ name: http
+ {%- if ENABLE_HTTPS %}
+ - port: 443
+ name: https
+ {%- endif %}
+ selector:
+ app.kubernetes.io/name: caddy
+{% else %}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: caddy
+ labels:
+ app.kubernetes.io/name: caddy
+spec:
+ type: ClusterIP
+ ports:
+ - port: {{ CADDY_HTTP_PORT }}
+ name: http
+ selector:
+ app.kubernetes.io/name: caddy
+{% endif %}
+{% if RUN_CMS %}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: cms
+ labels:
+ app.kubernetes.io/name: cms
+spec:
+ type: ClusterIP
+ ports:
+ - port: 8000
+ protocol: TCP
+ selector:
+ app.kubernetes.io/name: cms
+{% endif %}
+{% if RUN_LMS %}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: lms
+ labels:
+ app.kubernetes.io/name: lms
+spec:
+ type: ClusterIP
+ ports:
+ - port: 8000
+ protocol: TCP
+ selector:
+ app.kubernetes.io/name: lms
+{% endif %}
+{% if RUN_ELASTICSEARCH %}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: elasticsearch
+ labels:
+ app.kubernetes.io/name: elasticsearch
+spec:
+ type: ClusterIP
+ ports:
+ - port: 9200
+ protocol: TCP
+ selector:
+ app.kubernetes.io/name: elasticsearch
+{% endif %}
+{% if RUN_MONGODB %}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: mongodb
+ labels:
+ app.kubernetes.io/name: mongodb
+spec:
+ type: ClusterIP
+ ports:
+ - port: 27017
+ protocol: TCP
+ selector:
+ app.kubernetes.io/name: mongodb
+{% endif %}
+{% if RUN_MYSQL %}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: mysql
+ labels:
+ app.kubernetes.io/name: mysql
+spec:
+ type: ClusterIP
+ ports:
+ - port: 3306
+ protocol: TCP
+ selector:
+ app.kubernetes.io/name: mysql
+{% endif %}
+{% if RUN_REDIS %}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: redis
+ labels:
+ app.kubernetes.io/name: redis
+spec:
+ type: ClusterIP
+ ports:
+ - port: {{ REDIS_PORT }}
+ protocol: TCP
+ selector:
+ app.kubernetes.io/name: redis
+{% endif %}
+{% if RUN_SMTP %}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: smtp
+ labels:
+ app.kubernetes.io/name: smtp
+spec:
+ type: ClusterIP
+ ports:
+ - port: 8025
+ protocol: TCP
+ selector:
+ app.kubernetes.io/name: smtp
+{% endif %}
+{{ patch("k8s-services") }}
diff --git a/drydock/templates/kustomized/tutor13/base/k8s/volumes.yml b/drydock/templates/kustomized/tutor13/base/k8s/volumes.yml
new file mode 100644
index 00000000..ffb4b664
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/k8s/volumes.yml
@@ -0,0 +1,81 @@
+{% if ENABLE_WEB_PROXY %}
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: caddy
+ labels:
+ app.kubernetes.io/component: volume
+ app.kubernetes.io/name: caddy
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 1Gi
+{% endif %}
+{% if RUN_ELASTICSEARCH %}
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: elasticsearch
+ labels:
+ app.kubernetes.io/component: volume
+ app.kubernetes.io/name: elasticsearch
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 2Gi
+{% endif %}
+{% if RUN_MONGODB %}
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: mongodb
+ labels:
+ app.kubernetes.io/component: volume
+ app.kubernetes.io/name: mongodb
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 5Gi
+{% endif %}
+{% if RUN_MYSQL %}
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: mysql
+ labels:
+ app.kubernetes.io/component: volume
+ app.kubernetes.io/name: mysql
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 5Gi
+{% endif %}
+{% if RUN_REDIS %}
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: redis
+ labels:
+ app.kubernetes.io/component: volume
+ app.kubernetes.io/name: redis
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 1Gi
+{% endif %}
+{{ patch("k8s-volumes") }}
\ No newline at end of file
diff --git a/drydock/templates/kustomized/tutor13/base/kustomization.yml b/drydock/templates/kustomized/tutor13/base/kustomization.yml
new file mode 100644
index 00000000..7d807284
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/base/kustomization.yml
@@ -0,0 +1,65 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+- k8s/namespace.yml
+- k8s/deployments.yml
+- k8s/services.yml
+- k8s/volumes.yml
+{%- if not DRYDOCK_DISABLE_JOBS | default(false) %}
+- k8s/jobs.yml
+{%- endif %}
+
+{{ patch("kustomization-resources") }}
+
+# namespace to deploy all Resources to
+namespace: {{ K8S_NAMESPACE }}
+
+# annotations added to all Resources
+# https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/commonannotations/
+commonAnnotations:
+ app.kubernetes.io/version: {{ TUTOR_VERSION }}
+
+# labels (and label selectors) added to all Resources
+# https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
+# https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/commonlabels/
+commonLabels:
+ app.kubernetes.io/instance: openedx-{{ ID }}
+ app.kubernetes.io/part-of: openedx
+ app.kubernetes.io/managed-by: tutor
+ {{ patch("kustomization-commonlabels")|indent(2) }}
+
+configMapGenerator:
+- name: caddy-config
+ files:
+ - apps/caddy/Caddyfile
+ options:
+ labels:
+ app.kubernetes.io/name: caddy
+- name: openedx-settings-lms
+ files:{% for file in "apps/openedx/settings/lms"|walk_templates %}
+ - {{ file }}{% endfor %}
+ options:
+ labels:
+ app.kubernetes.io/name: openedx
+- name: openedx-settings-cms
+ files:{% for file in "apps/openedx/settings/cms"|walk_templates %}
+ - {{ file }}{% endfor %}
+ options:
+ labels:
+ app.kubernetes.io/name: openedx
+- name: openedx-config
+ files:{% for file in "apps/openedx/config"|walk_templates %}
+ - {{ file }}{% endfor %}
+ options:
+ labels:
+ app.kubernetes.io/name: openedx
+- name: redis-config
+ files:
+ - apps/redis/redis.conf
+ options:
+ labels:
+ app.kubernetes.io/name: redis
+{{ patch("kustomization-configmapgenerator") }}
+
+{{ patch("kustomization") }}
diff --git a/drydock/templates/kustomized/tutor13/defaults.yml b/drydock/templates/kustomized/tutor13/defaults.yml
new file mode 100644
index 00000000..9d2ce2a1
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/defaults.yml
@@ -0,0 +1,35 @@
+DRYDOCK_CMS_LIMIT_CPU: 1
+DRYDOCK_CMS_LIMIT_MEMORY: "2Gi"
+DRYDOCK_CMS_MAX_REPLICAS: 3
+DRYDOCK_CMS_MIN_REPLICAS: 3
+DRYDOCK_CMS_REQUEST_CPU: "600m"
+DRYDOCK_CMS_REQUEST_MEMORY: "1Gi"
+DRYDOCK_CMS_TARGET_CPU: 90
+DRYDOCK_CMS_WORKERS_MAX_REPLICAS: 3
+DRYDOCK_CMS_WORKERS_MIN_REPLICAS: 1
+DRYDOCK_CMS_WORKERS_TARGET_CPU: 90
+DRYDOCK_CMS_WORKER_LIMIT_CPU: 1
+DRYDOCK_CMS_WORKER_LIMIT_MEMORY: "2Gi"
+DRYDOCK_CMS_WORKER_REQUEST_CPU: "600m"
+DRYDOCK_CMS_WORKER_REQUEST_MEMORY: "1Gi"
+DRYDOCK_DISABLE_JOBS: False
+DRYDOCK_ENABLE_OVERRIDES: False
+DRYDOCK_FLOWER: False
+DRYDOCK_HPA: False
+DRYDOCK_ENABLE_RESOURCE_MANAGEMENT: False
+DRYDOCK_INGRESS: False
+DRYDOCK_INGRESS_EXTRA_HOSTS: []
+DRYDOCK_LMS_LIMIT_CPU: 1
+DRYDOCK_LMS_LIMIT_MEMORY: "2Gi"
+DRYDOCK_LMS_MAX_REPLICAS: 1
+DRYDOCK_LMS_MIN_REPLICAS: 1
+DRYDOCK_LMS_REQUEST_CPU: "600m"
+DRYDOCK_LMS_REQUEST_MEMORY: "1Gi"
+DRYDOCK_LMS_TARGET_CPU: 90
+DRYDOCK_LMS_WORKERS_MAX_REPLICAS: 3
+DRYDOCK_LMS_WORKERS_MIN_REPLICAS: 1
+DRYDOCK_LMS_WORKERS_TARGET_CPU: 90
+DRYDOCK_LMS_WORKER_LIMIT_CPU: 1
+DRYDOCK_LMS_WORKER_LIMIT_MEMORY: "2Gi"
+DRYDOCK_LMS_WORKER_REQUEST_CPU: "600m"
+DRYDOCK_LMS_WORKER_REQUEST_MEMORY: "1Gi"
diff --git a/drydock/templates/kustomized/tutor13/extensions/flowers.yml b/drydock/templates/kustomized/tutor13/extensions/flowers.yml
new file mode 100644
index 00000000..13172355
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/extensions/flowers.yml
@@ -0,0 +1,25 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: flower-edxapp
+ labels:
+ app.kubernetes.io/name: flower-edxapp
+spec:
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: flower-edxapp
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: flower-edxapp
+ spec:
+ containers:
+ - name: flower-edxapp
+ image: docker.io/mher/flower:0.9.5
+ ports:
+ - containerPort: 5555
+ env:
+ - name: CELERY_BROKER_URL
+ value: redis://{{ REDIS_USERNAME }}:{{ REDIS_PASSWORD }}@{{ REDIS_HOST }}:{{ REDIS_PORT }}/{{ OPENEDX_CELERY_REDIS_DB }}
+ - name: FLOWER_PORT
+ value: "5555"
diff --git a/drydock/templates/kustomized/tutor13/extensions/hpa.yml b/drydock/templates/kustomized/tutor13/extensions/hpa.yml
new file mode 100644
index 00000000..640a36ed
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/extensions/hpa.yml
@@ -0,0 +1,57 @@
+---
+apiVersion: v1
+kind: List
+metadata: {}
+items:
+- apiVersion: autoscaling/v1
+ kind: HorizontalPodAutoscaler
+ metadata:
+ name: hpa-lms-deployment
+ namespace: {{ K8S_NAMESPACE }}
+ spec:
+ maxReplicas: {{ DRYDOCK_LMS_MAX_REPLICAS }}
+ minReplicas: {{ DRYDOCK_LMS_MIN_REPLICAS }}
+ scaleTargetRef:
+ apiVersion: apps/v1
+ kind: Deployment
+ name: lms
+ targetCPUUtilizationPercentage: {{ DRYDOCK_LMS_TARGET_CPU }}
+- apiVersion: autoscaling/v1
+ kind: HorizontalPodAutoscaler
+ metadata:
+ name: hpa-cms-deployment
+ namespace: {{ K8S_NAMESPACE }}
+ spec:
+ maxReplicas: {{ DRYDOCK_CMS_MAX_REPLICAS }}
+ minReplicas: {{ DRYDOCK_CMS_MIN_REPLICAS }}
+ scaleTargetRef:
+ apiVersion: apps/v1
+ kind: Deployment
+ name: cms
+ targetCPUUtilizationPercentage: {{ DRYDOCK_CMS_TARGET_CPU }}
+- apiVersion: autoscaling/v1
+ kind: HorizontalPodAutoscaler
+ metadata:
+ name: hpa-lms-worker-deployment
+ namespace: {{ K8S_NAMESPACE }}
+ spec:
+ maxReplicas: {{ DRYDOCK_LMS_WORKERS_MAX_REPLICAS }}
+ minReplicas: {{ DRYDOCK_LMS_WORKERS_MIN_REPLICAS }}
+ scaleTargetRef:
+ apiVersion: apps/v1
+ kind: Deployment
+ name: lms-worker
+ targetCPUUtilizationPercentage: {{ DRYDOCK_LMS_WORKERS_TARGET_CPU }}
+- apiVersion: autoscaling/v1
+ kind: HorizontalPodAutoscaler
+ metadata:
+ name: hpa-cms-worker-deployment
+ namespace: {{ K8S_NAMESPACE }}
+ spec:
+ maxReplicas: {{ DRYDOCK_CMS_WORKERS_MAX_REPLICAS }}
+ minReplicas: {{ DRYDOCK_CMS_WORKERS_MIN_REPLICAS }}
+ scaleTargetRef:
+ apiVersion: apps/v1
+ kind: Deployment
+ name: cms-worker
+ targetCPUUtilizationPercentage: {{ DRYDOCK_CMS_WORKERS_TARGET_CPU }}
diff --git a/drydock/templates/kustomized/tutor13/extensions/ingress.yml b/drydock/templates/kustomized/tutor13/extensions/ingress.yml
new file mode 100644
index 00000000..8332e54e
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/extensions/ingress.yml
@@ -0,0 +1,70 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress
+ namespace: {{ K8S_NAMESPACE }}
+ annotations:
+ kubernetes.io/ingress.class: nginx
+ {%- if ENABLE_HTTPS %}
+ cert-manager.io/issuer: letsencrypt
+ {%- endif %}
+spec:
+ rules:
+ {%- for host in [LMS_HOST, PREVIEW_LMS_HOST, CMS_HOST] %}
+ - host: {{ host }}
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: caddy
+ port:
+ number: 80
+ {%- endfor %}
+ {%- for host in DRYDOCK_INGRESS_EXTRA_HOSTS %}
+ - host: {{ host }}
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: caddy
+ port:
+ number: 80
+ {%- endfor %}
+ {% if ENABLE_HTTPS -%}
+ tls:
+ - hosts:
+ {%- for host in [LMS_HOST, PREVIEW_LMS_HOST, CMS_HOST] %}
+ - {{ host }}
+ {%- endfor %}
+ {%- for host in DRYDOCK_INGRESS_EXTRA_HOSTS %}
+ - {{ host }}
+ {%- endfor %}
+ secretName: {{ K8S_NAMESPACE }}-tls
+ {%- endif %}
+{% if ENABLE_HTTPS -%}
+---
+apiVersion: cert-manager.io/v1
+kind: Issuer
+metadata:
+ name: letsencrypt
+ namespace: {{ K8S_NAMESPACE }}
+ labels:
+ app.kubernetes.io/name: letsencrypt
+spec:
+ acme:
+ # Let's Encrypt will use this to contact you about expiring
+ # certificates, and issues related to your account.
+ email: {{ DRYDOCK_LETSENCRYPT_EMAIL|default(CONTACT_EMAIL) }}
+ # Secret resource that will be used to store the account's private key.
+ privateKeySecretRef:
+ name: {{ K8S_NAMESPACE }}-letsencrypt-account-key
+ server: https://acme-v02.api.letsencrypt.org/directory
+ solvers:
+ - http01:
+ ingress:
+ class: nginx
+{% endif -%}
diff --git a/drydock/templates/kustomized/tutor13/extensions/kustomization.yml b/drydock/templates/kustomized/tutor13/extensions/kustomization.yml
new file mode 100644
index 00000000..40d01510
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/extensions/kustomization.yml
@@ -0,0 +1,13 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+{% if DRYDOCK_FLOWERS|default(false) -%}
+- flowers.yml
+{% endif -%}
+{% if DRYDOCK_INGRESS|default(false) -%}
+- ingress.yml
+{% endif -%}
+{% if DRYDOCK_HPA|default(false) and DRYDOCK_ENABLE_OVERRIDES|default(false) -%}
+- hpa.yml
+{% endif -%}
diff --git a/drydock/templates/kustomized/tutor13/extensions/overrides.yml b/drydock/templates/kustomized/tutor13/extensions/overrides.yml
new file mode 100644
index 00000000..b24d51e2
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/extensions/overrides.yml
@@ -0,0 +1,71 @@
+{%- if DRYDOCK_ENABLE_RESOURCE_MANAGEMENT -%}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: lms
+spec:
+ template:
+ spec:
+ containers:
+ - name: lms
+ resources:
+ limits:
+ cpu: "{{ DRYDOCK_LMS_LIMIT_CPU }}"
+ memory: "{{ DRYDOCK_LMS_LIMIT_MEMORY }}"
+ requests:
+ cpu: "{{ DRYDOCK_LMS_REQUEST_CPU }}"
+ memory: "{{ DRYDOCK_LMS_REQUEST_MEMORY }}"
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: cms
+spec:
+ template:
+ spec:
+ containers:
+ - name: cms
+ resources:
+ limits:
+ cpu: "{{ DRYDOCK_CMS_LIMIT_CPU }}"
+ memory: "{{ DRYDOCK_CMS_LIMIT_MEMORY }}"
+ requests:
+ cpu: "{{ DRYDOCK_CMS_REQUEST_CPU }}"
+ memory: "{{ DRYDOCK_CMS_REQUEST_MEMORY }}"
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: lms-worker
+spec:
+ template:
+ spec:
+ containers:
+ - name: lms-worker
+ resources:
+ limits:
+ cpu: "{{ DRYDOCK_LMS_WORKER_LIMIT_CPU }}"
+ memory: "{{ DRYDOCK_LMS_WORKER_LIMIT_MEMORY }}"
+ requests:
+ cpu: "{{ DRYDOCK_LMS_WORKER_REQUEST_CPU }}"
+ memory: "{{ DRYDOCK_LMS_WORKER_REQUEST_MEMORY }}"
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: cms-worker
+spec:
+ template:
+ spec:
+ containers:
+ - name: cms-worker
+ resources:
+ limits:
+ cpu: "{{ DRYDOCK_CMS_WORKER_LIMIT_CPU }}"
+ memory: "{{ DRYDOCK_CMS_WORKER_LIMIT_MEMORY }}"
+ requests:
+ cpu: "{{ DRYDOCK_CMS_WORKER_REQUEST_CPU }}"
+ memory: "{{ DRYDOCK_CMS_WORKER_REQUEST_MEMORY }}"
+---
+{% endif -%}
+{{ patch("drydock-overrides") }}
diff --git a/drydock/templates/kustomized/tutor13/kustomization.yml b/drydock/templates/kustomized/tutor13/kustomization.yml
new file mode 100644
index 00000000..0d104bb0
--- /dev/null
+++ b/drydock/templates/kustomized/tutor13/kustomization.yml
@@ -0,0 +1,15 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+- base
+- extensions
+{{ patch("drydock-kustomization-resources") }}
+
+{%- if DRYDOCK_ENABLE_OVERRIDES %}
+patchesStrategicMerge:
+{%- if DRYDOCK_ENABLE_RESOURCE_MANAGEMENT or patch("drydock-overrides") %}
+- extensions/overrides.yml
+{%- endif %}
+{{ patch("drydock-kustomization-patches") }}
+{%- endif %}