This commit is contained in:
itumi
2026-02-16 15:37:06 +02:00
parent 70741688e0
commit 6f9d46e833
10 changed files with 983 additions and 937 deletions

60
CLAUDE.md Normal file
View File

@@ -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'`)

3
helm/Chart.yaml Normal file
View File

@@ -0,0 +1,3 @@
apiVersion: v2
name: memelord
version: 0.1.0

View File

@@ -1,131 +1,130 @@
--- ---
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: memelord name: {{ .Release.Name }}
namespace: memelord-jake spec:
spec: replicas: 1
replicas: 1 selector:
selector: matchLabels:
matchLabels: app: {{ .Release.Name }}
app: memelord template:
template: metadata:
metadata: labels:
labels: app: {{ .Release.Name }}
app: memelord spec:
spec: containers:
containers: - name: {{ .Release.Name }}
- name: memelord image: ghcr.io/l4rm4nd/memelord:latest
image: ghcr.io/l4rm4nd/memelord:latest imagePullPolicy: Always
imagePullPolicy: Always ports:
ports: - name: http
- name: http containerPort: 8000
containerPort: 8000
env:
env: - name: DOMAIN
- name: DOMAIN value: {{ .Values.hostname | quote }}
value: "memelord-jake.ee-lte-1.codemowers.io"
# Database Configuration
# Database Configuration - name: DB_ENGINE
- name: DB_ENGINE value: "postgres"
value: "postgres" - name: POSTGRES_HOST
- name: POSTGRES_HOST value: "{{ .Release.Name }}-database-rw"
value: "memelord-jake-database-rw" - name: POSTGRES_PORT
- name: POSTGRES_PORT value: "5432"
value: "5432" - name: POSTGRES_DB
- name: POSTGRES_DB value: {{ .Release.Name | quote }}
value: "memelord-jake" - name: POSTGRES_USER
- name: POSTGRES_USER valueFrom:
valueFrom: secretKeyRef:
secretKeyRef: name: {{ .Release.Name }}-database
name: memelord-jake-database key: username
key: username - name: POSTGRES_PASSWORD
- name: POSTGRES_PASSWORD valueFrom:
valueFrom: secretKeyRef:
secretKeyRef: name: {{ .Release.Name }}-database
name: memelord-jake-database key: password
key: password
# Redis Configuration
# Redis Configuration - name: REDIS_HOST
- name: REDIS_HOST value: "{{ .Release.Name }}-redis"
value: "memelord-jake-redis" - name: REDIS_PORT
- name: REDIS_PORT value: "6379"
value: "6379" - name: REDIS_PASSWORD
- name: REDIS_PASSWORD valueFrom:
valueFrom: secretKeyRef:
secretKeyRef: name: {{ .Release.Name }}-redis
name: memelord-jake-redis key: redis-password
key: redis-password
# S3/MinIO Storage Configuration
# S3/MinIO Storage Configuration - name: STORAGE_BACKEND
- name: STORAGE_BACKEND value: "s3"
value: "s3" - name: AWS_ACCESS_KEY_ID
- name: AWS_ACCESS_KEY_ID valueFrom:
valueFrom: secretKeyRef:
secretKeyRef: name: {{ .Release.Name }}-bucket
name: memelord-jake-bucket key: accessKey
key: accessKey - name: AWS_SECRET_ACCESS_KEY
- name: AWS_SECRET_ACCESS_KEY valueFrom:
valueFrom: secretKeyRef:
secretKeyRef: name: {{ .Release.Name }}-bucket
name: memelord-jake-bucket key: secretKey
key: secretKey - name: AWS_S3_ADDRESSING_STYLE
- name: AWS_S3_ADDRESSING_STYLE value: path
value: path - name: AWS_STORAGE_BUCKET_NAME
- name: AWS_STORAGE_BUCKET_NAME value: {{ .Release.Name | quote }}
value: "memelord-jake" - name: AWS_S3_ENDPOINT_URL
- name: AWS_S3_ENDPOINT_URL value: "https://minio.ee-lte-1.codemowers.io"
value: "https://minio.ee-lte-1.codemowers.io" - name: AWS_S3_REGION_NAME
- name: AWS_S3_REGION_NAME value: "ee-lte-1"
value: "ee-lte-1"
# OIDC Configuration
# OIDC Configuration - name: OIDC_ENABLED
- name: OIDC_ENABLED value: "True"
value: "True" - name: OIDC_CREATE_USER
- name: OIDC_CREATE_USER value: "True"
value: "True" - name: OIDC_RP_CLIENT_ID
- name: OIDC_RP_CLIENT_ID valueFrom:
valueFrom: secretKeyRef:
secretKeyRef: name: oidc-client-{{ .Release.Name }}-owner-secrets
name: oidc-client-memelord-jake-owner-secrets key: OIDC_CLIENT_ID
key: OIDC_CLIENT_ID - name: OIDC_RP_CLIENT_SECRET
- name: OIDC_RP_CLIENT_SECRET valueFrom:
valueFrom: secretKeyRef:
secretKeyRef: name: oidc-client-{{ .Release.Name }}-owner-secrets
name: oidc-client-memelord-jake-owner-secrets key: OIDC_CLIENT_SECRET
key: OIDC_CLIENT_SECRET
# Browser-facing endpoint (external URL)
# Browser-facing endpoint (external URL) - name: OIDC_OP_AUTHORIZATION_ENDPOINT
- name: OIDC_OP_AUTHORIZATION_ENDPOINT value: "https://auth.ee-lte-1.codemowers.io/auth"
value: "https://auth.ee-lte-1.codemowers.io/auth"
# Server-to-server endpoints (internal URLs)
# Server-to-server endpoints (internal URLs) - name: OIDC_OP_TOKEN_ENDPOINT
- name: OIDC_OP_TOKEN_ENDPOINT value: "http://passmower.passmower.svc.cluster.local/token"
value: "http://passmower.passmower.svc.cluster.local/token" - name: OIDC_OP_USER_ENDPOINT
- name: OIDC_OP_USER_ENDPOINT value: "http://passmower.passmower.svc.cluster.local/me"
value: "http://passmower.passmower.svc.cluster.local/me" - name: OIDC_OP_JWKS_ENDPOINT
- name: OIDC_OP_JWKS_ENDPOINT value: "http://passmower.passmower.svc.cluster.local/jwks"
value: "http://passmower.passmower.svc.cluster.local/jwks"
- name: OIDC_RP_SIGN_ALGO
- name: OIDC_RP_SIGN_ALGO value: "RS256"
value: "RS256" - name: OIDC_AUTOLOGIN
- name: OIDC_AUTOLOGIN value: "False"
value: "False"
# General Configuration
# General Configuration - name: DEBUG
- name: DEBUG value: "True"
value: "True" - name: SECURE_COOKIES
- name: SECURE_COOKIES value: "True"
value: "True"
# Use the patched app code (including patched settings.py)
# Use the patched app code (including patched settings.py) volumeMounts:
volumeMounts: - name: settings
- name: settings mountPath: /opt/app/myproject/settings.py
mountPath: /opt/app/myproject/settings.py subPath: settings.py
subPath: settings.py readOnly: true
readOnly: true
volumes:
volumes: - name: settings
- name: settings configMap:
configMap: name: settings
name: settings

View File

