Compare commits

..

12 Commits

Author SHA1 Message Date
2ba05a2913 Sonarqube temporarily turned off 2026-01-19 14:44:48 +01:00
b9e1a06e09 Error pages in correct design
Some checks failed
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 4s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 5s
SonarQube Scan / SonarQube Trigger (push) Failing after 49s
2026-01-19 13:52:07 +01:00
1a0c74bfa2 Static file serving out of DEBUG mode addressed
Some checks failed
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 1m4s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 5s
SonarQube Scan / SonarQube Trigger (push) Failing after 55s
2026-01-19 13:26:26 +01:00
82455358ff Allowed IPs in configmap changed, again
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 47s
2026-01-15 17:07:15 +01:00
713798352d Allowed IPs in configmap changed
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 48s
2026-01-15 17:05:22 +01:00
0e8e2da169 Removed secret deployment from argocd
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 48s
2026-01-15 16:59:24 +01:00
e8f34f7fa5 Django options pulled out into configmap; Docker build should now succeed despite no ENV-var with secret
Some checks failed
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 39s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
SonarQube Scan / SonarQube Trigger (push) Failing after 47s
2026-01-15 16:34:56 +01:00
67d4087e3a Changed secret key deployment; Updated requirements due to vulnerability in urllib
Some checks failed
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Failing after 1m3s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 7s
SonarQube Scan / SonarQube Trigger (push) Failing after 8s
2026-01-15 16:18:25 +01:00
ffda7ca601 SECRET_KEY now uses a kubernetes secret with a fallback value for local testing
Some checks failed
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 2m9s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 9s
SonarQube Scan / SonarQube Trigger (push) Failing after 2m29s
2026-01-15 16:04:25 +01:00
9d0a838238 Deployment
Some checks failed
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 22s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
SonarQube Scan / SonarQube Trigger (push) Failing after 46s
2026-01-09 14:21:29 +01:00
b0725fb385 Refactored XML export code between management command and view
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 56s
2026-01-09 14:18:33 +01:00
c77e8c0432 XML export adjusted 2026-01-09 13:55:43 +01:00
26 changed files with 1733 additions and 239 deletions

22
.argocdignore Normal file
View File

@@ -0,0 +1,22 @@
# ArgoCD ignore patterns
# Exclude template files from ArgoCD deployment
# Secret templates (deployed separately by scripts)
templates/
**/secret.yaml
# Documentation and scripts
docs/
scripts/
*.md
README*
# Development files
.env*
.git*
.vscode/
.idea/
# CI/CD files
.gitea/
.github/

View File

@@ -1,4 +1,4 @@
FROM python:3.14 AS baustelle
FROM python:3.15-rc-trixie AS baustelle
RUN mkdir /app
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
@@ -7,12 +7,12 @@ RUN pip install --upgrade pip
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.14-slim
FROM python:3.15-rc-slim-trixie
RUN useradd -m -r appuser && \
mkdir /app && \
chown -R appuser /app
COPY --from=baustelle /usr/local/lib/python3.14/site-packages/ /usr/local/lib/python3.14/site-packages/
COPY --from=baustelle /usr/local/lib/python3.15/site-packages/ /usr/local/lib/python3.15/site-packages/
COPY --from=baustelle /usr/local/bin/ /usr/local/bin/
RUN rm /usr/bin/tar /usr/lib/x86_64-linux-gnu/libncur*
WORKDIR /app
@@ -21,6 +21,8 @@ ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
USER appuser
EXPOSE 8000
# Set build environment variable to enable fallback secret key during build
ENV DOCKER_BUILDKIT=1
RUN rm -rvf /app/Dockerfile* \
/app/README.md \
/app/argocd \

View File

@@ -20,13 +20,34 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '429ti9tugj9güLLO))(G&G94KF452R3Fieaek$&6s#zlao-ca!#)_@j6*u+8s&bvfil^qyo%&-sov$ysi'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
DEBUG = os.environ.get('DEBUG', 'True').lower() in ('true', '1', 'yes', 'on')
ALLOWED_HOSTS = ["10.128.128.144","localhost","127.0.0.1","*"]
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('VORGABENUI_SECRET')
if not SECRET_KEY:
# Check if we're in a build environment (Docker build, CI, etc.)
is_build_env = any([
os.environ.get('DOCKER_BUILDKIT'), # Docker build
os.environ.get('CI'), # CI environment
os.environ.get('GITHUB_ACTIONS'), # GitHub Actions
os.environ.get('GITEA_ACTIONS'), # Gitea Actions
])
# Use DEBUG environment variable or assume debug mode for local development
debug_mode = os.environ.get('DEBUG', 'True').lower() in ('true', '1', 'yes', 'on')
if debug_mode or is_build_env:
# Fixed fallback key for local development and build environments
SECRET_KEY = 'dev-fallback-key-for-local-debugging-only-not-for-production-use-12345'
if not is_build_env: # Don't log during build to avoid noise
import logging
logging.warning("🚨 Using fallback SECRET_KEY for local development. This should NEVER happen in production!")
else:
raise ValueError("VORGABENUI_SECRET environment variable is required")
ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', "10.128.128.144,localhost,127.0.0.1,*").split(",")
# Application definition
@@ -37,6 +58,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'whitenoise',
'dokumente',
'abschnitte',
'stichworte',
@@ -48,6 +70,7 @@ INSTALLED_APPS = [
]
MIDDLEWARE = [
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
@@ -143,6 +166,12 @@ DIAGRAM_CACHE_DIR = 'diagram_cache' # relative to MEDIA_ROOT
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Custom error pages
handler400 = 'pages.views.custom_400'
handler403 = 'pages.views.custom_403'
handler404 = 'pages.views.custom_404'
handler500 = 'pages.views.custom_500'
DATA_UPLOAD_MAX_NUMBER_FIELDS=10250
NESTED_ADMIN_LAZY_INLINES = True

View File

@@ -40,9 +40,7 @@ urlpatterns = [
path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(template_name='registration/password_change_done.html'), name='password_change_done'),
]
# Serve static files
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# Serve media files (including cached diagrams)
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

25
argocd/configmap.yaml Normal file
View File

@@ -0,0 +1,25 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: django-config
namespace: vorgabenui
data:
# Django Configuration
DEBUG: "false"
DJANGO_ALLOWED_HOSTS: "vorgabenportal.knowyoursecurity.com,localhost,127.0.0.1,*"
DJANGO_SETTINGS_MODULE: "VorgabenUI.settings"
# Application Configuration
LANGUAGE_CODE: "de-ch"
TIME_ZONE: "UTC"
# Static and Media Configuration
STATIC_URL: "/static/"
MEDIA_URL: "/media/"
# Database Configuration (for future use)
# DATABASE_ENGINE: "django.db.backends.sqlite3"
# DATABASE_NAME: "/app/data/db.sqlite3"
# Security Configuration
# CSRF_TRUSTED_ORIGINS: "https://vorgabenportal.knowyoursecurity.com"

View File

@@ -25,8 +25,31 @@ spec:
mountPath: /data
containers:
- name: web
image: git.baumann.gr/adebaumann/vui:0.974
image: git.baumann.gr/adebaumann/vui:0.980
imagePullPolicy: Always
env:
# Secret configuration
- name: VORGABENUI_SECRET
valueFrom:
secretKeyRef:
name: vorgabenui-secrets
key: vorgabenui_secret
# ConfigMap configuration
- name: DEBUG
valueFrom:
configMapKeyRef:
name: django-config
key: DEBUG
- name: DJANGO_ALLOWED_HOSTS
valueFrom:
configMapKeyRef:
name: django-config
key: DJANGO_ALLOWED_HOSTS
- name: DJANGO_SETTINGS_MODULE
valueFrom:
configMapKeyRef:
name: django-config
key: DJANGO_SETTINGS_MODULE
ports:
- containerPort: 8000
volumeMounts:

383
docs/kubernetes-secrets.md Normal file
View File

