From 3db95054129a25ebf9182df4d04d0de055a3c8ca Mon Sep 17 00:00:00 2001 From: acoronels Date: Wed, 9 Sep 2026 17:29:21 -0500 Subject: [PATCH] feat: add a multipurpose edge-proxy --- .github/environments/enabled/config.yml | 1 + .github/workflows/config-files-validation.yml | 11 +++- CHANGELOG.md | 16 ++++++ README.md | 56 ++++++++++++++++++- ...24_163701_piotr_daemonset_image_secrets.md | 29 ---------- ...3919_piotr_exclude_individual_init_jobs.md | 30 ---------- drydock/patches/k8s-deployments | 30 ++++++++++ drydock/patches/k8s-services | 16 ++++++ .../patches/kustomization-configmapgenerator | 8 +++ drydock/plugin.py | 2 + .../templates/drydock/edge-proxy/Caddyfile | 50 +++++++++++++++++ drydock/templates/drydock/k8s/ingress/cms.yml | 6 +- .../drydock/k8s/ingress/extra-hosts.yml | 2 +- drydock/templates/drydock/k8s/ingress/lms.yml | 12 ++-- .../drydock/k8s/ingress/meilisearch.yml | 2 +- drydock/templates/drydock/k8s/ingress/mfe.yml | 2 +- .../templates/drydock/k8s/ingress/notes.yml | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 19 files changed, 203 insertions(+), 76 deletions(-) delete mode 100644 changelog.d/20260924_163701_piotr_daemonset_image_secrets.md delete mode 100644 changelog.d/20260924_163919_piotr_exclude_individual_init_jobs.md create mode 100644 drydock/patches/k8s-deployments create mode 100644 drydock/patches/k8s-services create mode 100644 drydock/templates/drydock/edge-proxy/Caddyfile diff --git a/.github/environments/enabled/config.yml b/.github/environments/enabled/config.yml index 9a9c2632..42f59be7 100644 --- a/.github/environments/enabled/config.yml +++ b/.github/environments/enabled/config.yml @@ -27,6 +27,7 @@ DRYDOCK_PDB_MINAVAILABLE_PERCENTAGE_LMS_WORKER: 50 DRYDOCK_PDB_MINAVAILABLE_PERCENTAGE_CMS: 50 DRYDOCK_PDB_MINAVAILABLE_PERCENTAGE_CMS_WORKER: 50 DRYDOCK_MIGRATE_FROM: 13 +DRYDOCK_EDGE_PROXY_ENABLED: true LMS_HOST: local.edly.io CMS_HOST: studio.local.edly.io MFE_HOST: apps.local.edly.io diff --git a/.github/workflows/config-files-validation.yml b/.github/workflows/config-files-validation.yml index e7b6b05a..23df6015 100644 --- a/.github/workflows/config-files-validation.yml +++ b/.github/workflows/config-files-validation.yml @@ -56,7 +56,6 @@ jobs: - name: Setup Tutor environment run: | echo "TUTOR_ROOT=$GITHUB_WORKSPACE/.github/environments/${{ matrix.environment }}" >> $GITHUB_ENV - echo "TUTOR_PLUGINS_ROOT=$TUTOR_ROOT/plugins" >> $GITHUB_ENV - name: Tutor config save run: | @@ -72,6 +71,16 @@ jobs: caddy:2 \ caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile + - name: Validate generated edge-proxy Caddyfile + run: | + EDGE_PROXY_CADDYFILE="$TUTOR_ROOT/env/plugins/drydock/edge-proxy/Caddyfile" + if [ -f "$EDGE_PROXY_CADDYFILE" ]; then + docker run --rm \ + -v "$EDGE_PROXY_CADDYFILE:/etc/caddy/Caddyfile:ro" \ + caddy:2 \ + caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile + fi + - name: Print versions run: | echo "Kubectl version installed:" diff --git a/CHANGELOG.md b/CHANGELOG.md index 3960fe3c..fe37aad5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,22 @@ See the fragment files in the [changelog.d/ directory](./changelog.d). + +## 22.2.0 — 2026-10-06 + +### Added + +- DaemonSets can now use image secrets to pull private container images. + +- Individual init jobs can now be excluded from execution. + +- A customizable edge reverse proxy layer can be enabled by the + `DRYDOCK_EDGE_PROXY_ENABLED` setting. the proxy sits in front of OpenEdX's + Caddy instance and automatically updates Kubernetes Ingress routing when + enabled. It serves a built-in 503 maintenance page by default for scheduled + downtime and supports full Caddyfile overrides via the + `drydock-edge-proxy-caddyfile` Tutor patch for custom needs. + ## 22.1.0 — 2026-08-13 diff --git a/README.md b/README.md index 5fe18801..61362ccf 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,62 @@ The following configuration options are available: - `DRYDOCK_PDB_MINAVAILABLE_PERCENTAGE_CMS`: The minimum available percentage for the CMS's PodDisruptionBudget. To disable the PodDisruptionBudget, set `0`. Defaults to `0`. - `DRYDOCK_PDB_MINAVAILABLE_PERCENTAGE_CMS_WORKER`: The minimum available percentage for the worker's PodDisruptionBudget. To disable the PodDisruptionBudget, set `0`. Defaults to `0`. - `DRYDOCK_MIGRATE_FROM`: it allows defining the version of the OpenedX platform we are migrating from. It accepts the integer value mapping the origin release, for instance, `13`(maple) or `14`(nutmeg). When this variable is set, a group of `release-specific upgrade jobs` are added to the Kubernetes manifests. These jobs are applied to the cluster in a suitable order (thanks to the GitOps implementation with ArgoCD + sync waves) to guarantee the correct behavior of the platform in the new version. This brings the `tutor k8s upgrade `_ command to the GitOps pattern. The release-specific upgrade jobs are supported from release `13`(maple). Defaults to `0` (which disables release-specific upgrade jobs) +- `DRYDOCK_EDGE_PROXY_ENABLED`: Whether to deploy a dedicated edge proxy (Caddy) and redirect all Ingress backends to it. Defaults to `false`. + +> **_NOTE:_** You also need to set `DRYDOCK_INIT_JOBS` to `true` to enable the +> release-specific upgrade jobs in the case of a platform migration. + +Edge Proxy +---------- + +When `DRYDOCK_EDGE_PROXY_ENABLED` is `true` and `DRYDOCK_INGRESS` is enabled, +Drydock deploys an `edge-proxy` service (Caddy) and redirects **all** Ingress +backends (LMS, Studio, MFE, Notes, Meilisearch, and extra hosts) to it. + +By default the edge proxy simply serves a static HTML page with a `503` status +code. The patch `drydock-edge-proxy-caddyfile` can be used to construct a +Caddyfile from scratch for the edge-proxy instead of the static HTML page with +custom routing rules. + +An example on how to use the patch is as follows: + +```python +from tutor import hooks + +CADDYFILE_CONTENT = """ +{ + servers { + trusted_proxies static 10.0.0.0/8 private_ranges + } +} +:80 { + log { + output stdout + format json + } + @allowed client_ip 104.20.23.154 + handle @allowed { + reverse_proxy caddy:80 + } + + @redirect_paths path /login /login/ + handle @redirect_paths { + redir https://www.google.com permanent + } + + handle { + respond "Under maintenance" 503 + } +} +""" +hooks.Filters.ENV_PATCHES.add_items([("drydock-edge-proxy-caddyfile", CADDYFILE_CONTENT)]) +``` -> **_NOTE:_** You also need to set `DRYDOCK_INIT_JOBS` to `true` to enable the release-specific upgrade jobs in the case of a platform migration. +This configuration will show the maintenance page to everyone besides the user +with ip `104.20.23.154` and will redirect to google when accesing `/login` and +`/login/`. For ingress objects not handled by drydock and/or when +`DRYDOCK_INGRESS=False` the ingress will need to be modified to forward to the +edge-proxy service. Job generation -------------- diff --git a/changelog.d/20260924_163701_piotr_daemonset_image_secrets.md b/changelog.d/20260924_163701_piotr_daemonset_image_secrets.md deleted file mode 100644 index 64f2e601..00000000 --- a/changelog.d/20260924_163701_piotr_daemonset_image_secrets.md +++ /dev/null @@ -1,29 +0,0 @@ - - -### Added - -- DaemonSets can now use image secrets to pull private container images. - - - - diff --git a/changelog.d/20260924_163919_piotr_exclude_individual_init_jobs.md b/changelog.d/20260924_163919_piotr_exclude_individual_init_jobs.md deleted file mode 100644 index 9714be04..00000000 --- a/changelog.d/20260924_163919_piotr_exclude_individual_init_jobs.md +++ /dev/null @@ -1,30 +0,0 @@ - - - -### Added - -- Individual init jobs can now be excluded from execution. - - - - diff --git a/drydock/patches/k8s-deployments b/drydock/patches/k8s-deployments new file mode 100644 index 00000000..1ec99f0d --- /dev/null +++ b/drydock/patches/k8s-deployments @@ -0,0 +1,30 @@ +{% if DRYDOCK_EDGE_PROXY_ENABLED %} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: edge-proxy + labels: + app.kubernetes.io/name: edge-proxy +spec: + selector: + matchLabels: + app.kubernetes.io/name: edge-proxy + template: + metadata: + labels: + app.kubernetes.io/name: edge-proxy + spec: + containers: + - name: caddy + image: {{ DOCKER_IMAGE_CADDY }} + volumeMounts: + - mountPath: /etc/caddy/ + name: config + ports: + - containerPort: 80 + volumes: + - name: config + configMap: + name: edge-proxy-config +{% endif %} diff --git a/drydock/patches/k8s-services b/drydock/patches/k8s-services new file mode 100644 index 00000000..0af14110 --- /dev/null +++ b/drydock/patches/k8s-services @@ -0,0 +1,16 @@ +{% if DRYDOCK_EDGE_PROXY_ENABLED %} +--- +apiVersion: v1 +kind: Service +metadata: + name: edge-proxy + labels: + app.kubernetes.io/name: edge-proxy +spec: + type: ClusterIP + ports: + - port: 80 + name: http + selector: + app.kubernetes.io/name: edge-proxy +{% endif %} diff --git a/drydock/patches/kustomization-configmapgenerator b/drydock/patches/kustomization-configmapgenerator index 2dfa9d60..d7d03f64 100644 --- a/drydock/patches/kustomization-configmapgenerator +++ b/drydock/patches/kustomization-configmapgenerator @@ -6,3 +6,11 @@ labels: app.kubernetes.io/name: openedx {% endif -%} +{%- if DRYDOCK_EDGE_PROXY_ENABLED and DRYDOCK_INGRESS %} +- name: edge-proxy-config + files: + - plugins/drydock/edge-proxy/Caddyfile + options: + labels: + app.kubernetes.io/name: edge-proxy +{%- endif %} diff --git a/drydock/plugin.py b/drydock/plugin.py index 6b818456..8edf6758 100644 --- a/drydock/plugin.py +++ b/drydock/plugin.py @@ -32,6 +32,7 @@ ("DRYDOCK_INGRESS_LMS_EXTRA_HOSTS", []), ("DRYDOCK_NEWRELIC_LICENSE_KEY", ""), ("DRYDOCK_CUSTOM_CERTS", {}), + ("DRYDOCK_EDGE_PROXY_ENABLED", False), ("DRYDOCK_LETSENCRYPT_EMAIL", "{{ CONTACT_EMAIL }}"), ("DRYDOCK_ENABLE_MULTITENANCY", True), ("DRYDOCK_ENABLE_SCORM", True), @@ -189,6 +190,7 @@ def get_sync_waves_for_resource(resource_name: str) -> int: ("drydock/build", "plugins"), ("drydock/apps", "plugins"), ("drydock/k8s", "plugins"), + ("drydock/edge-proxy", "plugins"), ], ) # Load all patches from the "patches" folder diff --git a/drydock/templates/drydock/edge-proxy/Caddyfile b/drydock/templates/drydock/edge-proxy/Caddyfile new file mode 100644 index 00000000..2fa3e2f3 --- /dev/null +++ b/drydock/templates/drydock/edge-proxy/Caddyfile @@ -0,0 +1,50 @@ +{% if patch("drydock-edge-proxy-caddyfile") -%} +{{ patch("drydock-edge-proxy-caddyfile") }} +{%- else -%} +:80 { + handle { + header Content-Type "text/html; charset=utf-8" + respond 503 { + body < + + + + + Under maintenance + + + +
+

