fix(hermes): read the agent credentials from the managed scope #387

Merged
grachevko merged 1 commit from fix/hermes-managed-scope into main 2026-10-12 14:33:23 +00:00
Collaborator

What was wrong

Since 0.21.6 the gateway multiplexes every profile by default (gateway.multiplex_profiles cannot be turned off) and resolves a credential only through the profile secret scope: the profile's own .env plus the managed layer. gateway/config.py::_getenv reads through agent/secret_scope.get_secret, which fails closed while multiplexing, so a value that exists ONLY as a variable of the pod is invisible. api_server is then never enabled, port 8642 stays closed, the readiness probe never passes and the Helm upgrade times out — that is how the 0.21.6 rollout of grachevaeu failed, with Flux rolling the release back every five minutes.

What this fixes

  • kubernetes/apps/{grachevaeu,grachevko}/hermes/secrets/ks.yaml and …/hermes/env/ks.yaml — new: the credentials are Kustomizations of their own, in the order this repository keeps for a CR of an operator, the Secret the CR reads and the application that reads the Secret. secrets/ holds the generator of the API server key and the read point of the generated Secrets, env/ holds the ExternalSecret that renders the .env and reads both, and the Kustomization of the pod depends on env — the file it mounts has to exist before the container is created.
  • kubernetes/apps/{grachevaeu,grachevko}/hermes/secrets/app/secretstore-kubernetes.yaml — new: the read point for the values the cluster makes itself. A namespaced SecretStore of the kubernetes provider, constrained to one namespace, with no new permission — the operator's chart already carries secrets:get and serviceaccounts/token:create; the CA is the one Kubernetes maintains in every namespace (kube-root-ca.crt), not the internal CA of the gateway domains.
  • kubernetes/apps/{grachevaeu,grachevko}/hermes/env/app/externalsecret-managed-env.yaml — new: the credentials of the instance as one Secret, hermes-managed-env, rendered generically. Every property of the record of that instance in Vault becomes one KEY=VALUE line, the property name being the variable name; HINDSIGHT_API_KEY comes from the shared record ai/hindsight, API_SERVER_KEY from the generated Secret of the cluster. refreshInterval: 1m, so a key written in Vault reaches the pod within a minute and no commit is needed for a new key.
  • kubernetes/apps/{grachevaeu,grachevko}/hermes/app/helmrelease.yaml — the Secret is mounted read-only as /etc/hermes/.env, the file of the managed scope, in the directory the image already treats as its own: no HERMES_MANAGED_DIR (the variable only relocates the same directory, and with no config.yaml beside it the config half of the scope stays inert), no init container, no emptyDir. subPath, not a directory mount: a directory mount would hide the two files the image keeps in /etc/hermes (agent-browser-executable-path, image-provenance.json). The pod keeps no key of its own and nothing inside it is a source of truth: Hermes still writes its own $HERMES_HOME/.env, while the mounted file wins for every key it carries, so a stale line in the volume file changes nothing and needs no cleanup. The reloader annotation stays and now watches the Secret that carries the file.
  • kubernetes/apps/grachevaeu/hermes/app/externalsecret-keys.yaml (deleted) — the keys now arrive as .env lines, so the envFrom Secret and that ExternalSecret are gone. externalsecret-api-key.yaml is moved, unchanged apart from its comment, into secrets/app/: the key of the API server is generated once in the cluster and the value is read back through the kubernetes store instead of being handed to the pod as a variable — reading a Secret is not reading a generator, so the refresh cannot rotate it.
  • both personal instances change together because the task is one: the same mechanism for the same kind of instance, and the owner asked for one change covering both namespaces.

Why the managed path and not $HERMES_HOME/.env directly: Hermes writes that file itself (settings), a mount would freeze it, and then two files would disagree about which one wins.

