From 6f9d46e8332b1ee13a23f4c8d306050dec1297d5 Mon Sep 17 00:00:00 2001 From: itumi Date: Mon, 16 Feb 2026 15:37:06 +0200 Subject: [PATCH] helm --- CLAUDE.md | 60 ++ helm/Chart.yaml | 3 + app.yaml => helm/templates/app.yaml | 261 +++-- config.yaml => helm/templates/config.yaml | 219 ++-- .../templates/deployment.yaml | 354 +++---- grafana.yaml => helm/templates/grafana.yaml | 953 +++++++++--------- .../templates/monitoring.yaml | 29 +- helm/templates/oidc.yaml | 19 + helm/values.yaml | 2 + oidc.yaml | 20 - 10 files changed, 983 insertions(+), 937 deletions(-) create mode 100644 CLAUDE.md create mode 100644 helm/Chart.yaml rename app.yaml => helm/templates/app.yaml (79%) rename config.yaml => helm/templates/config.yaml (97%) rename deployment.yaml => helm/templates/deployment.yaml (70%) rename grafana.yaml => helm/templates/grafana.yaml (87%) rename monitoring.yaml => helm/templates/monitoring.yaml (66%) create mode 100644 helm/templates/oidc.yaml create mode 100644 helm/values.yaml delete mode 100644 oidc.yaml diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5251f97 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,60 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Kubernetes deployment manifests for "Memelord Jake" — a Django meme-sharing application deployed on a cloud-native Kubernetes cluster. This repo contains **no application source code**, only infrastructure-as-code YAML manifests. + +The Django app image is `ghcr.io/l4rm4nd/memelord:latest`. The cluster domain is `ee-lte-1.codemowers.io`. + +## Deploying + +```bash +kubectl apply -f deployment.yaml # Backing services: PostgreSQL, Redis, S3, networking +kubectl apply -f config.yaml # Django settings.py ConfigMap +kubectl apply -f app.yaml # Memelord Deployment +kubectl apply -f oidc.yaml # OIDC client configuration +kubectl apply -f grafana.yaml # Grafana monitoring (includes namespace) +kubectl apply -f monitoring.yaml # Prometheus Probe +``` + +The cluster requires these operators pre-installed: CloudNativePG, DragonflyDB, Onyxia S3, cert-manager, Traefik, Codemowers Cloud OIDC, Prometheus operator. + +## File Map + +- **`deployment.yaml`** — Backing services: StringSecret + Dragonfly (Redis), StringSecret + Cluster + Database (PostgreSQL), Policy + S3User + Bucket (S3), Service + Certificate + Ingress +- **`config.yaml`** — ConfigMap containing the full Django `settings.py`; the largest and most complex file. Configures DB, cache, security headers (CSP/HSTS), storage backends, OIDC, logging +- **`app.yaml`** — Deployment for the Django app (1 replica, port 8000). Mounts `settings.py` from ConfigMap via `subPath`. All config injected via environment variables from Secrets +- **`grafana.yaml`** — Complete Grafana stack: Namespace, ConfigMaps (Prometheus + Loki datasources, dashboard JSON), StatefulSet (5Gi SQLite), OIDC auth, Ingress with TLS +- **`oidc.yaml`** — OIDCClient CR for Memelord app authentication via Passmower +- **`monitoring.yaml`** — Prometheus Probe CR + +## Architecture + +``` +Namespace: memelord-jake + +Memelord (Deployment) ──► PostgreSQL (CloudNativePG Cluster) + ──► DragonflyDB (Redis-compatible cache/sessions) + ──► MinIO S3 (media storage via Onyxia operator) + ──► Passmower OIDC (authentication) + +Grafana (StatefulSet) ──► Prometheus (monitoring ns) + ──► Loki (monitoring ns) + ──► Passmower OIDC (authentication) + +External access: Traefik Ingress + cert-manager TLS + - memelord-jake.ee-lte-1.codemowers.io + - grafana-jake.ee-lte-1.codemowers.io +``` + +## Key Conventions + +- Resource naming: prefix `memelord-jake-` for all backing services +- Secrets auto-generated via `StringSecret` CRs (mittwald secret generator) +- Django settings are fully environment-driven (12-factor); `config.yaml` reads everything from env vars +- Storage class `postgres` for DB, `sqlite` for Grafana +- Node selector: `codemowers.io/lvm-ubuntu-vg: enterprise-ssd` +- ArgoCD destination cluster: `https://10.254.10.31:6443` +- S3 uses path-style addressing (`AWS_S3_ADDRESSING_STYLE = 'path'`) diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 0000000..5b8294f --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: memelord +version: 0.1.0 diff --git a/app.yaml b/helm/templates/app.yaml similarity index 79% rename from app.yaml rename to helm/templates/app.yaml index 4a65417..7198719 100644 --- a/app.yaml +++ b/helm/templates/app.yaml @@ -1,131 +1,130 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: memelord - namespace: memelord-jake -spec: - replicas: 1 - selector: - matchLabels: - app: memelord - template: - metadata: - labels: - app: memelord - spec: - containers: - - name: memelord - image: ghcr.io/l4rm4nd/memelord:latest - imagePullPolicy: Always - ports: - - name: http - containerPort: 8000 - - env: - - name: DOMAIN - value: "memelord-jake.ee-lte-1.codemowers.io" - - # Database Configuration - - name: DB_ENGINE - value: "postgres" - - name: POSTGRES_HOST - value: "memelord-jake-database-rw" - - name: POSTGRES_PORT - value: "5432" - - name: POSTGRES_DB - value: "memelord-jake" - - name: POSTGRES_USER - valueFrom: - secretKeyRef: - name: memelord-jake-database - key: username - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: memelord-jake-database - key: password - - # Redis Configuration - - name: REDIS_HOST - value: "memelord-jake-redis" - - name: REDIS_PORT - value: "6379" - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - name: memelord-jake-redis - key: redis-password - - # S3/MinIO Storage Configuration - - name: STORAGE_BACKEND - value: "s3" - - name: AWS_ACCESS_KEY_ID - valueFrom: - secretKeyRef: - name: memelord-jake-bucket - key: accessKey - - name: AWS_SECRET_ACCESS_KEY - valueFrom: - secretKeyRef: - name: memelord-jake-bucket - key: secretKey - - name: AWS_S3_ADDRESSING_STYLE - value: path - - name: AWS_STORAGE_BUCKET_NAME - value: "memelord-jake" - - name: AWS_S3_ENDPOINT_URL - value: "https://minio.ee-lte-1.codemowers.io" - - name: AWS_S3_REGION_NAME - value: "ee-lte-1" - - # OIDC Configuration - - name: OIDC_ENABLED - value: "True" - - name: OIDC_CREATE_USER - value: "True" - - name: OIDC_RP_CLIENT_ID - valueFrom: - secretKeyRef: - name: oidc-client-memelord-jake-owner-secrets - key: OIDC_CLIENT_ID - - name: OIDC_RP_CLIENT_SECRET - valueFrom: - secretKeyRef: - name: oidc-client-memelord-jake-owner-secrets - key: OIDC_CLIENT_SECRET - - # Browser-facing endpoint (external URL) - - name: OIDC_OP_AUTHORIZATION_ENDPOINT - value: "https://auth.ee-lte-1.codemowers.io/auth" - - # Server-to-server endpoints (internal URLs) - - name: OIDC_OP_TOKEN_ENDPOINT - value: "http://passmower.passmower.svc.cluster.local/token" - - name: OIDC_OP_USER_ENDPOINT - value: "http://passmower.passmower.svc.cluster.local/me" - - name: OIDC_OP_JWKS_ENDPOINT - value: "http://passmower.passmower.svc.cluster.local/jwks" - - - name: OIDC_RP_SIGN_ALGO - value: "RS256" - - name: OIDC_AUTOLOGIN - value: "False" - - # General Configuration - - name: DEBUG - value: "True" - - name: SECURE_COOKIES - value: "True" - - # Use the patched app code (including patched settings.py) - volumeMounts: - - name: settings - mountPath: /opt/app/myproject/settings.py - subPath: settings.py - readOnly: true - - volumes: - - name: settings - configMap: - name: settings \ No newline at end of file +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} +spec: + replicas: 1 + selector: + matchLabels: + app: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ .Release.Name }} + spec: + containers: + - name: {{ .Release.Name }} + image: ghcr.io/l4rm4nd/memelord:latest + imagePullPolicy: Always + ports: + - name: http + containerPort: 8000 + + env: + - name: DOMAIN + value: {{ .Values.hostname | quote }} + + # Database Configuration + - name: DB_ENGINE + value: "postgres" + - name: POSTGRES_HOST + value: "{{ .Release.Name }}-database-rw" + - name: POSTGRES_PORT + value: "5432" + - name: POSTGRES_DB + value: {{ .Release.Name | quote }} + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-database + key: username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-database + key: password + + # Redis Configuration + - name: REDIS_HOST + value: "{{ .Release.Name }}-redis" + - name: REDIS_PORT + value: "6379" + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-redis + key: redis-password + + # S3/MinIO Storage Configuration + - name: STORAGE_BACKEND + value: "s3" + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-bucket + key: accessKey + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-bucket + key: secretKey + - name: AWS_S3_ADDRESSING_STYLE + value: path + - name: AWS_STORAGE_BUCKET_NAME + value: {{ .Release.Name | quote }} + - name: AWS_S3_ENDPOINT_URL + value: "https://minio.ee-lte-1.codemowers.io" + - name: AWS_S3_REGION_NAME + value: "ee-lte-1" + + # OIDC Configuration + - name: OIDC_ENABLED + value: "True" + - name: OIDC_CREATE_USER + value: "True" + - name: OIDC_RP_CLIENT_ID + valueFrom: + secretKeyRef: + name: oidc-client-{{ .Release.Name }}-owner-secrets + key: OIDC_CLIENT_ID + - name: OIDC_RP_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: oidc-client-{{ .Release.Name }}-owner-secrets + key: OIDC_CLIENT_SECRET + + # Browser-facing endpoint (external URL) + - name: OIDC_OP_AUTHORIZATION_ENDPOINT + value: "https://auth.ee-lte-1.codemowers.io/auth" + + # Server-to-server endpoints (internal URLs) + - name: OIDC_OP_TOKEN_ENDPOINT + value: "http://passmower.passmower.svc.cluster.local/token" + - name: OIDC_OP_USER_ENDPOINT + value: "http://passmower.passmower.svc.cluster.local/me" + - name: OIDC_OP_JWKS_ENDPOINT + value: "http://passmower.passmower.svc.cluster.local/jwks" + + - name: OIDC_RP_SIGN_ALGO + value: "RS256" + - name: OIDC_AUTOLOGIN + value: "False" + + # General Configuration + - name: DEBUG + value: "True" + - name: SECURE_COOKIES + value: "True" + + # Use the patched app code (including patched settings.py) + volumeMounts: + - name: settings + mountPath: /opt/app/myproject/settings.py + subPath: settings.py + readOnly: true + + volumes: + - name: settings + configMap: + name: settings diff --git a/config.yaml b/helm/templates/config.yaml similarity index 97% rename from config.yaml rename to helm/templates/config.yaml index 1233d99..4a5139a 100644 --- a/config.yaml +++ b/helm/templates/config.yaml @@ -2,21 +2,20 @@ apiVersion: v1 kind: ConfigMap metadata: name: settings - namespace: memelord-jake data: settings.py: | """ Django settings for myproject project. - + Generated by 'django-admin startproject' using Django 3.2.16. - + For more information on this file, see https://docs.djangoproject.com/en/3.2/topics/settings/ - + For the full list of settings and their values, see https://docs.djangoproject.com/en/3.2/ref/settings/ """ - + from pathlib import Path from dotenv import load_dotenv import os @@ -25,33 +24,33 @@ data: from django.utils.html import escape from django.utils.translation import gettext_lazy as _ from csp.constants import NONE, SELF, UNSAFE_INLINE, UNSAFE_EVAL - + # Load environment variables from .env file load_dotenv() - + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent - + # get debug modus from env DEBUG = os.environ.get('DEBUG', 'False').lower() in ['true'] - + # get container version from env VERSION = escape(os.environ.get("VERSION", '')) - + # Enable/disable public meme feed feature (must be after import os) ENABLE_PUBLIC_FEED = os.environ.get('ENABLE_PUBLIC_FEED', 'False').lower() in ['true', '1', 'yes'] - + # auto-generate a secure secret key or use from env variable SECRET_KEY = os.environ.get("SECRET_KEY", secrets.token_urlsafe(32)) - + # define allowed hosts and trusted domains via env variables DOMAIN = "" ALLOWED_HOSTS = ["127.0.0.1"] CSRF_TRUSTED_ORIGINS = ["http://127.0.0.1:8000"] - + DOMAIN = str(os.environ.get("DOMAIN", "localhost")) TRUSTED_PORT = str(os.environ.get("PORT", "8000")) - + if DOMAIN: domains = DOMAIN.split(',') for domain in domains: @@ -63,14 +62,14 @@ data: TRUSTED_USER_DOMAIN_HTTPS = f"https://{domain}:{TRUSTED_PORT}" TRUSTED_USER_DOMAIN_HTTPS_443_DEFAULT = f"https://{domain}" CSRF_TRUSTED_ORIGINS.extend([TRUSTED_USER_DOMAIN_HTTP, TRUSTED_USER_DOMAIN_HTTPS, TRUSTED_USER_DOMAIN_HTTP_80_DEFAULT, TRUSTED_USER_DOMAIN_HTTPS_443_DEFAULT]) - + #Session Management CSRF_COOKIE_HTTPONLY = True SESSION_EXPIRE_AT_BROWSER_CLOSE = os.environ.get('SESSION_EXPIRE_AT_BROWSER_CLOSE', 'True').lower() in ['true'] SESSION_COOKIE_AGE = int(os.environ.get('SESSION_COOKIE_AGE', '30')) * 60 SESSION_COOKIE_NAME = 'Session' SESSION_COOKIE_SAMESITE = 'Lax' - + # ============================================================================= # REDIS CACHE CONFIGURATION (for sessions and general caching) # ============================================================================= @@ -79,7 +78,7 @@ data: REDIS_PORT = os.environ.get("REDIS_PORT", "6379") REDIS_DB = os.environ.get("REDIS_DB", "0") REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD", "") - + if REDIS_HOST: # Redis is available - use it for caching and sessions CACHES = { @@ -103,7 +102,7 @@ data: "TIMEOUT": 300, # 5 minutes default } } - + # Use Redis for session storage (cloud-native) SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_CACHE_ALIAS = "default" @@ -111,9 +110,9 @@ data: # Redis not configured - use database sessions (backward compatible) SESSION_ENGINE = "django.contrib.sessions.backends.db" # No CACHES configuration - Django will use local memory cache - + SECURE_COOKIES = os.environ.get('SECURE_COOKIES', 'False').lower() in ['true'] - + if SECURE_COOKIES: # transmit cookies over encrypted https only SESSION_COOKIE_SECURE = True @@ -126,22 +125,22 @@ data: # transmit cookies over unencrypted http SESSION_COOKIE_SECURE = False CSRF_COOKIE_SECURE = False - + # http security response headers SECURE_BROWSER_XSS_FILTER = True SECURE_CONTENT_TYPE_NOSNIFF = True X_FRAME_OPTIONS = 'DENY' REFERRER_POLICY = 'same-origin' - + # Load from environment, default to "'self'" raw_frame_ancestors = os.environ.get("CSP_FRAME_ANCESTORS", "'none'") # Split by comma, strip spaces, and keep properly quoted entries FRAME_ANCESTORS = [item.strip() for item in raw_frame_ancestors.split(',') if item.strip()] - + # Build CSP img-src list dynamically based on storage backend STORAGE_BACKEND = os.environ.get('STORAGE_BACKEND', 'local').lower() IMG_SRC_LIST = ["'self'", "data:", "blob:", "https://img.logo.dev"] - + # Add S3 domains to CSP if using S3 storage if STORAGE_BACKEND == 's3': AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME') @@ -149,11 +148,11 @@ data: AWS_S3_REGION_NAME = os.environ.get('AWS_S3_REGION_NAME', 'us-east-1') AWS_S3_ENDPOINT_URL = os.environ.get('AWS_S3_ENDPOINT_URL') AWS_S3_USE_ACCELERATE_ENDPOINT = os.environ.get('AWS_S3_USE_ACCELERATE_ENDPOINT', 'False').lower() in ['true'] - + # Always add custom domain if specified (CDN like CloudFront) if AWS_S3_CUSTOM_DOMAIN: IMG_SRC_LIST.append(f"https://{AWS_S3_CUSTOM_DOMAIN}") - + # Detect S3 provider based on endpoint URL if AWS_S3_ENDPOINT_URL: # S3-compatible service detected @@ -161,92 +160,92 @@ data: parsed_url = urlparse(AWS_S3_ENDPOINT_URL) endpoint_domain = parsed_url.netloc endpoint_scheme = parsed_url.scheme or 'https' - + # Add the endpoint domain IMG_SRC_LIST.append(f"{endpoint_scheme}://{endpoint_domain}") - + # Add bucket-based subdomain format if applicable if endpoint_domain and AWS_STORAGE_BUCKET_NAME: IMG_SRC_LIST.append(f"{endpoint_scheme}://{AWS_STORAGE_BUCKET_NAME}.{endpoint_domain}") - + # Provider-specific URL patterns endpoint_lower = endpoint_domain.lower() if endpoint_domain else "" - + # DigitalOcean Spaces: also add CDN domain if 'digitaloceanspaces.com' in endpoint_lower: # DigitalOcean Spaces CDN format: bucket-name.region.cdn.digitaloceanspaces.com if AWS_STORAGE_BUCKET_NAME and AWS_S3_REGION_NAME: IMG_SRC_LIST.append(f"https://{AWS_STORAGE_BUCKET_NAME}.{AWS_S3_REGION_NAME}.cdn.digitaloceanspaces.com") - + # Cloudflare R2: also add public bucket URL format elif 'r2.cloudflarestorage.com' in endpoint_lower: # Cloudflare R2 can use custom domains via public.r2.dev # Format: https://bucket-name.account-id.r2.dev (if public) # This is typically set via AWS_S3_CUSTOM_DOMAIN, but we note it here pass - + # Wasabi: supports path-style and virtual-hosted-style elif 'wasabisys.com' in endpoint_lower: # Already covered by endpoint_domain and bucket subdomain pass - - # Linode Object Storage: supports path-style and virtual-hosted-style + + # Linode Object Storage: supports path-style and virtual-hosted-style elif 'linodeobjects.com' in endpoint_lower: # Already covered by endpoint_domain and bucket subdomain pass - + # Backblaze B2: supports path-style and virtual-hosted-style elif 'backblazeb2.com' in endpoint_lower: # Already covered by endpoint_domain and bucket subdomain pass - + # MinIO: custom deployment, already covered # Other S3-compatible services: already covered - + else: # No endpoint URL = Standard AWS S3 if AWS_STORAGE_BUCKET_NAME: # Add AWS S3 virtual-hosted-style URLs IMG_SRC_LIST.append(f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com") IMG_SRC_LIST.append(f"https://{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.amazonaws.com") - + # Add path-style URL format (legacy but still supported) IMG_SRC_LIST.append("https://s3.amazonaws.com") IMG_SRC_LIST.append(f"https://s3.{AWS_S3_REGION_NAME}.amazonaws.com") - + # Add dual-stack endpoints (IPv6 support) IMG_SRC_LIST.append(f"https://{AWS_STORAGE_BUCKET_NAME}.s3.dualstack.{AWS_S3_REGION_NAME}.amazonaws.com") - + # Add S3 Transfer Acceleration endpoint if enabled if AWS_S3_USE_ACCELERATE_ENDPOINT: IMG_SRC_LIST.append(f"https://{AWS_STORAGE_BUCKET_NAME}.s3-accelerate.amazonaws.com") IMG_SRC_LIST.append(f"https://{AWS_STORAGE_BUCKET_NAME}.s3-accelerate.dualstack.amazonaws.com") - + # Add Azure domains to CSP if using Azure storage elif STORAGE_BACKEND == 'azure': AZURE_ACCOUNT_NAME = os.environ.get('AZURE_ACCOUNT_NAME') AZURE_CUSTOM_DOMAIN = os.environ.get('AZURE_CUSTOM_DOMAIN') - + if AZURE_CUSTOM_DOMAIN: IMG_SRC_LIST.append(f"https://{AZURE_CUSTOM_DOMAIN}") - + if AZURE_ACCOUNT_NAME: IMG_SRC_LIST.append(f"https://{AZURE_ACCOUNT_NAME}.blob.core.windows.net") - + # Add GCS domains to CSP if using GCS storage elif STORAGE_BACKEND == 'gcs': GS_BUCKET_NAME = os.environ.get('GS_BUCKET_NAME') GS_CUSTOM_ENDPOINT = os.environ.get('GS_CUSTOM_ENDPOINT') - + if GS_CUSTOM_ENDPOINT: IMG_SRC_LIST.append(GS_CUSTOM_ENDPOINT) - + if GS_BUCKET_NAME: # GCS can use multiple URL formats IMG_SRC_LIST.append("https://storage.googleapis.com") IMG_SRC_LIST.append("https://storage.cloud.google.com") IMG_SRC_LIST.append(f"https://{GS_BUCKET_NAME}.storage.googleapis.com") - + # SFTP/Dropbox typically serve through Django (using 'self' origin) elif STORAGE_BACKEND == 'sftp': # SFTP files are retrieved by Django and served through Django views @@ -254,17 +253,17 @@ data: SFTP_CUSTOM_DOMAIN = os.environ.get('SFTP_CUSTOM_DOMAIN') if SFTP_CUSTOM_DOMAIN: IMG_SRC_LIST.append(f"https://{SFTP_CUSTOM_DOMAIN}") - + elif STORAGE_BACKEND == 'dropbox': # Dropbox files are retrieved by Django and served through Django views # No additional CSP configuration needed pass - + CSP_IMG_SRC_EXTRA = os.environ.get('CSP_IMG_SRC_EXTRA', '') - if CSP_IMG_SRC_EXTRA: - extra_domains = [domain.strip() for domain in CSP_IMG_SRC_EXTRA.split(',') if domain.strip()] + if CSP_IMG_SRC_EXTRA: + extra_domains = [domain.strip() for domain in CSP_IMG_SRC_EXTRA.split(',') if domain.strip()] IMG_SRC_LIST.extend(extra_domains) - + CONTENT_SECURITY_POLICY = { "DIRECTIVES": { "default-src": ["'self'"], @@ -277,7 +276,7 @@ data: "frame-ancestors": FRAME_ANCESTORS, }, } - + # Application definition INSTALLED_APPS = [ 'myapp', @@ -291,7 +290,7 @@ data: 'csp', 'storages', ] - + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -303,9 +302,9 @@ data: 'django_http_referrer_policy.middleware.ReferrerPolicyMiddleware', 'csp.middleware.CSPMiddleware', ] - + ROOT_URLCONF = 'myproject.urls' - + TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -321,29 +320,29 @@ data: }, }, ] - + # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases - + DB_ENGINE = os.environ.get("DB_ENGINE", "sqlite3") - + if DB_ENGINE == "sqlite3": - + DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'database', 'db.sqlite3'), } } - + elif DB_ENGINE == "postgres": - + DB_HOST = os.environ.get("POSTGRES_HOST", "db") DB_PORT = os.environ.get("POSTGRES_PORT", "5432") DB_USER = os.environ.get("POSTGRES_USER", "memelord") DB_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "memelord") DB_NAME = os.environ.get("POSTGRES_DB", "memelord") - + DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', @@ -354,10 +353,10 @@ data: 'PASSWORD': DB_PASSWORD, } } - + # Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators - + AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', @@ -372,7 +371,7 @@ data: 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] - + # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ LANGUAGE_CODE = 'en-us' @@ -380,23 +379,23 @@ data: USE_I18N = True USE_L10N = True USE_TZ = True - + LANGUAGES = [ ('en', _('English')), ('de', _('German')), ('fr', _('French')), ('it', _('Italian')), ] - + LOCALE_PATHS = [ os.path.join(BASE_DIR, 'locale') ] - + # Celery configuration # http://docs.celeryproject.org/en/latest/configuration.html - + LOGS_DIR = os.path.join(BASE_DIR, 'logs') - + # ============================================================================= # LOGGING CONFIGURATION # ============================================================================= @@ -458,30 +457,30 @@ data: 'level': 'INFO', }, } - + # Create logs directory if it doesn't exist os.makedirs(LOGS_DIR, exist_ok=True) - + STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'myapp', 'static') - + DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - + LOGIN_URL = '/accounts/login/' LOGIN_REDIRECT_URL = '/' LOGOUT_REDIRECT_URL = '/post-logout/' ALLOW_LOGOUT_GET_METHOD = True - + WSGI_APPLICATION = 'myproject.wsgi.application' - + # check if oidc is enabled OIDC_ENABLED = os.environ.get('OIDC_ENABLED', 'False').lower() in ['true'] OIDC_AUTOLOGIN = os.environ.get('OIDC_AUTOLOGIN', 'False').lower() in ['true'] - + # Max file upload size in MB (default: 10MB) # Can be configured via MAX_UPLOAD_SIZE_MB environment variable MAX_UPLOAD_SIZE_MB = int(os.environ.get('MAX_UPLOAD_SIZE_MB', '10')) * 1024 * 1024 - + # ============================================================================= # STORAGE BACKEND CONFIGURATION # ============================================================================= @@ -489,9 +488,9 @@ data: # Configure storage backend via STORAGE_BACKEND environment variable # Supported backends: local, s3, azure, gcs, sftp, dropbox # Default: local (filesystem storage) - + STORAGE_BACKEND = os.environ.get('STORAGE_BACKEND', 'local').lower() - + # Django 5.0+ uses STORAGES instead of DEFAULT_FILE_STORAGE # Set both for compatibility if STORAGE_BACKEND == 's3': @@ -509,14 +508,14 @@ data: AWS_QUERYSTRING_AUTH = os.environ.get('AWS_QUERYSTRING_AUTH', 'True').lower() in ['true'] AWS_S3_FILE_OVERWRITE = os.environ.get('AWS_S3_FILE_OVERWRITE', 'False').lower() in ['true'] AWS_LOCATION = os.environ.get('AWS_LOCATION', 'media') - + # Use signature version 4 (required for all regions) AWS_S3_SIGNATURE_VERSION = 's3v4' - + # Use virtual-hosted-style URLs (bucket-name.s3.region.amazonaws.com) # This is the default and recommended format AWS_S3_ADDRESSING_STYLE = 'path' - + # Django 5.0+ STORAGES configuration STORAGES = { "default": { @@ -526,10 +525,10 @@ data: "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", }, } - + # Legacy setting for older Django versions DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' - + # Update MEDIA_URL if AWS_S3_CUSTOM_DOMAIN: MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{AWS_LOCATION}/' @@ -541,9 +540,9 @@ data: MEDIA_URL = f'https://{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.amazonaws.com/{AWS_LOCATION}/' else: MEDIA_URL = "/media/" - + MEDIA_ROOT = BASE_DIR / "media" - + elif STORAGE_BACKEND == 'azure': # Microsoft Azure Blob Storage AZURE_ACCOUNT_NAME = os.environ.get('AZURE_ACCOUNT_NAME') @@ -557,7 +556,7 @@ data: AZURE_OVERWRITE_FILES = os.environ.get('AZURE_OVERWRITE_FILES', 'False').lower() in ['true'] AZURE_LOCATION = os.environ.get('AZURE_LOCATION', '') AZURE_CUSTOM_DOMAIN = os.environ.get('AZURE_CUSTOM_DOMAIN') - + STORAGES = { "default": { "BACKEND": "storages.backends.azure_storage.AzureStorage", @@ -566,18 +565,18 @@ data: "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", }, } - + DEFAULT_FILE_STORAGE = 'storages.backends.azure_storage.AzureStorage' - + if AZURE_CUSTOM_DOMAIN: MEDIA_URL = f'https://{AZURE_CUSTOM_DOMAIN}/{AZURE_LOCATION}' elif AZURE_ACCOUNT_NAME: MEDIA_URL = f'https://{AZURE_ACCOUNT_NAME}.blob.core.windows.net/{AZURE_CONTAINER}/{AZURE_LOCATION}' else: MEDIA_URL = "/media/" - + MEDIA_ROOT = BASE_DIR / "media" - + elif STORAGE_BACKEND == 'gcs': # Google Cloud Storage GS_BUCKET_NAME = os.environ.get('GS_BUCKET_NAME') @@ -588,7 +587,7 @@ data: GS_LOCATION = os.environ.get('GS_LOCATION', 'media') GS_CUSTOM_ENDPOINT = os.environ.get('GS_CUSTOM_ENDPOINT') GS_QUERYSTRING_AUTH = os.environ.get('GS_QUERYSTRING_AUTH', 'True').lower() in ['true'] - + STORAGES = { "default": { "BACKEND": "storages.backends.gcloud.GoogleCloudStorage", @@ -597,18 +596,18 @@ data: "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", }, } - + DEFAULT_FILE_STORAGE = 'storages.backends.gcloud.GoogleCloudStorage' - + if GS_CUSTOM_ENDPOINT: MEDIA_URL = f'{GS_CUSTOM_ENDPOINT}/{GS_LOCATION}/' elif GS_BUCKET_NAME: MEDIA_URL = f'https://storage.googleapis.com/{GS_BUCKET_NAME}/{GS_LOCATION}/' else: MEDIA_URL = "/media/" - + MEDIA_ROOT = BASE_DIR / "media" - + elif STORAGE_BACKEND == 'sftp': # SFTP Storage SFTP_STORAGE_HOST = os.environ.get('SFTP_STORAGE_HOST') @@ -625,7 +624,7 @@ data: SFTP_STORAGE_UID = os.environ.get('SFTP_STORAGE_UID') SFTP_STORAGE_GID = os.environ.get('SFTP_STORAGE_GID') SFTP_KNOWN_HOST_FILE = os.environ.get('SFTP_KNOWN_HOST_FILE') - + STORAGES = { "default": { "BACKEND": "storages.backends.sftpstorage.SFTPStorage", @@ -634,18 +633,18 @@ data: "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", }, } - + DEFAULT_FILE_STORAGE = 'storages.backends.sftpstorage.SFTPStorage' MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media" - + elif STORAGE_BACKEND == 'dropbox': # Dropbox Storage DROPBOX_OAUTH2_TOKEN = os.environ.get('DROPBOX_OAUTH2_TOKEN') DROPBOX_ROOT_PATH = os.environ.get('DROPBOX_ROOT_PATH', '/media') DROPBOX_TIMEOUT = int(os.environ.get('DROPBOX_TIMEOUT', '100')) DROPBOX_WRITE_MODE = os.environ.get('DROPBOX_WRITE_MODE', 'add') - + STORAGES = { "default": { "BACKEND": "storages.backends.dropbox.DropBoxStorage", @@ -654,11 +653,11 @@ data: "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", }, } - + DEFAULT_FILE_STORAGE = 'storages.backends.dropbox.DropBoxStorage' MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media" - + else: # Local filesystem storage (default) STORAGES = { @@ -669,11 +668,11 @@ data: "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", }, } - + DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media" - + if OIDC_ENABLED: # get oidc config from env OIDC_CREATE_USER = os.environ.get('OIDC_CREATE_USER', 'True').lower() in ['true'] @@ -687,20 +686,20 @@ data: OIDC_OP_USER_ENDPOINT = os.environ.get('OIDC_OP_USER_ENDPOINT') OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS = float(os.environ.get('OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS', 900)) OIDC_USERNAME_ALGO = 'myapp.utils.generate_username' - + # Add 'mozilla_django_oidc.middleware.SessionRefresh' to INSTALLED_APPS INSTALLED_APPS.append('mozilla_django_oidc') - + # Add 'mozilla_django_oidc' authentication backend AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'mozilla_django_oidc.auth.OIDCAuthenticationBackend', ) - + # Add 'mozilla_django_oidc.middleware.SessionRefresh' to MIDDLEWARE # https://mozilla-django-oidc.readthedocs.io/en/stable/installation.html#validate-id-tokens-by-renewing-them MIDDLEWARE.append('mozilla_django_oidc.middleware.SessionRefresh') - + # Fix http callback issue in mozilla-django-oidc by forcing https; https://github.com/mozilla/mozilla-django-oidc/issues/417 # OIDC should only be setup behind a TLS reverse proxy anyways SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") diff --git a/deployment.yaml b/helm/templates/deployment.yaml similarity index 70% rename from deployment.yaml rename to helm/templates/deployment.yaml index ec2d4f0..6c2d790 100644 --- a/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -1,177 +1,177 @@ ---- -apiVersion: secretgenerator.mittwald.de/v1alpha1 -kind: StringSecret -metadata: - name: memelord-jake-redis -spec: - fields: - - fieldName: redis-password - length: "32" - encoding: hex ---- -apiVersion: dragonflydb.io/v1alpha1 -kind: Dragonfly -metadata: - name: memelord-jake-redis -spec: - authentication: - passwordFromSecret: - name: memelord-jake-redis - key: redis-password - replicas: 1 - resources: - requests: - cpu: 500m - memory: 500Mi - limits: - cpu: 600m - memory: 750Mi ---- -apiVersion: secretgenerator.mittwald.de/v1alpha1 -kind: StringSecret -metadata: - name: memelord-jake-database - labels: - cnpg.io/reload: "true" -spec: - data: - username: memelord-jake - fields: - - fieldName: password - length: "32" - encoding: hex ---- -apiVersion: postgresql.cnpg.io/v1 -kind: Cluster -metadata: - name: memelord-jake-database -spec: - instances: 1 - imageName: ghcr.io/cloudnative-pg/postgresql:17 - storage: - size: 1Gi - storageClass: postgres - affinity: - podAntiAffinityType: required - nodeSelector: - codemowers.io/lvm-ubuntu-vg: enterprise-ssd - resources: - requests: - cpu: "100m" - memory: "1Gi" - limits: - cpu: "1" - memory: "4Gi" - postgresql: - parameters: - max_connections: "300" - shared_buffers: "512MB" - effective_cache_size: "2GB" - managed: - roles: - - name: memelord-jake - ensure: present - login: true - passwordSecret: - name: memelord-jake-database ---- -apiVersion: postgresql.cnpg.io/v1 -kind: Database -metadata: - name: memelord-jake -spec: - name: memelord-jake - owner: memelord-jake - cluster: - name: memelord-jake-database ---- -apiVersion: s3.onyxia.sh/v1alpha1 -kind: Policy -metadata: - name: memelord-jake-policy -spec: - name: memelord-jake-policy - s3InstanceRef: minio/default - policyContent: >- - { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "s3:*" - ], - "Resource": [ - "arn:aws:s3:::memelord-jake", - "arn:aws:s3:::memelord-jake/*" - ] - } - ] - } ---- -apiVersion: s3.onyxia.sh/v1alpha1 -kind: S3User -metadata: - name: memelord-jake-bucket -spec: - accessKey: memelord-jake-bucket - policies: - - memelord-jake-policy - s3InstanceRef: minio/default ---- -apiVersion: s3.onyxia.sh/v1alpha1 -kind: Bucket -metadata: - name: memelord-jake -spec: - name: memelord-jake - s3InstanceRef: minio/default - quota: - default: 100000000 ---- -apiVersion: v1 -kind: Service -metadata: - name: memelord -spec: - type: ClusterIP - selector: - app: memelord - ports: - - name: http - port: 80 - targetPort: 8000 ---- -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: memelord-jake -spec: - secretName: memelord-jake-tls - dnsNames: - - memelord-jake.ee-lte-1.codemowers.io - issuerRef: - name: letsencrypt - kind: ClusterIssuer ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: memelord-jake - annotations: - traefik.ingress.kubernetes.io/router.entrypoints: websecure -spec: - ingressClassName: traefik - rules: - - host: memelord-jake.ee-lte-1.codemowers.io - http: - paths: - - pathType: Prefix - path: "/" - backend: - service: - name: memelord - port: - number: 80 - tls: - - secretName: memelord-jake-tls +--- +apiVersion: secretgenerator.mittwald.de/v1alpha1 +kind: StringSecret +metadata: + name: {{ .Release.Name }}-redis +spec: + fields: + - fieldName: redis-password + length: "32" + encoding: hex +--- +apiVersion: dragonflydb.io/v1alpha1 +kind: Dragonfly +metadata: + name: {{ .Release.Name }}-redis +spec: + authentication: + passwordFromSecret: + name: {{ .Release.Name }}-redis + key: redis-password + replicas: 1 + resources: + requests: + cpu: 500m + memory: 500Mi + limits: + cpu: 600m + memory: 750Mi +--- +apiVersion: secretgenerator.mittwald.de/v1alpha1 +kind: StringSecret +metadata: + name: {{ .Release.Name }}-database + labels: + cnpg.io/reload: "true" +spec: + data: + username: {{ .Release.Name }} + fields: + - fieldName: password + length: "32" + encoding: hex +--- +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: {{ .Release.Name }}-database +spec: + instances: 1 + imageName: ghcr.io/cloudnative-pg/postgresql:17 + storage: + size: 1Gi + storageClass: postgres + affinity: + podAntiAffinityType: required + nodeSelector: + codemowers.io/lvm-ubuntu-vg: enterprise-ssd + resources: + requests: + cpu: "100m" + memory: "1Gi" + limits: + cpu: "1" + memory: "4Gi" + postgresql: + parameters: + max_connections: "300" + shared_buffers: "512MB" + effective_cache_size: "2GB" + managed: + roles: + - name: {{ .Release.Name }} + ensure: present + login: true + passwordSecret: + name: {{ .Release.Name }}-database +--- +apiVersion: postgresql.cnpg.io/v1 +kind: Database +metadata: + name: {{ .Release.Name }} +spec: + name: {{ .Release.Name }} + owner: {{ .Release.Name }} + cluster: + name: {{ .Release.Name }}-database +--- +apiVersion: s3.onyxia.sh/v1alpha1 +kind: Policy +metadata: + name: {{ .Release.Name }}-policy +spec: + name: {{ .Release.Name }}-policy + s3InstanceRef: minio/default + policyContent: >- + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:*" + ], + "Resource": [ + "arn:aws:s3:::{{ .Release.Name }}", + "arn:aws:s3:::{{ .Release.Name }}/*" + ] + } + ] + } +--- +apiVersion: s3.onyxia.sh/v1alpha1 +kind: S3User +metadata: + name: {{ .Release.Name }}-bucket +spec: + accessKey: {{ .Release.Name }}-bucket + policies: + - {{ .Release.Name }}-policy + s3InstanceRef: minio/default +--- +apiVersion: s3.onyxia.sh/v1alpha1 +kind: Bucket +metadata: + name: {{ .Release.Name }} +spec: + name: {{ .Release.Name }} + s3InstanceRef: minio/default + quota: + default: 100000000 +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }} +spec: + type: ClusterIP + selector: + app: {{ .Release.Name }} + ports: + - name: http + port: 80 + targetPort: 8000 +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ .Release.Name }} +spec: + secretName: {{ .Release.Name }}-tls + dnsNames: + - {{ .Values.hostname }} + issuerRef: + name: letsencrypt + kind: ClusterIssuer +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ .Release.Name }} + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + rules: + - host: {{ .Values.hostname }} + http: + paths: + - pathType: Prefix + path: "/" + backend: + service: + name: {{ .Release.Name }} + port: + number: 80 + tls: + - secretName: {{ .Release.Name }}-tls diff --git a/grafana.yaml b/helm/templates/grafana.yaml similarity index 87% rename from grafana.yaml rename to helm/templates/grafana.yaml index d044839..fa2fa05 100644 --- a/grafana.yaml +++ b/helm/templates/grafana.yaml @@ -1,484 +1,469 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: memelord-jake ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: grafana-provisioning - namespace: memelord-jake -data: - datasources.yaml: | - apiVersion: 1 - datasources: - - name: Prometheus - type: prometheus - access: proxy - url: http://prometheus-operated.monitoring.svc.cluster.local:9090 - isDefault: true - - name: Loki - type: loki - access: proxy - url: http://loki.monitoring.svc.cluster.local:3100 - dashboards.yaml: | - apiVersion: 1 - providers: - - name: 'Default' - orgId: 1 - folder: '' - type: file - disableDeletion: false - editable: true - options: - path: /var/lib/grafana/dashboards ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: grafana-dashboards - namespace: memelord-jake -data: - log-aggregator.json: | - { - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "id": 1, - "links": [], - "panels": [ - { - "datasource": { - "type": "loki", - "uid": "P8E80F9AEF21F6940" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "opacity", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "showValues": false, - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "msg/s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 2, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "12.2.1", - "targets": [ - { - "datasource": { - "type": "loki", - "uid": "P8E80F9AEF21F6940" - }, - "direction": "backward", - "editorMode": "code", - "expr": "sum by (detected_level) (count_over_time ({app=~\"$app\",namespace=~\"$namespace\"}[1m]))", - "legendFormat": "{{detected_level}}", - "queryType": "range", - "refId": "A" - } - ], - "title": "Log records", - "type": "timeseries" - }, - { - "datasource": { - "type": "loki", - "uid": "P8E80F9AEF21F6940" - }, - "fieldConfig": { - "defaults": {}, - "overrides": [] - }, - "gridPos": { - "h": 20, - "w": 24, - "x": 0, - "y": 8 - }, - "id": 1, - "options": { - "dedupStrategy": "none", - "enableInfiniteScrolling": false, - "enableLogDetails": true, - "prettifyLogMessage": true, - "showCommonLabels": true, - "showLabels": true, - "showTime": true, - "sortOrder": "Descending", - "wrapLogMessage": true - }, - "pluginVersion": "12.2.1", - "targets": [ - { - "datasource": { - "type": "loki", - "uid": "P8E80F9AEF21F6940" - }, - "direction": "backward", - "editorMode": "code", - "expr": "{app=~\"$app\",namespace=~\"$namespace\"}", - "queryType": "range", - "refId": "A" - } - ], - "title": "Loki", - "type": "logs" - } - ], - "preload": false, - "refresh": "30s", - "schemaVersion": 42, - "tags": [], - "templating": { - "list": [ - { - "allValue": ".*", - "current": { - "text": "All", - "value": [ - "$__all" - ] - }, - "datasource": { - "type": "loki", - "uid": "P8E80F9AEF21F6940" - }, - "definition": "", - "includeAll": true, - "multi": true, - "name": "app", - "options": [], - "query": { - "label": "app", - "refId": "LokiVariableQueryEditor-VariableQuery", - "stream": "", - "type": 1 - }, - "refresh": 1, - "regex": "", - "sort": 5, - "type": "query" - }, - { - "allValue": ".+", - "current": { - "text": "All", - "value": [ - "$__all" - ] - }, - "datasource": { - "type": "loki", - "uid": "P8E80F9AEF21F6940" - }, - "definition": "", - "includeAll": true, - "label": "namespace", - "multi": true, - "name": "namespace", - "options": [], - "query": { - "label": "namespace", - "refId": "LokiVariableQueryEditor-VariableQuery", - "stream": "", - "type": 1 - }, - "refresh": 1, - "regex": "", - "type": "query" - } - ] - }, - "time": { - "from": "now-5m", - "to": "now" - }, - "timepicker": {}, - "timezone": "browser", - "title": "Log Aggregator", - "uid": "lawf6g2", - "version": 1 - } ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: grafana - namespace: memelord-jake - labels: - app: grafana -spec: - serviceName: grafana - replicas: 1 - selector: - matchLabels: - app: grafana - template: - metadata: - labels: - app: grafana - spec: - securityContext: - fsGroup: 472 - containers: - - name: grafana - image: grafana/grafana:latest - imagePullPolicy: IfNotPresent - ports: - - containerPort: 3000 - name: http - env: - # sqlite DB on PVC - - name: GF_DATABASE_TYPE - value: sqlite3 - - name: GF_DATABASE_PATH - value: /var/lib/grafana/grafana.db - - # Ingress URL (important for OAuth callback + links) - - name: GF_SERVER_ROOT_URL - value: https://grafana-jake.ee-lte-1.codemowers.io/ - - name: GF_SERVER_SERVE_FROM_SUB_PATH - value: "false" - - # ---- OIDC (Passmower) via Generic OAuth ---- - - name: GF_AUTH_GENERIC_OAUTH_ENABLED - value: "true" - - name: GF_AUTH_GENERIC_OAUTH_NAME - value: "Passmower" - - name: GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP - value: "true" - - - name: GF_AUTH_GENERIC_OAUTH_USE_ID_TOKEN - value: "false" - - name: GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH - value: "contains(groups[*], 'github.com:codemowers:admins') && 'Admin' || 'Viewer'" - # matches OIDCClient pkce: false - - name: GF_AUTH_GENERIC_OAUTH_USE_PKCE - value: "false" - - # IMPORTANT: - # After OIDCClient grafana-jake is created successfully, - # set this secret name to the generated one (likely oidc-client-grafana-jake-owner-secrets) - - name: GF_AUTH_GENERIC_OAUTH_CLIENT_ID - valueFrom: - secretKeyRef: - name: oidc-client-grafana-jake-owner-secrets - key: OIDC_CLIENT_ID - - name: GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET - valueFrom: - secretKeyRef: - name: oidc-client-grafana-jake-owner-secrets - key: OIDC_CLIENT_SECRET - - - name: GF_AUTH_GENERIC_OAUTH_SCOPES - value: "openid profile groups" - - # From your existing OIDC secret: auth/token/me endpoints - - name: GF_AUTH_GENERIC_OAUTH_AUTH_URL - value: "https://auth.ee-lte-1.codemowers.io/auth" - - name: GF_AUTH_GENERIC_OAUTH_TOKEN_URL - value: "https://auth.ee-lte-1.codemowers.io/token" - - name: GF_AUTH_GENERIC_OAUTH_API_URL - value: "https://auth.ee-lte-1.codemowers.io/me" - - - name: GF_AUTH_GENERIC_OAUTH_SIGNOUT_REDIRECT_URL - value: https://grafana-jake.ee-lte-1.codemowers.io/ - - volumeMounts: - - name: grafana-storage - mountPath: /var/lib/grafana - - name: grafana-provisioning - mountPath: /etc/grafana/provisioning/datasources/datasources.yaml - subPath: datasources.yaml - readOnly: true - - name: grafana-provisioning - mountPath: /etc/grafana/provisioning/dashboards/dashboards.yaml - subPath: dashboards.yaml - readOnly: true - - name: grafana-dashboards - mountPath: /var/lib/grafana/dashboards - readOnly: true - readinessProbe: - httpGet: - path: /api/health - port: 3000 - initialDelaySeconds: 10 - periodSeconds: 10 - livenessProbe: - httpGet: - path: /api/health - port: 3000 - initialDelaySeconds: 30 - periodSeconds: 10 - volumes: - - name: grafana-provisioning - configMap: - name: grafana-provisioning - - name: grafana-dashboards - configMap: - name: grafana-dashboards - volumeClaimTemplates: - - metadata: - name: grafana-storage - spec: - accessModes: [ReadWriteOnce] - storageClassName: sqlite - resources: - requests: - storage: 5Gi ---- -apiVersion: v1 -kind: Service -metadata: - name: grafana - namespace: memelord-jake - labels: - app: grafana -spec: - type: ClusterIP - selector: - app: grafana - ports: - - name: http - port: 3000 - targetPort: 3000 ---- -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: grafana-jake - namespace: memelord-jake -spec: - secretName: grafana-jake-tls - dnsNames: - - grafana-jake.ee-lte-1.codemowers.io - issuerRef: - name: letsencrypt - kind: ClusterIssuer ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: grafana-jake - namespace: memelord-jake - annotations: - traefik.ingress.kubernetes.io/router.entrypoints: websecure -spec: - ingressClassName: traefik - rules: - - host: grafana-jake.ee-lte-1.codemowers.io - http: - paths: - - pathType: Prefix - path: "/" - backend: - service: - name: grafana - port: - number: 3000 - tls: - - secretName: grafana-jake-tls ---- -apiVersion: codemowers.cloud/v1beta1 -kind: OIDCClient -metadata: - name: grafana-jake - namespace: memelord-jake -spec: - displayName: Grafana jake - uri: https://grafana-jake.ee-lte-1.codemowers.io/login/generic_oauth - redirectUris: - - https://grafana-jake.ee-lte-1.codemowers.io/login/generic_oauth - grantTypes: - - authorization_code - - refresh_token - responseTypes: - - code - availableScopes: - - openid - - profile - - offline_access - pkce: false +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-provisioning +data: + datasources.yaml: | + apiVersion: 1 + datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus-operated.monitoring.svc.cluster.local:9090 + isDefault: true + - name: Loki + type: loki + access: proxy + url: http://loki.monitoring.svc.cluster.local:3100 + dashboards.yaml: | + apiVersion: 1 + providers: + - name: 'Default' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboards +data: + log-aggregator.json: | + { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "msg/s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.1", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "direction": "backward", + "editorMode": "code", + "expr": "sum by (detected_level) (count_over_time ({app=~\"$app\",namespace=~\"$namespace\"}[1m]))", + "legendFormat": "{{`{{detected_level}}`}}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Log records", + "type": "timeseries" + }, + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 20, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 1, + "options": { + "dedupStrategy": "none", + "enableInfiniteScrolling": false, + "enableLogDetails": true, + "prettifyLogMessage": true, + "showCommonLabels": true, + "showLabels": true, + "showTime": true, + "sortOrder": "Descending", + "wrapLogMessage": true + }, + "pluginVersion": "12.2.1", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "direction": "backward", + "editorMode": "code", + "expr": "{app=~\"$app\",namespace=~\"$namespace\"}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Loki", + "type": "logs" + } + ], + "preload": false, + "refresh": "30s", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "allValue": ".*", + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "definition": "", + "includeAll": true, + "multi": true, + "name": "app", + "options": [], + "query": { + "label": "app", + "refId": "LokiVariableQueryEditor-VariableQuery", + "stream": "", + "type": 1 + }, + "refresh": 1, + "regex": "", + "sort": 5, + "type": "query" + }, + { + "allValue": ".+", + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "definition": "", + "includeAll": true, + "label": "namespace", + "multi": true, + "name": "namespace", + "options": [], + "query": { + "label": "namespace", + "refId": "LokiVariableQueryEditor-VariableQuery", + "stream": "", + "type": 1 + }, + "refresh": 1, + "regex": "", + "type": "query" + } + ] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Log Aggregator", + "uid": "lawf6g2", + "version": 1 + } +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: grafana + labels: + app: grafana +spec: + serviceName: grafana + replicas: 1 + selector: + matchLabels: + app: grafana + template: + metadata: + labels: + app: grafana + spec: + securityContext: + fsGroup: 472 + containers: + - name: grafana + image: grafana/grafana:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 3000 + name: http + env: + # sqlite DB on PVC + - name: GF_DATABASE_TYPE + value: sqlite3 + - name: GF_DATABASE_PATH + value: /var/lib/grafana/grafana.db + + # Ingress URL (important for OAuth callback + links) + - name: GF_SERVER_ROOT_URL + value: https://{{ .Values.grafanaHostname }}/ + - name: GF_SERVER_SERVE_FROM_SUB_PATH + value: "false" + + # ---- OIDC (Passmower) via Generic OAuth ---- + - name: GF_AUTH_GENERIC_OAUTH_ENABLED + value: "true" + - name: GF_AUTH_GENERIC_OAUTH_NAME + value: "Passmower" + - name: GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP + value: "true" + + - name: GF_AUTH_GENERIC_OAUTH_USE_ID_TOKEN + value: "false" + - name: GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH + value: "contains(groups[*], 'github.com:codemowers:admins') && 'Admin' || 'Viewer'" + # matches OIDCClient pkce: false + - name: GF_AUTH_GENERIC_OAUTH_USE_PKCE + value: "false" + + - name: GF_AUTH_GENERIC_OAUTH_CLIENT_ID + valueFrom: + secretKeyRef: + name: oidc-client-grafana-{{ .Release.Name }}-owner-secrets + key: OIDC_CLIENT_ID + - name: GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: oidc-client-grafana-{{ .Release.Name }}-owner-secrets + key: OIDC_CLIENT_SECRET + + - name: GF_AUTH_GENERIC_OAUTH_SCOPES + value: "openid profile groups" + + # From your existing OIDC secret: auth/token/me endpoints + - name: GF_AUTH_GENERIC_OAUTH_AUTH_URL + value: "https://auth.ee-lte-1.codemowers.io/auth" + - name: GF_AUTH_GENERIC_OAUTH_TOKEN_URL + value: "https://auth.ee-lte-1.codemowers.io/token" + - name: GF_AUTH_GENERIC_OAUTH_API_URL + value: "https://auth.ee-lte-1.codemowers.io/me" + + - name: GF_AUTH_GENERIC_OAUTH_SIGNOUT_REDIRECT_URL + value: https://{{ .Values.grafanaHostname }}/ + + volumeMounts: + - name: grafana-storage + mountPath: /var/lib/grafana + - name: grafana-provisioning + mountPath: /etc/grafana/provisioning/datasources/datasources.yaml + subPath: datasources.yaml + readOnly: true + - name: grafana-provisioning + mountPath: /etc/grafana/provisioning/dashboards/dashboards.yaml + subPath: dashboards.yaml + readOnly: true + - name: grafana-dashboards + mountPath: /var/lib/grafana/dashboards + readOnly: true + readinessProbe: + httpGet: + path: /api/health + port: 3000 + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /api/health + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 10 + volumes: + - name: grafana-provisioning + configMap: + name: grafana-provisioning + - name: grafana-dashboards + configMap: + name: grafana-dashboards + volumeClaimTemplates: + - metadata: + name: grafana-storage + spec: + accessModes: [ReadWriteOnce] + storageClassName: sqlite + resources: + requests: + storage: 5Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: grafana + labels: + app: grafana +spec: + type: ClusterIP + selector: + app: grafana + ports: + - name: http + port: 3000 + targetPort: 3000 +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: grafana-{{ .Release.Name }} +spec: + secretName: grafana-{{ .Release.Name }}-tls + dnsNames: + - {{ .Values.grafanaHostname }} + issuerRef: + name: letsencrypt + kind: ClusterIssuer +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: grafana-{{ .Release.Name }} + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + rules: + - host: {{ .Values.grafanaHostname }} + http: + paths: + - pathType: Prefix + path: "/" + backend: + service: + name: grafana + port: + number: 3000 + tls: + - secretName: grafana-{{ .Release.Name }}-tls +--- +apiVersion: codemowers.cloud/v1beta1 +kind: OIDCClient +metadata: + name: grafana-{{ .Release.Name }} +spec: + displayName: Grafana {{ .Release.Name }} + uri: https://{{ .Values.grafanaHostname }}/login/generic_oauth + redirectUris: + - https://{{ .Values.grafanaHostname }}/login/generic_oauth + grantTypes: + - authorization_code + - refresh_token + responseTypes: + - code + availableScopes: + - openid + - profile + - offline_access + pkce: false diff --git a/monitoring.yaml b/helm/templates/monitoring.yaml similarity index 66% rename from monitoring.yaml rename to helm/templates/monitoring.yaml index 6c0cd5e..73b7d5e 100644 --- a/monitoring.yaml +++ b/helm/templates/monitoring.yaml @@ -1,15 +1,14 @@ -apiVersion: monitoring.coreos.com/v1 -kind: Probe -metadata: - name: memelord-probe - namespace: memelord-jake - labels: - app: memelord -spec: - module: http_2xx - prober: - url: blackbox-exporter.monitoring.svc.cluster.local - targets: - staticConfig: - static: - - google.com +apiVersion: monitoring.coreos.com/v1 +kind: Probe +metadata: + name: {{ .Release.Name }}-probe + labels: + app: {{ .Release.Name }} +spec: + module: http_2xx + prober: + url: blackbox-exporter.monitoring.svc.cluster.local + targets: + staticConfig: + static: + - {{ .Values.hostname }} diff --git a/helm/templates/oidc.yaml b/helm/templates/oidc.yaml new file mode 100644 index 0000000..dad87cf --- /dev/null +++ b/helm/templates/oidc.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: codemowers.cloud/v1beta1 +kind: OIDCClient +metadata: + name: {{ .Release.Name }} +spec: + displayName: Memelord {{ .Release.Name }} + uri: https://{{ .Values.hostname }}/ + redirectUris: + - https://{{ .Values.hostname }}/oidc/callback/ + grantTypes: + - authorization_code + - refresh_token + responseTypes: + - code + availableScopes: + - openid + - profile + pkce: false diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 0000000..36119da --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1,2 @@ +hostname: memelord-jake.ee-lte-1.codemowers.io +grafanaHostname: grafana-jake.ee-lte-1.codemowers.io diff --git a/oidc.yaml b/oidc.yaml deleted file mode 100644 index 886c011..0000000 --- a/oidc.yaml +++ /dev/null @@ -1,20 +0,0 @@ ---- -apiVersion: codemowers.cloud/v1beta1 -kind: OIDCClient -metadata: - name: memelord-jake - namespace: memelord-jake -spec: - displayName: Memelord jake - uri: https://memelord-jake.ee-lte-1.codemowers.io/ - redirectUris: - - https://memelord-jake.ee-lte-1.codemowers.io/oidc/callback/ - grantTypes: - - authorization_code - - refresh_token - responseTypes: - - code - availableScopes: - - openid - - profile - pkce: false \ No newline at end of file