deer-flow/deploy/helm/deer-flow/templates/provisioner-deployment.yaml
Zheng Feng d57f695769
fix(helm): default sandbox Services to ClusterIP (#3929) (#4190)
* fix(helm): default sandbox Services to ClusterIP (#3929)

The K8s sandbox provisioner supports both NodePort and ClusterIP via
SANDBOX_SERVICE_TYPE (added in #4016), but the Helm chart never set it,
so real-cluster installs inherited the NodePort default. That bound the
code-execution sandbox on every node's interfaces - including externally
reachable ones on GKE/EKS/AKS - and pinned every sandbox URL to one node
IP (SPOF on node reboot/drain/ephemeral-IP).

Default the chart to ClusterIP: the provisioner returns a cluster-DNS URL
(http://sandbox-<id>-svc.<ns>.svc.cluster.local:8080) so the gateway->
sandbox hop stays inside the cluster network - no node IP, no 30xxx port,
no external exposure. The chart always runs the gateway in-cluster, so
ClusterIP is always correct there.

NodePort remains an opt-in (provisioner.sandboxServiceType: NodePort +
nodeHost) for the Docker-Compose/hybrid path where the gateway is not in
K8s and cannot resolve .svc.cluster.local; the provisioner code default
stays NodePort for that path.

- values.yaml: add provisioner.sandboxServiceType ("ClusterIP")
- provisioner-deployment.yaml: emit SANDBOX_SERVICE_TYPE; gate the
  NODE_HOST block on NodePort mode (default "ClusterIP" for upgrade safety)
- NOTES.txt + README.md: document ClusterIP default + NodePort opt-in

No change to docker/provisioner/app.py (already mode-aware since #4016)
or RBAC (services verbs already cover ClusterIP).

* test(helm): assert sandbox Service-type gating + CHANGELOG the default flip (#3929)

Address review on #4190:

- Add scripts/check_chart_sandbox_service.sh: renders the chart for the
  default (ClusterIP, no NODE_HOST), the NodePort opt-in (both emitted),
  and NodePort+nodeHost (literal value, not downward API). Locks in the
  #3929 gating so a regression (e.g. re-adding an unconditional NODE_HOST,
  or dropping the `default "ClusterIP"` upgrade-safety fallback) fails CI.
  Wired into .github/workflows/chart.yaml validate-chart job. (#2)
- CHANGELOG [Unreleased] -> Changed: note the NodePort->ClusterIP default
  flip on upgrade + the `sandboxServiceType: NodePort` opt-back-in. (#4)

No chart template changes (the gating itself landed in the first commit).

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-07-16 14:30:05 +08:00

103 lines
4.2 KiB
YAML

{{- if .Values.provisioner.enabled -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "deer-flow.fullname" . }}-provisioner
namespace: {{ include "deer-flow.namespace" . }}
labels:
{{- include "deer-flow.labels" . | nindent 4 }}
app.kubernetes.io/component: provisioner
spec:
replicas: 1
selector:
matchLabels:
{{- include "deer-flow.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: provisioner
template:
metadata:
labels:
{{- include "deer-flow.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: provisioner
spec:
{{- include "deer-flow.imagePullSecrets" . | nindent 6 }}
serviceAccountName: {{ include "deer-flow.fullname" . }}-provisioner
securityContext:
# Non-root: provisioner image has no USER directive. uid/gid 1000.
# No fsGroup — it mounts no PVCs (it references PVC names for the
# sandbox Pods it spawns, never mounting them itself).
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: provisioner
image: {{ include "deer-flow.provisionerImage" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
ports:
- name: http
containerPort: 8002
env:
# Running as non-root uid 1000: suppress .pyc writes under root-owned /app.
- name: PYTHONDONTWRITEBYTECODE
value: "1"
- name: K8S_NAMESPACE
value: {{ include "deer-flow.namespace" . | quote }}
- name: SANDBOX_IMAGE
value: {{ .Values.provisioner.sandboxImage | quote }}
# In-cluster API access via the mounted ServiceAccount token.
# K8S_API_SERVER is intentionally unset (do NOT use host.docker.internal).
# Service type for per-sandbox Services. ClusterIP (default) routes the
# gateway->sandbox hop over cluster DNS; NodePort is the compose/hybrid
# escape hatch (gateway not in K8s), which needs NODE_HOST below.
- name: SANDBOX_SERVICE_TYPE
value: {{ .Values.provisioner.sandboxServiceType | default "ClusterIP" | quote }}
{{- if eq (default "ClusterIP" .Values.provisioner.sandboxServiceType) "NodePort" }}
# NODE_HOST: address the gateway uses to reach sandbox NodePorts
# (NodePort mode only). When `provisioner.nodeHost` is set, use it;
# otherwise default to this pod's node IP via the downward API. A
# NodePort is exposed on every node, so <node-IP>:<NodePort> routes
# from the gateway on most clusters. Override only when pod->node-IP
# traffic is blocked by the CNI/policy.
- name: NODE_HOST
{{- if .Values.provisioner.nodeHost }}
value: {{ .Values.provisioner.nodeHost | quote }}
{{- else }}
valueFrom:
fieldRef:
fieldPath: status.hostIP
{{- end }}
{{- end }}
# PVC mode — sandbox Pods mount the same PVCs the gateway uses.
{{- if .Values.persistence.home.enabled }}
- name: USERDATA_PVC_NAME
value: {{ include "deer-flow.homePVC" . | quote }}
{{- end }}
{{- if .Values.skills.existingClaim }}
- name: SKILLS_PVC_NAME
value: {{ .Values.skills.existingClaim | quote }}
{{- end }}
- name: SANDBOX_CONTAINER_PORT
value: {{ .Values.provisioner.sandboxPort | quote }}
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 15
periodSeconds: 20
{{- with .Values.provisioner.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end -}}