Describe the bug
During Trident controller startup (and backend re-initialization generally), export-policy
reconciliation for ontap-nas-economy backends with autoExportPolicy: true computed an
empty "desired nodes" set and deleted every rule from the backend-wide export policy
trident-<backendUUID>.
This policy is assigned to every FlexVol the driver manages (publishQtreeShare sets it on
the parent FlexVol on each publish). ONTAP evaluates the export policy of each junction on
the mount path, so an empty FlexVol-level policy denies new NFS mounts for every node, for
every volume on the backend, even though the per-qtree policies still contain the correct
rules. Established mounts survive; every new mount fails with
mount.nfs: access denied / exit status 32.
Because export-policy rules for a node are only ever re-added during that node's own
ControllerPublishVolume (see companion report on the missing repair path), and Kubernetes
does not re-issue ControllerPublish while a VolumeAttachment exists, access came back one
node at a time — only for nodes whose VolumeAttachments happened to be deleted and recreated.
In our incident this produced a multi-hour outage: 5 of 6 workers could not mount two shared
RWX volumes, and recovery required deleting pods/VolumeAttachments per node to force fresh
publishes.
Root cause in code (verified on tag v26.06.0; unchanged on master as of 2026-08-31):
core/orchestrator_core.go — reconcileNodeAccessOnBackend: because the economy driver
implements PublishEnforceable, the node list passed to the driver is
publishedNodesForBackend(b), not the registered-node list.
publishedNodesForBackend intersects three caches: the volume-publication cache, the
backend object's own volume map (b.Volumes()), and the node cache. During controller
startup and during backend replacement (addBackend → updateBackend builds a fresh
backend object), this intersection is transiently empty.
- The empty result flows into
reconcileNASNodeAccess → reconcileExportPolicyRules
(storage_drivers/ontap/ontap_common.go), which faithfully deletes every "undesired"
rule (up to 10 per pass) from the live, in-use policy.
There is no guard anywhere on this path: no minimum-view check, no "never reconcile to an
empty rule set while publications exist for this backend", and no suppression of
reconciliation while a backend object is being replaced.
The same trust-the-cache pattern exists a second time in removeExportPolicyRules
(ontap_common.go), which is called on every unpublish of a qtree-level policy: if
publishInfo.Nodes arrives empty, it takes an explicit branch — "No active nodes remaining,
removing ALL export policy rules" — and empties the policy. We did not observe this branch
misfire in the incident, but it is reachable under the same transient-cache conditions on a
shared RWX volume, and it would revoke all remaining nodes at the qtree level. We suggest
fixing both call sites as one pattern. (#1086 was a previous incident of unpublish-side
collateral revocation in this same area.)
Environment
- Trident version: 26.06.0
- Trident installation flags used: operator-based install, defaults
- Container runtime: containerd (RKE2)
- Kubernetes version: v1.34.2
- Kubernetes orchestrator: RKE2 (Rancher-managed)
- Kubernetes enabled feature gates: defaults
- OS: Linux (RKE2 nodes)
- NetApp backend types: ONTAP,
ontap-nas-economy driver, REST API, autoExportPolicy: true, autoExportCIDRs: ["10.63.0.0/21"]
- Other: 6 worker nodes; two shared RWX PVCs (tool caches for CI runners) mounted by all workers; ~30 short-lived RWO PVCs churning continuously
To Reproduce
Timing-dependent; observed in production during a controller pod rescheduling. Sequence:
ontap-nas-economy backend, autoExportPolicy: true, multiple nodes holding
publications for RWX volumes, steady publish/unpublish churn from short-lived pods.
- Drain the node running the Trident controller so the controller pod is rescheduled
(in our case the drain also queued unpublish/publish work for the drained node).
- On startup, the new controller re-initializes each backend from its
TridentBackendConfig (crdControllerEvent=add reconciles) while the queued CSI calls
drain.
- Observe the backend export policy: all rules deleted.
Evidence from our incident (2026-08-31, times UTC). We captured an HTTP-level trace of
Trident's ONTAP REST traffic covering the whole window, plus the controller log:
- 19:05:41 — first ONTAP REST call from the restarted controller (rescheduled to a new node).
- 19:05:43.586–.762 —
DELETE /api/protocols/nfs/export-policies/21474840540/rules/{2,3,4,5,6,1},
six calls ~30 ms apart: every worker rule removed from the first backend's policy
(trident-ce7d4ac1-…). Immediately followed by an attempted delete of the policy itself →
400, ONTAP error 1703945 "Ruleset is in use by a volume" (see companion report on
Terminate).
- 19:05:52.271–.448 — identical six-rule wipe + failed policy delete on the second backend's
policy (trident-e0821f5f-…, id 21474855140).
- Controller log, same seconds: three warnings
error deleting export policy: … [400] … "Ruleset is in use by a volume" tagged
crdControllerEvent=add, workflow="cr=reconcile".
- First queued CSI unpublish was processed at 19:05:47 — after the first wipe, so the wipe
was not unpublish-driven.
tridentvolumepublications listed all 6 workers for the shared volumes throughout;
tridentnodes IPs were correct. Trident's recorded intent was right; only the ONTAP policy
was emptied.
- 19:11:29–:30 — the drained node's first fresh publish re-added its (and only its) rule;
it became the only node able to mount. FailedMount exit status 32 on the other five
workers from 19:12 onward.
- 20:02:38–:53 — after we deleted stuck pods (freeing VolumeAttachments), four fresh
publishes re-added four more rules; those nodes recovered within seconds. One node whose
replacement pods re-pinned the old VolumeAttachments stayed locked out until we intervened
on the array.
Expected behavior
Reconciliation must never treat a transiently empty or partial cache view as authority to
delete rules from an in-use export policy. Concretely, some combination of:
- Skip (or defer) node-access reconciliation until the publication, volume, and node caches
are fully populated after startup or backend replacement.
- Refuse to reconcile a backend policy down to zero rules while any publication exists for a
volume on that backend; log and retry instead.
- Apply the same guard to
removeExportPolicyRules' remove-all branch.
Additional context
AI assistance
Assisted-by: Claude Fable 5 noreply@anthropic.com
Describe the bug
During Trident controller startup (and backend re-initialization generally), export-policy
reconciliation for
ontap-nas-economybackends withautoExportPolicy: truecomputed anempty "desired nodes" set and deleted every rule from the backend-wide export policy
trident-<backendUUID>.This policy is assigned to every FlexVol the driver manages (
publishQtreeSharesets it onthe parent FlexVol on each publish). ONTAP evaluates the export policy of each junction on
the mount path, so an empty FlexVol-level policy denies new NFS mounts for every node, for
every volume on the backend, even though the per-qtree policies still contain the correct
rules. Established mounts survive; every new mount fails with
mount.nfs: access denied/exit status 32.Because export-policy rules for a node are only ever re-added during that node's own
ControllerPublishVolume(see companion report on the missing repair path), and Kubernetesdoes not re-issue ControllerPublish while a VolumeAttachment exists, access came back one
node at a time — only for nodes whose VolumeAttachments happened to be deleted and recreated.
In our incident this produced a multi-hour outage: 5 of 6 workers could not mount two shared
RWX volumes, and recovery required deleting pods/VolumeAttachments per node to force fresh
publishes.
Root cause in code (verified on tag v26.06.0; unchanged on master as of 2026-08-31):
core/orchestrator_core.go—reconcileNodeAccessOnBackend: because the economy driverimplements
PublishEnforceable, the node list passed to the driver ispublishedNodesForBackend(b), not the registered-node list.publishedNodesForBackendintersects three caches: the volume-publication cache, thebackend object's own volume map (
b.Volumes()), and the node cache. During controllerstartup and during backend replacement (
addBackend→updateBackendbuilds a freshbackend object), this intersection is transiently empty.
reconcileNASNodeAccess→reconcileExportPolicyRules(
storage_drivers/ontap/ontap_common.go), which faithfully deletes every "undesired"rule (up to 10 per pass) from the live, in-use policy.
There is no guard anywhere on this path: no minimum-view check, no "never reconcile to an
empty rule set while publications exist for this backend", and no suppression of
reconciliation while a backend object is being replaced.
The same trust-the-cache pattern exists a second time in
removeExportPolicyRules(
ontap_common.go), which is called on every unpublish of a qtree-level policy: ifpublishInfo.Nodesarrives empty, it takes an explicit branch — "No active nodes remaining,removing ALL export policy rules" — and empties the policy. We did not observe this branch
misfire in the incident, but it is reachable under the same transient-cache conditions on a
shared RWX volume, and it would revoke all remaining nodes at the qtree level. We suggest
fixing both call sites as one pattern. (#1086 was a previous incident of unpublish-side
collateral revocation in this same area.)
Environment
ontap-nas-economydriver, REST API,autoExportPolicy: true,autoExportCIDRs: ["10.63.0.0/21"]To Reproduce
Timing-dependent; observed in production during a controller pod rescheduling. Sequence:
ontap-nas-economybackend,autoExportPolicy: true, multiple nodes holdingpublications for RWX volumes, steady publish/unpublish churn from short-lived pods.
(in our case the drain also queued unpublish/publish work for the drained node).
TridentBackendConfig (
crdControllerEvent=addreconciles) while the queued CSI callsdrain.
Evidence from our incident (2026-08-31, times UTC). We captured an HTTP-level trace of
Trident's ONTAP REST traffic covering the whole window, plus the controller log:
DELETE /api/protocols/nfs/export-policies/21474840540/rules/{2,3,4,5,6,1},six calls ~30 ms apart: every worker rule removed from the first backend's policy
(
trident-ce7d4ac1-…). Immediately followed by an attempted delete of the policy itself →400, ONTAP error 1703945 "Ruleset is in use by a volume" (see companion report onTerminate).policy (
trident-e0821f5f-…, id 21474855140).error deleting export policy: … [400] … "Ruleset is in use by a volume"taggedcrdControllerEvent=add,workflow="cr=reconcile".was not unpublish-driven.
tridentvolumepublicationslisted all 6 workers for the shared volumes throughout;tridentnodesIPs were correct. Trident's recorded intent was right; only the ONTAP policywas emptied.
it became the only node able to mount. FailedMount
exit status 32on the other fiveworkers from 19:12 onward.
publishes re-added four more rules; those nodes recovered within seconds. One node whose
replacement pods re-pinned the old VolumeAttachments stayed locked out until we intervened
on the array.
Expected behavior
Reconciliation must never treat a transiently empty or partial cache view as authority to
delete rules from an in-use export policy. Concretely, some combination of:
are fully populated after startup or backend replacement.
volume on that backend; log and retry instead.
removeExportPolicyRules' remove-all branch.Additional context
related discussion about FlexVol-level policies for this driver: With the ontap-nas-economy driver, use the same export policy for the trident_qtree_pool volume as with the qtrees #1149, Feature request: improve export policies at the FlexVol level in the context of ONTAP-NAS-ECO #709.
concurrency enabled).
Terminatedestroys this same live policy onevery backend update (ontap-nas-economy: driver Terminate attempts to delete the shared backend export policy while live FlexVols still use it — fires on every TridentBackendConfig update #1180); no repair path exists once a rule is missing
(autoExportPolicy: a node with an existing publication is locked out permanently when its export rule is missing or stale — no code path ever repairs export policies #1181) — which is what turned this transient wipe into a multi-hour outage.
AI assistance
Assisted-by: Claude Fable 5 noreply@anthropic.com