@@ -0,0 +1,383 @@
# Kubernetes Configuration Management for VorgabenUI Django
This document describes how to manage Django configuration using Kubernetes secrets and ConfigMaps.
## Overview
Django configuration has been moved to Kubernetes-native resources for improved security and flexibility:
### **Secrets** (for sensitive data)
- `VORGABENUI_SECRET` - Django SECRET_KEY
- Future: Database passwords, API keys, etc.
### **ConfigMaps** (for non-sensitive configuration)
- `DEBUG` - Debug mode setting
- `DJANGO_ALLOWED_HOSTS` - Allowed hostnames
- `DJANGO_SETTINGS_MODULE` - Settings module path
- Application configuration settings
This approach ensures that:
1. Sensitive data is not stored in version control
2. Configuration is environment-specific
3. Non-sensitive settings are easily manageable
4. Follows Kubernetes best practices
5. Includes fallback for local development
## Files Changed
### VorgabenUI/settings.py
- Replaced hardcoded `SECRET_KEY` with `VORGABENUI_SECRET` environment variable lookup
- Added fallback secret key for local development (only works when DEBUG=True)
- Added warning when fallback key is used
### Files Created/Updated
#### **Configuration Resources**
- `argocd/configmap.yaml` - Django configuration (DEBUG, ALLOWED_HOSTS, etc.)
- `templates/configmap.yaml` - ConfigMap template (excluded from ArgoCD)
- `templates/secret.yaml` - Secret template (excluded from ArgoCD deployment)
- `argocd/secret.yaml` - ArgoCD-specific secret template with ignore annotation
#### **Deployment Configuration**
- `argocd/deployment.yaml` - Updated with Secret and ConfigMap environment variables
- `.argocdignore` - ArgoCD ignore patterns for templates and scripts
#### **Deployment Scripts**
- `scripts/deploy-argocd-secret.sh` - ArgoCD-specific script to deploy secrets
- `scripts/deploy-argocd-configmap.sh` - ArgoCD-specific script to deploy ConfigMap
#### **Application Code**
- `VorgabenUI/settings.py` - Updated to use environment variables from ConfigMap
#### **Examples and Documentation**
- `k8s/django-secret.yaml` - Updated for consistency (vorgabenui namespace)
- `k8s/django-deployment-example.yaml` - Updated example deployment
- `scripts/deploy-django-secret.sh` - Updated with new defaults
## Usage
### 1. Deploy ConfigMap (ArgoCD Production)
**Deploy configuration first** (required before the application starts):
```bash
# Deploy ConfigMap to vorgabenui namespace
./scripts/deploy-argocd-configmap.sh
# Verify existing ConfigMap
./scripts/deploy-argocd-configmap.sh --verify-only
# Dry run to see what would happen
./scripts/deploy-argocd-configmap.sh --dry-run
# Get help
./scripts/deploy-argocd-configmap.sh --help
```
### 2. Deploy the Secret (ArgoCD Production)
**Deploy secret second** (contains sensitive SECRET_KEY):
```bash
# Deploy secret to vorgabenui namespace
./scripts/deploy-argocd-secret.sh
# Verify existing secret
./scripts/deploy-argocd-secret.sh --verify-only
# Dry run to see what would happen
./scripts/deploy-argocd-secret.sh --dry-run
# Get help
./scripts/deploy-argocd-secret.sh --help
```
### 3. Deploy Resources for Other Environments
For development or other environments, use the general scripts:
```bash
# Deploy ConfigMap to vorgabenui namespace (default)
./scripts/deploy-django-secret.sh # (includes ConfigMap deployment)
# Deploy to specific namespace
./scripts/deploy-django-secret.sh -n development
# Get help
./scripts/deploy-django-secret.sh --help
```
### 4. Environment Variable Configuration
The ArgoCD deployment (`argocd/deployment.yaml`) is configured with:
**Secret Variables:**
```yaml
env:
# Secret configuration
- name: VORGABENUI_SECRET
valueFrom:
secretKeyRef:
name: vorgabenui-secrets
key: vorgabenui_secret
```
**ConfigMap Variables:**
```yaml
# ConfigMap configuration
- name: DEBUG
valueFrom:
configMapKeyRef:
name: django-config
key: DEBUG
- name: DJANGO_ALLOWED_HOSTS
valueFrom:
configMapKeyRef:
name: django-config
key: DJANGO_ALLOWED_HOSTS
- name: DJANGO_SETTINGS_MODULE
valueFrom:
configMapKeyRef:
name: django-config
key: DJANGO_SETTINGS_MODULE
```
For other deployments, see `k8s/django-deployment-example.yaml` for a complete example.
### 5. Verify the Deployment
**Check ConfigMap:**
```bash
kubectl get configmap django-config -n vorgabenui
kubectl describe configmap django-config -n vorgabenui
```
**Check Secret:**
```bash
kubectl get secrets vorgabenui-secrets -n vorgabenui
kubectl describe secret vorgabenui-secrets -n vorgabenui
```
**Check Django pods can access configuration:**
```bash
# Check secret variable
kubectl exec -n vorgabenui deployment/django -- printenv VORGABENUI_SECRET
# Check ConfigMap variables
kubectl exec -n vorgabenui deployment/django -- printenv DEBUG
kubectl exec -n vorgabenui deployment/django -- printenv DJANGO_ALLOWED_HOSTS
# Check all environment variables
kubectl exec -n vorgabenui deployment/django -- printenv | grep -E "(VORGABENUI|DEBUG|DJANGO)"
```
## Development Environment
### Local Development with Fallback
The application now includes a fallback secret key for local development. When running locally:
1. **Automatic fallback**: If `VORGABENUI_SECRET` is not set and `DEBUG=True`, a fallback key is used automatically
2. **Warning message**: The application will log a warning when using the fallback key (except during builds)
3. **Production safety**: Fallback only works when `DEBUG=True` or in build environments
### Docker Build Support
The Django settings are designed to work seamlessly during Docker builds:
1. **Build environment detection**: Automatically detects Docker builds, CI environments
2. **Fallback activation**: Uses fallback key during build without requiring environment variables
3. **No build-time secrets**: No need to provide `VORGABENUI_SECRET` during `docker build`
4. **Runtime security**: Production containers still require the proper environment variable
**Supported build environments:**
- Docker builds (`DOCKER_BUILDKIT`)
- CI environments (`CI`)
- GitHub Actions (`GITHUB_ACTIONS`)
- Gitea Actions (`GITEA_ACTIONS`)
- Local development (`DEBUG=True`)
### Manual Environment Variable
You can still set the environment variable manually:
```bash
# Option 1: Export the variable
export VORGABENUI_SECRET="your-development-key-here"
python manage.py runserver
# Option 2: Use a .env file (recommended)
echo "VORGABENUI_SECRET=your-development-key-here" > .env
# Then load it in your settings or use python-dotenv
```
### Development vs Production
- **Local Development**: Fallback key works automatically when `DEBUG=True`
- **Production**: Must have `VORGABENUI_SECRET` environment variable set, no fallback
## ArgoCD Integration and Exclusions
### Preventing ArgoCD from Deploying Secret Templates
This setup includes multiple approaches to prevent ArgoCD from trying to deploy the secret template:
#### 1. Template Directory (`templates/`)
- Secret template moved to `templates/` directory
- ArgoCD deployment script automatically uses this location
- Excluded via `.argocdignore` file
#### 2. ArgoCD Ignore Annotation
- `argocd/secret.yaml` has `argocd.argoproj.io/ignore: "true"` annotation
- Provides fallback if templates directory approach fails
#### 3. `.argocdignore` File
- Global exclusion patterns for templates, scripts, and documentation
- Prevents ArgoCD from syncing non-deployment files
### ArgoCD Sync Behavior
- ArgoCD will sync only the actual deployment files (`deployment.yaml`, `ingress.yaml`, etc.)
- Secret templates are excluded and must be deployed manually using the deployment script
- This ensures secrets are created outside of GitOps workflow for security
## Security Considerations
1. **Never commit the actual SECRET_KEY** - Only templates and scripts are in version control
2. **Use different keys per environment** - Production, staging, and development should all have unique keys
3. **Rotate keys regularly** - Run the deployment script periodically to generate new keys
4. **Limit access** - Use Kubernetes RBAC to control who can access secrets
5. **ArgoCD exclusion** - Secret templates are excluded from ArgoCD to prevent empty/template secrets from being deployed
## Troubleshooting
### Django fails to start with "VORGABENUI_SECRET environment variable is required"
This means the environment variable is not set in your pod and fallback conditions aren't met. Check:
1. **Secret exists**: `kubectl get secret vorgabenui-secrets -n vorgabenui`
2. **Deployment references secret correctly**: Check `argocd/deployment.yaml` env section
3. **Pod has environment variable**: `kubectl exec <pod-name> -n vorgabenui -- env | grep VORGABENUI_SECRET`
4. **For local development**: Ensure `DEBUG=True` to use the fallback key
5. **For Docker builds**: Build should work automatically with fallback
### Docker build fails with SECRET_KEY error
This should no longer happen with the updated settings. If you still see issues:
1. **Check build environment variables**: Build should detect `DOCKER_BUILDKIT=1`
2. **Verify settings changes**: Ensure the updated `settings.py` is being used
3. **Force environment detection**: Set `CI=1` during build if needed
4. **Use explicit DEBUG**: Set `DEBUG=True` during build as fallback
### Secret deployment fails
Check that:
1. You have kubectl access to the cluster
2. You have permission to create secrets in the `vorgabenui` namespace
3. Python3 is available for key generation
4. The ArgoCD secret template exists: `argocd/secret.yaml`
### Key rotation
To rotate the SECRET_KEY:
1. **For ArgoCD production**: Run `./scripts/deploy-argocd-secret.sh` again
2. **For other environments**: Run `./scripts/deploy-django-secret.sh` again
3. Restart your Django pods to pick up the new key:
```bash
# For ArgoCD production
kubectl rollout restart deployment/django -n vorgabenui
# For other environments
kubectl rollout restart deployment/your-django-deployment -n your-namespace
```
## Script Options
### ArgoCD Production Scripts
#### **ConfigMap Script (`deploy-argocd-configmap.sh`)**
Deploy Django configuration (non-sensitive):
- `--verify-only` - Only verify existing ConfigMap, don't deploy
- `--dry-run` - Show what would be deployed without applying
- `-h, --help` - Show help message
Configuration is hardcoded for ArgoCD:
- Namespace: `vorgabenui`
- ConfigMap name: `django-config`
- ConfigMap file: `argocd/configmap.yaml`
#### **Secret Script (`deploy-argocd-secret.sh`)**
Deploy sensitive configuration:
- `--verify-only` - Only verify existing secret, don't create new one
- `--dry-run` - Show what would be done without making changes
- `-h, --help` - Show help message
Configuration is hardcoded for ArgoCD:
- Namespace: `vorgabenui`
- Secret name: `vorgabenui-secrets`
- Secret key: `vorgabenui_secret`
- Template location: `templates/secret.yaml` (excluded from ArgoCD)
### General Script (`deploy-django-secret.sh`)
For development and other environments:
- `-n, --namespace NAMESPACE` - Target Kubernetes namespace (default: vorgabenui)
- `-s, --secret-name NAME` - Secret name (default: vorgabenui-secrets)
- `-k, --key-name NAME` - Secret key name (default: vorgabenui_secret)
- `-h, --help` - Show help message
Environment variables:
- `NAMESPACE` - Override default namespace
## Migration from Hardcoded Key
### Migration from Old Setup
If you're migrating from the previous `DJANGO_SECRET_KEY` setup:
1. **Deploy the new secret** using `./scripts/deploy-argocd-secret.sh`
2. **Update any existing deployments** to use `VORGABENUI_SECRET` instead of `DJANGO_SECRET_KEY`
3. **Test locally** - the fallback key should work automatically in DEBUG mode
4. **Deploy the updated application** - ArgoCD deployment is already configured
### Migration from Hardcoded Key
If you're migrating from a completely hardcoded key:
1. **Backup your current key** (in case you need to rollback)
2. **Deploy the secret first** using the deployment script
3. **Apply the updated ArgoCD deployment** (already done in this setup)
4. **Test thoroughly** - local development should work with fallback
5. **Deploy the updated settings.py** after confirming the secret works
The ArgoCD deployment (`argocd/deployment.yaml`) now includes the environment variable configuration, so Django will automatically pick up the secret after deployment.
## Deployment Order
**Critical: Deploy resources in this order:**
1. **ConfigMap first** (required for Django to start):
```bash
./scripts/deploy-argocd-configmap.sh
```
2. **Secret second** (contains sensitive data):
```bash
./scripts/deploy-argocd-secret.sh
```
3. **Application deployment** (ArgoCD will sync this automatically):
```bash
kubectl apply -f argocd/deployment.yaml
# OR let ArgoCD sync from Git
```
If you deploy in the wrong order, Django pods will fail to start because they require both the ConfigMap and Secret to be available.

