Initial commit
Signed-off-by: Alex Vanin <a.vanin@yadro.com>
This commit is contained in:
commit
bedb008081
7 changed files with 344 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
bin/*
|
3
Makefile
Normal file
3
Makefile
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.PHONY: build
|
||||||
|
build:
|
||||||
|
CGO_ENABLED=0 go build -o ./bin/chi-s3-vhs ./
|
57
README.md
Normal file
57
README.md
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# Dynamic S3 address-style routing prototype
|
||||||
|
|
||||||
|
Prototype of dynamic routing for path-style and virtual-hosted-style requests
|
||||||
|
implemented with go-chi framework.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```
|
||||||
|
$ make
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
### Default setting.
|
||||||
|
|
||||||
|
VHS is disabled. VHS can be overridden by HTTP Header.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./bin/chi-s3-vhs
|
||||||
|
|
||||||
|
$ curl bucketA.localhost:44412/bucketB/obj
|
||||||
|
bucket:[bucketB] object:[obj]
|
||||||
|
|
||||||
|
$ curl -H "X-FrostFS-VHS: enabled" bucketA.localhost:44412/bucketB/obj
|
||||||
|
bucket:[bucketA] object:[bucketB/obj]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global VHS
|
||||||
|
|
||||||
|
VHS can be overridden by HTTP Header
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./bin/chi-s3-vhs --global
|
||||||
|
|
||||||
|
$ curl bucketA.localhost:44412/bucketB/obj
|
||||||
|
bucket:[bucketA] object:[bucketB/obj]
|
||||||
|
|
||||||
|
$ curl -H "X-FrostFS-VHS: disabled" bucketA.localhost:44412/bucketB/obj
|
||||||
|
bucket:[bucketB] object:[obj]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Namespace override
|
||||||
|
|
||||||
|
Namespace setting may override default value, but not header value
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./chi-s3-vhs --global --ns foo=disabled
|
||||||
|
|
||||||
|
$ curl bucketA.localhost:44412/bucketB/obj
|
||||||
|
bucket:[bucketA] object:[bucketB/obj]
|
||||||
|
|
||||||
|
$ curl -H "X-FrostFS-Namespace: foo" bucketA.localhost:44412/bucketB/obj
|
||||||
|
bucket:[bucketB] object:[obj]
|
||||||
|
|
||||||
|
$ curl -H "X-FrostFS-Namespace: foo" -H "X-FrostFS-VHS: enabled" bucketA.localhost:44412/bucketB/obj
|
||||||
|
bucket:[bucketA] object:[bucketB/obj]
|
||||||
|
```
|
5
go.mod
Normal file
5
go.mod
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module chi-s3-vhs
|
||||||
|
|
||||||
|
go 1.22.5
|
||||||
|
|
||||||
|
require github.com/go-chi/chi/v5 v5.1.0
|
2
go.sum
Normal file
2
go.sum
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||||
|
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
134
main.go
Normal file
134
main.go
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AddressTypeRouter struct {
|
||||||
|
vhsGlobalEnabled bool // setting from gateway application
|
||||||
|
vhsNamespaceEnabled map[string]bool // setting from gateway application
|
||||||
|
|
||||||
|
pathStyleRouter chi.Router // look for 'defaultRouter' in S3 Gateway
|
||||||
|
vhsRouter chi.Router // look for 'bucketRouter' in S3 Gateway
|
||||||
|
}
|
||||||
|
|
||||||
|
type nsSettings struct {
|
||||||
|
settings map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *nsSettings) Set(value string) error {
|
||||||
|
v := strings.Split(value, "=")
|
||||||
|
if len(v) != 2 {
|
||||||
|
return fmt.Errorf("invalid argument: %s", value)
|
||||||
|
}
|
||||||
|
switch v[1] {
|
||||||
|
case "enabled":
|
||||||
|
s.settings[v[0]] = true
|
||||||
|
case "disabled":
|
||||||
|
s.settings[v[0]] = false
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (s *nsSettings) String() string {
|
||||||
|
return fmt.Sprintf("%v", s.settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
addr := flag.String("a", ":44412", "http listen address")
|
||||||
|
globalVHSEnabled := flag.Bool("global", false, "enable global VHS")
|
||||||
|
namespaceVHSEnabled := nsSettings{
|
||||||
|
settings: make(map[string]bool),
|
||||||
|
}
|
||||||
|
flag.Var(&namespaceVHSEnabled, "ns", "namespace settings")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
objRouter := ObjectRouter() // router for object operations
|
||||||
|
bktRouter := BucketRouter(objRouter) // router for bucket operations
|
||||||
|
pathStyleRouter := chi.NewRouter() // router for path-style requests
|
||||||
|
pathStyleRouter.Mount("/{bucket}", bktRouter)
|
||||||
|
atr := AddressTypeRouter{ // router for path-style and virtual-host-style switch
|
||||||
|
vhsGlobalEnabled: *globalVHSEnabled,
|
||||||
|
vhsNamespaceEnabled: namespaceVHSEnabled.settings,
|
||||||
|
pathStyleRouter: pathStyleRouter,
|
||||||
|
vhsRouter: bktRouter,
|
||||||
|
}
|
||||||
|
|
||||||
|
api := chi.NewRouter() // global router
|
||||||
|
api.Use(Request())
|
||||||
|
api.Mount("/", atr)
|
||||||
|
|
||||||
|
srv := &http.Server{}
|
||||||
|
srv.Handler = api
|
||||||
|
l, err := net.Listen("tcp", *addr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(fmt.Sprintf("failed to listen on %s: %v", *addr, err))
|
||||||
|
}
|
||||||
|
defer l.Close()
|
||||||
|
|
||||||
|
go srv.Serve(l)
|
||||||
|
log.Printf("listening on %s", *addr)
|
||||||
|
log.Printf("global VHS enabled: %v", *globalVHSEnabled)
|
||||||
|
log.Printf("namespace override: %s", &namespaceVHSEnabled)
|
||||||
|
<-ctx.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (atr AddressTypeRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
namespace := r.Header.Get("X-FrostFS-Namespace")
|
||||||
|
router := atr.pathStyleRouter
|
||||||
|
|
||||||
|
vhsEnabled := atr.isVHSAddress(r, namespace)
|
||||||
|
if vhsEnabled {
|
||||||
|
log.Println("VHS is enabled")
|
||||||
|
router = atr.vhsRouter
|
||||||
|
bucketName := atr.vhsBucketName(r)
|
||||||
|
if rctx := chi.RouteContext(r.Context()); rctx != nil {
|
||||||
|
rctx.URLParams.Add("bucket", bucketName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (atr AddressTypeRouter) isVHSAddress(r *http.Request, namespace string) bool {
|
||||||
|
result := atr.vhsGlobalEnabled // default global value
|
||||||
|
|
||||||
|
// check namespace override
|
||||||
|
if v, ok := atr.vhsNamespaceEnabled[namespace]; ok {
|
||||||
|
result = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// check header override
|
||||||
|
switch r.Header.Get("X-FrostFS-VHS") {
|
||||||
|
case "enabled":
|
||||||
|
result = true
|
||||||
|
case "disabled":
|
||||||
|
result = false
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (atr AddressTypeRouter) vhsBucketName(r *http.Request) string {
|
||||||
|
hostname := r.Host
|
||||||
|
if r.URL.IsAbs() {
|
||||||
|
hostname = r.URL.Hostname()
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(hostname, ".")
|
||||||
|
|
||||||
|
return parts[0]
|
||||||
|
}
|
142
s3.go
Normal file
142
s3.go
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Func func(h http.Handler) http.Handler
|
||||||
|
|
||||||
|
ReqInfo struct {
|
||||||
|
BucketName string
|
||||||
|
ObjectName string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const ctxRequestInfo = "FrostFS-S3-GW"
|
||||||
|
|
||||||
|
func GetReqInfo(ctx context.Context) *ReqInfo {
|
||||||
|
if ctx == nil {
|
||||||
|
return &ReqInfo{}
|
||||||
|
} else if r, ok := ctx.Value(ctxRequestInfo).(*ReqInfo); ok {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return &ReqInfo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetReqInfo(ctx context.Context, req *ReqInfo) context.Context {
|
||||||
|
if ctx == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, ctxRequestInfo, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddBucketName() Func {
|
||||||
|
return func(h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
reqInfo := GetReqInfo(ctx)
|
||||||
|
reqInfo.BucketName = chi.URLParam(r, "bucket")
|
||||||
|
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddObjectName adds objects name to ReqInfo from context.
|
||||||
|
func AddObjectName() Func {
|
||||||
|
return func(h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
reqInfo := GetReqInfo(ctx)
|
||||||
|
|
||||||
|
rctx := chi.RouteContext(ctx)
|
||||||
|
// trim leading slash (always present)
|
||||||
|
reqInfo.ObjectName = rctx.RoutePath[1:]
|
||||||
|
|
||||||
|
if r.URL.RawPath != "" {
|
||||||
|
// we have to do this because of
|
||||||
|
// https://github.com/go-chi/chi/issues/641
|
||||||
|
// https://github.com/go-chi/chi/issues/642
|
||||||
|
if obj, err := url.PathUnescape(reqInfo.ObjectName); err != nil {
|
||||||
|
// log error
|
||||||
|
} else {
|
||||||
|
reqInfo.ObjectName = obj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Request() Func {
|
||||||
|
return func(h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
reqInfo := &ReqInfo{}
|
||||||
|
r = r.WithContext(SetReqInfo(r.Context(), reqInfo))
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BucketRouter(objRouter chi.Router) chi.Router {
|
||||||
|
bktRouter := chi.NewRouter()
|
||||||
|
bktRouter.Use(AddBucketName())
|
||||||
|
|
||||||
|
bktRouter.Mount("/", objRouter)
|
||||||
|
|
||||||
|
bktRouter.Group(func(r chi.Router) {
|
||||||
|
r.MethodFunc(http.MethodGet, "/", BucketHandler())
|
||||||
|
})
|
||||||
|
|
||||||
|
return bktRouter
|
||||||
|
}
|
||||||
|
|
||||||
|
func ObjectRouter() chi.Router {
|
||||||
|
objRouter := chi.NewRouter()
|
||||||
|
objRouter.Use(AddObjectName())
|
||||||
|
|
||||||
|
objRouter.Group(func(r chi.Router) {
|
||||||
|
r.MethodFunc(http.MethodGet, "/*", ObjectHandler())
|
||||||
|
})
|
||||||
|
|
||||||
|
return objRouter
|
||||||
|
}
|
||||||
|
|
||||||
|
func ObjectHandler() http.HandlerFunc {
|
||||||
|
return func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("object handler is triggered")
|
||||||
|
ri := GetReqInfo(r.Context())
|
||||||
|
if ri == nil {
|
||||||
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
rw.Write([]byte(fmt.Sprintf("bucket:[%s] object:[%s]\n", ri.BucketName, ri.ObjectName)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BucketHandler() http.HandlerFunc {
|
||||||
|
return func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Printf("bucket handler is triggered")
|
||||||
|
ri := GetReqInfo(r.Context())
|
||||||
|
if ri == nil {
|
||||||
|
log.Println("can't find request info")
|
||||||
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("bucket:[%s]", ri.BucketName)
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
rw.Write([]byte(fmt.Sprintf("bucket:[%s]\n", ri.BucketName)))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue