mirror of
https://github.com/hanzoai/vfs.git
synced 2026-10-30 18:18:21 +00:00
* vfs: gate Mount() + cmd/vfsd behind cloud_mount build tag The Mount(), SetInstance(), Shutdown() HTTP-server surface drags in hanzoai/cloud + hanzoai/zip. Downstream services that consume the core block + FS API (vfs.VFS, vfs.FS, vfs.File, NewCrypto) directly — Liquidity ATS shard layer, KMS, BD — don't need the HTTP wire and should not pull in the cloud module graph. Without -tags cloud_mount: vfs builds with only backend/cache/crypto/ fs/file/block. With it: Mount() + cmd/vfsd are compiled in, exactly as before. vfs_test, fs_test, sqlite_test, production_test still run + pass on the default build (verified locally). * chore: bump go to 1.26.4 Picks up upstream security fixes for crypto/x509, mime, net/textproto, crypto/fips140. No dependency bumps — only the toolchain directive. go test -race ./... is green.
140 lines
3.9 KiB
Go
140 lines
3.9 KiB
Go
//go:build cloud_mount
|
|
|
|
// vfsd — HIP-0106 thin shim.
|
|
//
|
|
// Mounts pkg/vfs into a zip.App via the same Mount() the unified cloud
|
|
// binary calls. The CLI tooling for put/get/stats/mount lives in
|
|
// cmd/vfs; this binary is the standalone HTTP server shape for
|
|
// environments that need block CRUD over HTTP without the full cloud
|
|
// surface.
|
|
//
|
|
// The data plane is only enabled when a VFS instance is attached via
|
|
// vfs.SetInstance. Without one the binary still serves /v1/vfs/health
|
|
// and a 503 /v1/vfs/readyz — useful for ConfigMap-driven k8s probes
|
|
// while the operator wires the backend.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/luxfi/age"
|
|
"github.com/luxfi/log"
|
|
|
|
"github.com/hanzoai/cloud"
|
|
"github.com/hanzoai/vfs/pkg/backend"
|
|
_ "github.com/hanzoai/vfs/pkg/backend/file"
|
|
_ "github.com/hanzoai/vfs/pkg/backend/s3"
|
|
"github.com/hanzoai/vfs"
|
|
"github.com/hanzoai/zip"
|
|
"github.com/hanzoai/zip/middleware"
|
|
)
|
|
|
|
// version is overridden at build time via -ldflags "-X main.version=...".
|
|
var version = "dev"
|
|
|
|
func main() {
|
|
vfs.Version = version
|
|
|
|
cfg := cloud.LoadConfig()
|
|
if err := cfg.Validate(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "config: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
deps := cloud.BuildDeps(cfg)
|
|
|
|
// Optional backend wiring via env. VFSD_BACKEND=file:///tmp/v or
|
|
// s3://bucket/prefix. When unset, the binary boots with no data
|
|
// plane — /v1/vfs/blocks/* return 503 until SetInstance fires.
|
|
if backendURL := strings.TrimSpace(os.Getenv("VFSD_BACKEND")); backendURL != "" {
|
|
if v, err := buildVFS(backendURL); err != nil {
|
|
log.Crit("vfsd: backend wiring failed", "err", err)
|
|
} else {
|
|
vfs.SetInstance(v)
|
|
log.Info("vfsd: backend attached", "backend", backendURL)
|
|
}
|
|
} else {
|
|
log.Info("vfsd: VFSD_BACKEND unset — data plane disabled (readyz returns 503)")
|
|
}
|
|
|
|
app := zip.New(zip.Config{
|
|
Logger: deps.Logger,
|
|
AppName: "vfsd",
|
|
})
|
|
app.Use(middleware.Recover())
|
|
app.Use(middleware.RequestID())
|
|
app.Use(middleware.Logger(deps.Logger))
|
|
|
|
if err := vfs.Mount(app, deps); err != nil {
|
|
log.Crit("vfsd: mount", "err", err)
|
|
}
|
|
|
|
listenErr := make(chan error, 1)
|
|
go func() {
|
|
log.Info("vfsd: listening", "addr", cfg.ListenAddr)
|
|
listenErr <- app.Listen(cfg.ListenAddr)
|
|
}()
|
|
|
|
sig := make(chan os.Signal, 1)
|
|
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
|
select {
|
|
case s := <-sig:
|
|
log.Info("vfsd: shutting down", "signal", s)
|
|
case err := <-listenErr:
|
|
log.Crit("vfsd: listen failed", "err", err)
|
|
}
|
|
|
|
stopCtx, stopCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer stopCancel()
|
|
_ = vfs.Shutdown(stopCtx)
|
|
_ = app.ShutdownWithContext(stopCtx)
|
|
}
|
|
|
|
// buildVFS wires a VFS instance from env. Mirrors the cmd/vfs CLI's
|
|
// openVFS but reads from env instead of flags. age keys come from
|
|
// VFSD_AGE_KEY (path to key file) — required for any meaningful
|
|
// roundtrip. Without it the instance is write-only.
|
|
func buildVFS(backendURL string) (*vfs.VFS, error) {
|
|
ctx := context.Background()
|
|
be, err := backend.Open(ctx, backendURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("backend.Open: %w", err)
|
|
}
|
|
|
|
var recs []age.Recipient
|
|
var ids []age.Identity
|
|
if keyPath := strings.TrimSpace(os.Getenv("VFSD_AGE_KEY")); keyPath != "" {
|
|
keyBytes, err := os.ReadFile(keyPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read VFSD_AGE_KEY: %w", err)
|
|
}
|
|
parsed, err := age.ParseIdentities(strings.NewReader(string(keyBytes)))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse age identities: %w", err)
|
|
}
|
|
ids = parsed
|
|
// Recipients default to the identities' own public keys.
|
|
for _, id := range parsed {
|
|
if x25519, ok := id.(*age.X25519Identity); ok {
|
|
recs = append(recs, x25519.Recipient())
|
|
}
|
|
}
|
|
}
|
|
|
|
crypto, err := vfs.NewCrypto(recs, ids)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("vfs.NewCrypto: %w", err)
|
|
}
|
|
return vfs.New(vfs.Config{
|
|
Backend: be,
|
|
Crypto: crypto,
|
|
CacheMax: 256 << 20,
|
|
})
|
|
}
|