View File

@@ -1,7 +1,7 @@
from django.core.management.base import BaseCommand
import xml.etree.ElementTree as ET
from datetime import datetime
from dokumente.models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage
from dokumente.models import Dokument
from dokumente.utils import build_dokument_xml_element, prettify_xml
class Command(BaseCommand):
@@ -16,7 +16,7 @@ class Command(BaseCommand):
def handle(self, *args, **options):
dokumente = Dokument.objects.filter(aktiv=True).prefetch_related(
'autoren', 'pruefende', 'vorgaben__thema',
'autoren', 'pruefende', 'vorgaben__thema',
'vorgaben__referenzen', 'vorgaben__stichworte',
'vorgaben__checklistenfragen', 'vorgaben__vorgabekurztext_set',
'vorgaben__vorgabelangtext_set', 'geltungsbereich_set',
@@ -26,119 +26,14 @@ class Command(BaseCommand):
root = ET.Element('Vorgabendokumente')
for dokument in dokumente:
doc_element = ET.SubElement(root, 'Vorgabendokument')
ET.SubElement(doc_element, 'Typ').text = dokument.dokumententyp.name if dokument.dokumententyp else ""
ET.SubElement(doc_element, 'Nummer').text = dokument.nummer
ET.SubElement(doc_element, 'Name').text = dokument.name
autoren_element = ET.SubElement(doc_element, 'Autoren')
for autor in dokument.autoren.all():
ET.SubElement(autoren_element, 'Autor').text = autor.name
pruefende_element = ET.SubElement(doc_element, 'Pruefende')
for pruefender in dokument.pruefende.all():
ET.SubElement(pruefende_element, 'Pruefender').text = pruefender.name
gueltigkeit_element = ET.SubElement(doc_element, 'Gueltigkeit')
ET.SubElement(gueltigkeit_element, 'Von').text = dokument.gueltigkeit_von.strftime("%Y-%m-%d") if dokument.gueltigkeit_von else ""
ET.SubElement(gueltigkeit_element, 'Bis').text = dokument.gueltigkeit_bis.strftime("%Y-%m-%d") if dokument.gueltigkeit_bis else None
ET.SubElement(doc_element, 'SignaturCSO').text = dokument.signatur_cso
geltungsbereich_sections = dokument.geltungsbereich_set.all().order_by('order')
if geltungsbereich_sections:
geltungsbereich_element = ET.SubElement(doc_element, 'Geltungsbereich')
abschnitt_element = ET.SubElement(geltungsbereich_element, 'Abschnitt')
for gb in geltungsbereich_sections:
section = ET.SubElement(abschnitt_element, 'Teil')
section.set('typ', gb.abschnitttyp.abschnitttyp if gb.abschnitttyp else "text")
section.text = gb.inhalt
einleitung_sections = dokument.einleitung_set.all().order_by('order')
if einleitung_sections:
einleitung_element = ET.SubElement(doc_element, 'Einleitung')
abschnitt_element = ET.SubElement(einleitung_element, 'Abschnitt')
for ei in einleitung_sections:
section = ET.SubElement(abschnitt_element, 'Teil')
section.set('typ', ei.abschnitttyp.abschnitttyp if ei.abschnitttyp else "text")
section.text = ei.inhalt
ET.SubElement(doc_element, 'Ziel').text = ""
ET.SubElement(doc_element, 'Grundlagen').text = ""
changelog_element = ET.SubElement(doc_element, 'Changelog')
for cl in dokument.changelog.all().order_by('-datum'):
entry = ET.SubElement(changelog_element, 'Eintrag')
ET.SubElement(entry, 'Datum').text = cl.datum.strftime("%Y-%m-%d")
autoren = ET.SubElement(entry, 'Autoren')
for autor in cl.autoren.all():
ET.SubElement(autoren, 'Autor').text = autor.name
ET.SubElement(entry, 'Aenderung').text = cl.aenderung
anhaenge_element = ET.SubElement(doc_element, 'Anhaenge')
ET.SubElement(anhaenge_element, 'Anhang').text = dokument.anhaenge
ET.SubElement(doc_element, 'Verantwortlich').text = "Information Security Management BIT"
ET.SubElement(doc_element, 'Klassifizierung').text = ""
glossar_element = ET.SubElement(doc_element, 'Glossar')
vorgaben_element = ET.SubElement(doc_element, 'Vorgaben')
for vorgabe in dokument.vorgaben.all().order_by('order'):
vorgabe_el = ET.SubElement(vorgaben_element, 'Vorgabe')
ET.SubElement(vorgabe_el, 'Nummer').text = str(vorgabe.nummer)
ET.SubElement(vorgabe_el, 'Titel').text = vorgabe.titel
ET.SubElement(vorgabe_el, 'Thema').text = vorgabe.thema.name if vorgabe.thema else ""
kurztext_sections = vorgabe.vorgabekurztext_set.all().order_by('order')
if kurztext_sections:
kurztext_element = ET.SubElement(vorgabe_el, 'Kurztext')
abschnitt = ET.SubElement(kurztext_element, 'Abschnitt')
for kt in kurztext_sections:
teil = ET.SubElement(abschnitt, 'Teil')
teil.set('typ', kt.abschnitttyp.abschnitttyp if kt.abschnitttyp else "text")
teil.text = kt.inhalt
langtext_sections = vorgabe.vorgabelangtext_set.all().order_by('order')
if langtext_sections:
langtext_element = ET.SubElement(vorgabe_el, 'Langtext')
abschnitt = ET.SubElement(langtext_element, 'Abschnitt')
for lt in langtext_sections:
teil = ET.SubElement(abschnitt, 'Teil')
teil.set('typ', lt.abschnitttyp.abschnitttyp if lt.abschnitttyp else "text")
teil.text = lt.inhalt
referenz_element = ET.SubElement(vorgabe_el, 'Referenzen')
for ref in vorgabe.referenzen.all():
ref_text = f"{ref.name_nummer}: {ref.name_text}" if ref.name_text else ref.name_nummer
ET.SubElement(referenz_element, 'Referenz').text = ref_text
vorgabe_gueltigkeit = ET.SubElement(vorgabe_el, 'Gueltigkeit')
ET.SubElement(vorgabe_gueltigkeit, 'Von').text = vorgabe.gueltigkeit_von.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_von else ""
ET.SubElement(vorgabe_gueltigkeit, 'Bis').text = vorgabe.gueltigkeit_bis.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_bis else None
checklistenfragen_element = ET.SubElement(vorgabe_el, 'Checklistenfragen')
for cf in vorgabe.checklistenfragen.all():
ET.SubElement(checklistenfragen_element, 'Frage').text = cf.frage
stichworte_element = ET.SubElement(vorgabe_el, 'Stichworte')
for stw in vorgabe.stichworte.all():
ET.SubElement(stichworte_element, 'Stichwort').text = stw.stichwort
build_dokument_xml_element(dokument, root)
xml_str = ET.tostring(root, encoding='unicode', method='xml')
xml_output = self._prettify_xml(xml_str)
xml_output = prettify_xml(xml_str)
if options['output']:
with open(options['output'], 'w', encoding='utf-8') as f:
f.write(xml_output)
self.stdout.write(self.style.SUCCESS(f'XML exported to {options["output"]}'))
else:
self.stdout.write(xml_output)
def _prettify_xml(self, xml_string):
import xml.dom.minidom
dom = xml.dom.minidom.parseString(xml_string)
return dom.toprettyxml(indent=" ", encoding="UTF-8").decode('utf-8')

View File

@@ -1675,6 +1675,54 @@ class ExportXMLCommandTest(TestCase):
# Should still contain active document
self.assertIn('TEST-001', output)
self.assertIn('Test Standard', output)
def test_export_xml_command_table_structure(self):
"""Test export_xml command converts markdown tables to proper XML structure"""
# Create document with table
table_doc = Dokument.objects.create(
nummer="TABLE-001",
dokumententyp=self.dokumententyp,
name="Table Test Document",
aktiv=True
)
table_doc.autoren.add(self.autor1)
table_vorgabe = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=table_doc,
thema=self.thema,
titel="Table Test Vorgabe",
gueltigkeit_von=date(2023, 1, 1),
gueltigkeit_bis=date(2025, 12, 31)
)
table_content = "| Spalte1 | Spalte2 |\n|---------|---------|\n| Wert1 | Wert2 |\n| Wert3 | Wert4 |"
self.langtext_table = VorgabeLangtext.objects.create(
abschnitt=table_vorgabe,
abschnitttyp=self.abschnitttyp_table,
inhalt=table_content,
order=1
)
out = StringIO()
call_command('export_xml', stdout=out)
output = out.getvalue()
# Check that table structure is properly exported
self.assertIn('<table>', output)
self.assertIn('<header>', output)
self.assertIn('<column>Spalte1</column>', output)
self.assertIn('<column>Spalte2</column>', output)
self.assertIn('<row>', output)
self.assertIn('<column>Wert1</column>', output)
self.assertIn('<column>Wert2</column>', output)
self.assertIn('<column>Wert3</column>', output)
self.assertIn('<column>Wert4</column>', output)
# Should not contain the markdown table content as plain text
self.assertNotIn('| Spalte1 | Spalte2 |', output)
class StandardJSONViewTest(TestCase):
@@ -1915,6 +1963,7 @@ class StandardXMLViewTest(TestCase):
# Create text sections
self.abschnitttyp_text = AbschnittTyp.objects.create(abschnitttyp="text")
self.abschnitttyp_table = AbschnittTyp.objects.create(abschnitttyp="table")
self.geltungsbereich = Geltungsbereich.objects.create(
geltungsbereich=self.dokument,
@@ -2049,6 +2098,71 @@ class StandardXMLViewTest(TestCase):
self.assertIn('<?xml version', response.content.decode())
self.assertIn('\n', response.content.decode())
self.assertIn(' ', response.content.decode()) # Check for indentation
def test_standard_xml_view_table_structure(self):
"""Test standard_xml view converts markdown tables to proper XML structure"""
# Create document with table
table_doc = Dokument.objects.create(
nummer="TABLE-XML-001",
dokumententyp=self.dokumententyp,
name="Table XML Test Document",
aktiv=True
)
table_doc.autoren.add(self.autor)
table_vorgabe = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=table_doc,
thema=self.thema,
titel="Table XML Test Vorgabe",
gueltigkeit_von=date(2023, 1, 1),
gueltigkeit_bis=date(2025, 12, 31)
)
table_content = "| Col1 | Col2 |\n|------|------|\n| A | B |\n| C | D |"
langtext_table = VorgabeLangtext.objects.create(
abschnitt=table_vorgabe,
abschnitttyp=self.abschnitttyp_table,
inhalt=table_content,
order=1
)
url = reverse('standard_xml', kwargs={'nummer': 'TABLE-XML-001'})
response = self.client.get(url)
# Parse XML response
import xml.etree.ElementTree as ET
root = ET.fromstring(response.content)
# Find table element
table = root.find('.//table')
self.assertIsNotNone(table, 'Table element should exist')
# Check header structure
header = table.find('header')
self.assertIsNotNone(header, 'Header should exist')
header_cols = header.findall('column')
self.assertEqual(len(header_cols), 2, 'Header should have 2 columns')
self.assertEqual(header_cols[0].text, 'Col1')
self.assertEqual(header_cols[1].text, 'Col2')
# Check row structure
rows = table.findall('row')
self.assertEqual(len(rows), 2, 'Should have 2 data rows')
# Check first row
row1_cols = rows[0].findall('column')
self.assertEqual(len(row1_cols), 2)
self.assertEqual(row1_cols[0].text, 'A')
self.assertEqual(row1_cols[1].text, 'B')
# Check second row
row2_cols = rows[1].findall('column')
self.assertEqual(len(row2_cols), 2)
self.assertEqual(row2_cols[0].text, 'C')
self.assertEqual(row2_cols[1].text, 'D')
class VorgabeCommentModelTest(TestCase):

