diff --git a/k8s/iabweb/kustomization.yaml b/k8s/iabweb/kustomization.yaml new file mode 100644 index 00000000..eba3d8e9 --- /dev/null +++ b/k8s/iabweb/kustomization.yaml @@ -0,0 +1,12 @@ +namespace: iabwww +namePrefix: iabwww- +configMapGenerator: + - name: files-cfgmap + files: + - local.py + - supervisord.conf + - nginx-default.conf + - nginx.conf +resources: + - memcached.yaml + - wagtail.yaml diff --git a/k8s/iabweb/local.py b/k8s/iabweb/local.py new file mode 100644 index 00000000..ce5a82d4 --- /dev/null +++ b/k8s/iabweb/local.py @@ -0,0 +1,111 @@ +# Copyright The IETF Trust 2007-2024, All Rights Reserved +# -*- coding: utf-8 -*- + +from email.utils import parseaddr +import os + +def _multiline_to_list(s): + """Helper to split at newlines and conver to list""" + return [item.strip() for item in s.split("\n")] + + +DEFAULT_FROM_EMAIL = "donotreply@iab.org" +SERVER_EMAIL = "donotreply@iab.org" +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = os.environ.get("IABWWW_EMAIL_HOST", "localhost") +EMAIL_PORT = int(os.environ.get("IABWWW_EMAIL_PORT", "2025")) + +# Secrets +_SECRET_KEY = os.environ.get("IABWWW_DJANGO_SECRET_KEY", None) +if _SECRET_KEY is not None: + SECRET_KEY = _SECRET_KEY +else: + raise RuntimeError("IABWWW_DJANGO_SECRET_KEY must be set") + + +_CSRF_TRUSTED_ORIGINS_STR = os.environ.get("IABWWW_CSRF_TRUSTED_ORIGINS", None) +if _CSRF_TRUSTED_ORIGINS_STR is not None: + CSRF_TRUSTED_ORIGINS = _multiline_to_list(_CSRF_TRUSTED_ORIGINS_STR) + +FILE_UPLOAD_PERMISSIONS = 0o664 +_WAGTAILADMIN_BASE_URL = os.environ.get("WAGTAILADMIN_BASE_URL", None) +if _WAGTAILADMIN_BASE_URL is not None: + WAGTAILADMIN_BASE_URL = _WAGTAILADMIN_BASE_URL +else: + raise RuntimeError("WAGTAILADMIN_BASE_URL must be present") + +# Set DEBUG if IABWWW_DEBUG env var is the word "true" +DEBUG = os.environ.get("IABWWW_DEBUG", "false").lower() == "true" + +# IABWWW_ALLOWED_HOSTS env var is a comma-separated list of allowed hosts +_ALLOWED_HOSTS_STR = os.environ.get("IABWWW_ALLOWED_HOSTS", None) +if _ALLOWED_HOSTS_STR is not None: + ALLOWED_HOSTS = _multiline_to_list(_ALLOWED_HOSTS_STR) + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "HOST": os.environ.get("IABWWW_DB_HOST", "db"), + "PORT": os.environ.get("IABWWW_DB_PORT", "5432"), + "NAME": os.environ.get("IABWWW_DB_NAME", "iabweb"), + "USER": os.environ.get("IABWWW_DB_USER", "django"), + "PASSWORD": os.environ.get("IABWWW_DB_PASS", ""), + "CONN_MAX_AGE": 600, # number of seconds database connections should persist for + }, +} + +# IABWWW_ADMINS is a newline-delimited list of addresses parseable by email.utils.parseaddr +_admins_str = os.environ.get("IABWWW_ADMINS", None) +if _admins_str is not None: + ADMINS = [parseaddr(admin) for admin in _multiline_to_list(_admins_str)] +else: + raise RuntimeError("IABWWW_ADMINS must be set") + +# Leave IABWWW_MATOMO_SITE_ID unset to disable Matomo reporting +if "IABWWW_MATOMO_SITE_ID" in os.environ: + MATOMO_DOMAIN_PATH = os.environ.get("IABWWW_MATOMO_DOMAIN_PATH", "analytics.ietf.org") + MATOMO_SITE_ID = os.environ.get("IABWWW_MATOMO_SITE_ID", None) + MATOMO_DISABLE_COOKIES = True + +# Duplicating production cache from settings.py and using it whether we're in production mode or not +MEMCACHED_HOST = os.environ.get("IABWWW_MEMCACHED_SERVICE_HOST", "127.0.0.1") +MEMCACHED_PORT = os.environ.get("IABWWW_MEMCACHED_SERVICE_PORT", "11211") +MEMCACHED_KEY_PREFIX = "iab" +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", + "LOCATION": f"{MEMCACHED_HOST}:{MEMCACHED_PORT}", + "KEY_PREFIX": MEMCACHED_KEY_PREFIX, + }, + "sessions": { + "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", + "LOCATION": f"{MEMCACHED_HOST}:{MEMCACHED_PORT}", + "KEY_PREFIX": MEMCACHED_KEY_PREFIX, + }, + "dummy": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}, +} + +# Logging + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "mail_admins": { + "level": "ERROR", + "class": "django.utils.log.AdminEmailHandler", + }, + }, + "loggers": { + "django.request": { + "handlers": ["mail_admins"], + "level": "ERROR", + "propagate": False, + }, + "django.security": { + "handlers": ["mail_admins"], + "level": "ERROR", + "propagate": False, + }, + }, +} diff --git a/k8s/iabweb/memcached.yaml b/k8s/iabweb/memcached.yaml new file mode 100644 index 00000000..8ad91ca1 --- /dev/null +++ b/k8s/iabweb/memcached.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: memcached +spec: + replicas: 1 + revisionHistoryLimit: 2 + selector: + matchLabels: + app: memcached + template: + metadata: + labels: + app: memcached + spec: + securityContext: + runAsNonRoot: true + containers: + - image: "quay.io/prometheus/memcached-exporter:v0.14.3" + imagePullPolicy: IfNotPresent + name: memcached-exporter + ports: + - name: metrics + containerPort: 9150 + protocol: TCP + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsUser: 65534 # nobody + runAsGroup: 65534 # nobody + - image: "memcached:1.6-alpine" + imagePullPolicy: IfNotPresent + args: ["-m", "256"] + name: memcached + ports: + - name: memcached + containerPort: 11211 + protocol: TCP + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + # memcached image sets up uid/gid 11211 + runAsUser: 11211 + runAsGroup: 11211 + dnsPolicy: ClusterFirst + restartPolicy: Always + terminationGracePeriodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: memcached + annotations: + k8s.grafana.com/scrape: "true" # this is not a bool + k8s.grafana.com/metrics.portName: "metrics" +spec: + type: ClusterIP + ports: + - port: 11211 + targetPort: memcached + protocol: TCP + name: memcached + - port: 9150 + targetPort: metrics + protocol: TCP + name: metrics + selector: + app: memcached diff --git a/k8s/iabweb/nginx-default.conf b/k8s/iabweb/nginx-default.conf new file mode 100644 index 00000000..a2650d7e --- /dev/null +++ b/k8s/iabweb/nginx-default.conf @@ -0,0 +1,32 @@ +server { + listen 8080 default_server; + listen [::]:8080 default_server; + server_name _; + gzip on; + access_log /dev/stdout; + error_log /dev/stdout warn; + location / { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $${keepempty}host; + proxy_set_header X-Forwarded-For $${keepempty}proxy_add_x_forwarded_for; + } + location /media/ { + alias /app/media/; + + error_page 404 = @error_redirect; + } + location /static/ { + alias /app/static/; + + error_page 404 = @error_redirect; + } + location /robots.txt { + add_header Content-Type text/plain; + return 200 "User-agent: *\nDisallow: /admin/\n"; + } + location @error_redirect { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $${keepempty}host; + proxy_set_header X-Forwarded-For $${keepempty}proxy_add_x_forwarded_for; + } +} diff --git a/k8s/iabweb/nginx.conf b/k8s/iabweb/nginx.conf new file mode 100644 index 00000000..66790987 --- /dev/null +++ b/k8s/iabweb/nginx.conf @@ -0,0 +1,53 @@ +worker_processes auto; +pid /var/lib/nginx/nginx.pid; +error_log /dev/stdout; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 768; + # multi_accept on; +} + +http { + + ## + # Basic Settings + ## + + sendfile on; + tcp_nopush on; + types_hash_max_size 2048; + # server_tokens off; + + # server_names_hash_bucket_size 64; + # server_name_in_redirect off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ## + # SSL Settings + ## + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE + ssl_prefer_server_ciphers on; + + ## + # Logging Settings + ## + + access_log /dev/stdout; + + ## + # Gzip Settings + ## + + gzip on; + + ## + # Virtual Host Configs + ## + + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; +} diff --git a/k8s/iabweb/secrets.yaml b/k8s/iabweb/secrets.yaml new file mode 100644 index 00000000..4cf0e823 --- /dev/null +++ b/k8s/iabweb/secrets.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secrets-env +type: Opaque +stringData: + IABWWW_ADMINS: |- + Robert Sparks + Kesara Rathnayake + + IABWWW_ALLOWED_HOSTS: ".iab.org" # newline-separated list also allowed + WAGTAILADMIN_BASE_URL: "https://www.iab.org/admin/" + + # Outgoing email details + IABWWW_EMAIL_HOST: "iab.mr.ietf.org" + IABWWW_EMAIL_PORT: "10027" + + IABWWW_MATOMO_SITE_ID: null # must be present to enable Matomo + + # Can also be a newline-separated list + IABWWW_CSRF_TRUSTED_ORIGINS: "https://www.iab.org" + + # Database connection details - to be fetched from Vault + # IABWWW_DB_HOST: "" + # IABWWW_DB_NAME: "" + # IABWWW_DB_PASS: "" + # IABWWW_DB_PORT: "" + # IABWWW_DB_USER: "" + + # Django secret key - to be fetched from Vault + # IABWWW_DJANGO_SECRET_KEY: "" diff --git a/k8s/iabweb/supervisord.conf b/k8s/iabweb/supervisord.conf new file mode 100644 index 00000000..afbbfb5c --- /dev/null +++ b/k8s/iabweb/supervisord.conf @@ -0,0 +1,17 @@ +[supervisord] +nodaemon=true +logfile=/dev/stdout +logfile_maxbytes=0 + +[program:nginx] +command=nginx -g "daemon off;" +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +redirect_stderr=true + +[program:gunicorn] +command=/usr/local/bin/gunicorn --config /app/docker/gunicorn.py ietf.wsgi +directory=/app +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +redirect_stderr=true diff --git a/k8s/iabweb/wagtail.yaml b/k8s/iabweb/wagtail.yaml new file mode 100644 index 00000000..f065e03a --- /dev/null +++ b/k8s/iabweb/wagtail.yaml @@ -0,0 +1,104 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: wagtail +spec: + replicas: 1 + revisionHistoryLimit: 2 + selector: + matchLabels: + app: wagtail + template: + metadata: + labels: + app: wagtail + spec: + securityContext: + fsGroup: 1000 + runAsNonRoot: true + containers: + # ----------------------------------------------------- + # wagtail Container + # ----------------------------------------------------- + - name: wagtail + image: "ghcr.io/ietf-tools/www:$APP_IMAGE_TAG" + imagePullPolicy: Always + ports: + - containerPort: 8080 + name: http + protocol: TCP + volumeMounts: + - name: iabwww-tmp + mountPath: /tmp + - name: iabwww-nginx + mountPath: /var/lib/nginx + - name: iabwww-media + mountPath: /app/media + - name: iabwww-cfg + mountPath: /app/supervisord.conf + subPath: supervisord.conf + - name: iabwww-cfg + mountPath: /app/ietf/settings/local.py + subPath: local.py + - name: iabwww-cfg + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + - name: iabwww-cfg + mountPath: /etc/nginx/sites-enabled/default + subPath: nginx-default.conf + env: + - name: "CONTAINER_ROLE" + value: "iabweb" + - name: "DJANGO_SETTINGS_MODULE" + value: "ietf.settings.production" + # ensures the pod gets recreated on every deploy: + - name: "DEPLOY_UID" + value: "$DEPLOY_UID" + envFrom: + - secretRef: + name: iabwww-secrets-env + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsUser: 1000 + runAsGroup: 1000 + volumes: + - name: iabwww-tmp + emptyDir: + sizeLimit: "1Gi" + - name: iabwww-nginx + emptyDir: + sizeLimit: "1Gi" + - name: iabwww-cfg + configMap: + name: files-cfgmap + dnsPolicy: ClusterFirst + restartPolicy: Always + terminationGracePeriodSeconds: 30 + volumeClaimTemplates: + - metadata: + name: iabwww-media + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + storageClassName: "generic" +--- +apiVersion: v1 +kind: Service +metadata: + name: wagtail +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app: wagtail