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