View File

@@ -1,7 +1,9 @@
"""
Utility functions for Vorgaben sanity checking
Utility functions for Vorgaben sanity checking and XML export
"""
import datetime
import xml.etree.ElementTree as ET
import xml.dom.minidom
from django.db.models import Count
from itertools import combinations
from dokumente.models import Vorgabe
@@ -119,5 +121,192 @@ def format_conflict_report(conflicts, verbose=False):
lines.append(f" Overlap: {overlap_start} to {overlap_end}")
else:
lines.append(f" Overlap starts: {overlap_start} (no end)")
return "\n".join(lines)
return "\n".join(lines)
# XML Export utilities
def parse_markdown_table(markdown_content):
"""
Parse markdown table content and return XML element with <table><header><row><column> structure
"""
lines = [line.strip() for line in markdown_content.strip().split('\n') if line.strip()]
if not lines:
return None
# Create table element
table = ET.Element('table')
# Parse first row as header
header_row = [cell.strip() for cell in lines[0].split('|') if cell.strip()]
header = ET.SubElement(table, 'header')
for cell in header_row:
column = ET.SubElement(header, 'column')
column.text = cell
# Parse remaining rows (skip separator row if it exists)
for line in lines[2:] if len(lines) > 1 and all(c in '-| ' for c in lines[1]) else lines[1:]:
# Check if this is a separator row
if all(c in '-| ' for c in line):
continue
row = ET.SubElement(table, 'row')
row_cells = [cell.strip() for cell in line.split('|') if cell.strip()]
for cell in row_cells:
column = ET.SubElement(row, 'column')
column.text = cell
return table
def prettify_xml(xml_string):
"""
Prettify XML string with proper indentation
"""
dom = xml.dom.minidom.parseString(xml_string)
return dom.toprettyxml(indent=" ", encoding="UTF-8").decode('utf-8')
def build_dokument_xml_element(dokument, parent_element):
"""
Build XML element for a single Dokument and append it to parent_element.
Args:
dokument: Dokument instance (should be prefetched with related data)
parent_element: Parent XML element to append to
Returns:
The created document element
"""
doc_element = ET.SubElement(parent_element, 'Vorgabendokument')
ET.SubElement(doc_element, 'Typ').text = dokument.dokumententyp.name if dokument.dokumententyp else ""
ET.SubElement(doc_element, 'Nummer').text = dokument.nummer
ET.SubElement(doc_element, 'Name').text = dokument.name
autoren_element = ET.SubElement(doc_element, 'Autoren')
for autor in dokument.autoren.all():
ET.SubElement(autoren_element, 'Autor').text = autor.name
pruefende_element = ET.SubElement(doc_element, 'Pruefende')
for pruefender in dokument.pruefende.all():
ET.SubElement(pruefende_element, 'Pruefender').text = pruefender.name
gueltigkeit_element = ET.SubElement(doc_element, 'Gueltigkeit')
ET.SubElement(gueltigkeit_element, 'Von').text = dokument.gueltigkeit_von.strftime("%Y-%m-%d") if dokument.gueltigkeit_von else ""
ET.SubElement(gueltigkeit_element, 'Bis').text = dokument.gueltigkeit_bis.strftime("%Y-%m-%d") if dokument.gueltigkeit_bis else None
ET.SubElement(doc_element, 'SignaturCSO').text = dokument.signatur_cso
geltungsbereich_sections = dokument.geltungsbereich_set.all().order_by('order')
if geltungsbereich_sections:
geltungsbereich_element = ET.SubElement(doc_element, 'Geltungsbereich')
for gb in geltungsbereich_sections:
section_type = gb.abschnitttyp.abschnitttyp if gb.abschnitttyp else "text"
if section_type in ('tabelle', 'table'):
table = parse_markdown_table(gb.inhalt)
if table is not None:
abschnitt_element = ET.SubElement(geltungsbereich_element, 'Abschnitt')
abschnitt_element.set('typ', section_type)
abschnitt_element.append(table)
else:
abschnitt_element = ET.SubElement(geltungsbereich_element, 'Abschnitt')
abschnitt_element.set('typ', section_type)
abschnitt_element.text = gb.inhalt
einleitung_sections = dokument.einleitung_set.all().order_by('order')
if einleitung_sections:
einleitung_element = ET.SubElement(doc_element, 'Einleitung')
for ei in einleitung_sections:
section_type = ei.abschnitttyp.abschnitttyp if ei.abschnitttyp else "text"
if section_type in ('tabelle', 'table'):
table = parse_markdown_table(ei.inhalt)
if table is not None:
abschnitt_element = ET.SubElement(einleitung_element, 'Abschnitt')
abschnitt_element.set('typ', section_type)
abschnitt_element.append(table)
else:
abschnitt_element = ET.SubElement(einleitung_element, 'Abschnitt')
abschnitt_element.set('typ', section_type)
abschnitt_element.text = ei.inhalt
ET.SubElement(doc_element, 'Ziel').text = ""
ET.SubElement(doc_element, 'Grundlagen').text = ""
changelog_element = ET.SubElement(doc_element, 'Changelog')
for cl in dokument.changelog.all().order_by('-datum'):
entry = ET.SubElement(changelog_element, 'Eintrag')
ET.SubElement(entry, 'Datum').text = cl.datum.strftime("%Y-%m-%d")
autoren = ET.SubElement(entry, 'Autoren')
for autor in cl.autoren.all():
ET.SubElement(autoren, 'Autor').text = autor.name
ET.SubElement(entry, 'Aenderung').text = cl.aenderung
anhaenge_element = ET.SubElement(doc_element, 'Anhaenge')
ET.SubElement(anhaenge_element, 'Anhang').text = dokument.anhaenge
ET.SubElement(doc_element, 'Verantwortlich').text = "Information Security Management BIT"
ET.SubElement(doc_element, 'Klassifizierung').text = ""
glossar_element = ET.SubElement(doc_element, 'Glossar')
vorgaben_element = ET.SubElement(doc_element, 'Vorgaben')
for vorgabe in dokument.vorgaben.all().order_by('order'):
vorgabe_el = ET.SubElement(vorgaben_element, 'Vorgabe')
ET.SubElement(vorgabe_el, 'Nummer').text = str(vorgabe.nummer)
ET.SubElement(vorgabe_el, 'Titel').text = vorgabe.titel
ET.SubElement(vorgabe_el, 'Thema').text = vorgabe.thema.name if vorgabe.thema else ""
kurztext_sections = vorgabe.vorgabekurztext_set.all().order_by('order')
if kurztext_sections:
kurztext_element = ET.SubElement(vorgabe_el, 'Kurztext')
for kt in kurztext_sections:
section_type = kt.abschnitttyp.abschnitttyp if kt.abschnitttyp else "text"
if section_type in ('tabelle', 'table'):
table = parse_markdown_table(kt.inhalt)
if table is not None:
abschnitt = ET.SubElement(kurztext_element, 'Abschnitt')
abschnitt.set('typ', section_type)
abschnitt.append(table)
else:
abschnitt = ET.SubElement(kurztext_element, 'Abschnitt')
abschnitt.set('typ', section_type)
abschnitt.text = kt.inhalt
langtext_sections = vorgabe.vorgabelangtext_set.all().order_by('order')
if langtext_sections:
langtext_element = ET.SubElement(vorgabe_el, 'Langtext')
for lt in langtext_sections:
section_type = lt.abschnitttyp.abschnitttyp if lt.abschnitttyp else "text"
if section_type in ('tabelle', 'table'):
table = parse_markdown_table(lt.inhalt)
if table is not None:
abschnitt = ET.SubElement(langtext_element, 'Abschnitt')
abschnitt.set('typ', section_type)
abschnitt.append(table)
else:
abschnitt = ET.SubElement(langtext_element, 'Abschnitt')
abschnitt.set('typ', section_type)
abschnitt.text = lt.inhalt
referenz_element = ET.SubElement(vorgabe_el, 'Referenzen')
for ref in vorgabe.referenzen.all():
ref_text = f"{ref.name_nummer}: {ref.name_text}" if ref.name_text else ref.name_nummer
ET.SubElement(referenz_element, 'Referenz').text = ref_text
vorgabe_gueltigkeit = ET.SubElement(vorgabe_el, 'Gueltigkeit')
ET.SubElement(vorgabe_gueltigkeit, 'Von').text = vorgabe.gueltigkeit_von.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_von else ""
ET.SubElement(vorgabe_gueltigkeit, 'Bis').text = vorgabe.gueltigkeit_bis.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_bis else None
checklistenfragen_element = ET.SubElement(vorgabe_el, 'Checklistenfragen')
for cf in vorgabe.checklistenfragen.all():
ET.SubElement(checklistenfragen_element, 'Frage').text = cf.frage
stichworte_element = ET.SubElement(vorgabe_el, 'Stichworte')
for stw in vorgabe.stichworte.all():
ET.SubElement(stichworte_element, 'Stichwort').text = stw.stichwort
return doc_element

