feat(shared-storage): resolve NFS Service ClusterIP at install time#107
Open
oren-openteams wants to merge 1 commit into
Open
feat(shared-storage): resolve NFS Service ClusterIP at install time#107oren-openteams wants to merge 1 commit into
oren-openteams wants to merge 1 commit into
Conversation
The shared-storage PV's spec.nfs.server was hardcoded to the in-cluster
NFS Service's FQDN. The kubelet performs NFS mounts in the host's
network namespace, where /etc/resolv.conf typically points at the
upstream DNS provider (the VPC resolver on managed Kubernetes) — not
the cluster's CoreDNS. As a result, mounts that reference the Service
FQDN fail with "Failed to resolve server ...: Name or service not
known" on most cloud-managed Kubernetes clusters (EKS, GKE without
node-local DNS, AKS without DNS forwarders). Singleuser pods stay
stuck at the volume-attach step and JupyterHub spawns never complete.
Legacy nebari avoided this by computing the NFS Service's ClusterIP
at terraform-apply time and baking the IP into the PV — see
src/_nebari/stages/kubernetes_services/template/modules/kubernetes/
nfs-server/output.tf (`endpoint_ip` output) and the wiring at
jupyterhub.tf:
nfs_endpoint = ... ? module.kubernetes-nfs-server.0.endpoint_ip
: var.jupyterhub-shared-endpoint
This change ports that design forward to the helm chart via
`helm lookup`. At install time, the template queries the cluster for
the NFS Service object and uses its `.spec.clusterIP` for the PV's
nfs.server. kube-proxy programs host-side iptables for Service IPs,
so the mount works without any host-DNS dependency.
When the lookup returns nil — which happens on the very first
`helm install` (Service doesn't exist yet) and during `helm template`
-only renders (no live API) — the PV falls back to the Service FQDN.
That preserves backward compatibility for clusters whose host DNS
does forward cluster.local to CoreDNS (k3s, microk8s with custom
resolvers — likely why this wasn't caught in upstream testing).
PV `.spec.persistentvolumesource` is immutable, so a fresh install
that lands the FQDN can't be patched into the IP form via `helm
upgrade` — the API server rejects the change. Documented recovery:
delete the PV (Retain reclaim policy preserves the backing storage
on the NFS server's RWO PVC) and re-sync. A follow-up could add an
optional `sharedStorage.nfsServer.endpoint` chart value as an
explicit operator override, mirroring legacy's `jupyterhub-shared-
endpoint` — out of scope here.
Closes #106.
Tests:
- tests/unit/test_shared_pvc_nfs_server.py pins the FQDN-fallback
behavior (`helm template` has no live API so lookup always returns
nil; the FQDN form must keep working there) and guards against
whitespace-stripping regressions in the lookup block.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #106.
Problem
The shared-storage PV's
spec.nfs.serveris hardcoded to the in-cluster NFS Service's FQDN:The kubelet performs NFS mounts in the host's network namespace, where
/etc/resolv.conftypically points at the upstream DNS provider (the VPC resolver on managed Kubernetes), not the cluster's CoreDNS. As a result, mounts that reference the Service FQDN fail with:on most cloud-managed clusters (EKS, GKE without node-local DNS, AKS without DNS forwarders). Singleuser pods stay stuck at the volume-attach step and JupyterHub spawns never complete.
Why legacy nebari didn't hit this
Legacy nebari deliberately computed the NFS Service's ClusterIP at terraform-apply time and baked the IP into the PV — see
nfs-server/output.tf:and the wiring at
jupyterhub.tf:That design got lost in the helm-chart refactor. This PR ports it forward.
Change
templates/shared-pvc.yaml— the PV'snfs.serveris now resolved at install time viahelm lookup:kube-proxy programs host-side iptables for Service IPs, so the mount works without any host-DNS dependency once the IP is in the PV.
Backward compatibility
When
lookupreturns nil — which happens on the very firsthelm install(Service doesn't exist yet) and duringhelm template-only renders (no live API) — the PV falls back to the FQDN. That keeps the chart working on clusters where host DNS DOES forwardcluster.localto CoreDNS (k3s, microk8s with custom resolvers, which is likely why this regression wasn't caught in upstream testing).Caveat: PV is immutable
PersistentVolume.spec.persistentvolumesourceis immutable, so a fresh install that lands the FQDN cannot be patched into the IP form viahelm upgrade— the API server rejects the change. Recovery path on affected clusters: delete the PV (Retain reclaim policy preserves the backing storage on the NFS server's RWO PVC) and re-sync. Subsequent installs in the same namespace will see the existing Service and bake the IP directly.A follow-up could add an optional
sharedStorage.nfsServer.endpointchart value as an explicit operator override (mirroring legacy'sjupyterhub-shared-endpoint) to make first-install on managed Kubernetes clusters work without the delete-and-resync dance. Out of scope here; happy to add it if reviewers prefer the combined approach.Tests
tests/unit/test_shared_pvc_nfs_server.pyadds two regression tests:helm templatehas no live API, solookupalways returns nil. The PV'snfs.servermust fall back to the cluster-local FQDN. Pins the contract that backward-compatible rendering still works.-}}/}}whitespace-trimming choices around the lookup block are easy to break. Without this guard, a stray-}}collapsespersistentVolumeReclaimPolicy: Retainandnfs:onto one line and produces an inscrutablemapping values not allowed in this contexterror.The "live Service returns the ClusterIP" path is exercised end-to-end by any
helm installagainst a real cluster, so it's not covered byhelm template-based unit tests. Could add an integration test against a kind cluster if that's the project's preference.How I tested
→ Renders cleanly with
server: "test-release-nebari-data-science-pack-nfs.test-ns.svc.cluster.local"(FQDN fallback path, since no Service exists inhelm templatecontext).pytest tests/unit/test_shared_pvc_nfs_server.py -v # → 2 passed