Skip to content
Open
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
6 changes: 6 additions & 0 deletions .github/environments/enabled/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +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_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
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/config-files-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand All @@ -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/maintenance/apps/caddy/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:"
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,36 @@ 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 <https://github.com/overhangio/tutor/blob/v15.3.7/tutor/commands/k8s.py#L484>`_ 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`.
- `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.

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.

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
--------------

Expand Down
24 changes: 24 additions & 0 deletions drydock/patches/drydock-edge-proxy-caddyfile
Original file line number Diff line number Diff line change
@@ -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
}
20 changes: 20 additions & 0 deletions drydock/patches/drydock-edge-proxy-caddyfile-global
Original file line number Diff line number Diff line change
@@ -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
}
Empty file added drydock/patches/kustomization
Empty file.
9 changes: 9 additions & 0 deletions drydock/patches/kustomization-configmapgenerator
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,12 @@
labels:
app.kubernetes.io/name: openedx
{% endif -%}
{%- if DRYDOCK_EDGE_PROXY_ENABLED %}
- name: edge-proxy-config
files:
- plugins/drydock/edge-proxy/apps/caddy/Caddyfile
- plugins/drydock/edge-proxy/static/maintenance.html
options:
labels:
app.kubernetes.io/name: edge-proxy
{%- endif %}
4 changes: 4 additions & 0 deletions drydock/patches/kustomization-resources
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@
{% if DRYDOCK_REGISTRY_CREDENTIALS -%}
- plugins/drydock/k8s/secrets/image-pull-secret.yml
{% endif -%}
{%- if DRYDOCK_EDGE_PROXY_ENABLED and DRYDOCK_INGRESS %}
- plugins/drydock/edge-proxy/k8s/deployment.yml
- plugins/drydock/edge-proxy/k8s/service.yml
{%- endif %}
6 changes: 6 additions & 0 deletions drydock/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
["lms", "cms", "forum", "lms-worker", "cms-worker", "superset", "superset-worker", "superset-celery-beat"],
),
("DRYDOCK_REGISTRY_CREDENTIALS", ""),
("DRYDOCK_EDGE_PROXY_ENABLED", False),
("DRYDOCK_EDGE_PROXY_HTTP_STATUS", 503),
("DRYDOCK_EDGE_PROXY_ALLOWED_IPS", []),
]
)

Expand Down Expand Up @@ -186,6 +189,9 @@ def get_sync_waves_for_resource(resource_name: str) -> int:
("drydock/build", "plugins"),
("drydock/apps", "plugins"),
("drydock/k8s", "plugins"),
("drydock/edge-proxy/k8s", "plugins"),
("drydock/edge-proxy/apps", "plugins"),
("drydock/edge-proxy/static", "plugins"),
],
)
# Load all patches from the "patches" folder
Expand Down
34 changes: 34 additions & 0 deletions drydock/templates/drydock/edge-proxy/apps/caddy/Caddyfile
Original file line number Diff line number Diff line change
@@ -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
}
}
}
51 changes: 51 additions & 0 deletions drydock/templates/drydock/edge-proxy/k8s/deployment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
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 }}
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: edge-proxy-config
- name: html
configMap:
name: edge-proxy-config
items:
- key: maintenance.html
path: maintenance.html
14 changes: 14 additions & 0 deletions drydock/templates/drydock/edge-proxy/k8s/service.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
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
22 changes: 22 additions & 0 deletions drydock/templates/drydock/edge-proxy/static/maintenance.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Maintenance</title>
<style>
body {
font-family: sans-serif;
text-align: center;
padding: 4rem;
background: #f5f5f5;
color: #333;
}
h1 { margin-bottom: .5rem; }
</style>
</head>
<body>
<h1>Under maintenance</h1>
<p>We will be back shortly.</p>
</body>
</html>
10 changes: 5 additions & 5 deletions drydock/templates/drydock/k8s/ingress/cms.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand All @@ -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 }}
Expand All @@ -50,4 +50,4 @@ spec:
{%- else %}
secretName: cms-host-tls
{%- endif %}
{%- endif %}
{%- endif %}
6 changes: 3 additions & 3 deletions drydock/templates/drydock/k8s/ingress/extra-hosts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -31,5 +31,5 @@ spec:
{%- else %}
secretName: {{ host|replace(".", "-") }}-tls
{%- endif %}
{%- endif %}
{%- endif %}
{%- endfor %}
Loading