View File

@@ -9,6 +9,7 @@ from django.utils.safestring import SafeString
import json
import xml.etree.ElementTree as ET
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage, VorgabeComment
from .utils import build_dokument_xml_element, prettify_xml
from abschnitte.utils import render_textabschnitte
from datetime import date
@@ -262,7 +263,7 @@ def standard_xml(request, nummer):
# Get the document with all related data
dokument = get_object_or_404(
Dokument.objects.prefetch_related(
'autoren', 'pruefende', 'vorgaben__thema',
'autoren', 'pruefende', 'vorgaben__thema',
'vorgaben__referenzen', 'vorgaben__stichworte',
'vorgaben__checklistenfragen', 'vorgaben__vorgabekurztext_set',
'vorgaben__vorgabelangtext_set', 'geltungsbereich_set',
@@ -271,122 +272,21 @@ def standard_xml(request, nummer):
nummer=nummer
)
root = ET.Element('Vorgabendokument')
ET.SubElement(root, 'Typ').text = dokument.dokumententyp.name if dokument.dokumententyp else ""
ET.SubElement(root, 'Nummer').text = dokument.nummer
ET.SubElement(root, 'Name').text = dokument.name
autoren_element = ET.SubElement(root, 'Autoren')
for autor in dokument.autoren.all():
ET.SubElement(autoren_element, 'Autor').text = autor.name
pruefende_element = ET.SubElement(root, 'Pruefende')
for pruefender in dokument.pruefende.all():
ET.SubElement(pruefende_element, 'Pruefender').text = pruefender.name
gueltigkeit_element = ET.SubElement(root, 'Gueltigkeit')
ET.SubElement(gueltigkeit_element, 'Von').text = dokument.gueltigkeit_von.strftime("%Y-%m-%d") if dokument.gueltigkeit_von else ""
ET.SubElement(gueltigkeit_element, 'Bis').text = dokument.gueltigkeit_bis.strftime("%Y-%m-%d") if dokument.gueltigkeit_bis else None
ET.SubElement(root, 'SignaturCSO').text = dokument.signatur_cso
geltungsbereich_sections = dokument.geltungsbereich_set.all().order_by('order')
if geltungsbereich_sections:
geltungsbereich_element = ET.SubElement(root, 'Geltungsbereich')
abschnitt_element = ET.SubElement(geltungsbereich_element, 'Abschnitt')
for gb in geltungsbereich_sections:
section = ET.SubElement(abschnitt_element, 'Teil')
section.set('typ', gb.abschnitttyp.abschnitttyp if gb.abschnitttyp else "text")
section.text = gb.inhalt
einleitung_sections = dokument.einleitung_set.all().order_by('order')
if einleitung_sections:
einleitung_element = ET.SubElement(root, 'Einleitung')
abschnitt_element = ET.SubElement(einleitung_element, 'Abschnitt')
for ei in einleitung_sections:
section = ET.SubElement(abschnitt_element, 'Teil')
section.set('typ', ei.abschnitttyp.abschnitttyp if ei.abschnitttyp else "text")
section.text = ei.inhalt
ET.SubElement(root, 'Ziel').text = ""
ET.SubElement(root, 'Grundlagen').text = ""
changelog_element = ET.SubElement(root, 'Changelog')
for cl in dokument.changelog.all().order_by('-datum'):
entry = ET.SubElement(changelog_element, 'Eintrag')
ET.SubElement(entry, 'Datum').text = cl.datum.strftime("%Y-%m-%d")
autoren = ET.SubElement(entry, 'Autoren')
for autor in cl.autoren.all():
ET.SubElement(autoren, 'Autor').text = autor.name
ET.SubElement(entry, 'Aenderung').text = cl.aenderung
anhaenge_element = ET.SubElement(root, 'Anhaenge')
ET.SubElement(anhaenge_element, 'Anhang').text = dokument.anhaenge
ET.SubElement(root, 'Verantwortlich').text = "Information Security Management BIT"
ET.SubElement(root, 'Klassifizierung').text = ""
glossar_element = ET.SubElement(root, 'Glossar')
vorgaben_element = ET.SubElement(root, 'Vorgaben')
for vorgabe in dokument.vorgaben.all().order_by('order'):
vorgabe_el = ET.SubElement(vorgaben_element, 'Vorgabe')
ET.SubElement(vorgabe_el, 'Nummer').text = str(vorgabe.nummer)
ET.SubElement(vorgabe_el, 'Titel').text = vorgabe.titel
ET.SubElement(vorgabe_el, 'Thema').text = vorgabe.thema.name if vorgabe.thema else ""
kurztext_sections = vorgabe.vorgabekurztext_set.all().order_by('order')
if kurztext_sections:
kurztext_element = ET.SubElement(vorgabe_el, 'Kurztext')
abschnitt = ET.SubElement(kurztext_element, 'Abschnitt')
for kt in kurztext_sections:
teil = ET.SubElement(abschnitt, 'Teil')
teil.set('typ', kt.abschnitttyp.abschnitttyp if kt.abschnitttyp else "text")
teil.text = kt.inhalt
langtext_sections = vorgabe.vorgabelangtext_set.all().order_by('order')
if langtext_sections:
langtext_element = ET.SubElement(vorgabe_el, 'Langtext')
abschnitt = ET.SubElement(langtext_element, 'Abschnitt')
for lt in langtext_sections:
teil = ET.SubElement(abschnitt, 'Teil')
teil.set('typ', lt.abschnitttyp.abschnitttyp if lt.abschnitttyp else "text")
teil.text = lt.inhalt
referenz_element = ET.SubElement(vorgabe_el, 'Referenzen')
for ref in vorgabe.referenzen.all():
ref_text = f"{ref.name_nummer}: {ref.name_text}" if ref.name_text else ref.name_nummer
ET.SubElement(referenz_element, 'Referenz').text = ref_text
vorgabe_gueltigkeit = ET.SubElement(vorgabe_el, 'Gueltigkeit')
ET.SubElement(vorgabe_gueltigkeit, 'Von').text = vorgabe.gueltigkeit_von.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_von else ""
ET.SubElement(vorgabe_gueltigkeit, 'Bis').text = vorgabe.gueltigkeit_bis.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_bis else None
checklistenfragen_element = ET.SubElement(vorgabe_el, 'Checklistenfragen')
for cf in vorgabe.checklistenfragen.all():
ET.SubElement(checklistenfragen_element, 'Frage').text = cf.frage
stichworte_element = ET.SubElement(vorgabe_el, 'Stichworte')
for stw in vorgabe.stichworte.all():
ET.SubElement(stichworte_element, 'Stichwort').text = stw.stichwort
# Create a temporary root element to build the document
root = ET.Element('root')
build_dokument_xml_element(dokument, root)
# Get the actual document element (first child of root)
doc_element = root[0]
xml_str = ET.tostring(doc_element, encoding='unicode', method='xml')
xml_output = prettify_xml(xml_str)
xml_str = ET.tostring(root, encoding='unicode', method='xml')
xml_output = _prettify_xml(xml_str)
response = HttpResponse(xml_output, content_type='application/xml; charset=utf-8')
response['Content-Disposition'] = f'attachment; filename="{dokument.nummer}.xml"'
return response
def _prettify_xml(xml_string):
import xml.dom.minidom
dom = xml.dom.minidom.parseString(xml_string)
return dom.toprettyxml(indent=" ", encoding="UTF-8").decode('utf-8')
@login_required
def get_vorgabe_comments(request, vorgabe_id):
"""Get comments for a specific Vorgabe"""

View File

@@ -0,0 +1,52 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: vgui-cicd-django
namespace: vorgabenui
spec:
replicas: 1
selector:
matchLabels:
app: vgui-cicd-django
template:
metadata:
labels:
app: vgui-cicd-django
spec:
containers:
- name: django
image: your-django-image:latest
ports:
- containerPort: 8000
env:
# Django SECRET_KEY from Kubernetes secret
- name: VORGABENUI_SECRET
valueFrom:
secretKeyRef:
name: vorgabenui-secrets
key: vorgabenui_secret
# Other environment variables can be added here
- name: DEBUG
value: "False"
# Add database configuration, etc.
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: vgui-cicd-django-service
namespace: vorgabenui
spec:
selector:
app: vgui-cicd-django
ports:
- protocol: TCP
port: 80
targetPort: 8000
type: ClusterIP

9
k8s/django-secret.yaml Normal file
View File

@@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: vorgabenui-secrets
namespace: vorgabenui
type: Opaque
data:
# Base64 encoded SECRET_KEY - will be populated by deployment script
vorgabenui_secret: ""

16
pages/templates/400.html Normal file
View File

@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}Ungültige Anfrage{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="alert alert-warning">
<h2><i class="icon icon--alert"></i> Ungültige Anfrage (400)</h2>
<p>Ihre Anfrage konnte nicht verarbeitet werden.</p>
<p>Bitte überprüfen Sie die eingegebenen Daten und versuchen Sie es erneut.</p>
<p><a href="/" class="btn btn-primary">Zur Startseite</a></p>
</div>
</div>
</div>
{% endblock %}

21
pages/templates/403.html Normal file
View File

@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block title %}Zugriff verweigert{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="alert alert-warning">
<h2><i class="icon icon--alert"></i> Zugriff verweigert (403)</h2>
<p>Sie haben keine Berechtigung, auf diese Seite zuzugreifen.</p>
<p>Bitte melden Sie sich an oder wenden Sie sich an den Administrator.</p>
<p>
<a href="/" class="btn btn-primary">Zur Startseite</a>
{% if not user.is_authenticated %}
<a href="{% url 'login' %}" class="btn btn-secondary">Anmelden</a>
{% endif %}
</p>
</div>
</div>
</div>
{% endblock %}

21
pages/templates/404.html Normal file
View File

@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block title %}Seite nicht gefunden{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="alert alert-danger">
<h2><i class="icon icon--alert"></i> Seite nicht gefunden (404)</h2>
<p>Die gewünschte Seite konnte nicht gefunden werden.</p>
<p>Mögliche Gründe:</p>
<ul>
<li>Sie haben eine falsche URL eingegeben</li>
<li>Die Seite wurde verschoben oder gelöscht</li>
<li>Sie haben keine Berechtigung für diese Seite</li>
</ul>
<p><a href="/" class="btn btn-primary">Zur Startseite</a></p>
</div>
</div>
</div>
{% endblock %}

16
pages/templates/500.html Normal file
View File

@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}Serverfehler{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="alert alert-danger">
<h2><i class="icon icon--alert"></i> Serverfehler (500)</h2>
<p>Bei der Verarbeitung Ihrer Anfrage ist ein interner Fehler aufgetreten.</p>
<p>Der Administrator wurde über dieses Problem informiert.</p>
<p><a href="/" class="btn btn-primary">Zur Startseite</a></p>
</div>
</div>
</div>
{% endblock %}

View File

@@ -219,7 +219,7 @@
</p>
</div>
<div class="col-sm-6 text-right">
<p class="text-muted">Version {{ version|default:"0.973" }}</p>
<p class="text-muted">Version {{ version|default:"0.980" }}</p>
</div>
</div>
</div>

View File

@@ -69,3 +69,15 @@ def search(request):
return render(request,"results.html",{"suchbegriff":safe_search_term,"resultat":result})
def custom_400(request, exception):
return render(request, '400.html', status=400)
def custom_403(request, exception):
return render(request, '403.html', status=403)
def custom_404(request, exception):
return render(request, '404.html', status=404)
def custom_500(request):
return render(request, '500.html', status=500)

View File

@@ -30,7 +30,8 @@ pyxdg==0.28
requests==2.32.5
six==1.17.0
sqlparse==0.5.3
urllib3==2.6.0
urllib3==2.6.3
wcwidth==0.2.13
bleach==6.1.0
coverage==7.6.1
whitenoise==6.8.2

View File

@@ -0,0 +1,216 @@
#!/bin/bash
# deploy-argocd-configmap.sh
# Script to deploy Django ConfigMap to vorgabenui namespace for ArgoCD
set -euo pipefail
# ArgoCD-specific configuration (hardcoded for consistency)
NAMESPACE="vorgabenui"
CONFIGMAP_NAME="django-config"
SCRIPT_DIR="$(dirname "$0")"
ARGOCD_DIR="$SCRIPT_DIR/../argocd"
CONFIGMAP_FILE="$ARGOCD_DIR/configmap.yaml"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_step() {
echo -e "${BLUE}[STEP]${NC} $1"
}
# Function to check if kubectl is available
check_kubectl() {
if ! command -v kubectl &> /dev/null; then
log_error "kubectl is not installed or not in PATH"
exit 1
fi
}
# Function to check if configmap file exists
check_configmap_file() {
if [ ! -f "$CONFIGMAP_FILE" ]; then
log_error "ConfigMap file not found: $CONFIGMAP_FILE"
log_error "Expected ArgoCD ConfigMap file at: $CONFIGMAP_FILE"
exit 1
fi
}
# Function to deploy the configmap
deploy_configmap() {
log_step "Deploying ConfigMap '$CONFIGMAP_NAME' to namespace '$NAMESPACE'..."
kubectl apply -f "$CONFIGMAP_FILE"
if [ $? -eq 0 ]; then
log_info "Successfully deployed ConfigMap '$CONFIGMAP_NAME'"
return 0
else
log_error "Failed to deploy ConfigMap '$CONFIGMAP_NAME'"
return 1
fi
}
# Function to verify the configmap
verify_configmap() {
log_step "Verifying ConfigMap deployment..."
if kubectl get configmap "$CONFIGMAP_NAME" --namespace="$NAMESPACE" &> /dev/null; then
log_info "✅ ConfigMap '$CONFIGMAP_NAME' exists in namespace '$NAMESPACE'"
echo ""
log_info "ConfigMap details:"
kubectl describe configmap "$CONFIGMAP_NAME" --namespace="$NAMESPACE"
echo ""
log_info "ConfigMap data:"
kubectl get configmap "$CONFIGMAP_NAME" --namespace="$NAMESPACE" -o yaml | grep -A 20 "^data:"
return 0
else
log_error "❌ ConfigMap '$CONFIGMAP_NAME' not found in namespace '$NAMESPACE'"
return 1
fi
}
# Function to show usage
show_usage() {
echo "ArgoCD ConfigMap Deployment Script for VorgabenUI"
echo ""
echo "Usage: $0 [OPTIONS]"
echo ""
echo "This script deploys Django configuration to the vorgabenui namespace for ArgoCD."
echo ""
echo "Options:"
echo " -h, --help Show this help message"
echo " --verify-only Only verify existing ConfigMap, don't deploy"
echo " --dry-run Show what would be deployed without applying"
echo ""
echo "Configuration (hardcoded for ArgoCD):"
echo " Namespace: $NAMESPACE"
echo " ConfigMap Name: $CONFIGMAP_NAME"
echo " ConfigMap File: $CONFIGMAP_FILE"
echo ""
echo "Examples:"
echo " $0 # Deploy ConfigMap"
echo " $0 --verify-only # Verify existing ConfigMap"
echo " $0 --dry-run # Preview deployment"
echo ""
echo "Note: Run this before deploying the ArgoCD deployment to ensure configuration is available."
}
# Parse command line arguments
VERIFY_ONLY=false
DRY_RUN=false
while [[ $# -gt 0 ]]; do
case $1 in
--verify-only)
VERIFY_ONLY=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
-h|--help)
show_usage
exit 0
;;
*)
log_error "Unknown option: $1"
show_usage
exit 1
;;
esac
done
# Main execution
main() {
echo ""
log_info "🚀 ArgoCD Django ConfigMap Deployment Script"
log_info "============================================"
echo ""
log_info "Target Configuration:"
log_info " Namespace: $NAMESPACE"
log_info " ConfigMap Name: $CONFIGMAP_NAME"
log_info " ConfigMap File: $CONFIGMAP_FILE"
echo ""
# Perform checks
log_step "Performing pre-flight checks..."
check_kubectl
check_configmap_file
log_info "✅ All pre-flight checks passed"
echo ""
# Verify-only mode
if [ "$VERIFY_ONLY" = true ]; then
log_info "🔍 Verify-only mode - checking existing ConfigMap"
verify_configmap
exit $?
fi
# Dry-run mode
if [ "$DRY_RUN" = true ]; then
log_info "🔍 Dry-run mode - showing what would be deployed:"
echo ""
log_info "ConfigMap content that would be deployed:"
cat "$CONFIGMAP_FILE"
echo ""
log_info "Would run: kubectl apply -f $CONFIGMAP_FILE"
echo ""
log_info "Run without --dry-run to execute the deployment"
exit 0
fi
# Create namespace if it doesn't exist
if ! kubectl get namespace "$NAMESPACE" &> /dev/null; then
log_warn "Namespace '$NAMESPACE' does not exist, creating..."
kubectl create namespace "$NAMESPACE"
log_info "✅ Created namespace '$NAMESPACE'"
fi
# Deploy the ConfigMap
if deploy_configmap; then
echo ""
# Verify deployment
verify_configmap
echo ""
log_info "🎉 ConfigMap deployment completed successfully!"
echo ""
log_info "📋 Next steps:"
log_info "1. Deploy the secret (if not already done):"
echo " ./scripts/deploy-argocd-secret.sh"
echo ""
log_info "2. Apply the updated deployment:"
echo " kubectl apply -f argocd/deployment.yaml"
echo ""
log_info "3. Verify Django pods start with proper configuration"
echo ""
else
log_error "ConfigMap deployment failed"
exit 1
fi
}
# Run main function
main

