vfs: decouple from hanzoai/cloud — HIP-0106 mount adapter + vfsd move to cloud/{mounts,cmd}/vfs

vfs is OSS and stands alone as a pure block-filesystem library; the private
cloud aggregator imports vfs, never the reverse. mount.go (build-tagged but
still forcing the module require), its test, and cmd/vfsd relocated to cloud.
GOWORK=off go build/test ./... green; hanzoai/cloud no longer in the graph.
This commit is contained in:
zeekay
2026-06-11 19:52:10 -07:00
parent d9ea0d5b08
commit ea13fb13be
5 changed files with 1 additions and 565 deletions
-139
View File
@@ -1,139 +0,0 @@
//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,
})
}
+1 -16
View File
@@ -7,11 +7,8 @@ require (
github.com/aws/aws-sdk-go-v2 v1.41.2
github.com/aws/aws-sdk-go-v2/config v1.32.8
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2
github.com/hanzoai/cloud v0.1.0
github.com/hanzoai/zip v0.2.0
github.com/jacobsa/fuse v0.0.0-20260302145937-f1ba38d60fdf
github.com/luxfi/age v1.5.0
github.com/luxfi/log v1.4.3
github.com/spf13/cobra v1.10.2
github.com/zeebo/blake3 v0.2.4
modernc.org/sqlite v1.50.0
@@ -19,7 +16,6 @@ require (
require (
filippo.io/hpke v0.4.0 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.8 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
@@ -37,27 +33,16 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
github.com/aws/smithy-go v1.24.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gofiber/fiber/v3 v3.2.0 // indirect
github.com/gofiber/schema v1.7.1 // indirect
github.com/gofiber/utils/v2 v2.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.21 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.70.0 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
golang.org/x/tools v0.43.0 // indirect
modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
-49
View File
@@ -4,8 +4,6 @@ c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAw
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
@@ -45,77 +43,38 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlW
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gofiber/fiber/v3 v3.2.0 h1:g9+09D320foINPpCnR3ibQ5oBEFHjAWRRfDG1te54u8=
github.com/gofiber/fiber/v3 v3.2.0/go.mod h1:FHOsc2Db7HhHpsE62QAaJlXVV1pNkbZEptZ4jtti7m4=
github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=
github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=
github.com/gofiber/utils/v2 v2.0.4 h1:WwAxUA7L4MW2DjdEHF234lfqvBqd2vYYuBtA9TJq2ec=
github.com/gofiber/utils/v2 v2.0.4/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hanzoai/cloud v0.1.0 h1:SLN0oTzKukHEObfXP9KI0ko2/1BmGCh9Bfn19LG9Jwc=
github.com/hanzoai/cloud v0.1.0/go.mod h1:M9Zle5oc6OD5KsylaDmksH6asjhAT2o0P8vNI3TgbY0=
github.com/hanzoai/zip v0.2.0 h1:2GI1OoUv4QdCxmfiwoiyYsSHpiPAxrp0v+Gu4BLtTVI=
github.com/hanzoai/zip v0.2.0/go.mod h1:3G+k2wy5bQ1wld66m0OPH8/LJ0kgIeNi1KPymMmlbf4=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jacobsa/fuse v0.0.0-20260302145937-f1ba38d60fdf h1:1FpPcJSf6jjJGvIltaLwJCpbFCMI9bVUCAAxUSxqWnY=
github.com/jacobsa/fuse v0.0.0-20260302145937-f1ba38d60fdf/go.mod h1:fcpw1yk/suvFhB8rT9P+pst+NLboWsBLky9csooKjPc=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/luxfi/age v1.5.0 h1:G69HbSV4R3vKEH9B0CulnRaMdSdf4RalMgP8xKmxHeI=
github.com/luxfi/age v1.5.0/go.mod h1:iAYAxgvrXxcy746+Ovh/eWWDuF9teJLNcCSSOX9RYW0=
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU=
github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ=
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=
github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
@@ -127,21 +86,13 @@ golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
-221
View File
@@ -1,221 +0,0 @@
//go:build cloud_mount
// HIP-0106 Mount() entry point. Lets cmd/cloud import this package and
// register VFS with the shared zip.App alongside every other Hanzo
// subsystem (iam, base, kms, amqp, …).
//
// Wire shape:
//
// import _ "github.com/hanzoai/vfs" // init() registers
//
// VFS has no public HTTP surface today — it's an S3-backed virtual
// block filesystem driven through FUSE mounts and CLI/sidecar usage.
// Mount() exposes liveness/readiness probes plus a minimal HTTP
// CRUD surface (PUT/GET/DELETE single block by ID) so other in-process
// subsystems can talk to it without falling back to the CLI.
//
// The VFS instance itself is opt-in: callers either inject one via
// SetInstance(v) (used by the standalone shim and tests) or skip it
// (the cloud binary mounts VFS as a passthrough until a backend +
// crypto config lands in cloud.Config). Readiness reports "not_ready"
// until an instance is attached.
package vfs
import (
"context"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"sync"
"github.com/hanzoai/cloud"
"github.com/hanzoai/vfs/pkg/backend"
"github.com/hanzoai/zip"
)
// Version is overridden at build time via -ldflags
// "-X github.com/hanzoai/vfs.Version=...".
var Version = "dev"
// mountedInstance is the VFS handle the HTTP routes operate against.
// nil until SetInstance() is called. Guarded by mu.
var (
mu sync.RWMutex
mountedInstance *VFS
)
// SetInstance attaches a VFS handle for the mounted HTTP routes.
// Calling this with nil disables the data-plane routes (Get/Put/Delete
// return 503). Idempotent.
//
// The standalone shim wires this from CLI flags. cloud.Config has no
// VFS-backend block yet — cloud binary deploys leave it nil until
// HIP-0107 config lands.
func SetInstance(v *VFS) {
mu.Lock()
defer mu.Unlock()
mountedInstance = v
}
// Instance returns the currently attached VFS handle (or nil).
func Instance() *VFS {
mu.RLock()
defer mu.RUnlock()
return mountedInstance
}
// Mount registers VFS routes with the shared cloud zip.App per HIP-0106.
//
// Routes:
//
// GET /v1/vfs/health — liveness, never blocks
// GET /v1/vfs/readyz — readiness, 200 only when instance attached
// PUT /v1/vfs/blocks — body = raw block bytes; returns {"id": "..."}
// GET /v1/vfs/blocks/:id — returns raw decrypted block bytes
// DELETE /v1/vfs/blocks/:id — removes a block from the backend
// GET /v1/vfs/stats — backend/cache counters
//
// Identity headers (X-Org-Id, X-User-Id) are honoured but VFS is a
// single-tenant block layer today — multitenant prefixing lands when
// HIP-0107 specifies per-org namespacing.
func Mount(app *zip.App, deps cloud.Deps) error {
logger := deps.Logger.New("subsystem", "vfs")
app.Get("/v1/vfs/health", func(c *zip.Ctx) error {
return c.JSON(http.StatusOK, map[string]any{
"status": "ok",
"service": "vfs",
"version": Version,
})
})
app.Get("/v1/vfs/readyz", func(c *zip.Ctx) error {
if Instance() == nil {
return c.JSON(http.StatusServiceUnavailable, map[string]any{
"status": "not_ready",
"service": "vfs",
"reason": "no backend attached (call vfs.SetInstance)",
})
}
return c.JSON(http.StatusOK, map[string]any{
"status": "ready",
"service": "vfs",
"version": Version,
})
})
app.Get("/v1/vfs/stats", func(c *zip.Ctx) error {
v := Instance()
if v == nil {
return c.JSON(http.StatusServiceUnavailable, map[string]any{
"error": "vfs not attached",
})
}
return c.JSON(http.StatusOK, v.Stats())
})
app.Put("/v1/vfs/blocks", func(c *zip.Ctx) error {
v := Instance()
if v == nil {
return c.JSON(http.StatusServiceUnavailable, map[string]any{"error": "vfs not attached"})
}
body := c.Body()
if len(body) == 0 {
return c.JSON(http.StatusBadRequest, map[string]any{"error": "empty body"})
}
if len(body) > BlockSize {
return c.JSON(http.StatusBadRequest, map[string]any{
"error": fmt.Sprintf("payload %d > BlockSize %d", len(body), BlockSize),
"blockSize": BlockSize,
})
}
id, err := v.PutBlock(c.Context(), body)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]any{"error": err.Error()})
}
return c.JSON(http.StatusCreated, map[string]any{"id": string(id)})
})
app.Get("/v1/vfs/blocks/:id", func(c *zip.Ctx) error {
v := Instance()
if v == nil {
return c.JSON(http.StatusServiceUnavailable, map[string]any{"error": "vfs not attached"})
}
raw := c.Param("id")
if !isHexBlockID(raw) {
return c.JSON(http.StatusBadRequest, map[string]any{"error": "block id must be lowercase hex"})
}
pt, err := v.GetBlock(c.Context(), BlockID(raw))
if err != nil {
if errors.Is(err, backend.ErrNotFound) {
return c.JSON(http.StatusNotFound, map[string]any{"error": "not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]any{"error": err.Error()})
}
c.SetHeader("Content-Type", "application/octet-stream")
return c.Bytes(http.StatusOK, pt)
})
app.Delete("/v1/vfs/blocks/:id", func(c *zip.Ctx) error {
v := Instance()
if v == nil {
return c.JSON(http.StatusServiceUnavailable, map[string]any{"error": "vfs not attached"})
}
raw := c.Param("id")
if !isHexBlockID(raw) {
return c.JSON(http.StatusBadRequest, map[string]any{"error": "block id must be lowercase hex"})
}
if err := v.Delete(c.Context(), BlockID(raw)); err != nil {
if errors.Is(err, backend.ErrNotFound) {
return c.JSON(http.StatusNotFound, map[string]any{"error": "not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]any{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]any{"ok": true})
})
logger.Info("vfs mounted", "version", Version, "instance_attached", Instance() != nil)
return nil
}
// Shutdown drains the attached VFS instance (closing its backend).
// Idempotent. Safe to call when no instance was attached.
func Shutdown(_ context.Context) error {
mu.Lock()
defer mu.Unlock()
if mountedInstance == nil {
return nil
}
err := mountedInstance.Close()
mountedInstance = nil
return err
}
// isHexBlockID matches the lowercase hex string output by HashBlock.
// Used to reject path traversal / control bytes before hitting the
// backend.
func isHexBlockID(s string) bool {
if len(s) == 0 || len(s) > 128 {
return false
}
_, err := hex.DecodeString(s)
return err == nil
}
// ensure io.Copy stays imported (used by the data-plane streaming
// branch when multi-block File support lands in 0.4.0).
var _ = io.Copy
// init registers VFS with the cloud subsystem registry. Order 20 —
// after kms (10) and ahead of amqp (30) / mq (40).
func init() {
cloud.Register("vfs", 20, func(app any, deps cloud.Deps) error {
a, ok := app.(*zip.App)
if !ok {
return fmt.Errorf("vfs.Mount: app is %T, want *zip.App", app)
}
return Mount(a, deps)
})
}
-140
View File
@@ -1,140 +0,0 @@
//go:build cloud_mount
package vfs_test
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http/httptest"
"strings"
"testing"
"github.com/luxfi/age"
luxlog "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"
"github.com/hanzoai/zip"
)
// newMountTestVFS creates a file-backed VFS suitable for HTTP testing.
func newMountTestVFS(t *testing.T) *vfs.VFS {
t.Helper()
be, err := backend.Open(context.Background(), "file://"+t.TempDir())
if err != nil {
t.Fatalf("backend.Open: %v", err)
}
t.Cleanup(func() { _ = be.Close() })
id, err := age.GenerateX25519Identity()
if err != nil {
t.Fatalf("age.GenerateX25519Identity: %v", err)
}
c, err := vfs.NewCrypto([]age.Recipient{id.Recipient()}, []age.Identity{id})
if err != nil {
t.Fatalf("NewCrypto: %v", err)
}
v, err := vfs.New(vfs.Config{Backend: be, Crypto: c})
if err != nil {
t.Fatalf("vfs.New: %v", err)
}
return v
}
func TestMount_HealthReadyz(t *testing.T) {
app := zip.New(zip.Config{Logger: luxlog.New("test")})
deps := cloud.Deps{Logger: luxlog.New("test")}
// Detach any prior instance (other tests may have set one).
vfs.SetInstance(nil)
t.Cleanup(func() { vfs.SetInstance(nil) })
if err := vfs.Mount(app, deps); err != nil {
t.Fatalf("Mount: %v", err)
}
// Health always 200, even without an instance.
req := httptest.NewRequest("GET", "/v1/vfs/health", nil)
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatalf("health test: %v", err)
}
if resp.StatusCode != 200 {
t.Fatalf("health status = %d, want 200", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
var hb map[string]any
_ = json.Unmarshal(body, &hb)
if hb["service"] != "vfs" {
t.Fatalf("health body service = %v, want vfs", hb["service"])
}
// Readyz without instance → 503.
req = httptest.NewRequest("GET", "/v1/vfs/readyz", nil)
resp, err = app.Fiber().Test(req)
if err != nil {
t.Fatalf("readyz test: %v", err)
}
if resp.StatusCode != 503 {
t.Fatalf("readyz (no instance) status = %d, want 503", resp.StatusCode)
}
// Attach instance → 200.
vfs.SetInstance(newMountTestVFS(t))
req = httptest.NewRequest("GET", "/v1/vfs/readyz", nil)
resp, err = app.Fiber().Test(req)
if err != nil {
t.Fatalf("readyz attached test: %v", err)
}
if resp.StatusCode != 200 {
t.Fatalf("readyz (attached) status = %d, want 200", resp.StatusCode)
}
}
func TestMount_PutGetRoundtrip(t *testing.T) {
app := zip.New(zip.Config{Logger: luxlog.New("test")})
deps := cloud.Deps{Logger: luxlog.New("test")}
vfs.SetInstance(newMountTestVFS(t))
t.Cleanup(func() { vfs.SetInstance(nil) })
if err := vfs.Mount(app, deps); err != nil {
t.Fatalf("Mount: %v", err)
}
plain := []byte("zip mount roundtrip — small block payload")
// PUT.
put := httptest.NewRequest("PUT", "/v1/vfs/blocks", bytes.NewReader(plain))
putResp, err := app.Fiber().Test(put)
if err != nil {
t.Fatalf("PUT: %v", err)
}
if putResp.StatusCode != 201 {
t.Fatalf("PUT status = %d, want 201", putResp.StatusCode)
}
var putBody struct {
ID string `json:"id"`
}
_ = json.NewDecoder(putResp.Body).Decode(&putBody)
if putBody.ID == "" || len(putBody.ID) != 64 {
t.Fatalf("PUT body id = %q (want 64-hex)", putBody.ID)
}
// GET.
get := httptest.NewRequest("GET", "/v1/vfs/blocks/"+putBody.ID, nil)
getResp, err := app.Fiber().Test(get)
if err != nil {
t.Fatalf("GET: %v", err)
}
if getResp.StatusCode != 200 {
t.Fatalf("GET status = %d, want 200", getResp.StatusCode)
}
gotRaw, _ := io.ReadAll(getResp.Body)
if !strings.HasPrefix(string(gotRaw), string(plain)) {
t.Fatalf("GET body prefix mismatch:\n got=%q\nwant prefix=%q", gotRaw[:64], plain)
}
}