mirror of
https://github.com/hanzoai/s3-csi.git
synced 2026-09-30 23:52:32 +00:00
Fork of seaweedfs/seaweedfs-csi-driver, renamed seaweedfs -> s3 throughout and ported off protobuf entirely. WHY a fork was unavoidable: hanzoai/s3 serves HanzoFiler over the native ZAP transport on the port still named "grpc" (18888) — s3/command/filer.go binds it with transport.ListenStream and says it "replaces the legacy gRPC HanzoFiler server: the whole filer (28 unary + 5 streaming RPCs) now answers over ZAP, no gRPC". RegisterHanzoFilerServer exists nowhere in that tree. So the upstream driver's gRPC client cannot connect at all; it fails as `Unimplemented: unknown service filer_pb.SeaweedFiler`, which LOOKS like a service-name mismatch but is a gRPC client hitting a non-gRPC port. Renaming the service in the client would not have helped. pkg/filer is the only place this driver touches storage, and it speaks ZAP: Mkdir -> CreateEntry (CreateVolume) Remove -> DeleteEntry (DeleteVolume) Exists -> LookupDirectoryEntry (ValidateVolumeCapabilities) One shared, concurrency-safe client multiplexes over a single ZAP connection, dials lazily so controller start does not depend on filer readiness, and redials exactly once on a transport fault (covers a filer restart closing an idle conn without becoming a retry storm against a struggling backend). pkg/driver/volume.go Quota() likewise moves to s3/wire/mount — the fork generates no mount_pb at all. Dead gRPC plumbing (WithFilerClient, AdjustedUrl, grpcDialOption) is deleted rather than left inert. The kubelet-facing side stays CSI gRPC: that is the Kubernetes contract between kubelet and any CSI driver, not a Hanzo choice. Zero protobuf call sites remain. Whole module compiles clean. Co-authored-by: Hanzo Dev <[email protected]>
159 lines
5.1 KiB
Go
159 lines
5.1 KiB
Go
package driver
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/hanzoai/s3/s3/glog"
|
|
"k8s.io/mount-utils"
|
|
)
|
|
|
|
var mountutil = mount.New("")
|
|
|
|
// isStagingPathHealthy checks if the staging path has a healthy FUSE mount.
|
|
// It returns true if the path is mounted and accessible, false otherwise.
|
|
func isStagingPathHealthy(stagingPath string) bool {
|
|
// Check if path exists
|
|
info, err := os.Stat(stagingPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
glog.V(4).Infof("staging path %s does not exist", stagingPath)
|
|
return false
|
|
}
|
|
// "Transport endpoint is not connected" or similar FUSE errors
|
|
if mount.IsCorruptedMnt(err) {
|
|
glog.Warningf("staging path %s has corrupted mount: %v", stagingPath, err)
|
|
return false
|
|
}
|
|
glog.V(4).Infof("staging path %s stat error: %v", stagingPath, err)
|
|
return false
|
|
}
|
|
|
|
// Check if it's a directory
|
|
if !info.IsDir() {
|
|
glog.Warningf("staging path %s is not a directory", stagingPath)
|
|
return false
|
|
}
|
|
|
|
// Check if it's a mount point
|
|
isMnt, err := mountutil.IsMountPoint(stagingPath)
|
|
if err != nil {
|
|
if mount.IsCorruptedMnt(err) {
|
|
glog.Warningf("staging path %s has corrupted mount point: %v", stagingPath, err)
|
|
return false
|
|
}
|
|
glog.V(4).Infof("staging path %s mount point check error: %v", stagingPath, err)
|
|
return false
|
|
}
|
|
|
|
if !isMnt {
|
|
glog.V(4).Infof("staging path %s is not a mount point", stagingPath)
|
|
return false
|
|
}
|
|
|
|
// Try to read the directory to verify FUSE is responsive
|
|
_, err = os.ReadDir(stagingPath)
|
|
if err != nil {
|
|
glog.Warningf("staging path %s is not readable (FUSE may be dead): %v", stagingPath, err)
|
|
return false
|
|
}
|
|
|
|
glog.V(4).Infof("staging path %s is healthy", stagingPath)
|
|
return true
|
|
}
|
|
|
|
// cleanupCorruptedStagingPath force-cleans a staging path whose FUSE
|
|
// daemon is already dead (ENOTCONN / IsCorruptedMnt). Safe because the
|
|
// kernel will reject reads/writes through a corrupted mount, so cleanup
|
|
// cannot propagate deletes through a live FUSE.
|
|
func cleanupCorruptedStagingPath(stagingPath string) error {
|
|
if err := mount.CleanupMountPoint(stagingPath, mountutil, true); err != nil {
|
|
glog.Warningf("failed to cleanup corrupted mount point %s: %v", stagingPath, err)
|
|
return err
|
|
}
|
|
glog.Infof("successfully cleaned up corrupted staging path %s", stagingPath)
|
|
return nil
|
|
}
|
|
|
|
// cleanupStaleStagingPath cleans up a stale or corrupted staging mount point.
|
|
// It attempts to unmount and remove the directory.
|
|
//
|
|
// Safety invariant: this function MUST NOT call os.RemoveAll on a path that
|
|
// is still a live mount point. If the staging path is still a working FUSE
|
|
// mount, RemoveAll would walk into the mount and recursively unlink user
|
|
// data through it. Callers (health monitor recovery, NodeStage/NodePublish
|
|
// re-stage) must treat a refusal as a hard failure rather than re-staging
|
|
// over an undeleted mount.
|
|
func cleanupStaleStagingPath(stagingPath string) error {
|
|
glog.Infof("cleaning up stale staging path %s", stagingPath)
|
|
|
|
// Surface unmount errors. A failed unmount almost always means the
|
|
// FUSE mount is still alive (EBUSY because pods or bind mounts still
|
|
// pin it), and silently dropping the error is what lets the
|
|
// post-unmount RemoveAll below recurse into a live mount.
|
|
unmountErr := mountutil.Unmount(stagingPath)
|
|
if unmountErr != nil {
|
|
glog.Warningf("unmount staging path %s failed: %v", stagingPath, unmountErr)
|
|
}
|
|
|
|
// Use Lstat so a leftover dangling symlink at stagingPath is still
|
|
// discoverable (and removable) instead of being mis-classified by
|
|
// stat-following ENOENT.
|
|
_, statErr := os.Lstat(stagingPath)
|
|
if statErr != nil {
|
|
if os.IsNotExist(statErr) {
|
|
glog.Infof("successfully cleaned up staging path %s", stagingPath)
|
|
return nil
|
|
}
|
|
if mount.IsCorruptedMnt(statErr) {
|
|
return cleanupCorruptedStagingPath(stagingPath)
|
|
}
|
|
glog.Warningf("stat on staging path %s failed during cleanup: %v", stagingPath, statErr)
|
|
return statErr
|
|
}
|
|
|
|
// Re-check whether the path is still a mount point AFTER unmount.
|
|
// If it is, the unmount failed (or completed only as a lazy detach
|
|
// while the kernel still routes I/O to the FUSE daemon) and
|
|
// RemoveAll would walk a live FUSE mount.
|
|
isMnt, mntErr := mountutil.IsMountPoint(stagingPath)
|
|
if mntErr != nil {
|
|
if mount.IsCorruptedMnt(mntErr) {
|
|
return cleanupCorruptedStagingPath(stagingPath)
|
|
}
|
|
return fmt.Errorf("check mount point %s after unmount: %w", stagingPath, mntErr)
|
|
}
|
|
if isMnt {
|
|
return fmt.Errorf("refuse to remove staging path %s: still a mount point after unmount (unmount err: %v); not deleting through a live FUSE", stagingPath, unmountErr)
|
|
}
|
|
|
|
if err := os.RemoveAll(stagingPath); err != nil {
|
|
glog.Warningf("failed to remove staging path %s: %v", stagingPath, err)
|
|
return err
|
|
}
|
|
|
|
glog.Infof("successfully cleaned up staging path %s", stagingPath)
|
|
return nil
|
|
}
|
|
|
|
func waitForMount(path string, timeout time.Duration) error {
|
|
var elapsed time.Duration
|
|
var interval = 10 * time.Millisecond
|
|
for {
|
|
notMount, err := mountutil.IsLikelyNotMountPoint(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !notMount {
|
|
return nil
|
|
}
|
|
time.Sleep(interval)
|
|
elapsed = elapsed + interval
|
|
if elapsed >= timeout {
|
|
return errors.New("timeout waiting for mount")
|
|
}
|
|
}
|
|
}
|