Compare commits

..

3 Commits

Author SHA1 Message Date
Platform Engineer
6f1826761e fix(argocd): preserve roger admin access + set default readonly in RBAC ConfigMap 2026-07-06 08:28:25 +00:00
Platform Engineer
eacfb48cbf fix(myorg-assistant): fix CronJob image references to use Gitea registry image
CronJobs were referencing 'myorg-assistant:latest' (a local image that
doesn't exist in the cluster), causing ImagePullBackOff on all 4 active
CronJob pods. Updated all 5 CronJob manifests to use the same image as
the Deployment (git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf)
and added imagePullSecrets: gitea-registry for authentication.
2026-07-06 08:28:25 +00:00
Platform Engineer
792380b631 fix(argocd): add RBAC ConfigMap granting platform-engineer account read+sync access
The platform-engineer ArgoCD account was created (argocd-cm.yaml) with
apiKey capability, but no argocd-rbac-cm ConfigMap existed, so the account
had default (empty) RBAC and every API call returned 403.

This adds the RBAC policy granting the platform-engineer role:
- get applications (all projects/namespaces)
- sync applications (all projects/namespaces)
- get projects

Needed for the platform-engineer cron jobs to read app health and trigger
syncs via the ArgoCD API.
2026-07-06 08:28:25 +00:00
15 changed files with 27 additions and 413 deletions

246
AGENTS.md
View File

@@ -1,246 +0,0 @@
# AGENTS.md - Guide for Coding Agents
This file provides essential information for AI coding agents working with this Kubernetes cluster project.
## Project Overview
This repository contains Kubernetes manifests for a K3s cluster running self-hosted services on the `rogi.casa` domain. The cluster is managed via **GitOps using ArgoCD** - all changes to the cluster are deployed automatically from this Git repository.
**⚠️ CRITICAL: Permission Model**
You **DO NOT** have permission to push changes to this repository. Before applying any changes to the cluster:
1. Make the necessary code changes to the manifests
2. Clearly present the changes to the user
3. Ask the user to review and push the changes
4. Wait for confirmation that changes have been pushed
5. Only then will ArgoCD automatically deploy the changes to the cluster
## Architecture & GitOps Workflow
### ArgoCD App-of-Apps Pattern
This project uses ArgoCD's "app-of-apps" pattern:
```
argocd-bootstrap.yaml (root Application)
argocd/apps/ (directory containing all Application manifests)
Individual Applications (one per service directory)
Kubernetes manifests in each service directory (e.g., pihole/, homeassistant/)
```
### Deployment Flow
1. You make changes to Kubernetes manifests in the repository
2. User reviews and pushes changes to the `main` branch
3. ArgoCD detects changes (automatically or on sync)
4. ArgoCD applies changes to the cluster with `prune: true` and `selfHeal: true`
5. Cluster state converges to match the Git state
### Key Files
- **`argocd-bootstrap.yaml`**: The root Application that bootstraps ArgoCD. Points to `argocd/apps/` directory. This is the only file that needs manual `kubectl apply` during initial setup.
- **`argocd/apps/project.yaml`**: ArgoCD AppProject defining permissions for all applications
- **`argocd/apps/*.yaml`**: Individual ArgoCD Application manifests (one per service)
- **`argocd/gen-apps.sh`**: Script to regenerate all ArgoCD manifests from the `APPS` array
## Repository Structure
```
k3s-cluster/
├── argocd-bootstrap.yaml # Root ArgoCD Application (app-of-apps)
├── argocd/
│ ├── apps/ # Individual ArgoCD Application manifests
│ │ ├── project.yaml # AppProject definition
│ │ ├── pihole.yaml # Application for pihole/
│ │ ├── homeassistant.yaml # Application for homeassistant/
│ │ └── ... # One per service
│ ├── gen-apps.sh # Generates argocd/apps/* manifests
│ └── ingress.yaml # ArgoCD's own ingress
├── <service-name>/ # Each service has its own directory
│ ├── namespace.yaml # (Optional) Namespace definition
│ ├── deployment.yaml # Main deployment/statefulset
│ ├── service.yaml # Service definition
│ ├── ingress.yaml # Ingress configuration
│ ├── configmap.yaml # (Optional) ConfigMaps
│ ├── pvc.yaml # (Optional) PersistentVolumeClaims
│ └── secret.yaml # (Optional) Secrets (rarely committed)
├── cert-manager/ # cert-manager installation manifests
├── nas/ # External NAS service configuration
├── monitoring/ # Prometheus + Grafana stack
└── README.md # Comprehensive project documentation
```
## Current Services
The cluster runs these services (each in its own directory):
- **argocd** - GitOps continuous delivery platform
- **cert-manager** - SSL certificate management (Let's Encrypt)
- **fava** - Beancount accounting web interface
- **gitea** - Self-hosted Git server
- **glance** - Personal dashboard
- **gym-tracker** - Workout tracking application
- **homeassistant** - Home automation
- **jellyfin** - Media server
- **litellm** - LLM proxy
- **minecraft-server** - Minecraft server
- **monitoring** - Prometheus + Grafana
- **myorg-assistant** - Organization assistant
- **n8n** - Workflow automation
- **nas** - External NAS proxy
- **openwebui** - Web UI for LLMs
- **phoenix** - AI observability platform
- **pihole** - Network-wide ad blocking
- **platform-engineer** - Platform engineering tools
- **qbittorrent** - Torrent client
- **searxng** - Meta search engine
- **vaultwarden** - Password manager (Bitwarden compatible)
## How to Make Changes
### Adding a New Service
1. Create a new directory: `mkdir new-service`
2. Create Kubernetes manifests in `new-service/`:
- `namespace.yaml` (if dedicated namespace needed)
- `deployment.yaml` or `statefulset.yaml`
- `service.yaml`
- `ingress.yaml`
- Any ConfigMaps, Secrets, PVCs needed
3. Add the service to `argocd/gen-apps.sh`:
- Add a line to the `APPS` array: `"new-service|namespace|new-service|true|true"`
- Format: `name|namespace|path|recurse|validate`
4. Run `./argocd/gen-apps.sh` to regenerate ArgoCD manifests
5. **Present changes to user for review and push**
### Modifying an Existing Service
1. Edit the relevant manifest(s) in the service directory
2. If changing ArgoCD configuration, also update `argocd/gen-apps.sh` and regenerate
3. **Present changes to user for review and push**
### Removing a Service
1. Remove the service directory: `rm -rf service-name/`
2. Remove from `APPS` array in `argocd/gen-apps.sh`
3. Run `./argocd/gen-apps.sh` to regenerate
4. **Present changes to user for review and push**
5. ArgoCD will automatically prune the resources from the cluster
## Common Patterns
### Ingress Configuration
Each service has its own `ingress.yaml` with:
- `ingressClassName: traefik` (K3s default)
- TLS configured with `cert-manager.io/cluster-issuer: letsencrypt-prod`
- Host-based routing (e.g., `pihole.rogi.casa`)
Example:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: pihole
namespace: pihole
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: traefik
tls:
- hosts:
- pihole.rogi.casa
secretName: pihole-tls
rules:
- host: pihole.rogi.casa
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: pihole-web
port:
number: 80
```
### Resource Management
- Each service typically has its own namespace
- Use ResourceRequests and Limits for all containers
- PVCs for persistent data
- ConfigMaps for configuration files
## Important Notes
### What You CAN Do
- Read and understand all manifests
- Create new manifest files
- Modify existing manifest files
- Run `./argocd/gen-apps.sh` to regenerate ArgoCD manifests
- Explain how the cluster works
- Troubleshoot issues by reading manifests
### What You CANNOT Do
- Push changes to the Git repository (no push permissions)
- Directly apply manifests with `kubectl apply` (unless explicitly asked)
- Access the Kubernetes cluster directly (unless explicitly configured)
- Create secrets that should remain private (those are managed manually)
### Secrets Management
Secrets are generally **not committed to the repository**. They must be created manually in the cluster:
```bash
kubectl create secret docker-registry gitea-registry \
--docker-server=gitea.rogi.casa \
--docker-username=<user> \
--docker-password=<token> \
-n <namespace>
```
## Workflow Summary
When asked to make changes:
1. **Understand** the current state by reading relevant files
2. **Modify** the manifests (create/edit files)
3. **Regenerate** ArgoCD manifests if needed (`./argocd/gen-apps.sh`)
4. **Present** the changes clearly to the user:
```
I've made the following changes:
- Modified pihole/deployment.yaml to update image version
- Regenerated argocd/apps/pihole.yaml
Please review and push these changes to deploy them.
```
5. **Wait** for user confirmation that changes are pushed
6. **Verify** (if possible) that ArgoCD has synced the changes
## Useful Commands (for reference)
```bash
# Regenerate ArgoCD manifests after modifying gen-apps.sh
./argocd/gen-apps.sh
# Check ArgoCD applications status (requires kubectl access)
kubectl get applications -n argocd
# View logs of a pod (requires kubectl access)
kubectl logs -n <namespace> <pod-name>
# Check ingress status (requires kubectl access)
kubectl get ingress -n <namespace>
```
## Questions?
If you're unsure about anything:
1. Read the comprehensive `README.md` in the repository root
2. Check existing service directories for examples
3. Ask the user for clarification before making changes
4. Remember: **never push without explicit user review and approval**

View File

@@ -22,9 +22,3 @@ spec:
selfHeal: true selfHeal: true
syncOptions: syncOptions:
- CreateNamespace=false - CreateNamespace=false
ignoreDifferences:
- group: argoproj.io
kind: Application
jsonPointers:
- /status
- /operation

View File

@@ -7,11 +7,6 @@ metadata:
app.kubernetes.io/name: argocd-cm app.kubernetes.io/name: argocd-cm
app.kubernetes.io/part-of: argocd app.kubernetes.io/part-of: argocd
data: data:
# Serve HTTP (no redirect to HTTPS) so the TLS-terminating Traefik ingress works.
# Without this, argocd-server redirects HTTP->HTTPS, causing an infinite
# redirect loop behind the ingress (argocd.rogi.casa unreachable).
server.insecure: "true"
# add an additional local user with apiKey and login capabilities # add an additional local user with apiKey and login capabilities
# apiKey - allows generating API keys # apiKey - allows generating API keys
# login - allows to login using UI # login - allows to login using UI

View File

@@ -7,23 +7,10 @@ metadata:
app.kubernetes.io/name: argocd-rbac-cm app.kubernetes.io/name: argocd-rbac-cm
app.kubernetes.io/part-of: argocd app.kubernetes.io/part-of: argocd
data: data:
policy.csv: |
# Grant platform-engineer read-only access to applications
g, platform-engineer, role:readonly
# Custom policy for platform-engineer with application read permissions
p, role:platform-engineer, applications, get, *, allow
p, role:platform-engineer, applications, list, *, allow
p, role:platform-engineer, clusters, get, *, allow
p, role:platform-engineer, clusters, list, *, allow
p, role:platform-engineer, repositories, get, *, allow
p, role:platform-engineer, repositories, list, *, allow
p, role:platform-engineer, projects, get, *, allow
p, role:platform-engineer, projects, list, *, allow
g, platform-engineer, role:platform-engineer
# Default policy - deny by default (ArgoCD default)
policy.default: role:readonly policy.default: role:readonly
policy.csv: |
# Enable RBAC p, role:platform-engineer, applications, get, */*, allow
rbac.enabled: "true" p, role:platform-engineer, applications, sync, */*, allow
p, role:platform-engineer, projects, get, *, allow
g, platform-engineer, role:platform-engineer
g, roger, role:admin

View File

@@ -58,9 +58,9 @@ spec:
image: ghcr.io/home-assistant/home-assistant:stable image: ghcr.io/home-assistant/home-assistant:stable
resources: resources:
requests: requests:
memory: "512Mi" memory: "256Mi"
limits: limits:
memory: "1Gi" memory: "512Mi"
ports: ports:
- containerPort: 8123 - containerPort: 8123
volumeMounts: volumeMounts:

View File

@@ -11,30 +11,18 @@ metadata:
data: data:
config.yaml: | config.yaml: |
model_list: model_list:
- model_name: gpt-5.6-luna - model_name: gpt-5-mini
litellm_params: litellm_params:
model: openai/gpt-5.6-luna model: openai/gpt-5-mini-2025-08-07
api_key: "os.environ/OPENAI_API_KEY" api_key: "os.environ/OPENAI_API_KEY"
- model_name: claude-haiku-4.5 - model_name: claude-4.5-haiku
litellm_params: litellm_params:
model: "anthropic/claude-haiku-4-5-20251001" model: "anthropic/claude-haiku-4-5-20251001"
api_key: "os.environ/ANTHROPIC_API_KEY" api_key: "os.environ/ANTHROPIC_API_KEY"
- model_name: claude-sonnet-5
litellm_params:
model: "anthropic/claude-sonnet-5"
api_key: "os.environ/ANTHROPIC_API_KEY"
- model_name: gemini-3-flash - model_name: gemini-3-flash
litellm_params: litellm_params:
model: gemini/gemini-3-flash-preview model: gemini/gemini-3-flash-preview
api_key: "os.environ/GEMINI_API_KEY" api_key: "os.environ/GEMINI_API_KEY"
- model_name: tencent/hy3:free
litellm_params:
model: openrouter/tencent/hy3:free
api_key: "os.environ/OPENROUTER_API_KEY"
- model_name: z-ai/glm-5.2
litellm_params:
model: openrouter/z-ai/glm-5.2
api_key: "os.environ/OPENROUTER_API_KEY"
- model_name: glm-4.7-flash - model_name: glm-4.7-flash
litellm_params: litellm_params:
model: ollama/glm-4.7-flash model: ollama/glm-4.7-flash

View File

@@ -27,11 +27,10 @@ spec:
containers: containers:
- name: deadline-checker - name: deadline-checker
image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf
imagePullPolicy: Always imagePullPolicy: IfNotPresent
command: command:
- python - python
- -c - run_job.py
- "from src.scheduler.jobs import run_job; import sys; run_job(sys.argv[1])"
- deadline-checker - deadline-checker
env: env:
- name: MYORG_REPO_PATH - name: MYORG_REPO_PATH
@@ -54,16 +53,6 @@ spec:
secretKeyRef: secretKeyRef:
name: myorg-assistant-secret name: myorg-assistant-secret
key: LITELLM_API_KEY key: LITELLM_API_KEY
- name: WEB_SECRET_KEY
valueFrom:
secretKeyRef:
name: myorg-assistant-secret
key: WEB_SECRET_KEY
- name: GIT_TOKEN
valueFrom:
secretKeyRef:
name: myorg-assistant-secret
key: GIT_TOKEN
volumeMounts: volumeMounts:
- name: myorg-data - name: myorg-data
mountPath: /data/myorg mountPath: /data/myorg

View File

@@ -27,11 +27,10 @@ spec:
containers: containers:
- name: evening-summary - name: evening-summary
image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf
imagePullPolicy: Always imagePullPolicy: IfNotPresent
command: command:
- python - python
- -c - run_job.py
- "from src.scheduler.jobs import run_job; import sys; run_job(sys.argv[1])"
- evening-summary - evening-summary
env: env:
- name: MYORG_REPO_PATH - name: MYORG_REPO_PATH
@@ -54,16 +53,6 @@ spec:
secretKeyRef: secretKeyRef:
name: myorg-assistant-secret name: myorg-assistant-secret
key: LITELLM_API_KEY key: LITELLM_API_KEY
- name: WEB_SECRET_KEY
valueFrom:
secretKeyRef:
name: myorg-assistant-secret
key: WEB_SECRET_KEY
- name: GIT_TOKEN
valueFrom:
secretKeyRef:
name: myorg-assistant-secret
key: GIT_TOKEN
volumeMounts: volumeMounts:
- name: myorg-data - name: myorg-data
mountPath: /data/myorg mountPath: /data/myorg

View File

@@ -24,55 +24,13 @@ spec:
restartPolicy: OnFailure restartPolicy: OnFailure
imagePullSecrets: imagePullSecrets:
- name: gitea-registry - name: gitea-registry
initContainers:
- name: git-clone
image: alpine/git:latest
command:
- sh
- -c
- |
# Normalize the repo URL host to the correct Gitea subdomain.
# The GIT_REPO_URL secret historically contained gitea.rogi.casa,
# which 526s through Cloudflare; the working subdomain is git.rogi.casa.
export GIT_REPO_URL="$(echo "${GIT_REPO_URL}" | sed -E 's#https?://[^/@]+@?gitea\.rogi\.casa#https://'"${GIT_USERNAME}"':'"${GIT_TOKEN}"'@git.rogi.casa#')"
if [ ! -d /data/myorg/.git ]; then
echo "Cloning repository from ${GIT_REPO_URL}..."
git clone ${GIT_REPO_URL} /data/myorg
cd /data/myorg
git config user.name "${GIT_USERNAME}"
git config user.email "${GIT_USERNAME}@rogi.casa"
git config credential.helper store
echo "https://${GIT_USERNAME}:${GIT_TOKEN}@git.rogi.casa" > ~/.git-credentials
else
echo "Repository already exists, skipping clone."
fi
env:
- name: GIT_REPO_URL
valueFrom:
secretKeyRef:
name: myorg-assistant-secret
key: GIT_REPO_URL
- name: GIT_USERNAME
valueFrom:
secretKeyRef:
name: myorg-assistant-secret
key: GIT_USERNAME
- name: GIT_TOKEN
valueFrom:
secretKeyRef:
name: myorg-assistant-secret
key: GIT_TOKEN
volumeMounts:
- name: myorg-data
mountPath: /data/myorg
containers: containers:
- name: git-sync - name: git-sync
image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf
imagePullPolicy: Always imagePullPolicy: IfNotPresent
command: command:
- python - python
- -c - run_job.py
- "from src.scheduler.jobs import run_job; import sys; run_job(sys.argv[1])"
- git-sync - git-sync
env: env:
- name: MYORG_REPO_PATH - name: MYORG_REPO_PATH
@@ -110,11 +68,6 @@ spec:
secretKeyRef: secretKeyRef:
name: myorg-assistant-secret name: myorg-assistant-secret
key: LITELLM_API_KEY key: LITELLM_API_KEY
- name: WEB_SECRET_KEY
valueFrom:
secretKeyRef:
name: myorg-assistant-secret
key: WEB_SECRET_KEY
volumeMounts: volumeMounts:
- name: myorg-data - name: myorg-data
mountPath: /data/myorg mountPath: /data/myorg

View File

@@ -27,11 +27,10 @@ spec:
containers: containers:
- name: morning-briefing - name: morning-briefing
image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf
imagePullPolicy: Always imagePullPolicy: IfNotPresent
command: command:
- python - python
- -c - run_job.py
- "from src.scheduler.jobs import run_job; import sys; run_job(sys.argv[1])"
- morning-briefing - morning-briefing
env: env:
# From ConfigMap # From ConfigMap
@@ -61,16 +60,6 @@ spec:
secretKeyRef: secretKeyRef:
name: myorg-assistant-secret name: myorg-assistant-secret
key: LITELLM_API_KEY key: LITELLM_API_KEY
- name: WEB_SECRET_KEY
valueFrom:
secretKeyRef:
name: myorg-assistant-secret
key: WEB_SECRET_KEY
- name: GIT_TOKEN
valueFrom:
secretKeyRef:
name: myorg-assistant-secret
key: GIT_TOKEN
volumeMounts: volumeMounts:
- name: myorg-data - name: myorg-data
mountPath: /data/myorg mountPath: /data/myorg

View File

@@ -27,11 +27,10 @@ spec:
containers: containers:
- name: waiting-followup - name: waiting-followup
image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf
imagePullPolicy: Always imagePullPolicy: IfNotPresent
command: command:
- python - python
- -c - run_job.py
- "from src.scheduler.jobs import run_job; import sys; run_job(sys.argv[1])"
- waiting-followup - waiting-followup
env: env:
- name: MYORG_REPO_PATH - name: MYORG_REPO_PATH
@@ -54,16 +53,6 @@ spec:
secretKeyRef: secretKeyRef:
name: myorg-assistant-secret name: myorg-assistant-secret
key: LITELLM_API_KEY key: LITELLM_API_KEY
- name: WEB_SECRET_KEY
valueFrom:
secretKeyRef:
name: myorg-assistant-secret
key: WEB_SECRET_KEY
- name: GIT_TOKEN
valueFrom:
secretKeyRef:
name: myorg-assistant-secret
key: GIT_TOKEN
volumeMounts: volumeMounts:
- name: myorg-data - name: myorg-data
mountPath: /data/myorg mountPath: /data/myorg

View File

@@ -34,7 +34,7 @@ spec:
git config user.name "${GIT_USERNAME}" git config user.name "${GIT_USERNAME}"
git config user.email "${GIT_USERNAME}@rogi.casa" git config user.email "${GIT_USERNAME}@rogi.casa"
git config credential.helper store git config credential.helper store
echo "https://${GIT_USERNAME}:${GIT_TOKEN}@git.rogi.casa" > ~/.git-credentials echo "https://${GIT_USERNAME}:${GIT_TOKEN}@gitea.rogi.casa" > ~/.git-credentials
else else
echo "Repository already exists, pulling latest changes..." echo "Repository already exists, pulling latest changes..."
cd /data/myorg cd /data/myorg

View File

@@ -53,17 +53,15 @@ spec:
value: http value: http
- name: N8N_PORT - name: N8N_PORT
value: "5678" value: "5678"
- name: NODE_OPTIONS
value: "--max-old-space-size=768"
image: n8nio/n8n image: n8nio/n8n
name: n8n name: n8n
ports: ports:
- containerPort: 5678 - containerPort: 5678
resources: resources:
requests: requests:
memory: "512Mi" memory: "250Mi"
limits: limits:
memory: "1Gi" memory: "500Mi"
volumeMounts: volumeMounts:
- mountPath: /home/node/.n8n - mountPath: /home/node/.n8n
name: n8n-claim0 name: n8n-claim0

View File

@@ -9,18 +9,18 @@ data:
config.yaml: | config.yaml: |
model: model:
provider: openai-api provider: openai-api
default: qwen3.6 default: z-ai/glm-5.2
base_url: "http://litellm-service.litellm:80/v1" base_url: "http://litellm-service.litellm:80/v1"
api_mode: chat_completions api_mode: chat_completions
auxiliary: auxiliary:
compression: compression:
provider: openai-api provider: openai-api
model: qwen3.6 model: z-ai/glm-5.2
base_url: "http://litellm-service.litellm:80/v1" base_url: "http://litellm-service.litellm:80/v1"
title_generation: title_generation:
provider: openai-api provider: openai-api
model: qwen3.6 model: z-ai/glm-5.2
base_url: "http://litellm-service.litellm:80/v1" base_url: "http://litellm-service.litellm:80/v1"
terminal: terminal:
@@ -59,16 +59,6 @@ data:
cron: cron:
wrap_response: false wrap_response: false
discord:
allowed_channels: '1470909384162017444' # DISCORD_HOME_CHANNEL
free_response_channels: '1470909384162017444' # no @mention needed here
# Per-platform gateway auth. Paired with GATEWAY_ALLOW_ALL_USERS=true in
# the env (secret.yaml), this lets the bot reply to inbound DMs and
# group messages from anyone. Tighten later by switching to
# DISCORD_ALLOWED_USERS=<id> in the secret and dropping these two lines.
dm_policy: open
group_policy: open
memory: memory:
memory_enabled: true memory_enabled: true
user_profile_enabled: true user_profile_enabled: true

View File

@@ -86,7 +86,6 @@ spec:
: > /opt/data/.env : > /opt/data/.env
chmod 600 /opt/data/.env chmod 600 /opt/data/.env
for k in OPENAI_API_KEY OPENAI_BASE_URL DISCORD_BOT_TOKEN DISCORD_HOME_CHANNEL \ for k in OPENAI_API_KEY OPENAI_BASE_URL DISCORD_BOT_TOKEN DISCORD_HOME_CHANNEL \
DISCORD_ALLOW_ALL_USERS DISCORD_FREE_RESPONSE_CHANNELS \
GITEA_TOKEN GITEA_REPO_URL ARGOCD_API_TOKEN ARGOCD_SERVER \ GITEA_TOKEN GITEA_REPO_URL ARGOCD_API_TOKEN ARGOCD_SERVER \
HERMES_DASHBOARD HERMES_DASHBOARD_BASIC_AUTH_USERNAME \ HERMES_DASHBOARD HERMES_DASHBOARD_BASIC_AUTH_USERNAME \
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD HERMES_DASHBOARD_BASIC_AUTH_SECRET; do HERMES_DASHBOARD_BASIC_AUTH_PASSWORD HERMES_DASHBOARD_BASIC_AUTH_SECRET; do