@@ -2,21 +2,20 @@ apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
name: settings name: settings
namespace: memelord-jake
data: data:
settings.py: | settings.py: |
""" """
Django settings for myproject project. Django settings for myproject project.
Generated by 'django-admin startproject' using Django 3.2.16. Generated by 'django-admin startproject' using Django 3.2.16.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/ https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/ https://docs.djangoproject.com/en/3.2/ref/settings/
""" """
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv from dotenv import load_dotenv
import os import os
@@ -25,33 +24,33 @@ data:
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from csp.constants import NONE, SELF, UNSAFE_INLINE, UNSAFE_EVAL from csp.constants import NONE, SELF, UNSAFE_INLINE, UNSAFE_EVAL
# Load environment variables from .env file # Load environment variables from .env file
load_dotenv() load_dotenv()
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# get debug modus from env # get debug modus from env
DEBUG = os.environ.get('DEBUG', 'False').lower() in ['true'] DEBUG = os.environ.get('DEBUG', 'False').lower() in ['true']
# get container version from env # get container version from env
VERSION = escape(os.environ.get("VERSION", '')) VERSION = escape(os.environ.get("VERSION", ''))
# Enable/disable public meme feed feature (must be after import os) # 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'] 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 # auto-generate a secure secret key or use from env variable
SECRET_KEY = os.environ.get("SECRET_KEY", secrets.token_urlsafe(32)) SECRET_KEY = os.environ.get("SECRET_KEY", secrets.token_urlsafe(32))
# define allowed hosts and trusted domains via env variables # define allowed hosts and trusted domains via env variables
DOMAIN = "" DOMAIN = ""
ALLOWED_HOSTS = ["127.0.0.1"] ALLOWED_HOSTS = ["127.0.0.1"]
CSRF_TRUSTED_ORIGINS = ["http://127.0.0.1:8000"] CSRF_TRUSTED_ORIGINS = ["http://127.0.0.1:8000"]
DOMAIN = str(os.environ.get("DOMAIN", "localhost")) DOMAIN = str(os.environ.get("DOMAIN", "localhost"))
TRUSTED_PORT = str(os.environ.get("PORT", "8000")) TRUSTED_PORT = str(os.environ.get("PORT", "8000"))
if DOMAIN: if DOMAIN:
domains = DOMAIN.split(',') domains = DOMAIN.split(',')
for domain in domains: for domain in domains:
@@ -63,14 +62,14 @@ data:
TRUSTED_USER_DOMAIN_HTTPS = f"https://{domain}:{TRUSTED_PORT}" TRUSTED_USER_DOMAIN_HTTPS = f"https://{domain}:{TRUSTED_PORT}"
TRUSTED_USER_DOMAIN_HTTPS_443_DEFAULT = f"https://{domain}" 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]) 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 #Session Management
CSRF_COOKIE_HTTPONLY = True CSRF_COOKIE_HTTPONLY = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = os.environ.get('SESSION_EXPIRE_AT_BROWSER_CLOSE', 'True').lower() in ['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_AGE = int(os.environ.get('SESSION_COOKIE_AGE', '30')) * 60
SESSION_COOKIE_NAME = 'Session' SESSION_COOKIE_NAME = 'Session'
SESSION_COOKIE_SAMESITE = 'Lax' SESSION_COOKIE_SAMESITE = 'Lax'
# ============================================================================= # =============================================================================
# REDIS CACHE CONFIGURATION (for sessions and general caching) # REDIS CACHE CONFIGURATION (for sessions and general caching)
# ============================================================================= # =============================================================================
@@ -79,7 +78,7 @@ data:
REDIS_PORT = os.environ.get("REDIS_PORT", "6379") REDIS_PORT = os.environ.get("REDIS_PORT", "6379")
REDIS_DB = os.environ.get("REDIS_DB", "0") REDIS_DB = os.environ.get("REDIS_DB", "0")
REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD", "") REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD", "")
if REDIS_HOST: if REDIS_HOST:
# Redis is available - use it for caching and sessions # Redis is available - use it for caching and sessions
CACHES = { CACHES = {
@@ -103,7 +102,7 @@ data:
"TIMEOUT": 300, # 5 minutes default "TIMEOUT": 300, # 5 minutes default
} }
} }
# Use Redis for session storage (cloud-native) # Use Redis for session storage (cloud-native)
SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default" SESSION_CACHE_ALIAS = "default"
@@ -111,9 +110,9 @@ data:
# Redis not configured - use database sessions (backward compatible) # Redis not configured - use database sessions (backward compatible)
SESSION_ENGINE = "django.contrib.sessions.backends.db" SESSION_ENGINE = "django.contrib.sessions.backends.db"
# No CACHES configuration - Django will use local memory cache # No CACHES configuration - Django will use local memory cache
SECURE_COOKIES = os.environ.get('SECURE_COOKIES', 'False').lower() in ['true'] SECURE_COOKIES = os.environ.get('SECURE_COOKIES', 'False').lower() in ['true']
if SECURE_COOKIES: if SECURE_COOKIES:
# transmit cookies over encrypted https only # transmit cookies over encrypted https only
SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True
@@ -126,22 +125,22 @@ data:
# transmit cookies over unencrypted http # transmit cookies over unencrypted http
SESSION_COOKIE_SECURE = False SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False CSRF_COOKIE_SECURE = False
# http security response headers # http security response headers
SECURE_BROWSER_XSS_FILTER = True SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY' X_FRAME_OPTIONS = 'DENY'
REFERRER_POLICY = 'same-origin' REFERRER_POLICY = 'same-origin'
# Load from environment, default to "'self'" # Load from environment, default to "'self'"
raw_frame_ancestors = os.environ.get("CSP_FRAME_ANCESTORS", "'none'") raw_frame_ancestors = os.environ.get("CSP_FRAME_ANCESTORS", "'none'")
# Split by comma, strip spaces, and keep properly quoted entries # Split by comma, strip spaces, and keep properly quoted entries
FRAME_ANCESTORS = [item.strip() for item in raw_frame_ancestors.split(',') if item.strip()] FRAME_ANCESTORS = [item.strip() for item in raw_frame_ancestors.split(',') if item.strip()]
# Build CSP img-src list dynamically based on storage backend # Build CSP img-src list dynamically based on storage backend
STORAGE_BACKEND = os.environ.get('STORAGE_BACKEND', 'local').lower() STORAGE_BACKEND = os.environ.get('STORAGE_BACKEND', 'local').lower()
IMG_SRC_LIST = ["'self'", "data:", "blob:", "https://img.logo.dev"] IMG_SRC_LIST = ["'self'", "data:", "blob:", "https://img.logo.dev"]
# Add S3 domains to CSP if using S3 storage # Add S3 domains to CSP if using S3 storage
if STORAGE_BACKEND == 's3': if STORAGE_BACKEND == 's3':
AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME') 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_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_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'] 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) # Always add custom domain if specified (CDN like CloudFront)
if AWS_S3_CUSTOM_DOMAIN: if AWS_S3_CUSTOM_DOMAIN:
IMG_SRC_LIST.append(f"https://{AWS_S3_CUSTOM_DOMAIN}") IMG_SRC_LIST.append(f"https://{AWS_S3_CUSTOM_DOMAIN}")
# Detect S3 provider based on endpoint URL # Detect S3 provider based on endpoint URL
if AWS_S3_ENDPOINT_URL: if AWS_S3_ENDPOINT_URL:
# S3-compatible service detected # S3-compatible service detected
@@ -161,92 +160,92 @@ data:
parsed_url = urlparse(AWS_S3_ENDPOINT_URL) parsed_url = urlparse(AWS_S3_ENDPOINT_URL)
endpoint_domain = parsed_url.netloc endpoint_domain = parsed_url.netloc
endpoint_scheme = parsed_url.scheme or 'https' endpoint_scheme = parsed_url.scheme or 'https'
# Add the endpoint domain # Add the endpoint domain
IMG_SRC_LIST.append(f"{endpoint_scheme}://{endpoint_domain}") IMG_SRC_LIST.append(f"{endpoint_scheme}://{endpoint_domain}")
# Add bucket-based subdomain format if applicable # Add bucket-based subdomain format if applicable
if endpoint_domain and AWS_STORAGE_BUCKET_NAME: if endpoint_domain and AWS_STORAGE_BUCKET_NAME:
IMG_SRC_LIST.append(f"{endpoint_scheme}://{AWS_STORAGE_BUCKET_NAME}.{endpoint_domain}") IMG_SRC_LIST.append(f"{endpoint_scheme}://{AWS_STORAGE_BUCKET_NAME}.{endpoint_domain}")
# Provider-specific URL patterns # Provider-specific URL patterns
endpoint_lower = endpoint_domain.lower() if endpoint_domain else "" endpoint_lower = endpoint_domain.lower() if endpoint_domain else ""
# DigitalOcean Spaces: also add CDN domain # DigitalOcean Spaces: also add CDN domain
if 'digitaloceanspaces.com' in endpoint_lower: if 'digitaloceanspaces.com' in endpoint_lower:
# DigitalOcean Spaces CDN format: bucket-name.region.cdn.digitaloceanspaces.com # DigitalOcean Spaces CDN format: bucket-name.region.cdn.digitaloceanspaces.com
if AWS_STORAGE_BUCKET_NAME and AWS_S3_REGION_NAME: 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") 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 # Cloudflare R2: also add public bucket URL format
elif 'r2.cloudflarestorage.com' in endpoint_lower: elif 'r2.cloudflarestorage.com' in endpoint_lower:
# Cloudflare R2 can use custom domains via public.r2.dev # Cloudflare R2 can use custom domains via public.r2.dev
# Format: https://bucket-name.account-id.r2.dev (if public) # Format: https://bucket-name.account-id.r2.dev (if public)
# This is typically set via AWS_S3_CUSTOM_DOMAIN, but we note it here # This is typically set via AWS_S3_CUSTOM_DOMAIN, but we note it here
pass pass
# Wasabi: supports path-style and virtual-hosted-style # Wasabi: supports path-style and virtual-hosted-style
elif 'wasabisys.com' in endpoint_lower: elif 'wasabisys.com' in endpoint_lower:
# Already covered by endpoint_domain and bucket subdomain # Already covered by endpoint_domain and bucket subdomain
pass 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: elif 'linodeobjects.com' in endpoint_lower:
# Already covered by endpoint_domain and bucket subdomain # Already covered by endpoint_domain and bucket subdomain
pass pass
# Backblaze B2: supports path-style and virtual-hosted-style # Backblaze B2: supports path-style and virtual-hosted-style
elif 'backblazeb2.com' in endpoint_lower: elif 'backblazeb2.com' in endpoint_lower:
# Already covered by endpoint_domain and bucket subdomain # Already covered by endpoint_domain and bucket subdomain
pass pass
# MinIO: custom deployment, already covered # MinIO: custom deployment, already covered
# Other S3-compatible services: already covered # Other S3-compatible services: already covered
else: else:
# No endpoint URL = Standard AWS S3 # No endpoint URL = Standard AWS S3
if AWS_STORAGE_BUCKET_NAME: if AWS_STORAGE_BUCKET_NAME:
# Add AWS S3 virtual-hosted-style URLs # 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.amazonaws.com")
IMG_SRC_LIST.append(f"https://{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.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) # Add path-style URL format (legacy but still supported)
IMG_SRC_LIST.append("https://s3.amazonaws.com") IMG_SRC_LIST.append("https://s3.amazonaws.com")
IMG_SRC_LIST.append(f"https://s3.{AWS_S3_REGION_NAME}.amazonaws.com") IMG_SRC_LIST.append(f"https://s3.{AWS_S3_REGION_NAME}.amazonaws.com")
# Add dual-stack endpoints (IPv6 support) # Add dual-stack endpoints (IPv6 support)
IMG_SRC_LIST.append(f"https://{AWS_STORAGE_BUCKET_NAME}.s3.dualstack.{AWS_S3_REGION_NAME}.amazonaws.com") 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 # Add S3 Transfer Acceleration endpoint if enabled
if AWS_S3_USE_ACCELERATE_ENDPOINT: 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.amazonaws.com")
IMG_SRC_LIST.append(f"https://{AWS_STORAGE_BUCKET_NAME}.s3-accelerate.dualstack.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 # Add Azure domains to CSP if using Azure storage
elif STORAGE_BACKEND == 'azure': elif STORAGE_BACKEND == 'azure':
AZURE_ACCOUNT_NAME = os.environ.get('AZURE_ACCOUNT_NAME') AZURE_ACCOUNT_NAME = os.environ.get('AZURE_ACCOUNT_NAME')
AZURE_CUSTOM_DOMAIN = os.environ.get('AZURE_CUSTOM_DOMAIN') AZURE_CUSTOM_DOMAIN = os.environ.get('AZURE_CUSTOM_DOMAIN')
if AZURE_CUSTOM_DOMAIN: if AZURE_CUSTOM_DOMAIN:
IMG_SRC_LIST.append(f"https://{AZURE_CUSTOM_DOMAIN}") IMG_SRC_LIST.append(f"https://{AZURE_CUSTOM_DOMAIN}")
if AZURE_ACCOUNT_NAME: if AZURE_ACCOUNT_NAME:
IMG_SRC_LIST.append(f"https://{AZURE_ACCOUNT_NAME}.blob.core.windows.net") IMG_SRC_LIST.append(f"https://{AZURE_ACCOUNT_NAME}.blob.core.windows.net")
# Add GCS domains to CSP if using GCS storage # Add GCS domains to CSP if using GCS storage
elif STORAGE_BACKEND == 'gcs': elif STORAGE_BACKEND == 'gcs':
GS_BUCKET_NAME = os.environ.get('GS_BUCKET_NAME') GS_BUCKET_NAME = os.environ.get('GS_BUCKET_NAME')
GS_CUSTOM_ENDPOINT = os.environ.get('GS_CUSTOM_ENDPOINT') GS_CUSTOM_ENDPOINT = os.environ.get('GS_CUSTOM_ENDPOINT')
if GS_CUSTOM_ENDPOINT: if GS_CUSTOM_ENDPOINT:
IMG_SRC_LIST.append(GS_CUSTOM_ENDPOINT) IMG_SRC_LIST.append(GS_CUSTOM_ENDPOINT)
if GS_BUCKET_NAME: if GS_BUCKET_NAME:
# GCS can use multiple URL formats # GCS can use multiple URL formats
IMG_SRC_LIST.append("https://storage.googleapis.com") IMG_SRC_LIST.append("https://storage.googleapis.com")
IMG_SRC_LIST.append("https://storage.cloud.google.com") IMG_SRC_LIST.append("https://storage.cloud.google.com")
IMG_SRC_LIST.append(f"https://{GS_BUCKET_NAME}.storage.googleapis.com") IMG_SRC_LIST.append(f"https://{GS_BUCKET_NAME}.storage.googleapis.com")
# SFTP/Dropbox typically serve through Django (using 'self' origin) # SFTP/Dropbox typically serve through Django (using 'self' origin)
elif STORAGE_BACKEND == 'sftp': elif STORAGE_BACKEND == 'sftp':
# SFTP files are retrieved by Django and served through Django views # 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') SFTP_CUSTOM_DOMAIN = os.environ.get('SFTP_CUSTOM_DOMAIN')
if SFTP_CUSTOM_DOMAIN: if SFTP_CUSTOM_DOMAIN:
IMG_SRC_LIST.append(f"https://{SFTP_CUSTOM_DOMAIN}") IMG_SRC_LIST.append(f"https://{SFTP_CUSTOM_DOMAIN}")
elif STORAGE_BACKEND == 'dropbox': elif STORAGE_BACKEND == 'dropbox':
# Dropbox files are retrieved by Django and served through Django views # Dropbox files are retrieved by Django and served through Django views
# No additional CSP configuration needed # No additional CSP configuration needed
pass pass
CSP_IMG_SRC_EXTRA = os.environ.get('CSP_IMG_SRC_EXTRA', '') CSP_IMG_SRC_EXTRA = os.environ.get('CSP_IMG_SRC_EXTRA', '')
if CSP_IMG_SRC_EXTRA: if CSP_IMG_SRC_EXTRA:
extra_domains = [domain.strip() for domain in CSP_IMG_SRC_EXTRA.split(',') if domain.strip()] extra_domains = [domain.strip() for domain in CSP_IMG_SRC_EXTRA.split(',') if domain.strip()]
IMG_SRC_LIST.extend(extra_domains) IMG_SRC_LIST.extend(extra_domains)
CONTENT_SECURITY_POLICY = { CONTENT_SECURITY_POLICY = {
"DIRECTIVES": { "DIRECTIVES": {
"default-src": ["'self'"], "default-src": ["'self'"],
@@ -277,7 +276,7 @@ data:
"frame-ancestors": FRAME_ANCESTORS, "frame-ancestors": FRAME_ANCESTORS,
}, },
} }
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'myapp', 'myapp',
@@ -291,7 +290,7 @@ data:
'csp', 'csp',
'storages', 'storages',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
@@ -303,9 +302,9 @@ data:
'django_http_referrer_policy.middleware.ReferrerPolicyMiddleware', 'django_http_referrer_policy.middleware.ReferrerPolicyMiddleware',
'csp.middleware.CSPMiddleware', 'csp.middleware.CSPMiddleware',
] ]
ROOT_URLCONF = 'myproject.urls' ROOT_URLCONF = 'myproject.urls'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
@@ -321,29 +320,29 @@ data:
}, },
}, },
] ]
# Database # Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases # https://docs.djangoproject.com/en/3.2/ref/settings/#databases
DB_ENGINE = os.environ.get("DB_ENGINE", "sqlite3") DB_ENGINE = os.environ.get("DB_ENGINE", "sqlite3")
if DB_ENGINE == "sqlite3": if DB_ENGINE == "sqlite3":
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'database', 'db.sqlite3'), 'NAME': os.path.join(BASE_DIR, 'database', 'db.sqlite3'),
} }
} }
elif DB_ENGINE == "postgres": elif DB_ENGINE == "postgres":
DB_HOST = os.environ.get("POSTGRES_HOST", "db") DB_HOST = os.environ.get("POSTGRES_HOST", "db")
DB_PORT = os.environ.get("POSTGRES_PORT", "5432") DB_PORT = os.environ.get("POSTGRES_PORT", "5432")
DB_USER = os.environ.get("POSTGRES_USER", "memelord") DB_USER = os.environ.get("POSTGRES_USER", "memelord")
DB_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "memelord") DB_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "memelord")
DB_NAME = os.environ.get("POSTGRES_DB", "memelord") DB_NAME = os.environ.get("POSTGRES_DB", "memelord")
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql', 'ENGINE': 'django.db.backends.postgresql',
@@ -354,10 +353,10 @@ data:
'PASSWORD': DB_PASSWORD, 'PASSWORD': DB_PASSWORD,
} }
} }
# Password validation # Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
@@ -372,7 +371,7 @@ data:
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
}, },
] ]
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/ # https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'en-us'
@@ -380,23 +379,23 @@ data:
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
USE_TZ = True USE_TZ = True
LANGUAGES = [ LANGUAGES = [
('en', _('English')), ('en', _('English')),
('de', _('German')), ('de', _('German')),
('fr', _('French')), ('fr', _('French')),
('it', _('Italian')), ('it', _('Italian')),
] ]
LOCALE_PATHS = [ LOCALE_PATHS = [
os.path.join(BASE_DIR, 'locale') os.path.join(BASE_DIR, 'locale')
] ]
# Celery configuration # Celery configuration
# http://docs.celeryproject.org/en/latest/configuration.html # http://docs.celeryproject.org/en/latest/configuration.html
LOGS_DIR = os.path.join(BASE_DIR, 'logs') LOGS_DIR = os.path.join(BASE_DIR, 'logs')
# ============================================================================= # =============================================================================
# LOGGING CONFIGURATION # LOGGING CONFIGURATION
# ============================================================================= # =============================================================================
@@ -458,30 +457,30 @@ data:
'level': 'INFO', 'level': 'INFO',
}, },
} }
# Create logs directory if it doesn't exist # Create logs directory if it doesn't exist
os.makedirs(LOGS_DIR, exist_ok=True) os.makedirs(LOGS_DIR, exist_ok=True)
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'myapp', 'static') STATIC_ROOT = os.path.join(BASE_DIR, 'myapp', 'static')
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_URL = '/accounts/login/' LOGIN_URL = '/accounts/login/'
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/post-logout/' LOGOUT_REDIRECT_URL = '/post-logout/'
ALLOW_LOGOUT_GET_METHOD = True ALLOW_LOGOUT_GET_METHOD = True
WSGI_APPLICATION = 'myproject.wsgi.application' WSGI_APPLICATION = 'myproject.wsgi.application'
# check if oidc is enabled # check if oidc is enabled
OIDC_ENABLED = os.environ.get('OIDC_ENABLED', 'False').lower() in ['true'] OIDC_ENABLED = os.environ.get('OIDC_ENABLED', 'False').lower() in ['true']
OIDC_AUTOLOGIN = os.environ.get('OIDC_AUTOLOGIN', 'False').lower() in ['true'] OIDC_AUTOLOGIN = os.environ.get('OIDC_AUTOLOGIN', 'False').lower() in ['true']
# Max file upload size in MB (default: 10MB) # Max file upload size in MB (default: 10MB)
# Can be configured via MAX_UPLOAD_SIZE_MB environment variable # 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 MAX_UPLOAD_SIZE_MB = int(os.environ.get('MAX_UPLOAD_SIZE_MB', '10')) * 1024 * 1024
# ============================================================================= # =============================================================================
# STORAGE BACKEND CONFIGURATION # STORAGE BACKEND CONFIGURATION
# ============================================================================= # =============================================================================
@@ -489,9 +488,9 @@ data:
# Configure storage backend via STORAGE_BACKEND environment variable # Configure storage backend via STORAGE_BACKEND environment variable
# Supported backends: local, s3, azure, gcs, sftp, dropbox # Supported backends: local, s3, azure, gcs, sftp, dropbox
# Default: local (filesystem storage) # Default: local (filesystem storage)
STORAGE_BACKEND = os.environ.get('STORAGE_BACKEND', 'local').lower() STORAGE_BACKEND = os.environ.get('STORAGE_BACKEND', 'local').lower()
# Django 5.0+ uses STORAGES instead of DEFAULT_FILE_STORAGE # Django 5.0+ uses STORAGES instead of DEFAULT_FILE_STORAGE
# Set both for compatibility # Set both for compatibility
if STORAGE_BACKEND == 's3': if STORAGE_BACKEND == 's3':
@@ -509,14 +508,14 @@ data:
AWS_QUERYSTRING_AUTH = os.environ.get('AWS_QUERYSTRING_AUTH', 'True').lower() in ['true'] 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_S3_FILE_OVERWRITE = os.environ.get('AWS_S3_FILE_OVERWRITE', 'False').lower() in ['true']
AWS_LOCATION = os.environ.get('AWS_LOCATION', 'media') AWS_LOCATION = os.environ.get('AWS_LOCATION', 'media')
# Use signature version 4 (required for all regions) # Use signature version 4 (required for all regions)
AWS_S3_SIGNATURE_VERSION = 's3v4' AWS_S3_SIGNATURE_VERSION = 's3v4'
# Use virtual-hosted-style URLs (bucket-name.s3.region.amazonaws.com) # Use virtual-hosted-style URLs (bucket-name.s3.region.amazonaws.com)
# This is the default and recommended format # This is the default and recommended format
AWS_S3_ADDRESSING_STYLE = 'path' AWS_S3_ADDRESSING_STYLE = 'path'
# Django 5.0+ STORAGES configuration # Django 5.0+ STORAGES configuration
STORAGES = { STORAGES = {
"default": { "default": {
@@ -526,10 +525,10 @@ data:
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
}, },
} }
# Legacy setting for older Django versions # Legacy setting for older Django versions
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
# Update MEDIA_URL # Update MEDIA_URL
if AWS_S3_CUSTOM_DOMAIN: if AWS_S3_CUSTOM_DOMAIN:
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{AWS_LOCATION}/' 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}/' MEDIA_URL = f'https://{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.amazonaws.com/{AWS_LOCATION}/'
else: else:
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media" MEDIA_ROOT = BASE_DIR / "media"
elif STORAGE_BACKEND == 'azure': elif STORAGE_BACKEND == 'azure':
# Microsoft Azure Blob Storage # Microsoft Azure Blob Storage
AZURE_ACCOUNT_NAME = os.environ.get('AZURE_ACCOUNT_NAME') 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_OVERWRITE_FILES = os.environ.get('AZURE_OVERWRITE_FILES', 'False').lower() in ['true']
AZURE_LOCATION = os.environ.get('AZURE_LOCATION', '') AZURE_LOCATION = os.environ.get('AZURE_LOCATION', '')
AZURE_CUSTOM_DOMAIN = os.environ.get('AZURE_CUSTOM_DOMAIN') AZURE_CUSTOM_DOMAIN = os.environ.get('AZURE_CUSTOM_DOMAIN')
STORAGES = { STORAGES = {
"default": { "default": {
"BACKEND": "storages.backends.azure_storage.AzureStorage", "BACKEND": "storages.backends.azure_storage.AzureStorage",
@@ -566,18 +565,18 @@ data:
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
}, },
} }
DEFAULT_FILE_STORAGE = 'storages.backends.azure_storage.AzureStorage' DEFAULT_FILE_STORAGE = 'storages.backends.azure_storage.AzureStorage'
if AZURE_CUSTOM_DOMAIN: if AZURE_CUSTOM_DOMAIN:
MEDIA_URL = f'https://{AZURE_CUSTOM_DOMAIN}/{AZURE_LOCATION}' MEDIA_URL = f'https://{AZURE_CUSTOM_DOMAIN}/{AZURE_LOCATION}'
elif AZURE_ACCOUNT_NAME: elif AZURE_ACCOUNT_NAME:
MEDIA_URL = f'https://{AZURE_ACCOUNT_NAME}.blob.core.windows.net/{AZURE_CONTAINER}/{AZURE_LOCATION}' MEDIA_URL = f'https://{AZURE_ACCOUNT_NAME}.blob.core.windows.net/{AZURE_CONTAINER}/{AZURE_LOCATION}'
else: else:
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media" MEDIA_ROOT = BASE_DIR / "media"
elif STORAGE_BACKEND == 'gcs': elif STORAGE_BACKEND == 'gcs':
# Google Cloud Storage # Google Cloud Storage
GS_BUCKET_NAME = os.environ.get('GS_BUCKET_NAME') GS_BUCKET_NAME = os.environ.get('GS_BUCKET_NAME')
@@ -588,7 +587,7 @@ data:
GS_LOCATION = os.environ.get('GS_LOCATION', 'media') GS_LOCATION = os.environ.get('GS_LOCATION', 'media')
GS_CUSTOM_ENDPOINT = os.environ.get('GS_CUSTOM_ENDPOINT') GS_CUSTOM_ENDPOINT = os.environ.get('GS_CUSTOM_ENDPOINT')
GS_QUERYSTRING_AUTH = os.environ.get('GS_QUERYSTRING_AUTH', 'True').lower() in ['true'] GS_QUERYSTRING_AUTH = os.environ.get('GS_QUERYSTRING_AUTH', 'True').lower() in ['true']
STORAGES = { STORAGES = {
"default": { "default": {
"BACKEND": "storages.backends.gcloud.GoogleCloudStorage", "BACKEND": "storages.backends.gcloud.GoogleCloudStorage",
@@ -597,18 +596,18 @@ data:
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
}, },
} }
DEFAULT_FILE_STORAGE = 'storages.backends.gcloud.GoogleCloudStorage' DEFAULT_FILE_STORAGE = 'storages.backends.gcloud.GoogleCloudStorage'
if GS_CUSTOM_ENDPOINT: if GS_CUSTOM_ENDPOINT:
MEDIA_URL = f'{GS_CUSTOM_ENDPOINT}/{GS_LOCATION}/' MEDIA_URL = f'{GS_CUSTOM_ENDPOINT}/{GS_LOCATION}/'
elif GS_BUCKET_NAME: elif GS_BUCKET_NAME:
MEDIA_URL = f'https://storage.googleapis.com/{GS_BUCKET_NAME}/{GS_LOCATION}/' MEDIA_URL = f'https://storage.googleapis.com/{GS_BUCKET_NAME}/{GS_LOCATION}/'
else: else:
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media" MEDIA_ROOT = BASE_DIR / "media"
elif STORAGE_BACKEND == 'sftp': elif STORAGE_BACKEND == 'sftp':
# SFTP Storage # SFTP Storage
SFTP_STORAGE_HOST = os.environ.get('SFTP_STORAGE_HOST') 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_UID = os.environ.get('SFTP_STORAGE_UID')
SFTP_STORAGE_GID = os.environ.get('SFTP_STORAGE_GID') SFTP_STORAGE_GID = os.environ.get('SFTP_STORAGE_GID')
SFTP_KNOWN_HOST_FILE = os.environ.get('SFTP_KNOWN_HOST_FILE') SFTP_KNOWN_HOST_FILE = os.environ.get('SFTP_KNOWN_HOST_FILE')
STORAGES = { STORAGES = {
"default": { "default": {
"BACKEND": "storages.backends.sftpstorage.SFTPStorage", "BACKEND": "storages.backends.sftpstorage.SFTPStorage",
@@ -634,18 +633,18 @@ data:
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
}, },
} }
DEFAULT_FILE_STORAGE = 'storages.backends.sftpstorage.SFTPStorage' DEFAULT_FILE_STORAGE = 'storages.backends.sftpstorage.SFTPStorage'
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media" MEDIA_ROOT = BASE_DIR / "media"
elif STORAGE_BACKEND == 'dropbox': elif STORAGE_BACKEND == 'dropbox':
# Dropbox Storage # Dropbox Storage
DROPBOX_OAUTH2_TOKEN = os.environ.get('DROPBOX_OAUTH2_TOKEN') DROPBOX_OAUTH2_TOKEN = os.environ.get('DROPBOX_OAUTH2_TOKEN')
DROPBOX_ROOT_PATH = os.environ.get('DROPBOX_ROOT_PATH', '/media') DROPBOX_ROOT_PATH = os.environ.get('DROPBOX_ROOT_PATH', '/media')
DROPBOX_TIMEOUT = int(os.environ.get('DROPBOX_TIMEOUT', '100')) DROPBOX_TIMEOUT = int(os.environ.get('DROPBOX_TIMEOUT', '100'))
DROPBOX_WRITE_MODE = os.environ.get('DROPBOX_WRITE_MODE', 'add') DROPBOX_WRITE_MODE = os.environ.get('DROPBOX_WRITE_MODE', 'add')
STORAGES = { STORAGES = {
"default": { "default": {
"BACKEND": "storages.backends.dropbox.DropBoxStorage", "BACKEND": "storages.backends.dropbox.DropBoxStorage",
@@ -654,11 +653,11 @@ data:
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
}, },
} }
DEFAULT_FILE_STORAGE = 'storages.backends.dropbox.DropBoxStorage' DEFAULT_FILE_STORAGE = 'storages.backends.dropbox.DropBoxStorage'
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media" MEDIA_ROOT = BASE_DIR / "media"
else: else:
# Local filesystem storage (default) # Local filesystem storage (default)
STORAGES = { STORAGES = {
@@ -669,11 +668,11 @@ data:
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
}, },
} }
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media" MEDIA_ROOT = BASE_DIR / "media"
if OIDC_ENABLED: if OIDC_ENABLED:
# get oidc config from env # get oidc config from env
OIDC_CREATE_USER = os.environ.get('OIDC_CREATE_USER', 'True').lower() in ['true'] 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_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_RENEW_ID_TOKEN_EXPIRY_SECONDS = float(os.environ.get('OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS', 900))
OIDC_USERNAME_ALGO = 'myapp.utils.generate_username' OIDC_USERNAME_ALGO = 'myapp.utils.generate_username'
# Add 'mozilla_django_oidc.middleware.SessionRefresh' to INSTALLED_APPS # Add 'mozilla_django_oidc.middleware.SessionRefresh' to INSTALLED_APPS
INSTALLED_APPS.append('mozilla_django_oidc') INSTALLED_APPS.append('mozilla_django_oidc')
# Add 'mozilla_django_oidc' authentication backend # Add 'mozilla_django_oidc' authentication backend
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
'mozilla_django_oidc.auth.OIDCAuthenticationBackend', 'mozilla_django_oidc.auth.OIDCAuthenticationBackend',
) )
# Add 'mozilla_django_oidc.middleware.SessionRefresh' to MIDDLEWARE # Add 'mozilla_django_oidc.middleware.SessionRefresh' to MIDDLEWARE
# https://mozilla-django-oidc.readthedocs.io/en/stable/installation.html#validate-id-tokens-by-renewing-them # https://mozilla-django-oidc.readthedocs.io/en/stable/installation.html#validate-id-tokens-by-renewing-them
MIDDLEWARE.append('mozilla_django_oidc.middleware.SessionRefresh') 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 # 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 # OIDC should only be setup behind a TLS reverse proxy anyways
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

