added glance dashboard

This commit is contained in:
2026-05-24 17:04:49 -05:00
parent 3017c27910
commit 6584ed9dc4
9 changed files with 762 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
# the-hub
**Glance** dashboard for mk-labs, deployed on `fastpass` (Talos/Kubernetes).
Managed by ArgoCD. All config is code. No manual kubectl required after initial bootstrap.
---
## Repo layout
```
cluster/
argocd/
the-hub-application.yaml # ArgoCD Application — drop this in with your platform apps
applications/
the-hub/
namespace.yaml
rbac.yaml # ServiceAccount + ClusterRole (read-only K8s access)
externalsecret.yaml # ESO ExternalSecret — pulls tokens from 1Password
deployment.yaml
service.yaml
ingress.yaml
configmap.yaml # glance.yml lives here — this is where you edit the dashboard
```
---
## URL
`https://the-hub.local.mk-labs.cloud`
TLS via cert-manager + Cloudflare DNS-01 (`letsencrypt-prod` ClusterIssuer).
---
## Secrets (1Password → ESO)
Create a 1Password vault item named **`the-hub`** with these fields:
| Field | Value |
|---|---|
| `proxmox_token` | `user@pam!token-name=<uuid>` |
| `pbs_token` | `PBSAPIToken=user@pam!token-name=<uuid>` |
| `grafana_token` | Grafana service account token (read-only) |
| `gitea_token` | Gitea personal access token (read-only) |
ESO syncs these into the `the-hub-secrets` Kubernetes Secret. The Deployment mounts
them as environment variables, which `glance.yml` references via `${VAR_NAME}` syntax.
---
## Proxmox API token setup
Create a **read-only** API token in Proxmox:
1. Datacenter → Permissions → API Tokens → Add
2. User: any read-only user (or `root@pam` for lab use)
3. Token ID: `glance`
4. Uncheck "Privilege Separation" only if you want full access
5. Assign role `PVEAuditor` to the token at the `/` path
Token format in 1Password: `user@pam!glance=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
---
## SSO (Authentik forward-auth)
SSO annotations are present but commented out in `ingress.yaml`. Uncomment when
`guest-relations` Authentik SSO is configured for `fastpass` ingress.
---
## Editing the dashboard
The entire dashboard lives in `configmap.yaml` under the `glance.yml` key.
1. Edit `cluster/applications/the-hub/configmap.yaml`
2. Commit + push
3. ArgoCD syncs automatically (or `argocd app sync the-hub`)
4. Glance hot-reloads the config — no pod restart needed for content changes
(pod restarts only needed when env var secrets change)
---
## Pages
| Page | Purpose |
|---|---|
| **Overview** | Service health monitors + bookmarks + Gitea repo activity |
| **Infrastructure** | Proxmox node stats, PBS status, Grafana iframe |
| **Updates** | Homelab YouTuber RSS, K8s/Argo release tracking, tech news |

View File

@@ -0,0 +1,29 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: the-hub
namespace: argocd
labels:
app.kubernetes.io/name: the-hub
app.kubernetes.io/part-of: mk-labs
annotations:
argocd.argoproj.io/sync-wave: "20" # Wave 20 — applications tier
spec:
project: default
source:
repoURL: ssh://git@gitea.mk-labs.cloud/rblundon/homelab.git
targetRevision: HEAD
path: cluster/applications/the-hub
directory:
recurse: false
exclude: application.yaml # prevent self-reference loop
destination:
server: https://kubernetes.default.svc
namespace: the-hub
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ApplyOutOfSyncOnly=true

View File

@@ -0,0 +1,388 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: the-hub-config
namespace: the-hub
labels:
app.kubernetes.io/name: the-hub
app.kubernetes.io/part-of: mk-labs
data:
glance.yml: |
# ============================================================
# the-hub — mk-labs dashboard
# Managed by ArgoCD. Edit here, commit, let ArgoCD sync.
# Secrets injected as env vars from the-hub-secrets (ESO/1Password)
# ============================================================
server:
port: 8080
proxied: true # behind ingress-nginx; use X-Forwarded-* headers
branding:
app-name: "mk-labs | the hub"
logo-text: "MK"
hide-footer: false
theme:
# Deep dark — feels right for a server room
background-color: 220 13 10
primary-color: 213 80 60
contrast-multiplier: 1.1
# ============================================================
# PAGE 1 — OVERVIEW
# The park map. One pane of glass for the whole kingdom.
# ============================================================
pages:
- name: Overview
columns:
# --- LEFT: fastpass cluster + uptime monitors ---
- size: small
widgets:
- type: monitor
title: fastpass — Control Plane
cache: 1m
sites:
- title: space-mountain
url: https://10.1.71.66:6443/healthz
icon: di:kubernetes
allow-insecure: true
- title: big-thunder-mountain
url: https://10.1.71.67:6443/healthz
icon: di:kubernetes
allow-insecure: true
- title: splash-mountain
url: https://10.1.71.68:6443/healthz
icon: di:kubernetes
allow-insecure: true
- type: monitor
title: fastpass — Workers
cache: 1m
sites:
- title: jungle-cruise
url: http://10.1.71.69:10248/healthz
icon: di:kubernetes
- title: haunted-mansion
url: http://10.1.71.70:10248/healthz
icon: di:kubernetes
- title: peter-pans-flight
url: http://10.1.71.71:10248/healthz
icon: di:kubernetes
- type: monitor
title: magic-kingdom — Proxmox
cache: 2m
sites:
- title: main-street-usa
url: https://10.1.71.11:8006
icon: di:proxmox
allow-insecure: true
- title: tomorrowland
url: https://10.1.71.12:8006
icon: di:proxmox
allow-insecure: true
- title: fantasyland
url: https://10.1.71.13:8006
icon: di:proxmox
allow-insecure: true
- type: monitor
title: utilidor — Storage
cache: 2m
sites:
- title: emporium (Synology)
url: https://10.1.71.9:5001
icon: di:synology
allow-insecure: true
- title: utilidor (FlashArray)
url: https://10.1.71.28
icon: mdi:database
allow-insecure: true
# --- CENTER: Core services health ---
- size: full
widgets:
- type: monitor
title: Platform Services
cache: 1m
sites:
- title: lightning-lane (Traefik)
url: https://traefik.local.mk-labs.cloud/ping
icon: di:traefik
- title: guest-relations (Authentik)
url: https://authentik.local.mk-labs.cloud/-/health/live/
icon: di:authentik
- title: monorail (DNS)
url: http://10.1.71.32:5380
icon: di:technitium
- title: fire-station (NetBox)
url: https://netbox.local.mk-labs.cloud/api/
icon: di:netbox
- title: mad-tea-party (Gitea)
url: https://gitea.mk-labs.cloud/-/health
icon: di:gitea
- title: city-hall (Control Node)
url: http://10.1.71.38:8080
icon: mdi:ansible
- title: imagineering (Semaphore)
url: https://semaphore.local.mk-labs.cloud/api/ping
icon: di:semaphore
- title: tiki-room (n8n)
url: https://n8n.local.mk-labs.cloud/healthz
icon: di:n8n
- title: cinderella-castle (Grafana)
url: https://grafana.local.mk-labs.cloud/api/health
icon: di:grafana
- title: turnstile (step-ca)
url: https://10.1.71.34:8443/health
icon: mdi:certificate
allow-insecure: true
- title: the-grid (Nextcloud)
url: https://nextcloud.local.mk-labs.cloud/status.php
icon: di:nextcloud
- title: timekeeper (PBS)
url: https://pbs.local.mk-labs.cloud
icon: di:proxmox
allow-insecure: true
- type: bookmarks
title: Navigation
groups:
- title: Cluster
links:
- title: ArgoCD
url: https://argocd.local.mk-labs.cloud
icon: di:argocd
- title: Grafana
url: https://grafana.local.mk-labs.cloud
icon: di:grafana
- title: Prometheus
url: https://prometheus.local.mk-labs.cloud
icon: di:prometheus
- title: Infrastructure
links:
- title: Proxmox (main-street-usa)
url: https://10.1.71.11:8006
icon: di:proxmox
- title: Proxmox (tomorrowland)
url: https://10.1.71.12:8006
icon: di:proxmox
- title: Proxmox (fantasyland)
url: https://10.1.71.13:8006
icon: di:proxmox
- title: NetBox
url: https://netbox.local.mk-labs.cloud
icon: di:netbox
- title: Technitium DNS
url: http://10.1.71.32:5380
icon: di:technitium
- title: Apps
links:
- title: Gitea
url: https://gitea.mk-labs.cloud
icon: di:gitea
- title: Semaphore
url: https://semaphore.local.mk-labs.cloud
icon: di:semaphore
- title: n8n
url: https://n8n.local.mk-labs.cloud
icon: di:n8n
- title: Nextcloud
url: https://nextcloud.local.mk-labs.cloud
icon: di:nextcloud
- title: Authentik
url: https://authentik.local.mk-labs.cloud
icon: di:authentik
# --- RIGHT: Gitea repo activity + releases ---
- size: small
widgets:
- type: repository
title: homelab repo
repository: rblundon/homelab
# Uses Gitea (self-hosted) — point at your instance
# Once Gitea mirroring from GitHub is live, switch url here
url: https://gitea.mk-labs.cloud
token: ${GITEA_TOKEN}
pull-requests-limit: 5
issues-limit: 5
commits-limit: 5
- type: releases
title: Stack Releases
cache: 6h
releases:
- repository: argoproj/argo-cd
- repository: cilium/cilium
- repository: cert-manager/cert-manager
- repository: kubernetes/kubernetes
- repository: glanceapp/glance
- repository: talos-systems/talos
- repository: traefik/traefik
# ============================================================
# PAGE 2 — INFRA
# Proxmox cluster stats via community custom-api widgets
# ============================================================
- name: Infrastructure
columns:
- size: full
widgets:
- type: custom-api
title: magic-kingdom — Cluster Overview
cache: 2m
allow-insecure: true
url: https://10.1.71.11:8006/api2/json/cluster/resources
headers:
Authorization: "PVEAPIToken=${PROXMOX_TOKEN}"
template: |
{{ $vms := 0 }}{{ $lxcs := 0 }}{{ $running := 0 }}
{{ range .JSON.Array "data" }}
{{ if eq (.String "type") "qemu" }}{{ $vms = add $vms 1 }}{{ end }}
{{ if eq (.String "type") "lxc" }}{{ $lxcs = add $lxcs 1 }}{{ end }}
{{ if eq (.String "status") "running" }}{{ $running = add $running 1 }}{{ end }}
{{ end }}
<div class="flex gap-4 justify-around">
<div class="text-center">
<div class="text-2xl font-bold">{{ $vms }}</div>
<div class="text-sm text-muted">VMs</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold">{{ $lxcs }}</div>
<div class="text-sm text-muted">LXCs</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-ok">{{ $running }}</div>
<div class="text-sm text-muted">Running</div>
</div>
</div>
# Proxmox VE node stats (community widget pattern)
# One block per node — copy and adjust IP for each
- type: custom-api
title: main-street-usa (10.1.71.11)
cache: 1m
allow-insecure: true
url: https://10.1.71.11:8006/api2/json/nodes/main-street-usa/status
headers:
Authorization: "PVEAPIToken=${PROXMOX_TOKEN}"
template: |
{{ $cpu := mul (.JSON.Float "data.cpu") 100.0 }}
{{ $memUsed := .JSON.Float "data.memory.used" }}
{{ $memTotal := .JSON.Float "data.memory.total" }}
{{ $memPct := mul (div $memUsed $memTotal) 100.0 }}
<div class="flex gap-4">
<div>CPU: <strong>{{ printf "%.1f" $cpu }}%</strong></div>
<div>RAM: <strong>{{ printf "%.1f" $memPct }}%</strong></div>
</div>
- type: custom-api
title: tomorrowland (10.1.71.12)
cache: 1m
allow-insecure: true
url: https://10.1.71.12:8006/api2/json/nodes/tomorrowland/status
headers:
Authorization: "PVEAPIToken=${PROXMOX_TOKEN}"
template: |
{{ $cpu := mul (.JSON.Float "data.cpu") 100.0 }}
{{ $memUsed := .JSON.Float "data.memory.used" }}
{{ $memTotal := .JSON.Float "data.memory.total" }}
{{ $memPct := mul (div $memUsed $memTotal) 100.0 }}
<div class="flex gap-4">
<div>CPU: <strong>{{ printf "%.1f" $cpu }}%</strong></div>
<div>RAM: <strong>{{ printf "%.1f" $memPct }}%</strong></div>
</div>
- type: custom-api
title: fantasyland (10.1.71.13)
cache: 1m
allow-insecure: true
url: https://10.1.71.13:8006/api2/json/nodes/fantasyland/status
headers:
Authorization: "PVEAPIToken=${PROXMOX_TOKEN}"
template: |
{{ $cpu := mul (.JSON.Float "data.cpu") 100.0 }}
{{ $memUsed := .JSON.Float "data.memory.used" }}
{{ $memTotal := .JSON.Float "data.memory.total" }}
{{ $memPct := mul (div $memUsed $memTotal) 100.0 }}
<div class="flex gap-4">
<div>CPU: <strong>{{ printf "%.1f" $cpu }}%</strong></div>
<div>RAM: <strong>{{ printf "%.1f" $memPct }}%</strong></div>
</div>
- size: small
widgets:
# Proxmox Backup Server — community widget
# Requires PBS_TOKEN env var (PBSAPIToken=user@pam!token=<secret>)
- type: custom-api
title: timekeeper (PBS)
cache: 5m
allow-insecure: true
url: https://pbs.local.mk-labs.cloud:8007/api2/json/nodes/localhost/status
headers:
Authorization: "${PBS_TOKEN}"
template: |
{{ $cpu := mul (.JSON.Float "data.cpu") 100.0 }}
{{ $memUsed := .JSON.Float "data.memory.used" }}
{{ $memTotal := .JSON.Float "data.memory.total" }}
{{ $memPct := mul (div $memUsed $memTotal) 100.0 }}
<div class="flex gap-4">
<div>CPU: <strong>{{ printf "%.1f" $cpu }}%</strong></div>
<div>RAM: <strong>{{ printf "%.1f" $memPct }}%</strong></div>
</div>
- type: iframe
title: cinderella-castle — Grafana
source: https://grafana.local.mk-labs.cloud/d/rYdddlPWk/node-exporter-full?orgId=1&kiosk
height: 400
# ============================================================
# PAGE 3 — HOMELAB RSS
# Stay current. Jarvis reads the feeds so you don't have to.
# ============================================================
- name: Updates
columns:
- size: full
widgets:
- type: rss
title: Homelab Community
style: horizontal-cards
feeds:
- url: https://www.youtube.com/feeds/videos.xml?channel_id=UCZNhwA1B5YqiY1nLzmM0ZRg
title: Christian Lempa
- url: https://www.youtube.com/feeds/videos.xml?channel_id=UCOk-gHyjcWZNj3Br4oxwh0A
title: Techno Tim
- url: https://www.youtube.com/feeds/videos.xml?channel_id=UCg6gPGh8HU2U01vaFCAsvmQ
title: Network Chuck
- url: https://www.youtube.com/feeds/videos.xml?channel_id=UCrRQxEQyp3H2aCMYmXTMfhQ
title: Jim's Garage
limit: 6
collapse-after: 4
- type: releases
title: Pure Storage
cache: 12h
releases:
- repository: pure-storage/purity-fb-python-client
- repository: portworx/px-backup
- type: rss
title: Tech / Ops News
feeds:
- url: https://feeds.feedburner.com/TheHackersNews
title: The Hacker News
- url: https://kubernetes.io/feed.xml
title: Kubernetes Blog
- url: https://blog.argoproj.io/feed
title: Argo Blog
limit: 8
collapse-after: 5

View File

@@ -0,0 +1,92 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: the-hub
namespace: the-hub
labels:
app.kubernetes.io/name: the-hub
app.kubernetes.io/part-of: mk-labs
spec:
replicas: 1
revisionHistoryLimit: 3
selector:
matchLabels:
app.kubernetes.io/name: the-hub
strategy:
type: RollingUpdate
template:
metadata:
labels:
app.kubernetes.io/name: the-hub
annotations:
# Force pod restart when the glance config ConfigMap changes
# (Kubernetes does not automatically restart pods on ConfigMap updates)
checksum/config: "{{ include (print $.Template.BasePath \"/configmap.yaml\") . | sha256sum }}"
spec:
serviceAccountName: the-hub
automountServiceAccountToken: true
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: glance
image: glanceapp/glance:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 8080
protocol: TCP
env:
# Injected from ExternalSecret — API tokens used in glance.yml
# via ${ENV_VAR} syntax
- name: PROXMOX_TOKEN
valueFrom:
secretKeyRef:
name: the-hub-secrets
key: PROXMOX_TOKEN
- name: PBS_TOKEN
valueFrom:
secretKeyRef:
name: the-hub-secrets
key: PBS_TOKEN
- name: GRAFANA_TOKEN
valueFrom:
secretKeyRef:
name: the-hub-secrets
key: GRAFANA_TOKEN
- name: GITEA_TOKEN
valueFrom:
secretKeyRef:
name: the-hub-secrets
key: GITEA_TOKEN
volumeMounts:
- name: config
mountPath: /app/config/glance.yml
subPath: glance.yml
readOnly: true
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 15
periodSeconds: 30
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: config
configMap:
name: the-hub-config

View File

@@ -0,0 +1,47 @@
---
# ExternalSecret pulls credentials from 1Password Connect
# and creates a standard Kubernetes Secret that Glance reads
# as environment variables injected into the container.
#
# Prerequisites: ESO + 1Password Connect installed in fastpass
# 1Password vault item: "the-hub" with fields matching below
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: the-hub-secrets
namespace: the-hub
labels:
app.kubernetes.io/name: the-hub
app.kubernetes.io/part-of: mk-labs
spec:
refreshInterval: 1h
secretStoreRef:
name: onepassword-connect # ClusterSecretStore name from your ESO bootstrap
kind: ClusterSecretStore
target:
name: the-hub-secrets
creationPolicy: Owner
data:
# Proxmox API token (read-only, created in Datacenter > API Tokens)
- secretKey: PROXMOX_TOKEN
remoteRef:
key: the-hub
property: proxmox_token
# Proxmox Backup Server API token
- secretKey: PBS_TOKEN
remoteRef:
key: the-hub
property: pbs_token
# Grafana service account token (read-only)
- secretKey: GRAFANA_TOKEN
remoteRef:
key: the-hub
property: grafana_token
# Gitea personal access token (read-only)
- secretKey: GITEA_TOKEN
remoteRef:
key: the-hub
property: gitea_token

View File

@@ -0,0 +1,35 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: the-hub
namespace: the-hub
labels:
app.kubernetes.io/name: the-hub
app.kubernetes.io/part-of: mk-labs
annotations:
# TLS certificate managed by cert-manager + Cloudflare DNS-01
cert-manager.io/cluster-issuer: letsencrypt-prod
# When Authentik forward-auth is ready, uncomment these:
# nginx.ingress.kubernetes.io/auth-url: "https://guest-relations.local.mk-labs.cloud/outpost.goauthentik.io/auth/nginx"
# nginx.ingress.kubernetes.io/auth-signin: "https://guest-relations.local.mk-labs.cloud/outpost.goauthentik.io/start?rd=$escaped_request_uri"
# nginx.ingress.kubernetes.io/auth-response-headers: "Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid"
# nginx.ingress.kubernetes.io/auth-snippet: |
# proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
spec:
ingressClassName: nginx
tls:
- hosts:
- the-hub.local.mk-labs.cloud
secretName: the-hub-tls
rules:
- host: the-hub.local.mk-labs.cloud
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: the-hub
port:
name: http

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: the-hub
labels:
app.kubernetes.io/name: the-hub
app.kubernetes.io/part-of: mk-labs

View File

@@ -0,0 +1,57 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: the-hub
namespace: the-hub
labels:
app.kubernetes.io/name: the-hub
app.kubernetes.io/part-of: mk-labs
---
# ClusterRole grants read-only access to cluster resources
# required for the Glance Kubernetes community widget
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: the-hub
labels:
app.kubernetes.io/name: the-hub
app.kubernetes.io/part-of: mk-labs
rules:
- apiGroups: [""]
resources:
- nodes
- pods
- services
- namespaces
- persistentvolumes
- persistentvolumeclaims
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources:
- deployments
- replicasets
- statefulsets
- daemonsets
verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
resources:
- nodes
- pods
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: the-hub
labels:
app.kubernetes.io/name: the-hub
app.kubernetes.io/part-of: mk-labs
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: the-hub
subjects:
- kind: ServiceAccount
name: the-hub
namespace: the-hub

View File

@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: the-hub
namespace: the-hub
labels:
app.kubernetes.io/name: the-hub
app.kubernetes.io/part-of: mk-labs
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: the-hub
ports:
- name: http
port: 80
targetPort: http
protocol: TCP