From 2864230685e7d9178376d712230fb53a7ab979dd Mon Sep 17 00:00:00 2001 From: acoronels Date: Wed, 9 Sep 2026 17:29:21 -0500 Subject: [PATCH 1/4] feat(drydock): add global maintenance ingress mode --- .github/environments/enabled/config.yml | 1 + .github/workflows/config-files-validation.yml | 11 ++++++- .../patches/kustomization-configmapgenerator | 14 +++++++++ drydock/patches/kustomization-resources | 4 +++ drydock/plugin.py | 29 +++++++++++++++++++ pyproject.toml | 2 +- uv.lock | 2 +- 7 files changed, 60 insertions(+), 3 deletions(-) diff --git a/.github/environments/enabled/config.yml b/.github/environments/enabled/config.yml index 9a9c2632..243c1fa2 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_MAINTENANCE_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..19a9730d 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 maintenance Caddyfile + run: | + MAINTENANCE_CADDYFILE="$TUTOR_ROOT/env/plugins/drydock/maintenance/apps/caddy/Caddyfile" + if [ -f "$MAINTENANCE_CADDYFILE" ]; then + docker run --rm \ + -v "$MAINTENANCE_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/drydock/patches/kustomization-configmapgenerator b/drydock/patches/kustomization-configmapgenerator index 2dfa9d60..57246207 100644 --- a/drydock/patches/kustomization-configmapgenerator +++ b/drydock/patches/kustomization-configmapgenerator @@ -6,3 +6,17 @@ labels: app.kubernetes.io/name: openedx {% endif -%} +{%- if DRYDOCK_MAINTENANCE_ENABLED %} +- name: maintenance-caddy-config + files: + - plugins/drydock/maintenance/apps/caddy/Caddyfile + - plugins/drydock/maintenance/static/maintenance.html + options: + labels: + app.kubernetes.io/name: maintenance-caddy +- name: maintenance-ingress-backend + literals: + - service=maintenance-caddy + options: + disableNameSuffixHash: true +{%- endif %} diff --git a/drydock/patches/kustomization-resources b/drydock/patches/kustomization-resources index 05f39ba3..c511c52a 100644 --- a/drydock/patches/kustomization-resources +++ b/drydock/patches/kustomization-resources @@ -15,3 +15,7 @@ {% if DRYDOCK_REGISTRY_CREDENTIALS -%} - plugins/drydock/k8s/secrets/image-pull-secret.yml {% endif -%} +{%- if DRYDOCK_MAINTENANCE_ENABLED and DRYDOCK_INGRESS %} +- plugins/drydock/maintenance/k8s/deployment.yml +- plugins/drydock/maintenance/k8s/service.yml +{%- endif %} diff --git a/drydock/plugin.py b/drydock/plugin.py index e514e6dd..84745567 100644 --- a/drydock/plugin.py +++ b/drydock/plugin.py @@ -48,6 +48,8 @@ ["lms", "cms", "forum", "lms-worker", "cms-worker", "superset", "superset-worker", "superset-celery-beat"], ), ("DRYDOCK_REGISTRY_CREDENTIALS", ""), + ("DRYDOCK_MAINTENANCE_ENABLED", False), + ("DRYDOCK_MAINTENANCE_HTTP_STATUS", 503), ] ) @@ -176,6 +178,29 @@ def get_sync_waves_for_resource(resource_name: str) -> int: return get_sync_waves_order().get(resource_name, 0) +def maintenance_ingress_names() -> list[str]: + """Return Drydock Ingress names whose backends should point to maintenance-caddy.""" + if not TUTOR_CONFIG.get("DRYDOCK_INGRESS", True): + return [] + + names = ["lms", "cms"] + + if TUTOR_CONFIG.get("MFE_HOST") is not None: + names.append("mfe") + if TUTOR_CONFIG.get("RUN_MEILISEARCH", True): + names.append("meilisearch") + plugins = TUTOR_CONFIG.get("PLUGINS", []) + if isinstance(plugins, list) and "notes" in plugins: + names.append("notes") + + extra_hosts = TUTOR_CONFIG.get("DRYDOCK_INGRESS_EXTRA_HOSTS", []) + if isinstance(extra_hosts, list): + for host in extra_hosts: + names.append(str(host).replace(".", "-")) + + return names + + ################# You don't really have to bother about what's below this line, ################# except maybe for educational purposes :) @@ -186,6 +211,9 @@ def get_sync_waves_for_resource(resource_name: str) -> int: ("drydock/build", "plugins"), ("drydock/apps", "plugins"), ("drydock/k8s", "plugins"), + ("drydock/maintenance/k8s", "plugins"), + ("drydock/maintenance/apps", "plugins"), + ("drydock/maintenance/static", "plugins"), ], ) # Load all patches from the "patches" folder @@ -202,6 +230,7 @@ def get_sync_waves_for_resource(resource_name: str) -> int: ("get_init_tasks", get_init_tasks), ("iter_sync_waves_order", iter_sync_waves_order), ("get_sync_waves_for_resource", get_sync_waves_for_resource), + ("maintenance_ingress_names", maintenance_ingress_names), ] ) 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" }, From d3eff9f8eb1fec19a597992f9e25466c4977a043 Mon Sep 17 00:00:00 2001 From: acoronels Date: Wed, 9 Sep 2026 17:29:41 -0500 Subject: [PATCH 2/4] docs(drydock): update README --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 5fe18801..fa6eac1e 100644 --- a/README.md +++ b/README.md @@ -77,9 +77,19 @@ 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_MAINTENANCE_ENABLED`: Whether to deploy a dedicated maintenance Caddy and redirect all Drydock-managed Ingress backends to it. Defaults to `false`. +- `DRYDOCK_MAINTENANCE_HTTP_STATUS`: HTTP status code returned with the maintenance HTML page. Defaults to `503`. > **_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. +Maintenance mode +---------------- + +When `DRYDOCK_MAINTENANCE_ENABLED` is `true` and `DRYDOCK_INGRESS` is enabled, Drydock deploys a `maintenance-caddy` service and uses Kustomize replacements to point LMS, Studio, MFE, Notes, Meilisearch, and `DRYDOCK_INGRESS_EXTRA_HOSTS` Ingress backends to it. The maintenance Caddy serves a default HTML page to external clients. + +Custom maintenance routing can be added through the `drydock-maintenance-caddyfile` Tutor patch. The `drydock-maintenance-caddyfile-global` patch is available for global Caddy options such as `trusted_proxies`. Drydock does not provide an IP restriction or bypass by default. Clients can implement it through these patches, keeping redirects, headers, and other maintenance-only rules isolated from the main Open edX Caddyfile. + + Job generation -------------- From db6c44f75c9b86fdea790b7482c3d05abfa432b3 Mon Sep 17 00:00:00 2001 From: acoronels Date: Wed, 9 Sep 2026 17:39:57 -0500 Subject: [PATCH 3/4] chore(drydock): add missing templates --- drydock/patches/kustomization | 15 ++++++ .../drydock/maintenance/apps/caddy/Caddyfile | 23 +++++++++ .../drydock/maintenance/k8s/deployment.yml | 51 +++++++++++++++++++ .../drydock/maintenance/k8s/service.yml | 14 +++++ .../maintenance/static/maintenance.html | 22 ++++++++ 5 files changed, 125 insertions(+) create mode 100644 drydock/patches/kustomization create mode 100644 drydock/templates/drydock/maintenance/apps/caddy/Caddyfile create mode 100644 drydock/templates/drydock/maintenance/k8s/deployment.yml create mode 100644 drydock/templates/drydock/maintenance/k8s/service.yml create mode 100644 drydock/templates/drydock/maintenance/static/maintenance.html diff --git a/drydock/patches/kustomization b/drydock/patches/kustomization new file mode 100644 index 00000000..6a5cfd28 --- /dev/null +++ b/drydock/patches/kustomization @@ -0,0 +1,15 @@ +{%- if DRYDOCK_MAINTENANCE_ENABLED and DRYDOCK_INGRESS %} +replacements: +{%- for ingress_name in maintenance_ingress_names() %} +- source: + kind: ConfigMap + name: maintenance-ingress-backend + fieldPath: data.service + targets: + - select: + kind: Ingress + name: {{ ingress_name }} + fieldPaths: + - spec.rules.*.http.paths.*.backend.service.name +{%- endfor %} +{%- endif %} diff --git a/drydock/templates/drydock/maintenance/apps/caddy/Caddyfile b/drydock/templates/drydock/maintenance/apps/caddy/Caddyfile new file mode 100644 index 00000000..8aea0670 --- /dev/null +++ b/drydock/templates/drydock/maintenance/apps/caddy/Caddyfile @@ -0,0 +1,23 @@ +{{ patch("drydock-maintenance-caddyfile-global") }} + +:80 { + handle /health { + respond "OK" 200 + } + + {{ patch("drydock-maintenance-caddyfile") | indent(4) }} + + handle { + error {{ DRYDOCK_MAINTENANCE_HTTP_STATUS }} + } + + handle_errors { + @maintenance expression {http.error.status_code} == {{ DRYDOCK_MAINTENANCE_HTTP_STATUS }} + handle @maintenance { + root * /srv/maintenance + rewrite * /maintenance.html + header Content-Type "text/html; charset=utf-8" + file_server + } + } +} diff --git a/drydock/templates/drydock/maintenance/k8s/deployment.yml b/drydock/templates/drydock/maintenance/k8s/deployment.yml new file mode 100644 index 00000000..d4b9e398 --- /dev/null +++ b/drydock/templates/drydock/maintenance/k8s/deployment.yml @@ -0,0 +1,51 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: maintenance-caddy + labels: + app.kubernetes.io/name: maintenance-caddy +spec: + selector: + matchLabels: + app.kubernetes.io/name: maintenance-caddy + template: + metadata: + labels: + app.kubernetes.io/name: maintenance-caddy + spec: + containers: + - name: caddy + image: {{ DOCKER_IMAGE_CADDY }} + env: + - name: default_site_port + value: ":80" + volumeMounts: + - mountPath: /etc/caddy/ + name: config + - mountPath: /srv/maintenance/ + name: html + ports: + - containerPort: 80 + readinessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 2 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: config + configMap: + name: maintenance-caddy-config + - name: html + configMap: + name: maintenance-caddy-config + items: + - key: maintenance.html + path: maintenance.html diff --git a/drydock/templates/drydock/maintenance/k8s/service.yml b/drydock/templates/drydock/maintenance/k8s/service.yml new file mode 100644 index 00000000..f3343f3d --- /dev/null +++ b/drydock/templates/drydock/maintenance/k8s/service.yml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: maintenance-caddy + labels: + app.kubernetes.io/name: maintenance-caddy +spec: + type: ClusterIP + ports: + - port: 80 + name: http + selector: + app.kubernetes.io/name: maintenance-caddy diff --git a/drydock/templates/drydock/maintenance/static/maintenance.html b/drydock/templates/drydock/maintenance/static/maintenance.html new file mode 100644 index 00000000..a8f0cf13 --- /dev/null +++ b/drydock/templates/drydock/maintenance/static/maintenance.html @@ -0,0 +1,22 @@ + + + + + + Maintenance + + + +

Under maintenance

+

We will be back shortly.

+ + From bd1f8be543f7e31169b6539d7484a01204e14621 Mon Sep 17 00:00:00 2001 From: DonatoBD Date: Tue, 15 Sep 2026 15:01:07 -0400 Subject: [PATCH 4/4] chore: some improvements --- .github/environments/enabled/config.yml | 7 +++- .github/workflows/config-files-validation.yml | 8 ++--- README.md | 29 +++++++++++---- drydock/patches/drydock-edge-proxy-caddyfile | 24 +++++++++++++ .../drydock-edge-proxy-caddyfile-global | 20 +++++++++++ drydock/patches/kustomization | 15 -------- .../patches/kustomization-configmapgenerator | 15 +++----- drydock/patches/kustomization-resources | 6 ++-- drydock/plugin.py | 35 ++++--------------- .../drydock/edge-proxy/apps/caddy/Caddyfile | 34 ++++++++++++++++++ .../k8s/deployment.yml | 12 +++---- .../k8s/service.yml | 6 ++-- .../static/maintenance.html | 0 drydock/templates/drydock/k8s/ingress/cms.yml | 10 +++--- .../drydock/k8s/ingress/extra-hosts.yml | 6 ++-- drydock/templates/drydock/k8s/ingress/lms.yml | 20 +++++------ .../drydock/k8s/ingress/meilisearch.yml | 6 ++-- drydock/templates/drydock/k8s/ingress/mfe.yml | 6 ++-- .../templates/drydock/k8s/ingress/notes.yml | 6 ++-- .../drydock/maintenance/apps/caddy/Caddyfile | 23 ------------ 20 files changed, 161 insertions(+), 127 deletions(-) create mode 100644 drydock/patches/drydock-edge-proxy-caddyfile create mode 100644 drydock/patches/drydock-edge-proxy-caddyfile-global create mode 100644 drydock/templates/drydock/edge-proxy/apps/caddy/Caddyfile rename drydock/templates/drydock/{maintenance => edge-proxy}/k8s/deployment.yml (80%) rename drydock/templates/drydock/{maintenance => edge-proxy}/k8s/service.yml (52%) rename drydock/templates/drydock/{maintenance => edge-proxy}/static/maintenance.html (100%) delete mode 100644 drydock/templates/drydock/maintenance/apps/caddy/Caddyfile diff --git a/.github/environments/enabled/config.yml b/.github/environments/enabled/config.yml index 243c1fa2..5e4ed747 100644 --- a/.github/environments/enabled/config.yml +++ b/.github/environments/enabled/config.yml @@ -27,7 +27,12 @@ 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_MAINTENANCE_ENABLED: true +DRYDOCK_EDGE_PROXY_ENABLED: true +DRYDOCK_EDGE_PROXY_ROUTES: +- lms +- cms +DRYDOCK_EDGE_PROXY_ALLOWED_IPS: +- "10.0.0.0/8" 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 19a9730d..d410ccb9 100644 --- a/.github/workflows/config-files-validation.yml +++ b/.github/workflows/config-files-validation.yml @@ -71,12 +71,12 @@ jobs: caddy:2 \ caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile - - name: Validate generated maintenance Caddyfile + - name: Validate generated edge-proxy Caddyfile run: | - MAINTENANCE_CADDYFILE="$TUTOR_ROOT/env/plugins/drydock/maintenance/apps/caddy/Caddyfile" - if [ -f "$MAINTENANCE_CADDYFILE" ]; then + EDGE_PROXY_CADDYFILE="$TUTOR_ROOT/env/plugins/drydock/maintenance/apps/caddy/Caddyfile" + if [ -f "$EDGE_PROXY_CADDYFILE" ]; then docker run --rm \ - -v "$MAINTENANCE_CADDYFILE:/etc/caddy/Caddyfile:ro" \ + -v "$EDGE_PROXY_CADDYFILE:/etc/caddy/Caddyfile:ro" \ caddy:2 \ caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile fi diff --git a/README.md b/README.md index fa6eac1e..89caebc0 100644 --- a/README.md +++ b/README.md @@ -77,17 +77,34 @@ 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_MAINTENANCE_ENABLED`: Whether to deploy a dedicated maintenance Caddy and redirect all Drydock-managed Ingress backends to it. Defaults to `false`. -- `DRYDOCK_MAINTENANCE_HTTP_STATUS`: HTTP status code returned with the maintenance HTML page. Defaults to `503`. +- `DRYDOCK_EDGE_PROXY_ENABLED`: Whether to deploy a dedicated edge proxy (Caddy) and redirect all Ingress backends to it. Defaults to `false`. +- `DRYDOCK_EDGE_PROXY_HTTP_STATUS`: HTTP status code returned with the maintenance HTML page. Defaults to `503`. +- `DRYDOCK_EDGE_PROXY_ALLOWED_IPS`: List of CIDR ranges allowed to bypass the edge proxy and reach the original caddy. Defaults to `[]`. > **_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. -Maintenance mode ----------------- +Edge Proxy +---------- -When `DRYDOCK_MAINTENANCE_ENABLED` is `true` and `DRYDOCK_INGRESS` is enabled, Drydock deploys a `maintenance-caddy` service and uses Kustomize replacements to point LMS, Studio, MFE, Notes, Meilisearch, and `DRYDOCK_INGRESS_EXTRA_HOSTS` Ingress backends to it. The maintenance Caddy serves a default HTML page to external clients. +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. -Custom maintenance routing can be added through the `drydock-maintenance-caddyfile` Tutor patch. The `drydock-maintenance-caddyfile-global` patch is available for global Caddy options such as `trusted_proxies`. Drydock does not provide an IP restriction or bypass by default. Clients can implement it through these patches, keeping redirects, headers, and other maintenance-only rules isolated from the main Open edX Caddyfile. +The edge proxy serves a default HTML page with the configured HTTP status code (`DRYDOCK_EDGE_PROXY_HTTP_STATUS`, default 503) to external clients. + +**Built-in IP allowlist**: Configure `DRYDOCK_EDGE_PROXY_ALLOWED_IPS` with a list of CIDR ranges (e.g., `["10.0.0.0/8", "192.168.1.100/32"]`) to allow specific IPs to bypass the edge proxy and reach the original Open edX Caddy directly. + +**Custom routing**: Additional Caddy configuration can be added through the `drydock-edge-proxy-caddyfile` Tutor patch. The `drydock-edge-proxy-caddyfile-global` patch is available for global Caddy options such as `trusted_proxies`. This keeps custom rules isolated from the main Open edX Caddyfile. + +**Example - Allow specific paths to bypass maintenance**: +```caddyfile +# In drydock-edge-proxy-caddyfile patch +@bypass { + path /health + path /api/v1/status +} +handle @bypass { + reverse_proxy caddy:80 +} +``` Job generation diff --git a/drydock/patches/drydock-edge-proxy-caddyfile b/drydock/patches/drydock-edge-proxy-caddyfile new file mode 100644 index 00000000..1617d9be --- /dev/null +++ b/drydock/patches/drydock-edge-proxy-caddyfile @@ -0,0 +1,24 @@ +# Example patch to allow specific paths to bypass edge-proxy and go directly to caddy +# Enable this patch by adding it to your tutor config: +# DRYDOCK_PATCHES: +# - drydock-edge-proxy-caddyfile +# +# This allows requests matching @bypass to be proxied to the main caddy service +# instead of showing the maintenance page. +# +# Usage examples: +# - Allow /health endpoint to pass through +# - Allow specific API paths during maintenance +# - IP-based bypass (combine with DRYDOCK_EDGE_PROXY_ALLOWED_IPS) + +@bypass { + # Example: Allow health checks to pass through + path /health + # Example: Allow specific paths during maintenance + # path /api/v1/status + # path /heartbeat +} + +handle @bypass { + reverse_proxy caddy:80 +} \ No newline at end of file diff --git a/drydock/patches/drydock-edge-proxy-caddyfile-global b/drydock/patches/drydock-edge-proxy-caddyfile-global new file mode 100644 index 00000000..aa28fb25 --- /dev/null +++ b/drydock/patches/drydock-edge-proxy-caddyfile-global @@ -0,0 +1,20 @@ +# Example global patch for edge-proxy Caddyfile +# Enable this patch by adding it to your tutor config: +# DRYDOCK_PATCHES: +# - drydock-edge-proxy-caddyfile-global +# +# This patch adds global Caddy configuration options for edge-proxy + +{ + # Trusted proxies for correct client IP detection behind load balancers + trusted_proxies private_ranges + + # Example: Custom log format + # log { + # output stdout + # format json + # } + + # Example: Admin API endpoint (disabled by default for security) + # admin off +} \ No newline at end of file diff --git a/drydock/patches/kustomization b/drydock/patches/kustomization index 6a5cfd28..e69de29b 100644 --- a/drydock/patches/kustomization +++ b/drydock/patches/kustomization @@ -1,15 +0,0 @@ -{%- if DRYDOCK_MAINTENANCE_ENABLED and DRYDOCK_INGRESS %} -replacements: -{%- for ingress_name in maintenance_ingress_names() %} -- source: - kind: ConfigMap - name: maintenance-ingress-backend - fieldPath: data.service - targets: - - select: - kind: Ingress - name: {{ ingress_name }} - fieldPaths: - - spec.rules.*.http.paths.*.backend.service.name -{%- endfor %} -{%- endif %} diff --git a/drydock/patches/kustomization-configmapgenerator b/drydock/patches/kustomization-configmapgenerator index 57246207..4fae1fa2 100644 --- a/drydock/patches/kustomization-configmapgenerator +++ b/drydock/patches/kustomization-configmapgenerator @@ -6,17 +6,12 @@ labels: app.kubernetes.io/name: openedx {% endif -%} -{%- if DRYDOCK_MAINTENANCE_ENABLED %} -- name: maintenance-caddy-config +{%- if DRYDOCK_EDGE_PROXY_ENABLED %} +- name: edge-proxy-config files: - - plugins/drydock/maintenance/apps/caddy/Caddyfile - - plugins/drydock/maintenance/static/maintenance.html + - plugins/drydock/edge-proxy/apps/caddy/Caddyfile + - plugins/drydock/edge-proxy/static/maintenance.html options: labels: - app.kubernetes.io/name: maintenance-caddy -- name: maintenance-ingress-backend - literals: - - service=maintenance-caddy - options: - disableNameSuffixHash: true + app.kubernetes.io/name: edge-proxy {%- endif %} diff --git a/drydock/patches/kustomization-resources b/drydock/patches/kustomization-resources index c511c52a..ae0fc647 100644 --- a/drydock/patches/kustomization-resources +++ b/drydock/patches/kustomization-resources @@ -15,7 +15,7 @@ {% if DRYDOCK_REGISTRY_CREDENTIALS -%} - plugins/drydock/k8s/secrets/image-pull-secret.yml {% endif -%} -{%- if DRYDOCK_MAINTENANCE_ENABLED and DRYDOCK_INGRESS %} -- plugins/drydock/maintenance/k8s/deployment.yml -- plugins/drydock/maintenance/k8s/service.yml +{%- if DRYDOCK_EDGE_PROXY_ENABLED and DRYDOCK_INGRESS %} +- plugins/drydock/edge-proxy/k8s/deployment.yml +- plugins/drydock/edge-proxy/k8s/service.yml {%- endif %} diff --git a/drydock/plugin.py b/drydock/plugin.py index 84745567..0743cb93 100644 --- a/drydock/plugin.py +++ b/drydock/plugin.py @@ -48,8 +48,9 @@ ["lms", "cms", "forum", "lms-worker", "cms-worker", "superset", "superset-worker", "superset-celery-beat"], ), ("DRYDOCK_REGISTRY_CREDENTIALS", ""), - ("DRYDOCK_MAINTENANCE_ENABLED", False), - ("DRYDOCK_MAINTENANCE_HTTP_STATUS", 503), + ("DRYDOCK_EDGE_PROXY_ENABLED", False), + ("DRYDOCK_EDGE_PROXY_HTTP_STATUS", 503), + ("DRYDOCK_EDGE_PROXY_ALLOWED_IPS", []), ] ) @@ -178,29 +179,6 @@ def get_sync_waves_for_resource(resource_name: str) -> int: return get_sync_waves_order().get(resource_name, 0) -def maintenance_ingress_names() -> list[str]: - """Return Drydock Ingress names whose backends should point to maintenance-caddy.""" - if not TUTOR_CONFIG.get("DRYDOCK_INGRESS", True): - return [] - - names = ["lms", "cms"] - - if TUTOR_CONFIG.get("MFE_HOST") is not None: - names.append("mfe") - if TUTOR_CONFIG.get("RUN_MEILISEARCH", True): - names.append("meilisearch") - plugins = TUTOR_CONFIG.get("PLUGINS", []) - if isinstance(plugins, list) and "notes" in plugins: - names.append("notes") - - extra_hosts = TUTOR_CONFIG.get("DRYDOCK_INGRESS_EXTRA_HOSTS", []) - if isinstance(extra_hosts, list): - for host in extra_hosts: - names.append(str(host).replace(".", "-")) - - return names - - ################# You don't really have to bother about what's below this line, ################# except maybe for educational purposes :) @@ -211,9 +189,9 @@ def maintenance_ingress_names() -> list[str]: ("drydock/build", "plugins"), ("drydock/apps", "plugins"), ("drydock/k8s", "plugins"), - ("drydock/maintenance/k8s", "plugins"), - ("drydock/maintenance/apps", "plugins"), - ("drydock/maintenance/static", "plugins"), + ("drydock/edge-proxy/k8s", "plugins"), + ("drydock/edge-proxy/apps", "plugins"), + ("drydock/edge-proxy/static", "plugins"), ], ) # Load all patches from the "patches" folder @@ -230,7 +208,6 @@ def maintenance_ingress_names() -> list[str]: ("get_init_tasks", get_init_tasks), ("iter_sync_waves_order", iter_sync_waves_order), ("get_sync_waves_for_resource", get_sync_waves_for_resource), - ("maintenance_ingress_names", maintenance_ingress_names), ] ) diff --git a/drydock/templates/drydock/edge-proxy/apps/caddy/Caddyfile b/drydock/templates/drydock/edge-proxy/apps/caddy/Caddyfile new file mode 100644 index 00000000..aeb3a900 --- /dev/null +++ b/drydock/templates/drydock/edge-proxy/apps/caddy/Caddyfile @@ -0,0 +1,34 @@ +{{ patch("drydock-edge-proxy-caddyfile-global") }} + +:80 { + handle /health { + respond "OK" 200 + } + + {{ patch("drydock-edge-proxy-caddyfile") | indent(4) }} + + {% if DRYDOCK_EDGE_PROXY_ALLOWED_IPS %} + @allowed_ips { + {% for ip in DRYDOCK_EDGE_PROXY_ALLOWED_IPS %} + remote_ip {{ ip }} + {% endfor %} + } + handle @allowed_ips { + reverse_proxy caddy:80 + } + {% endif %} + + handle { + error {{ DRYDOCK_EDGE_PROXY_HTTP_STATUS }} + } + + handle_errors { + @maintenance expression {http.error.status_code} == {{ DRYDOCK_EDGE_PROXY_HTTP_STATUS }} + handle @maintenance { + root * /srv/maintenance + rewrite * /maintenance.html + header Content-Type "text/html; charset=utf-8" + file_server + } + } +} diff --git a/drydock/templates/drydock/maintenance/k8s/deployment.yml b/drydock/templates/drydock/edge-proxy/k8s/deployment.yml similarity index 80% rename from drydock/templates/drydock/maintenance/k8s/deployment.yml rename to drydock/templates/drydock/edge-proxy/k8s/deployment.yml index d4b9e398..d840f550 100644 --- a/drydock/templates/drydock/maintenance/k8s/deployment.yml +++ b/drydock/templates/drydock/edge-proxy/k8s/deployment.yml @@ -2,17 +2,17 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: maintenance-caddy + name: edge-proxy labels: - app.kubernetes.io/name: maintenance-caddy + app.kubernetes.io/name: edge-proxy spec: selector: matchLabels: - app.kubernetes.io/name: maintenance-caddy + app.kubernetes.io/name: edge-proxy template: metadata: labels: - app.kubernetes.io/name: maintenance-caddy + app.kubernetes.io/name: edge-proxy spec: containers: - name: caddy @@ -42,10 +42,10 @@ spec: volumes: - name: config configMap: - name: maintenance-caddy-config + name: edge-proxy-config - name: html configMap: - name: maintenance-caddy-config + name: edge-proxy-config items: - key: maintenance.html path: maintenance.html diff --git a/drydock/templates/drydock/maintenance/k8s/service.yml b/drydock/templates/drydock/edge-proxy/k8s/service.yml similarity index 52% rename from drydock/templates/drydock/maintenance/k8s/service.yml rename to drydock/templates/drydock/edge-proxy/k8s/service.yml index f3343f3d..c9ecd502 100644 --- a/drydock/templates/drydock/maintenance/k8s/service.yml +++ b/drydock/templates/drydock/edge-proxy/k8s/service.yml @@ -2,13 +2,13 @@ apiVersion: v1 kind: Service metadata: - name: maintenance-caddy + name: edge-proxy labels: - app.kubernetes.io/name: maintenance-caddy + app.kubernetes.io/name: edge-proxy spec: type: ClusterIP ports: - port: 80 name: http selector: - app.kubernetes.io/name: maintenance-caddy + app.kubernetes.io/name: edge-proxy diff --git a/drydock/templates/drydock/maintenance/static/maintenance.html b/drydock/templates/drydock/edge-proxy/static/maintenance.html similarity index 100% rename from drydock/templates/drydock/maintenance/static/maintenance.html rename to drydock/templates/drydock/edge-proxy/static/maintenance.html diff --git a/drydock/templates/drydock/k8s/ingress/cms.yml b/drydock/templates/drydock/k8s/ingress/cms.yml index 61bec919..5c493b13 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,10 +38,10 @@ 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 %} +{%- if DRYDOCK_AUTO_TLS or DRYDOCK_CUSTOM_CERTS %} tls: - hosts: - {{ CMS_HOST }} @@ -50,4 +50,4 @@ spec: {%- else %} secretName: cms-host-tls {%- endif %} - {%- endif %} +{%- endif %} diff --git a/drydock/templates/drydock/k8s/ingress/extra-hosts.yml b/drydock/templates/drydock/k8s/ingress/extra-hosts.yml index a1950734..c02852b5 100644 --- a/drydock/templates/drydock/k8s/ingress/extra-hosts.yml +++ b/drydock/templates/drydock/k8s/ingress/extra-hosts.yml @@ -19,10 +19,10 @@ 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 %} +{%- if DRYDOCK_AUTO_TLS or DRYDOCK_CUSTOM_CERTS %} tls: - hosts: - {{ host }} @@ -31,5 +31,5 @@ spec: {%- else %} secretName: {{ host|replace(".", "-") }}-tls {%- endif %} - {%- endif %} +{%- endif %} {%- endfor %} diff --git a/drydock/templates/drydock/k8s/ingress/lms.yml b/drydock/templates/drydock/k8s/ingress/lms.yml index df273a12..65ae1e3d 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,11 +38,11 @@ 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) }} - {%- for host in DRYDOCK_INGRESS_LMS_EXTRA_HOSTS %} +{%- for host in DRYDOCK_INGRESS_LMS_EXTRA_HOSTS %} - host: {{ host }} http: paths: @@ -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,12 +68,12 @@ 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) }} - {%- endfor %} - {%- if DRYDOCK_AUTO_TLS or DRYDOCK_CUSTOM_CERTS %} +{%- endfor %} +{%- if DRYDOCK_AUTO_TLS or DRYDOCK_CUSTOM_CERTS %} tls: - hosts: - {{ LMS_HOST }} @@ -85,4 +85,4 @@ spec: {%- else %} secretName: lms-host-tls {%- endif %} - {%- endif %} +{%- endif %} diff --git a/drydock/templates/drydock/k8s/ingress/meilisearch.yml b/drydock/templates/drydock/k8s/ingress/meilisearch.yml index c7e84ec4..2c43fcf5 100644 --- a/drydock/templates/drydock/k8s/ingress/meilisearch.yml +++ b/drydock/templates/drydock/k8s/ingress/meilisearch.yml @@ -18,10 +18,10 @@ 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 %} +{%- if DRYDOCK_AUTO_TLS or DRYDOCK_CUSTOM_CERTS %} tls: - hosts: - {{ MEILISEARCH_HOST }} @@ -30,5 +30,5 @@ spec: {%- else %} secretName: meilisearch-host-tls {%- endif %} - {%- endif %} +{%- endif %} {%- endif %} diff --git a/drydock/templates/drydock/k8s/ingress/mfe.yml b/drydock/templates/drydock/k8s/ingress/mfe.yml index 9b65bc1a..c4db4850 100644 --- a/drydock/templates/drydock/k8s/ingress/mfe.yml +++ b/drydock/templates/drydock/k8s/ingress/mfe.yml @@ -19,10 +19,10 @@ 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 %} +{%- if DRYDOCK_AUTO_TLS or DRYDOCK_CUSTOM_CERTS %} tls: - hosts: - {{ MFE_HOST }} @@ -31,5 +31,5 @@ spec: {%- else %} secretName: mfe-host-tls {%- endif %} - {%- endif %} +{%- endif %} {%- endif %} diff --git a/drydock/templates/drydock/k8s/ingress/notes.yml b/drydock/templates/drydock/k8s/ingress/notes.yml index a4550d65..41b71b6e 100644 --- a/drydock/templates/drydock/k8s/ingress/notes.yml +++ b/drydock/templates/drydock/k8s/ingress/notes.yml @@ -18,10 +18,10 @@ 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 %} +{%- if DRYDOCK_AUTO_TLS or DRYDOCK_CUSTOM_CERTS %} tls: - hosts: - {{ NOTES_HOST }} @@ -30,5 +30,5 @@ spec: {%- else %} secretName: notes-host-tls {%- endif %} - {%- endif %} +{%- endif %} {%- endif %} diff --git a/drydock/templates/drydock/maintenance/apps/caddy/Caddyfile b/drydock/templates/drydock/maintenance/apps/caddy/Caddyfile deleted file mode 100644 index 8aea0670..00000000 --- a/drydock/templates/drydock/maintenance/apps/caddy/Caddyfile +++ /dev/null @@ -1,23 +0,0 @@ -{{ patch("drydock-maintenance-caddyfile-global") }} - -:80 { - handle /health { - respond "OK" 200 - } - - {{ patch("drydock-maintenance-caddyfile") | indent(4) }} - - handle { - error {{ DRYDOCK_MAINTENANCE_HTTP_STATUS }} - } - - handle_errors { - @maintenance expression {http.error.status_code} == {{ DRYDOCK_MAINTENANCE_HTTP_STATUS }} - handle @maintenance { - root * /srv/maintenance - rewrite * /maintenance.html - header Content-Type "text/html; charset=utf-8" - file_server - } - } -}