View File

@@ -1,177 +1,177 @@
--- ---
apiVersion: secretgenerator.mittwald.de/v1alpha1 apiVersion: secretgenerator.mittwald.de/v1alpha1
kind: StringSecret kind: StringSecret
metadata: metadata:
name: memelord-jake-redis name: {{ .Release.Name }}-redis
spec: spec:
fields: fields:
- fieldName: redis-password - fieldName: redis-password
length: "32" length: "32"
encoding: hex encoding: hex
--- ---
apiVersion: dragonflydb.io/v1alpha1 apiVersion: dragonflydb.io/v1alpha1
kind: Dragonfly kind: Dragonfly
metadata: metadata:
name: memelord-jake-redis name: {{ .Release.Name }}-redis
spec: spec:
authentication: authentication:
passwordFromSecret: passwordFromSecret:
name: memelord-jake-redis name: {{ .Release.Name }}-redis
key: redis-password key: redis-password
replicas: 1 replicas: 1
resources: resources:
requests: requests:
cpu: 500m cpu: 500m
memory: 500Mi memory: 500Mi
limits: limits:
cpu: 600m cpu: 600m
memory: 750Mi memory: 750Mi
--- ---
apiVersion: secretgenerator.mittwald.de/v1alpha1 apiVersion: secretgenerator.mittwald.de/v1alpha1
kind: StringSecret kind: StringSecret
metadata: metadata:
name: memelord-jake-database name: {{ .Release.Name }}-database
labels: labels:
cnpg.io/reload: "true" cnpg.io/reload: "true"
spec: spec:
data: data:
username: memelord-jake username: {{ .Release.Name }}
fields: fields:
- fieldName: password - fieldName: password
length: "32" length: "32"
encoding: hex encoding: hex
--- ---
apiVersion: postgresql.cnpg.io/v1 apiVersion: postgresql.cnpg.io/v1
kind: Cluster kind: Cluster
metadata: metadata:
name: memelord-jake-database name: {{ .Release.Name }}-database
spec: spec:
instances: 1 instances: 1
imageName: ghcr.io/cloudnative-pg/postgresql:17 imageName: ghcr.io/cloudnative-pg/postgresql:17
storage: storage:
size: 1Gi size: 1Gi
storageClass: postgres storageClass: postgres
affinity: affinity:
podAntiAffinityType: required podAntiAffinityType: required
nodeSelector: nodeSelector:
codemowers.io/lvm-ubuntu-vg: enterprise-ssd codemowers.io/lvm-ubuntu-vg: enterprise-ssd
resources: resources:
requests: requests:
cpu: "100m" cpu: "100m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "1" cpu: "1"
memory: "4Gi" memory: "4Gi"
postgresql: postgresql:
parameters: parameters:
max_connections: "300" max_connections: "300"
shared_buffers: "512MB" shared_buffers: "512MB"
effective_cache_size: "2GB" effective_cache_size: "2GB"
managed: managed:
roles: roles:
- name: memelord-jake - name: {{ .Release.Name }}
ensure: present ensure: present
login: true login: true
passwordSecret: passwordSecret:
name: memelord-jake-database name: {{ .Release.Name }}-database
--- ---
apiVersion: postgresql.cnpg.io/v1 apiVersion: postgresql.cnpg.io/v1
kind: Database kind: Database
metadata: metadata:
name: memelord-jake name: {{ .Release.Name }}
spec: spec:
name: memelord-jake name: {{ .Release.Name }}
owner: memelord-jake owner: {{ .Release.Name }}
cluster: cluster:
name: memelord-jake-database name: {{ .Release.Name }}-database
--- ---
apiVersion: s3.onyxia.sh/v1alpha1 apiVersion: s3.onyxia.sh/v1alpha1
kind: Policy kind: Policy
metadata: metadata:
name: memelord-jake-policy name: {{ .Release.Name }}-policy
spec: spec:
name: memelord-jake-policy name: {{ .Release.Name }}-policy
s3InstanceRef: minio/default s3InstanceRef: minio/default
policyContent: >- policyContent: >-
{ {
"Version": "2012-10-17", "Version": "2012-10-17",
"Statement": [ "Statement": [
{ {
"Effect": "Allow", "Effect": "Allow",
"Action": [ "Action": [
"s3:*" "s3:*"
], ],
"Resource": [ "Resource": [
"arn:aws:s3:::memelord-jake", "arn:aws:s3:::{{ .Release.Name }}",
"arn:aws:s3:::memelord-jake/*" "arn:aws:s3:::{{ .Release.Name }}/*"
] ]
} }
] ]
} }
--- ---
apiVersion: s3.onyxia.sh/v1alpha1 apiVersion: s3.onyxia.sh/v1alpha1
kind: S3User kind: S3User
metadata: metadata:
name: memelord-jake-bucket name: {{ .Release.Name }}-bucket
spec: spec:
accessKey: memelord-jake-bucket accessKey: {{ .Release.Name }}-bucket
policies: policies:
- memelord-jake-policy - {{ .Release.Name }}-policy
s3InstanceRef: minio/default s3InstanceRef: minio/default
--- ---
apiVersion: s3.onyxia.sh/v1alpha1 apiVersion: s3.onyxia.sh/v1alpha1
kind: Bucket kind: Bucket
metadata: metadata:
name: memelord-jake name: {{ .Release.Name }}
spec: spec:
name: memelord-jake name: {{ .Release.Name }}
s3InstanceRef: minio/default s3InstanceRef: minio/default
quota: quota:
default: 100000000 default: 100000000
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: memelord name: {{ .Release.Name }}
spec: spec:
type: ClusterIP type: ClusterIP
selector: selector:
app: memelord app: {{ .Release.Name }}
ports: ports:
- name: http - name: http
port: 80 port: 80
targetPort: 8000 targetPort: 8000
--- ---
apiVersion: cert-manager.io/v1 apiVersion: cert-manager.io/v1
kind: Certificate kind: Certificate
metadata: metadata:
name: memelord-jake name: {{ .Release.Name }}
spec: spec:
secretName: memelord-jake-tls secretName: {{ .Release.Name }}-tls
dnsNames: dnsNames:
- memelord-jake.ee-lte-1.codemowers.io - {{ .Values.hostname }}
issuerRef: issuerRef:
name: letsencrypt name: letsencrypt
kind: ClusterIssuer kind: ClusterIssuer
--- ---
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: memelord-jake name: {{ .Release.Name }}
annotations: annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure traefik.ingress.kubernetes.io/router.entrypoints: websecure
spec: spec:
ingressClassName: traefik ingressClassName: traefik
rules: rules:
- host: memelord-jake.ee-lte-1.codemowers.io - host: {{ .Values.hostname }}
http: http:
paths: paths:
- pathType: Prefix - pathType: Prefix
path: "/" path: "/"
backend: backend:
service: service:
name: memelord name: {{ .Release.Name }}
port: port:
number: 80 number: 80
tls: tls:
- secretName: memelord-jake-tls - secretName: {{ .Release.Name }}-tls