Under maintenance

+

We are performing scheduled maintenance and will be back online shortly.

+
+ + +HTML + } + } +} +{%- endif %} diff --git a/drydock/templates/drydock/k8s/ingress/cms.yml b/drydock/templates/drydock/k8s/ingress/cms.yml index 61bec919..341c03d5 100644 --- a/drydock/templates/drydock/k8s/ingress/cms.yml +++ b/drydock/templates/drydock/k8s/ingress/cms.yml @@ -21,14 +21,14 @@ spec: path: "/course-authoring" backend: service: - name: caddy + name: {{ "edge-proxy" if DRYDOCK_EDGE_PROXY_ENABLED else "caddy" }} port: number: 80 - pathType: Prefix path: "/{{app_name}}" backend: service: - name: caddy + name: {{ "edge-proxy" if DRYDOCK_EDGE_PROXY_ENABLED else "caddy" }} port: number: 80 {%- endif %} @@ -38,7 +38,7 @@ spec: path: "/" backend: service: - name: caddy + name: {{ "edge-proxy" if DRYDOCK_EDGE_PROXY_ENABLED else "caddy" }} port: number: 80 {%- if DRYDOCK_AUTO_TLS or DRYDOCK_CUSTOM_CERTS %} diff --git a/drydock/templates/drydock/k8s/ingress/extra-hosts.yml b/drydock/templates/drydock/k8s/ingress/extra-hosts.yml index a1950734..3d8d1b69 100644 --- a/drydock/templates/drydock/k8s/ingress/extra-hosts.yml +++ b/drydock/templates/drydock/k8s/ingress/extra-hosts.yml @@ -19,7 +19,7 @@ spec: path: "/" backend: service: - name: caddy + name: {{ "edge-proxy" if DRYDOCK_EDGE_PROXY_ENABLED else "caddy" }} port: number: 80 {%- if DRYDOCK_AUTO_TLS or DRYDOCK_CUSTOM_CERTS %} diff --git a/drydock/templates/drydock/k8s/ingress/lms.yml b/drydock/templates/drydock/k8s/ingress/lms.yml index df273a12..d479f52a 100644 --- a/drydock/templates/drydock/k8s/ingress/lms.yml +++ b/drydock/templates/drydock/k8s/ingress/lms.yml @@ -18,7 +18,7 @@ spec: path: "/learning" backend: service: - name: caddy + name: {{ "edge-proxy" if DRYDOCK_EDGE_PROXY_ENABLED else "caddy" }} port: number: 80 {%- if MFE_DOCKER_IMAGE is defined %} @@ -28,7 +28,7 @@ spec: path: "/{{app_name}}" backend: service: - name: caddy + name: {{ "edge-proxy" if DRYDOCK_EDGE_PROXY_ENABLED else "caddy" }} port: number: 80 {%- endif %} @@ -38,7 +38,7 @@ spec: path: "/" backend: service: - name: caddy + name: {{ "edge-proxy" if DRYDOCK_EDGE_PROXY_ENABLED else "caddy" }} port: number: 80 {{ patch("drydock-lms-extra-paths")|indent(6) }} @@ -50,7 +50,7 @@ spec: path: "/learning" backend: service: - name: caddy + name: {{ "edge-proxy" if DRYDOCK_EDGE_PROXY_ENABLED else "caddy" }} port: number: 80 {%- if MFE_DOCKER_IMAGE is defined %} @@ -59,7 +59,7 @@ spec: path: "/{{app_name}}" backend: service: - name: caddy + name: {{ "edge-proxy" if DRYDOCK_EDGE_PROXY_ENABLED else "caddy" }} port: number: 80 {%- endfor %} @@ -68,7 +68,7 @@ spec: path: "/" backend: service: - name: caddy + name: {{ "edge-proxy" if DRYDOCK_EDGE_PROXY_ENABLED else "caddy" }} port: number: 80 {{ patch("drydock-lms-extra-paths")|indent(6) }} diff --git a/drydock/templates/drydock/k8s/ingress/meilisearch.yml b/drydock/templates/drydock/k8s/ingress/meilisearch.yml index c7e84ec4..022d3c3f 100644 --- a/drydock/templates/drydock/k8s/ingress/meilisearch.yml +++ b/drydock/templates/drydock/k8s/ingress/meilisearch.yml @@ -18,7 +18,7 @@ spec: path: "/" backend: service: - name: caddy + name: {{ "edge-proxy" if DRYDOCK_EDGE_PROXY_ENABLED else "caddy" }} port: number: 80 {%- if DRYDOCK_AUTO_TLS or DRYDOCK_CUSTOM_CERTS %} diff --git a/drydock/templates/drydock/k8s/ingress/mfe.yml b/drydock/templates/drydock/k8s/ingress/mfe.yml index 9b65bc1a..ad1e4b65 100644 --- a/drydock/templates/drydock/k8s/ingress/mfe.yml +++ b/drydock/templates/drydock/k8s/ingress/mfe.yml @@ -19,7 +19,7 @@ spec: path: "/" backend: service: - name: caddy + name: {{ "edge-proxy" if DRYDOCK_EDGE_PROXY_ENABLED else "caddy" }} port: number: 80 {%- if DRYDOCK_AUTO_TLS or DRYDOCK_CUSTOM_CERTS %} diff --git a/drydock/templates/drydock/k8s/ingress/notes.yml b/drydock/templates/drydock/k8s/ingress/notes.yml index a4550d65..f321cd2a 100644 --- a/drydock/templates/drydock/k8s/ingress/notes.yml +++ b/drydock/templates/drydock/k8s/ingress/notes.yml @@ -18,7 +18,7 @@ spec: path: "/" backend: service: - name: caddy + name: {{ "edge-proxy" if DRYDOCK_EDGE_PROXY_ENABLED else "caddy" }} port: number: 80 {%- if DRYDOCK_AUTO_TLS or DRYDOCK_CUSTOM_CERTS %} diff --git a/pyproject.toml b/pyproject.toml index b53fcc1a..cb6ea31a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "tutor-contrib-drydock" -version = "22.1.0" +version = "22.2.0" description = "A Tutor plugin to manage our opinionated Open edX operations" readme = "README.md" requires-python = ">=3.10" diff --git a/uv.lock b/uv.lock index b606ff07..82ba949a 100644 --- a/uv.lock +++ b/uv.lock @@ -1398,7 +1398,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/d7/6c/f58136bcbc099fb97 [[package]] name = "tutor-contrib-drydock" -version = "22.1.0" +version = "22.2.0" source = { editable = "." } dependencies = [ { name = "tutor" },