Files
e8b89e78c3 chore: no seaweed, no weed — it is s3 (#4)
* fix(docker): clone the fork that exists, at a commit that exists

The s3-mount image has never built. A global rename turned the upstream
seaweedfs reference into `github.com/s3/s3`, which is not a repository, so the
build died on `git clone ... exit code 128` before compiling anything.

Three more things were wrong behind it, each of which would have failed the next
step in turn:

  clone   github.com/s3/s3        -> github.com/hanzoai/s3
  path    /go/src/github.com/s3/s3 -> /go/src/github.com/hanzoai/s3
  dir     cd weed                  -> cd s3   (the fork renamed it)
  commit  19ce7c0b6fdb             -> 6dda4e4d6bca

That commit is the real one. 19ce7c0b6fdb is an UPSTREAM seaweedfs hash and does
not exist in our fork — the API 422s on it — so the checkout could never have
succeeded even with the URL fixed. The ARG stays so a build can still pin.

SEAWEEDFS_COMMIT is renamed S3_COMMIT while we are here; nothing consumed it yet.

* chore: no seaweed, no weed — it is s3

369 occurrences across 33 files, plus nine files whose names carried it. All of
it gone: manifests, helm chart, nomad jobs, workflows, docs and code.

Two of these were not cosmetic.

The driver spawned a binary called "weed":

    DefaultWeedBinary = "weed"

Our fork builds it as `s3` — the running filer pod has /usr/bin/s3 and no weed
at all — so every mount would have failed with an executable that is not there,
however well the image built.

And the crash-recovery integration test killed processes matching `pgrep -x
weed`. Nothing is named that any more, so it would have found no pids, killed
nothing, and passed while testing exactly nothing. It now matches `s3`. The test
already had a guard that fails when there is nothing to kill, which is the only
reason this would have been noticed at all.

The rest follows the same rename the Dockerfile got: SEAWEEDFS_COMMIT is
S3_COMMIT, and the release/sync workflows that patch that ARG were updated with
it or they would silently stop pinning anything.

No upstream import paths were touched — go.mod depends on github.com/hanzoai/s3,
not on anything upstream, so nothing here was load-bearing for the build.
Verified: go build clean, all three test packages pass.

---------

Co-authored-by: hanzo-dev <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 18:26:50 -07:00

194 lines
6.2 KiB
Go

// Package filer is the driver's ZAP-native client for the HanzoFiler service.
//
// It replaces the upstream driver's protobuf/gRPC filer client. hanzoai/s3 serves
// HanzoFiler over the native ZAP transport on the port historically called the
// "gRPC port" (18888) — s3/command/filer.go binds it with transport.ListenStream
// and states plainly that this "replaces the legacy gRPC HanzoFiler server: the
// whole filer (28 unary + 5 streaming RPCs) now answers over ZAP, no gRPC".
// There is no RegisterHanzoFilerServer anywhere in that tree.
//
// So a gRPC client cannot talk to it at all — the failure mode is
// `Unimplemented: unknown service filer_pb.S3Filer`, which reads like a
// service-name mismatch but is really a gRPC client hitting a non-gRPC port. No
// amount of renaming or re-addressing fixes that; the transport has to change.
//
// Only three operations are needed to back CSI volumes, because a volume IS a
// directory under the filer root:
//
// Mkdir -> CreateEntry (CreateVolume)
// Remove -> DeleteEntry (DeleteVolume)
// Exists -> LookupDirectoryEntry (ValidateVolumeCapabilities)
//
// The kubelet-facing side of the driver stays CSI gRPC — that is the Kubernetes
// contract between kubelet and any CSI driver, not a Hanzo choice. This package
// is the only place the driver speaks to storage, and it speaks ZAP.
package filer
import (
"context"
"fmt"
"sync"
"time"
filerwire "github.com/hanzoai/s3/s3/wire/filer"
)
// Client is a ZAP connection to one filer endpoint. It is safe for concurrent
// use: the CSI controller serves Create/Delete/Validate concurrently, and the
// underlying transport.Conn is multiplexed per-call by PromiseID, but dial and
// reconnect are serialized here so a broken conn is replaced once, not per call.
type Client struct {
addr string
mu sync.Mutex
conn *filerwire.Client
}
// New returns a client for addr (e.g. "s3-filer.hanzo.svc.cluster.local:18888").
// It does NOT dial eagerly: the controller starts before the filer may be
// reachable, and a lazy first dial keeps startup order from mattering.
func New(addr string) *Client {
return &Client{addr: addr}
}
// dial returns a live client, reconnecting if needed.
func (c *Client) dial() (*filerwire.Client, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.conn != nil {
return c.conn, nil
}
cl, err := filerwire.Dial("tcp", c.addr)
if err != nil {
return nil, fmt.Errorf("filer: dial %s over ZAP: %w", c.addr, err)
}
c.conn = cl
return cl, nil
}
// drop discards a connection believed dead so the next call redials. Passing the
// observed client makes this a no-op if another goroutine already reconnected.
func (c *Client) drop(seen *filerwire.Client) {
c.mu.Lock()
defer c.mu.Unlock()
if c.conn == seen {
_ = c.conn.Close()
c.conn = nil
}
}
// Close releases the connection.
func (c *Client) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.conn == nil {
return nil
}
err := c.conn.Close()
c.conn = nil
return err
}
// Mkdir creates dir/name as a directory entry. Backs CreateVolume.
//
// Directory mode is 0o777|os.ModeDir expressed as the filer's uint32 mode. The
// entry carries mtime/crtime so a freshly provisioned volume does not appear
// with a zero timestamp to anything that lists it.
func (c *Client) Mkdir(ctx context.Context, dir, name string) error {
now := time.Now().Unix()
entry := filerwire.NewEntry(filerwire.EntryInput{
Name: name,
IsDirectory: true,
Attributes: filerwire.NewFuseAttributes(filerwire.FuseAttributesInput{
Mtime: now,
Crtime: now,
FileMode: uint32(0o777) | (1 << 31), // os.ModeDir
}),
})
req := filerwire.NewCreateEntryRequest(filerwire.CreateEntryRequestInput{
Directory: dir,
Entry: entry,
})
return c.call(ctx, "mkdir", dir, name, func(cl *filerwire.Client) error {
resp, err := cl.CreateEntry(ctx, req)
if err != nil {
return err
}
// CreateEntry reports failure in-band, not as a transport error.
if e := resp.Error(); e != "" {
return fmt.Errorf("%s", e)
}
return nil
})
}
// Remove deletes dir/name recursively, including its chunk data. Backs
// DeleteVolume, which must be idempotent — a missing entry is success, since
// CSI may retry a delete whose first attempt already landed.
func (c *Client) Remove(ctx context.Context, dir, name string) error {
req := filerwire.NewDeleteEntryRequest(filerwire.DeleteEntryRequestInput{
Directory: dir,
Name: name,
IsDeleteData: true,
IsRecursive: true,
IgnoreRecursiveError: false,
})
return c.call(ctx, "remove", dir, name, func(cl *filerwire.Client) error {
resp, err := cl.DeleteEntry(ctx, req)
if err != nil {
return err
}
if e := resp.Error(); e != "" {
return fmt.Errorf("%s", e)
}
return nil
})
}
// Exists reports whether dir/name is present. Backs ValidateVolumeCapabilities.
// A lookup miss is (false, nil) — "absent" is an answer, not a failure — so only
// a transport or filer fault surfaces as an error.
func (c *Client) Exists(ctx context.Context, dir, name string) (bool, error) {
req := filerwire.NewLookupDirectoryEntryRequest(filerwire.LookupDirectoryEntryRequestInput{
Directory: dir,
Name: name,
})
var found bool
err := c.call(ctx, "exists", dir, name, func(cl *filerwire.Client) error {
resp, err := cl.LookupDirectoryEntry(ctx, req)
if err != nil {
return err
}
found = resp.HasEntry()
return nil
})
if err != nil {
return false, err
}
return found, nil
}
// call runs fn against a live connection, redialing once if the first attempt
// fails at the transport. One retry is deliberate: it covers the common case of
// a filer restart having closed an idle pooled connection, without turning a
// genuine filer fault into a retry storm against a struggling backend.
func (c *Client) call(ctx context.Context, op, dir, name string, fn func(*filerwire.Client) error) error {
cl, err := c.dial()
if err != nil {
return err
}
if err = fn(cl); err == nil {
return nil
}
c.drop(cl)
cl2, derr := c.dial()
if derr != nil {
return fmt.Errorf("filer %s %s/%s: %v (redial: %w)", op, dir, name, err, derr)
}
if err2 := fn(cl2); err2 != nil {
return fmt.Errorf("filer %s %s/%s: %w", op, dir, name, err2)
}
return nil
}