mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-20 08:14:03 +01:00
v0.5: (BREAKING) replacing path with path_patterns, improved docker monitoring mechanism, bug fixes
This commit is contained in:
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
U "github.com/yusing/go-proxy/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/config"
|
||||
PT "github.com/yusing/go-proxy/proxy/fields"
|
||||
R "github.com/yusing/go-proxy/route"
|
||||
)
|
||||
|
||||
@@ -24,17 +23,7 @@ func CheckHealth(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
U.HandleErr(w, r, U.ErrNotFound("target", target), http.StatusNotFound)
|
||||
return
|
||||
case *R.HTTPRoute:
|
||||
path, err := PT.NewPath(r.FormValue("path"))
|
||||
if err.IsNotNil() {
|
||||
U.HandleErr(w, r, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sr, hasSr := route.GetSubroute(path)
|
||||
if !hasSr {
|
||||
U.HandleErr(w, r, U.ErrNotFound("path", string(path)), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
ok = U.IsSiteHealthy(sr.TargetURL.String())
|
||||
ok = U.IsSiteHealthy(route.TargetURL.String())
|
||||
case *R.StreamRoute:
|
||||
ok = U.IsStreamHealthy(
|
||||
string(route.Scheme.ProxyScheme),
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func yamlParser[T any](value string) (any, E.NestedError) {
|
||||
var data T
|
||||
func yamlListParser(value string) (any, E.NestedError) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return []string{}, E.Nil()
|
||||
}
|
||||
var data []string
|
||||
err := E.From(yaml.Unmarshal([]byte(value), &data))
|
||||
return data, err
|
||||
}
|
||||
|
||||
func setHeadersParser(value string) (any, E.NestedError) {
|
||||
func yamlStringMappingParser(value string) (any, E.NestedError) {
|
||||
value = strings.TrimSpace(value)
|
||||
lines := strings.Split(value, "\n")
|
||||
h := make(http.Header)
|
||||
h := make(map[string]string)
|
||||
for _, line := range lines {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, E.Invalid("set header statement", line)
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
vals := strings.Split(parts[1], ",")
|
||||
for i := range vals {
|
||||
h.Add(key, strings.TrimSpace(vals[i]))
|
||||
val := strings.TrimSpace(parts[1])
|
||||
if existing, ok := h[key]; ok {
|
||||
h[key] = existing + ", " + val
|
||||
} else {
|
||||
h[key] = val
|
||||
}
|
||||
}
|
||||
return h, E.Nil()
|
||||
@@ -56,8 +61,9 @@ const NSProxy = "proxy"
|
||||
var _ = func() int {
|
||||
RegisterNamespace(NSProxy, ValueParserMap{
|
||||
"aliases": commaSepParser,
|
||||
"set_headers": setHeadersParser,
|
||||
"hide_headers": yamlParser[[]string],
|
||||
"path_patterns": yamlListParser,
|
||||
"set_headers": yamlStringMappingParser,
|
||||
"hide_headers": yamlListParser,
|
||||
"no_tls_verify": boolParser,
|
||||
})
|
||||
return 0
|
||||
|
||||
@@ -2,7 +2,6 @@ package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -82,26 +81,22 @@ X-Custom-Header1: foo, bar
|
||||
X-Custom-Header1: baz
|
||||
X-Custom-Header2: boo`
|
||||
v = strings.TrimPrefix(v, "\n")
|
||||
h := make(http.Header, 0)
|
||||
h.Set("X-Custom-Header1", "foo")
|
||||
h.Add("X-Custom-Header1", "bar")
|
||||
h.Add("X-Custom-Header1", "baz")
|
||||
h.Set("X-Custom-Header2", "boo")
|
||||
h := map[string]string{
|
||||
"X-Custom-Header1": "foo, bar, baz",
|
||||
"X-Custom-Header2": "boo",
|
||||
}
|
||||
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "set_headers"), v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
hGot, ok := pl.Value.(http.Header)
|
||||
hGot, ok := pl.Value.(map[string]string)
|
||||
if !ok {
|
||||
t.Error("value is not http.Header")
|
||||
t.Errorf("value is not a map[string]string, but %T", pl.Value)
|
||||
return
|
||||
}
|
||||
for k, vWant := range h {
|
||||
vGot := hGot[k]
|
||||
if !reflect.DeepEqual(vGot, vWant) {
|
||||
t.Errorf("expected %s=%q, got %q", k, vWant, vGot)
|
||||
}
|
||||
if !reflect.DeepEqual(h, hGot) {
|
||||
t.Errorf("expected %v, got %v", h, hGot)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
R "github.com/yusing/go-proxy/route"
|
||||
"github.com/yusing/go-proxy/server"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
W "github.com/yusing/go-proxy/watcher"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -70,7 +69,6 @@ func main() {
|
||||
|
||||
onShutdown.Add(func() {
|
||||
docker.CloseAllClients()
|
||||
W.StopAllFileWatchers()
|
||||
cfg.Dispose()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
@@ -9,14 +8,14 @@ import (
|
||||
|
||||
type (
|
||||
ProxyEntry struct {
|
||||
Alias string `yaml:"-" json:"-"`
|
||||
Scheme string `yaml:"scheme" json:"scheme"`
|
||||
Host string `yaml:"host" json:"host"`
|
||||
Port string `yaml:"port" json:"port"`
|
||||
NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // http proxy only
|
||||
Path string `yaml:"path" json:"path"` // http proxy only
|
||||
SetHeaders http.Header `yaml:"set_headers" json:"set_headers"` // http proxy only
|
||||
HideHeaders []string `yaml:"hide_headers" json:"hide_headers"` // http proxy only
|
||||
Alias string `yaml:"-" json:"-"`
|
||||
Scheme string `yaml:"scheme" json:"scheme"`
|
||||
Host string `yaml:"host" json:"host"`
|
||||
Port string `yaml:"port" json:"port"`
|
||||
NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // https proxy only
|
||||
PathPatterns []string `yaml:"path_patterns" json:"path_patterns"` // http(s) proxy only
|
||||
SetHeaders map[string]string `yaml:"set_headers" json:"set_headers"` // http(s) proxy only
|
||||
HideHeaders []string `yaml:"hide_headers" json:"hide_headers"` // http(s) proxy only
|
||||
}
|
||||
|
||||
ProxyEntries = *F.Map[string, *ProxyEntry]
|
||||
@@ -37,8 +36,8 @@ func (e *ProxyEntry) SetDefaults() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if e.Path == "" {
|
||||
e.Path = "/"
|
||||
if e.Host == "" {
|
||||
e.Host = "localhost"
|
||||
}
|
||||
switch e.Scheme {
|
||||
case "http":
|
||||
|
||||
@@ -12,15 +12,15 @@ import (
|
||||
|
||||
type (
|
||||
Entry struct { // real model after validation
|
||||
Alias T.Alias
|
||||
Scheme T.Scheme
|
||||
Host T.Host
|
||||
Port T.Port
|
||||
URL *url.URL
|
||||
NoTLSVerify bool
|
||||
Path T.Path
|
||||
SetHeaders http.Header
|
||||
HideHeaders []string
|
||||
Alias T.Alias
|
||||
Scheme T.Scheme
|
||||
Host T.Host
|
||||
Port T.Port
|
||||
URL *url.URL
|
||||
NoTLSVerify bool
|
||||
PathPatterns T.PathPatterns
|
||||
SetHeaders http.Header
|
||||
HideHeaders []string
|
||||
}
|
||||
StreamEntry struct {
|
||||
Alias T.Alias `json:"alias"`
|
||||
@@ -51,7 +51,11 @@ func validateEntry(m *M.ProxyEntry, s T.Scheme) (*Entry, E.NestedError) {
|
||||
if err.IsNotNil() {
|
||||
return nil, err
|
||||
}
|
||||
path, err := T.NewPath(m.Path)
|
||||
pathPatterns, err := T.NewPathPatterns(m.PathPatterns)
|
||||
if err.IsNotNil() {
|
||||
return nil, err
|
||||
}
|
||||
setHeaders, err := T.NewHTTPHeaders(m.SetHeaders)
|
||||
if err.IsNotNil() {
|
||||
return nil, err
|
||||
}
|
||||
@@ -60,15 +64,15 @@ func validateEntry(m *M.ProxyEntry, s T.Scheme) (*Entry, E.NestedError) {
|
||||
return nil, err
|
||||
}
|
||||
return &Entry{
|
||||
Alias: T.NewAlias(m.Alias),
|
||||
Scheme: s,
|
||||
Host: host,
|
||||
Port: port,
|
||||
URL: url,
|
||||
NoTLSVerify: m.NoTLSVerify,
|
||||
Path: path,
|
||||
SetHeaders: m.SetHeaders,
|
||||
HideHeaders: m.HideHeaders,
|
||||
Alias: T.NewAlias(m.Alias),
|
||||
Scheme: s,
|
||||
Host: host,
|
||||
Port: port,
|
||||
URL: url,
|
||||
NoTLSVerify: m.NoTLSVerify,
|
||||
PathPatterns: pathPatterns,
|
||||
SetHeaders: setHeaders,
|
||||
HideHeaders: m.HideHeaders,
|
||||
}, E.Nil()
|
||||
}
|
||||
|
||||
|
||||
19
src/proxy/fields/headers.go
Normal file
19
src/proxy/fields/headers.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
func NewHTTPHeaders(headers map[string]string) (http.Header, E.NestedError) {
|
||||
h := make(http.Header)
|
||||
for k, v := range headers {
|
||||
vSplit := strings.Split(v, ",")
|
||||
for _, header := range vSplit {
|
||||
h.Add(k, strings.TrimSpace(header))
|
||||
}
|
||||
}
|
||||
return h, E.Nil()
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
type Path string
|
||||
|
||||
func NewPath(s string) (Path, E.NestedError) {
|
||||
if s == "" || s[0] == '/' {
|
||||
return Path(s), E.Nil()
|
||||
}
|
||||
return "", E.Invalid("path", s).With("must be empty or start with '/'")
|
||||
}
|
||||
37
src/proxy/fields/path_pattern.go
Normal file
37
src/proxy/fields/path_pattern.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
type PathPattern string
|
||||
type PathPatterns = []PathPattern
|
||||
|
||||
func NewPathPattern(s string) (PathPattern, E.NestedError) {
|
||||
if len(s) == 0 {
|
||||
return "", E.Invalid("path", "must not be empty")
|
||||
}
|
||||
if !pathPattern.MatchString(string(s)) {
|
||||
return "", E.Invalid("path pattern", s)
|
||||
}
|
||||
return PathPattern(s), E.Nil()
|
||||
}
|
||||
|
||||
func NewPathPatterns(s []string) (PathPatterns, E.NestedError) {
|
||||
if len(s) == 0 {
|
||||
return []PathPattern{"/"}, E.Nil()
|
||||
}
|
||||
pp := make(PathPatterns, len(s))
|
||||
for i, v := range s {
|
||||
if pattern, err := NewPathPattern(v); err.IsNotNil() {
|
||||
return nil, err
|
||||
} else {
|
||||
pp[i] = pattern
|
||||
}
|
||||
}
|
||||
return pp, E.Nil()
|
||||
}
|
||||
|
||||
var pathPattern = regexp.MustCompile("^((GET|POST|DELETE|PUT|PATCH|HEAD|OPTIONS|CONNECT)\\s)?(/\\w*)+/?$")
|
||||
@@ -107,6 +107,7 @@ func (p *Provider) StartAllRoutes() E.NestedError {
|
||||
func (p *Provider) StopAllRoutes() E.NestedError {
|
||||
if p.watcherCancel != nil {
|
||||
p.watcherCancel()
|
||||
p.watcherCancel = nil
|
||||
}
|
||||
errors := E.NewBuilder("errors stopping routes for provider %q", p.name)
|
||||
nStopped := 0
|
||||
@@ -126,17 +127,9 @@ func (p *Provider) StopAllRoutes() E.NestedError {
|
||||
func (p *Provider) ReloadRoutes() {
|
||||
defer p.l.Info("routes reloaded")
|
||||
|
||||
select {
|
||||
case p.reloadReqCh <- struct{}{}:
|
||||
defer func() {
|
||||
<-p.reloadReqCh
|
||||
}()
|
||||
p.StopAllRoutes()
|
||||
p.loadRoutes()
|
||||
p.StartAllRoutes()
|
||||
default:
|
||||
return
|
||||
}
|
||||
p.StopAllRoutes()
|
||||
p.loadRoutes()
|
||||
p.StartAllRoutes()
|
||||
}
|
||||
|
||||
func (p *Provider) GetCurrentRoutes() *R.Routes {
|
||||
@@ -149,13 +142,14 @@ func (p *Provider) watchEvents() {
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-p.reloadReqCh:
|
||||
case <-p.reloadReqCh: // block until last reload is done
|
||||
p.ReloadRoutes()
|
||||
continue // ignore events once after reload
|
||||
case event, ok := <-events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
l.Infof("watcher event: %s", event)
|
||||
l.Info(event)
|
||||
p.reloadReqCh <- struct{}{}
|
||||
case err, ok := <-errs:
|
||||
if !ok {
|
||||
|
||||
@@ -19,23 +19,18 @@ import (
|
||||
|
||||
type (
|
||||
HTTPRoute struct {
|
||||
Alias PT.Alias `json:"alias"`
|
||||
Subroutes HTTPSubroutes `json:"subroutes"`
|
||||
Alias PT.Alias `json:"alias"`
|
||||
|
||||
mux *http.ServeMux
|
||||
TargetURL URL
|
||||
PathPatterns PT.PathPatterns
|
||||
|
||||
mux *http.ServeMux
|
||||
handler *P.ReverseProxy
|
||||
}
|
||||
|
||||
HTTPSubroute struct {
|
||||
TargetURL *URL `json:"targetURL"`
|
||||
Path PathKey `json:"path"`
|
||||
|
||||
proxy *P.ReverseProxy
|
||||
}
|
||||
|
||||
URL url.URL
|
||||
PathKey = PT.Path
|
||||
SubdomainKey = PT.Alias
|
||||
HTTPSubroutes = map[PathKey]HTTPSubroute
|
||||
URL url.URL
|
||||
PathKey = PT.PathPattern
|
||||
SubdomainKey = PT.Alias
|
||||
)
|
||||
|
||||
var httpRoutes = F.NewMap[SubdomainKey, *HTTPRoute]()
|
||||
@@ -57,49 +52,28 @@ func NewHTTPRoute(entry *P.Entry) (*HTTPRoute, E.NestedError) {
|
||||
r, ok := httpRoutes.UnsafeGet(entry.Alias)
|
||||
if !ok {
|
||||
r = &HTTPRoute{
|
||||
Alias: entry.Alias,
|
||||
Subroutes: make(HTTPSubroutes),
|
||||
mux: http.NewServeMux(),
|
||||
Alias: entry.Alias,
|
||||
TargetURL: URL(*entry.URL),
|
||||
PathPatterns: entry.PathPatterns,
|
||||
handler: rp,
|
||||
}
|
||||
httpRoutes.UnsafeSet(entry.Alias, r)
|
||||
}
|
||||
|
||||
path := entry.Path
|
||||
if _, exists := r.Subroutes[path]; exists {
|
||||
return nil, E.Duplicated("path", path)
|
||||
}
|
||||
r.mux.HandleFunc(string(path), rp.ServeHTTP)
|
||||
if err := recover(); err != nil {
|
||||
switch t := err.(type) {
|
||||
case error:
|
||||
// NOTE: likely path pattern error
|
||||
return nil, E.From(t)
|
||||
default:
|
||||
return nil, E.From(fmt.Errorf("%v", t))
|
||||
}
|
||||
}
|
||||
|
||||
sr := HTTPSubroute{
|
||||
TargetURL: (*URL)(entry.URL),
|
||||
proxy: rp,
|
||||
Path: path,
|
||||
}
|
||||
|
||||
rewrite := rp.Rewrite
|
||||
|
||||
if logrus.GetLevel() == logrus.DebugLevel {
|
||||
l := logrus.WithField("alias", entry.Alias)
|
||||
|
||||
sr.proxy.Rewrite = func(pr *P.ProxyRequest) {
|
||||
rp.Rewrite = func(pr *P.ProxyRequest) {
|
||||
l.Debug("request URL: ", pr.In.Host, pr.In.URL.Path)
|
||||
l.Debug("request headers: ", pr.In.Header)
|
||||
rewrite(pr)
|
||||
}
|
||||
} else {
|
||||
sr.proxy.Rewrite = rewrite
|
||||
rp.Rewrite = rewrite
|
||||
}
|
||||
|
||||
r.Subroutes[path] = sr
|
||||
return r, E.Nil()
|
||||
}
|
||||
|
||||
@@ -108,20 +82,20 @@ func (r *HTTPRoute) String() string {
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) Start() E.NestedError {
|
||||
r.mux = http.NewServeMux()
|
||||
for _, p := range r.PathPatterns {
|
||||
r.mux.HandleFunc(string(p), r.handler.ServeHTTP)
|
||||
}
|
||||
httpRoutes.Set(r.Alias, r)
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) Stop() E.NestedError {
|
||||
r.mux = nil
|
||||
httpRoutes.Delete(r.Alias)
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) GetSubroute(path PathKey) (HTTPSubroute, bool) {
|
||||
sr, ok := r.Subroutes[path]
|
||||
return sr, ok
|
||||
}
|
||||
|
||||
func (u *URL) String() string {
|
||||
return (*url.URL)(u).String()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/events"
|
||||
@@ -47,14 +48,28 @@ func (w *DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Nest
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
errCh <- E.From(<-cErrCh)
|
||||
if err := <-cErrCh; err != nil {
|
||||
errCh <- E.From(err)
|
||||
}
|
||||
return
|
||||
case msg := <-cEventCh:
|
||||
var Action Action
|
||||
switch msg.Action {
|
||||
case events.ActionStart:
|
||||
Action = ActionCreated
|
||||
case events.ActionDie:
|
||||
Action = ActionDeleted
|
||||
default: // NOTE: should not happen
|
||||
Action = ActionModified
|
||||
}
|
||||
eventCh <- Event{
|
||||
ActorName: msg.Actor.Attributes["name"],
|
||||
Action: ActionModified,
|
||||
ActorName: fmt.Sprintf("container %q", msg.Actor.Attributes["name"]),
|
||||
Action: Action,
|
||||
}
|
||||
case err := <-cErrCh:
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
errCh <- E.From(err)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -74,7 +89,7 @@ func (w *DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Nest
|
||||
}
|
||||
|
||||
var dwOptions = events.ListOptions{Filters: filters.NewArgs(
|
||||
filters.Arg("type", "container"),
|
||||
filters.Arg("event", "start"),
|
||||
filters.Arg("event", "die"), // 'stop' already triggering 'die'
|
||||
filters.Arg("type", string(events.ContainerEventType)),
|
||||
filters.Arg("event", string(events.ActionStart)),
|
||||
filters.Arg("event", string(events.ActionDie)), // 'stop' already triggering 'die'
|
||||
)}
|
||||
|
||||
@@ -12,8 +12,9 @@ type (
|
||||
|
||||
const (
|
||||
ActionModified Action = "MODIFIED"
|
||||
ActionDeleted Action = "DELETED"
|
||||
ActionCreated Action = "CREATED"
|
||||
ActionStarted Action = "STARTED"
|
||||
ActionDeleted Action = "DELETED"
|
||||
)
|
||||
|
||||
func (e Event) String() string {
|
||||
@@ -23,11 +24,3 @@ func (e Event) String() string {
|
||||
func (a Action) IsDelete() bool {
|
||||
return a == ActionDeleted
|
||||
}
|
||||
|
||||
func (a Action) IsModify() bool {
|
||||
return a == ActionModified
|
||||
}
|
||||
|
||||
func (a Action) IsCreate() bool {
|
||||
return a == ActionCreated
|
||||
}
|
||||
|
||||
@@ -18,10 +18,6 @@ func NewFileWatcher(filename string) Watcher {
|
||||
return &fileWatcher{filename: filename}
|
||||
}
|
||||
|
||||
func StopAllFileWatchers() {
|
||||
fwHelper.close()
|
||||
}
|
||||
|
||||
func (f *fileWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.NestedError) {
|
||||
return fwHelper.Add(ctx, f)
|
||||
}
|
||||
|
||||
@@ -80,13 +80,6 @@ func (h *fileWatcherHelper) Remove(w *fileWatcher) {
|
||||
delete(h.m, w.filename)
|
||||
}
|
||||
|
||||
// deinit closes the fs watcher
|
||||
// and waits for the start() loop to finish
|
||||
func (h *fileWatcherHelper) close() {
|
||||
_ = h.w.Close()
|
||||
h.wg.Wait() // wait for `start()` loop to finish
|
||||
}
|
||||
|
||||
func (h *fileWatcherHelper) start() {
|
||||
defer h.wg.Done()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user