Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a basic manifest repository implementation #1

Merged
merged 1 commit into from
Jul 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 5 additions & 1 deletion drydock/manifest_builder/domain/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion drydock/manifest_builder/domain/manifest_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
class ManifestRepository(ABC):

@abstractmethod
def save(config: DrydockConfig) -> None:
def save(self, config: DrydockConfig) -> None:
pass
Original file line number Diff line number Diff line change
@@ -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)
Comment on lines +91 to +95
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is my current folder structure:

├── config.yml
├── env
│   ├── base
│   │   ├── apps
│   │   │   ├── caddy
│   │   │   ├── openedx
│   │   │   └── redis
│   │   ├── k8s
│   │   │   ├── deployments.yml
│   │   │   ├── jobs.yml
│   │   │   ├── namespace.yml
│   │   │   ├── services.yml
│   │   │   └── volumes.yml
│   │   ├── kustomization.yml
│   │   └── plugins
│   │       ├── forum
│   │       ├── notes
│   │       └── settingmanager.yml
│   ├── extensions
│   │   ├── flowers.yml
│   │   ├── hpa.yml
│   │   ├── ingress.yml
│   │   ├── kustomization.yml
│   │   └── overrides.yml
│   └── kustomization.yml
├── reference.yml
└── requirements.txt

My tutor plugins list:

android         	(disabled)	13.0.0
discovery       	(disabled)	13.0.1
distro          	          	2.1.0
drydock         	          	0.1.0
ecommerce       	(disabled)	13.0.1
forum           	          	13.0.0
license         	(disabled)	13.0.0
mfe             	(disabled)	13.0.6
minio           	          	13.0.1
notes           	          	13.0.1
richie          	(disabled)	13.0.2
settingmanager  	          	0.1.0
webui           	(disabled)	13.0.2
xqueue          	(disabled)	13.0.0

Then I run:
tutor drydock save -r reference.yml
When the command finishes, I run:
tutor plugins list
And this happens:

⚠️  Failed to enable plugin 'settingmanager': plugin 'settingmanager' is not installed.
android    	(disabled)	13.0.0
discovery  	(disabled)	13.0.1
distro     	          	2.1.0
drydock    	          	0.1.0
ecommerce  	(disabled)	13.0.1
forum      	          	13.0.0
license    	(disabled)	13.0.0
mfe        	(disabled)	13.0.6
minio      	          	13.0.1
notes      	          	13.0.1
richie     	(disabled)	13.0.2
webui      	(disabled)	13.0.2
xqueue     	(disabled)	13.0.0

Is that okay?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes (actually is not ok, but is a current limitation). At the moment we don't render module nor yaml plugins on base/plugins. The current workaround is to define the TUTOR_PLUGINS_ROOTS outside env.

The folder structure would look something like this:

├── config.yml
├── env
│   ├── base
│   │   ├── apps
│   │   │   ├── caddy
│   │   │   ├── openedx
│   │   │   └── redis
│   │   ├── k8s
│   │   │   ├── deployments.yml
│   │   │   ├── jobs.yml
│   │   │   ├── namespace.yml
│   │   │   ├── services.yml
│   │   │   └── volumes.yml
│   │   ├── kustomization.yml
│   │   └── plugins
│   │       ├── forum
│   │       └──  notes
│   ├── extensions
│   │   ├── flowers.yml
│   │   ├── hpa.yml
│   │   ├── ingress.yml
│   │   ├── kustomization.yml
│   │   └── overrides.yml
│   └── kustomization.yml
├── reference.yml
└── requirements.txt
└── plugins
    └── settingmanager.yml

and your TUTOR_PLUGINS_ROOT should be something like $TUTOR_ROOT/plugins


fmt.echo_info(f"Drydock environment generated in {dst}")
mariajgrimaldi marked this conversation as resolved.
Show resolved Hide resolved
59 changes: 55 additions & 4 deletions drydock/manifest_builder/infrastructure/tutor_config.py
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions drydock/references/kustomized_v13.yml
Original file line number Diff line number Diff line change
@@ -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"
32 changes: 32 additions & 0 deletions drydock/templates/README.md
Original file line number Diff line number Diff line change
@@ -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)

<details>
<summary>List of patches</summary>

```
{{ patch("drydock-kustomization-resources")}}
{{ patch("drydock-kustomization-patches")}}
{{ patch("drydock-overrides") }}
```
</details>

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.
62 changes: 62 additions & 0 deletions drydock/templates/kustomized/tutor13/base/apps/caddy/Caddyfile
Original file line number Diff line number Diff line change
@@ -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") }}
Loading