311
scripts/deploy-argocd-secret.sh Executable file
View File

@@ -0,0 +1,311 @@
#!/bin/bash
# deploy-argocd-secret.sh
# ArgoCD-specific script to generate and deploy Django SECRET_KEY to vorgabenui namespace
set -euo pipefail
# ArgoCD-specific configuration (hardcoded for consistency)
NAMESPACE="vorgabenui"
SECRET_NAME="vorgabenui-secrets"
SECRET_KEY_NAME="vorgabenui_secret"
SCRIPT_DIR="$(dirname "$0")"
ARGOCD_DIR="$SCRIPT_DIR/../argocd"
TEMPLATES_DIR="$SCRIPT_DIR/../templates"
SECRET_TEMPLATE="$TEMPLATES_DIR/secret.yaml"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_step() {
echo -e "${BLUE}[STEP]${NC} $1"
}
# Function to generate a secure Django SECRET_KEY
generate_secret_key() {
# Generate a 50-character secret key using Python (same as Django's default)
python3 -c "
import secrets
import string
# Django-style secret key generation
chars = string.ascii_letters + string.digits + '!@#$%^&*(-_=+)'
print(''.join(secrets.choice(chars) for _ in range(50)))
"
}
# Function to check if kubectl is available
check_kubectl() {
if ! command -v kubectl &> /dev/null; then
log_error "kubectl is not installed or not in PATH"
exit 1
fi
}
# Function to check if Python3 is available
check_python() {
if ! command -v python3 &> /dev/null; then
log_error "python3 is not installed or not in PATH"
exit 1
fi
}
# Function to check if secret template exists
check_template() {
if [ ! -f "$SECRET_TEMPLATE" ]; then
# Fallback to argocd directory if templates directory doesn't exist
FALLBACK_TEMPLATE="$ARGOCD_DIR/secret.yaml"
if [ -f "$FALLBACK_TEMPLATE" ]; then
SECRET_TEMPLATE="$FALLBACK_TEMPLATE"
log_warn "Using fallback template: $SECRET_TEMPLATE"
else
log_error "Secret template not found at either:"
log_error " Primary: $TEMPLATES_DIR/secret.yaml"
log_error " Fallback: $FALLBACK_TEMPLATE"
exit 1
fi
fi
}
# Function to create the secret
create_secret() {
local secret_key="$1"
log_step "Creating Kubernetes secret '$SECRET_NAME' in namespace '$NAMESPACE'..."
# Create the secret directly with kubectl (this will create or update)
kubectl create secret generic "$SECRET_NAME" \
--from-literal="$SECRET_KEY_NAME=$secret_key" \
--namespace="$NAMESPACE" \
--dry-run=client -o yaml | kubectl apply -f -
if [ $? -eq 0 ]; then
log_info "Successfully created/updated secret '$SECRET_NAME'"
return 0
else
log_error "Failed to create/update secret '$SECRET_NAME'"
return 1
fi
}
# Function to verify the secret
verify_secret() {
log_step "Verifying secret deployment..."
if kubectl get secret "$SECRET_NAME" --namespace="$NAMESPACE" &> /dev/null; then
log_info "✅ Secret '$SECRET_NAME' exists in namespace '$NAMESPACE'"
# Show secret metadata (without revealing the actual key)
echo ""
log_info "Secret details:"
kubectl describe secret "$SECRET_NAME" --namespace="$NAMESPACE" | grep -E "^(Name|Namespace|Type|Data)"
# Verify the key exists in the secret
if kubectl get secret "$SECRET_NAME" --namespace="$NAMESPACE" -o jsonpath="{.data.$SECRET_KEY_NAME}" &> /dev/null; then
log_info "✅ Secret key '$SECRET_KEY_NAME' is present in the secret"
return 0
else
log_error "❌ Secret key '$SECRET_KEY_NAME' not found in secret"
return 1
fi
else
log_error "❌ Secret '$SECRET_NAME' not found in namespace '$NAMESPACE'"
return 1
fi
}
# Function to test secret in pod (if deployment exists)
test_secret_in_pod() {
log_step "Testing secret accessibility in Django deployment..."
# Check if Django deployment exists
if kubectl get deployment django --namespace="$NAMESPACE" &> /dev/null; then
log_info "Django deployment found, testing secret access..."
# Try to get the secret value from a pod (this will fail if env var not configured)
local pod_name
pod_name=$(kubectl get pods -l app=django --namespace="$NAMESPACE" -o jsonpath="{.items[0].metadata.name}" 2>/dev/null)
if [ -n "$pod_name" ] && [ "$pod_name" != "" ]; then
log_info "Testing secret in pod: $pod_name"
if kubectl exec "$pod_name" --namespace="$NAMESPACE" -- printenv VORGABENUI_SECRET &> /dev/null; then
log_info "✅ VORGABENUI_SECRET environment variable is accessible in pod"
else
log_warn "⚠️ VORGABENUI_SECRET environment variable not found in pod"
log_warn " This is expected if the deployment hasn't been updated yet"
fi
else
log_warn "⚠️ No running Django pods found"
fi
else
log_info "Django deployment not found - secret will be available when deployment is updated"
fi
}
# Function to show usage
show_usage() {
echo "ArgoCD Secret Deployment Script for VorgabenUI"
echo ""
echo "Usage: $0 [OPTIONS]"
echo ""
echo "This script deploys Django SECRET_KEY to the vorgabenui namespace for ArgoCD."
echo ""
echo "Options:"
echo " -h, --help Show this help message"
echo " --verify-only Only verify existing secret, don't create new one"
echo " --dry-run Show what would be done without making changes"
echo ""
echo "Configuration (hardcoded for ArgoCD):"
echo " Namespace: $NAMESPACE"
echo " Secret Name: $SECRET_NAME"
echo " Secret Key: $SECRET_KEY_NAME"
echo " Template: $SECRET_TEMPLATE"
echo ""
echo "Examples:"
echo " $0 # Generate and deploy new secret"
echo " $0 --verify-only # Verify existing secret"
echo " $0 --dry-run # Preview changes"
echo ""
echo "After running this script, update argocd/deployment.yaml to reference the secret."
}
# Parse command line arguments
VERIFY_ONLY=false
DRY_RUN=false
while [[ $# -gt 0 ]]; do
case $1 in
--verify-only)
VERIFY_ONLY=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
-h|--help)
show_usage
exit 0
;;
*)
log_error "Unknown option: $1"
show_usage
exit 1
;;
esac
done
# Main execution
main() {
echo ""
log_info "🚀 ArgoCD Django SECRET_KEY Deployment Script"
log_info "============================================="
echo ""
log_info "Target Configuration:"
log_info " Namespace: $NAMESPACE"
log_info " Secret Name: $SECRET_NAME"
log_info " Secret Key Name: $SECRET_KEY_NAME"
echo ""
# Perform checks
log_step "Performing pre-flight checks..."
check_kubectl
check_python
check_template
log_info "✅ All pre-flight checks passed"
echo ""
# Verify-only mode
if [ "$VERIFY_ONLY" = true ]; then
log_info "🔍 Verify-only mode - checking existing secret"
verify_secret
test_secret_in_pod
exit $?
fi
# Generate new secret key
log_step "Generating new Django SECRET_KEY..."
SECRET_KEY=$(generate_secret_key)
if [ -z "$SECRET_KEY" ]; then
log_error "Failed to generate secret key"
exit 1
fi
log_info "✅ Generated secret key (first 10 chars): ${SECRET_KEY:0:10}..."
echo ""
# Dry-run mode
if [ "$DRY_RUN" = true ]; then
log_info "🔍 Dry-run mode - showing what would be done:"
echo ""
log_info "Would create secret with the following command:"
echo " kubectl create secret generic $SECRET_NAME \\"
echo " --from-literal=$SECRET_KEY_NAME='[GENERATED_KEY]' \\"
echo " --namespace=$NAMESPACE \\"
echo " --dry-run=client -o yaml | kubectl apply -f -"
echo ""
log_info "Secret key would be: ${SECRET_KEY:0:10}...${SECRET_KEY: -5}"
echo ""
log_info "Run without --dry-run to execute the deployment"
exit 0
fi
# Create namespace if it doesn't exist
if ! kubectl get namespace "$NAMESPACE" &> /dev/null; then
log_warn "Namespace '$NAMESPACE' does not exist, creating..."
kubectl create namespace "$NAMESPACE"
log_info "✅ Created namespace '$NAMESPACE'"
fi
# Create the secret
if create_secret "$SECRET_KEY"; then
echo ""
# Verify deployment
verify_secret
echo ""
test_secret_in_pod
echo ""
log_info "🎉 Secret deployment completed successfully!"
echo ""
log_info "📋 Next steps:"
log_info "1. Update argocd/deployment.yaml to include environment variable:"
echo ""
echo " env:"
echo " - name: VORGABENUI_SECRET"
echo " valueFrom:"
echo " secretKeyRef:"
echo " name: $SECRET_NAME"
echo " key: $SECRET_KEY_NAME"
echo ""
log_info "2. Apply the updated deployment:"
echo " kubectl apply -f argocd/deployment.yaml"
echo ""
log_info "3. Verify Django pods restart and pick up the new secret"
echo ""
else
log_error "Secret deployment failed"
exit 1
fi
}
# Run main function
main

