diff --git a/cluster/applications/the-hub/README.md b/cluster/applications/the-hub/README.md new file mode 100644 index 0000000..5795b95 --- /dev/null +++ b/cluster/applications/the-hub/README.md @@ -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=` | +| `pbs_token` | `PBSAPIToken=user@pam!token-name=` | +| `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 | diff --git a/cluster/applications/the-hub/application.yaml b/cluster/applications/the-hub/application.yaml new file mode 100644 index 0000000..14cdbcc --- /dev/null +++ b/cluster/applications/the-hub/application.yaml @@ -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 diff --git a/cluster/applications/the-hub/configmap.yaml b/cluster/applications/the-hub/configmap.yaml new file mode 100644 index 0000000..deb2f6c --- /dev/null +++ b/cluster/applications/the-hub/configmap.yaml @@ -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 }} +
+
+
{{ $vms }}
+
VMs
+
+
+
{{ $lxcs }}
+
LXCs
+
+
+
{{ $running }}
+
Running
+
+
+ + # 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 }} +
+
CPU: {{ printf "%.1f" $cpu }}%
+
RAM: {{ printf "%.1f" $memPct }}%
+
+ + - 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 }} +
+
CPU: {{ printf "%.1f" $cpu }}%
+
RAM: {{ printf "%.1f" $memPct }}%
+
+ + - 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 }} +
+
CPU: {{ printf "%.1f" $cpu }}%
+
RAM: {{ printf "%.1f" $memPct }}%
+
+ + - size: small + widgets: + + # Proxmox Backup Server — community widget + # Requires PBS_TOKEN env var (PBSAPIToken=user@pam!token=) + - 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 }} +
+
CPU: {{ printf "%.1f" $cpu }}%
+
RAM: {{ printf "%.1f" $memPct }}%
+
+ + - 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 diff --git a/cluster/applications/the-hub/deployment.yaml b/cluster/applications/the-hub/deployment.yaml new file mode 100644 index 0000000..fe866d1 --- /dev/null +++ b/cluster/applications/the-hub/deployment.yaml @@ -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 diff --git a/cluster/applications/the-hub/externalsecret.yaml b/cluster/applications/the-hub/externalsecret.yaml new file mode 100644 index 0000000..f187d48 --- /dev/null +++ b/cluster/applications/the-hub/externalsecret.yaml @@ -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 diff --git a/cluster/applications/the-hub/ingress.yaml b/cluster/applications/the-hub/ingress.yaml new file mode 100644 index 0000000..bc8724e --- /dev/null +++ b/cluster/applications/the-hub/ingress.yaml @@ -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 diff --git a/cluster/applications/the-hub/namespace.yaml b/cluster/applications/the-hub/namespace.yaml new file mode 100644 index 0000000..4adbf31 --- /dev/null +++ b/cluster/applications/the-hub/namespace.yaml @@ -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 diff --git a/cluster/applications/the-hub/rbac.yaml b/cluster/applications/the-hub/rbac.yaml new file mode 100644 index 0000000..98acd30 --- /dev/null +++ b/cluster/applications/the-hub/rbac.yaml @@ -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 diff --git a/cluster/applications/the-hub/service.yaml b/cluster/applications/the-hub/service.yaml new file mode 100644 index 0000000..e827235 --- /dev/null +++ b/cluster/applications/the-hub/service.yaml @@ -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