Files
zeekayandHanzo Dev 69ddc9947a s3-csi: ZAP-native CSI driver for the Hanzo filer
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]>
2026-07-27 16:42:29 -07:00

233 lines
5.5 KiB
Go

package driver
import (
"fmt"
"path"
"strconv"
"strings"
"github.com/hanzoai/s3-csi/pkg/datalocality"
"github.com/hanzoai/s3-csi/pkg/mountmanager"
"github.com/hanzoai/s3/s3/glog"
)
type Unmounter interface {
Unmount() error
}
type Mounter interface {
Mount(target string) (Unmounter, error)
}
type mountServiceMounter struct {
driver *S3Driver
volumeID string
readOnly bool
volContext map[string]string
client *mountmanager.Client
}
type mountServiceUnmounter struct {
client *mountmanager.Client
volumeID string
}
func newMounter(volumeID string, readOnly bool, driver *S3Driver, volContext map[string]string) (Mounter, error) {
client, err := mountmanager.NewClient(driver.mountEndpoint)
if err != nil {
return nil, err
}
contextCopy := make(map[string]string, len(volContext))
for k, v := range volContext {
contextCopy[k] = v
}
return &mountServiceMounter{
driver: driver,
volumeID: volumeID,
readOnly: readOnly,
volContext: contextCopy,
client: client,
}, nil
}
func (m *mountServiceMounter) Mount(target string) (Unmounter, error) {
if target == "" {
return nil, fmt.Errorf("target path is required")
}
filers := make([]string, len(m.driver.filers))
for i, address := range m.driver.filers {
filers[i] = string(address)
}
cacheDir := GetCacheDir(m.driver.CacheDir, m.volumeID)
localSocket := GetLocalSocket(m.driver.volumeSocketDir, m.volumeID)
args, err := m.buildMountArgs(target, cacheDir, localSocket, filers)
if err != nil {
return nil, err
}
req := &mountmanager.MountRequest{
VolumeID: m.volumeID,
TargetPath: target,
CacheDir: cacheDir,
MountArgs: args,
LocalSocket: localSocket,
}
_, err = m.client.Mount(req)
if err != nil {
return nil, err
}
return &mountServiceUnmounter{
client: m.client,
volumeID: m.volumeID,
}, nil
}
func (u *mountServiceUnmounter) Unmount() error {
_, err := u.client.Unmount(&mountmanager.UnmountRequest{VolumeID: u.volumeID})
return err
}
func (m *mountServiceMounter) buildMountArgs(targetPath, cacheDir, localSocket string, filers []string) ([]string, error) {
volumeContext := m.volContext
if volumeContext == nil {
volumeContext = map[string]string{}
}
var filerPath string
if path.IsAbs(m.volumeID) {
// path already resolved in controller and passed as volumeID
filerPath = m.volumeID
} else {
// non-absolute-path volume ID, volume is either legacy or this is a static provision
contextPath := volumeContext["path"]
if contextPath == "" {
// Backward-compatibility for legacy volume ID
filerPath = path.Join("/buckets", m.volumeID)
} else {
// This is a static provision
// Always use the context path parameter as filerPath
filerPath = contextPath
}
}
collection := volumeContext["collection"]
if collection == "" {
// Use path basename when collection is not set
collection = path.Base(filerPath)
}
args := []string{
"-logtostderr=true",
"mount",
"-dirAutoCreate=true",
"-umask=000",
fmt.Sprintf("-dir=%s", targetPath),
fmt.Sprintf("-localSocket=%s", localSocket),
fmt.Sprintf("-cacheDir=%s", cacheDir),
}
if m.readOnly {
args = append(args, "-readOnly")
}
argsMap := map[string]string{
"collection": collection,
"collectionQuotaMB": initialCollectionQuotaMB(volumeContext[volumeCapacityKey]),
"filer": strings.Join(filers, ","),
"filer.path": filerPath,
"cacheCapacityMB": strconv.Itoa(m.driver.CacheCapacityMB),
"cacheMetaTtlSec": strconv.Itoa(m.driver.CacheMetaTtlSec),
"concurrentReaders": strconv.Itoa(m.driver.ConcurrentReaders),
"concurrentWriters": strconv.Itoa(m.driver.ConcurrentWriters),
"map.uid": m.driver.UidMap,
"map.gid": m.driver.GidMap,
"disk": "",
"dataCenter": "",
"replication": "",
"ttl": "",
"chunkSizeLimitMB": "",
"volumeServerAccess": "",
"readRetryTime": "",
}
dataLocality := m.driver.DataLocality
if contextLocality, ok := volumeContext["dataLocality"]; ok && contextLocality != "" {
if dl, ok := datalocality.FromString(contextLocality); ok {
dataLocality = dl
} else {
return nil, fmt.Errorf("invalid volumeContext dataLocality: %s", contextLocality)
}
}
dataCenter := m.driver.DataCenter
if err := CheckDataLocality(&dataLocality, &dataCenter); err != nil {
return nil, err
}
switch dataLocality {
case datalocality.Write_preferLocalDc:
argsMap["dataCenter"] = dataCenter
}
parameterArgMap := map[string]string{
"uidMap": "map.uid",
"gidMap": "map.gid",
"diskType": "disk",
}
ignoredArgs := map[string]struct{}{
"collectionQuotaMB": {},
"dataLocality": {},
"path": {},
"parentDir": {},
"volumeName": {},
volumeCapacityKey: {},
}
for key, value := range volumeContext {
if _, ignored := ignoredArgs[key]; ignored {
continue
}
if mapped, ok := parameterArgMap[key]; ok {
key = mapped
}
if _, ok := argsMap[key]; !ok {
glog.Warningf("VolumeContext '%s' ignored", key)
continue
}
if value != "" {
argsMap[key] = value
}
}
for key, value := range argsMap {
if value == "" {
continue
}
args = append(args, fmt.Sprintf("-%s=%s", key, value))
}
return args, nil
}
func initialCollectionQuotaMB(capacityBytes string) string {
capacity, err := strconv.ParseInt(capacityBytes, 10, 64)
if err != nil || capacity <= 1 {
return ""
}
const bytesPerMiB = int64(1024 * 1024)
capacityMB := capacity / bytesPerMiB
if capacity%bytesPerMiB != 0 {
capacityMB++
}
return strconv.FormatInt(capacityMB, 10)
}