AI overhaul

This commit is contained in:
2026-10-23 12:49:53 -04:00
parent b040ca26c3
commit b4db1a0182
21 changed files with 1226 additions and 189 deletions
+71
View File
@@ -0,0 +1,71 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: membership-config
namespace: {{ www_namespace }}
data:
{% for key, value in www_membership_environment | dictsort %}
{{ key }}: {{ value | string | to_json }}
{% endfor %}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: membership-tiers
namespace: {{ www_namespace }}
data:
tiers.json: {{ www_membership_tiers | to_json | to_json }}
---
# Only Apache may assert the trusted proxy headers to the membership service.
# Requires a CNI which actually enforces NetworkPolicy.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: membership-web
namespace: {{ www_namespace }}
spec:
podSelector:
matchLabels:
app: membership
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels:
app: httpd
ports:
- protocol: TCP
port: 8000
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: membership-worker
namespace: {{ www_namespace }}
spec:
podSelector:
matchLabels:
app: membership-worker
policyTypes: [Ingress]
ingress: []
{% if www_membership_postgres_managed %}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: membership-db
namespace: {{ www_namespace }}
spec:
podSelector:
matchLabels:
app: membership-db
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels:
www-membership-client: "true"
ports:
- protocol: TCP
port: 5432
{% endif %}
+22
View File
@@ -0,0 +1,22 @@
{% if www_membership_enabled %}
ProxyRequests Off
ProxyPreserveHost On
ProxyPass "/members/" "http://membership:8000/members/" connectiontimeout=5 timeout=120
ProxyPassReverse "/members/" "http://membership:8000/members/"
<Location "/members/">
# NetworkPolicy permits only Apache to assert these headers.
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Membership-Client-IP "expr=%{REMOTE_ADDR}"
LimitRequestBody 262144
# The Tor/public static mirror remains available, but account/billing flows
# use only the canonical HTTPS origin registered in Dex and Stripe.
<If "%{HTTP_HOST} != '{{ www_hostname }}'">
Require all denied
</If>
</Location>
{% else %}
# Membership proxy disabled. Existing stateful workloads are not deleted.
<Location "/members/">
Require all denied
</Location>
{% endif %}
+16
View File
@@ -0,0 +1,16 @@
{% set component = 'migrate' %}
apiVersion: batch/v1
kind: Job
metadata:
name: membership-migrate-{{ _www_membership_revision[:16] }}
namespace: {{ www_namespace }}
spec:
backoffLimit: 2
activeDeadlineSeconds: {{ www_wait_timeout }}
template:
metadata:
labels:
app: membership-migrate
www-membership-client: "true"
spec:
{% filter indent(6, true) %}{% include 'membership-pod.yml.j2' %}{% endfilter %}
+79
View File
@@ -0,0 +1,79 @@
# Shared pod spec for web, worker and migration Job.
automountServiceAccountToken: false
restartPolicy: {{ 'OnFailure' if component == 'migrate' else 'Always' }}
{% if www_image_pull_secrets | length %}
imagePullSecrets:
{{ www_image_pull_secrets | to_nice_yaml | indent(2, true) }}
{% endif %}
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: {{ component }}
image: {{ www_membership_image | to_json }}
imagePullPolicy: {{ www_membership_image_pull_policy }}
{% if component == 'migrate' %}
command: [python, manage.py, migrate, --noinput]
{% elif component == 'worker' %}
command: [python, manage.py, sync_memberships, --loop, --interval, {{ www_membership_worker_interval | string | to_json }}]
{% endif %}
envFrom:
- configMapRef:
name: membership-config
- secretRef:
name: {{ www_membership_secret_name }}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
resources:
{{ (www_membership_worker_resources if component == 'worker' else www_membership_resources) | to_nice_yaml | indent(6, true) }}
{% if component == 'web' %}
ports:
- name: http
containerPort: 8000
startupProbe:
tcpSocket: {port: http}
periodSeconds: 5
failureThreshold: 24
readinessProbe:
exec:
command:
- python
- -c
- {{ ("import http.client; c=http.client.HTTPConnection('127.0.0.1',8000,timeout=3); c.request('GET','/members/',headers={'Host': '" ~ www_hostname ~ "', 'X-Forwarded-Proto': 'https'}); r=c.getresponse(); assert r.status==200; c.close()") | to_json }}
timeoutSeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket: {port: http}
periodSeconds: 20
{% endif %}
volumeMounts:
- name: tiers
mountPath: /etc/membership/tiers
readOnly: true
- name: tmp
mountPath: /tmp
{% if www_membership_ca_secret_name | length %}
- name: ca
mountPath: /etc/membership/ca
readOnly: true
{% endif %}
volumes:
- name: tiers
configMap:
name: membership-tiers
- name: tmp
emptyDir:
sizeLimit: 64Mi
{% if www_membership_ca_secret_name | length %}
- name: ca
secret:
secretName: {{ www_membership_ca_secret_name }}
defaultMode: 0444
{% endif %}
+90
View File
@@ -0,0 +1,90 @@
apiVersion: v1
kind: Service
metadata:
name: membership-db
namespace: {{ www_namespace }}
spec:
type: ClusterIP
clusterIP: None
selector:
app: membership-db
ports:
- name: postgres
port: 5432
targetPort: postgres
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: membership-db
namespace: {{ www_namespace }}
spec:
serviceName: membership-db
replicas: 1
selector:
matchLabels:
app: membership-db
template:
metadata:
labels:
app: membership-db
spec:
automountServiceAccountToken: false
{% if www_image_pull_secrets | length %}
imagePullSecrets:
{{ www_image_pull_secrets | to_nice_yaml | indent(8, true) }}
{% endif %}
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
fsGroup: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: postgres
image: {{ www_membership_postgres_image | to_json }}
env:
- name: POSTGRES_DB
value: {{ www_membership_environment.POSTGRES_DB | to_json }}
- name: POSTGRES_USER
value: {{ www_membership_environment.POSTGRES_USER | to_json }}
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ www_membership_secret_name }}
key: POSTGRES_PASSWORD
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- name: postgres
containerPort: 5432
readinessProbe:
exec:
command: [sh, -c, 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"']
periodSeconds: 5
startupProbe:
exec:
command: [sh, -c, 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"']
failureThreshold: 60
periodSeconds: 5
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
resources:
{{ www_membership_postgres_resources | to_nice_yaml | indent(12, true) }}
volumeMounts:
- name: database
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: database
spec:
accessModes: [ReadWriteOnce]
{% if www_membership_postgres_storage_class | length %}
storageClassName: {{ www_membership_postgres_storage_class | to_json }}
{% endif %}
resources:
requests:
storage: {{ www_membership_postgres_storage_size }}
+41
View File
@@ -0,0 +1,41 @@
{% for component in ['web', 'worker'] %}
{% set deployment = 'membership' if component == 'web' else 'membership-worker' %}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ deployment }}
namespace: {{ www_namespace }}
spec:
replicas: {{ www_membership_replicas if component == 'web' else 1 }}
{% if component == 'worker' %}
strategy:
type: Recreate
{% endif %}
selector:
matchLabels:
app: {{ deployment }}
template:
metadata:
labels:
app: {{ deployment }}
www-membership-client: "true"
annotations:
membership/config-revision: {{ _www_membership_revision | to_json }}
spec:
{% filter indent(6, true) %}{% include 'membership-pod.yml.j2' %}{% endfilter %}
{% endfor %}
---
apiVersion: v1
kind: Service
metadata:
name: membership
namespace: {{ www_namespace }}
spec:
type: ClusterIP
selector:
app: membership
ports:
- name: http
port: 8000
targetPort: http
+172
View File
@@ -0,0 +1,172 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
namespace: {{ www_namespace }}
spec:
accessModes: [ReadWriteMany]
resources:
requests:
storage: {{ www_archive_storage_size }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: httpd
namespace: {{ www_namespace }}
data:
httpd.conf: {{ lookup('ansible.builtin.file', role_path ~ '/files/httpd.conf') | to_json }}
membership.conf: {{ _www_proxy_config | to_json }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: httpd
namespace: {{ www_namespace }}
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: httpd
template:
metadata:
labels:
app: httpd
annotations:
www/config-revision: {{ (lookup('ansible.builtin.file', role_path ~ '/files/httpd.conf') ~ _www_proxy_config ~ www_rollout_revision ~ www_repo_url ~ www_repo_version) | hash('sha256') | to_json }}
spec:
automountServiceAccountToken: false
{% if www_image_pull_secrets | length %}
imagePullSecrets:
{{ www_image_pull_secrets | to_nice_yaml | indent(8, true) }}
{% endif %}
initContainers:
- name: init
image: {{ www_source_image | to_json }}
command:
- /bin/bash
- -ec
- |
apt-get update
apt-get install --no-install-recommends -y git ca-certificates
git clone --depth 1 -- "$WWW_REPO_URL" /tmp/www
git -C /tmp/www fetch --depth 1 origin "$WWW_REPO_VERSION"
git -C /tmp/www checkout --detach FETCH_HEAD
# Never delete /data: it contains persistent archives, not just
# this repository's static site. Copy hidden site assets too.
cp -a /tmp/www/htdocs/. /data/
env:
- name: WWW_REPO_URL
value: {{ www_repo_url | to_json }}
- name: WWW_REPO_VERSION
value: {{ www_repo_version | to_json }}
volumeMounts:
- name: data
mountPath: /data
containers:
- name: httpd
image: {{ www_httpd_image | to_json }}
imagePullPolicy: Always
ports:
- name: http
containerPort: 80
- name: metrics
containerPort: 9117
readinessProbe:
tcpSocket: {port: http}
volumeMounts:
- name: data
mountPath: /usr/local/apache2/htdocs
- name: httpd
mountPath: /usr/local/apache2/conf/httpd.conf
subPath: httpd.conf
readOnly: true
- name: httpd
mountPath: /usr/local/apache2/conf/extra/membership.conf
subPath: membership.conf
readOnly: true
volumes:
- name: data
persistentVolumeClaim:
claimName: data
- name: httpd
configMap:
name: httpd
---
apiVersion: v1
kind: Service
metadata:
name: prometheus
namespace: {{ www_namespace }}
spec:
type: ClusterIP
selector:
app: httpd
ports:
- name: http
port: 9117
targetPort: metrics
---
apiVersion: v1
kind: Service
metadata:
name: httpd
namespace: {{ www_namespace }}
spec:
type: ClusterIP
selector:
app: httpd
ports:
- name: http
port: 80
targetPort: http
{% if www_onion_enabled %}
---
apiVersion: tor.k8s.torproject.org/v1alpha2
kind: OnionService
metadata:
name: httpd
namespace: {{ www_namespace }}
spec:
version: 3
rules:
- port:
number: 80
backend:
service:
name: httpd
port:
number: 80
{% endif %}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: www
namespace: {{ www_namespace }}
annotations:
cert-manager.io/cluster-issuer: {{ www_cluster_issuer | to_json }}
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# Redact email verification tokens and OIDC codes at the edge, not only in
# Apache/Gunicorn. Do not enable request-body/error tracing for /members/.
nginx.ingress.kubernetes.io/enable-access-log: "false"
nginx.ingress.kubernetes.io/proxy-body-size: "256k"
nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
spec:
ingressClassName: {{ www_ingress_class }}
rules:
- host: {{ www_hostname }}
http:
paths:
- pathType: Prefix
path: /
backend:
service:
name: httpd
port:
number: 80
tls:
- hosts: [{{ www_hostname }}]
secretName: www