fix(helm): align provisioner state root for PVC mounts (#5625)

* fix(helm): align provisioner state root for PVC mounts

* test(helm): pin sandbox PVC subPath contract
This commit is contained in:
spud 2026-09-23 10:31:17 +08:00 committed by GitHub
parent ce99b37a67
commit 73e2be20e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 111 additions and 0 deletions

View File

@ -22,6 +22,7 @@ on:
- ".github/workflows/chart.yaml" - ".github/workflows/chart.yaml"
- "scripts/check_config_version.sh" - "scripts/check_config_version.sh"
- "scripts/check_chart_sandbox_service.sh" - "scripts/check_chart_sandbox_service.sh"
- "scripts/check_chart_sandbox_storage.sh"
- "scripts/check_chart_skill_upload_size.sh" - "scripts/check_chart_skill_upload_size.sh"
jobs: jobs:
@ -50,6 +51,13 @@ jobs:
- name: Validate sandbox Service-type gating - name: Validate sandbox Service-type gating
run: bash scripts/check_chart_sandbox_service.sh run: bash scripts/check_chart_sandbox_service.sh
# Gateway-generated skill projections are sent to the provisioner as
# paths under DEER_FLOW_HOST_BASE_DIR. With the shared home PVC enabled,
# both components must use the same logical root so those paths can be
# translated into PVC subPaths instead of being rejected as out-of-base.
- name: Validate sandbox storage path contract
run: bash scripts/check_chart_sandbox_storage.sh
# Keep the rendered Ingress aligned with the Gateway's .skill upload # Keep the rendered Ingress aligned with the Gateway's .skill upload
# size, streaming, and long-running validation requirements. # size, streaming, and long-running validation requirements.
- name: Validate skill upload ingress policy - name: Validate skill upload ingress policy

View File

@ -74,6 +74,11 @@ spec:
{{- end }} {{- end }}
# PVC mode — sandbox Pods mount the same PVCs the gateway uses. # PVC mode — sandbox Pods mount the same PVCs the gateway uses.
{{- if .Values.persistence.home.enabled }} {{- if .Values.persistence.home.enabled }}
# Extra-mount paths originate in the Gateway's state namespace.
# Keep this logical base aligned so the provisioner can translate
# those paths into USERDATA PVC subPaths.
- name: DEER_FLOW_HOST_BASE_DIR
value: /app/backend/.deer-flow
- name: USERDATA_PVC_NAME - name: USERDATA_PVC_NAME
value: {{ include "deer-flow.homePVC" . | quote }} value: {{ include "deer-flow.homePVC" . | quote }}
{{- end }} {{- end }}

View File

@ -0,0 +1,98 @@
#!/usr/bin/env bash
# Assert the Helm chart keeps the Gateway and provisioner on the same logical
# DeerFlow state root when the shared home PVC backs sandbox projection mounts.
#
# The Gateway sends provisioner extra_mount host paths rooted at
# DEER_FLOW_HOST_BASE_DIR. In USERDATA_PVC mode the provisioner validates those
# paths against its own DEER_FLOW_HOST_BASE_DIR, then converts the relative
# suffix to a deer-flow/<suffix> PVC subPath. The Gateway must mount that same
# PVC subtree at the logical state root. Drift in any leg of this contract can
# either reject valid mounts with HTTP 400 or resolve them to the wrong subtree.
#
# When persistence.home.enabled=false the provisioner does not receive the
# Gateway's home PVC, so this check intentionally leaves its state-root env
# unset rather than making pod-local Gateway paths look hostPath-compatible.
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CHART="$ROOT/deploy/helm/deer-flow"
if ! command -v helm >/dev/null 2>&1; then
echo "::error::helm is required to run this check" >&2
exit 1
fi
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
render_component() {
local template="$1"
local output="$2"
shift 2
helm template deer-flow "$CHART" --include-crds --show-only "$template" "$@" >"$output"
}
if ! render_component templates/gateway-deployment.yaml "$TMP/gateway.yaml"; then
echo "::error::gateway chart render failed" >&2
exit 1
fi
if ! render_component templates/provisioner-deployment.yaml "$TMP/provisioner.yaml"; then
echo "::error::provisioner chart render failed" >&2
exit 1
fi
if ! render_component templates/provisioner-deployment.yaml "$TMP/provisioner-ephemeral.yaml" --set persistence.home.enabled=false; then
echo "::error::ephemeral provisioner chart render failed" >&2
exit 1
fi
env_value() {
grep -A1 -E "^[[:space:]]*- name: $1$" "$2" |
grep -E "^[[:space:]]*value:" |
head -1 |
sed -E 's/^[[:space:]]*value:[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/'
}
has_env() { grep -qE "^[[:space:]]*- name: $1$" "$2"; }
has_gateway_home_mount_contract() {
grep -A1 -E '^[[:space:]]*mountPath: /app/backend/\.deer-flow$' "$1" |
grep -qE '^[[:space:]]*subPath: deer-flow$'
}
errors=0
check() {
if [ "$1" -eq 0 ]; then
echo " PASS $2"
else
echo " FAIL $2"
errors=$((errors + 1))
fi
}
gateway_root="$(env_value DEER_FLOW_HOST_BASE_DIR "$TMP/gateway.yaml")"
provisioner_root="$(env_value DEER_FLOW_HOST_BASE_DIR "$TMP/provisioner.yaml")"
echo "## Shared home PVC render"
[ -n "$gateway_root" ]; check $? "Gateway DEER_FLOW_HOST_BASE_DIR is present"
[ -n "$provisioner_root" ]; check $? "Provisioner DEER_FLOW_HOST_BASE_DIR is present"
[ "$gateway_root" = "$provisioner_root" ]; check $? "Gateway and provisioner state roots match"
[ "$gateway_root" = "/app/backend/.deer-flow" ]; check $? "Shared logical state root is /app/backend/.deer-flow"
has_env USERDATA_PVC_NAME "$TMP/provisioner.yaml"; check $? "Provisioner USERDATA_PVC_NAME is present"
has_gateway_home_mount_contract "$TMP/gateway.yaml"; check $? "Gateway home mount keeps subPath deer-flow at the shared logical root"
echo "## persistence.home.enabled=false render"
if has_env DEER_FLOW_HOST_BASE_DIR "$TMP/provisioner-ephemeral.yaml"; then
check 1 "Provisioner state root stays unset without the shared home PVC"
else
check 0 "Provisioner state root stays unset without the shared home PVC"
fi
echo
if [ "$errors" -eq 0 ]; then
echo "All sandbox storage path-contract assertions passed."
exit 0
fi
echo "::error::$errors assertion(s) failed (see above)" >&2
exit 1