201
scripts/deploy-django-secret.sh Executable file
View File

@@ -0,0 +1,201 @@
#!/bin/bash
# deploy-django-secret.sh
# Script to generate a secure Django SECRET_KEY and deploy it to Kubernetes
set -euo pipefail
# Configuration
NAMESPACE="${NAMESPACE:-vorgabenui}"
SECRET_NAME="vorgabenui-secrets"
SECRET_KEY_NAME="vorgabenui_secret"
K8S_DIR="$(dirname "$0")/../k8s"
SECRET_YAML="$K8S_DIR/django-secret.yaml"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Function to generate a secure Django SECRET_KEY
generate_secret_key() {
# Generate a 50-character secret key using Python (same as Django's default)
python3 -c "
import secrets
import string
# Django-style secret key generation
chars = string.ascii_letters + string.digits + '!@#$%^&*(-_=+)'
print(''.join(secrets.choice(chars) for _ in range(50)))
"
}
# Function to check if kubectl is available
check_kubectl() {
if ! command -v kubectl &> /dev/null; then
log_error "kubectl is not installed or not in PATH"
exit 1
fi
}
# Function to check if Python3 is available
check_python() {
if ! command -v python3 &> /dev/null; then
log_error "python3 is not installed or not in PATH"
exit 1
fi
}
# Function to create the secret
create_secret() {
local secret_key="$1"
local encoded_key
# Base64 encode the secret key
encoded_key=$(echo -n "$secret_key" | base64 -w 0)
log_info "Creating Kubernetes secret '$SECRET_NAME' in namespace '$NAMESPACE'..."
# Create the secret directly with kubectl
kubectl create secret generic "$SECRET_NAME" \
--from-literal="$SECRET_KEY_NAME=$secret_key" \
--namespace="$NAMESPACE" \
--dry-run=client -o yaml | kubectl apply -f -
if [ $? -eq 0 ]; then
log_info "Successfully created/updated secret '$SECRET_NAME'"
else
log_error "Failed to create/update secret '$SECRET_NAME'"
exit 1
fi
}
# Function to verify the secret
verify_secret() {
log_info "Verifying secret deployment..."
if kubectl get secret "$SECRET_NAME" --namespace="$NAMESPACE" &> /dev/null; then
log_info "Secret '$SECRET_NAME' exists in namespace '$NAMESPACE'"
# Show secret (without revealing the actual key)
kubectl describe secret "$SECRET_NAME" --namespace="$NAMESPACE"
return 0
else
log_error "Secret '$SECRET_NAME' not found in namespace '$NAMESPACE'"
return 1
fi
}
# Function to show usage
show_usage() {
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " -n, --namespace NAMESPACE Kubernetes namespace (default: vorgabenui)"
echo " -s, --secret-name NAME Secret name (default: django-secrets)"
echo " -k, --key-name NAME Secret key name (default: django-secret-key)"
echo " -h, --help Show this help message"
echo ""
echo "Environment variables:"
echo " NAMESPACE Override default namespace"
echo ""
echo "Examples:"
echo " $0 # Deploy to vorgabenui namespace"
echo " $0 -n production # Deploy to production namespace"
echo " NAMESPACE=staging $0 # Deploy to staging namespace"
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-n|--namespace)
NAMESPACE="$2"
shift 2
;;
-s|--secret-name)
SECRET_NAME="$2"
shift 2
;;
-k|--key-name)
SECRET_KEY_NAME="$2"
shift 2
;;
-h|--help)
show_usage
exit 0
;;
*)
log_error "Unknown option: $1"
show_usage
exit 1
;;
esac
done
# Main execution
main() {
log_info "Django SECRET_KEY Deployment Script"
log_info "==================================="
log_info "Namespace: $NAMESPACE"
log_info "Secret Name: $SECRET_NAME"
log_info "Secret Key Name: $SECRET_KEY_NAME"
echo ""
# Perform checks
check_kubectl
check_python
# Generate new secret key
log_info "Generating new Django SECRET_KEY..."
SECRET_KEY=$(generate_secret_key)
if [ -z "$SECRET_KEY" ]; then
log_error "Failed to generate secret key"
exit 1
fi
log_info "Generated secret key (first 10 chars): ${SECRET_KEY:0:10}..."
# Create namespace if it doesn't exist
if ! kubectl get namespace "$NAMESPACE" &> /dev/null; then
log_warn "Namespace '$NAMESPACE' does not exist, creating..."
kubectl create namespace "$NAMESPACE"
fi
# Create the secret
create_secret "$SECRET_KEY"
# Verify deployment
verify_secret
echo ""
log_info "Deployment completed successfully!"
log_info "To use this secret in your Django deployment, add the following to your pod spec:"
echo ""
echo " env:"
echo " - name: VORGABENUI_SECRET"
echo " valueFrom:"
echo " secretKeyRef:"
echo " name: $SECRET_NAME"
echo " key: $SECRET_KEY_NAME"
echo ""
log_warn "The old secret key in settings.py has been replaced with environment variable lookup."
log_warn "Make sure your Django deployment uses the environment variable before deploying."
}
# Run main function
main

