diff --git a/k8s/ietfweb/django-config.yaml b/k8s/ietfweb/django-config.yaml new file mode 100644 index 00000000..6aedd8f8 --- /dev/null +++ b/k8s/ietfweb/django-config.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: django-config +data: + IETFWWW_ADMINS: |- + Robert Sparks + Kesara Rathnayake + IETFWWW_ALLOWED_HOSTS: ".ietf.org" # newline-separated list also allowed + + IETFWWW_DJANGO_SECRET_KEY: "PDwXboUq!=hPjnrtG2=ge#N$Dwy+wn@uivrugwpic8mxyPfHk" # secret + + + # IETFWWW_MATOMO_SITE_ID: "1" # must be present to enable Matomo + # IETFWWW_MATOMO_DOMAIN_PATH: "analytics.ietf.org" + + # use this to override default - one entry per line + # IETFWWW_CSRF_TRUSTED_ORIGINS: |- + # https://www.staging.ietf.org diff --git a/k8s/ietfweb/kustomization.yaml b/k8s/ietfweb/kustomization.yaml new file mode 100644 index 00000000..469ee3ac --- /dev/null +++ b/k8s/ietfweb/kustomization.yaml @@ -0,0 +1,13 @@ +namespace: ietfwww +namePrefix: ietfwww- +configMapGenerator: + - name: files-cfgmap + files: + - local.py + - supervisord.conf + - nginx-default.conf + - nginx.conf +resources: + - django-config.yaml + - memcached.yaml + - wagtail.yaml diff --git a/k8s/ietfweb/local.py b/k8s/ietfweb/local.py new file mode 100644 index 00000000..3392becc --- /dev/null +++ b/k8s/ietfweb/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@ietf.org" +SERVER_EMAIL = "donotreply@ietf.org" +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = os.environ.get("IETFWWW_EMAIL_HOST", "localhost") +EMAIL_PORT = int(os.environ.get("IETFWWW_EMAIL_PORT", "2025")) + +# Secrets +_SECRET_KEY = os.environ.get("IETFWWW_DJANGO_SECRET_KEY", None) +if _SECRET_KEY is not None: + SECRET_KEY = _SECRET_KEY +else: + raise RuntimeError("IETFWWW_DJANGO_SECRET_KEY must be set") + + +_CSRF_TRUSTED_ORIGINS_STR = os.environ.get("IETFWWW_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 IETFWWW_DEBUG env var is the word "true" +DEBUG = os.environ.get("IETFWWW_DEBUG", "false").lower() == "true" + +# IETFWWW_ALLOWED_HOSTS env var is a comma-separated list of allowed hosts +_ALLOWED_HOSTS_STR = os.environ.get("IETFWWW_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("IETFWWW_DB_HOST", "db"), + "PORT": os.environ.get("IETFWWW_DB_PORT", "5432"), + "NAME": os.environ.get("IETFWWW_DB_NAME", "ietfweb"), + "USER": os.environ.get("IETFWWW_DB_USER", "django"), + "PASSWORD": os.environ.get("IETFWWW_DB_PASS", ""), + "CONN_MAX_AGE": 600, # number of seconds database connections should persist for + }, +} + +# IETFWWW_ADMINS is a newline-delimited list of addresses parseable by email.utils.parseaddr +_admins_str = os.environ.get("IETFWWW_ADMINS", None) +if _admins_str is not None: + ADMINS = [parseaddr(admin) for admin in _multiline_to_list(_admins_str)] +else: + raise RuntimeError("IETFWWW_ADMINS must be set") + +# Leave IETFWWW_MATOMO_SITE_ID unset to disable Matomo reporting +if "IETFWWW_MATOMO_SITE_ID" in os.environ: + MATOMO_DOMAIN_PATH = os.environ.get("IETFWWW_MATOMO_DOMAIN_PATH", "analytics.ietf.org") + MATOMO_SITE_ID = os.environ.get("IETFWWW_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("IETFWWW_MEMCACHED_SERVICE_HOST", "127.0.0.1") +MEMCACHED_PORT = os.environ.get("IETFWWW_MEMCACHED_SERVICE_PORT", "11211") +MEMCACHED_KEY_PREFIX = "ietf" +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/ietfweb/memcached.yaml b/k8s/ietfweb/memcached.yaml new file mode 100644 index 00000000..bfd1093b --- /dev/null +++ b/k8s/ietfweb/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", "1024"] + 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/ietfweb/nginx-default.conf b/k8s/ietfweb/nginx-default.conf new file mode 100644 index 00000000..23e62478 --- /dev/null +++ b/k8s/ietfweb/nginx-default.conf @@ -0,0 +1,119 @@ +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 /charter { + alias /a/ietfdata/doc/charter/; + autoindex on; + autoindex_exact_size off; + autoindex_localtime on; + charset utf-8; + + location ~* \.xml$ { + add_header Content-Disposition 'attachment'; + } + + error_page 404 = @error_redirect; + } + location /cr { + alias /a/ietfdata/doc/conflict-review/; + autoindex on; + autoindex_exact_size off; + autoindex_localtime on; + charset utf-8; + + location ~* \.xml$ { + add_header Content-Disposition 'attachment'; + } + + error_page 404 = @error_redirect; + } + location /slides { + alias /a/ietfdata/doc/slides/; + autoindex on; + autoindex_exact_size off; + autoindex_localtime on; + charset utf-8; + + location ~* \.xml$ { + add_header Content-Disposition 'attachment'; + } + + error_page 404 = @error_redirect; + } + location /archive/id { + alias /a/ietfdata/draft/archive/; + autoindex on; + autoindex_exact_size off; + autoindex_localtime on; + charset utf-8; + + location ~* \.xml$ { + add_header Content-Disposition 'attachment'; + } + + error_page 404 = @error_redirect; + } + location /id { + alias /a/ietfdata/draft/repository; + autoindex on; + autoindex_exact_size off; + autoindex_localtime on; + charset utf-8; + + location ~* \.xml$ { + add_header Content-Disposition 'attachment'; + } + + error_page 404 = @error_redirect; + } + location /ietf-ftp { + alias /a/www/ietf-ftp; + autoindex on; + autoindex_exact_size off; + autoindex_localtime on; + charset utf-8; + + location ~* \.xml$ { + add_header Content-Disposition 'attachment'; + } + + error_page 404 = @error_redirect; + } + location /rfc { + alias /a/www/ietf-ftp/rfc; + autoindex on; + autoindex_exact_size off; + autoindex_localtime on; + charset utf-8; + + location ~* \.xml$ { + add_header Content-Disposition 'attachment'; + } + + error_page 404 = @error_redirect; + } + 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/ietfweb/nginx.conf b/k8s/ietfweb/nginx.conf new file mode 100644 index 00000000..66790987 --- /dev/null +++ b/k8s/ietfweb/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/ietfweb/supervisord.conf b/k8s/ietfweb/supervisord.conf new file mode 100644 index 00000000..afbbfb5c --- /dev/null +++ b/k8s/ietfweb/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/ietfweb/wagtail.yaml b/k8s/ietfweb/wagtail.yaml new file mode 100644 index 00000000..9d672dc9 --- /dev/null +++ b/k8s/ietfweb/wagtail.yaml @@ -0,0 +1,106 @@ +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: dt-vol + mountPath: /a + - name: www-tmp + mountPath: /tmp + - name: www-nginx + mountPath: /var/lib/nginx + - name: www-media + mountPath: /app/media + - name: www-cfg + mountPath: /app/supervisord.conf + subPath: supervisord.conf + - name: www-cfg + mountPath: /app/ietf/settings/local.py + subPath: local.py + - name: www-cfg + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + - name: www-cfg + mountPath: /etc/nginx/sites-enabled/default + subPath: nginx-default.conf + env: + - name: "CONTAINER_ROLE" + value: "ietfweb" + - name: "DJANGO_SETTINGS_MODULE" + value: "ietf.settings.production" + envFrom: + - configMapRef: + name: django-config + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsUser: 1000 + runAsGroup: 1000 + volumes: + # To be overriden with the actual shared volume + - name: dt-vol + # --- + - name: www-tmp + emptyDir: + sizeLimit: "1Gi" + - name: www-nginx + emptyDir: + sizeLimit: "1Gi" + - name: www-cfg + configMap: + name: files-cfgmap + dnsPolicy: ClusterFirst + restartPolicy: Always + terminationGracePeriodSeconds: 30 + volumeClaimTemplates: + - metadata: + name: www-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