View File

@@ -1,484 +1,469 @@
apiVersion: v1 apiVersion: v1
kind: Namespace kind: ConfigMap
metadata: metadata:
name: memelord-jake name: grafana-provisioning
--- data:
apiVersion: v1 datasources.yaml: |
kind: ConfigMap apiVersion: 1
metadata: datasources:
name: grafana-provisioning - name: Prometheus
namespace: memelord-jake type: prometheus
data: access: proxy
datasources.yaml: | url: http://prometheus-operated.monitoring.svc.cluster.local:9090
apiVersion: 1 isDefault: true
datasources: - name: Loki
- name: Prometheus type: loki
type: prometheus access: proxy
access: proxy url: http://loki.monitoring.svc.cluster.local:3100
url: http://prometheus-operated.monitoring.svc.cluster.local:9090 dashboards.yaml: |
isDefault: true apiVersion: 1
- name: Loki providers:
type: loki - name: 'Default'
access: proxy orgId: 1
url: http://loki.monitoring.svc.cluster.local:3100 folder: ''
dashboards.yaml: | type: file
apiVersion: 1 disableDeletion: false
providers: editable: true
- name: 'Default' options:
orgId: 1 path: /var/lib/grafana/dashboards
folder: '' ---
type: file apiVersion: v1
disableDeletion: false kind: ConfigMap
editable: true metadata:
options: name: grafana-dashboards
path: /var/lib/grafana/dashboards data:
--- log-aggregator.json: |
apiVersion: v1 {
kind: ConfigMap "annotations": {
metadata: "list": [
name: grafana-dashboards {
namespace: memelord-jake "builtIn": 1,
data: "datasource": {
log-aggregator.json: | "type": "grafana",
{ "uid": "-- Grafana --"
"annotations": { },
"list": [ "enable": true,
{ "hide": true,
"builtIn": 1, "iconColor": "rgba(0, 211, 255, 1)",
"datasource": { "name": "Annotations & Alerts",
"type": "grafana", "type": "dashboard"
"uid": "-- Grafana --" }
}, ]
"enable": true, },
"hide": true, "editable": true,
"iconColor": "rgba(0, 211, 255, 1)", "fiscalYearStartMonth": 0,
"name": "Annotations & Alerts", "graphTooltip": 0,
"type": "dashboard" "id": 1,
} "links": [],
] "panels": [
}, {
"editable": true, "datasource": {
"fiscalYearStartMonth": 0, "type": "loki",
"graphTooltip": 0, "uid": "P8E80F9AEF21F6940"
"id": 1, },
"links": [], "fieldConfig": {
"panels": [ "defaults": {
{ "color": {
"datasource": { "mode": "palette-classic"
"type": "loki", },
"uid": "P8E80F9AEF21F6940" "custom": {
}, "axisBorderShow": false,
"fieldConfig": { "axisCenteredZero": false,
"defaults": { "axisColorMode": "text",
"color": { "axisLabel": "",
"mode": "palette-classic" "axisPlacement": "auto",
}, "barAlignment": 0,
"custom": { "barWidthFactor": 0.6,
"axisBorderShow": false, "drawStyle": "line",
"axisCenteredZero": false, "fillOpacity": 0,
"axisColorMode": "text", "gradientMode": "opacity",
"axisLabel": "", "hideFrom": {
"axisPlacement": "auto", "legend": false,
"barAlignment": 0, "tooltip": false,
"barWidthFactor": 0.6, "viz": false
"drawStyle": "line", },
"fillOpacity": 0, "insertNulls": false,
"gradientMode": "opacity", "lineInterpolation": "linear",
"hideFrom": { "lineWidth": 1,
"legend": false, "pointSize": 5,
"tooltip": false, "scaleDistribution": {
"viz": false "type": "linear"
}, },
"insertNulls": false, "showPoints": "auto",
"lineInterpolation": "linear", "showValues": false,
"lineWidth": 1, "spanNulls": false,
"pointSize": 5, "stacking": {
"scaleDistribution": { "group": "A",
"type": "linear" "mode": "normal"
}, },
"showPoints": "auto", "thresholdsStyle": {
"showValues": false, "mode": "off"
"spanNulls": false, }
"stacking": { },
"group": "A", "mappings": [],
"mode": "normal" "thresholds": {
}, "mode": "absolute",
"thresholdsStyle": { "steps": [
"mode": "off" {
} "color": "green",
}, "value": 0
"mappings": [], },
"thresholds": { {
"mode": "absolute", "color": "red",
"steps": [ "value": 80
{ }
"color": "green", ]
"value": 0 },
}, "unit": "msg/s"
{ },
"color": "red", "overrides": []
"value": 80 },
} "gridPos": {
] "h": 8,
}, "w": 24,
"unit": "msg/s" "x": 0,
}, "y": 0
"overrides": [] },
}, "id": 2,
"gridPos": { "options": {
"h": 8, "legend": {
"w": 24, "calcs": [],
"x": 0, "displayMode": "list",
"y": 0 "placement": "bottom",
}, "showLegend": true
"id": 2, },
"options": { "tooltip": {
"legend": { "hideZeros": false,
"calcs": [], "mode": "single",
"displayMode": "list", "sort": "none"
"placement": "bottom", }
"showLegend": true },
}, "pluginVersion": "12.2.1",
"tooltip": { "targets": [
"hideZeros": false, {
"mode": "single", "datasource": {
"sort": "none" "type": "loki",
} "uid": "P8E80F9AEF21F6940"
}, },
"pluginVersion": "12.2.1", "direction": "backward",
"targets": [ "editorMode": "code",
{ "expr": "sum by (detected_level) (count_over_time ({app=~\"$app\",namespace=~\"$namespace\"}[1m]))",
"datasource": { "legendFormat": "{{`{{detected_level}}`}}",
"type": "loki", "queryType": "range",
"uid": "P8E80F9AEF21F6940" "refId": "A"
}, }
"direction": "backward", ],
"editorMode": "code", "title": "Log records",
"expr": "sum by (detected_level) (count_over_time ({app=~\"$app\",namespace=~\"$namespace\"}[1m]))", "type": "timeseries"
"legendFormat": "{{detected_level}}", },
"queryType": "range", {
"refId": "A" "datasource": {
} "type": "loki",
], "uid": "P8E80F9AEF21F6940"
"title": "Log records", },
"type": "timeseries" "fieldConfig": {
}, "defaults": {},
{ "overrides": []
"datasource": { },
"type": "loki", "gridPos": {
"uid": "P8E80F9AEF21F6940" "h": 20,
}, "w": 24,
"fieldConfig": { "x": 0,
"defaults": {}, "y": 8
"overrides": [] },
}, "id": 1,
"gridPos": { "options": {
"h": 20, "dedupStrategy": "none",
"w": 24, "enableInfiniteScrolling": false,
"x": 0, "enableLogDetails": true,
"y": 8 "prettifyLogMessage": true,
}, "showCommonLabels": true,
"id": 1, "showLabels": true,
"options": { "showTime": true,
"dedupStrategy": "none", "sortOrder": "Descending",
"enableInfiniteScrolling": false, "wrapLogMessage": true
"enableLogDetails": true, },
"prettifyLogMessage": true, "pluginVersion": "12.2.1",
"showCommonLabels": true, "targets": [
"showLabels": true, {
"showTime": true, "datasource": {
"sortOrder": "Descending", "type": "loki",
"wrapLogMessage": true "uid": "P8E80F9AEF21F6940"
}, },
"pluginVersion": "12.2.1", "direction": "backward",
"targets": [ "editorMode": "code",
{ "expr": "{app=~\"$app\",namespace=~\"$namespace\"}",
"datasource": { "queryType": "range",
"type": "loki", "refId": "A"
"uid": "P8E80F9AEF21F6940" }
}, ],
"direction": "backward", "title": "Loki",
"editorMode": "code", "type": "logs"
"expr": "{app=~\"$app\",namespace=~\"$namespace\"}", }
"queryType": "range", ],
"refId": "A" "preload": false,
} "refresh": "30s",
], "schemaVersion": 42,
"title": "Loki", "tags": [],
"type": "logs" "templating": {
} "list": [
], {
"preload": false, "allValue": ".*",
"refresh": "30s", "current": {
"schemaVersion": 42, "text": "All",
"tags": [], "value": [
"templating": { "$__all"
"list": [ ]
{ },
"allValue": ".*", "datasource": {
"current": { "type": "loki",
"text": "All", "uid": "P8E80F9AEF21F6940"
"value": [ },
"$__all" "definition": "",
] "includeAll": true,
}, "multi": true,
"datasource": { "name": "app",
"type": "loki", "options": [],
"uid": "P8E80F9AEF21F6940" "query": {
}, "label": "app",
"definition": "", "refId": "LokiVariableQueryEditor-VariableQuery",
"includeAll": true, "stream": "",
"multi": true, "type": 1
"name": "app", },
"options": [], "refresh": 1,
"query": { "regex": "",
"label": "app", "sort": 5,
"refId": "LokiVariableQueryEditor-VariableQuery", "type": "query"
"stream": "", },
"type": 1 {
}, "allValue": ".+",
"refresh": 1, "current": {
"regex": "", "text": "All",
"sort": 5, "value": [
"type": "query" "$__all"
}, ]
{ },
"allValue": ".+", "datasource": {
"current": { "type": "loki",
"text": "All", "uid": "P8E80F9AEF21F6940"
"value": [ },
"$__all" "definition": "",
] "includeAll": true,
}, "label": "namespace",
"datasource": { "multi": true,
"type": "loki", "name": "namespace",
"uid": "P8E80F9AEF21F6940" "options": [],
}, "query": {
"definition": "", "label": "namespace",
"includeAll": true, "refId": "LokiVariableQueryEditor-VariableQuery",
"label": "namespace", "stream": "",
"multi": true, "type": 1
"name": "namespace", },
"options": [], "refresh": 1,
"query": { "regex": "",
"label": "namespace", "type": "query"
"refId": "LokiVariableQueryEditor-VariableQuery", }
"stream": "", ]
"type": 1 },
}, "time": {
"refresh": 1, "from": "now-5m",
"regex": "", "to": "now"
"type": "query" },
} "timepicker": {},
] "timezone": "browser",
}, "title": "Log Aggregator",
"time": { "uid": "lawf6g2",
"from": "now-5m", "version": 1
"to": "now" }
}, ---
"timepicker": {}, apiVersion: apps/v1
"timezone": "browser", kind: StatefulSet
"title": "Log Aggregator", metadata:
"uid": "lawf6g2", name: grafana
"version": 1 labels:
} app: grafana
--- spec:
apiVersion: apps/v1 serviceName: grafana
kind: StatefulSet replicas: 1
metadata: selector:
name: grafana matchLabels:
namespace: memelord-jake app: grafana
labels: template:
app: grafana metadata:
spec: labels:
serviceName: grafana app: grafana
replicas: 1 spec:
selector: securityContext:
matchLabels: fsGroup: 472
app: grafana containers:
template: - name: grafana
metadata: image: grafana/grafana:latest
labels: imagePullPolicy: IfNotPresent
app: grafana ports:
spec: - containerPort: 3000
securityContext: name: http
fsGroup: 472 env:
containers: # sqlite DB on PVC
- name: grafana - name: GF_DATABASE_TYPE
image: grafana/grafana:latest value: sqlite3
imagePullPolicy: IfNotPresent - name: GF_DATABASE_PATH
ports: value: /var/lib/grafana/grafana.db
- containerPort: 3000
name: http # Ingress URL (important for OAuth callback + links)
env: - name: GF_SERVER_ROOT_URL
# sqlite DB on PVC value: https://{{ .Values.grafanaHostname }}/
- name: GF_DATABASE_TYPE - name: GF_SERVER_SERVE_FROM_SUB_PATH
value: sqlite3 value: "false"
- name: GF_DATABASE_PATH
value: /var/lib/grafana/grafana.db # ---- OIDC (Passmower) via Generic OAuth ----
- name: GF_AUTH_GENERIC_OAUTH_ENABLED
# Ingress URL (important for OAuth callback + links) value: "true"
- name: GF_SERVER_ROOT_URL - name: GF_AUTH_GENERIC_OAUTH_NAME
value: https://grafana-jake.ee-lte-1.codemowers.io/ value: "Passmower"
- name: GF_SERVER_SERVE_FROM_SUB_PATH - name: GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP
value: "false" value: "true"
# ---- OIDC (Passmower) via Generic OAuth ---- - name: GF_AUTH_GENERIC_OAUTH_USE_ID_TOKEN
- name: GF_AUTH_GENERIC_OAUTH_ENABLED value: "false"
value: "true" - name: GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH
- name: GF_AUTH_GENERIC_OAUTH_NAME value: "contains(groups[*], 'github.com:codemowers:admins') && 'Admin' || 'Viewer'"
value: "Passmower" # matches OIDCClient pkce: false
- name: GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP - name: GF_AUTH_GENERIC_OAUTH_USE_PKCE
value: "true" value: "false"
- name: GF_AUTH_GENERIC_OAUTH_USE_ID_TOKEN - name: GF_AUTH_GENERIC_OAUTH_CLIENT_ID
value: "false" valueFrom:
- name: GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH secretKeyRef:
value: "contains(groups[*], 'github.com:codemowers:admins') && 'Admin' || 'Viewer'" name: oidc-client-grafana-{{ .Release.Name }}-owner-secrets
# matches OIDCClient pkce: false key: OIDC_CLIENT_ID
- name: GF_AUTH_GENERIC_OAUTH_USE_PKCE - name: GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET
value: "false" valueFrom:
secretKeyRef:
# IMPORTANT: name: oidc-client-grafana-{{ .Release.Name }}-owner-secrets
# After OIDCClient grafana-jake is created successfully, key: OIDC_CLIENT_SECRET
# set this secret name to the generated one (likely oidc-client-grafana-jake-owner-secrets)
- name: GF_AUTH_GENERIC_OAUTH_CLIENT_ID - name: GF_AUTH_GENERIC_OAUTH_SCOPES
valueFrom: value: "openid profile groups"
secretKeyRef:
name: oidc-client-grafana-jake-owner-secrets # From your existing OIDC secret: auth/token/me endpoints
key: OIDC_CLIENT_ID - name: GF_AUTH_GENERIC_OAUTH_AUTH_URL
- name: GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET value: "https://auth.ee-lte-1.codemowers.io/auth"
valueFrom: - name: GF_AUTH_GENERIC_OAUTH_TOKEN_URL
secretKeyRef: value: "https://auth.ee-lte-1.codemowers.io/token"
name: oidc-client-grafana-jake-owner-secrets - name: GF_AUTH_GENERIC_OAUTH_API_URL
key: OIDC_CLIENT_SECRET value: "https://auth.ee-lte-1.codemowers.io/me"
- name: GF_AUTH_GENERIC_OAUTH_SCOPES - name: GF_AUTH_GENERIC_OAUTH_SIGNOUT_REDIRECT_URL
value: "openid profile groups" value: https://{{ .Values.grafanaHostname }}/
# From your existing OIDC secret: auth/token/me endpoints volumeMounts:
- name: GF_AUTH_GENERIC_OAUTH_AUTH_URL - name: grafana-storage
value: "https://auth.ee-lte-1.codemowers.io/auth" mountPath: /var/lib/grafana
- name: GF_AUTH_GENERIC_OAUTH_TOKEN_URL - name: grafana-provisioning
value: "https://auth.ee-lte-1.codemowers.io/token" mountPath: /etc/grafana/provisioning/datasources/datasources.yaml
- name: GF_AUTH_GENERIC_OAUTH_API_URL subPath: datasources.yaml
value: "https://auth.ee-lte-1.codemowers.io/me" readOnly: true
- name: grafana-provisioning
- name: GF_AUTH_GENERIC_OAUTH_SIGNOUT_REDIRECT_URL mountPath: /etc/grafana/provisioning/dashboards/dashboards.yaml
value: https://grafana-jake.ee-lte-1.codemowers.io/ subPath: dashboards.yaml
readOnly: true
volumeMounts: - name: grafana-dashboards
- name: grafana-storage mountPath: /var/lib/grafana/dashboards
mountPath: /var/lib/grafana readOnly: true
- name: grafana-provisioning readinessProbe:
mountPath: /etc/grafana/provisioning/datasources/datasources.yaml httpGet:
subPath: datasources.yaml path: /api/health
readOnly: true port: 3000
- name: grafana-provisioning initialDelaySeconds: 10
mountPath: /etc/grafana/provisioning/dashboards/dashboards.yaml periodSeconds: 10
subPath: dashboards.yaml livenessProbe:
readOnly: true httpGet:
- name: grafana-dashboards path: /api/health
mountPath: /var/lib/grafana/dashboards port: 3000
readOnly: true initialDelaySeconds: 30
readinessProbe: periodSeconds: 10
httpGet: volumes:
path: /api/health - name: grafana-provisioning
port: 3000 configMap:
initialDelaySeconds: 10 name: grafana-provisioning
periodSeconds: 10 - name: grafana-dashboards
livenessProbe: configMap:
httpGet: name: grafana-dashboards
path: /api/health volumeClaimTemplates:
port: 3000 - metadata:
initialDelaySeconds: 30 name: grafana-storage
periodSeconds: 10 spec:
volumes: accessModes: [ReadWriteOnce]
- name: grafana-provisioning storageClassName: sqlite
configMap: resources:
name: grafana-provisioning requests:
- name: grafana-dashboards storage: 5Gi
configMap: ---
name: grafana-dashboards apiVersion: v1
volumeClaimTemplates: kind: Service
- metadata: metadata:
name: grafana-storage name: grafana
spec: labels:
accessModes: [ReadWriteOnce] app: grafana
storageClassName: sqlite spec:
resources: type: ClusterIP
requests: selector:
storage: 5Gi app: grafana
--- ports:
apiVersion: v1 - name: http
kind: Service port: 3000
metadata: targetPort: 3000
name: grafana ---
namespace: memelord-jake apiVersion: cert-manager.io/v1
labels: kind: Certificate
app: grafana metadata:
spec: name: grafana-{{ .Release.Name }}
type: ClusterIP spec:
selector: secretName: grafana-{{ .Release.Name }}-tls
app: grafana dnsNames:
ports: - {{ .Values.grafanaHostname }}
- name: http issuerRef:
port: 3000 name: letsencrypt
targetPort: 3000 kind: ClusterIssuer
--- ---
apiVersion: cert-manager.io/v1 apiVersion: networking.k8s.io/v1
kind: Certificate kind: Ingress
metadata: metadata:
name: grafana-jake name: grafana-{{ .Release.Name }}
namespace: memelord-jake annotations:
spec: traefik.ingress.kubernetes.io/router.entrypoints: websecure
secretName: grafana-jake-tls spec:
dnsNames: ingressClassName: traefik
- grafana-jake.ee-lte-1.codemowers.io rules:
issuerRef: - host: {{ .Values.grafanaHostname }}
name: letsencrypt http:
kind: ClusterIssuer paths:
--- - pathType: Prefix
apiVersion: networking.k8s.io/v1 path: "/"
kind: Ingress backend:
metadata: service:
name: grafana-jake name: grafana
namespace: memelord-jake port:
annotations: number: 3000
traefik.ingress.kubernetes.io/router.entrypoints: websecure tls:
spec: - secretName: grafana-{{ .Release.Name }}-tls
ingressClassName: traefik ---
rules: apiVersion: codemowers.cloud/v1beta1
- host: grafana-jake.ee-lte-1.codemowers.io kind: OIDCClient
http: metadata:
paths: name: grafana-{{ .Release.Name }}
- pathType: Prefix spec:
path: "/" displayName: Grafana {{ .Release.Name }}
backend: uri: https://{{ .Values.grafanaHostname }}/login/generic_oauth
service: redirectUris:
name: grafana - https://{{ .Values.grafanaHostname }}/login/generic_oauth
port: grantTypes:
number: 3000 - authorization_code
tls: - refresh_token
- secretName: grafana-jake-tls responseTypes:
--- - code
apiVersion: codemowers.cloud/v1beta1 availableScopes:
kind: OIDCClient - openid
metadata: - profile
name: grafana-jake - offline_access
namespace: memelord-jake pkce: false
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

View File

@@ -1,15 +1,14 @@
apiVersion: monitoring.coreos.com/v1 apiVersion: monitoring.coreos.com/v1
kind: Probe kind: Probe
metadata: metadata:
name: memelord-probe name: {{ .Release.Name }}-probe
namespace: memelord-jake labels:
labels: app: {{ .Release.Name }}
app: memelord spec:
spec: module: http_2xx
module: http_2xx prober:
prober: url: blackbox-exporter.monitoring.svc.cluster.local
url: blackbox-exporter.monitoring.svc.cluster.local targets:
targets: staticConfig:
staticConfig: static:
static: - {{ .Values.hostname }}
- google.com

19
helm/templates/oidc.yaml Normal file
View File

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

2
helm/values.yaml Normal file
View File

@@ -0,0 +1,2 @@
hostname: memelord-jake.ee-lte-1.codemowers.io
grafanaHostname: grafana-jake.ee-lte-1.codemowers.io

View File

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