Compare commits

185 Commits
Author SHA1 Message Date
azykov@mail.ru d66fef8f2b jitsi fixed passwords 2026-11-04 14:25:37 +03:00
azykov@mail.ru 9865f5b386 dedicated metallb pool 10.0.64.9 for jitsi 2026-11-04 13:59:34 +03:00
azykov@mail.ru 5ccf1ffffa added public ip to jitsui 2026-11-04 13:02:23 +03:00
azykov@mail.ru 6ca5c7b396 jitsi server 2026-11-04 12:59:48 +03:00
azykov@mail.ru 73e25266df generous retries for ntfy and map_monitor 2026-10-26 14:41:05 +03:00
azykov@mail.ru 0050dbc3af map_monitor 2026-10-26 14:36:42 +03:00
azykov@mail.ru 9505761c9b ntfy acl fix 2026-10-26 12:45:02 +03:00
azykov@mail.ru 224fffe7d2 added ha user to ntfy 2026-10-26 11:50:48 +03:00
azykov@mail.ru 815e29ed16 fixed ntfy NTFY_AUTH_ACCESS error 2026-10-26 11:30:44 +03:00
azykov@mail.ru 2ae3c26880 ntfy image name typo fixed 2026-10-26 11:20:10 +03:00
azykov@mail.ru c5db75828a ntfy users via env+secret 2026-10-26 11:15:14 +03:00
azykov@mail.ru 66a5d976bf ntfy bump to 2.34.0 2026-10-26 11:14:52 +03:00
azykov@mail.ru a1a9dfdcf4 ntfy 2026-10-26 10:53:42 +03:00
azykov@mail.ru 546212679f replaced it-tools with next-tools fork 2026-10-20 14:06:58 +03:00
azykov@mail.ru 8cf2b66fa1 it-tools app 2026-10-20 13:53:12 +03:00
azykov@mail.ru a27119e04b fixed homeassistant manifest error 2026-10-20 12:21:11 +03:00
azykov@mail.ru 51cf68683d moved all apps from authelia to tinyauth 2026-10-20 12:20:39 +03:00
azykov@mail.ru e0e4232bad longhorn-frontend switched to tinyauth 2026-10-20 12:09:11 +03:00
azykov@mail.ru fce161e13e tinyauth oidc clients and keys 2026-10-20 12:05:05 +03:00
azykov@mail.ru e9b0ae25c7 tinyauth background image replaced 2026-10-20 11:11:58 +03:00
azykov@mail.ru a243ccccdb tinyauth 2026-10-20 10:20:11 +03:00
azykov@mail.ru a29ecfdfa6 make HA claim fix on empty cluster also 2026-10-20 08:51:10 +03:00
azykov@mail.ru dd9c364860 HA pvc name fix 2026-10-20 08:45:23 +03:00
azykov@mail.ru 57ed781366 media dir for HA 2026-10-19 21:12:44 +03:00
azykov@mail.ru b992f47264 longhorn staleReplicaTimeout 2880 2026-10-13 18:06:46 +03:00
azykov@mail.ru d909f3e317 custom longhorn storage class 2026-10-13 14:58:00 +03:00
azykov@mail.ru 406678fdc2 gitea ALLOWED_HOST_LIST to ci.aggtaa.com 2026-10-08 11:16:48 +03:00
azykov@mail.ru dedba652d6 zigbee2mqtt experimental_event_entities:true flag in config map 2026-10-07 22:54:25 +03:00
azykov@mail.ru 1e0908ee90 woodpecker grpc key and persistent agent id 2026-10-07 11:40:34 +03:00
azykov@mail.ru ecf03f2b01 woodpecker automountServiceAccountToken and bump to 3.18.1 2026-10-07 11:31:00 +03:00
azykov@mail.ru 44b33f8030 woodpecker errors fixed 2026-10-07 11:26:41 +03:00
azykov@mail.ru a31372b953 woodpecker ci 2026-10-07 11:23:22 +03:00
azykov@mail.ru b8c581eab8 dev.aggtaa.com/m path 2026-09-29 16:22:33 +03:00
azykov@mail.ru 1af1a3527d dev.aggtaa.com/api/admin path auth, prefix stip 2026-09-29 13:17:55 +03:00
azykov@mail.ru 954a0b4000 dev.aggtaa.com, /, /api, /m to 10.0.64.157 2026-09-29 12:50:05 +03:00
azykov@mail.ru 8494263a3f 1 year retries for NAS mounts on webdav 2026-09-28 10:29:49 +03:00
azykov@mail.ru 050f86e503 webdav HostToContainer mountPropagation 2026-09-28 10:19:10 +03:00
azykov@mail.ru 06d92a034f soft nfs mounts for NAS shares 2026-09-18 10:59:04 +03:00
azykov@mail.ru 60fd88b9c1 authelia image name fix 2026-09-14 22:02:44 +03:00
azykov@mail.ru cbbe199633 longhorn generous readiness probe 5s timeout 2026-09-07 09:46:09 +03:00
azykov@mail.ru a10942bb3c gitea ssh address changed to git-ssh.aggtaa.com. flux points to the new in-cluster gitea 2026-09-07 09:36:09 +03:00
azykov@mail.ru 454922b475 gitea port 2222 2026-09-05 22:31:49 +03:00
azykov@mail.ru 4623c7c9f7 gitea config path fix 2026-09-05 16:03:44 +03:00
azykov@mail.ru f2045463c4 gitea ready to start, replicas = 1 2026-09-05 15:52:51 +03:00
azykov@mail.ru 891727b487 gitea 2026-09-05 14:40:02 +03:00
azykov@mail.ru 67284c4d32 openwebui removed 2026-08-28 08:41:30 +03:00
azykov@mail.ru 852ca6c8f3 empty joplin 2026-08-27 20:05:27 +03:00
azykov@mail.ru 489439d330 joplin 2026-08-27 19:37:51 +03:00
azykov@mail.ru 5cabacee84 separate keys per app 2026-08-25 21:25:40 +03:00
azykov@mail.ru 07fd7110e9 fix for photo-stats remnants not cleaned up 2026-08-25 16:53:58 +03:00
azykov@mail.ru 2d78870b94 more generous liveness probe for metallb 2026-08-25 12:18:56 +03:00
azykov@mail.ru a6c72aa3bc cpu & memory limits for st-backup-proxy 2026-08-25 12:18:33 +03:00
azykov@mail.ru 3b833cc575 immich data-old folder deleted 2026-08-24 10:03:11 +03:00
azykov@mail.ru 6600fa660e immich app is back on after folder split 2026-08-24 09:51:28 +03:00
azykov@mail.ru f59b01e290 prepare immich for data volume split 2026-08-23 21:13:49 +03:00
azykov@mail.ru 1df90d3a6e md-vault-mcd longer startup probe 2026-08-23 17:52:22 +03:00
azykov@mail.ru 90014b22c1 fix for authelia and md-vault-mcp image versions 2026-08-23 17:45:35 +03:00
azykov@mail.ru cc7eabfedc fix for mosquitto and fava image versions 2026-08-23 17:39:19 +03:00
azykov@mail.ru cb86b45293 apps pinned to exact version numbers 2026-08-23 15:25:16 +03:00
azykov@mail.ru 9075f52f90 bump immich to 3.1.0 2026-08-23 12:02:56 +03:00
azykov@mail.ru 8bce8e2b63 immich moved to photo.aggtaa.com 2026-08-23 10:21:52 +03:00
azykov@mail.ru 1fdae9c55f immich temporarily moved to photo2.aggtaa.com. photo-stats removed 2026-08-23 10:08:33 +03:00
azykov@mail.ru c186e2b765 removed rsync & rsync-web from immich-server 2026-08-23 09:49:20 +03:00
azykov@mail.ru 82deffb39f pin immich ver to 2.7.5 2026-08-23 09:30:05 +03:00
azykov@mail.ru a26528a00c immich data pv bump to 200gb 2026-08-23 07:21:51 +03:00
azykov@mail.ru 805711e98e removed nfs-photo 2026-08-22 11:46:01 +03:00
azykov@mail.ru 25e74fc0cb immich mergerfs 2026-08-22 09:55:19 +03:00
azykov@mail.ru 87cc20df72 immich volume 100 to 800 gb size increase 2026-08-21 23:30:31 +03:00
azykov@mail.ru 09eefb20ae rsync from nas for immich 2026-08-21 21:39:14 +03:00
azykov@mail.ru b889c3a2f6 immich-server longer stratup probe 2026-08-21 15:42:39 +03:00
azykov@mail.ru 5f2b951480 immich-postgres fix 2026-08-21 15:37:25 +03:00
azykov@mail.ru 6a94f95c1c immich 2026-08-21 15:23:58 +03:00
azykov@mail.ru 85f758b76c category.search=newest for photo mergedfs 2026-08-18 08:18:51 +03:00
azykov@mail.ru e0bf2c08cf re-export of base and sub1 photo dirs via webdav 2026-08-18 02:09:34 +03:00
azykov@mail.ru ec6d48adb5 cluster ip for nfs-photo 2026-08-18 01:54:34 +03:00
azykov@mail.ru 6729a0bdd0 photo-stats now count to year ago exactly 2026-08-16 21:09:01 +03:00
azykov@mail.ru 42937adbac photo-stats 2026-08-16 20:48:51 +03:00
azykov@mail.ru 592f295e64 removed redundand namespace reference 2026-08-16 20:44:17 +03:00
azykov@mail.ru ce67fce3c2 fix for nfs-photo not exporting root dir 2026-08-16 20:42:10 +03:00
azykov@mail.ru 8600e786c6 fix for nfs-photo mergefs start before ganesha 2026-08-16 07:36:46 +03:00
azykov@mail.ru b87970e2d1 nfs-photo 2026-08-15 17:27:07 +03:00
azykov@mail.ru b7afe1f1ab webdav generous helm timeouts and retries 2026-08-15 09:45:48 +03:00
azykov@mail.ru 9b44c60723 bump longhorn to 1.12.1 2026-08-15 07:47:24 +03:00
azykov@mail.ru 22a9385dc8 dir changes to all apps that use ganesha shares 2026-08-13 20:45:07 +03:00
azykov@mail.ru 65c50d3bdd sync webdav with ganesha folder renamed 2026-08-13 20:26:08 +03:00
azykov@mail.ru cd647a619b ganesha share renamed 2026-08-13 20:20:26 +03:00
azykov@mail.ru 2ba8356bdb webdav shares renamed 2026-08-13 20:17:42 +03:00
azykov@mail.ru 0e4802c611 webdav folder shares restructured 2026-08-13 14:24:30 +03:00
azykov@mail.ru 5403c9d820 fix for NAS folder paths in webdav 2026-08-13 11:03:26 +03:00
azykov@mail.ru 2c8068dc42 NAS 10.0.64.80 exposed at dav.aggtaa.com 2026-08-13 10:52:05 +03:00
azykov@mail.ru ff17a67ea1 longhorn backup schedule 2026-08-11 22:29:15 +03:00
azykov@mail.ru f6077bbe44 longhorn default backup target 2026-08-11 22:24:41 +03:00
azykov@mail.ru 555a84fa28 longhorn backup target 2026-08-11 22:17:47 +03:00
azykov@mail.ru be53f040c7 rclone s3 proxy 2026-08-11 21:34:21 +03:00
azykov@mail.ru 3a0a00f994 synthing 2026-08-11 10:17:59 +03:00
azykov@mail.ru 56bf723968 radicale and agendav removed. infcloud at c.aggtaa.com 2026-08-10 11:06:34 +03:00
azykov@mail.ru c30a03cdc5 infcloud added. davical additional ingress at c2.aggtaa.com 2026-08-10 10:46:50 +03:00
azykov@mail.ru d4e0b37ce1 agendav switched to davical 2026-08-09 21:26:20 +03:00
azykov@mail.ru c8425bf0f2 fix for davical /server url path 2026-08-09 20:59:43 +03:00
azykov@mail.ru 6b966107d1 moved radicale to c.aggtaa.com/server2 and davical to c.aggtaa.com/server 2026-08-09 20:48:17 +03:00
azykov@mail.ru dc00fd2467 davical readiness probe 2026-08-09 08:20:36 +03:00
azykov@mail.ru 29984ca533 davical initcontainer 2026-08-09 08:00:28 +03:00
azykov@mail.ru 7e19f3cef7 davical 2026-08-08 19:39:02 +03:00
azykov@mail.ru 625d43b033 switch flux from main to prod 2026-08-08 19:28:34 +03:00
azykov@mail.ru fc14ffb02f bump openwebui to v0.11.0 2026-08-03 21:53:01 +03:00
azykov@mail.ru 4f0aa341e0 --poll-watcher for fava, removed actimeo from its pv 2026-08-01 23:12:01 +03:00
azykov@mail.ru 55e958673a noac to actimeo for fava 2026-08-01 22:41:46 +03:00
azykov@mail.ru 0724da6d35 fava nfs noac option 2026-08-01 22:24:10 +03:00
azykov@mail.ru 0c0927d63a driftDetection mode enabled for all apps 2026-08-01 20:04:51 +03:00
azykov@mail.ru 463a5608ff implicit consent for all internal oidc applications 2026-08-01 19:28:46 +03:00
azykov@mail.ru 765cb3a157 nfs-ganesha explicit chmod /export 2026-07-31 13:30:08 +03:00
azykov@mail.ru f34f9c9b50 fix for markdown-vault-mcp PVC 2026-07-31 12:58:33 +03:00
azykov@mail.ru 1070cd1be7 webdav depends on nfs, not mcp 2026-07-31 12:50:21 +03:00
azykov@mail.ru 04c88aabf1 fava files fix 2026-07-31 12:48:11 +03:00
azykov@mail.ru 7fed7d4d60 allow all file extensions in markdown-vault-mcp 2026-07-31 12:19:37 +03:00
azykov@mail.ru 4cb79c0e35 fava accounting viewer 2026-07-31 12:06:50 +03:00
azykov@mail.ru 299907cb6a webdav set to nfs root 2026-07-31 11:56:31 +03:00
azykov@mail.ru 0cd0dee6eb authelia config fix 2026-07-31 11:43:30 +03:00
azykov@mail.ru 095834ef77 longer authelia oidc keys 2026-07-31 11:38:38 +03:00
azykov@mail.ru 8b327ab538 markdown-vault adaptation to nfs-ganesha 2026-07-31 11:20:25 +03:00
azykov@mail.ru 4203b66402 webdav adaptation to nfs-ganesha 2026-07-31 11:13:43 +03:00
azykov@mail.ru b85207202a yet another metallb nfs pool fix 2026-07-31 09:03:45 +03:00
azykov@mail.ru 70401e78b5 another metallb nfs pool fix 2026-07-31 09:02:09 +03:00
azykov@mail.ru b85c86502b metallb nfs-pool fix 2026-07-31 08:55:57 +03:00
azykov@mail.ru 9e30326e9f nfs-ganesha 2026-07-31 08:51:16 +03:00
azykov@mail.ru 30f3696bd6 fix webdav manifest subpath field 2026-07-27 22:33:57 +03:00
azykov@mail.ru aeba7287de webdav 2026-07-27 14:11:26 +03:00
azykov@mail.ru 709da47a9f authelia audience for markdown-vault-mcp 2026-07-27 10:00:31 +03:00
azykov@mail.ru 67030be4b7 markdown-vault-mcp to authelia oidc 2026-07-27 09:44:07 +03:00
azykov@mail.ru f453de2842 authelia secret reencrypted due to MAC error 2026-07-26 22:38:40 +03:00
azykov@mail.ru 79f9c37c83 markdown-vault-mcp: point STATE_PATH at a file, not the PVC mount dir
Fixes IsADirectoryError on startup when the app tried to atomically
write its change-tracking state to /data/state itself.
2026-07-26 21:02:36 +03:00
azykov@mail.ru 50bbce8474 longhorn.aggtaa.com 2026-07-26 20:56:13 +03:00
azykov@mail.ru 4f75b37427 markdown-vault-mcp 2026-07-26 20:43:06 +03:00
azykov@mail.ru 61a99ffe88 openwebui:main 2026-07-26 20:05:11 +03:00
azykov@mail.ru 1205f71df5 openwebui:main-slim 2026-07-26 19:08:10 +03:00
azykov@mail.ru 5ec8726f67 openwebui 2026-07-26 18:33:24 +03:00
azykov@mail.ru f8eb00b1f3 give HA 5 minutes startup probe 2026-07-25 16:16:46 +03:00
azykov@mail.ru 1eab1d4b9d bump vw to 1.37.0 2026-07-25 16:03:41 +03:00
azykov@mail.ru 584327251e switched to another git 2026-07-25 15:49:38 +03:00
azykov@mail.ru 152ff0bc1f homeassistant 2026-07-25 08:46:46 +03:00
azykov@mail.ru fb12548ff6 zigbee2mqtt hidden behind authelia 2026-07-25 07:49:41 +03:00
azykov@mail.ru fd4f6e34a5 radicale-ui 2026-07-23 21:46:34 +03:00
azykov@mail.ru 572f42e278 radicale at /server web path 2026-07-23 17:20:43 +03:00
azykov@mail.ru 7739e3501a mosquitto 2026-07-21 22:13:15 +03:00
azykov@mail.ru cf2c016c0a faster flux: requeue-dependency=5s 2026-07-21 21:58:18 +03:00
azykov@mail.ru e46545f215 metallb 2026-07-21 21:49:41 +03:00
azykov@mail.ru 31e83a2e3f retryInterval 5s for everything 2026-07-21 21:40:16 +03:00
azykov@mail.ru 0b0f162bb9 typo fix 2026-07-21 20:47:40 +03:00
azykov@mail.ru 9e02835121 authelia 2026-07-21 20:44:04 +03:00
azykov@mail.ru 46afe4f522 vaultwarden meta.name changed 2026-07-21 20:32:46 +03:00
azykov@mail.ru 1ab5f07780 radicale meta.name changed 2026-07-21 20:30:04 +03:00
azykov@mail.ru 19fc6983ce radicale fix reverted. 2026-07-21 19:50:10 +03:00
azykov@mail.ru 080129db9c radicale manifest refactoring 2026-07-21 19:07:51 +03:00
azykov@mail.ru c197a63d13 radicale rwx pvc 2026-07-21 15:18:26 +03:00
azykov@mail.ru 35d36fb14b fixed directories and names 2026-07-21 13:10:40 +03:00
azykov@mail.ru c4529d9d90 typo fix 2026-07-21 13:04:14 +03:00
azykov@mail.ru d9a8f5d59d bump longhorn to 1.12.x 2026-07-21 13:02:13 +03:00
azykov@mail.ru 5c32ef2476 fix cert-manager depends on infra 2026-07-21 12:52:50 +03:00
azykov@mail.ru 1fddd776b5 typo fix 2026-07-21 12:51:26 +03:00
azykov@mail.ru 2a0a0acc71 folder structure. longhorn installation 2026-07-21 12:49:13 +03:00
azykov@mail.ru fe3e885b33 longhorn-retain -> longhorn, longhorn -> longhorn-no-retain 2026-07-21 12:04:43 +03:00
azykov@mail.ru d0a98c77a3 typo fix 2026-07-21 09:17:32 +03:00
azykov@mail.ru 417208ee97 local-path patch 2026-07-21 09:15:08 +03:00
azykov@mail.ru 6884505ef1 switch to new longhorn-retain storageclass 2026-07-20 22:49:10 +03:00
azykov@mail.ru d97c36f42f radicale namespace changed to pim 2026-07-20 22:19:48 +03:00
azykov@mail.ru 47e4ae6d82 retain: true for apps 2026-07-20 21:23:41 +03:00
azykov@mail.ru 8de86e0318 data-migate ns fix 2026-07-20 21:23:28 +03:00
azykov@mail.ru dea1346094 data-migrate utility 2026-07-20 20:57:55 +03:00
azykov@mail.ru 2989700647 kustomization per app 2026-07-20 19:17:08 +03:00
azykov@mail.ru 5b36590c04 radicale 2026-07-20 18:27:54 +03:00
azykov@mail.ru 75db76b185 zigbee2mqtt 2026-07-19 12:55:16 +03:00
azykov@mail.ru 57296b2d34 10m reconciliation interval for everything 2026-07-19 10:38:31 +03:00
azykov@mail.ru 7cf2f0b2e4 added decryption to all layers 2026-07-19 00:28:04 +03:00
azykov@mail.ru 6370c63629 prod letsencrypt certificates 2026-07-18 23:49:21 +03:00
azykov@mail.ru d30eb48264 cert-manager-issuers 2026-07-18 23:46:38 +03:00
azykov@mail.ru 5fdb19a553 vaultwarden 2026-07-18 22:17:58 +03:00
azykov@mail.ru ef3d2a8f05 fix vw.yaml ref path 2026-07-18 21:51:32 +03:00
azykov@mail.ru 08c9965d2f path fix 2026-07-18 21:47:51 +03:00
azykov@mail.ru 6b24b82749 vw app yaml fix 2026-07-18 21:42:00 +03:00
azykov@mail.ru 5a32cf5620 empty vaultwarden yaml 2026-07-18 21:37:50 +03:00
azykov@mail.ru c96e3a891f fix duplicate name 2026-07-18 21:35:31 +03:00
azykov@mail.ru fda2e9cebb cluster structure 2026-07-18 17:24:06 +03:00
azykov@mail.ru af587c6e45 sops age key 2026-07-18 15:49:08 +03:00
FluxandFlux bb558db08f Add Flux sync manifests 2026-07-18 15:26:20 +03:00
FluxandFlux ae22af0434 Add Flux v2.9.2 component manifests 2026-07-18 15:26:12 +03:00
197 changed files with 13336 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
migration-from-old-files/
migration-from-docker/
.decrypted~secret.enc.yaml
gitea-backup/
*.agekey
+52
View File
@@ -0,0 +1,52 @@
creation_rules:
- path_regex: authelia.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1ncw6uagcgncd5m2admqxt73ey0mcd99jsw9sa9lcdv3gu70mcerql8ya0k
- path_regex: davical.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1yx8mey02c0mu6te9hwxwdqp5n4enjz35e8thchsmvhqw7xsrzy0syg367g
- path_regex: immich.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1wmm8qrqnjzvd5nkraed6ylrsxlpn3ru7d7zettdudj7y9sg9dd2su0u3mr
- path_regex: markdown-vault-mcp.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1kct5g7d7eus2hvdnmx99ewsrtz70lhlasxa25euqwdctqhwqqpxqz0n7gg
- path_regex: mosquitto.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1u236u5mzkzj72y4mfxx68gfqw0u0j6tdgkudnm5pdfpe7apq4u6s89jr2h
- path_regex: vaultwarden.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age109zta6l0cwpkxn5wek82kvauxn6yqrq80rsz4mvu5atdnpcn9pusuvnqk5
- path_regex: webdav.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1y8t9v56yuvskznddqfsd4yddzzch0yf2dutvvt8rrx6rjh5f9g0s3qeays
- path_regex: zigbee2mqtt.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1tu4xgfefvgdda0vn2thaqndq4p9tuydq2ud6qa98tzzgnff6749qnlcx29
- path_regex: longhorn-system.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age18hhmdmupj90zjgjpxk3z5cf3lx36hcznnmqsp0fpyg50823pudrqjupydt
- path_regex: s3-backup-proxy.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age15vc3geq4lsec69rnxczluvwgcexeqae02zxzjpswsuatthaasu7qzath48
- path_regex: joplin.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1dertyl956fjsmh00cw468gdnl7lf3frasg4n4c82z0chm6qm2cks6w8gha
- path_regex: gitea.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1ktlxm0hukw6jxm62txqy5x2nl8hpdnal8nv00kfhrf3pw33574aqa6j5cj
- path_regex: woodpecker.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age13qwkssk8esashln2j3gfv6e9csashr4vddkdav5v38hm5wejl36qd2ew9s
- path_regex: tinyauth.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1rvyt7nlnjqeyt6ykwh9fj9d88kjmq9vetcx667ul3ql6fyfz4gqqh9lq9q
- path_regex: ntfy.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1uzs33ep6k3htnuwyyp54y373wkf7urd0qvk5kxcmptr5tt0fffus4v06gn
- path_regex: map_monitor.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1k4j5vyqqt7w7k52gvkpfe6znqr9mlgt076l7xgdtyc75lamsh4aqshj5qs
- path_regex: jitsi.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1zmr32r5mcgyh73asvzl8l690pzwqsx9hle27u3cydngsgv56wykq3v67px
@@ -0,0 +1,70 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: authelia
namespace: auth
spec:
interval: 10m
chartRef:
kind: OCIRepository
name: authelia
driftDetection:
mode: enabled
values:
controllers:
auth:
containers:
app:
image:
repository: authelia/authelia
tag: 4.39.21
probes:
liveness:
enabled: true
readiness:
enabled: true
service:
app:
controller: auth
ports:
http:
port: 9091
ingress:
app:
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: auth.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: auth-tls
hosts:
- auth.aggtaa.com
persistence:
config:
type: secret
name: authelia-secrets
globalMounts:
- path: /config/configuration.yml
subPath: configuration.yml
readOnly: true
- path: /config/users_database.yml
subPath: users_database.yml
readOnly: true
db:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 1Gi
storageClass: longhorn
retain: true
globalMounts:
- path: /db
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: authelia
namespace: auth
spec:
interval: 1h
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- helmrelease.yaml
- middleware.yaml
- secret.enc.yaml
@@ -0,0 +1,14 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: authelia
namespace: auth
spec:
forwardAuth:
address: http://authelia.auth.svc.cluster.local:9091/api/verify?rd=https://auth.aggtaa.com
trustForwardHeader: true
authResponseHeaders:
- Remote-User
- Remote-Groups
- Remote-Name
- Remote-Email
File diff suppressed because one or more lines are too long
@@ -0,0 +1,118 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: davical
namespace: pim
spec:
interval: 10m
dependsOn:
- name: davical-postgres
chartRef:
kind: OCIRepository
name: davical
driftDetection:
mode: enabled
values:
controllers:
app:
initContainers:
seed-config:
image:
repository: tuxnvape/davical-standalone
tag: "82-1.1.12"
command:
- sh
- -c
- |
if [ ! -f /mnt/config-pvc/davical.php ]; then
cp -r /config/. /mnt/config-pvc/
fi
# Apache's SCRIPT_NAME (and DAViCal's base_url derived from it)
# only reflects the /server prefix if Apache itself resolves
# that prefix - Traefik forwards the path unstripped. Add an
# Alias so /server/* maps to the app root; idempotent so it
# self-heals if apache.conf predates this fix.
if ! grep -q '^\s*Alias /server ' /mnt/config-pvc/apache.conf; then
sed -i '/^\s*DocumentRoot \/usr\/share\/davical\/htdocs/a\ Alias /server /usr/share/davical/htdocs' /mnt/config-pvc/apache.conf
fi
containers:
app:
image:
repository: tuxnvape/davical-standalone
tag: "82-1.1.12"
env:
TZ: Europe/Moscow
HOST_NAME: c.aggtaa.com
DBHOST: davical-postgres
UPDATE_FROM_OLD: "false"
PGSQL_ROOT_PASS:
valueFrom:
secretKeyRef:
name: davical-secrets
key: PGSQL_ROOT_PASS
PASSDAVDB:
valueFrom:
secretKeyRef:
name: davical-secrets
key: PASSDAVDB
ADMINDAVICALPASS:
valueFrom:
secretKeyRef:
name: davical-secrets
key: ADMINDAVICALPASS
probes:
liveness:
enabled: true
readiness:
enabled: true
startup:
enabled: true
spec:
periodSeconds: 5
failureThreshold: 30
timeoutSeconds: 3
service:
app:
controller: app
ports:
http:
port: 80
ingress:
app:
className: traefik
# no cert-manager annotation here: infcloud's own Ingress already
# requests/owns the Certificate for c.aggtaa.com -> radicale-tls.
# A second Ingress with the annotation would create a competing
# Certificate for the same host/secret.
#
# No stripPrefix middleware either: Apache needs to see the /server
# prefix itself (via the Alias added in the seed-config initContainer)
# so that DAViCal's self-referential links come out correct.
hosts:
- host: c.aggtaa.com
paths:
- path: /server
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: radicale-tls
hosts:
- c.aggtaa.com
persistence:
config:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 256Mi
storageClass: longhorn
retain: true
advancedMounts:
app:
app:
- path: /config
seed-config:
- path: /mnt/config-pvc
@@ -0,0 +1,45 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: davical-postgres
namespace: pim
spec:
interval: 10m
chartRef:
kind: OCIRepository
name: davical
driftDetection:
mode: enabled
values:
controllers:
postgres:
containers:
app:
image:
repository: postgres
tag: "16.14-alpine"
env:
TZ: Europe/Moscow
POSTGRES_PASSWORD:
valueFrom:
secretKeyRef:
name: davical-secrets
key: PGSQL_ROOT_PASS
service:
app:
controller: postgres
ports:
postgres:
port: 5432
persistence:
data:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 2Gi
storageClass: longhorn
retain: true
globalMounts:
- path: /var/lib/postgresql/data
subPath: data
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: davical
namespace: pim
spec:
interval: 10m
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- secret.enc.yaml
- helmrelease-postgres.yaml
- helmrelease-davical.yaml
@@ -0,0 +1,24 @@
apiVersion: v1
kind: Secret
metadata:
name: davical-secrets
namespace: pim
stringData:
PGSQL_ROOT_PASS: ENC[AES256_GCM,data:XEhP2zDIz0QCpAXXnZi5JtPsVIfns9zp4fvxcg9/bskHiLU5rR+EgU5n/SdOEDkJ,iv:wR3CGk3csHSCSBqFX+ukOUAmS/p+oZMmdNgvCFNHZlI=,tag:ttAe0GuLmwqA77cWLlne2Q==,type:str]
PASSDAVDB: ENC[AES256_GCM,data:d9RazQG7xeIH4g3ovti6HwyiIyLTzyaPjAcQvLmhsrwdgev7i0AJK7yQeRXxu/16,iv:9ZdfWTlOuyUNpV2ycykOgx8SY7Golz7/Lb+gfTa9fW0=,tag:eL1euF9uQJ+nDLsR1kxtvg==,type:str]
ADMINDAVICALPASS: ENC[AES256_GCM,data:1Zq7duPTIfJX2Stp6AEhKcLHkmQB6vWf4y1f0Iju/zI=,iv:PMOyD3O7UzVPRlFmHTOQSAys032iD4CF8JbI51agWcg=,tag:eDgMbg2lRMnl57NCf5iIkw==,type:str]
sops:
age:
- recipient: age1yx8mey02c0mu6te9hwxwdqp5n4enjz35e8thchsmvhqw7xsrzy0syg367g
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjNTVJZEFuRFhGazF6T1Yv
dnhhTHVUb1h4SEN2eEtDUGREUERkR2wrTUVVCktGSkVLZmlYR3BjekhjNmk4L3Q4
UEJsQXJjZXY1VlNDaFV2eER4NTdwRGcKLS0tIDJxb1AyYm01YlNUVk93Tm1Hb0xK
NVh0K25yOFUya0dYSGt2SGdJQURnQ2sKaKgwThB5ePeM091/zYgQXmiPeBeIhTdo
60PkAPNs659xyVqnlZw2YKEhxbEHS3ZMTBgC4tgng0yjClBizcTv5w==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-08-08T16:37:01Z"
mac: ENC[AES256_GCM,data:OoE6fml/3DSH2EMafCoeYz/JHo89Fb9uwQ58k7hxQDU6YMazdO/eGM0jKX5ghjuMKJqA/zQs4FeZ4yASkIUT1CBbxRpycmE9qo6Zny121NLeqveBp3YMjmUZGt3tgw2dNqtkhpOnl/BmHQCtTTvX2brtSVmANx3LbFxjqbK2OGM=,iv:8Us7LJSZ2aHZQnYIjnsvkR7Av0KeHFEYEETZd7WZX4Q=,tag:7NbbEOq1k2c5IcsnqpvlOQ==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.11.0
+104
View File
@@ -0,0 +1,104 @@
# Three Ingresses on one host because middlewares and router priority are
# per-Ingress. Traefik matches pathType Prefix character-wise (not per path
# segment), so public prefixes carry a trailing slash and priorities are
# explicit - by default Traefik orders routers by rule length, which would
# not reliably put /api/admin above /api/.
# 300 dev-admin /api/admin -> Authelia -> api (prefix /api stripped)
# 200 dev-public /api/ /m /m/ -> api / m, public (prefixes stripped)
# 100 dev / -> Authelia -> ui (also bare /api)
# All carry the tls section (otherwise the router is http-only), but only
# "dev" has the cert-manager annotation so a single Certificate owns dev-tls.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dev
namespace: dev
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.priority: "100"
traefik.ingress.kubernetes.io/router.middlewares: auth-tinyauth@kubernetescrd
spec:
ingressClassName: traefik
rules:
- host: dev.aggtaa.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: dev
port:
name: ui
tls:
- secretName: dev-tls
hosts:
- dev.aggtaa.com
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dev-admin
namespace: dev
annotations:
traefik.ingress.kubernetes.io/router.priority: "300"
traefik.ingress.kubernetes.io/router.middlewares: auth-tinyauth@kubernetescrd,dev-strip-prefix@kubernetescrd
spec:
ingressClassName: traefik
rules:
- host: dev.aggtaa.com
http:
paths:
# no trailing slash on purpose: also catches bare /api/admin
# (and /api/administrator...), all of which stay behind Authelia
- path: /api/admin
pathType: Prefix
backend:
service:
name: dev
port:
name: api
tls:
- secretName: dev-tls
hosts:
- dev.aggtaa.com
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dev-public
namespace: dev
annotations:
traefik.ingress.kubernetes.io/router.priority: "200"
traefik.ingress.kubernetes.io/router.middlewares: dev-strip-prefix@kubernetescrd
spec:
ingressClassName: traefik
rules:
- host: dev.aggtaa.com
http:
paths:
- path: /api/
pathType: Prefix
backend:
service:
name: dev
port:
name: api
- path: /m
pathType: Exact
backend:
service:
name: dev
port:
name: m
- path: /m/
pathType: Prefix
backend:
service:
name: dev
port:
name: m
tls:
- secretName: dev-tls
hosts:
- dev.aggtaa.com
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- service.yaml
- middleware.yaml
- ingress.yaml
+13
View File
@@ -0,0 +1,13 @@
# Shared by dev-admin and dev-public: /api/a -> /a, /api/admin/b -> /admin/b,
# /m/main.css -> /main.css. Only the first matching prefix is stripped, and
# none of these routes can match both.
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: strip-prefix
namespace: dev
spec:
stripPrefix:
prefixes:
- /api
- /m
+40
View File
@@ -0,0 +1,40 @@
# dev.aggtaa.com backends live outside the cluster on 10.0.64.157:
# selector-less Service + hand-written EndpointSlice point at them.
apiVersion: v1
kind: Service
metadata:
name: dev
namespace: dev
spec:
ports:
- name: ui
port: 10001
- name: api
port: 10002
- name: m
port: 10003
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: dev-external
namespace: dev
labels:
kubernetes.io/service-name: dev
endpointslice.kubernetes.io/managed-by: flux
addressType: IPv4
ports:
- name: ui
port: 10001
protocol: TCP
- name: api
port: 10002
protocol: TCP
- name: m
port: 10003
protocol: TCP
endpoints:
- addresses:
- 10.0.64.157
conditions:
ready: true
+81
View File
@@ -0,0 +1,81 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: fava
namespace: finance
spec:
interval: 30m
chartRef:
kind: OCIRepository
name: fava
driftDetection:
mode: enabled
values:
controllers:
app:
initContainers:
seed-ledger:
image:
repository: busybox
tag: "1.36"
command:
- sh
- -c
- |
if [ ! -f /documents/main.beancount ]; then
printf '1970-01-01 open Assets:Cash\n1970-01-01 open Equity:Opening-Balances\n' > /documents/main.beancount
fi
containers:
app:
image:
repository: yegle/fava
tag: "v1.30"
env:
TZ: Europe/Moscow
BEANCOUNT_FILE: /documents/main.beancount
args:
- "--poll-watcher"
probes:
liveness:
enabled: true
readiness:
enabled: true
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi
service:
app:
controller: app
ports:
http:
port: 5000
ingress:
app:
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.middlewares: auth-tinyauth@kubernetescrd
hosts:
- host: acc.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: fava-tls
hosts:
- acc.aggtaa.com
persistence:
documents:
type: persistentVolumeClaim
existingClaim: fava-documents
globalMounts:
- path: /documents
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: fava
namespace: finance
spec:
interval: 1h
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- helmrelease.yaml
- pv-pvc-documents.yaml
@@ -0,0 +1,35 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: fava-documents-nfs
spec:
capacity:
storage: 30Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
mountOptions:
- nfsvers=4.1
nfs:
server: 10.0.64.5
path: /export/accounting
claimRef:
namespace: finance
name: fava-documents
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: fava-documents
namespace: finance
annotations:
helm.sh/resource-policy: keep
spec:
accessModes:
- ReadWriteMany
storageClassName: ""
volumeName: fava-documents-nfs
resources:
requests:
storage: 30Gi
+105
View File
@@ -0,0 +1,105 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: gitea
namespace: git
spec:
interval: 10m
chartRef:
kind: OCIRepository
name: gitea
driftDetection:
mode: enabled
values:
controllers:
app:
replicas: 1
containers:
app:
image:
repository: gitea/gitea
tag: "1.27.0"
env:
USER_UID: "1000"
USER_GID: "1000"
GITEA__server__DOMAIN: git.aggtaa.com
GITEA__server__ROOT_URL: https://git.aggtaa.com/
GITEA__server__SSH_DOMAIN: git-ssh.aggtaa.com
GITEA__server__SSH_PORT: "2222"
GITEA__server__LFS_START_SERVER: "true"
GITEA__webhook__ALLOWED_HOST_LIST: ci.aggtaa.com
GITEA__security__INSTALL_LOCK: "true"
GITEA__security__PASSWORD_HASH_ALGO: pbkdf2
GITEA__security__INTERNAL_TOKEN:
valueFrom:
secretKeyRef:
name: gitea-secrets
key: INTERNAL_TOKEN
GITEA__oauth2__JWT_SECRET:
valueFrom:
secretKeyRef:
name: gitea-secrets
key: OAUTH2_JWT_SECRET
GITEA__server__LFS_JWT_SECRET:
valueFrom:
secretKeyRef:
name: gitea-secrets
key: LFS_JWT_SECRET
probes:
startup:
enabled: true
spec:
periodSeconds: 5
failureThreshold: 60
liveness:
enabled: true
readiness:
enabled: true
resources:
requests:
cpu: 50m
memory: 256Mi
limits:
memory: 1Gi
service:
app:
controller: app
ports:
http:
port: 3000
ssh:
controller: app
type: LoadBalancer
labels:
loadBalancerPool: default
ports:
ssh:
port: 2222
targetPort: 22
protocol: TCP
ingress:
app:
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: git.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: gitea-tls
hosts:
- git.aggtaa.com
persistence:
data:
type: persistentVolumeClaim
existingClaim: gitea-data
globalMounts:
- path: /data
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: gitea
namespace: git
spec:
interval: 10m
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- secret.enc.yaml
- pvc.yaml
- helmrelease.yaml
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitea-data
namespace: git
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 3Gi
+24
View File
@@ -0,0 +1,24 @@
apiVersion: v1
kind: Secret
metadata:
name: gitea-secrets
namespace: git
stringData:
INTERNAL_TOKEN: ENC[AES256_GCM,data:+eS1bjyL/NKvOpkXa1Kiyk9dBPemWvfRplJJCXG+cLXipbMlyHBDQLEMeFEnz9VdnwN/MFRWYL5ink3Lh6yNGf0vmVNR+OLMlvlaKDNaCA28HuUBxJbWLAqhITsRImH6rKcy/EfqEEcK,iv:ModTSwxLMj4Gx0e/bVKY8GVWzBjgNxATKnkWYfvKiIs=,tag:6gFkA/0toCBJ7QKL16654A==,type:str]
OAUTH2_JWT_SECRET: ENC[AES256_GCM,data:9626EzmbEmyAsTqfbRhASqLkzS+4xnJxpSEgJDSLzkjfuNfAq7uMfc/HsA==,iv:lMCRwHt4gT31hYZ5Ywt4GU/DJNj7Fya2GOUkyr/l6ro=,tag:aQ8UlVwpaVNDoOHju1oiMQ==,type:str]
LFS_JWT_SECRET: ENC[AES256_GCM,data:gZ37cb9uqSF1MkdoUFbVM3VhqBxEIyUv3IxiV4c3gbIVTk7FMHncZekM7Q==,iv:m7JxF7S1OnHy86s5s1tJHQ8kqhRJq5kFRoArLL/ab0o=,tag:GZuE7f9pNq15T+R+PlSZdA==,type:str]
sops:
age:
- recipient: age1ktlxm0hukw6jxm62txqy5x2nl8hpdnal8nv00kfhrf3pw33574aqa6j5cj
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkRVpZRTVLWU80RjEvZnRs
TU5YVko4V1M5Q0d5SXZGT1h2NStCYk5UZWpRCmNxVXVtR3VCZXJqNHYyaDUyZmdr
WXJKZHhJaFJhZll0VVBld0QrSGhtbncKLS0tIGZZREFXRUMvTi8rRHIyaXBUOE45
d2JBTG5oT2kvZzdLejMzdEpRbm03QmcKItsS4nKJRu3pRC8xLTf9kYpqFiD9S1tx
BFnJf3IsbGpl1RM8RmkN9O5QsZSq1hbw5pFhlx0XKeH5KgoJInbnog==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-09-05T11:29:22Z"
mac: ENC[AES256_GCM,data:906TL61C7WcxxTAfH3zpXLTLQn2WSIg2RPm0y/6LeVA6pvI6fMbFA32Usvg+EkpWVRGqcm8JPgHy63WQDhq1Ap3imJXpgVDgTgfo64TxJLmdUtCAdm0/CaGxtGeU2J3gm653wl75hOGoCyfBR3iOt0Vo1kUdUxS9B5h3u5L6P6o=,iv:C+CZe31F/AWk0ZqM6lgV248ys/L3mG6S8Tz44b5o628=,tag:C9LdYCtsiHWiHSZWaYCGWA==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.11.0
@@ -0,0 +1,85 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: homeassistant
namespace: homeassistant
spec:
interval: 30m
chartRef:
kind: OCIRepository
name: homeassistant
driftDetection:
mode: enabled
values:
controllers:
app:
strategy: Recreate
pod:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
containers:
app:
image:
repository: ghcr.io/home-assistant/home-assistant
tag: "2026.7.4"
env:
TZ: Europe/Moscow
probes:
liveness:
enabled: true
readiness:
enabled: true
startup:
enabled: true
spec:
periodSeconds: 10
failureThreshold: 30 # HA is sloooow to start
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 1Gi
service:
app:
controller: app
ports:
http:
port: 8123
ingress:
app:
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: p.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: homeassistant-tls
hosts:
- p.aggtaa.com
persistence:
config:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 10Gi
storageClass: longhorn
retain: true
globalMounts:
- path: /config
media:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 1Gi
storageClass: longhorn
retain: true
globalMounts:
- path: /media
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: homeassistant
namespace: homeassistant
spec:
interval: 1h
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- helmrelease.yaml
@@ -0,0 +1,54 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: immich-ml
namespace: photo
spec:
interval: 30m
chartRef:
kind: OCIRepository
name: immich
driftDetection:
mode: enabled
values:
controllers:
ml:
containers:
app:
# CPU-only: hardware-accelerated tags (-openvino, -cuda, etc)
# need specific host devices (e.g. /dev/dri) that aren't set up
# on these nodes. Switch tags later if that changes.
image:
# pinned to match immich-server's version (see
# helmrelease-server.yaml) - server and ML communicate over
# an internal API contract that isn't guaranteed compatible
# across versions.
repository: ghcr.io/immich-app/immich-machine-learning
tag: v3.1.0
env:
TZ: Europe/Moscow
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
memory: 4Gi
service:
app:
controller: ml
ports:
http:
port: 3003
persistence:
# caches downloaded ML models so they survive pod restarts instead of
# re-downloading (a few hundred MB) every time.
cache:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 10Gi
storageClass: longhorn-single-replica
retain: true
globalMounts:
- path: /cache
@@ -0,0 +1,70 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: immich-postgres
namespace: photo
spec:
interval: 30m
chartRef:
kind: OCIRepository
name: immich
driftDetection:
mode: enabled
values:
controllers:
postgres:
containers:
app:
# immich requires this specific image - it bundles the
# vectorchord/pgvecto.rs extensions immich's DB schema depends
# on; a plain postgres image will not work.
image:
repository: ghcr.io/immich-app/postgres
tag: 14-vectorchord0.4.3-pgvectors0.2.0
env:
TZ: Europe/Moscow
POSTGRES_USER: postgres
POSTGRES_DB: immich
POSTGRES_INITDB_ARGS: "--data-checksums"
POSTGRES_PASSWORD:
valueFrom:
secretKeyRef:
name: immich-secrets
key: DB_PASSWORD
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 2Gi
service:
app:
controller: postgres
ports:
postgres:
port: 5432
persistence:
data:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 10Gi
storageClass: longhorn
retain: true
globalMounts:
# subPath, not the raw mount root: a fresh Longhorn ext4 volume's
# root always contains a "lost+found" dir, and postgres's initdb
# refuses to initialize into a non-empty directory even when
# lost+found is the only thing in it.
- path: /var/lib/postgresql/data
subPath: data
# postgres' parallel query workers need real shared memory, not the
# default 64Mi container tmpfs - matches shm_size: 128mb in the
# original docker-compose.
dshm:
type: emptyDir
medium: Memory
sizeLimit: 128Mi
globalMounts:
- path: /dev/shm
@@ -0,0 +1,33 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: immich-redis
namespace: photo
spec:
interval: 30m
chartRef:
kind: OCIRepository
name: immich
driftDetection:
mode: enabled
values:
controllers:
redis:
containers:
app:
image:
repository: docker.io/valkey/valkey
tag: "9.1.1"
resources:
requests:
cpu: 20m
memory: 32Mi
limits:
memory: 256Mi
service:
app:
controller: redis
ports:
redis:
port: 6379
@@ -0,0 +1,235 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: immich-server
namespace: photo
spec:
interval: 30m
chartRef:
kind: OCIRepository
name: immich
driftDetection:
mode: enabled
values:
controllers:
server:
strategy: Recreate
podDisruptionBudget:
maxUnavailable: "0"
initContainers:
fix-permissions:
image:
repository: hectorm/nfs-ganesha
tag: v10
# hectorm does not publish exact versions, v10.0 digest is 93df70b78...
digest: sha256:93df70b78aae56957f4a4f5c606c4250d84b729435f76132dca07b7290f5bfce
command:
- sh
- -c
- chmod -R a+rwX /disks/library-recent /disks/library-archive
# native sidecar (restartPolicy: Always): mounts the two library
# disks and exposes their union at /disks/merged-library via
# FUSE. It must be up and actually mounted *before* the "app"
# container is created, since HostToContainer propagation only
# relays mounts made *after* the receiving container's own mount
# exists - it won't retroactively pick up a FUSE mount that's
# already there. Running it as a restartPolicy:Always init
# container (started, gated on the startupProbe below, before
# regular containers) guarantees that ordering; a regular sidecar
# container racing "app" on every pod start does not.
mergerfs:
dependsOn:
- fix-permissions
restartPolicy: Always
image:
repository: ghcr.io/hotio/mergerfs
tag: release-v2.42.0
args:
- "-o"
# library-recent listed first: category.create=ff means new
# files created through the merged mount (i.e. new immich
# uploads going forward) land on library-recent by default,
# which is exactly what's wanted for freshly-uploaded photos.
- "allow_other,use_ino,func.create=ff,category.action=all,category.create=ff,category.search=newest"
- "/disks/library-recent:/disks/library-archive"
- "/disks/merged-library"
securityContext:
privileged: true
probes:
startup:
enabled: true
custom: true
spec:
exec:
command:
- sh
- -c
- grep -q fuse.mergerfs /proc/mounts
periodSeconds: 1
timeoutSeconds: 2
failureThreshold: 30
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 512Mi
containers:
app:
image:
repository: ghcr.io/immich-app/immich-server
# make sure immich-machine-learning version match this
tag: v3.1.0
env:
TZ: Europe/Moscow
DB_HOSTNAME: immich-postgres
DB_PORT: "5432"
DB_USERNAME: postgres
DB_DATABASE_NAME: immich
DB_PASSWORD:
valueFrom:
secretKeyRef:
name: immich-secrets
key: DB_PASSWORD
REDIS_HOSTNAME: immich-redis
REDIS_PORT: "6379"
IMMICH_MACHINE_LEARNING_URL: http://immich-ml:3003
probes:
# first boot runs DB migrations + vector index setup, which
# can take a while - without a startup probe the default
# liveness probe (periodSeconds=10, failureThreshold=3) kills
# the container ~30s in, well before it's finished binding
# the port, causing an endless restart loop.
startup:
enabled: true
spec:
periodSeconds: 5
failureThreshold: 60
liveness:
enabled: true
readiness:
enabled: true
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
memory: 4Gi
service:
app:
controller: server
ports:
http:
port: 2283
ingress:
app:
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: photo.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: immich-tls
hosts:
- photo.aggtaa.com
persistence:
# Immich's "library" folder, mergerfs-merged from two disks: recent
# files (per the rsync migration's cutoff) and everything else.
# New uploads going forward land on library-recent (see mergerfs
# category.create above). Not reusing the old single 800Gi library
# PVC/PV - this replaces it (that PVC becomes orphaned, retained,
# and unused; clean it up separately if no longer wanted).
library-recent:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 200Gi
storageClass: longhorn-single-replica
retain: true
advancedMounts:
server:
mergerfs:
- path: /disks/library-recent
fix-permissions:
- path: /disks/library-recent
library-archive:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 500Gi
storageClass: longhorn-single-replica
retain: true
advancedMounts:
server:
mergerfs:
- path: /disks/library-archive
fix-permissions:
- path: /disks/library-archive
merged-library:
type: emptyDir
advancedMounts:
server:
mergerfs:
- path: /disks/merged-library
mountPropagation: Bidirectional
app:
- path: /data/library
mountPropagation: HostToContainer
dev-fuse:
type: hostPath
hostPath: /dev/fuse
advancedMounts:
server:
mergerfs:
- path: /dev/fuse
# split out of the old combined "other" volume below: each of these
# purposes now gets its own Longhorn volume instead of sharing one
# 200Gi disk, so a single hot subtree (e.g. thumbs) doesn't force
# replica rebuilds/relocation to drag the whole 200Gi with it.
data:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 20Gi
storageClass: longhorn
retain: true
advancedMounts:
server:
app:
- path: /data
encoded-video:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 100Gi
storageClass: longhorn
retain: true
advancedMounts:
server:
app:
- path: /data/encoded-video
thumbs:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 100Gi
storageClass: longhorn
retain: true
advancedMounts:
server:
app:
- path: /data/thumbs
backups:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 30Gi
storageClass: longhorn
retain: true
advancedMounts:
server:
app:
- path: /data/backups
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: immich
namespace: photo
spec:
interval: 1h
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- secret.enc.yaml
- helmrelease-postgres.yaml
- helmrelease-redis.yaml
- helmrelease-ml.yaml
- helmrelease-server.yaml
+22
View File
@@ -0,0 +1,22 @@
apiVersion: v1
kind: Secret
metadata:
name: immich-secrets
namespace: photo
stringData:
DB_PASSWORD: ENC[AES256_GCM,data:+bJ/+NEQCT3ZEqjnkzep4OsbLkJ9gyLZmGxCpoww2Wo=,iv:n1sLYgLl3zNZBdjMBhIrbSv/qHJFQ/nvZfBknFAUdug=,tag:DFnoc5K6XvHngVIrgJlM6Q==,type:str]
sops:
age:
- recipient: age1wmm8qrqnjzvd5nkraed6ylrsxlpn3ru7d7zettdudj7y9sg9dd2su0u3mr
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtd1hFZy82d0c0RXZMd28r
dWs1VWNlVG9tTE5WZDVLdGpFSXhuVXQ5anprCk5LWWMrWUVCd1N4Y1JENnRyWExC
d2VRNDJON2RzSHRyMGhRdmMva1NNY3cKLS0tIDNML2Q0YXFRMEs3V3prUkZYSS85
MWRGa3duNHgvbjNvVnZLOXpYMW1mbGcKaN7h3W0YKsRjnyk6P9+bziDy6If5xLQ8
kxFJf4CX9XU5jgYXqw2UNqRDusphclqfULUMRaEzMJkkPhd1cZ9PvQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-08-21T12:23:20Z"
mac: ENC[AES256_GCM,data:vGSUhl73AIQr1Ld1SHc+i4GpvOc0b0j8Vx9DmoRDjNs17TQSfmfulEZwIua12P5DHIz36P9yqggHfSyyrrEoMjfaUSTIDxCIGGvsfTl4vBt9Vis0e2gALaHtAgWZwxr5QMe+OP1tkXOZoX9E8VcoBq1ULvbIjqsPs2uxPVKsEWk=,iv:1exDUqurlFdBt1eZYWWMlkar75HbTLZ6RZelC0RsYcU=,tag:A0IhJHjVum2HBT5RwHo5qg==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.11.0
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,65 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: infcloud
namespace: pim
spec:
interval: 10m
chartRef:
kind: OCIRepository
name: infcloud
driftDetection:
mode: enabled
values:
controllers:
infcloud:
containers:
app:
image:
repository: waja/infcloud
tag: "0.13.1-16"
probes:
liveness:
enabled: true
readiness:
enabled: true
service:
app:
controller: infcloud
ports:
http:
port: 80
ingress:
app:
className: traefik
# this ingress owns the cert-manager request for c.aggtaa.com
# (secret kept as "radicale-tls" post-migration so the existing,
# already-valid cert is reused instead of re-issued). davical's
# ingress just references the same secret without its own
# annotation - a second annotated Ingress would create a
# competing Certificate for the same host/secret.
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: c.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: radicale-tls
hosts:
- c.aggtaa.com
persistence:
config:
type: configMap
name: infcloud-config
globalMounts:
- path: /srv/infcloud/config/config.js
subPath: config.js
readOnly: true
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: infcloud
namespace: pim
spec:
interval: 10m
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- configmap.yaml
- helmrelease-infcloud.yaml
@@ -0,0 +1,57 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: it-tools
namespace: tools
spec:
interval: 10m
chartRef:
kind: OCIRepository
name: it-tools
driftDetection:
mode: enabled
values:
controllers:
it-tools:
containers:
app:
image:
repository: ghcr.io/willjayyyy/next-tools
tag: 1.10.3
probes:
liveness:
enabled: true
readiness:
enabled: true
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 128Mi
service:
app:
controller: it-tools
ports:
http:
port: 80
ingress:
app:
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.middlewares: auth-tinyauth@kubernetescrd
hosts:
- host: tools.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: it-tools-tls
hosts:
- tools.aggtaa.com
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: it-tools
namespace: tools
spec:
interval: 1h
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- helmrelease.yaml
+77
View File
@@ -0,0 +1,77 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: jitsi
namespace: video
spec:
interval: 30m
chart:
spec:
chart: jitsi-meet
version: ">=1.0.0 <2.0.0"
sourceRef:
kind: HelmRepository
name: jitsi
install:
remediation:
retries: -1
upgrade:
remediation:
retries: 5
driftDetection:
mode: enabled
# fixed xmpp credentials: the chart generates random ones on every render, which
# desyncs jicofo/jvb from the already running prosody after an upgrade
valuesFrom:
- kind: Secret
name: jitsi-secrets
valuesKey: JICOFO_AUTH_PASSWORD
targetPath: jicofo.xmpp.password
- kind: Secret
name: jitsi-secrets
valuesKey: JICOFO_COMPONENT_SECRET
targetPath: jicofo.xmpp.componentSecret
- kind: Secret
name: jitsi-secrets
valuesKey: JVB_AUTH_PASSWORD
targetPath: jvb.xmpp.password
values:
publicURL: https://v.aggtaa.com
web:
ingress:
enabled: true
ingressClassName: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: v.aggtaa.com
paths: ["/"]
tls:
- secretName: jitsi-tls
hosts:
- v.aggtaa.com
# media traffic: UDP 10000 must be forwarded from the WAN router to the JVB service ip (kubectl get svc -n video)
jvb:
UDPPort: 10000
# wan ip first, then the service ip for clients on the lan (no hairpin nat needed);
# the chart passes this string as-is to JVB_ADVERTISE_IPS, which accepts a comma separated list
publicIPs:
- 91.233.45.43,10.0.64.9
service:
enabled: true
type: LoadBalancer
externalTrafficPolicy: Cluster
postRenderers:
- kustomize:
patches:
# dmz-pool has a single ip held by traefik, so use the dedicated public-ports-pool (see infra/metallb/config.yaml)
- target:
kind: Service
name: jitsi-jitsi-meet-jvb
patch: |
- op: add
path: /metadata/annotations/metallb.io~1address-pool
value: public-ports-pool
@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: jitsi
namespace: video
spec:
interval: 1h
url: https://jitsi-contrib.github.io/jitsi-helm/
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- helmrelease.yaml
- secret.enc.yaml
+24
View File
@@ -0,0 +1,24 @@
apiVersion: v1
kind: Secret
metadata:
name: jitsi-secrets
namespace: video
stringData:
JICOFO_AUTH_PASSWORD: ENC[AES256_GCM,data:OUEZWPlqpkuhEI8kf65kKUooeahC3z03STevw7oOWx8=,iv:KSfrwehZG9kW7bfzQ5yavq6gYj+TrYKS/LZPNzaZdLI=,tag:/FiUF75ib3RXPuzO0EhIpw==,type:str]
JICOFO_COMPONENT_SECRET: ENC[AES256_GCM,data:y8a84NdaClBRUFDPtXhulJxrX68Tn+4s1NrNqyhBjvI=,iv:fdqlgRwHNTdqegvfOrvX1Iph5aMKAtyW5zJUr+kBNx0=,tag:yNQ53KQQWafV7CSbNttJUQ==,type:str]
JVB_AUTH_PASSWORD: ENC[AES256_GCM,data:GWSHUDfEZmLbKzU02U+ImCR0nVIp5ebngWVKBOmjOjY=,iv:uG7/4QPD8Wd2Q6lqPgKTD533GdbkXjdtZqJnshmNzjI=,tag:jhj5L86v0ZFalyhfQOTgCA==,type:str]
sops:
age:
- recipient: age1zmr32r5mcgyh73asvzl8l690pzwqsx9hle27u3cydngsgv56wykq3v67px
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuSXZWTzloNTMzWk5samRU
amhURmFvdDlSMjd1RVFjUWxuWEtoVE13WUNJClptVUgxblRWL1RIYmZPZ2x2MG1H
Y3ovWWlwL2dLeW9xbndXWEMvQVZONGcKLS0tIGxZNDZkRDBkZ0NDVWxMNGxoUFR4
VG1wSm5wUWRVUEZ3cWlMb2lBNTVkZGcKAa4/S8EWBMt+uvT/6hYRl1GascDul+ox
qlSV/20eA9uamssuId63ewaO4hL7ekwKwqM5cAFH+wx2dno9Ajuang==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-11-04T11:21:36Z"
mac: ENC[AES256_GCM,data:r7iq6iTFL70Zat0sVw9nAHiueZNUWzV2j6q6POBUjeGLqn53ygJAXqG31REBLNKBwabLxsNlTmZRujpP6ySkbFve6fN1C6+6HFWdHnkeJx1nk4iJTvb5+Ld7Exp8yt6b46M/hHZxOffO23jVHuAa47hZ4lHphFpSrlgif79W/CI=,iv:bP9ll+tuTFOfh3kE7xiXtF0cH0sCfhaIs9fhNt0JoEU=,tag:hpBZBwRZxyAgn20aaGoGWA==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.11.0
@@ -0,0 +1,80 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: joplin
namespace: pim
spec:
interval: 10m
dependsOn:
- name: joplin-postgres
chartRef:
kind: OCIRepository
name: joplin
driftDetection:
mode: enabled
values:
controllers:
app:
containers:
app:
image:
repository: joplin/server
# verified via Docker Hub API (hub.docker.com/v2/repositories/
# joplin/server/tags): "3.7.1" and "latest" were pushed at the
# same timestamp, i.e. this is what "latest" (the original
# compose.yaml's tag) currently resolves to.
tag: "3.7.1"
env:
APP_PORT: "22300"
APP_BASE_URL: https://notes.aggtaa.com
DB_CLIENT: pg
POSTGRES_DATABASE: joplin
POSTGRES_USER: joplin
POSTGRES_HOST: joplin-postgres
POSTGRES_PORT: "5432"
POSTGRES_PASSWORD:
valueFrom:
secretKeyRef:
name: joplin-secrets
key: POSTGRES_PASSWORD
probes:
startup:
enabled: true
spec:
periodSeconds: 5
failureThreshold: 60
liveness:
enabled: true
readiness:
enabled: true
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 1Gi
service:
app:
controller: app
ports:
http:
port: 22300
ingress:
app:
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: notes.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: joplin-tls
hosts:
- notes.aggtaa.com
@@ -0,0 +1,50 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: joplin-postgres
namespace: pim
spec:
interval: 10m
chartRef:
kind: OCIRepository
name: joplin
driftDetection:
mode: enabled
values:
controllers:
postgres:
containers:
app:
image:
repository: postgres
# not alpine: joplin server needs a glibc-collation postgres,
# matching the official (Debian-based) image used everywhere
# else this pattern is followed in this repo.
tag: "16.15"
env:
TZ: Europe/Moscow
POSTGRES_USER: joplin
POSTGRES_DB: joplin
POSTGRES_PASSWORD:
valueFrom:
secretKeyRef:
name: joplin-secrets
key: POSTGRES_PASSWORD
service:
app:
controller: postgres
ports:
postgres:
port: 5432
persistence:
data:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 20Gi
storageClass: longhorn
retain: true
globalMounts:
- path: /var/lib/postgresql/data
subPath: data
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: joplin
namespace: pim
spec:
interval: 10m
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- secret.enc.yaml
- helmrelease-postgres.yaml
- helmrelease-joplin.yaml
+22
View File
@@ -0,0 +1,22 @@
apiVersion: v1
kind: Secret
metadata:
name: joplin-secrets
namespace: pim
stringData:
POSTGRES_PASSWORD: ENC[AES256_GCM,data:cI3zfdaJUbo2344RuLIG4DjPsgx4Afl6GKMJVHrOHOg=,iv:8QliTSQ44ScVUchDxh0DF/wU9bTLK9Jq4ojxSMXsNC8=,tag:VB2mf4uhLnRnafWg0mRRpg==,type:str]
sops:
age:
- recipient: age1dertyl956fjsmh00cw468gdnl7lf3frasg4n4c82z0chm6qm2cks6w8gha
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFQ2JKZWxseVAxUWU5NlhL
S0xWQUh1NFpBU2ZpQkJXbkg4TlZ2cTR5TXhjCjlMTGdGMEF4UzBEbS9wdndJb1VE
OXBOanpJS1lpTFVhWm52K2xhaGx2M00KLS0tIGF5b3FZOWRNVWNpenRmUDVOT3Vp
aHpCbXkvdXM3SjYxWTlsT0cxU2RDOFkKximxt9bmd6FZAtU/SBirzDH3NMitQKrZ
PfwicphmFEXVN+HtD9vwRbP4TMa1swOMbOVINGKuA8YZwEk1GyPe4w==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-08-27T10:16:36Z"
mac: ENC[AES256_GCM,data:85dGV8WPFqd+4l6k7PLqDROm5+kwiAjO80ElNKfSJFOgeI9y7DyhyhDklcBJXMJljatbEfAmCFFqnoyRGmeIjc6FBfteUGQtpLgI6e8RTlRHPvaRzqDwA1OhnlQbAW9TTf5vha6tyYso/PprCWjJSMjBEfiu7ApiRMdZcLd1V4c=,iv:Dh9z/r7UbhdREwXT787HkmVM04X/BAtQmQteN3j7AKU=,tag:oUMYT5CL85PAYfItf5TIdw==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.11.0
@@ -0,0 +1,63 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: map-monitor
namespace: iot
spec:
interval: 30m
chartRef:
kind: OCIRepository
name: map-monitor
install:
remediation:
retries: -1
upgrade:
remediation:
retries: 5
driftDetection:
mode: enabled
values:
defaultPodOptions:
securityContext:
runAsUser: 65534
runAsGroup: 65534
runAsNonRoot: true
controllers:
map-monitor:
containers:
app:
image:
repository: python
tag: 3.12-slim
command:
- sh
- -c
- pip install --no-cache-dir --target /tmp/site -r /app/requirements.txt && exec python /app/map_mqtt.py
env:
PYTHONPATH: /tmp/site
PYTHONUNBUFFERED: "1"
PYTHONDONTWRITEBYTECODE: "1"
PIP_DISABLE_PIP_VERSION_CHECK: "1"
HOME: /tmp
envFrom:
- secretRef:
name: map-monitor-secrets
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 128Mi
persistence:
code:
type: configMap
name: map-monitor-code
globalMounts:
- path: /app
readOnly: true
tmp:
type: emptyDir
globalMounts:
- path: /tmp
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: map-monitor
namespace: iot
spec:
interval: 1h
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- secret.enc.yaml
- helmrelease.yaml
configMapGenerator:
- name: map-monitor-code
namespace: iot
files:
- src/map_mqtt.py
- src/map_read.py
- src/requirements.txt
generatorOptions:
disableNameSuffixHash: true
@@ -0,0 +1,36 @@
apiVersion: v1
kind: Secret
metadata:
name: map-monitor-secrets
namespace: iot
stringData:
SERIAL_HOST: ENC[AES256_GCM,data:6cCm6shCy2kymQ==,iv:mqUOG6RZ6HGh3dBWy2LdWWXklU2FFhra9Xi3VgFdQi4=,tag:sJq/S+QslUWxeuK1JgQuBA==,type:str]
SERIAL_PORT: ENC[AES256_GCM,data:ZiqkpB0=,iv:SryQv6eUrTZwA896oG83xHeLjzABqnn4pwlDPA56S3A=,tag:WPmz9uw+G+QQZW4L/Qfm1Q==,type:str]
SERIAL_POLL_INTERVAL: ENC[AES256_GCM,data:Cw==,iv:myGXUNhVKALsedj1Do56clisSiAvqE19rqTGQjtYxCI=,tag:XvTcCvI9B5YyizJ4skfB1A==,type:str]
SERIAL_TIMEOUT: ENC[AES256_GCM,data:mMtT,iv:nz7nJXuLU3tpO2l04+aRTczCwnmgubLOw7a/cg6Yjic=,tag:nMed9+RH6VKZDQURwuMd/g==,type:str]
MQTT_HOST: ENC[AES256_GCM,data:qyjHpa6fNG+IaQvj6LsUGF8iPxWU4NzdzPCt1ZcodFE=,iv:dxCP9gfasYovuBI6pxkynKkNAh99bUIEr8fn9Qbqxww=,tag:mz1BgruIsUXE/sJdiCqH6g==,type:str]
MQTT_PORT: ENC[AES256_GCM,data:PPxXlQ==,iv:s4b8gCGna5Va0B9BN02ZLlofYlIQiVn9KU8qJNECK5I=,tag:neXH/dwhfW1u1UfkRuY2mg==,type:str]
MQTT_USER: ENC[AES256_GCM,data:Xs6v,iv:DlyX9ErjbcJ4WxQFBhCdiBCRUqOpEYwWSg/X3uA4sSk=,tag:4N/85y5h2u754qP4iyIeiw==,type:str]
MQTT_PASSWORD: ENC[AES256_GCM,data:CD/cSnauAZ29KHuwGblqqDxSdrw=,iv:1mUsaq847o4rjbMIpSRErVIF8ZI+4oS4LDJGwbas0Jo=,tag:y7uSxXNFFM5HD/1IMRj5og==,type:str]
MQTT_TLS: ENC[AES256_GCM,data:lhmlb+8=,iv:mZiHwfgmkpWiBBRF5SvuSP/wlXMnAQmNnSZTYc0kMRM=,tag:VNxesMInF1Vj/gbeK/FlJA==,type:str]
MQTT_CLIENT_ID: ENC[AES256_GCM,data:Epdn4DGpQtgl53sc+Qo=,iv:gjVPQA1g0IFrzNJuqOWAAUgaFZsnZ2te5V32Q/SPTJg=,tag:zSauVShmjG0K8FDVevvolQ==,type:str]
MQTT_DISCOVERY_PREFIX: ENC[AES256_GCM,data:9ThR6rD3yXShgW1TmA==,iv:yn+89NBc6rXqbUnnKSqiWhzCruf0HPt3DfgxrhGZq7g=,tag:YoWynvyWRxdJ+UCbfjCJtQ==,type:str]
MQTT_BASE_TOPIC: ENC[AES256_GCM,data:Y8fGesOJmsXOvdE0khg=,iv:CyyDnQkUzGivWWAQnzWyElqoLeXmT7uLns5te03Hq0E=,tag:5CCuHp2ZeaZyXF4LmoLTUg==,type:str]
DEVICE_ID: ENC[AES256_GCM,data:RF2wLrHe/+CA0em/Yc0=,iv:IQx6zlX31rywQlhxchmGu3pjYg7JJlyNw1KT1/ELsL4=,tag:wVQ2s6XDN1l+tM9EQnjX/g==,type:str]
DEVICE_NAME: ENC[AES256_GCM,data:fHTyCXeA0aDlNqp7HRCuppTGkYnP,iv:wMp+umM2Y6S/54t5nc4h9PYCZd5NloOYIpCp3/4vOoE=,tag:PcP3e3hrJ+r78oPC7etm5g==,type:str]
LOG_LEVEL: ENC[AES256_GCM,data:/oGKyg==,iv:3A/971xbMz+dm+czrslz2nS3J1PRaM+7zLNACEHX7OM=,tag:OmLgstVHAT4GpviplU5V8Q==,type:str]
sops:
age:
- recipient: age1k4j5vyqqt7w7k52gvkpfe6znqr9mlgt076l7xgdtyc75lamsh4aqshj5qs
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZZ0R5UzhmOXV6T01KbTdr
Y2tCR2owUHZVV2VVakpEWXZjK3BNcTkzOVdZCjAzUnJaK2lMTWFhS0tTY2EvSU9u
OGh4SlI0em9YYUxkTGZZOUR2SG0yZjAKLS0tIFE5MWt4b3VWcmtVWXpwWjlTbHlT
R3ZQckkwMVJTekNRZW1uQVZVYXAvcTQKTeXGo0pBSFXderktCrgX6/hDRBraiOMF
SPr6hDe0ejMfWyOUSA9XQRQnn20gssRtA5DzNfpkjA3Gam+CU9Lu0w==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-10-26T11:29:23Z"
mac: ENC[AES256_GCM,data:jVnj8WpNjbFHwDfU2NBCcoqyfRYotmvc7w1b2A7e40GPYBKmkqVkZUlxBp4oc2p4qj+SRfPPKSUqDfxNqIS+oiqidui41/EhsJtPBjqyjoWmLfMMriwYUMlxrdFeCNNL97cUJin+Cy/OvPXgIU1BX9hnqyz0mTU5ce1zWju25Mc=,iv:4GJialKu0t9WmGoxKAA1P6wuan5gD8fSn3sToEL0QRU=,tag:yZbaQpIfUFh5eZvWQlKwxg==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.11.0
@@ -0,0 +1,10 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY map_read.py map_mqtt.py ./
ENV PYTHONUNBUFFERED=1
USER nobody
CMD ["python", "map_mqtt.py"]
@@ -0,0 +1,259 @@
#!/usr/bin/env python3
"""
Service: polls a MAP inverter via ser2net and publishes to MQTT for Home Assistant
(MQTT discovery). Values are also written to the console for diagnostics.
All configuration is done via environment variables (see .env.example).
"""
import json
import logging
import os
import signal
import sys
import threading
import paho.mqtt.client as mqtt
from map_read import MapClient, MapError, read_status
log = logging.getLogger("map_mqtt")
def env(name, default=None, cast=str):
v = os.environ.get(name, "")
if v == "":
if default is None:
sys.exit("Environment variable %s is not set" % name)
return default
return cast(v)
SERIAL_HOST = env("SERIAL_HOST", "10.0.64.24")
SERIAL_PORT = env("SERIAL_PORT", 33333, int)
SERIAL_TIMEOUT = env("SERIAL_TIMEOUT", 1.0, float)
SERIAL_POLL_INTERVAL = env("SERIAL_POLL_INTERVAL", 5.0, float)
MQTT_HOST = env("MQTT_HOST")
MQTT_PORT = env("MQTT_PORT", 1883, int)
MQTT_USER = env("MQTT_USER", "")
MQTT_PASSWORD = env("MQTT_PASSWORD", "")
MQTT_CLIENT_ID = env("MQTT_CLIENT_ID", "map-inverter")
MQTT_TLS = env("MQTT_TLS", "false").lower() in ("1", "true", "yes")
DISCOVERY_PREFIX = env("MQTT_DISCOVERY_PREFIX", "homeassistant")
BASE_TOPIC = env("MQTT_BASE_TOPIC", "map_inverter")
DEVICE_ID = env("DEVICE_ID", "map_inverter")
DEVICE_NAME = env("DEVICE_NAME", "MAP Energy")
STATE_TOPIC = "%s/state" % BASE_TOPIC
AVAIL_TOPIC = "%s/availability" % BASE_TOPIC
HA_STATUS_TOPIC = "%s/status" % DISCOVERY_PREFIX
# (key, component, name, unit, device_class, state_class, extra)
ENTITIES = [
("mode_text", "sensor", "Mode", None, None, None, {"icon": "mdi:state-machine"}),
("mode", "sensor", "Mode code", None, None, None,
{"icon": "mdi:numeric", "entity_category": "diagnostic"}),
("u_acc", "sensor", "Battery voltage", "V", "voltage", "measurement", {}),
("i_acc", "sensor", "Battery current", "A", "current", "measurement", {}),
("p_acc", "sensor", "Battery power", "W", "power", "measurement", {}),
("p_load", "sensor", "Load power", "W", "power", "measurement", {}),
("u_net", "sensor", "Grid voltage", "V", "voltage", "measurement", {}),
("i_net", "sensor", "Grid current", "A", "current", "measurement", {}),
("p_net", "sensor", "Grid power", "W", "power", "measurement", {}),
("f_net", "sensor", "Grid frequency", "Hz", "frequency", "measurement", {}),
("u_out", "sensor", "Output voltage", "V", "voltage", "measurement", {}),
("f_out", "sensor", "Output frequency", "Hz", "frequency", "measurement", {}),
("t_acc", "sensor", "Battery temperature", "°C", "temperature", "measurement", {}),
("t_tor", "sensor", "Transformer temperature", "°C", "temperature", "measurement", {}),
("t_trans", "sensor", "Transistor temperature", "°C", "temperature", "measurement", {}),
("u_net_limit", "sensor", "Last out-of-range grid voltage", "V", "voltage", None,
{"entity_category": "diagnostic"}),
("e_net", "sensor", "Energy from grid", "kWh", "energy", "total_increasing", {}),
("e_acc", "sensor", "Energy from battery", "kWh", "energy", "total_increasing", {}),
("e_acc_charge", "sensor", "Battery charge energy", "kWh", "energy", "total_increasing", {}),
("errors_text", "sensor", "Errors", None, None, None,
{"icon": "mdi:alert-circle-outline", "entity_category": "diagnostic"}),
("problem", "binary_sensor", "Problem", None, "problem", None,
{"entity_category": "diagnostic"}),
("net_reverse", "binary_sensor", "Grid export", None, None, None,
{"icon": "mdi:transmission-tower-export"}),
("relay1", "binary_sensor", "Relay 1", None, None, None, {"icon": "mdi:electric-switch"}),
("relay2", "binary_sensor", "Relay 2", None, None, None, {"icon": "mdi:electric-switch"}),
]
def to_payload(s):
"""Flatten the read_status result into JSON for the state topic."""
p = {k: s[k] for k in ("mode", "mode_text", "u_acc", "i_acc", "p_acc", "p_load", "u_net",
"i_net", "p_net", "f_net", "u_out", "f_out", "t_acc", "t_tor",
"t_trans", "u_net_limit")}
# counter unit is 0.01 kWh (malina html/meters.js; confirmed against integrated grid power)
p["e_net"] = s["e_net_raw"] / 100
p["e_acc"] = s["e_acc_raw"] / 100
p["e_acc_charge"] = s["e_acc_charge_raw"] / 100
for k in ("net_reverse", "relay1", "relay2"):
p[k] = "ON" if s[k] else "OFF"
p["problem"] = "ON" if s["errors"] else "OFF"
texts = ["%s: %s" % (name, "; ".join(e["bits"])) for name, e in s["errors"].items()]
p["errors_text"] = (" | ".join(texts) or "none")[:255]
p["errors"] = {name: e["value"] for name, e in s["errors"].items()}
return p
def discovery_messages():
device = {
"identifiers": [DEVICE_ID],
"name": DEVICE_NAME,
"manufacturer": "microART",
"model": "MAP",
}
for key, component, name, unit, dev_class, state_class, extra in ENTITIES:
cfg = {
"name": name,
"unique_id": "%s_%s" % (DEVICE_ID, key),
"state_topic": STATE_TOPIC,
"value_template": "{{ value_json.%s }}" % key,
"availability_topic": AVAIL_TOPIC,
"device": device,
}
if unit:
cfg["unit_of_measurement"] = unit
if dev_class:
cfg["device_class"] = dev_class
if state_class:
cfg["state_class"] = state_class
if key == "errors_text":
cfg["json_attributes_topic"] = STATE_TOPIC
cfg["json_attributes_template"] = "{{ value_json.errors | tojson }}"
cfg.update(extra)
topic = "%s/%s/%s/%s/config" % (DISCOVERY_PREFIX, component, DEVICE_ID, key)
yield topic, json.dumps(cfg, ensure_ascii=False)
class Publisher:
def __init__(self):
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=MQTT_CLIENT_ID)
if MQTT_USER:
self.client.username_pw_set(MQTT_USER, MQTT_PASSWORD)
if MQTT_TLS:
self.client.tls_set()
self.client.will_set(AVAIL_TOPIC, "offline", qos=1, retain=True)
self.client.on_connect = self._on_connect
self.client.on_disconnect = self._on_disconnect
self.client.on_message = self._on_message
self.online = False
self._connack = threading.Event()
self._connect_error = None
def start(self, timeout=10):
"""Connect to the broker; raise on failure (the program will exit).
Later disconnects are handled by paho's automatic reconnect."""
self.client.connect(MQTT_HOST, MQTT_PORT, keepalive=60) # OSError on a bad address
self.client.loop_start()
if not self._connack.wait(timeout):
self.client.loop_stop()
raise ConnectionError("broker did not respond within %d s" % timeout)
if self._connect_error:
self.client.loop_stop()
raise ConnectionError("broker refused connection: %s" % self._connect_error)
def stop(self):
self.client.publish(AVAIL_TOPIC, "offline", qos=1, retain=True).wait_for_publish(2)
self.client.disconnect()
self.client.loop_stop()
def _on_connect(self, client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
log.error("MQTT: broker refused connection: %s", reason_code)
if not self._connack.is_set():
self._connect_error = reason_code
self._connack.set()
return
self._connack.set()
log.info("MQTT: connected to %s:%d", MQTT_HOST, MQTT_PORT)
client.subscribe(HA_STATUS_TOPIC)
self.publish_discovery()
self.set_available(self.online)
def _on_disconnect(self, client, userdata, flags, reason_code, properties):
log.warning("MQTT: disconnected (%s), reconnecting...", reason_code)
def _on_message(self, client, userdata, msg):
# Home Assistant restarted - republish discovery
if msg.topic == HA_STATUS_TOPIC and msg.payload == b"online":
log.info("MQTT: Home Assistant online, republishing discovery")
self.publish_discovery()
self.set_available(self.online)
def publish_discovery(self):
for topic, payload in discovery_messages():
self.client.publish(topic, payload, qos=1, retain=True)
def set_available(self, online):
self.online = online
self.client.publish(AVAIL_TOPIC, "online" if online else "offline", qos=1, retain=True)
def publish_state(self, payload):
self.client.publish(STATE_TOPIC, json.dumps(payload, ensure_ascii=False), qos=0)
def format_line(p):
return ("mode=%d (%s) Ubat=%.1fV Ibat=%.1fA Pbat=%dW Pload=%dW Ugrid=%dV Igrid=%.1fA "
"Pgrid=%dW Fgrid=%sHz Uout=%dV Fout=%sHz Tbat=%d Ttr=%d Ttrans=%d "
"Egrid=%.2f Ebat=%.2f Echarge=%.2f errors=%s") % (
p["mode"], p["mode_text"], p["u_acc"], p["i_acc"], p["p_acc"], p["p_load"],
p["u_net"], p["i_net"], p["p_net"], p["f_net"], p["u_out"], p["f_out"],
p["t_acc"], p["t_tor"], p["t_trans"], p["e_net"], p["e_acc"], p["e_acc_charge"],
p["errors_text"])
def main():
logging.basicConfig(level=env("LOG_LEVEL", "INFO").upper(), stream=sys.stdout,
format="%(asctime)s %(levelname)s %(message)s")
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", line_buffering=True)
stop = threading.Event()
signal.signal(signal.SIGTERM, lambda *a: stop.set())
signal.signal(signal.SIGINT, lambda *a: stop.set())
log.info("Starting: ser2net %s:%d, poll every %.1f s, MQTT %s:%d, topic %s",
SERIAL_HOST, SERIAL_PORT, SERIAL_POLL_INTERVAL, MQTT_HOST, MQTT_PORT, STATE_TOPIC)
pub = Publisher()
try:
pub.start()
except OSError as e: # ConnectionError is an OSError too
log.error("MQTT: failed to connect to %s:%d: %s", MQTT_HOST, MQTT_PORT, e)
sys.exit(1)
client = None
while not stop.is_set():
try:
if client is None:
client = MapClient(SERIAL_HOST, SERIAL_PORT, SERIAL_TIMEOUT)
client.connect()
log.info("ser2net: connected to %s:%d", SERIAL_HOST, SERIAL_PORT)
payload = to_payload(read_status(client))
log.info(format_line(payload))
pub.publish_state(payload)
if not pub.online:
pub.set_available(True)
except (MapError, OSError) as e:
log.error("ser2net: %s", e)
if client is not None:
client.close()
client = None
if pub.online:
pub.set_available(False)
stop.wait(SERIAL_POLL_INTERVAL)
log.info("Stopping")
if client is not None:
client.close()
pub.stop()
if __name__ == "__main__":
main()
@@ -0,0 +1,385 @@
#!/usr/bin/env python3
"""
Read the current operating parameters of a MAP inverter (microART) via ser2net (raw TCP).
Protocol reverse-engineered from thirdparty/malina/usr/bin/mapd.c:
* 19200 8N1 (configured on the ser2net side);
* every byte sent by the host is echoed back by the MAP; every byte of the MAP
reply must be echoed back by the host, otherwise the MAP won't send the next one;
* request: cmd('r'=0x72) | count-1 | addr_hi | addr_lo | crc | '\\n'
* reply: 'o'(0x6F) | data... | crc | '\\n' ('e'=0x65 means error)
* byte 0x0A inside a frame is encoded as DB DC, byte 0xDB as DB DD;
* crc = (0x100 - sum of all frame bytes in encoded form) & 0xFF;
if crc == 0x0A it also serves as the frame terminator.
Usage:
python map_read.py # single read, 10.0.64.24:33333
python map_read.py --loop 2 # poll every 2 s
python map_read.py --json # JSON output
python map_read.py --host 1.2.3.4 --port 4001
The ser2net port must be in raw mode (not telnet), for example:
33333:raw:0:/dev/ttyUSB0:19200 8DATABITS NONE 1STOPBIT
"""
import argparse
import json
import select
import socket
import sys
import time
DEFAULT_HOST = "10.0.64.24"
DEFAULT_PORT = 33333
CMD_READ = 0x72 # 'r'
ANS_OK = 0x6F # 'o'
ANS_ERR = 0x65 # 'e'
LF = 0x0A
ESC = 0xDB
MODES = {
0: "System: OFF, Input grid: OFF",
1: "System: OFF, Input grid: ON",
2: "System: ON, Input grid: OFF, Local grid: BATTERY, Battery: DISCHARGING",
3: "System: ON, Input grid: ON, Local grid: PASS-THROUGH",
4: "System: ON, Input grid: ON, Local grid: PASS-THROUGH, Battery: CHARGING",
}
# Error bit field descriptions (from html/local/RU/local.inc)
ERROR_BITS = {
"RSErrJobM": [
"Battery fully discharged, voltage critically low",
"Battery overvoltage, inverting/charging suspended",
"Battery short circuit while charging, charging suspended",
"Battery short circuit, inverting/charging suspended",
"Main relay may be stuck",
"Short circuit on 220V grid",
"External voltage on 220V output, inverting disabled",
"Firmware reset (interference)",
],
"RSErrJob": [
"Battery discharged, inverting will stop in 1 minute",
"Battery overload",
"Load above rated power (limited operating time)",
"Overheating, inverting/charging suspended",
"Fan not spinning",
"Grid overload, switching to inverter",
"Operating mode failure (power loss/interference)",
"Shutdown after repeated short circuits while charging (cleared after 2 h)",
],
"RSWarning": [
"No action for START+CHARGE button combination",
"Grid voltage out of range",
"External voltage on output or voltage spikes from load",
"Voltage spikes on grid input",
"START or CHARGE button stuck",
"Charging not possible - no grid",
"Load power above EE_LCD_NetMaxPow, switching to inverter",
"Grid unstable",
],
"RSErrDop": [
"Invalid data from previous phase / no link",
"Phase shift outside 60 deg synchronization window",
"BMS I2C error",
"MPPT I2C error",
"Outdated MPPT firmware",
"Parallel MAP synchronization error",
"Slave MAP has no grid while master switched to grid",
None,
],
"I2C_Err": ["I2C Err Ack", "I2C Err Sum", "I2C Err Data Size", "I2C Err Protocol",
None, None, None, None],
"F_Acc_Over": [
"Shutdown: battery overcurrent",
"Shutdown: battery overcurrent while charging",
"Shutdown: battery voltage critically low",
"Shutdown: fan failure",
"Shutdown: power above rated for more than 30 min",
"Shutdown: battery fully discharged",
"Shutdown: temperature sensor out of range",
"Full shutdown after repeated battery overloads (press START)",
],
"F_Net_Over": [
"Inverting disabled - external voltage on output or faulty relay",
"Switched to inverter - grid overload",
None,
"Shutdown after repeated grid overloads (press START)",
None, None, None, None,
],
}
class MapError(Exception):
pass
class MapClient:
def __init__(self, host, port, timeout=1.0):
self.host = host
self.port = port
self.timeout = timeout
self.sock = None
def connect(self):
self.sock = socket.create_connection((self.host, self.port), timeout=5)
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.sock.settimeout(self.timeout)
self.flush()
def close(self):
if self.sock:
self.sock.close()
self.sock = None
def __enter__(self):
self.connect()
return self
def __exit__(self, *exc):
self.close()
# --- byte level ------------------------------------------------------
def flush(self):
"""Discard everything accumulated in the input buffer."""
while True:
r, _, _ = select.select([self.sock], [], [], 0.05)
if not r:
return
if not self.sock.recv(4096):
raise MapError("connection closed by ser2net")
def _recv1(self):
try:
b = self.sock.recv(1)
except socket.timeout:
raise MapError("MAP response timeout")
if not b:
raise MapError("connection closed by ser2net")
return b[0]
def _put(self, a):
"""Send a byte and wait for its echo (resyncing to the stream, as mapd.c does)."""
self.sock.sendall(bytes([a]))
for _ in range(21):
if self._recv1() == a:
return
raise MapError("no echo from MAP (0x%02X)" % a)
def _put_coded(self, a, csum):
if a == LF:
seq = (ESC, 0xDC)
elif a == ESC:
seq = (ESC, 0xDD)
else:
seq = (a,)
for b in seq:
self._put(b)
csum += b
return csum
# --- frames ----------------------------------------------------------
def _send_command(self, cmd, addr, count):
csum = 0
for b in (cmd, count - 1, addr >> 8, addr & 0xFF):
csum = self._put_coded(b, csum)
crc = (-csum) & 0xFF
self._put(crc)
if crc != LF:
self._put(LF)
def _read_answer(self):
raw = bytearray()
while True:
b = self._recv1()
raw.append(b)
self.sock.sendall(bytes([b])) # echo back to the MAP
if b == LF or len(raw) > 600:
break
if raw[0] == ANS_ERR:
raise MapError("MAP returned an error ('e')")
if raw[0] != ANS_OK or len(raw) < 3:
raise MapError("unexpected reply: %s" % raw[:16].hex())
if ((-sum(raw[:-2])) & 0xFF) == raw[-2]:
body = raw[1:-2]
elif ((-sum(raw[:-1])) & 0xFF) == LF:
body = raw[1:-1] # crc == 0x0A is itself the frame terminator
else:
raise MapError("checksum error")
data = bytearray()
i = 0
while i < len(body):
if body[i] == ESC and i + 1 < len(body) and body[i + 1] in (0xDC, 0xDD):
data.append(LF if body[i + 1] == 0xDC else ESC)
i += 2
else:
data.append(body[i])
i += 1
return bytes(data)
def read(self, addr, count, retries=3):
"""Read count (1..256) bytes of MAP memory starting at addr."""
last = None
for _ in range(retries):
try:
self._send_command(CMD_READ, addr, count)
data = self._read_answer()
if len(data) < count:
raise MapError("short reply: %d of %d bytes" % (len(data), count))
return data[:count]
except MapError as e:
last = e
time.sleep(0.2)
self.flush()
raise last
# --- data decoding -------------------------------------------------------
def decode_bits(name, value):
texts = ERROR_BITS.get(name, [])
out = []
for bit in range(8):
if value >> bit & 1:
t = texts[bit] if bit < len(texts) and texts[bit] else "bit %d" % bit
out.append(t)
return out
def phase_current(lo, hi):
v = (lo + (hi & 0x7F) * 256) / 10
return -v if hi & 0x80 else v
def read_status(client):
m = client.read(0x400, 256)
x = lambda addr: m[addr - 0x400]
e = client.read(0x527, 96)
y = lambda addr: e[addr - 0x527]
unet = x(0x422) + 100 if x(0x422) else 0
uout = x(0x427) + 100 if x(0x427) else 0
inet_raw = x(0x423)
hz = lambda t: round(6250 / t, 1) if t else None
s = {
"mode": x(0x400),
"mode_text": MODES.get(x(0x400), "unknown mode"),
"status_char": x(0x402),
"u_acc": (x(0x405) * 256 + x(0x406)) / 10,
"i_acc": round(x(0x432) / 16 + x(0x433) * 16, 1),
"i_acc_coarse": x(0x408) * 2,
"p_load": x(0x409) * 100,
"u_net": unet,
"i_net": round(x(0x431) / (16 if inet_raw < 16 else 4), 1),
"p_net": x(0x424) * 100,
"f_net": hz(x(0x425)),
"u_out": uout,
"f_out": hz(x(0x426)),
"u_net_limit": x(0x429) + 100,
"f_net_limit": round(2500 / x(0x428), 1) if x(0x428) else None,
"t_acc": x(0x42E) - 50,
"t_tor": x(0x42F) - 50,
"t_trans": x(0x430) - 50,
"net_reverse": (x(0x443) + x(0x444) * 256) > 32768,
"e_net_raw": x(0x44D) | x(0x44E) << 8 | x(0x44F) << 16,
"e_acc_raw": x(0x450) | x(0x451) << 8 | x(0x452) << 16,
"e_acc_charge_raw": x(0x453) | x(0x454) << 8 | x(0x455) << 16,
"flag_eco": y(0x585),
"relay1": bool(y(0x586) & 1),
"relay2": bool(y(0x586) & 2),
# meaningful only in a three-phase setup, otherwise these cells hold garbage
"i_phase": [phase_current(y(0x528), y(0x529)),
phase_current(y(0x52A), y(0x52B)),
phase_current(y(0x52C), y(0x52D))],
"errors": {},
}
s["p_acc"] = round(s["u_acc"] * s["i_acc"])
for name, addr in (("RSErrSis", 0x42A), ("RSErrJobM", 0x42B), ("RSErrJob", 0x42C),
("RSWarning", 0x42D), ("RSErrDop", 0x447), ("I2C_Err", 0x45A),
("F_Acc_Over", 0x41C), ("F_Net_Over", 0x41D)):
v = x(addr)
if v:
s["errors"][name] = {"value": v, "bits": decode_bits(name, v)}
return s
def print_status(s):
rows = [
("Mode", "%d - %s" % (s["mode"], s["mode_text"])),
("Battery voltage", "%.1f V" % s["u_acc"]),
("Battery current", "%.1f A (coarse %d A)" % (s["i_acc"], s["i_acc_coarse"])),
("Battery power (calc.)", "%d W" % s["p_acc"]),
("MAP load power", "%d W" % s["p_load"]),
("Grid voltage", "%d V" % s["u_net"]),
("Grid current", "%.1f A" % s["i_net"]),
("Grid power", "%d W%s" % (s["p_net"], " (export to grid)" if s["net_reverse"] else "")),
("Grid frequency", "%s Hz" % s["f_net"]),
("Output voltage", "%d V" % s["u_out"]),
("Output frequency", "%s Hz" % s["f_out"]),
("Battery temp.", "%d °C" % s["t_acc"]),
("Transformer temp.", "%d °C" % s["t_tor"]),
("Transistor temp.", "%d °C" % s["t_trans"]),
("Last out-of-range U", "%d V" % s["u_net_limit"]),
("Relay 1 / Relay 2", "%s / %s" % ("on" if s["relay1"] else "off",
"on" if s["relay2"] else "off")),
("Energy from grid", "%.2f kWh" % (s["e_net_raw"] / 100)),
("Energy from battery", "%.2f kWh" % (s["e_acc_raw"] / 100)),
("Battery charge energy", "%.2f kWh" % (s["e_acc_charge_raw"] / 100)),
]
w =max(len(r[0]) for r in rows)
print(time.strftime("%Y-%m-%d %H:%M:%S"))
for k, v in rows:
print(" %-*s : %s" % (w, k, v))
if s["errors"]:
print(" Errors:")
for name, err in s["errors"].items():
print(" %s = 0x%02X: %s" % (name, err["value"], "; ".join(err["bits"])))
else:
print(" No errors")
def main():
ap = argparse.ArgumentParser(description="Read MAP inverter parameters via ser2net")
ap.add_argument("--host", default=DEFAULT_HOST)
ap.add_argument("--port", type=int, default=DEFAULT_PORT)
ap.add_argument("--timeout", type=float, default=1.0, help="per-byte timeout, s")
ap.add_argument("--loop", type=float, metavar="SEC", help="poll every SEC seconds")
ap.add_argument("--json", action="store_true", help="JSON output")
args = ap.parse_args()
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
with MapClient(args.host, args.port, args.timeout) as c:
while True:
try:
s = read_status(c)
if args.json:
print(json.dumps(s, ensure_ascii=False), flush=True)
else:
print_status(s)
print(flush=True)
except MapError as e:
print("Error: %s" % e, file=sys.stderr)
if not args.loop:
return 1
if not args.loop:
return 0
time.sleep(args.loop)
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
pass
except OSError as e:
print("ser2net connection error: %s" % e, file=sys.stderr)
sys.exit(2)
@@ -0,0 +1 @@
paho-mqtt==2.1.0
@@ -0,0 +1,107 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: markdown-vault-mcp
namespace: mcp
spec:
interval: 30m
chartRef:
kind: OCIRepository
name: markdown-vault-mcp
driftDetection:
mode: enabled
values:
controllers:
app:
containers:
app:
image:
repository: ghcr.io/pvliesdonk/markdown-vault-mcp
tag: "v3.1.0"
env:
MARKDOWN_VAULT_MCP_SOURCE_DIR: /vault
MARKDOWN_VAULT_MCP_INDEX_PATH: /data/state/index.db
MARKDOWN_VAULT_MCP_EMBEDDINGS_PATH: /data/state/embeddings
MARKDOWN_VAULT_MCP_ATTACHMENT_EXTENSIONS: "*"
MARKDOWN_VAULT_MCP_STATE_PATH: /data/state/state.json
MARKDOWN_VAULT_MCP_READ_ONLY: "false"
MARKDOWN_VAULT_MCP_BASE_URL: "https://md-vault.mcp.aggtaa.com"
MARKDOWN_VAULT_MCP_OIDC_CONFIG_URL: "https://auth.aggtaa.com/.well-known/openid-configuration"
MARKDOWN_VAULT_MCP_OIDC_CLIENT_ID: "markdown-vault-mcp"
MARKDOWN_VAULT_MCP_OIDC_CLIENT_SECRET:
valueFrom:
secretKeyRef:
name: markdown-vault-mcp-secrets
key: OIDC_CLIENT_SECRET
MARKDOWN_VAULT_MCP_OIDC_JWT_SIGNING_KEY:
valueFrom:
secretKeyRef:
name: markdown-vault-mcp-secrets
key: OIDC_JWT_SIGNING_KEY
MARKDOWN_VAULT_MCP_BEARER_TOKEN:
valueFrom:
secretKeyRef:
name: markdown-vault-mcp-secrets
key: BEARER_TOKEN
probes:
# first boot downloads embedding models from HuggingFace Hub
# and builds the vector index/embeddings before binding the
# port - the default liveness probe budget (periodSeconds=10,
# failureThreshold=3 = 30s) isn't enough, so it gets killed
# mid-startup on every restart, in an endless loop.
startup:
enabled: true
spec:
periodSeconds: 5
failureThreshold: 36
liveness:
enabled: true
readiness:
enabled: true
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 1Gi
service:
app:
controller: app
ports:
http:
port: 8000
ingress:
app:
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: md-vault.mcp.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: markdown-vault-mcp-tls
hosts:
- md-vault.mcp.aggtaa.com
persistence:
vault:
type: persistentVolumeClaim
existingClaim: markdown-vault-mcp-vault
globalMounts:
- path: /vault
state:
type: persistentVolumeClaim
suffix: state
accessMode: ReadWriteOnce
size: 2Gi
storageClass: longhorn
retain: true
globalMounts:
- path: /data/state
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: markdown-vault-mcp
namespace: mcp
spec:
interval: 1h
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- helmrelease.yaml
- secret.enc.yaml
- pv-pvc-vault.yaml
@@ -0,0 +1,35 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: markdown-vault-mcp-vault-nfs
spec:
capacity:
storage: 30Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
mountOptions:
- nfsvers=4.1
nfs:
server: 10.0.64.5
path: /export
claimRef:
namespace: mcp
name: markdown-vault-mcp-vault
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: markdown-vault-mcp-vault
namespace: mcp
annotations:
helm.sh/resource-policy: keep
spec:
accessModes:
- ReadWriteMany
storageClassName: ""
volumeName: markdown-vault-mcp-vault-nfs
resources:
requests:
storage: 30Gi
@@ -0,0 +1,24 @@
apiVersion: v1
kind: Secret
metadata:
name: markdown-vault-mcp-secrets
namespace: mcp
stringData:
BEARER_TOKEN: ENC[AES256_GCM,data:vRsc/kmZ1qySDUrI1dMeLqSSn+jEgvPivw3dRYTLr7NRm6T7SYnmhhBb81CG6xQAl0Wz8RfOCescQpUJlbPl4w==,iv:Zha9o8uIdR6DPMk+KRL6ZnKdh3zPXuyjCCK9yKnuYFI=,tag:l+8oHY/hgZcKVQx9yiaEmg==,type:str]
OIDC_CLIENT_SECRET: ENC[AES256_GCM,data:2unvRtI/V0aTULgSacGvEoYYQN0uWBR3b2TKjl7hjvQTXesuglTtSNz9YsjDqc8ZHGNBndiz2Q6VZagS3ERprw==,iv:fjMhiKamjbIrNYAK9TU2PPn0smJR+Kr3qhRhEUbjm04=,tag:wuUZbccOBoFcdiTPqb8QCQ==,type:str]
OIDC_JWT_SIGNING_KEY: ENC[AES256_GCM,data:63S/0TsEis959MJRLd5atPXQrv5pNMC5iyMHJvT67luLLPkd2bdgfmX4Pu7R3Hr5YIGoujn1/xCuOdZH0RtIog==,iv:3Gi2ZukSzU9SIy7fPzmlvWbA4ykaNgta95DuviHf5zk=,tag:BPUXUPMIxW9kbyyQqoz0gQ==,type:str]
sops:
age:
- recipient: age1kct5g7d7eus2hvdnmx99ewsrtz70lhlasxa25euqwdctqhwqqpxqz0n7gg
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPOVJCaXpjeUEzWkN4SEdz
RnFkZmtXa0IwaE9xdGtBdVRwV1B5MkdDbVMwCmVYeUJpQmQwZ29BbnRPK1AzWGJx
NHV0NnRxenRWV2M4MytrZ3llNmxaMzQKLS0tIHVOcDVyTDkrNHJRMkhRVzFPSGpW
V1RRNTdiUTBjSWZlMlEvRXF3OUowZ00KXf/+ucgZuDAczE1WlEW7D2/cR+sB66BQ
7JC42VJiyEf2djlODmvK26bDTeiJ/Uk9HsSuvamGUM8b8vAj2w/r/A==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-07-27T06:42:40Z"
mac: ENC[AES256_GCM,data:XRxbyHL4iJMoRMb4X6C2y1ChTNSkqou01bwtIb6zNfwcGCt7uMGbm1HxvmjbdeJI/zQIIVfgxbbTxhZw+sL2FF+PYAjPcaGvaJ86O97lr9fUB30NC0tMluW82pt2iiisK+6dEEPH9i/kGFrJHN6RlaYVkEfv6U8xjlpIcaNx0+A=,iv:YnqMRlHl1WVYNqqN9hRH88hgIALrfgv6WrJGyeNDstI=,tag:M1DYFr3G9ub/R2kNF8ZnNQ==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.11.0
@@ -0,0 +1,6 @@
apiVersion: v2
name: mosquitto
description: Single-node Eclipse Mosquitto MQTT broker for homelab IoT
type: application
version: 0.1.0
appVersion: "2"
@@ -0,0 +1,14 @@
{{- define "mosquitto.fullname" -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "mosquitto.labels" -}}
app.kubernetes.io/name: mosquitto
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{- define "mosquitto.selectorLabels" -}}
app.kubernetes.io/name: mosquitto
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
@@ -0,0 +1,22 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mosquitto.fullname" . }}-config
labels:
{{- include "mosquitto.labels" . | nindent 4 }}
data:
mosquitto.conf: |
persistence true
persistence_location /mosquitto/data/
log_dest stdout
listener {{ .Values.service.mqttPort }}
allow_anonymous false
password_file /mosquitto/secrets/{{ .Values.auth.secretKey }}
{{- if .Values.websockets.enabled }}
listener {{ .Values.websockets.port }}
protocol websockets
allow_anonymous false
password_file /mosquitto/secrets/{{ .Values.auth.secretKey }}
{{- end }}
@@ -0,0 +1,25 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "mosquitto.fullname" . }}
labels:
{{- include "mosquitto.labels" . | nindent 4 }}
{{- if .Values.service.loadBalancerPool }}
loadBalancerPool: {{ .Values.service.loadBalancerPool }}
{{- end }}
spec:
type: {{ .Values.service.type }}
{{- if .Values.service.loadBalancerIP }}
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
{{- end }}
selector:
{{- include "mosquitto.selectorLabels" . | nindent 4 }}
ports:
- name: mqtt
port: {{ .Values.service.mqttPort }}
targetPort: mqtt
{{- if .Values.websockets.enabled }}
- name: ws
port: {{ .Values.websockets.port }}
targetPort: ws
{{- end }}
@@ -0,0 +1,70 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "mosquitto.fullname" . }}
labels:
{{- include "mosquitto.labels" . | nindent 4 }}
spec:
serviceName: {{ include "mosquitto.fullname" . }}
replicas: 1
selector:
matchLabels:
{{- include "mosquitto.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "mosquitto.selectorLabels" . | nindent 8 }}
spec:
securityContext:
runAsUser: {{ .Values.securityContext.runAsUser }}
runAsGroup: {{ .Values.securityContext.runAsGroup }}
fsGroup: {{ .Values.securityContext.runAsGroup }}
containers:
- name: mosquitto
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: mqtt
containerPort: {{ .Values.service.mqttPort }}
{{- if .Values.websockets.enabled }}
- name: ws
containerPort: {{ .Values.websockets.port }}
{{- end }}
env:
- name: MOSQUITTO_UNSAFE_ALLOW_SYMLINKS
value: "1"
volumeMounts:
- name: config
mountPath: /mosquitto/config
- name: secrets
mountPath: /mosquitto/secrets
readOnly: true
{{- if .Values.persistence.enabled }}
- name: data
mountPath: /mosquitto/data
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: config
configMap:
name: {{ include "mosquitto.fullname" . }}-config
- name: secrets
secret:
secretName: {{ .Values.auth.existingSecret }}
defaultMode: 0400
{{- if not .Values.persistence.enabled }}
- name: data
emptyDir: {}
{{- end }}
{{- if .Values.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: {{ .Values.persistence.storageClassName }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- end }}
@@ -0,0 +1,38 @@
image:
repository: eclipse-mosquitto
tag: "2.1.2-alpine"
pullPolicy: IfNotPresent
# not really needed, as StatefulSet has hardcoded "replicas: 1"
replicaCount: 1
securityContext:
runAsUser: 1883
runAsGroup: 1883
persistence:
enabled: true
size: 1Gi
service:
type: LoadBalancer
mqttPort: 1883
loadBalancerPool: "mqtt"
# or loadBalancerIP: X.X.X.X
auth:
# Mount a Secret with a pre-generated mosquitto passwd file instead of
# putting credentials in values.yaml / git. See README for how to create it.
existingSecret: mosquitto-passwd
secretKey: passwd
websockets:
enabled: false
port: 9001
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
memory: 128Mi
@@ -0,0 +1,52 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: mosquitto
namespace: mqtt
spec:
interval: 30m
chart:
spec:
chart: ./clusters/base/apps/mosquitto/chart
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
reconcileStrategy: Revision
driftDetection:
mode: enabled
values:
image:
repository: eclipse-mosquitto
tag: "2.1.2-alpine"
pullPolicy: IfNotPresent
replicaCount: 1
securityContext:
runAsUser: 1883
runAsGroup: 1883
persistence:
enabled: true
size: 1Gi
service:
type: LoadBalancer
mqttPort: 1883
loadBalancerPool: "mqtt"
auth:
existingSecret: mosquitto-passwd
secretKey: passwd
websockets:
enabled: false
port: 9001
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
memory: 128Mi
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- secret.enc.yaml
- helmrelease.yaml
@@ -0,0 +1,22 @@
apiVersion: v1
kind: Secret
metadata:
name: mosquitto-passwd
namespace: mqtt
stringData:
passwd: ENC[AES256_GCM,data:YpTczjZ73h4Td+TmEZIgil9DN7WfVwBsi75F/t3D1oWGjGsifklnRsZUJqXiXyUwIxTxlFiWgfb01LjmILE0nB9rxoEbFdrASaLgtnCoYz/fvSZLqwCK+FSSub3+r0nJTNvxzD+bC/av8JEPNDW/8CSrHMrTrVCcLhAchOAvviRLgO18862XWcvPh1plO8luaZG38cuh+VbKKwidkwozc6B7Ghys5NfZuB1rP4vLZ6/f1PzcFRugtJ0fzzl/BA==,iv:9FKm4V/lYXott/6aVBKg4g8GtzVqgT/xndj0CKqMm+Y=,tag:41gCVbrK6Rp2IvVUt98Hpw==,type:str]
sops:
age:
- recipient: age1u236u5mzkzj72y4mfxx68gfqw0u0j6tdgkudnm5pdfpe7apq4u6s89jr2h
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBobk1BQW0yemEzV1FQM2ZJ
YkZJbjJPZlRiSDlyamx1WVdyNTJBNXo1YlQwCnhBT01SeE0rZDVOcFRkZm9KQ0t2
dUIyOHpzenVZc3lsa2hmMjF3LzRGMFUKLS0tIENmM05Ud2xGblNpdjZjWWFPeWg3
UEtuZWZ5enkwSGk0MXR1czBibzQ1M2sKfp+76C0X2DgsffWZVtBevI4lCs8RM09p
tzsrneX4iIX6WCn1kRLsWTZpN6Vg8Wsl6kUWnSA3WfHWI9W4CJdrag==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-07-21T19:09:49Z"
mac: ENC[AES256_GCM,data:qecTVUaC/3j/oP1DVnXEKV9Ub+k5gvGHfteol4W5I3gq8IB2YYZ+vfuinPVJJeSsN5HvK62ftnIgRNu0dtzDOrnuZENczEMwPrOz90RCUbX7WBeKh23KZ8gSSw2MBn6gsDsz1gXFWENIyjQTJFZ4qAJY6jfeH2IFC9RkpoU3j14=,iv:ksLG/aHHBAkl6aA52Xufs2vijZlpsaXccoxheYV3Gm4=,tag:4YiaPGMBWSWhVBO1tNF6vQ==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.13.2
@@ -0,0 +1,51 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: nfs-ganesha-config
namespace: nfs
data:
ganesha.conf: |
NFS_Core_Param {
NFS_Port = 2049;
Protocols = NFSv4;
Fsid_Device = true;
}
NFSV4 {
RecoveryBackend = fs_ng;
RecoveryRoot = /recovery;
Lease_Lifetime = 40;
Grace_Period = 50;
}
VFS {
Link_Support = true;
Symlink_Support = true;
CanSetTime = true;
umask = 0000;
}
EXPORT {
Export_Id = 1;
Path = /export;
Pseudo = /export;
SecType = sys;
Protocols = NFSv4;
FSAL {
Name = VFS;
}
CLIENT {
Clients = *;
Access_Type = RW;
Squash = No_Root_Squash;
Protocols = NFSv4;
}
}
LOG {
Default_Log_Level = INFO;
}
@@ -0,0 +1,130 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: nfs-ganesha
namespace: nfs
spec:
interval: 30m
chartRef:
kind: OCIRepository
name: nfs-ganesha
driftDetection:
mode: enabled
values:
controllers:
app:
strategy: Recreate
podDisruptionBudget:
maxUnavailable: "0"
initContainers:
mtab-setup:
image:
repository: hectorm/nfs-ganesha
tag: v10
# hectorm does not publish exact versions, v10.0 digest is 93df70b78...
digest: sha256:93df70b78aae56957f4a4f5c606c4250d84b729435f76132dca07b7290f5bfce
command:
- sh
- -c
- cat /proc/self/mounts > /mtab-vol/mtab
fix-permissions:
image:
repository: hectorm/nfs-ganesha
tag: v10
# hectorm does not publish exact versions, v10.0 digest is 93df70b78...
digest: sha256:93df70b78aae56957f4a4f5c606c4250d84b729435f76132dca07b7290f5bfce
command:
- sh
- -c
- chmod -R a+rwX /export
containers:
app:
image:
repository: hectorm/nfs-ganesha
tag: v10
# hectorm does not publish exact versions, v10.0 digest is 93df70b78...
digest: sha256:93df70b78aae56957f4a4f5c606c4250d84b729435f76132dca07b7290f5bfce
securityContext:
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
add:
- CHOWN
- DAC_OVERRIDE
- DAC_READ_SEARCH
- FOWNER
- FSETID
- SETGID
- SETUID
- SYS_RESOURCE
probes:
liveness:
enabled: true
spec:
initialDelaySeconds: 2
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readiness:
enabled: true
spec:
initialDelaySeconds: 1
periodSeconds: 2
timeoutSeconds: 1
failureThreshold: 2
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi
service:
app:
controller: app
type: LoadBalancer
labels:
loadBalancerPool: nfs
ports:
nfs:
port: 2049
protocol: TCP
persistence:
config:
type: configMap
name: nfs-ganesha-config
globalMounts:
- path: /etc/ganesha/ganesha.conf
subPath: ganesha.conf
readOnly: true
export:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 30Gi
storageClass: longhorn
retain: true
globalMounts:
- path: /export
run:
type: emptyDir
globalMounts:
- path: /run
tmp:
type: emptyDir
globalMounts:
- path: /tmp
recovery:
type: emptyDir
globalMounts:
- path: /recovery
etc-mtab:
type: emptyDir
advancedMounts:
app:
app:
- path: /etc/mtab
subPath: mtab
mtab-setup:
- path: /mtab-vol
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: nfs-ganesha
namespace: nfs
spec:
interval: 1h
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- configmap.yaml
- helmrelease.yaml
+96
View File
@@ -0,0 +1,96 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: ntfy
namespace: notify
spec:
interval: 30m
chartRef:
kind: OCIRepository
name: ntfy
install:
remediation:
retries: -1
upgrade:
remediation:
retries: 5
driftDetection:
mode: enabled
values:
controllers:
ntfy:
containers:
app:
image:
repository: binwiederhier/ntfy
tag: v2.34.0
args:
- serve
env:
NTFY_BASE_URL: https://notify.aggtaa.com
NTFY_LISTEN_HTTP: ":80"
NTFY_BEHIND_PROXY: "true"
NTFY_CACHE_FILE: /var/lib/ntfy/cache.db
NTFY_AUTH_FILE: /var/lib/ntfy/user.db
NTFY_AUTH_DEFAULT_ACCESS: deny-all
NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments
NTFY_ENABLE_LOGIN: "true"
envFrom:
- secretRef:
name: ntfy-secrets
probes:
liveness:
enabled: true
custom: true
spec:
httpGet:
path: /v1/health
port: 80
readiness:
enabled: true
custom: true
spec:
httpGet:
path: /v1/health
port: 80
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 256Mi
service:
app:
controller: ntfy
ports:
http:
port: 80
ingress:
app:
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: notify.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: ntfy-tls
hosts:
- notify.aggtaa.com
persistence:
data:
type: persistentVolumeClaim
storageClass: longhorn
accessMode: ReadWriteOnce
size: 2Gi
retain: true
globalMounts:
- path: /var/lib/ntfy
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: ntfy
namespace: notify
spec:
interval: 1h
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- helmrelease.yaml
- secret.enc.yaml
+25
View File
@@ -0,0 +1,25 @@
apiVersion: v1
kind: Secret
metadata:
name: ntfy-secrets
namespace: notify
stringData:
#ENC[AES256_GCM,data:XneiueSaauWV0MEAzJcs730dwgrSt970o99Jq9iYbAF/RGAXPq0aMhSdNtK4PIgY5nSuVDsaGjXMEu+hF7kX4zWdIQg8MQ2n82fpcNc15RCMsyMNn9WVI1WKr5y7gonz5w==,iv:SPiP6hSzqdBk9WUVKT6TYp3qdZ3gje70CclLXaQNPkw=,tag:H8RtqMaF3bmMNUvfZMJtXQ==,type:comment]
NTFY_AUTH_USERS: ENC[AES256_GCM,data:G6VE3ZkkiHU1dFY52J2XVKg4RL55SBTNZRp9DKeUHFJho/PURi+UIzS0gk2377ZAIRdX7gn8DOqfCZ4gvUw40TnlC/wFRAgZO1a6G1WKEZHceUr3cwFGQQhN0pwbc7RfK+amWR4Ov3m/lsNCZjU2KxUps8jNCvnGNR9IxHXBGoNt7PzeNnn6vL5JDBFba3MNsNDUVOm3Q4pTwG9pb1cvQYUU89OMIq/MkZfwotlR0HyEG/8hDiClr9PGJSuVSerL3JmtV10txM2bKRCFRtDb1DtYrRu8EzQ=,iv:IukIdkjr1vRdQJBMI+adN8O0UPo4Vv25gB5vCzim74A=,tag:QYGGERjRr7n1Pww9bg6Oiw==,type:str]
#ENC[AES256_GCM,data:WDat/Zx7qfe6gY1t8npcfvHz+EpTbtEuHJMoUijlJq5FJEu6nVbrASJOkkWDxfhT,iv:6GX4Jff9YZ27RHaCym1ziSkZiF7VifRkxjleLcYVYHY=,tag:RhsfsYqNc9dyFc53TxAFcw==,type:comment]
NTFY_AUTH_ACCESS: ENC[AES256_GCM,data:Mc4c8bDGwIEUZnx2x1N5IDiLPCdHZw==,iv:LCFN1XXjGfzel/3O4FHumcJpn1DhqdqOhjjpzBSUpYs=,tag:QCqj6hIClhXTnINq+hQuow==,type:str]
sops:
age:
- recipient: age1uzs33ep6k3htnuwyyp54y373wkf7urd0qvk5kxcmptr5tt0fffus4v06gn
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4ZTg3RVF1Y2sxUjAzTVgw
d2ZkQVpmdzNqYzIwRVR1RGExUjM4SVRMSVN3Cis1bEpZNWZkY1VaWDkzUUlnN2RZ
TVUyZ2VNb0MvS3R5c283Ui9DRzkydUEKLS0tIFZMKzhJWnVZUVRsSFNHYStUaGti
aGFRSVg5alBnNnA4emxkeFFpZ3BnUFkKUPOqUjkjSl77Xvb3i+THWINbvtzrioqF
QBkQh5SSzq8fWrHoHHp7XHu02+p34dEJMOmEg1rlhu7pYRMNbbaRsQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-10-26T09:44:43Z"
mac: ENC[AES256_GCM,data:0z4ety9M00u1vIGn1NyqO9K42IGOQt28L08sKU4D3aB6sroqbY6UkafabBYGyzB7LCaOc3iukDCFtB8hBwg6UarNPbeQlZqG3yuHmVuNB2vOubE4U1YFxeTJ5bNvyP4kpSp6DwUiC9QeJtQ0T98aqj9R7dB3x4vU1s/hPmYSLxU=,iv:+5kSIP8prdAOaYCUhQzUlugncFH54tTMweMfo+tP4BY=,tag:bpjq0X3gCy+LbLeSmIX5Jw==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.11.0
@@ -0,0 +1,73 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: syncthing
namespace: syncthing
spec:
interval: 10m
chartRef:
kind: OCIRepository
name: syncthing
driftDetection:
mode: enabled
values:
controllers:
app:
containers:
app:
image:
repository: syncthing/syncthing
tag: "2.1.3"
env:
TZ: Europe/Moscow
probes:
liveness:
enabled: true
readiness:
enabled: true
service:
app:
controller: app
ports:
http:
port: 8384
ingress:
app:
className: traefik
# the GUI has no auth of its own out of the box, so it's gated
# behind Authelia the same way fava/zigbee2mqtt are.
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.middlewares: auth-tinyauth@kubernetescrd
hosts:
- host: sync.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: syncthing-tls
hosts:
- sync.aggtaa.com
persistence:
data:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 1Gi
storageClass: longhorn
retain: true
globalMounts:
- path: /var/syncthing
# same nfs-ganesha export webdav and markdown-vault-mcp use
# (10.0.64.5:/export); mounted at the same container path (/data)
# as webdav so the layout matches (/data/shared/documents/...).
shared:
type: persistentVolumeClaim
existingClaim: syncthing-shared
globalMounts:
- path: /data
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: syncthing
namespace: syncthing
spec:
interval: 10m
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- pv-pvc-shared.yaml
- helmrelease.yaml
@@ -0,0 +1,35 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: syncthing-shared-nfs
spec:
capacity:
storage: 30Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
mountOptions:
- nfsvers=4.1
nfs:
server: 10.0.64.5
path: /export
claimRef:
namespace: syncthing
name: syncthing-shared
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: syncthing-shared
namespace: syncthing
annotations:
helm.sh/resource-policy: keep
spec:
accessModes:
- ReadWriteMany
storageClassName: ""
volumeName: syncthing-shared-nfs
resources:
requests:
storage: 30Gi
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long
@@ -0,0 +1,108 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: tinyauth
namespace: auth
spec:
interval: 10m
chartRef:
kind: OCIRepository
name: tinyauth
driftDetection:
mode: enabled
values:
controllers:
tinyauth:
containers:
app:
image:
repository: ghcr.io/tinyauthapp/tinyauth
tag: v5.2.0
env:
TZ: Europe/Moscow
TINYAUTH_APPURL: https://login.aggtaa.com
TINYAUTH_UI_TITLE: "Welcome"
TINYAUTH_RESOURCES_PATH: /resources
TINYAUTH_UI_BACKGROUNDIMAGE: /resources/background.webp
TINYAUTH_AUTH_USERSFILE: /config/users
TINYAUTH_AUTH_SECURECOOKIE: "true"
TINYAUTH_LABELPROVIDER: none
TINYAUTH_ANALYTICS_ENABLED: "false"
TINYAUTH_DATABASE_DRIVER: memory
TINYAUTH_OIDC_PRIVATEKEYPATH: /oidc/key.pem
TINYAUTH_OIDC_PUBLICKEYPATH: /oidc/key.pub
TINYAUTH_OIDC_CLIENTS_FILEBROWSER_CLIENTID: filebrowser-quantum
TINYAUTH_OIDC_CLIENTS_FILEBROWSER_NAME: files.aggtaa.com
TINYAUTH_OIDC_CLIENTS_FILEBROWSER_TRUSTEDREDIRECTURIS: https://files.aggtaa.com/api/auth/oidc/callback
TINYAUTH_OIDC_CLIENTS_FILEBROWSER_CLIENTSECRET:
valueFrom:
secretKeyRef:
name: tinyauth-secrets
key: TINYAUTH_OIDC_CLIENTS_FILEBROWSER_CLIENTSECRET
TINYAUTH_OIDC_CLIENTS_MDVAULT_CLIENTID: markdown-vault-mcp
TINYAUTH_OIDC_CLIENTS_MDVAULT_NAME: markdown-vault-mcp
TINYAUTH_OIDC_CLIENTS_MDVAULT_TRUSTEDREDIRECTURIS: https://md-vault.mcp.aggtaa.com/auth/callback
TINYAUTH_OIDC_CLIENTS_MDVAULT_CLIENTSECRET:
valueFrom:
secretKeyRef:
name: tinyauth-secrets
key: TINYAUTH_OIDC_CLIENTS_MDVAULT_CLIENTSECRET
probes:
liveness:
enabled: true
readiness:
enabled: true
resources:
requests:
cpu: 20m
memory: 64Mi
limits:
memory: 256Mi
service:
app:
controller: tinyauth
ports:
http:
port: 3000
ingress:
app:
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: login.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: tinyauth-tls
hosts:
- login.aggtaa.com
persistence:
users:
type: secret
name: tinyauth-secrets
globalMounts:
- path: /config/users
subPath: users
readOnly: true
oidc-key:
type: secret
name: tinyauth-oidc-key
globalMounts:
- path: /oidc
readOnly: true
resources:
type: configMap
name: tinyauth-resources
globalMounts:
- path: /resources
readOnly: true
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: tinyauth
namespace: auth
spec:
interval: 1h
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- helmrelease.yaml
- middleware.yaml
- secret.enc.yaml
- configmap.yaml
- oidc-key.enc.yaml
@@ -0,0 +1,14 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: tinyauth
namespace: auth
spec:
forwardAuth:
address: http://tinyauth.auth.svc.cluster.local:3000/api/auth/traefik
trustForwardHeader: true
authResponseHeaders:
- Remote-User
- Remote-Groups
- Remote-Name
- Remote-Email
@@ -0,0 +1,23 @@
apiVersion: v1
kind: Secret
metadata:
name: tinyauth-oidc-key
namespace: auth
stringData:
key.pem: ENC[AES256_GCM,data:1LGq+HOfsvba2hrMBgwTiaJY4oSXNVRRyBQt7k+Dv5ZunCNbc57qxGIAi259zekb4yqdnP9SHUo0Uzmz1lgOUwhw4pPNq8YmWkjoLthjBB5FaNqilifUzFXYG3+fC4WP0IxHWUFDhB4tgP9zwnc682/QnQBMKzDXAL9bnRf8OTemNLnAjHt/+vbOtW6ySSmjnF+RRS0gz9/CjPwnCwfqqqRoIaiHT2I5nKuzSggGDNRENlGHsUCBypP08ZbuyQ+wRMWNDhtuG8KY8pWxkvOfwAjbJtJ34/xTGeWwCUQ0fs6khMXzMmvnPKmG/HTAAWrgLeMlhicDLB74nBMY7zbjX1y2HzJPVXAnWNE2sbWXhBIhi+/koyRdWSoUoD7flEu1O44cgRJSh2QqEa4cw4NC+tRm0Dm0R37vOEgMHF/D3cKBxSmgKLxUKw+ZCn5aYF2+Dwo5L+HtuI0VnQ5caq0HCrPN17m63gceVS/mbGYT/A9XSvxhWh5hrtzzn1v3xER/HdOEK1GlZBOL2QPfzyRx9Yx05f/NLGghK+Jk/OBtaV8+0ORGfjJFi+uU2tus8gznESTtZGG6Oz25aIYj7cYCNM93szk2q2lle8wCK8DX48wD5LEwS/7zcWJ2J0FeH/+ihIi2N6cRAgT2P5CHATFX4ghQkAH3UZF0oC2x0EfRXmVE9K2twuD70CSqmBIZvSqC1zEb5KpL283E5lsfVZu3RRryplONMaZZM2bIJqwaEq0wqbtvTnKFTWQEA4JRzgy4i8cFyxfq8ELoEacsjVgZCrASvMjK9NmuqUt4dbKMSuIz0S780Om1V0OoshJiGi9Thu7AWIi0WURRi0ZkvaiLDuqwx+VS5Vt+1y6JByUlsKZg4Dg6saYePKEqClD5nqTv4KsVU3psldOdlS/S4MhBCK31RPAI0yZbDF7job1K4pYWpzuJNH3jh/EDqzbBbNpnTnt6VA9KJ6obJ1vzOVAQB804WB0+hM8ap8vcqHLRPhqa7wZ9mJKVkPBxcr0B3ZulXn428FpjEzCLhUPXVNdJsxpPq0LtmN4yHsfGA/BVwXveT2gLxF+cT5IHOpQVR1ZI/I62ebCfupHYPhZ5aU/UFbCykc2PPQTVM5acVKnwbkU9nR8PmrrZxNmgTQ1akrrQoAMkPkKBsE8R3MIH4md4QhH1B7NV79OwJHrShGDajCPPkGnvFP6w+S4ecdwLkutg57Vhf9Gpq81ztzaq1W/W5y1ax4rGowlRU5nOHy5hDfyLUr0O79319oit4LD7VVWzFiBxWFNaky617V9B/xXFqxInGmQAJVPoPkCE9cHCNVbrblAeND36zjcr/tEWYlNCakt3sNFRQTWaO0MoF1+iLxCQvK7dpRpA3IhOPhFdBRj11J9PI+zpeSXZHYXP2XrfYna062KYb7BxiRiCrO2HAtgKY2TTcA8LHYQ+QiNefSqiNSN3zI7vclb4D6RmDpbm8Hg+SZV1KdvBYw/laj0ctNOnTqwZY51foDgjDnhbRcR5M1mbpq3LUY6TUUoV4+blR1cUKk3pQD5eHVEB/gI5r/mLoK/aesDhVsqEsI1fmwzGESJOL2zRXwyzuPrWj65yfcN0dDK/d06o7vnHdaJIPrEaAmX0M+jD92thIrrUMgkD6r6WqtCYKy81tKqBkMIb4M4wO1ttZ/6WjM7LjNfGAeYhMOTI/LWjmHR/JKrAantSgRgsE6hNGA+C8VUQTpO1rDWFNXRb/vxsibFSypi481ydaySw6Qum/f+eLrLGJdIgWCtI3H+L42N80+k5l03pyuJixqCk0kqVrDQmITNdkGJvbqA7uR3YncSOjPIWn7q79KUca4qW+O1CwtgBJYJqSJiDCVwi3tZ6zv7eDBcq7PGrN4R3hcarY9GVo0Vr4YdSN9VU5PkaXFfG10XZyrHBOLmm4/2iy7NxxWY8d4Z3nXUjitEIAkUa/CGhA3WMiqMkkbeLeiRtmbXZ8ZcCBO5FEUSG/z11PyLRi0o5kct3GlaUtl/QRg4SBGx4t3iDf5gHPb298+ulSErL40GjKLyiZh+YfNPDqRdIXBpKTjOand1EZe1yiY3otCj9NVUdrvkSFL6lYXcCQvVtzLIvrfev/QVqpIOf9d0gxp/em4JTZ+YauRNu5fXcnqezncpQAcXy27O4y2+4cko8tk7aoBwcrl/fKp13nx22I62IJGzsLA7BNjRHUG13iddZoBOzhbJ3xlgNR0nNlIYNSQ==,iv:ZO366aMLKSUkaMdlejlGGZ8rLAQITDJ+rtqHIDjHJCk=,tag:m3eUo6FRn5Gmrf/ozMGldQ==,type:str]
key.pub: ENC[AES256_GCM,data:F0YQFFEAwB+bruva4HB4BKeIjAPhnssnd13JLMRMyLMfF8JdIBbjyKcz8NUVq8l5FxFDy3W3jAJmOfVqUwHfajgdT61S2Rj+dBXwV947ruwBiLS3rTnJvgfLIEOf1f4mG2hZH4X7jodInh7wL+IPC7N9Wqmccdfc1eTvn7GjM5KgU6yzsDIdq8CMTOWciRIgLz81sDCP6mLAy4AG+a6nyzhCYSfoQogbl00CP9i6JdV1FcJfrzQY2lgshgrDOynK2bNxwaRP0CVajAn5ccj8sawvIA5EgcEdoasfGGf3WCUnFKDVmYZDZjpeTGDnCRZMFIpKfXbNavaNn2Gwl1icR58UFKsi7tXCePY8MW12gseONFXDoU6OKo+xtgiEk5SPryrskK3m7Dq+9O7No12f2XNhISGlf/sRtY+WmxxXceUTywoOHVRkbcapODRs0svmJBeQZKJ85tE+Fre1l+1mjLGB4ZeaA0euKEZDtTXT95icepMaHIcbZHURwp5jRfYxp7a9VzpWY+m1rwNtnpHe4A16JoU+0ZARPLCs8kX/DqgZDaSy9f+IUjvQYbNTsUNEJCvtMZPWmr/Vz8Je61/8pGz99g==,iv:L5JDiEFJnBPcXGZMmFUVC8Uq12Bb08qYFWhdZCYBivY=,tag:Lpz7BAOaQ1GK9W6WaZ4LLw==,type:str]
sops:
age:
- recipient: age1rvyt7nlnjqeyt6ykwh9fj9d88kjmq9vetcx667ul3ql6fyfz4gqqh9lq9q
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFZGwrK2lEVXBWQnBaeVlx
aUVPQVRyOTB2WE5HdWR2VldUbmQxZTBuTFIwCks1dkc5aGVXWnpIN1NYNVA0em14
c3h4K3hLYkZlU0VhRUEyajFEZzI3UDgKLS0tIFpLQkxlWGtUTGhoVUpiSW9WOStT
blZOcjI3TS84K1hZY0ZxbS95SndsUUEKt3ZYkU2/5uPeU7C72luZYP3tLJJ73ERX
A8eUMsVOI5T7Y6/i4B1OQSt1xFBXkMSJu3YU739fxFumdb8juALxzg==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-10-20T08:48:16Z"
mac: ENC[AES256_GCM,data:IcHXiY7PJpNy/sGirwh7rm8XzyuC/ZfajaxolOgfP0p1bMF6yn2tzgf1vyHJszVEwXE7rLZEriR357PaHyGgkIxDnb76FwLTiw+feEBgpl/77HY1oOnI4yc0LrHh5fkanxadw/krJoQ8JD827Ek9h1LR7ICTJ40EDSpdfwdH/1E=,iv:OXMfJyXrXH4S5v2cTdwJijzE2/4L475ZArA/nw06s48=,tag:ykl0rz2xmhJ9NZIjuU111A==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.11.0
@@ -0,0 +1,24 @@
apiVersion: v1
kind: Secret
metadata:
name: tinyauth-secrets
namespace: auth
stringData:
users: ENC[AES256_GCM,data:C3Dhfymnr+WLWy18EpY9upXn+f7sY6w0cf3oXO9352PTPCP+rLYQTm8eiJtsU8gp/Uea7lMpsRPHSy8CoRdI8DOGT+hyPyK1ovjLdyO7bEiq0TtYOIFlHSR7Kwo0Cm+pZegSN9e1dm/v51+YyFfX3JpLaymYrlmXL3sFPQ0yOzEuMp3zXSV3VAkJe6gZrswIAUNhY0xmaGItr4Vc2isItDuc16lx7tW2wJHI7NgLI98p9FyU8HlXd9MsiPMxDaXSGeAwnjGU+TEvQv9NbhcZO0sUocSi2zgnRbi0i8Zt,iv:5OVryBqH4VNC71QdZkqZsoDrfbT5++Z5xZjgCFNOAu0=,tag:V+OC7Mad6Dw38u91gp6d/Q==,type:str]
TINYAUTH_OIDC_CLIENTS_FILEBROWSER_CLIENTSECRET: ENC[AES256_GCM,data:kBA3jDc+xFyvLNtfPjO+gYtRrT3HgdsoC1MktIgFomA=,iv:JKStNWy9BHcpkSwAG917sC4zzStKcgtU7ZXpKdSraRQ=,tag:VV96+U1EaIQevZHnCDmQag==,type:str]
TINYAUTH_OIDC_CLIENTS_MDVAULT_CLIENTSECRET: ENC[AES256_GCM,data:SiipG4qFp536YfrMqfjeZyIA5hxXfDH1JgGGDrkz0bb2mwdSV2S3szLffxNwibL9xAF/dST6I++N5ErLXGH10w==,iv:hzJyoLO1z4eBuDmBYJ5AfSh4RPggJN07EWeKbP+FFV4=,tag:R46K78XLC0BmQ0n6b+irQA==,type:str]
sops:
age:
- recipient: age1rvyt7nlnjqeyt6ykwh9fj9d88kjmq9vetcx667ul3ql6fyfz4gqqh9lq9q
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJS1llcTdHUmx2ZXdjVFh1
ekJheVN2MVBwa0V1a2JOY2pFM1pmYUJrMzFRCllaRnlnTGFVYTlQeXVxNXFsNEZu
bWUxd3BWZTlEOWxsZVRnREx6YUJ6em8KLS0tIGNDd1Znb0dlSDZEUE1JUkdWMlBG
ajBNVW1pa20zKzhISmFBUmdiazA4T1UK0wyvTMGsyJthx2Py1LITvRePQsjKN/9v
jqKjeeCuVutQCo3FvoVrlkkmiD19jqPDoXjWz95sTN6QjWmP7u6AIA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-10-20T08:39:59Z"
mac: ENC[AES256_GCM,data:pKMx3OwJ5x41WghATDCwDNJADs2KCQkDmDsjfbL6Vx98jFOL+MwvCRujOM2UzzAl8QnNLFddimmpNwoFBifQEbeHTpVnT1uqZkiSV89mM1nIoyYIo3exNoynVUoqp1NXxhfwRrRVC3beF3keTxES//kh8/W3as16R1vgBSLoj6s=,iv:AAiLBicP0tywLWXA67cin51TqV8TBXtlpjazta2PyFs=,tag:wSkJBOIhryUNVlGeKrgmnA==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.11.0
@@ -0,0 +1,69 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: vaultwarden
namespace: vaultwarden
spec:
interval: 30m
chartRef:
kind: OCIRepository
name: vaultwarden
driftDetection:
mode: enabled
values:
controllers:
vaultwarden:
containers:
app:
image:
repository: vaultwarden/server
tag: 1.37.0
env:
DOMAIN: "https://vw.aggtaa.com"
SIGNUPS_ALLOWED: "false"
WEBSOCKET_ENABLED: "true"
ADMIN_TOKEN:
valueFrom:
secretKeyRef:
name: vaultwarden-secrets
key: ADMIN_TOKEN
probes:
liveness:
enabled: true
readiness:
enabled: true
service:
app:
controller: vaultwarden
ports:
http:
port: 80
ingress:
app:
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: vw.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: vaultwarden-tls
hosts:
- vw.aggtaa.com
persistence:
data:
type: persistentVolumeClaim
storageClass: longhorn
accessMode: ReadWriteOnce
size: 2Gi
retain: true
globalMounts:
- path: /data
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: vaultwarden
namespace: vaultwarden
spec:
interval: 1h
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- secret.enc.yaml
- helmrelease.yaml
@@ -0,0 +1,23 @@
apiVersion: v1
kind: Secret
metadata:
name: vaultwarden-secrets
namespace: vaultwarden
stringData:
#ENC[AES256_GCM,data:b8Yuhc+eN6k2X6xPhUUBAMyNmJhZl+cpszwhb9OUiZeDJUNC1CUFTSD4//XH81kud+iM3/6o854S,iv:0F4/f5sjeFBqp7zZfJhYd8tkiDRlfKJ6p5n337THiWo=,tag:ZNHbaR0tbHT6aWzyfqeqPw==,type:comment]
ADMIN_TOKEN: ENC[AES256_GCM,data:pYY8Nc2PevrFGU2pbZaFH7O+WNJzOHSEXMM3TAFYHO46/GGzQjrxYbc9IY/8PgY86N73kQAFLvw2bpVnjiliilLpnxm9Z0LyYDlAhui8545J67IqoQFDyZt5o3NHceWRP2eCxEL4Bf9gD6j4YfObRVYrp38x2Q==,iv:IvzPkKi/w/S8ueYl/m8kckWiddfl1oQKYiT8S77IVAA=,tag:rD5RsmPt4uihMHWSt4gGwA==,type:str]
sops:
age:
- recipient: age109zta6l0cwpkxn5wek82kvauxn6yqrq80rsz4mvu5atdnpcn9pusuvnqk5
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6RVkzM041NFZDMzlYRllw
a3BRS0JGZ1B0UGYyaTRLUkp2UTJ1S1pybDFnCnZhVWNYMFRlSXlFVTMvRXlpdlF3
Vk1xT1pKV1hzSUYwQXd4ZG92cnhDY0UKLS0tIEtLWXZiZUFDeGhFdytKbTJHSjRH
YkJhS2FJOTdLbHlRelBpUWRNUTdTMDQKhgb66tV4L0PKkPY8DvbypTpcBlavUvRH
0+Rz/UxfZpX8u8G9GeqF5glIpNG2x28VSOHchJxhvMKHIUq9ZUrHGw==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-07-18T19:13:05Z"
mac: ENC[AES256_GCM,data:Ufdytxb9uH6JVXsWu39YhE1AfD/oG7bXhIwoTXYhYBE4IWvugrwZLCvBrgkfuazVXyKBuDIuEa5Vj/bzlTGDSlRNx7SDqabvilhmYAsBcuU5DAVU47pDz1YnXeHd9TuQI1dHAc1Q3uvkZwWNxh2YYXKZGwNMy3tOAkaOxwI1rMw=,iv:iUZenTP/9JhmjcR4p5+RM5K2YuXBLnRj0cd/RHseqFY=,tag:pesF8Pp8vBoQXcniwrep9g==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.13.2
+18
View File
@@ -0,0 +1,18 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: webdav-config
namespace: webdav
data:
config.yml: |
address: 0.0.0.0
port: 6065
directory: /webdav
permissions: R
users:
- username: "{env}WEBDAV_USERNAME"
password: "{env}WEBDAV_PASSWORD"
permissions: CRUD
+107
View File
@@ -0,0 +1,107 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: webdav
namespace: webdav
spec:
interval: 30m
timeout: 10m
chartRef:
kind: OCIRepository
name: webdav
driftDetection:
mode: enabled
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
controllers:
app:
containers:
app:
image:
repository: ghcr.io/hacdias/webdav
tag: v5.14.2
args:
- "-c"
- "/config.yml"
env:
TZ: Europe/Moscow
WEBDAV_USERNAME:
valueFrom:
secretKeyRef:
name: webdav-secrets
key: WEBDAV_USERNAME
WEBDAV_PASSWORD:
valueFrom:
secretKeyRef:
name: webdav-secrets
key: WEBDAV_PASSWORD
probes:
liveness:
enabled: true
readiness:
enabled: true
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 256Mi
service:
app:
controller: app
ports:
http:
port: 6065
ingress:
app:
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: dav.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: webdav-tls
hosts:
- dav.aggtaa.com
persistence:
config-yml:
type: configMap
name: webdav-config
globalMounts:
- path: /config.yml
subPath: config.yml
readOnly: true
data:
type: persistentVolumeClaim
existingClaim: ganesha-export
globalMounts:
- path: /webdav/export
# NAS may be off at pod start: the PVs mount with "bg", so the NFS
# mount lands on the host later. HostToContainer relays that late
# mount into the container instead of leaving an empty dir.
sda2:
type: persistentVolumeClaim
existingClaim: nas1-sda2
globalMounts:
- path: /webdav/nas1/sda2
mountPropagation: HostToContainer
sdb2:
type: persistentVolumeClaim
existingClaim: nas1-sdb2
globalMounts:
- path: /webdav/nas1/sdb2
mountPropagation: HostToContainer
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: webdav
namespace: webdav
spec:
interval: 1h
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"

Some files were not shown because too many files have changed in this diff Show More