Add kustomize images: and configMapGenerator: across services (#264)

## Summary

- Move hardcoded image tags to kustomization.yaml `images:` transformer across **22 services** — image names in manifests become version-agnostic templates, with tags centralized in one place per service
- Replace hand-written ConfigMap manifests with `configMapGenerator:` in **12 services** — config data extracted to standalone files, generated ConfigMaps include content hashes that trigger automatic pod rollouts on changes
- Create new `kustomization.yaml` for **forgejo-runner** and **nvidia-device-plugin** (switches ArgoCD from directory mode to kustomize mode, rendered output identical)

### Services modified

**Images only (8):** cv, devpi, docs, kube-state-metrics, miniflux, navidrome, teslamate, torrent

**Images + configMapGenerator (10):** alloy-k8s, forgejo-runner, frigate, grafana, homepage, kiwix, loki, mosquitto, ntfy, prometheus

**Images only, no configMapGenerator (4):** authentik (skip blueprints — special YAML tags), tailscale-operator-base (Deployment only, CRD image fields left as-is)

**Skipped entirely (6):** argocd (remote upstream), databases (no image fields), external-secrets, grafana-config (cross-kustomization dashboards), immich (Helm-managed), 1password-connect/cloudnative-pg (no kustomization.yaml)

### What changes at deploy time

- **images:** — no functional diff, `kustomize build` produces identical output with tags
- **configMapGenerator:** — ConfigMap names gain hash suffixes (e.g., `prometheus-config` → `prometheus-config-6f42fhctcb`) and all Deployment/StatefulSet/DaemonSet references are updated automatically. Pods will restart once per service on first sync due to the name change

## Test plan

- [x] `kubectl kustomize` builds all 30 service directories successfully
- [x] Image tags verified in rendered output for all modified services
- [x] ConfigMap hash suffixes verified in rendered output
- [x] ConfigMap references in Deployments/StatefulSets confirmed to use hashed names
- [x] All pre-commit hooks pass (yamllint, shellcheck, prettier, etc.)
- [ ] `argocd app diff` each service to confirm only expected ConfigMap name changes
- [ ] Deploy from branch starting with a low-risk service (e.g., mosquitto)

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/264
This commit is contained in:
Erich Blume 2026-02-24 14:25:19 -08:00
commit 9b44a8ec51
79 changed files with 956 additions and 905 deletions

View file

@ -0,0 +1,169 @@
// Alloy k8s configuration - collects pod logs from all namespaces
// ============== K8S POD LOG DISCOVERY ==============
// Discover all pods in the cluster
discovery.kubernetes "pods" {
role = "pod"
}
// Relabel to extract useful metadata
discovery.relabel "pods" {
targets = discovery.kubernetes.pods.targets
// Keep only running pods
rule {
source_labels = ["__meta_kubernetes_pod_phase"]
regex = "Pending|Succeeded|Failed|Unknown"
action = "drop"
}
// Set namespace label
rule {
source_labels = ["__meta_kubernetes_namespace"]
target_label = "namespace"
}
// Set pod name label
rule {
source_labels = ["__meta_kubernetes_pod_name"]
target_label = "pod"
}
// Set container name label
rule {
source_labels = ["__meta_kubernetes_pod_container_name"]
target_label = "container"
}
// Set app label from pod labels
rule {
source_labels = ["__meta_kubernetes_pod_label_app"]
target_label = "app"
}
// Fallback: use app.kubernetes.io/name if no app label
rule {
source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"]
target_label = "app"
regex = "(.+)"
action = "replace"
}
// Set node name
rule {
source_labels = ["__meta_kubernetes_pod_node_name"]
target_label = "node"
}
// Build the log path for the pod container
rule {
source_labels = ["__meta_kubernetes_pod_uid", "__meta_kubernetes_pod_container_name"]
target_label = "__path__"
separator = "/"
replacement = "/var/log/pods/*$1/$2/*.log"
}
}
// Tail pod logs
loki.source.kubernetes "pods" {
targets = discovery.relabel.pods.output
forward_to = [loki.process.pods.receiver]
}
// Process logs - parse JSON if present, add labels
loki.process "pods" {
forward_to = [loki.write.loki.receiver]
// Drop noisy deprecation warning from minikube storage-provisioner
// See: https://github.com/kubernetes/minikube/issues/21009
stage.drop {
source = ""
expression = "v1 Endpoints is deprecated"
}
// Try to parse JSON logs (e.g., structured app logs)
// Handle both "msg" (common) and "message" (zot) field names
stage.json {
expressions = {
level = "level",
msg = "msg",
message = "message",
time = "time",
caller = "caller",
repository = "repository",
}
}
// Drop JSON parsing error labels (non-JSON logs are fine, just won't have extracted fields)
stage.label_drop {
values = ["__error__", "__error_details__"]
}
// Extract labels from parsed JSON data
stage.labels {
values = {
level = "",
caller = "",
repository = "",
}
}
}
// Write logs to Loki
loki.write "loki" {
endpoint {
url = "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push"
}
}
// ============== SERVICE HEALTH PROBES ==============
// Blackbox-style HTTP probes for k8s services
prometheus.exporter.blackbox "services" {
config = "{ modules: { http_2xx: { prober: http, timeout: 5s } } }"
target {
name = "miniflux"
address = "http://miniflux.miniflux.svc.cluster.local:8080/healthcheck"
module = "http_2xx"
}
target {
name = "kiwix"
address = "http://kiwix.kiwix.svc.cluster.local:80/"
module = "http_2xx"
}
target {
name = "transmission"
address = "http://transmission.torrent.svc.cluster.local:9091/transmission/web/"
module = "http_2xx"
}
target {
name = "devpi"
address = "http://devpi.devpi.svc.cluster.local:3141/+api"
module = "http_2xx"
}
target {
name = "argocd"
address = "http://argocd-server.argocd.svc.cluster.local:80/healthz"
module = "http_2xx"
}
}
// Scrape blackbox probe results
prometheus.scrape "blackbox" {
targets = prometheus.exporter.blackbox.services.targets
scrape_interval = "30s"
forward_to = [prometheus.remote_write.prometheus.receiver]
}
// Push metrics to Prometheus
prometheus.remote_write "prometheus" {
endpoint {
url = "http://prometheus.monitoring.svc.cluster.local:9090/api/v1/write"
}
}

View file

@ -1,176 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: alloy-config
namespace: alloy
data:
config.alloy: |
// Alloy k8s configuration - collects pod logs from all namespaces
// ============== K8S POD LOG DISCOVERY ==============
// Discover all pods in the cluster
discovery.kubernetes "pods" {
role = "pod"
}
// Relabel to extract useful metadata
discovery.relabel "pods" {
targets = discovery.kubernetes.pods.targets
// Keep only running pods
rule {
source_labels = ["__meta_kubernetes_pod_phase"]
regex = "Pending|Succeeded|Failed|Unknown"
action = "drop"
}
// Set namespace label
rule {
source_labels = ["__meta_kubernetes_namespace"]
target_label = "namespace"
}
// Set pod name label
rule {
source_labels = ["__meta_kubernetes_pod_name"]
target_label = "pod"
}
// Set container name label
rule {
source_labels = ["__meta_kubernetes_pod_container_name"]
target_label = "container"
}
// Set app label from pod labels
rule {
source_labels = ["__meta_kubernetes_pod_label_app"]
target_label = "app"
}
// Fallback: use app.kubernetes.io/name if no app label
rule {
source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"]
target_label = "app"
regex = "(.+)"
action = "replace"
}
// Set node name
rule {
source_labels = ["__meta_kubernetes_pod_node_name"]
target_label = "node"
}
// Build the log path for the pod container
rule {
source_labels = ["__meta_kubernetes_pod_uid", "__meta_kubernetes_pod_container_name"]
target_label = "__path__"
separator = "/"
replacement = "/var/log/pods/*$1/$2/*.log"
}
}
// Tail pod logs
loki.source.kubernetes "pods" {
targets = discovery.relabel.pods.output
forward_to = [loki.process.pods.receiver]
}
// Process logs - parse JSON if present, add labels
loki.process "pods" {
forward_to = [loki.write.loki.receiver]
// Drop noisy deprecation warning from minikube storage-provisioner
// See: https://github.com/kubernetes/minikube/issues/21009
stage.drop {
source = ""
expression = "v1 Endpoints is deprecated"
}
// Try to parse JSON logs (e.g., structured app logs)
// Handle both "msg" (common) and "message" (zot) field names
stage.json {
expressions = {
level = "level",
msg = "msg",
message = "message",
time = "time",
caller = "caller",
repository = "repository",
}
}
// Drop JSON parsing error labels (non-JSON logs are fine, just won't have extracted fields)
stage.label_drop {
values = ["__error__", "__error_details__"]
}
// Extract labels from parsed JSON data
stage.labels {
values = {
level = "",
caller = "",
repository = "",
}
}
}
// Write logs to Loki
loki.write "loki" {
endpoint {
url = "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push"
}
}
// ============== SERVICE HEALTH PROBES ==============
// Blackbox-style HTTP probes for k8s services
prometheus.exporter.blackbox "services" {
config = "{ modules: { http_2xx: { prober: http, timeout: 5s } } }"
target {
name = "miniflux"
address = "http://miniflux.miniflux.svc.cluster.local:8080/healthcheck"
module = "http_2xx"
}
target {
name = "kiwix"
address = "http://kiwix.kiwix.svc.cluster.local:80/"
module = "http_2xx"
}
target {
name = "transmission"
address = "http://transmission.torrent.svc.cluster.local:9091/transmission/web/"
module = "http_2xx"
}
target {
name = "devpi"
address = "http://devpi.devpi.svc.cluster.local:3141/+api"
module = "http_2xx"
}
target {
name = "argocd"
address = "http://argocd-server.argocd.svc.cluster.local:80/healthz"
module = "http_2xx"
}
}
// Scrape blackbox probe results
prometheus.scrape "blackbox" {
targets = prometheus.exporter.blackbox.services.targets
scrape_interval = "30s"
forward_to = [prometheus.remote_write.prometheus.receiver]
}
// Push metrics to Prometheus
prometheus.remote_write "prometheus" {
endpoint {
url = "http://prometheus.monitoring.svc.cluster.local:9090/api/v1/write"
}
}

View file

@ -19,7 +19,7 @@ spec:
fsGroup: 473 # alloy user group
containers:
- name: alloy
image: grafana/alloy:v1.13.1
image: grafana/alloy
args:
- run
- --server.http.listen-addr=0.0.0.0:12345

View file

@ -1,7 +1,18 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: alloy
resources:
- namespace.yaml
- rbac.yaml
- configmap.yaml
- daemonset.yaml
images:
- name: grafana/alloy
newTag: v1.13.1
configMapGenerator:
- name: alloy-config
files:
- config.alloy

View file

@ -18,7 +18,7 @@ spec:
spec:
containers:
- name: redis
image: docker.io/library/redis:7-alpine
image: docker.io/library/redis
ports:
- name: redis
containerPort: 6379

View file

@ -18,7 +18,7 @@ spec:
spec:
containers:
- name: server
image: registry.ops.eblu.me/blumeops/authentik:v2025.10.1-a72a0d8-nix
image: registry.ops.eblu.me/blumeops/authentik
args: ["server"]
ports:
- name: http

View file

@ -18,7 +18,7 @@ spec:
spec:
containers:
- name: worker
image: registry.ops.eblu.me/blumeops/authentik:v2025.10.1-a72a0d8-nix
image: registry.ops.eblu.me/blumeops/authentik
args: ["worker"]
env:
- name: AUTHENTIK_SECRET_KEY

View file

@ -11,3 +11,8 @@ resources:
- service.yaml
- service-redis.yaml
- ingress-tailscale.yaml
images:
- name: registry.ops.eblu.me/blumeops/authentik
newTag: v2025.10.1-a72a0d8-nix
- name: docker.io/library/redis
newTag: 7-alpine

View file

@ -16,7 +16,7 @@ spec:
spec:
containers:
- name: cv
image: registry.ops.eblu.me/blumeops/cv:v1.0.3-ffa8727
image: registry.ops.eblu.me/blumeops/cv
ports:
- containerPort: 80
name: http

View file

@ -6,3 +6,6 @@ resources:
- deployment.yaml
- service.yaml
- ingress-tailscale.yaml
images:
- name: registry.ops.eblu.me/blumeops/cv
newTag: v1.0.3-ffa8727

View file

@ -8,3 +8,7 @@ resources:
- service.yaml
- ingress-tailscale.yaml
- external-secret.yaml
images:
- name: registry.ops.eblu.me/blumeops/devpi
newTag: v6.19.1-ffa8727

View file

@ -18,7 +18,7 @@ spec:
fsGroup: 1000
containers:
- name: devpi
image: registry.ops.eblu.me/blumeops/devpi:v6.19.1-ffa8727
image: registry.ops.eblu.me/blumeops/devpi
env:
- name: DEVPI_ROOT_PASSWORD
valueFrom:

View file

@ -16,7 +16,7 @@ spec:
spec:
containers:
- name: docs
image: registry.ops.eblu.me/blumeops/quartz:v1.28.2-ffa8727
image: registry.ops.eblu.me/blumeops/quartz
ports:
- containerPort: 80
name: http

View file

@ -6,3 +6,6 @@ resources:
- deployment.yaml
- service.yaml
- ingress-tailscale.yaml
images:
- name: registry.ops.eblu.me/blumeops/quartz
newTag: v1.28.2-ffa8727

View file

@ -0,0 +1,19 @@
# Reviewed against v12.7.0 defaults (2026-02-22)
log:
level: info
runner:
file: /data/.runner
capacity: 2
timeout: 3h
shutdown_timeout: 3h
# Env vars injected into all job containers
envs:
DOCKER_HOST: tcp://127.0.0.1:2375
TZ: America/Los_Angeles
container:
# Job execution image is set via RUNNER_LABELS in deployment.yaml
network: "host"
# Connect to DinD sidecar via TCP (not socket)
docker_host: tcp://127.0.0.1:2375

View file

@ -1,30 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: forgejo-runner-config
namespace: forgejo-runner
data:
config.yaml: |
# Reviewed against v12.7.0 defaults (2026-02-22)
log:
level: info
runner:
file: /data/.runner
capacity: 2
timeout: 3h
shutdown_timeout: 3h
# Env vars injected into all job containers
envs:
DOCKER_HOST: tcp://127.0.0.1:2375
TZ: America/Los_Angeles
container:
# Job execution image is set via RUNNER_LABELS in deployment.yaml
network: "host"
# Connect to DinD sidecar via TCP (not socket)
docker_host: tcp://127.0.0.1:2375
daemon.json: |
{
"registry-mirrors": ["http://host.minikube.internal:5050"]
}

View file

@ -0,0 +1,3 @@
{
"registry-mirrors": ["http://host.minikube.internal:5050"]
}

View file

@ -18,7 +18,7 @@ spec:
containers:
# Forgejo runner daemon
- name: runner
image: code.forgejo.org/forgejo/runner:12.7.0
image: code.forgejo.org/forgejo/runner
env:
- name: TZ
value: America/Los_Angeles
@ -68,7 +68,7 @@ spec:
# Docker-in-Docker sidecar
- name: dind
image: docker:27-dind
image: docker
securityContext:
privileged: true
env:

View file

@ -0,0 +1,21 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: forgejo-runner
resources:
- namespace.yaml
- external-secret.yaml
- deployment.yaml
images:
- name: code.forgejo.org/forgejo/runner
newTag: "12.7.0"
- name: docker
newTag: 27-dind
configMapGenerator:
- name: forgejo-runner-config
files:
- config.yaml
- daemon.json

View file

@ -1,42 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: frigate-notify-config
namespace: frigate
data:
config.yml: |
frigate:
server: http://frigate:5000
public_url: https://nvr.ops.eblu.me
webapi:
enabled: true
interval: 15
mqtt:
enabled: false
alerts:
general:
title: "Frigate Alert"
nosnap: drop
snap_hires: true
notify_once: true
zones:
unzoned: drop
allow:
- driveway_entrance
- driveway
labels:
allow:
- person
- car
ntfy:
enabled: true
server: http://ntfy.ntfy.svc.cluster.local:80
topic: frigate-alerts
headers:
- X-Actions: "view, Open Event, {{.Extra.PublicURL}}/review?id={{.ID}}, clear=true; view, Open Camera, {{.Extra.PublicURL}}/#/cameras/{{.Camera}}"

View file

@ -1,90 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: frigate-config
namespace: frigate
data:
config.yml: |
mqtt:
host: mosquitto.mqtt.svc.cluster.local
port: 1883
go2rtc:
streams:
# GableCam IP is reserved in UX7 DHCP config
gablecam:
- "rtsp://{FRIGATE_CAMERA_USER}:{FRIGATE_CAMERA_PASSWORD}@192.168.1.159:554/h264Preview_01_main"
gablecam_sub:
- "rtsp://{FRIGATE_CAMERA_USER}:{FRIGATE_CAMERA_PASSWORD}@192.168.1.159:554/h264Preview_01_sub"
cameras:
gablecam:
enabled: true
ffmpeg:
inputs:
- path: rtsp://127.0.0.1:8554/gablecam
input_args: preset-rtsp-restream
roles: [record]
- path: rtsp://127.0.0.1:8554/gablecam_sub
input_args: preset-rtsp-restream
roles: [detect]
detect:
enabled: true
stationary:
max_frames:
default: 1500
motion:
mask:
- 0.401,0.026,0.4,0.078,0.587,0.072,0.585,0.02
- 0.881,0.422,0.789,0.245,0.595,0.054,0.531,0,0.634,0,0.824,0.192,0.892,0.307
zones:
driveway_entrance:
coordinates: 0.841,0.37,0.735,0.344,0.681,0.2,0.78,0.259
objects: [car, dog, person]
inertia: 3
loitering_time: 0
driveway:
coordinates: 0.767,0.25,0.58,0.2,0.218,0.25,0.128,0.296,0.003,0.565,0.001,0.992,0.826,0.992,0.897,0.665,0.869,0.608,0.788,0.354
review:
alerts:
labels: [person, car]
required_zones:
- driveway_entrance
- driveway
detections:
required_zones:
- driveway
- driveway_entrance
objects:
track: [person, car, dog, cat, bird]
detectors:
onnx:
type: onnx
model:
model_type: yolo-generic
width: 640
height: 640
input_tensor: nchw
input_dtype: float
path: /media/frigate/models/yolov9-c-640.onnx
labelmap_path: /labelmap/coco-80.txt
record:
enabled: true
continuous:
days: 3
alerts:
retain:
days: 30
mode: active_objects
detections:
retain:
days: 14
mode: motion
snapshots:
enabled: true
retain:
default: 14

View file

@ -16,7 +16,7 @@ spec:
spec:
containers:
- name: frigate-notify
image: ghcr.io/0x2142/frigate-notify:v0.5.4
image: ghcr.io/0x2142/frigate-notify
env:
- name: TZ
value: America/Los_Angeles

View file

@ -19,7 +19,7 @@ spec:
runtimeClassName: nvidia
initContainers:
- name: copy-config
image: busybox:1.37
image: busybox
command: ["cp", "/config-ro/config.yml", "/config/config.yml"]
volumeMounts:
- name: config-ro
@ -28,7 +28,7 @@ spec:
mountPath: /config
containers:
- name: frigate
image: ghcr.io/blakeblackshear/frigate:0.17.0-rc2-tensorrt
image: ghcr.io/blakeblackshear/frigate
ports:
- containerPort: 5000
name: http

View file

@ -0,0 +1,83 @@
mqtt:
host: mosquitto.mqtt.svc.cluster.local
port: 1883
go2rtc:
streams:
# GableCam IP is reserved in UX7 DHCP config
gablecam:
- "rtsp://{FRIGATE_CAMERA_USER}:{FRIGATE_CAMERA_PASSWORD}@192.168.1.159:554/h264Preview_01_main"
gablecam_sub:
- "rtsp://{FRIGATE_CAMERA_USER}:{FRIGATE_CAMERA_PASSWORD}@192.168.1.159:554/h264Preview_01_sub"
cameras:
gablecam:
enabled: true
ffmpeg:
inputs:
- path: rtsp://127.0.0.1:8554/gablecam
input_args: preset-rtsp-restream
roles: [record]
- path: rtsp://127.0.0.1:8554/gablecam_sub
input_args: preset-rtsp-restream
roles: [detect]
detect:
enabled: true
stationary:
max_frames:
default: 1500
motion:
mask:
- 0.401,0.026,0.4,0.078,0.587,0.072,0.585,0.02
- 0.881,0.422,0.789,0.245,0.595,0.054,0.531,0,0.634,0,0.824,0.192,0.892,0.307
zones:
driveway_entrance:
coordinates: 0.841,0.37,0.735,0.344,0.681,0.2,0.78,0.259
objects: [car, dog, person]
inertia: 3
loitering_time: 0
driveway:
coordinates: 0.767,0.25,0.58,0.2,0.218,0.25,0.128,0.296,0.003,0.565,0.001,0.992,0.826,0.992,0.897,0.665,0.869,0.608,0.788,0.354
review:
alerts:
labels: [person, car]
required_zones:
- driveway_entrance
- driveway
detections:
required_zones:
- driveway
- driveway_entrance
objects:
track: [person, car, dog, cat, bird]
detectors:
onnx:
type: onnx
model:
model_type: yolo-generic
width: 640
height: 640
input_tensor: nchw
input_dtype: float
path: /media/frigate/models/yolov9-c-640.onnx
labelmap_path: /labelmap/coco-80.txt
record:
enabled: true
continuous:
days: 3
alerts:
retain:
days: 30
mode: active_objects
detections:
retain:
days: 14
mode: motion
snapshots:
enabled: true
retain:
default: 14

View file

@ -0,0 +1,35 @@
frigate:
server: http://frigate:5000
public_url: https://nvr.ops.eblu.me
webapi:
enabled: true
interval: 15
mqtt:
enabled: false
alerts:
general:
title: "Frigate Alert"
nosnap: drop
snap_hires: true
notify_once: true
zones:
unzoned: drop
allow:
- driveway_entrance
- driveway
labels:
allow:
- person
- car
ntfy:
enabled: true
server: http://ntfy.ntfy.svc.cluster.local:80
topic: frigate-alerts
headers:
- X-Actions: "view, Open Event, {{.Extra.PublicURL}}/review?id={{.ID}}, clear=true; view, Open Camera, {{.Extra.PublicURL}}/#/cameras/{{.Camera}}"

View file

@ -4,8 +4,6 @@ kind: Kustomization
namespace: frigate
resources:
- external-secret.yaml
- configmap.yaml
- configmap-notify.yaml
- pv-nfs.yaml
- pvc-recordings.yaml
- pvc-database.yaml
@ -13,3 +11,19 @@ resources:
- deployment-notify.yaml
- service.yaml
- ingress-tailscale.yaml
images:
- name: busybox
newTag: "1.37"
- name: ghcr.io/blakeblackshear/frigate
newTag: 0.17.0-rc2-tensorrt
- name: ghcr.io/0x2142/frigate-notify
newTag: v0.5.4
configMapGenerator:
- name: frigate-config
files:
- config.yml=frigate-config.yml
- name: frigate-notify-config
files:
- config.yml=frigate-notify-config.yml

View file

@ -1,100 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana
namespace: monitoring
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
data:
grafana.ini: |
[analytics]
check_for_updates = false
reporting_enabled = false
[auth.generic_oauth]
allow_sign_up = true
api_url = https://authentik.ops.eblu.me/application/o/userinfo/
auth_url = https://authentik.ops.eblu.me/application/o/authorize/
auto_login = false
client_id = grafana
client_secret = $__env{GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET}
enabled = true
name = Authentik
role_attribute_path = contains(groups[*], 'admins') && 'Admin' || 'Viewer'
skip_org_role_sync = false
scopes = openid profile email
token_url = https://authentik.ops.eblu.me/application/o/token/
[log]
mode = console
[paths]
data = /var/lib/grafana/
logs = /var/log/grafana
plugins = /var/lib/grafana/plugins
provisioning = /etc/grafana/provisioning
[security]
allow_embedding = false
[server]
root_url = https://grafana.ops.eblu.me
datasources.yaml: |
apiVersion: 1
datasources:
- access: proxy
editable: false
isDefault: true
name: Prometheus
orgId: 1
type: prometheus
uid: prometheus
url: http://prometheus.monitoring.svc.cluster.local:9090
- access: proxy
editable: false
name: Loki
orgId: 1
type: loki
uid: loki
url: http://loki.monitoring.svc.cluster.local:3100
- access: proxy
database: teslamate
editable: false
jsonData:
database: teslamate
connMaxLifetime: 14400
maxIdleConns: 2
maxOpenConns: 5
sslmode: disable
name: TeslaMate
orgId: 1
secureJsonData:
password: $TESLAMATE_DB_PASSWORD
type: postgres
uid: TeslaMate
url: blumeops-pg-rw.databases.svc.cluster.local:5432
user: teslamate
---
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-config-dashboards
namespace: monitoring
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
data:
provider.yaml: |
apiVersion: 1
providers:
- name: 'sidecarProvider'
orgId: 1
type: file
disableDeletion: false
allowUiUpdates: false
updateIntervalSeconds: 30
options:
foldersFromFilesStructure: true
path: /tmp/dashboards

View file

@ -0,0 +1,34 @@
apiVersion: 1
datasources:
- access: proxy
editable: false
isDefault: true
name: Prometheus
orgId: 1
type: prometheus
uid: prometheus
url: http://prometheus.monitoring.svc.cluster.local:9090
- access: proxy
editable: false
name: Loki
orgId: 1
type: loki
uid: loki
url: http://loki.monitoring.svc.cluster.local:3100
- access: proxy
database: teslamate
editable: false
jsonData:
database: teslamate
connMaxLifetime: 14400
maxIdleConns: 2
maxOpenConns: 5
sslmode: disable
name: TeslaMate
orgId: 1
secureJsonData:
password: $TESLAMATE_DB_PASSWORD
type: postgres
uid: TeslaMate
url: blumeops-pg-rw.databases.svc.cluster.local:5432
user: teslamate

View file

@ -32,7 +32,7 @@ spec:
runAsUser: 472
initContainers:
- name: init-chown-data
image: docker.io/library/busybox:1.31.1
image: docker.io/library/busybox
imagePullPolicy: IfNotPresent
command: ["chown", "-R", "472:472", "/var/lib/grafana"]
securityContext:
@ -48,7 +48,7 @@ spec:
containers:
# Dashboard sidecar - watches ConfigMaps with grafana_dashboard=1
- name: grafana-sc-dashboard
image: quay.io/kiwigrid/k8s-sidecar:1.28.0
image: quay.io/kiwigrid/k8s-sidecar
imagePullPolicy: IfNotPresent
env:
- name: METHOD
@ -88,7 +88,7 @@ spec:
mountPath: /tmp/dashboards
# Grafana
- name: grafana
image: registry.ops.eblu.me/blumeops/grafana:v12.3.3-d05d2fb
image: registry.ops.eblu.me/blumeops/grafana
imagePullPolicy: IfNotPresent
env:
- name: POD_IP

View file

@ -0,0 +1,32 @@
[analytics]
check_for_updates = false
reporting_enabled = false
[auth.generic_oauth]
allow_sign_up = true
api_url = https://authentik.ops.eblu.me/application/o/userinfo/
auth_url = https://authentik.ops.eblu.me/application/o/authorize/
auto_login = false
client_id = grafana
client_secret = $__env{GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET}
enabled = true
name = Authentik
role_attribute_path = contains(groups[*], 'admins') && 'Admin' || 'Viewer'
skip_org_role_sync = false
scopes = openid profile email
token_url = https://authentik.ops.eblu.me/application/o/token/
[log]
mode = console
[paths]
data = /var/lib/grafana/
logs = /var/log/grafana
plugins = /var/lib/grafana/plugins
provisioning = /etc/grafana/provisioning
[security]
allow_embedding = false
[server]
root_url = https://grafana.ops.eblu.me

View file

@ -5,8 +5,24 @@ namespace: monitoring
resources:
- serviceaccount.yaml
- configmap.yaml
- pvc.yaml
- deployment.yaml
- service.yaml
- rbac.yaml
images:
- name: docker.io/library/busybox
newTag: 1.31.1
- name: quay.io/kiwigrid/k8s-sidecar
newTag: 1.28.0
- name: registry.ops.eblu.me/blumeops/grafana
newTag: v12.3.3-d05d2fb
configMapGenerator:
- name: grafana
files:
- grafana.ini
- datasources.yaml
- name: grafana-config-dashboards
files:
- provider.yaml

View file

@ -0,0 +1,11 @@
apiVersion: 1
providers:
- name: 'sidecarProvider'
orgId: 1
type: file
disableDeletion: false
allowUiUpdates: false
updateIntervalSeconds: 30
options:
foldersFromFilesStructure: true
path: /tmp/dashboards

View file

@ -0,0 +1,16 @@
- Admin:
- Tailscale Admin:
- href: https://login.tailscale.com/admin
icon: tailscale
- 1Password:
- href: https://my.1password.com
icon: 1password
- Pulumi:
- href: https://app.pulumi.com/eblume/blumeops-tailnet
icon: si-pulumi
- ArgoCD:
- href: https://argocd.ops.eblu.me
icon: argo-cd
- UniFi:
- href: https://unifi.ui.com
icon: ubiquiti

View file

@ -1,155 +0,0 @@
# Homepage configuration files
# Extracted from former Helm values.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: homepage-config
namespace: homepage
data:
bookmarks.yaml: |
- Admin:
- Tailscale Admin:
- href: https://login.tailscale.com/admin
icon: tailscale
- 1Password:
- href: https://my.1password.com
icon: 1password
- Pulumi:
- href: https://app.pulumi.com/eblume/blumeops-tailnet
icon: si-pulumi
- ArgoCD:
- href: https://argocd.ops.eblu.me
icon: argo-cd
- UniFi:
- href: https://unifi.ui.com
icon: ubiquiti
services.yaml: |
- Host Services:
- Forgejo:
href: https://forge.ops.eblu.me
icon: forgejo
description: Git forge
widget:
type: gitea
url: https://forge.ops.eblu.me
key: "{{HOMEPAGE_VAR_FORGEJO_API_KEY}}"
- Registry:
href: https://registry.ops.eblu.me
icon: zot-registry
description: Container registry
- Sifaka NAS:
href: https://nas.ops.eblu.me
icon: synology
description: NAS dashboard
widget:
type: prometheusmetric
url: https://prometheus.ops.eblu.me
metrics:
- label: Used
query: node_filesystem_size_bytes{mountpoint="/Volumes/backups"} - node_filesystem_avail_bytes{mountpoint="/Volumes/backups"}
format:
type: bytes
- label: Total
query: node_filesystem_size_bytes{mountpoint="/Volumes/backups"}
format:
type: bytes
- Borgmatic:
href: https://grafana.ops.eblu.me/d/borgmatic
icon: borgmatic
description: Backup system
widget:
type: prometheusmetric
url: https://prometheus.ops.eblu.me
metrics:
- label: Last backup
query: time() - borgmatic_last_archive_timestamp
format:
type: duration
- label: Archive size
query: borgmatic_repo_deduplicated_size_bytes
format:
type: bytes
- Jellyfin:
href: https://jellyfin.ops.eblu.me
icon: jellyfin
description: Media server
widget:
type: jellyfin
url: https://jellyfin.ops.eblu.me
key: "{{HOMEPAGE_VAR_JELLYFIN_API_KEY}}"
enableBlocks: true
enableNowPlaying: true
# TODO: Add Caddy widget when admin API is enabled (currently admin off)
# - Caddy:
# href: https://indri.tail8d86e.ts.net
# icon: caddy
# description: Reverse proxy
# widget:
# type: caddy
# url: http://indri.tail8d86e.ts.net:2019
- Infrastructure:
- Authentik:
href: https://authentik.ops.eblu.me
icon: authentik
description: Identity provider
- NVR:
href: https://nvr.ops.eblu.me
icon: frigate.png
description: Network video recorder
- Ntfy:
href: https://ntfy.ops.eblu.me
icon: ntfy.png
description: Push notifications
widgets.yaml: |
- greeting:
text_size: xl
text: Welcome to Blue Mops
- datetime:
text_size: lg
format:
dateStyle: long
timeStyle: short
hour12: true
- openweathermap:
label: Camano
latitude: 48.18235
longitude: -122.52590
units: imperial
provider: openweathermap
apiKey: "{{HOMEPAGE_VAR_OPENWEATHERMAP_API_KEY}}"
cache: 15
# TODO: Add UniFi widget when controller is set up
# - unifi_console:
# url: https://192.168.1.1
# username: homepage
# password: "{{HOMEPAGE_VAR_UNIFI_PASSWORD}}"
# TODO: Add Glances widget when Glances is deployed
# - glances:
# url: http://indri.tail8d86e.ts.net:61208
# metric: cpu
kubernetes.yaml: |
mode: cluster
docker.yaml: ""
settings.yaml: |
title: BlumeOps
headerStyle: boxed
quicklaunch:
searchDescriptions: true
showSearchSuggestions: true
provider: custom
url: https://kagi.com/search?q=
suggestionUrl: https://kagisuggest.com/api/autosuggest?q=
layout:
Host Services:
style: column
Content:
style: column
Infrastructure:
style: column
Services:
style: column

View file

@ -20,7 +20,7 @@ spec:
fsGroup: 1000
containers:
- name: homepage
image: registry.ops.eblu.me/blumeops/homepage:v1.10.1-a72a0d8
image: registry.ops.eblu.me/blumeops/homepage
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false

View file

View file

@ -0,0 +1 @@
mode: cluster

View file

@ -5,7 +5,6 @@ resources:
- serviceaccount.yaml
- clusterrole.yaml
- clusterrolebinding.yaml
- configmap.yaml
- deployment.yaml
- service.yaml
- ingress-tailscale.yaml
@ -15,3 +14,17 @@ resources:
- external-secret-grafana.yaml
- external-secret-miniflux.yaml
- external-secret-navidrome.yaml
images:
- name: registry.ops.eblu.me/blumeops/homepage
newTag: v1.10.1-a72a0d8
configMapGenerator:
- name: homepage-config
files:
- bookmarks.yaml
- services.yaml
- widgets.yaml
- kubernetes.yaml
- docker.yaml
- settings.yaml

View file

@ -0,0 +1,76 @@
- Host Services:
- Forgejo:
href: https://forge.ops.eblu.me
icon: forgejo
description: Git forge
widget:
type: gitea
url: https://forge.ops.eblu.me
key: "{{HOMEPAGE_VAR_FORGEJO_API_KEY}}"
- Registry:
href: https://registry.ops.eblu.me
icon: zot-registry
description: Container registry
- Sifaka NAS:
href: https://nas.ops.eblu.me
icon: synology
description: NAS dashboard
widget:
type: prometheusmetric
url: https://prometheus.ops.eblu.me
metrics:
- label: Used
query: node_filesystem_size_bytes{mountpoint="/Volumes/backups"} - node_filesystem_avail_bytes{mountpoint="/Volumes/backups"}
format:
type: bytes
- label: Total
query: node_filesystem_size_bytes{mountpoint="/Volumes/backups"}
format:
type: bytes
- Borgmatic:
href: https://grafana.ops.eblu.me/d/borgmatic
icon: borgmatic
description: Backup system
widget:
type: prometheusmetric
url: https://prometheus.ops.eblu.me
metrics:
- label: Last backup
query: time() - borgmatic_last_archive_timestamp
format:
type: duration
- label: Archive size
query: borgmatic_repo_deduplicated_size_bytes
format:
type: bytes
- Jellyfin:
href: https://jellyfin.ops.eblu.me
icon: jellyfin
description: Media server
widget:
type: jellyfin
url: https://jellyfin.ops.eblu.me
key: "{{HOMEPAGE_VAR_JELLYFIN_API_KEY}}"
enableBlocks: true
enableNowPlaying: true
# TODO: Add Caddy widget when admin API is enabled (currently admin off)
# - Caddy:
# href: https://indri.tail8d86e.ts.net
# icon: caddy
# description: Reverse proxy
# widget:
# type: caddy
# url: http://indri.tail8d86e.ts.net:2019
- Infrastructure:
- Authentik:
href: https://authentik.ops.eblu.me
icon: authentik
description: Identity provider
- NVR:
href: https://nvr.ops.eblu.me
icon: frigate.png
description: Network video recorder
- Ntfy:
href: https://ntfy.ops.eblu.me
icon: ntfy.png
description: Push notifications

View file

@ -0,0 +1,17 @@
title: BlumeOps
headerStyle: boxed
quicklaunch:
searchDescriptions: true
showSearchSuggestions: true
provider: custom
url: https://kagi.com/search?q=
suggestionUrl: https://kagisuggest.com/api/autosuggest?q=
layout:
Host Services:
style: column
Content:
style: column
Infrastructure:
style: column
Services:
style: column

View file

@ -0,0 +1,26 @@
- greeting:
text_size: xl
text: Welcome to Blue Mops
- datetime:
text_size: lg
format:
dateStyle: long
timeStyle: short
hour12: true
- openweathermap:
label: Camano
latitude: 48.18235
longitude: -122.52590
units: imperial
provider: openweathermap
apiKey: "{{HOMEPAGE_VAR_OPENWEATHERMAP_API_KEY}}"
cache: 15
# TODO: Add UniFi widget when controller is set up
# - unifi_console:
# url: https://192.168.1.1
# username: homepage
# password: "{{HOMEPAGE_VAR_UNIFI_PASSWORD}}"
# TODO: Add Glances widget when Glances is deployed
# - glances:
# url: http://indri.tail8d86e.ts.net:61208
# metric: cpu

View file

@ -1,68 +0,0 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: zim-torrent-sync-script
namespace: kiwix
data:
sync-zim-torrents.sh: |
#!/bin/bash
# Sync ZIM torrents from kiwix ConfigMap to Transmission
# Runs as a sidecar in the kiwix deployment
set -euo pipefail
TORRENT_LIST="${TORRENT_LIST:-/config/torrents.txt}"
TRANSMISSION_HOST="${TRANSMISSION_HOST:-transmission.torrent.svc.cluster.local}"
TRANSMISSION_PORT="${TRANSMISSION_PORT:-9091}"
echo "Syncing ZIM torrents to transmission at ${TRANSMISSION_HOST}:${TRANSMISSION_PORT}"
# Wait for transmission to be ready
# Transmission RPC returns 409 on first request (to provide session ID), which is fine
echo "Waiting for Transmission RPC..."
max_attempts=30
attempt=0
until curl -s -o /dev/null -w "%{http_code}" "http://${TRANSMISSION_HOST}:${TRANSMISSION_PORT}/transmission/rpc" | grep -qE "^(200|409)$"; do
attempt=$((attempt + 1))
if [[ $attempt -ge $max_attempts ]]; then
echo "Transmission not ready after ${max_attempts} attempts, will retry next cycle"
exit 0 # Don't fail, just skip this sync
fi
sleep 10
done
echo "Transmission is ready"
# Get current torrents from transmission
# transmission-remote returns header + data + footer, extract just torrent names
current=$(transmission-remote "${TRANSMISSION_HOST}:${TRANSMISSION_PORT}" -l 2>/dev/null | \
tail -n +2 | head -n -1 | awk '{print $NF}' || true)
added=0
skipped=0
while IFS= read -r url || [[ -n "$url" ]]; do
# Skip empty lines and comments
[[ -z "$url" || "$url" =~ ^[[:space:]]*# ]] && continue
# Trim whitespace
url=$(echo "$url" | xargs)
[[ -z "$url" ]] && continue
# Extract base name from URL (remove .torrent extension)
basename=$(basename "$url" .torrent)
# Also try without .zim in case transmission reports it differently
basename_no_zim="${basename%.zim}"
# Check if already in transmission
if echo "$current" | grep -qF "$basename_no_zim"; then
((skipped++)) || true
else
if transmission-remote "${TRANSMISSION_HOST}:${TRANSMISSION_PORT}" -a "$url" 2>/dev/null; then
echo "Added: $basename"
((added++)) || true
else
echo "Warning: Failed to add $url" >&2
fi
fi
done < "$TORRENT_LIST"
echo "Sync complete: $added added, $skipped already present"

View file

@ -1,69 +0,0 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: kiwix-zim-torrents
namespace: kiwix
data:
torrents.txt: |
# Declarative ZIM archive torrent URLs
# These are synced to transmission automatically by the kiwix sidecar
# Format: one URL per line, comments start with #
#
# Users can also add ZIM torrents manually via torrent.tail8d86e.ts.net
# and kiwix will pick them up automatically.
# Wikipedia - Top 1M English articles (43G)
https://download.kiwix.org/zim/wikipedia/wikipedia_en_top1m_maxi_2025-09.zim.torrent
# Project Gutenberg - Public domain books (72G)
https://download.kiwix.org/zim/gutenberg/gutenberg_en_all_2023-08.zim.torrent
# iFixit - Repair guides (3.3G)
https://download.kiwix.org/zim/ifixit/ifixit_en_all_2025-12.zim.torrent
# Stack Exchange
https://download.kiwix.org/zim/stack_exchange/superuser.com_en_all_2025-12.zim.torrent
https://download.kiwix.org/zim/stack_exchange/math.stackexchange.com_en_all_2025-12.zim.torrent
# LibreTexts - Open educational resources
https://download.kiwix.org/zim/libretexts/libretexts.org_en_bio_2025-01.zim.torrent
https://download.kiwix.org/zim/libretexts/libretexts.org_en_chem_2025-01.zim.torrent
https://download.kiwix.org/zim/libretexts/libretexts.org_en_eng_2025-01.zim.torrent
https://download.kiwix.org/zim/libretexts/libretexts.org_en_math_2025-01.zim.torrent
https://download.kiwix.org/zim/libretexts/libretexts.org_en_phys_2025-01.zim.torrent
https://download.kiwix.org/zim/libretexts/libretexts.org_en_human_2025-01.zim.torrent
# DevDocs - Programming documentation
https://download.kiwix.org/zim/devdocs/devdocs_en_bash_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_c_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_click_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_cmake_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_cpp_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_css_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_django-rest-framework_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_django_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_docker_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_duckdb_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_fish_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_gcc_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_git_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_go_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_godot_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_hammerspoon_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_homebrew_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_javascript_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_kubectl_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_kubernetes_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_latex_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_lua_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_markdown_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_nginx_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_nix_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_postgresql_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_python_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_redis_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_sqlite_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_typescript_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_werkzeug_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_zig_2026-01.zim.torrent

View file

@ -15,7 +15,7 @@ spec:
serviceAccountName: zim-watcher
containers:
- name: watcher
image: registry.ops.eblu.me/blumeops/kubectl:v1.34.4-a72a0d8
image: registry.ops.eblu.me/blumeops/kubectl
command: ["/bin/bash", "-c"]
args:
- |

View file

@ -20,7 +20,7 @@ spec:
containers:
# Main kiwix-serve container
- name: kiwix-serve
image: registry.ops.eblu.me/blumeops/kiwix-serve:v3.8.1-ffa8727
image: registry.ops.eblu.me/blumeops/kiwix-serve
args:
- "/bin/sh"
- "-c"
@ -53,7 +53,7 @@ spec:
# Sidecar: Syncs declarative ZIM torrents to transmission
- name: torrent-sync
image: registry.ops.eblu.me/blumeops/transmission:v4.0.6-r4-ffa8727
image: registry.ops.eblu.me/blumeops/transmission
command: ["/bin/bash", "-c"]
args:
- |

View file

@ -3,9 +3,23 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: kiwix
resources:
- configmap-zim-torrents.yaml
- configmap-sync-script.yaml
- deployment.yaml
- service.yaml
- ingress-tailscale.yaml
- cronjob-zim-watcher.yaml
images:
- name: registry.ops.eblu.me/blumeops/kiwix-serve
newTag: v3.8.1-ffa8727
- name: registry.ops.eblu.me/blumeops/transmission
newTag: v4.0.6-r4-ffa8727
- name: registry.ops.eblu.me/blumeops/kubectl
newTag: v1.34.4-a72a0d8
configMapGenerator:
- name: kiwix-zim-torrents
files:
- torrents.txt
- name: zim-torrent-sync-script
files:
- sync-zim-torrents.sh

View file

@ -0,0 +1,60 @@
#!/bin/bash
# Sync ZIM torrents from kiwix ConfigMap to Transmission
# Runs as a sidecar in the kiwix deployment
set -euo pipefail
TORRENT_LIST="${TORRENT_LIST:-/config/torrents.txt}"
TRANSMISSION_HOST="${TRANSMISSION_HOST:-transmission.torrent.svc.cluster.local}"
TRANSMISSION_PORT="${TRANSMISSION_PORT:-9091}"
echo "Syncing ZIM torrents to transmission at ${TRANSMISSION_HOST}:${TRANSMISSION_PORT}"
# Wait for transmission to be ready
# Transmission RPC returns 409 on first request (to provide session ID), which is fine
echo "Waiting for Transmission RPC..."
max_attempts=30
attempt=0
until curl -s -o /dev/null -w "%{http_code}" "http://${TRANSMISSION_HOST}:${TRANSMISSION_PORT}/transmission/rpc" | grep -qE "^(200|409)$"; do
attempt=$((attempt + 1))
if [[ $attempt -ge $max_attempts ]]; then
echo "Transmission not ready after ${max_attempts} attempts, will retry next cycle"
exit 0 # Don't fail, just skip this sync
fi
sleep 10
done
echo "Transmission is ready"
# Get current torrents from transmission
# transmission-remote returns header + data + footer, extract just torrent names
current=$(transmission-remote "${TRANSMISSION_HOST}:${TRANSMISSION_PORT}" -l 2>/dev/null | \
tail -n +2 | head -n -1 | awk '{print $NF}' || true)
added=0
skipped=0
while IFS= read -r url || [[ -n "$url" ]]; do
# Skip empty lines and comments
[[ -z "$url" || "$url" =~ ^[[:space:]]*# ]] && continue
# Trim whitespace
url=$(echo "$url" | xargs)
[[ -z "$url" ]] && continue
# Extract base name from URL (remove .torrent extension)
basename=$(basename "$url" .torrent)
# Also try without .zim in case transmission reports it differently
basename_no_zim="${basename%.zim}"
# Check if already in transmission
if echo "$current" | grep -qF "$basename_no_zim"; then
((skipped++)) || true
else
if transmission-remote "${TRANSMISSION_HOST}:${TRANSMISSION_PORT}" -a "$url" 2>/dev/null; then
echo "Added: $basename"
((added++)) || true
else
echo "Warning: Failed to add $url" >&2
fi
fi
done < "$TORRENT_LIST"
echo "Sync complete: $added added, $skipped already present"

View file

@ -0,0 +1,61 @@
# Declarative ZIM archive torrent URLs
# These are synced to transmission automatically by the kiwix sidecar
# Format: one URL per line, comments start with #
#
# Users can also add ZIM torrents manually via torrent.tail8d86e.ts.net
# and kiwix will pick them up automatically.
# Wikipedia - Top 1M English articles (43G)
https://download.kiwix.org/zim/wikipedia/wikipedia_en_top1m_maxi_2025-09.zim.torrent
# Project Gutenberg - Public domain books (72G)
https://download.kiwix.org/zim/gutenberg/gutenberg_en_all_2023-08.zim.torrent
# iFixit - Repair guides (3.3G)
https://download.kiwix.org/zim/ifixit/ifixit_en_all_2025-12.zim.torrent
# Stack Exchange
https://download.kiwix.org/zim/stack_exchange/superuser.com_en_all_2025-12.zim.torrent
https://download.kiwix.org/zim/stack_exchange/math.stackexchange.com_en_all_2025-12.zim.torrent
# LibreTexts - Open educational resources
https://download.kiwix.org/zim/libretexts/libretexts.org_en_bio_2025-01.zim.torrent
https://download.kiwix.org/zim/libretexts/libretexts.org_en_chem_2025-01.zim.torrent
https://download.kiwix.org/zim/libretexts/libretexts.org_en_eng_2025-01.zim.torrent
https://download.kiwix.org/zim/libretexts/libretexts.org_en_math_2025-01.zim.torrent
https://download.kiwix.org/zim/libretexts/libretexts.org_en_phys_2025-01.zim.torrent
https://download.kiwix.org/zim/libretexts/libretexts.org_en_human_2025-01.zim.torrent
# DevDocs - Programming documentation
https://download.kiwix.org/zim/devdocs/devdocs_en_bash_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_c_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_click_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_cmake_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_cpp_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_css_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_django-rest-framework_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_django_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_docker_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_duckdb_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_fish_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_gcc_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_git_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_go_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_godot_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_hammerspoon_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_homebrew_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_javascript_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_kubectl_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_kubernetes_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_latex_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_lua_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_markdown_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_nginx_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_nix_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_postgresql_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_python_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_redis_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_sqlite_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_typescript_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_werkzeug_2026-01.zim.torrent
https://download.kiwix.org/zim/devdocs/devdocs_en_zig_2026-01.zim.torrent

View file

@ -18,7 +18,7 @@ spec:
serviceAccountName: kube-state-metrics
containers:
- name: kube-state-metrics
image: registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.18.0
image: registry.k8s.io/kube-state-metrics/kube-state-metrics
ports:
- containerPort: 8080
name: http-metrics

View file

@ -4,3 +4,6 @@ resources:
- rbac.yaml
- deployment.yaml
- service.yaml
images:
- name: registry.k8s.io/kube-state-metrics/kube-state-metrics
newTag: v2.18.0

View file

@ -1,58 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: loki-config
namespace: monitoring
data:
loki-config.yaml: |
auth_enabled: false
server:
http_listen_port: 3100
http_listen_address: 0.0.0.0
grpc_listen_port: 9096
common:
instance_addr: 127.0.0.1
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
storage_config:
tsdb_shipper:
active_index_directory: /loki/tsdb-index
cache_location: /loki/tsdb-cache
limits_config:
retention_period: 744h # 31 days
compactor:
working_directory: /loki/compactor
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
retention_delete_worker_count: 150
delete_request_store: filesystem

View file

@ -4,7 +4,15 @@ kind: Kustomization
namespace: monitoring
resources:
- configmap.yaml
- statefulset.yaml
- service.yaml
- ingress-tailscale.yaml
images:
- name: grafana/loki
newTag: "3.6.5"
configMapGenerator:
- name: loki-config
files:
- loki-config.yaml

View file

@ -0,0 +1,51 @@
auth_enabled: false
server:
http_listen_port: 3100
http_listen_address: 0.0.0.0
grpc_listen_port: 9096
common:
instance_addr: 127.0.0.1
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
storage_config:
tsdb_shipper:
active_index_directory: /loki/tsdb-index
cache_location: /loki/tsdb-cache
limits_config:
retention_period: 744h # 31 days
compactor:
working_directory: /loki/compactor
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
retention_delete_worker_count: 150
delete_request_store: filesystem

View file

@ -20,7 +20,7 @@ spec:
runAsUser: 10001
containers:
- name: loki
image: grafana/loki:3.6.5
image: grafana/loki
args:
- -config.file=/etc/loki/loki-config.yaml
ports:

View file

@ -15,7 +15,7 @@ spec:
spec:
containers:
- name: miniflux
image: registry.ops.eblu.me/blumeops/miniflux:v2.2.17-a72a0d8
image: registry.ops.eblu.me/blumeops/miniflux
ports:
- containerPort: 8080
env:

View file

@ -7,3 +7,7 @@ resources:
- deployment.yaml
- service.yaml
- ingress-tailscale.yaml
images:
- name: registry.ops.eblu.me/blumeops/miniflux
newTag: v2.2.17-a72a0d8

View file

@ -1,10 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: mosquitto-config
namespace: mqtt
data:
mosquitto.conf: |
listener 1883
allow_anonymous true
persistence false

View file

@ -16,7 +16,7 @@ spec:
spec:
containers:
- name: mosquitto
image: eclipse-mosquitto:2.0.22
image: eclipse-mosquitto
ports:
- containerPort: 1883
name: mqtt

View file

@ -3,6 +3,12 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: mqtt
resources:
- configmap.yaml
- deployment.yaml
- service.yaml
images:
- name: eclipse-mosquitto
newTag: "2.0.22"
configMapGenerator:
- name: mosquitto-config
files:
- mosquitto.conf

View file

@ -0,0 +1,3 @@
listener 1883
allow_anonymous true
persistence false

View file

@ -20,7 +20,7 @@ spec:
fsGroup: 1000
containers:
- name: navidrome
image: registry.ops.eblu.me/blumeops/navidrome:v0.60.3-a72a0d8
image: registry.ops.eblu.me/blumeops/navidrome
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false

View file

@ -9,3 +9,6 @@ resources:
- deployment.yaml
- service.yaml
- ingress-tailscale.yaml
images:
- name: registry.ops.eblu.me/blumeops/navidrome
newTag: v0.60.3-a72a0d8

View file

@ -1,13 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: ntfy-config
namespace: ntfy
data:
server.yml: |
base-url: https://ntfy.ops.eblu.me
upstream-base-url: https://ntfy.sh
attachment-cache-dir: /var/cache/ntfy/attachments
attachment-total-size-limit: 1G
attachment-file-size-limit: 10M
attachment-expiry-duration: 24h

View file

@ -16,7 +16,7 @@ spec:
spec:
containers:
- name: ntfy
image: registry.ops.eblu.me/blumeops/ntfy:v2.17.0-ffa8727-nix
image: registry.ops.eblu.me/blumeops/ntfy
args: ["serve", "--config", "/etc/ntfy/server.yml"]
ports:
- containerPort: 80

View file

@ -3,7 +3,13 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: ntfy
resources:
- configmap.yaml
- deployment.yaml
- service.yaml
- ingress-tailscale.yaml
images:
- name: registry.ops.eblu.me/blumeops/ntfy
newTag: v2.17.0-ffa8727-nix
configMapGenerator:
- name: ntfy-config
files:
- server.yml

View file

@ -0,0 +1,6 @@
base-url: https://ntfy.ops.eblu.me
upstream-base-url: https://ntfy.sh
attachment-cache-dir: /var/cache/ntfy/attachments
attachment-total-size-limit: 1G
attachment-file-size-limit: 10M
attachment-expiry-duration: 24h

View file

@ -22,7 +22,7 @@ spec:
priorityClassName: system-node-critical
containers:
- name: nvidia-device-plugin
image: nvcr.io/nvidia/k8s-device-plugin:v0.18.2
image: nvcr.io/nvidia/k8s-device-plugin
args:
- --device-id-strategy=index
env:

View file

@ -0,0 +1,12 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: nvidia-device-plugin
resources:
- daemonset.yaml
- runtime-class.yaml
images:
- name: nvcr.io/nvidia/k8s-device-plugin
newTag: v0.18.2

View file

@ -1,53 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-config
namespace: monitoring
data:
prometheus.yml: |
global:
scrape_interval: 15s
evaluation_interval: 15s
# Indri system metrics are pushed via Alloy remote_write
# K8s services are scraped directly
scrape_configs:
# Sifaka NAS exporters (via Caddy L4 TCP proxy on indri)
- job_name: "node-exporter-sifaka"
static_configs:
- targets: ["nas.ops.eblu.me:9100"]
- job_name: "smartctl-sifaka"
scrape_interval: 60s
static_configs:
- targets: ["nas.ops.eblu.me:9633"]
# CNPG PostgreSQL metrics (k8s internal)
- job_name: "cnpg-postgres"
static_configs:
- targets: ["blumeops-pg-metrics-tailscale.databases.svc.cluster.local:9187"]
labels:
instance: "blumeops-pg"
# Prometheus self-monitoring
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
# Loki metrics
- job_name: "loki"
static_configs:
- targets: ["loki.monitoring.svc.cluster.local:3100"]
# Kubernetes state metrics (pods, deployments, resource usage, etc.)
- job_name: "kube-state-metrics"
static_configs:
- targets: ["kube-state-metrics.monitoring.svc.cluster.local:8080"]
# Frigate NVR metrics (via Caddy on indri — Frigate runs on ringtail)
- job_name: "frigate"
scheme: https
static_configs:
- targets: ["nvr.ops.eblu.me"]
metrics_path: /api/metrics

View file

@ -4,7 +4,15 @@ kind: Kustomization
namespace: monitoring
resources:
- configmap.yaml
- statefulset.yaml
- service.yaml
- ingress-tailscale.yaml
images:
- name: registry.ops.eblu.me/blumeops/prometheus
newTag: v3.9.1-2ba5d8a
configMapGenerator:
- name: prometheus-config
files:
- prometheus.yml

View file

@ -0,0 +1,46 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
# Indri system metrics are pushed via Alloy remote_write
# K8s services are scraped directly
scrape_configs:
# Sifaka NAS exporters (via Caddy L4 TCP proxy on indri)
- job_name: "node-exporter-sifaka"
static_configs:
- targets: ["nas.ops.eblu.me:9100"]
- job_name: "smartctl-sifaka"
scrape_interval: 60s
static_configs:
- targets: ["nas.ops.eblu.me:9633"]
# CNPG PostgreSQL metrics (k8s internal)
- job_name: "cnpg-postgres"
static_configs:
- targets: ["blumeops-pg-metrics-tailscale.databases.svc.cluster.local:9187"]
labels:
instance: "blumeops-pg"
# Prometheus self-monitoring
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
# Loki metrics
- job_name: "loki"
static_configs:
- targets: ["loki.monitoring.svc.cluster.local:3100"]
# Kubernetes state metrics (pods, deployments, resource usage, etc.)
- job_name: "kube-state-metrics"
static_configs:
- targets: ["kube-state-metrics.monitoring.svc.cluster.local:8080"]
# Frigate NVR metrics (via Caddy on indri — Frigate runs on ringtail)
- job_name: "frigate"
scheme: https
static_configs:
- targets: ["nvr.ops.eblu.me"]
metrics_path: /api/metrics

View file

@ -20,7 +20,7 @@ spec:
runAsUser: 65534
containers:
- name: prometheus
image: registry.ops.eblu.me/blumeops/prometheus:v3.9.1-2ba5d8a
image: registry.ops.eblu.me/blumeops/prometheus
args:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus

View file

@ -8,3 +8,7 @@ resources:
- operator.yaml
- proxyclass.yaml
- dnsconfig.yaml
images:
- name: docker.io/tailscale/k8s-operator
newTag: v1.94.2

View file

@ -5362,7 +5362,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.uid
image: docker.io/tailscale/k8s-operator:v1.94.2
image: docker.io/tailscale/k8s-operator
imagePullPolicy: Always
name: operator
volumeMounts:

View file

@ -15,7 +15,7 @@ spec:
spec:
containers:
- name: teslamate
image: registry.ops.eblu.me/blumeops/teslamate:v2.2.0-ffa8727
image: registry.ops.eblu.me/blumeops/teslamate
ports:
- containerPort: 4000
env:

View file

@ -9,3 +9,7 @@ resources:
- ingress-tailscale.yaml
- external-secret-db.yaml
- external-secret-encryption-key.yaml
images:
- name: registry.ops.eblu.me/blumeops/teslamate
newTag: v2.2.0-ffa8727

View file

@ -16,7 +16,7 @@ spec:
spec:
containers:
- name: transmission
image: registry.ops.eblu.me/blumeops/transmission:v4.0.6-r4-ffa8727
image: registry.ops.eblu.me/blumeops/transmission
env:
- name: PUID
value: "1000"

View file

@ -8,3 +8,6 @@ resources:
- deployment.yaml
- service.yaml
- ingress-tailscale.yaml
images:
- name: registry.ops.eblu.me/blumeops/transmission
newTag: v4.0.6-r4-ffa8727