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:
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user