Skip to content

ontap-nas-economy: controller restart deletes all rules from the backend export policy, revoking NFS access for every node on the backend #1179

Description

@notsrch

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):

  1. core/orchestrator_core.goreconcileNodeAccessOnBackend: because the economy driver
    implements PublishEnforceable, the node list passed to the driver is
    publishedNodesForBackend(b), not the registered-node list.
  2. 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 (addBackendupdateBackend builds a fresh
    backend object), this intersection is transiently empty.
  3. The empty result flows into reconcileNASNodeAccessreconcileExportPolicyRules
    (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:

  1. ontap-nas-economy backend, autoExportPolicy: true, multiple nodes holding
    publications for RWX volumes, steady publish/unpublish churn from short-lived pods.
  2. 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).
  3. On startup, the new controller re-initializes each backend from its
    TridentBackendConfig (crdControllerEvent=add reconciles) while the queued CSI calls
    drain.
  4. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions