my-homelab-configs/apps/heimdall/web-app.yaml

286 lines
7.6 KiB
YAML

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: heimdall-config
namespace: heimdall
spec:
accessModes:
- ReadWriteOnce
storageClassName: openebs-hostpath-retain
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
name: heimdall-link-seed
namespace: heimdall
data:
links.json: |
[
{
"title": "Website",
"url": "https://lab2025.duckdns.org/",
"description": "Public portfolio website",
"colour": "#0f766e",
"class": "Website"
},
{
"title": "Demo Apps",
"url": "https://lab2025.duckdns.org/demo-apps/",
"description": "Static browser-side demo catalog",
"colour": "#2563eb",
"class": "Demos"
},
{
"title": "Gitea",
"url": "https://lab2025.duckdns.org/git/",
"description": "External Git service on the Raspberry Pi",
"colour": "#609926",
"class": "Gitea"
},
{
"title": "Grafana",
"url": "http://100.77.80.72:30083/",
"description": "Monitoring dashboards",
"colour": "#f97316",
"class": "Grafana"
},
{
"title": "Prometheus",
"url": "http://100.77.80.72:30084/",
"description": "Prometheus query UI",
"colour": "#dc2626",
"class": "Prometheus"
},
{
"title": "Alertmanager",
"url": "http://100.77.80.72:30085/",
"description": "Alert routing and silences",
"colour": "#b91c1c",
"class": "Alertmanager"
},
{
"title": "Argo CD",
"url": "https://100.77.80.72:30086/",
"description": "GitOps application sync status",
"colour": "#0ea5e9",
"class": "ArgoCD"
},
{
"title": "Container Registry",
"url": "http://100.77.80.72:30500/v2/_catalog",
"description": "Local image registry catalog endpoint",
"colour": "#334155",
"class": "Docker"
},
{
"title": "Heimdall",
"url": "http://100.77.80.72:30082/",
"description": "Homelab service dashboard",
"colour": "#7c3aed",
"class": "Heimdall"
}
]
seed.py: |
import json
import os
import sqlite3
import time
from datetime import datetime, timezone
DB_CANDIDATES = (
"/config/www/app.sqlite",
"/config/app.sqlite",
)
LINKS_PATH = "/seed/links.json"
def find_database():
while True:
for path in DB_CANDIDATES:
if os.path.exists(path):
return path
time.sleep(5)
def table_exists(conn, name):
row = conn.execute(
"select name from sqlite_master where type = 'table' and name = ?",
(name,),
).fetchone()
return row is not None
def columns(conn, table):
return {row[1] for row in conn.execute(f"pragma table_info({table})")}
def wait_for_items_table(db_path):
while True:
try:
conn = sqlite3.connect(db_path)
if table_exists(conn, "items"):
return conn
conn.close()
except sqlite3.Error:
pass
time.sleep(5)
def upsert_links(conn, links):
item_columns = columns(conn, "items")
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
for order, link in enumerate(links):
existing = conn.execute(
"select id from items where title = ?",
(link["title"],),
).fetchone()
values = {
"title": link["title"],
"url": link["url"],
"description": link.get("description"),
"colour": link.get("colour"),
"pinned": 1,
"order": order,
"type": 0,
"user_id": 1,
"class": link.get("class"),
"appid": None,
"appdescription": link.get("description"),
"created_at": now,
"updated_at": now,
"deleted_at": None,
}
if existing:
update_columns = [
name for name in values
if name in item_columns and name not in ("id", "title", "created_at")
]
assignments = ", ".join(f"{name} = ?" for name in update_columns)
params = [values[name] for name in update_columns]
params.append(existing[0])
conn.execute(f"update items set {assignments} where id = ?", params)
continue
insert_columns = [
name for name in values
if name in item_columns and name != "deleted_at"
]
placeholders = ", ".join("?" for _ in insert_columns)
conn.execute(
f"insert into items ({', '.join(insert_columns)}) values ({placeholders})",
[values[name] for name in insert_columns],
)
conn.commit()
with open(LINKS_PATH, encoding="utf-8") as links_file:
links = json.load(links_file)
db_path = find_database()
connection = wait_for_items_table(db_path)
try:
upsert_links(connection, links)
finally:
connection.close()
while True:
time.sleep(3600)
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: heimdall
namespace: heimdall
labels:
app: heimdall
spec:
replicas: 1
selector:
matchLabels:
app: heimdall
template:
metadata:
labels:
app: heimdall
spec:
nodeSelector:
kubernetes.io/os: linux
homelab.dev/workload-class: platform
securityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
containers:
- name: heimdall
image: lscr.io/linuxserver/heimdall:v2.7.6-ls347
imagePullPolicy: IfNotPresent
env:
- name: PUID
value: "1000"
- name: PGID
value: "1000"
- name: TZ
value: America/Mexico_City
ports:
- containerPort: 80
name: http
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 20
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 60
periodSeconds: 30
resources:
requests:
cpu: 25m
memory: 128Mi
limits:
memory: 512Mi
volumeMounts:
- name: heimdall-config
mountPath: /config
- name: link-seeder
image: python:3.12-alpine
imagePullPolicy: IfNotPresent
command:
- python
- /seed/seed.py
resources:
requests:
cpu: 5m
memory: 32Mi
limits:
memory: 96Mi
volumeMounts:
- name: heimdall-config
mountPath: /config
- name: link-seed
mountPath: /seed
readOnly: true
volumes:
- name: heimdall-config
persistentVolumeClaim:
claimName: heimdall-config
- name: link-seed
configMap:
name: heimdall-link-seed
---
apiVersion: v1
kind: Service
metadata:
name: heimdall
namespace: heimdall
spec:
type: NodePort
ports:
- port: 80
targetPort: http
nodePort: 30082
selector:
app: heimdall