28
templates/configmap.yaml Normal file
View File

@@ -0,0 +1,28 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: django-config
namespace: vorgabenui
data:
# Django Configuration
DEBUG: "false"
DJANGO_ALLOWED_HOSTS: "vorgabenportal.knowyoursecurity.com,localhost,127.0.0.1"
DJANGO_SETTINGS_MODULE: "VorgabenUI.settings"
# Application Configuration
LANGUAGE_CODE: "de-ch"
TIME_ZONE: "UTC"
# Static and Media Configuration
STATIC_URL: "/static/"
MEDIA_URL: "/media/"
# Database Configuration (for future use)
# DATABASE_ENGINE: "django.db.backends.sqlite3"
# DATABASE_NAME: "/app/data/db.sqlite3"
# Security Configuration
# CSRF_TRUSTED_ORIGINS: "https://vorgabenportal.knowyoursecurity.com"
# Performance Configuration
# DATA_UPLOAD_MAX_NUMBER_FIELDS: "10250"

10
templates/secret.yaml Normal file
View File

@@ -0,0 +1,10 @@
apiVersion: v1
kind: Secret
metadata:
name: vorgabenui-secrets
namespace: vorgabenui
type: Opaque
data:
# Base64 encoded SECRET_KEY - populated by deployment script
# This is a TEMPLATE FILE in templates/ directory - not deployed by ArgoCD
vorgabenui_secret: ""