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

130
helm/templates/app.yaml Normal file
View File

@@ -0,0 +1,130 @@
---
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

705
helm/templates/config.yaml Normal file
View File

@@ -0,0 +1,705 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: settings
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
import pytz
import secrets
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:
domain = domain.strip().rstrip('/').replace('http://', '').replace('https://', '')
if domain:
ALLOWED_HOSTS.append(domain)
TRUSTED_USER_DOMAIN_HTTP = f"http://{domain}:{TRUSTED_PORT}"
TRUSTED_USER_DOMAIN_HTTP_80_DEFAULT = f"http://{domain}"
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)
# =============================================================================
# If REDIS_HOST is not set, sessions will use database (backward compatible)
REDIS_HOST = os.environ.get("REDIS_HOST", "")
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 = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"PASSWORD": REDIS_PASSWORD if REDIS_PASSWORD else None,
"SOCKET_CONNECT_TIMEOUT": 5, # seconds
"SOCKET_TIMEOUT": 5, # seconds
"RETRY_ON_TIMEOUT": True,
"CONNECTION_POOL_KWARGS": {
"max_connections": 50,
"retry_on_timeout": True,
},
"COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor",
"IGNORE_EXCEPTIONS": True, # Don't crash if Redis is down
},
"KEY_PREFIX": "memelord",
"TIMEOUT": 300, # 5 minutes default
}
}
# Use Redis for session storage (cloud-native)
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
else:
# 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
CSRF_COOKIE_SECURE = True
# also set hsts response header
SECURE_HSTS_SECONDS = "31536000"
SECURE_HSTS_PRELOAD = True
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
else:
# 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')
AWS_S3_CUSTOM_DOMAIN = os.environ.get('AWS_S3_CUSTOM_DOMAIN')
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
from urllib.parse import urlparse
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
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
# Optional: If you have nginx serving the same SFTP directory via HTTP
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()]
IMG_SRC_LIST.extend(extra_domains)
CONTENT_SECURITY_POLICY = {
"DIRECTIVES": {
"default-src": ["'self'"],
"style-src": ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdn.jsdelivr.net"],
"script-src": ["'self'", "'unsafe-inline'", "'unsafe-eval'", "https://cdn.jsdelivr.net"],
"font-src": ["'self'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
"img-src": IMG_SRC_LIST,
"object-src": ["'none'"],
"connect-src": ["'self'"],
"frame-ancestors": FRAME_ANCESTORS,
},
}
# Application definition
INSTALLED_APPS = [
'myapp',
'django_celery_beat',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'csp',
'storages',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
#'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django_http_referrer_policy.middleware.ReferrerPolicyMiddleware',
'csp.middleware.CSPMiddleware',
]
ROOT_URLCONF = 'myproject.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'myapp/templates/registration')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
# 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',
'NAME': DB_NAME,
'HOST': DB_HOST,
'PORT': DB_PORT,
'USER': DB_USER,
'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',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = os.environ.get('TZ', 'Europe/Berlin')
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
# =============================================================================
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {message}',
'style': '{',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
'file': {
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(LOGS_DIR, 'django.log'),
'maxBytes': 1024 * 1024 * 15, # 15MB
'backupCount': 10,
'formatter': 'verbose',
},
},
'loggers': {
'django': {
'handlers': ['console', 'file'],
'level': os.environ.get('DJANGO_LOG_LEVEL', 'INFO'),
'propagate': False,
},
'myapp': {
'handlers': ['console', 'file'],
'level': os.environ.get('DJANGO_LOG_LEVEL', 'INFO'),
'propagate': False,
},
'storages': {
'handlers': ['console', 'file'],
'level': 'DEBUG' if DEBUG else 'INFO',
'propagate': False,
},
'boto3': {
'handlers': ['console', 'file'],
'level': 'DEBUG' if DEBUG else 'INFO',
'propagate': False,
},
'botocore': {
'handlers': ['console', 'file'],
'level': 'DEBUG' if DEBUG else 'INFO',
'propagate': False,
},
},
'root': {
'handlers': ['console', 'file'],
'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
# =============================================================================
# IMPORTANT: This must be set BEFORE MEDIA_URL and MEDIA_ROOT
# 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':
# Amazon S3 / Compatible S3 Storage (MinIO, DigitalOcean Spaces, etc.)
AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME')
AWS_S3_REGION_NAME = os.environ.get('AWS_S3_REGION_NAME', 'us-east-1')
AWS_S3_CUSTOM_DOMAIN = os.environ.get('AWS_S3_CUSTOM_DOMAIN')
AWS_S3_ENDPOINT_URL = os.environ.get('AWS_S3_ENDPOINT_URL') # Only set for S3-compatible services (MinIO, etc.)
AWS_DEFAULT_ACL = os.environ.get('AWS_DEFAULT_ACL', 'private')
AWS_S3_OBJECT_PARAMETERS = {
'CacheControl': 'max-age=86400',
}
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": {
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
},
"staticfiles": {
"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}/'
elif AWS_STORAGE_BUCKET_NAME:
# Use region-specific virtual-hosted-style URL
if AWS_S3_REGION_NAME == 'us-east-1':
MEDIA_URL = f'https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/{AWS_LOCATION}/'
else:
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')
AZURE_ACCOUNT_KEY = os.environ.get('AZURE_ACCOUNT_KEY')
AZURE_CONTAINER = os.environ.get('AZURE_CONTAINER', 'media')
AZURE_SSL = os.environ.get('AZURE_SSL', 'True').lower() in ['true']
AZURE_UPLOAD_MAX_CONN = int(os.environ.get('AZURE_UPLOAD_MAX_CONN', '2'))
AZURE_CONNECTION_TIMEOUT_SECS = int(os.environ.get('AZURE_CONNECTION_TIMEOUT_SECS', '20'))
AZURE_BLOB_MAX_MEMORY_SIZE = os.environ.get('AZURE_BLOB_MAX_MEMORY_SIZE', '2MB')
AZURE_URL_EXPIRATION_SECS = int(os.environ.get('AZURE_URL_EXPIRATION_SECS', '3600'))
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",
},
"staticfiles": {
"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')
GS_PROJECT_ID = os.environ.get('GS_PROJECT_ID')
GS_CREDENTIALS = os.environ.get('GS_CREDENTIALS')
GS_DEFAULT_ACL = os.environ.get('GS_DEFAULT_ACL', 'private')
GS_FILE_OVERWRITE = os.environ.get('GS_FILE_OVERWRITE', 'False').lower() in ['true']
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",
},
"staticfiles": {
"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')
SFTP_STORAGE_ROOT = os.environ.get('SFTP_STORAGE_ROOT', '/media/')
SFTP_STORAGE_PARAMS = {
'port': int(os.environ.get('SFTP_STORAGE_PORT', '22')),
'username': os.environ.get('SFTP_STORAGE_USERNAME'),
'password': os.environ.get('SFTP_STORAGE_PASSWORD'),
'pkey': os.environ.get('SFTP_STORAGE_PRIVATE_KEY'),
}
SFTP_STORAGE_INTERACTIVE = os.environ.get('SFTP_STORAGE_INTERACTIVE', 'False').lower() in ['true']
SFTP_STORAGE_FILE_MODE = os.environ.get('SFTP_STORAGE_FILE_MODE')
SFTP_STORAGE_DIR_MODE = os.environ.get('SFTP_STORAGE_DIR_MODE')
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",
},
"staticfiles": {
"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",
},
"staticfiles": {
"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 = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"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']
OIDC_RP_SIGN_ALGO = os.environ.get('OIDC_RP_SIGN_ALGO', 'HS256')
OIDC_OP_JWKS_ENDPOINT = os.environ.get('OIDC_OP_JWKS_ENDPOINT')
OIDC_RP_IDP_SIGN_KEY = os.environ.get('OIDC_RP_IDP_SIGN_KEY')
OIDC_RP_CLIENT_ID = os.environ.get('OIDC_RP_CLIENT_ID')
OIDC_RP_CLIENT_SECRET = os.environ.get('OIDC_RP_CLIENT_SECRET')
OIDC_OP_AUTHORIZATION_ENDPOINT = os.environ.get('OIDC_OP_AUTHORIZATION_ENDPOINT')
OIDC_OP_TOKEN_ENDPOINT = os.environ.get('OIDC_OP_TOKEN_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_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")

View File

@@ -0,0 +1,177 @@
---
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

469
helm/templates/grafana.yaml Normal file
View File

@@ -0,0 +1,469 @@
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

View File

@@ -0,0 +1,14 @@
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 }}

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