Verified

  • flate test all -p ./kubernetes/flux/cluster — 308 passed; flate build ks secrets -n grachevaeu|grachevko and flate build ks env -n … render the objects in the right namespaces.
  • flate build hr hermes -p ./kubernetes/flux/cluster -n grachevaeu and -n grachevko render; in both, the app container and the pre-existing fix-run init container carry mountPath: /etc/hermes/.env with subPath: .env, readOnly: true, from the Secret hermes-managed-env; no HERMES_MANAGED_DIR, no assemble-managed-env, no envFrom with the credentials.
  • the ExternalSecret of each instance: data = API_SERVER_KEY (per-entry sourceRef to the kubernetes store, remoteRef.key: hermes-api-key) + HINDSIGHT_API_KEY (Vault, ai/hindsight); dataFrom.extract = the record of the instance. A per-entry sourceRef is part of the API (ExternalSecretData.SourceRef in the operator's own types), which is what lets one ExternalSecret read two stores.
  • the template of both Secrets sorts the rendered pairs (sortAlpha), so the Secret does not change on a refresh and the reloader does not roll the pods with it; same pattern as kubernetes/apps/ai/hindsight/app/externalsecret.yaml, and keys is not usable there because ESO hands over map[string]string.
  • kube-root-ca.crt exists in both namespaces, and vault is the only other store in the cluster; the account both stores authenticate as is the same operator.

After the merge

  1. kv/grachevko/hermes and kv/grachevaeu/hermes must hold the keys under the variable names — DEEPSEEK_API_KEY, TELEGRAM_BOT_TOKEN, ... kv/grachevaeu/hermes still holds deepseek_key and telegram_bot_token; until it is rewritten the ExternalSecret stays in SecretSyncedError and the instance gets no DEEPSEEK_API_KEY (the sync repeats every minute, so the fix needs no nudge):
    vault kv get -tls-skip-verify -format=json kv/grachevaeu/hermes \
      | jq -c '.data.data | .DEEPSEEK_API_KEY = .deepseek_key | .TELEGRAM_BOT_TOKEN = .telegram_bot_token | del(.deepseek_key, .telegram_bot_token)' \
      | vault kv put -tls-skip-verify kv/grachevaeu/hermes -
    
    Property names only, without the values — the same read from inside the cluster, where vault read skips the mount lookup that the kv commands perform:
    kubectl -n security exec vault-0 -- vault read -format=json kv/data/grachevko/hermes | jq -r '.data.data | keys[]'
  2. Delete the leftover Secret hermes-keys in grachevaeu: nothing creates it any more.
  3. Nothing else: the key of the API server keeps its value (the generator is untouched), and any line the volume .env still holds is overridden by the mounted file.

Rollback

git revert of the merge commit: the mount, the Secret name and the generator are unchanged from main, so the previous shape comes back on the next reconcile.

## What was wrong Since 0.21.6 the gateway multiplexes every profile by default (`gateway.multiplex_profiles` cannot be turned off) and resolves a credential only through the profile secret scope: the profile's own `.env` plus the managed layer. `gateway/config.py::_getenv` reads through `agent/secret_scope.get_secret`, which fails closed while multiplexing, so a value that exists ONLY as a variable of the pod is invisible. `api_server` is then never enabled, port 8642 stays closed, the readiness probe never passes and the Helm upgrade times out — that is how the 0.21.6 rollout of `grachevaeu` failed, with Flux rolling the release back every five minutes. ## What this fixes - `kubernetes/apps/{grachevaeu,grachevko}/hermes/secrets/ks.yaml` and `…/hermes/env/ks.yaml` — new: the credentials are Kustomizations of their own, in the order this repository keeps for a CR of an operator, the Secret the CR reads and the application that reads the Secret. `secrets/` holds the generator of the API server key and the read point of the generated Secrets, `env/` holds the ExternalSecret that renders the `.env` and reads both, and the Kustomization of the pod depends on `env` — the file it mounts has to exist before the container is created. - `kubernetes/apps/{grachevaeu,grachevko}/hermes/secrets/app/secretstore-kubernetes.yaml` — new: the read point for the values the cluster makes itself. A namespaced `SecretStore` of the kubernetes provider, constrained to one namespace, with no new permission — the operator's chart already carries `secrets:get` and `serviceaccounts/token:create`; the CA is the one Kubernetes maintains in every namespace (`kube-root-ca.crt`), not the internal CA of the gateway domains. - `kubernetes/apps/{grachevaeu,grachevko}/hermes/env/app/externalsecret-managed-env.yaml` — new: the credentials of the instance as one Secret, `hermes-managed-env`, rendered generically. Every property of the record of that instance in Vault becomes one `KEY=VALUE` line, the property name being the variable name; `HINDSIGHT_API_KEY` comes from the shared record `ai/hindsight`, `API_SERVER_KEY` from the generated Secret of the cluster. `refreshInterval: 1m`, so a key written in Vault reaches the pod within a minute and no commit is needed for a new key. - `kubernetes/apps/{grachevaeu,grachevko}/hermes/app/helmrelease.yaml` — the Secret is mounted read-only as `/etc/hermes/.env`, the file of the managed scope, in the directory the image already treats as its own: no `HERMES_MANAGED_DIR` (the variable only relocates the same directory, and with no `config.yaml` beside it the config half of the scope stays inert), no init container, no emptyDir. `subPath`, not a directory mount: a directory mount would hide the two files the image keeps in `/etc/hermes` (`agent-browser-executable-path`, `image-provenance.json`). The pod keeps no key of its own and nothing inside it is a source of truth: Hermes still writes its own `$HERMES_HOME/.env`, while the mounted file wins for every key it carries, so a stale line in the volume file changes nothing and needs no cleanup. The reloader annotation stays and now watches the Secret that carries the file. - `kubernetes/apps/grachevaeu/hermes/app/externalsecret-keys.yaml` (deleted) — the keys now arrive as `.env` lines, so the `envFrom` Secret and that ExternalSecret are gone. `externalsecret-api-key.yaml` is moved, unchanged apart from its comment, into `secrets/app/`: the key of the API server is generated once in the cluster and the value is read back through the kubernetes store instead of being handed to the pod as a variable — reading a Secret is not reading a generator, so the refresh cannot rotate it. - both personal instances change together because the task is one: the same mechanism for the same kind of instance, and the owner asked for one change covering both namespaces. Why the managed path and not `$HERMES_HOME/.env` directly: Hermes writes that file itself (settings), a mount would freeze it, and then two files would disagree about which one wins. ## Verified - `flate test all -p ./kubernetes/flux/cluster` — 308 passed; `flate build ks secrets -n grachevaeu|grachevko` and `flate build ks env -n …` render the objects in the right namespaces. - `flate build hr hermes -p ./kubernetes/flux/cluster -n grachevaeu` and `-n grachevko` render; in both, the app container and the pre-existing `fix-run` init container carry `mountPath: /etc/hermes/.env` with `subPath: .env`, `readOnly: true`, from the Secret `hermes-managed-env`; no `HERMES_MANAGED_DIR`, no `assemble-managed-env`, no `envFrom` with the credentials. - the ExternalSecret of each instance: `data` = `API_SERVER_KEY` (per-entry `sourceRef` to the `kubernetes` store, `remoteRef.key: hermes-api-key`) + `HINDSIGHT_API_KEY` (Vault, `ai/hindsight`); `dataFrom.extract` = the record of the instance. A per-entry `sourceRef` is part of the API (`ExternalSecretData.SourceRef` in the operator's own types), which is what lets one ExternalSecret read two stores. - the template of both Secrets sorts the rendered pairs (`sortAlpha`), so the Secret does not change on a refresh and the reloader does not roll the pods with it; same pattern as `kubernetes/apps/ai/hindsight/app/externalsecret.yaml`, and `keys` is not usable there because ESO hands over `map[string]string`. - `kube-root-ca.crt` exists in both namespaces, and `vault` is the only other store in the cluster; the account both stores authenticate as is the same operator. ## After the merge 1. `kv/grachevko/hermes` and `kv/grachevaeu/hermes` must hold the keys under the variable names — `DEEPSEEK_API_KEY`, `TELEGRAM_BOT_TOKEN`, ... `kv/grachevaeu/hermes` still holds `deepseek_key` and `telegram_bot_token`; until it is rewritten the ExternalSecret stays in `SecretSyncedError` and the instance gets no `DEEPSEEK_API_KEY` (the sync repeats every minute, so the fix needs no nudge): ``` vault kv get -tls-skip-verify -format=json kv/grachevaeu/hermes \ | jq -c '.data.data | .DEEPSEEK_API_KEY = .deepseek_key | .TELEGRAM_BOT_TOKEN = .telegram_bot_token | del(.deepseek_key, .telegram_bot_token)' \ | vault kv put -tls-skip-verify kv/grachevaeu/hermes - ``` Property names only, without the values — the same read from inside the cluster, where `vault read` skips the mount lookup that the `kv` commands perform: `kubectl -n security exec vault-0 -- vault read -format=json kv/data/grachevko/hermes | jq -r '.data.data | keys[]'` 2. Delete the leftover Secret `hermes-keys` in `grachevaeu`: nothing creates it any more. 3. Nothing else: the key of the API server keeps its value (the generator is untouched), and any line the volume `.env` still holds is overridden by the mounted file. ## Rollback `git revert` of the merge commit: the mount, the Secret name and the generator are unchanged from `main`, so the previous shape comes back on the next reconcile.
Collaborator

Failed to update PR description

Failed to update PR description

Rendered diff of the HelmReleases in this pull request:

--- HelmRelease: network/k8s-gateway Deployment: network/k8s-gateway

+++ HelmRelease: network/k8s-gateway Deployment: network/k8s-gateway

@@ -27,13 +27,13 @@

     spec:
       terminationGracePeriodSeconds: 30
       serviceAccountName: k8s-gateway
       dnsPolicy: Default
       containers:
       - name: k8s-gateway
-        image: codeberg.org/k8s-gateway/k8s_gateway:1.9.2
+        image: codeberg.org/k8s-gateway/k8s_gateway:1.9.0
         imagePullPolicy: IfNotPresent
         args:
         - -conf
         - /etc/coredns/Corefile
         volumeMounts:
         - name: config-volume
--- HelmRelease: media/qbittorrent Deployment: media/qbittorrent

+++ HelmRelease: media/qbittorrent Deployment: media/qbittorrent

@@ -63,13 +63,13 @@

           # The edits prove themselves here: a file without the whitelist would let the pod
           # start and leave the two neighbours failing at the client API, and a file with the
           # cross-site check left on would answer 401 to every entry through the gateway -
           # exactly the silent breakage the repository forbids.
           grep -q '^WebUI\\AuthSubnetWhitelist=10.42.0.0/16$' "$conf"
           grep -q '^WebUI\\CSRFProtection=false$' "$conf"
-        image: linuxserver/qbittorrent:5.2.4@sha256:97333cc1ccf903e0bbcd8eb67e3332e4970e9c1ecabe145e499e9c0847ebcaed
+        image: linuxserver/qbittorrent:5.2.4@sha256:b522f9f4b769f8f36d49d22d5eb6a92e9aa18904c6a1830b1439df511ec21983
         name: seed-config
         resources:
           requests:
             cpu: 10m
             memory: 32Mi
         volumeMounts:
@@ -88,13 +88,13 @@

         - name: TZ
           value: Europe/Moscow
         - name: UMASK
           value: '002'
         - name: WEBUI_PORT
           value: '8080'
-        image: linuxserver/qbittorrent:5.2.4@sha256:97333cc1ccf903e0bbcd8eb67e3332e4970e9c1ecabe145e499e9c0847ebcaed
+        image: linuxserver/qbittorrent:5.2.4@sha256:b522f9f4b769f8f36d49d22d5eb6a92e9aa18904c6a1830b1439df511ec21983
         name: app
         ports:
         - containerPort: 8080
           name: http
           protocol: TCP
         readinessProbe:
--- HelmRelease: grachevaeu/homepage Deployment: grachevaeu/homepage

+++ HelmRelease: grachevaeu/homepage Deployment: grachevaeu/homepage

@@ -46,13 +46,13 @@

       initContainers:
       - args:
         - cp -a /app/.next/server/pages/. /pages-cache/
         command:
         - /bin/sh
         - -c
-        image: ghcr.io/gethomepage/homepage:v2.5.0@sha256:57c4a0531937889c0b968b120fe9c2560bbe036b5c9e18c49a9e2479de9975d1
+        image: ghcr.io/gethomepage/homepage:v2.4.0@sha256:643bd0be730d40f69d58028a55d1a896739333e8815786df42bc97f109ecbe61
         name: seed-pages-cache
         securityContext:
           allowPrivilegeEscalation: false
           capabilities:
             drop:
             - ALL
@@ -96,13 +96,13 @@

           value: /etc/ssl/certs/internal-ca/ca.crt
         - name: TZ
           value: Europe/Moscow
         envFrom:
         - secretRef:
             name: homepage-auth
-        image: ghcr.io/gethomepage/homepage:v2.5.0@sha256:57c4a0531937889c0b968b120fe9c2560bbe036b5c9e18c49a9e2479de9975d1
+        image: ghcr.io/gethomepage/homepage:v2.4.0@sha256:643bd0be730d40f69d58028a55d1a896739333e8815786df42bc97f109ecbe61
         livenessProbe:
           failureThreshold: 3
           httpGet:
             httpHeaders:
             - name: Host
               value: eu...PLACEHOLDER_SECRET_DOMAIN..
--- HelmRelease: grachevko/homepage Deployment: grachevko/homepage

+++ HelmRelease: grachevko/homepage Deployment: grachevko/homepage

@@ -46,13 +46,13 @@

       initContainers:
       - args:
         - cp -a /app/.next/server/pages/. /pages-cache/
         command:
         - /bin/sh
         - -c
-        image: ghcr.io/gethomepage/homepage:v2.5.0@sha256:57c4a0531937889c0b968b120fe9c2560bbe036b5c9e18c49a9e2479de9975d1
+        image: ghcr.io/gethomepage/homepage:v2.4.0@sha256:643bd0be730d40f69d58028a55d1a896739333e8815786df42bc97f109ecbe61
         name: seed-pages-cache
         securityContext:
           allowPrivilegeEscalation: false
           capabilities:
             drop:
             - ALL
@@ -96,13 +96,13 @@

           value: /etc/ssl/certs/internal-ca/ca.crt
         - name: TZ
           value: Europe/Moscow
         envFrom:
         - secretRef:
             name: homepage-auth
-        image: ghcr.io/gethomepage/homepage:v2.5.0@sha256:57c4a0531937889c0b968b120fe9c2560bbe036b5c9e18c49a9e2479de9975d1
+        image: ghcr.io/gethomepage/homepage:v2.4.0@sha256:643bd0be730d40f69d58028a55d1a896739333e8815786df42bc97f109ecbe61
         livenessProbe:
           failureThreshold: 3
           httpGet:
             httpHeaders:
             - name: Host
               value: grachevko.ru
--- HelmRelease: grachevaeu/hermes Deployment: grachevaeu/hermes

+++ HelmRelease: grachevaeu/hermes Deployment: grachevaeu/hermes

@@ -7,12 +7,13 @@

     app.kubernetes.io/controller: hermes
     app.kubernetes.io/instance: hermes
     app.kubernetes.io/managed-by: Helm
     app.kubernetes.io/name: hermes
   annotations:
     reloader.stakater.com/auto: 'true'
+    secret.reloader.stakater.com/reload: hermes-managed-env
   namespace: grachevaeu
 spec:
   revisionHistoryLimit: 3
   replicas: 1
   strategy:
     type: Recreate
@@ -57,12 +58,16 @@

         volumeMounts:
         - mountPath: /opt/data
           name: data
         - mountPath: /etc/ssl/certs/internal-ca
           name: internal-ca
           readOnly: true
+        - mountPath: /etc/hermes/.env
+          name: managed-env
+          readOnly: true
+          subPath: .env
         - mountPath: /run
           name: run
         - mountPath: /tmp
           name: tmp
       containers:
       - args:
@@ -70,17 +75,12 @@

         - run
         env:
         - name: API_SERVER_ENABLED
           value: 'true'
         - name: API_SERVER_HOST
           value: 0.0.0.0
-        - name: API_SERVER_KEY
-          valueFrom:
-            secretKeyRef:
-              key: api-key
-              name: hermes-api-key
         - name: CURL_CA_BUNDLE
           value: /etc/ssl/certs/internal-ca/ca.crt
         - name: HERMES_DASHBOARD
           value: '1'
         - name: HERMES_DASHBOARD_OIDC_CLIENT_ID
           valueFrom:
@@ -98,15 +98,12 @@

         - name: HOME
           value: /opt/data
         - name: REQUESTS_CA_BUNDLE
           value: /etc/ssl/certs/internal-ca/ca.crt
         - name: SSL_CERT_FILE
           value: /etc/ssl/certs/internal-ca/ca.crt
-        envFrom:
-        - secretRef:
-            name: hermes-keys
         image: nousresearch/hermes-agent:v0.21.6@sha256:55e192fba0cd4fde61142abbff5adeacff40efdb482ccd0ff877924bd274f909
         name: app
         ports:
         - containerPort: 8642
           name: http
           protocol: TCP
@@ -135,22 +132,29 @@

         volumeMounts:
         - mountPath: /opt/data
           name: data
         - mountPath: /etc/ssl/certs/internal-ca
           name: internal-ca
           readOnly: true
+        - mountPath: /etc/hermes/.env
+          name: managed-env
+          readOnly: true
+          subPath: .env
         - mountPath: /run
           name: run
         - mountPath: /tmp
           name: tmp
       volumes:
       - name: data
         persistentVolumeClaim:
           claimName: hermes
       - configMap:
           name: internal-ca
         name: internal-ca
+      - name: managed-env
+        secret:
+          secretName: hermes-managed-env
       - emptyDir: {}
         name: run
       - emptyDir: {}
         name: tmp
 
--- HelmRelease: grachevko/hermes Deployment: grachevko/hermes

+++ HelmRelease: grachevko/hermes Deployment: grachevko/hermes

@@ -7,12 +7,13 @@

     app.kubernetes.io/controller: hermes
     app.kubernetes.io/instance: hermes
     app.kubernetes.io/managed-by: Helm
     app.kubernetes.io/name: hermes
   annotations:
     reloader.stakater.com/auto: 'true'
+    secret.reloader.stakater.com/reload: hermes-managed-env
   namespace: grachevko
 spec:
   revisionHistoryLimit: 3
   replicas: 1
   strategy:
     type: Recreate
@@ -54,12 +55,16 @@

           runAsGroup: 0
           runAsNonRoot: false
           runAsUser: 0
         volumeMounts:
         - mountPath: /opt/data
           name: data
+        - mountPath: /etc/hermes/.env
+          name: managed-env
+          readOnly: true
+          subPath: .env
         - mountPath: /run
           name: run
         - mountPath: /tmp
           name: tmp
       containers:
       - args:
@@ -67,17 +72,12 @@

         - run
         env:
         - name: API_SERVER_ENABLED
           value: 'true'
         - name: API_SERVER_HOST
           value: 0.0.0.0
-        - name: API_SERVER_KEY
-          valueFrom:
-            secretKeyRef:
-              key: api-key
-              name: hermes-api-key
         - name: CURL_CA_BUNDLE
           value: /opt/data/ca-internal-bundle.pem
         - name: HERMES_DASHBOARD
           value: '1'
         - name: HERMES_DASHBOARD_OIDC_CLIENT_ID
           valueFrom:
@@ -124,19 +124,26 @@

             drop:
             - ALL
           readOnlyRootFilesystem: true
         volumeMounts:
         - mountPath: /opt/data
           name: data
+        - mountPath: /etc/hermes/.env
+          name: managed-env
+          readOnly: true
+          subPath: .env
         - mountPath: /run
           name: run
         - mountPath: /tmp
           name: tmp
       volumes:
       - name: data
         persistentVolumeClaim:
           claimName: hermes
+      - name: managed-env
+        secret:
+          secretName: hermes-managed-env
       - emptyDir: {}
         name: run
       - emptyDir: {}
         name: tmp
 
Rendered diff of the HelmReleases in this pull request: ```diff --- HelmRelease: network/k8s-gateway Deployment: network/k8s-gateway +++ HelmRelease: network/k8s-gateway Deployment: network/k8s-gateway @@ -27,13 +27,13 @@ spec: terminationGracePeriodSeconds: 30 serviceAccountName: k8s-gateway dnsPolicy: Default containers: - name: k8s-gateway - image: codeberg.org/k8s-gateway/k8s_gateway:1.9.2 + image: codeberg.org/k8s-gateway/k8s_gateway:1.9.0 imagePullPolicy: IfNotPresent args: - -conf - /etc/coredns/Corefile volumeMounts: - name: config-volume --- HelmRelease: media/qbittorrent Deployment: media/qbittorrent +++ HelmRelease: media/qbittorrent Deployment: media/qbittorrent @@ -63,13 +63,13 @@ # The edits prove themselves here: a file without the whitelist would let the pod # start and leave the two neighbours failing at the client API, and a file with the # cross-site check left on would answer 401 to every entry through the gateway - # exactly the silent breakage the repository forbids. grep -q '^WebUI\\AuthSubnetWhitelist=10.42.0.0/16$' "$conf" grep -q '^WebUI\\CSRFProtection=false$' "$conf" - image: linuxserver/qbittorrent:5.2.4@sha256:97333cc1ccf903e0bbcd8eb67e3332e4970e9c1ecabe145e499e9c0847ebcaed + image: linuxserver/qbittorrent:5.2.4@sha256:b522f9f4b769f8f36d49d22d5eb6a92e9aa18904c6a1830b1439df511ec21983 name: seed-config resources: requests: cpu: 10m memory: 32Mi volumeMounts: @@ -88,13 +88,13 @@ - name: TZ value: Europe/Moscow - name: UMASK value: '002' - name: WEBUI_PORT value: '8080' - image: linuxserver/qbittorrent:5.2.4@sha256:97333cc1ccf903e0bbcd8eb67e3332e4970e9c1ecabe145e499e9c0847ebcaed + image: linuxserver/qbittorrent:5.2.4@sha256:b522f9f4b769f8f36d49d22d5eb6a92e9aa18904c6a1830b1439df511ec21983 name: app ports: - containerPort: 8080 name: http protocol: TCP readinessProbe: --- HelmRelease: grachevaeu/homepage Deployment: grachevaeu/homepage +++ HelmRelease: grachevaeu/homepage Deployment: grachevaeu/homepage @@ -46,13 +46,13 @@ initContainers: - args: - cp -a /app/.next/server/pages/. /pages-cache/ command: - /bin/sh - -c - image: ghcr.io/gethomepage/homepage:v2.5.0@sha256:57c4a0531937889c0b968b120fe9c2560bbe036b5c9e18c49a9e2479de9975d1 + image: ghcr.io/gethomepage/homepage:v2.4.0@sha256:643bd0be730d40f69d58028a55d1a896739333e8815786df42bc97f109ecbe61 name: seed-pages-cache securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL @@ -96,13 +96,13 @@ value: /etc/ssl/certs/internal-ca/ca.crt - name: TZ value: Europe/Moscow envFrom: - secretRef: name: homepage-auth - image: ghcr.io/gethomepage/homepage:v2.5.0@sha256:57c4a0531937889c0b968b120fe9c2560bbe036b5c9e18c49a9e2479de9975d1 + image: ghcr.io/gethomepage/homepage:v2.4.0@sha256:643bd0be730d40f69d58028a55d1a896739333e8815786df42bc97f109ecbe61 livenessProbe: failureThreshold: 3 httpGet: httpHeaders: - name: Host value: eu...PLACEHOLDER_SECRET_DOMAIN.. --- HelmRelease: grachevko/homepage Deployment: grachevko/homepage +++ HelmRelease: grachevko/homepage Deployment: grachevko/homepage @@ -46,13 +46,13 @@ initContainers: - args: - cp -a /app/.next/server/pages/. /pages-cache/ command: - /bin/sh - -c - image: ghcr.io/gethomepage/homepage:v2.5.0@sha256:57c4a0531937889c0b968b120fe9c2560bbe036b5c9e18c49a9e2479de9975d1 + image: ghcr.io/gethomepage/homepage:v2.4.0@sha256:643bd0be730d40f69d58028a55d1a896739333e8815786df42bc97f109ecbe61 name: seed-pages-cache securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL @@ -96,13 +96,13 @@ value: /etc/ssl/certs/internal-ca/ca.crt - name: TZ value: Europe/Moscow envFrom: - secretRef: name: homepage-auth - image: ghcr.io/gethomepage/homepage:v2.5.0@sha256:57c4a0531937889c0b968b120fe9c2560bbe036b5c9e18c49a9e2479de9975d1 + image: ghcr.io/gethomepage/homepage:v2.4.0@sha256:643bd0be730d40f69d58028a55d1a896739333e8815786df42bc97f109ecbe61 livenessProbe: failureThreshold: 3 httpGet: httpHeaders: - name: Host value: grachevko.ru --- HelmRelease: grachevaeu/hermes Deployment: grachevaeu/hermes +++ HelmRelease: grachevaeu/hermes Deployment: grachevaeu/hermes @@ -7,12 +7,13 @@ app.kubernetes.io/controller: hermes app.kubernetes.io/instance: hermes app.kubernetes.io/managed-by: Helm app.kubernetes.io/name: hermes annotations: reloader.stakater.com/auto: 'true' + secret.reloader.stakater.com/reload: hermes-managed-env namespace: grachevaeu spec: revisionHistoryLimit: 3 replicas: 1 strategy: type: Recreate @@ -57,12 +58,16 @@ volumeMounts: - mountPath: /opt/data name: data - mountPath: /etc/ssl/certs/internal-ca name: internal-ca readOnly: true + - mountPath: /etc/hermes/.env + name: managed-env + readOnly: true + subPath: .env - mountPath: /run name: run - mountPath: /tmp name: tmp containers: - args: @@ -70,17 +75,12 @@ - run env: - name: API_SERVER_ENABLED value: 'true' - name: API_SERVER_HOST value: 0.0.0.0 - - name: API_SERVER_KEY - valueFrom: - secretKeyRef: - key: api-key - name: hermes-api-key - name: CURL_CA_BUNDLE value: /etc/ssl/certs/internal-ca/ca.crt - name: HERMES_DASHBOARD value: '1' - name: HERMES_DASHBOARD_OIDC_CLIENT_ID valueFrom: @@ -98,15 +98,12 @@ - name: HOME value: /opt/data - name: REQUESTS_CA_BUNDLE value: /etc/ssl/certs/internal-ca/ca.crt - name: SSL_CERT_FILE value: /etc/ssl/certs/internal-ca/ca.crt - envFrom: - - secretRef: - name: hermes-keys image: nousresearch/hermes-agent:v0.21.6@sha256:55e192fba0cd4fde61142abbff5adeacff40efdb482ccd0ff877924bd274f909 name: app ports: - containerPort: 8642 name: http protocol: TCP @@ -135,22 +132,29 @@ volumeMounts: - mountPath: /opt/data name: data - mountPath: /etc/ssl/certs/internal-ca name: internal-ca readOnly: true + - mountPath: /etc/hermes/.env + name: managed-env + readOnly: true + subPath: .env - mountPath: /run name: run - mountPath: /tmp name: tmp volumes: - name: data persistentVolumeClaim: claimName: hermes - configMap: name: internal-ca name: internal-ca + - name: managed-env + secret: + secretName: hermes-managed-env - emptyDir: {} name: run - emptyDir: {} name: tmp --- HelmRelease: grachevko/hermes Deployment: grachevko/hermes +++ HelmRelease: grachevko/hermes Deployment: grachevko/hermes @@ -7,12 +7,13 @@ app.kubernetes.io/controller: hermes app.kubernetes.io/instance: hermes app.kubernetes.io/managed-by: Helm app.kubernetes.io/name: hermes annotations: reloader.stakater.com/auto: 'true' + secret.reloader.stakater.com/reload: hermes-managed-env namespace: grachevko spec: revisionHistoryLimit: 3 replicas: 1 strategy: type: Recreate @@ -54,12 +55,16 @@ runAsGroup: 0 runAsNonRoot: false runAsUser: 0 volumeMounts: - mountPath: /opt/data name: data + - mountPath: /etc/hermes/.env + name: managed-env + readOnly: true + subPath: .env - mountPath: /run name: run - mountPath: /tmp name: tmp containers: - args: @@ -67,17 +72,12 @@ - run env: - name: API_SERVER_ENABLED value: 'true' - name: API_SERVER_HOST value: 0.0.0.0 - - name: API_SERVER_KEY - valueFrom: - secretKeyRef: - key: api-key - name: hermes-api-key - name: CURL_CA_BUNDLE value: /opt/data/ca-internal-bundle.pem - name: HERMES_DASHBOARD value: '1' - name: HERMES_DASHBOARD_OIDC_CLIENT_ID valueFrom: @@ -124,19 +124,26 @@ drop: - ALL readOnlyRootFilesystem: true volumeMounts: - mountPath: /opt/data name: data + - mountPath: /etc/hermes/.env + name: managed-env + readOnly: true + subPath: .env - mountPath: /run name: run - mountPath: /tmp name: tmp volumes: - name: data persistentVolumeClaim: claimName: hermes + - name: managed-env + secret: + secretName: hermes-managed-env - emptyDir: {} name: run - emptyDir: {} name: tmp ```
Collaborator

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ No major issues detected
## PR Reviewer Guide 🔍 Here are some key observations to aid the review process: <table> <tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 3 🔵🔵🔵⚪⚪</td></tr> <tr><td>🧪&nbsp;<strong>No relevant tests</strong></td></tr> <tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr> <tr><td>⚡&nbsp;<strong>No major issues detected</strong></td></tr> </table>
Collaborator

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Исправить имя контроллера в advancedMounts

Первый уровень advancedMounts в app-template — это имя контроллера, а не имя
приложения. По видимому манифесту контроллер называется main, а контейнер — app,
поэтому ключ hermes не совпадает: монтирования не будут созданы, init-контейнер
упадёт при записи /etc/hermes/managed/.env. Замените hermes на main во всех трёх
новых записях managed-env, managed-env-seed и managed-env-key; аналогично исправьте
в kubernetes/apps/grachevko/hermes/app/helmrelease.yaml.

kubernetes/apps/grachevaeu/hermes/app/helmrelease.yaml [204-214]

       managed-env:
         type: emptyDir
         advancedMounts:
-          hermes:
+          main:
             assemble-managed-env:
               - path: /etc/hermes/managed
             app:
               - path: /etc/hermes/managed
                 readOnly: true
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies that advancedMounts keys must match the app-template controller name (main), not the application name hermes; using hermes would prevent the init container from mounting /etc/hermes/managed and break startup. This is a high-impact correctness fix, with a parallel issue in the grachevko file.

High
## PR Code Suggestions ✨ <!-- pr-agent:improve:summary --> <!-- 976ed20 --> Explore these optional code suggestions: <table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=1>Possible issue</td> <td> <details><summary>Исправить имя контроллера в advancedMounts</summary> ___ **Первый уровень <code>advancedMounts</code> в app-template — это имя контроллера, а не имя <br>приложения. По видимому манифесту контроллер называется <code>main</code>, а контейнер — <code>app</code>, <br>поэтому ключ <code>hermes</code> не совпадает: монтирования не будут созданы, init-контейнер <br>упадёт при записи <code>/etc/hermes/managed/.env</code>. Замените <code>hermes</code> на <code>main</code> во всех трёх <br>новых записях <code>managed-env</code>, <code>managed-env-seed</code> и <code>managed-env-key</code>; аналогично исправьте <br>в <code>kubernetes/apps/grachevko/hermes/app/helmrelease.yaml</code>.** [kubernetes/apps/grachevaeu/hermes/app/helmrelease.yaml [204-214]](https://forgejo.grachevko.ru/grachevko/home-ops/src/branch/fix/hermes-managed-scope/kubernetes/apps/grachevaeu/hermes/app/helmrelease.yaml#L204-L214) ```diff managed-env: type: emptyDir advancedMounts: - hermes: + main: assemble-managed-env: - path: /etc/hermes/managed app: - path: /etc/hermes/managed readOnly: true ``` <details><summary>Suggestion importance[1-10]: 9</summary> __ Why: The suggestion correctly identifies that `advancedMounts` keys must match the app-template controller name (`main`), not the application name `hermes`; using `hermes` would prevent the init container from mounting `/etc/hermes/managed` and break startup. This is a high-impact correctness fix, with a parallel issue in the grachevko file. </details></details></td><td align=center>High </td></tr></tr></tbody></table>
hermes force-pushed fix/hermes-managed-scope from 976ed2041c
Some checks failed
Flux Local / Flux Local - Test (pull_request) Successful in 1m1s
LLM Review / LLM Review - Success (pull_request) Failing after 3s
Flate / Flate - Filter (pull_request) Successful in 3s
Flux Local / Flux Local - Diff (pull_request) Successful in 1m36s
Flate / Flate (pull_request) Successful in 38s
Flux Local / Flux Local - Success (pull_request) Successful in 3s
Flate / Flate - Success (pull_request) Successful in 3s
LLM Review / LLM Review (pull_request) Failing after 6m32s
to 80ab892c88
All checks were successful
Flate / Flate - Filter (pull_request) Successful in 3s
Flux Local / Flux Local - Diff (pull_request) Successful in 1m36s
Flux Local / Flux Local - Test (pull_request) Successful in 51s
Flate / Flate (pull_request) Successful in 42s
Flux Local / Flux Local - Success (pull_request) Successful in 3s
Flate / Flate - Success (pull_request) Successful in 4s
LLM Review / LLM Review - Success (pull_request) Successful in 3s
LLM Review / LLM Review (pull_request) Successful in 6m55s
2026-10-10 13:15:27 +00:00
Compare
hermes force-pushed fix/hermes-managed-scope from 80ab892c88
All checks were successful
Flate / Flate - Filter (pull_request) Successful in 3s
Flux Local / Flux Local - Diff (pull_request) Successful in 1m36s
Flux Local / Flux Local - Test (pull_request) Successful in 51s
Flate / Flate (pull_request) Successful in 42s
Flux Local / Flux Local - Success (pull_request) Successful in 3s
Flate / Flate - Success (pull_request) Successful in 4s
LLM Review / LLM Review - Success (pull_request) Successful in 3s
LLM Review / LLM Review (pull_request) Successful in 6m55s
to 643c826f17
Some checks failed
LLM Review / LLM Review (pull_request) Failing after 4m54s
LLM Review / LLM Review - Success (pull_request) Failing after 3s
Flate / Flate - Filter (pull_request) Successful in 4s
Flux Local / Flux Local - Diff (pull_request) Successful in 1m34s
Flux Local / Flux Local - Test (pull_request) Successful in 1m5s
Flate / Flate (pull_request) Successful in 36s
Flux Local / Flux Local - Success (pull_request) Successful in 3s
Flate / Flate - Success (pull_request) Successful in 3s
2026-10-12 13:02:05 +00:00
Compare
hermes force-pushed fix/hermes-managed-scope from 643c826f17
Some checks failed
LLM Review / LLM Review (pull_request) Failing after 4m54s
LLM Review / LLM Review - Success (pull_request) Failing after 3s
Flate / Flate - Filter (pull_request) Successful in 4s
Flux Local / Flux Local - Diff (pull_request) Successful in 1m34s
Flux Local / Flux Local - Test (pull_request) Successful in 1m5s
Flate / Flate (pull_request) Successful in 36s
Flux Local / Flux Local - Success (pull_request) Successful in 3s
Flate / Flate - Success (pull_request) Successful in 3s
to a70fe57b68
Some checks failed
Flate / Flate - Filter (pull_request) Successful in 3s
Flux Local / Flux Local - Diff (pull_request) Successful in 1m37s
Flate / Flate (pull_request) Successful in 35s
LLM Review / LLM Review - Success (pull_request) Failing after 3s
Flux Local / Flux Local - Success (pull_request) Successful in 3s
Flate / Flate - Success (pull_request) Successful in 3s
LLM Review / LLM Review (pull_request) Failing after 5m3s
Flux Local / Flux Local - Test (pull_request) Successful in 55s
2026-10-12 13:16:15 +00:00
Compare
hermes force-pushed fix/hermes-managed-scope from a70fe57b68
Some checks failed
Flate / Flate - Filter (pull_request) Successful in 3s
Flux Local / Flux Local - Diff (pull_request) Successful in 1m37s
Flate / Flate (pull_request) Successful in 35s
LLM Review / LLM Review - Success (pull_request) Failing after 3s
Flux Local / Flux Local - Success (pull_request) Successful in 3s
Flate / Flate - Success (pull_request) Successful in 3s
LLM Review / LLM Review (pull_request) Failing after 5m3s
Flux Local / Flux Local - Test (pull_request) Successful in 55s
to fc5f66df9b
Some checks failed
Flate / Flate - Filter (pull_request) Successful in 3s
Flate / Flate (pull_request) Successful in 40s
Flux Local / Flux Local - Test (pull_request) Successful in 59s
Flux Local / Flux Local - Diff (pull_request) Successful in 1m54s
Flux Local / Flux Local - Success (pull_request) Successful in 3s
Flate / Flate - Success (pull_request) Successful in 3s
LLM Review / LLM Review (pull_request) Failing after 4m44s
LLM Review / LLM Review - Success (pull_request) Failing after 3s
2026-10-12 13:35:28 +00:00
Compare
hermes force-pushed fix/hermes-managed-scope from fc5f66df9b
Some checks failed
Flate / Flate - Filter (pull_request) Successful in 3s
Flate / Flate (pull_request) Successful in 40s
Flux Local / Flux Local - Test (pull_request) Successful in 59s
Flux Local / Flux Local - Diff (pull_request) Successful in 1m54s
Flux Local / Flux Local - Success (pull_request) Successful in 3s
Flate / Flate - Success (pull_request) Successful in 3s
LLM Review / LLM Review (pull_request) Failing after 4m44s
LLM Review / LLM Review - Success (pull_request) Failing after 3s
to 9b0ab97d56
Some checks failed
Flate / Flate - Filter (pull_request) Successful in 3s
Flux Local / Flux Local - Test (pull_request) Successful in 58s
Flux Local / Flux Local - Diff (pull_request) Successful in 1m35s
Flate / Flate (pull_request) Successful in 37s
Flux Local / Flux Local - Success (pull_request) Successful in 3s
Flate / Flate - Success (pull_request) Successful in 3s
LLM Review / LLM Review (pull_request) Failing after 5m19s
LLM Review / LLM Review - Success (pull_request) Failing after 3s
2026-10-12 13:45:08 +00:00
Compare
hermes force-pushed fix/hermes-managed-scope from 9b0ab97d56
Some checks failed
Flate / Flate - Filter (pull_request) Successful in 3s
Flux Local / Flux Local - Test (pull_request) Successful in 58s
Flux Local / Flux Local - Diff (pull_request) Successful in 1m35s
Flate / Flate (pull_request) Successful in 37s
Flux Local / Flux Local - Success (pull_request) Successful in 3s
Flate / Flate - Success (pull_request) Successful in 3s
LLM Review / LLM Review (pull_request) Failing after 5m19s
LLM Review / LLM Review - Success (pull_request) Failing after 3s
to 8815e40aca
Some checks failed
Flate / Flate - Filter (pull_request) Successful in 3s
Flux Local / Flux Local - Test (pull_request) Successful in 58s
Flux Local / Flux Local - Diff (pull_request) Successful in 1m33s
Flate / Flate (pull_request) Successful in 41s
Flux Local / Flux Local - Success (pull_request) Successful in 3s
Flate / Flate - Success (pull_request) Successful in 3s
LLM Review / LLM Review (pull_request) Failing after 6m9s
LLM Review / LLM Review - Success (pull_request) Failing after 3s
2026-10-12 13:57:08 +00:00
Compare
hermes force-pushed fix/hermes-managed-scope from 8815e40aca
Some checks failed
Flate / Flate - Filter (pull_request) Successful in 3s
Flux Local / Flux Local - Test (pull_request) Successful in 58s
Flux Local / Flux Local - Diff (pull_request) Successful in 1m33s
Flate / Flate (pull_request) Successful in 41s
Flux Local / Flux Local - Success (pull_request) Successful in 3s
Flate / Flate - Success (pull_request) Successful in 3s
LLM Review / LLM Review (pull_request) Failing after 6m9s
LLM Review / LLM Review - Success (pull_request) Failing after 3s
to 3f83c3ecb0
Some checks failed
Flate / Flate - Filter (pull_request) Successful in 3s
Flux Local / Flux Local - Test (pull_request) Successful in 1m2s
Flux Local / Flux Local - Diff (pull_request) Successful in 1m39s
Flate / Flate (pull_request) Successful in 38s
Flux Local / Flux Local - Success (pull_request) Successful in 3s
LLM Review / LLM Review - Success (pull_request) Failing after 3s
Flate / Flate - Success (pull_request) Successful in 3s
LLM Review / LLM Review (pull_request) Failing after 4m49s
2026-10-12 14:08:15 +00:00
Compare
hermes force-pushed fix/hermes-managed-scope from 3f83c3ecb0
Some checks failed
Flate / Flate - Filter (pull_request) Successful in 3s
Flux Local / Flux Local - Test (pull_request) Successful in 1m2s
Flux Local / Flux Local - Diff (pull_request) Successful in 1m39s
Flate / Flate (pull_request) Successful in 38s
Flux Local / Flux Local - Success (pull_request) Successful in 3s
LLM Review / LLM Review - Success (pull_request) Failing after 3s
Flate / Flate - Success (pull_request) Successful in 3s
LLM Review / LLM Review (pull_request) Failing after 4m49s
to 9a0667df76
All checks were successful
Flux Local / Flux Local - Test (pull_request) Successful in 1m7s
Flate / Flate - Filter (pull_request) Successful in 3s
Flux Local / Flux Local - Diff (pull_request) Successful in 1m48s
Flate / Flate (pull_request) Successful in 45s
Flux Local / Flux Local - Success (pull_request) Successful in 3s
Flate / Flate - Success (pull_request) Successful in 3s
LLM Review / LLM Review (pull_request) Successful in 6m59s
LLM Review / LLM Review - Success (pull_request) Successful in 3s
2026-10-12 14:15:47 +00:00
Compare
grachevko deleted branch fix/hermes-managed-scope 2026-10-12 14:33:23 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
grachevko/home-ops!387
No description provided.