Adding heimdal
Homelab Main / deploy (push) Failing after 6m25s Details

This commit is contained in:
juvdiaz 2026-06-02 09:29:37 -06:00
parent b76075e0fc
commit 0f02fa6efb
8 changed files with 394 additions and 5 deletions

View File

@ -61,8 +61,8 @@ accidentally modify the cluster.
- registers Argo CD Applications from the `applications` map - registers Argo CD Applications from the `applications` map
- passes the website image produced by the build step into Argo CD as a - passes the website image produced by the build step into Argo CD as a
Kustomize image override Kustomize image override
- default apps are `container-registry`, `website-production`, and - default apps are `container-registry`, `website-production`,
`demos-static` `demos-static`, and `heimdall`
5. `bootstrap/edge` 5. `bootstrap/edge`
- connects to the OCI jump box - connects to the OCI jump box
@ -151,6 +151,7 @@ kubectl -n argocd get applications
kubectl -n container-registry get pods kubectl -n container-registry get pods
kubectl -n website-production get pods -o wide kubectl -n website-production get pods -o wide
kubectl -n demos-static get pods -o wide kubectl -n demos-static get pods -o wide
kubectl -n heimdall get pods -o wide
ssh jv@192.168.100.89 'cd /opt/homelab-gitea && sudo docker compose ps' ssh jv@192.168.100.89 'cd /opt/homelab-gitea && sudo docker compose ps'
@ -263,7 +264,8 @@ tailscale_nodeport_access = {
target_port = 8080 target_port = 8080
} }
tailscale_nodeport_extra_ports = [30081] tailscale_nodeport_extra_ports = [30081, 30082, 30083, 30084, 30085, 30086]
tailscale_nodeport_extra_target_ports = [80, 3000, 9090, 9093]
``` ```
For `./lab.sh nuke`, set `WORKER_SSH_TARGETS` to a space-separated list of For `./lab.sh nuke`, set `WORKER_SSH_TARGETS` to a space-separated list of
@ -355,6 +357,14 @@ Add Kubernetes manifests under `apps/<name>` and register them in
`bootstrap/apps`'s `applications` map. Argo CD will own sync, pruning, and `bootstrap/apps`'s `applications` map. Argo CD will own sync, pruning, and
self-healing for the app. self-healing for the app.
The `heimdall` app is intentionally waited on at the end of `./lab.sh apps`.
It runs the LinuxServer.io Heimdall dashboard, persists `/config` on
OpenEBS, and seeds tiles for the website, demo apps, Gitea, Grafana,
Prometheus, Alertmanager, Argo CD, the local registry, and Heimdall itself.
Because Heimdall does not support reverse-proxy subfolder hosting cleanly, it
is exposed through NodePort `30082`; the dashboard's internal tool links use
the Raspberry Pi Tailscale NodePort address.
## Storage ## Storage
OpenEBS provides the platform storage provisioner. Stateful Kubernetes apps use OpenEBS provides the platform storage provisioner. Stateful Kubernetes apps use

View File

@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- web-app.yaml
- ui-nodeports.yaml

View File

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: heimdall

View File

@ -0,0 +1,62 @@
apiVersion: v1
kind: Service
metadata:
name: grafana-nodeport
namespace: monitoring
spec:
type: NodePort
ports:
- name: http
port: 80
targetPort: 3000
nodePort: 30083
selector:
app.kubernetes.io/instance: prometheus-stack
app.kubernetes.io/name: grafana
---
apiVersion: v1
kind: Service
metadata:
name: prometheus-nodeport
namespace: monitoring
spec:
type: NodePort
ports:
- name: http
port: 9090
targetPort: 9090
nodePort: 30084
selector:
app.kubernetes.io/name: prometheus
operator.prometheus.io/name: prometheus-stack-kube-prom-prometheus
---
apiVersion: v1
kind: Service
metadata:
name: alertmanager-nodeport
namespace: monitoring
spec:
type: NodePort
ports:
- name: http
port: 9093
targetPort: 9093
nodePort: 30085
selector:
app.kubernetes.io/name: alertmanager
alertmanager: prometheus-stack-kube-prom-alertmanager
---
apiVersion: v1
kind: Service
metadata:
name: argocd-server-nodeport
namespace: argocd
spec:
type: NodePort
ports:
- name: https
port: 443
targetPort: 8080
nodePort: 30086
selector:
app.kubernetes.io/name: argocd-server

285
apps/heimdall/web-app.yaml Normal file
View File

@ -0,0 +1,285 @@
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:version-2.7.6
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

View File

@ -57,5 +57,14 @@ variable "applications" {
self_heal = true self_heal = true
create_namespace = true create_namespace = true
} }
heimdall = {
project = "default"
path = "apps/heimdall"
namespace = "heimdall"
target_revision = "main"
prune = true
self_heal = true
create_namespace = true
}
} }
} }

View File

@ -93,10 +93,10 @@ variable "tailscale_nodeport_access" {
variable "tailscale_nodeport_extra_ports" { variable "tailscale_nodeport_extra_ports" {
type = list(number) type = list(number)
default = [30081] default = [30081, 30082, 30083, 30084, 30085, 30086]
} }
variable "tailscale_nodeport_extra_target_ports" { variable "tailscale_nodeport_extra_target_ports" {
type = list(number) type = list(number)
default = [] default = [80, 3000, 9090, 9093]
} }

13
lab.sh
View File

@ -172,6 +172,14 @@ adopt_apps_existing_resources() {
"argoproj.io/v1alpha1" \ "argoproj.io/v1alpha1" \
"Application" \ "Application" \
"demos-static" "demos-static"
adopt_tofu_kubernetes_manifest \
"${stack}" \
'kubernetes_manifest.argocd_application["heimdall"]' \
"${namespace}" \
"applications.argoproj.io" \
"argoproj.io/v1alpha1" \
"Application" \
"heimdall"
} }
ensure_homelab_node_labels() { ensure_homelab_node_labels() {
@ -2690,6 +2698,11 @@ apps() {
write_demos_image_state "${demos_image_state_file}" "${demos_source_hash}" "${demos_platforms}" "${demos_image_ref}" write_demos_image_state "${demos_image_state_file}" "${demos_source_hash}" "${demos_platforms}" "${demos_image_ref}"
fi fi
refresh_argocd_application heimdall
wait_for_namespace heimdall heimdall 300
wait_for_namespaced_resource heimdall deployment heimdall heimdall 300
wait_for_deployment_ready heimdall heimdall heimdall 300
echo "Application deployment successfully completed." echo "Application deployment successfully completed."
} }