diff --git a/.vscode/settings.example.json b/.vscode/settings.example.json
index a8acfd8c..bfbfaaa4 100644
--- a/.vscode/settings.example.json
+++ b/.vscode/settings.example.json
@@ -1,12 +1,13 @@
{
- "yaml.schemas": {
- "https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
- "config.example.yml",
- "config.yml"
- ],
- "https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
- "providers.example.yml",
- "*.providers.yml"
- ]
- }
-}
\ No newline at end of file
+ "yaml.schemas": {
+ "https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
+ "config.example.yml",
+ "config.yml"
+ ],
+ "https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
+ "providers.example.yml",
+ "*.providers.yml",
+ "providers.yml"
+ ]
+ }
+}
diff --git a/Dockerfile b/Dockerfile
index ba05a869..7aa86b4d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,9 +13,9 @@ FROM alpine:latest
LABEL maintainer="yusing@6uo.me"
RUN apk add --no-cache tzdata
-COPY schema/ /app/schema
# copy binary
COPY --from=builder /src/go-proxy /app/
+COPY schema/ /app/schema
RUN chmod +x /app/go-proxy
ENV DOCKER_HOST unix:///var/run/docker.sock
diff --git a/docs/docker.md b/docs/docker.md
index 1db8a7da..bfea1367 100644
--- a/docs/docker.md
+++ b/docs/docker.md
@@ -77,7 +77,7 @@
6. Run `docker compose up -d` to start the container
-7. Navigate to Web panel `http://gp.yourdomain.com` and edit proxy config
+7. Navigate to Web panel `http://gp.yourdomain.com` or use **Visual Studio Code (provides schema check)** to edit proxy config
[🔼Back to top](#table-of-content)
@@ -93,17 +93,16 @@
### Fields
-| Field | Description | Default | Allowed Values / Syntax |
-| --------------------- | ---------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `scheme` | proxy protocol |
- `http` for numeric port
- `tcp` for `x:y` port
| `http`, `https`, `tcp`, `udp` |
-| `host` | proxy host | `container_name` | IP address, hostname |
-| `port` | proxy port **(http/s)** | first port in `ports:` | number in range of `0 - 65535` |
-| `port` **(required)** | proxy port **(tcp/udp)** | N/A | `x:y`
- x: port for `go-proxy` to listen on
- y: port or [_service name_](../src/common/constants.go#L55) of target container
|
-| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean |
-| `path` | proxy path | empty | **(http/s only)** string |
-| `path_mode` | path handling **(http/s only)** | empty | empty, `forward` |
-| `set_headers` | header to set **(http/s only)** | empty | yaml style key-value mapping[1](#1-key-value-mapping-example) |
-| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[2](#2-list-example) |
+| Field | Description | Default | Allowed Values / Syntax |
+| --------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `scheme` | proxy protocol | - `http` for numeric port
- `tcp` for `x:y` port
| `http`, `https`, `tcp`, `udp` |
+| `host` | proxy host | - Docker: `container_name`
- File: `localhost`
| IP address, hostname |
+| `port` | proxy port **(http/s)** | first port in `ports:` | number in range of `1 - 65535` |
+| `port` **(required)** | proxy port **(tcp/udp)** | N/A | `x:y`
- x: port for `go-proxy` to listen on
- y: port or [_service name_](../src/common/constants.go#L55) of target container
|
+| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean |
+| `path_patterns` | proxy path patterns **(http/s only)**
only requests that matched a pattern will be proxied | empty **(proxy all requests)** | yaml style list[1](#list-example) of path patterns ([syntax](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)) |
+| `set_headers` | header to set **(http/s only)** | empty | yaml style key-value mapping[2](#key-value-mapping-example) of header-value pairs |
+| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[1](#list-example) of headers |
#### Key-value mapping example
@@ -142,6 +141,9 @@ services:
nginx:
...
labels:
+ proxy.nginx.path_patterns: | # remember to add the '|'
+ - GET /
+ - POST /auth
proxy.nginx.hide_headers: | # remember to add the '|'
- X-Custom-Header1
- X-Custom-Header2
@@ -152,6 +154,9 @@ File Provider
```yaml
service_a:
host: service_a.internal
+ path_patterns:
+ - GET /
+ - POST /auth
hide_headers:
- X-Custom-Header1
- X-Custom-Header2
diff --git a/schema/config.schema.json b/schema/config.schema.json
index 976c9bc8..e9a865b7 100644
--- a/schema/config.schema.json
+++ b/schema/config.schema.json
@@ -23,16 +23,19 @@
},
"cert_path": {
"title": "path of cert file to load/store",
- "description": "default: certs/cert.crt",
+ "default": "certs/cert.crt",
+ "markdownDescription": "default: `certs/cert.crt`",
"type": "string"
},
"key_path": {
"title": "path of key file to load/store",
- "description": "default: certs/priv.key",
+ "default": "certs/priv.key",
+ "markdownDescription": "default: `certs/priv.key`",
"type": "string"
},
"provider": {
"title": "DNS Challenge Provider",
+ "default": "local",
"type": "string",
"enum": ["local", "cloudflare", "clouddns", "duckdns"]
},
@@ -44,10 +47,11 @@
"allOf": [
{
"if": {
- "properties": {
- "provider": {
- "not": true,
- "const": "local"
+ "not": {
+ "properties": {
+ "provider": {
+ "const": "local"
+ }
}
}
},
@@ -151,7 +155,7 @@
},
"docker": {
"title": "Docker provider configuration",
- "description": "docker clients (name: address)",
+ "description": "docker clients (name-address pairs)",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9-_]+$": {
@@ -194,7 +198,7 @@
"minimum": 0
},
"redirect_to_https": {
- "title": "Redirect to HTTPS",
+ "title": "Redirect to HTTPS on HTTP requests",
"type": "boolean"
}
},
diff --git a/schema/providers.schema.json b/schema/providers.schema.json
index b31ef53b..438690ff 100644
--- a/schema/providers.schema.json
+++ b/schema/providers.schema.json
@@ -32,12 +32,17 @@
},
{
"type": "null",
- "description": "Auto detect base on port number"
+ "description": "Auto detect base on port format"
}
]
},
"host": {
+ "default": "localhost",
"oneOf": [
+ {
+ "type": "null",
+ "description": "localhost (default)"
+ },
{
"type": "string",
"format": "ipv4",
@@ -56,59 +61,38 @@
],
"title": "Proxy host (ipv4 / ipv6 / hostname)"
},
- "port": {
- "title": "Proxy port"
- },
- "path": {
- "title": "Proxy path pattern (See https://pkg.go.dev/net/http#ServeMux)"
- },
- "no_tls_verify": {
- "description": "Disable TLS verification for https proxy",
- "type": "boolean"
- },
+ "port": {},
+ "no_tls_verify": {},
+ "path_patterns": {},
"set_headers": {},
"hide_headers": {}
},
- "required": ["host"],
"additionalProperties": false,
"allOf": [
{
"if": {
- "anyOf": [
- {
- "properties": {
- "scheme": {
+ "properties": {
+ "scheme": {
+ "anyOf": [
+ {
"enum": ["http", "https"]
- }
- }
- },
- {
- "properties": {
- "scheme": {
- "not": true
- }
- }
- },
- {
- "properties": {
- "scheme": {
+ },
+ {
"type": "null"
}
- }
+ ]
}
- ]
+ }
},
"then": {
"properties": {
"port": {
+ "markdownDescription": "Proxy port from **1** to **65535**",
"oneOf": [
{
"type": "string",
- "pattern": "^[0-9]{1,5}$",
- "minimum": 1,
- "maximum": 65535,
- "markdownDescription": "Proxy port from **1** to **65535**",
- "patternErrorMessage": "'port' must be a number"
+ "pattern": "^\\d{1,5}$",
+ "patternErrorMessage": "`port` must be a number"
},
{
"type": "integer",
@@ -117,11 +101,16 @@
}
]
},
- "path": {
+ "path_patterns": {
"oneOf": [
{
- "type": "string",
- "description": "Proxy path"
+ "type": "array",
+ "markdownDescription": "A list of [path patterns](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)",
+ "items": {
+ "type": "string",
+ "pattern": "^((GET|POST|DELETE|PUT|PATCH|HEAD|OPTIONS|CONNECT)\\s)?(/\\w*)+/?$",
+ "patternErrorMessage": "invalid path pattern"
+ }
},
{
"type": "null",
@@ -133,7 +122,6 @@
"type": "object",
"description": "Proxy headers to set",
"additionalProperties": {
- "type": "array",
"items": {
"type": "string"
}
@@ -151,12 +139,15 @@
"else": {
"properties": {
"port": {
- "markdownDescription": "`listening port`:`proxy port | service name`",
+ "markdownDescription": "`listening port:proxy port` or `listening port:service name`",
"type": "string",
"pattern": "^[0-9]+\\:[0-9a-z]+$",
- "patternErrorMessage": "'port' must be in the format of ':'"
+ "patternErrorMessage": "invalid syntax"
},
- "path": {
+ "no_tls_verify": {
+ "not": true
+ },
+ "path_patterns": {
"not": true
},
"set_headers": {
@@ -171,15 +162,22 @@
},
{
"if": {
- "not": {
- "properties": {
- "scheme": {
- "const": "https"
- }
+ "properties": {
+ "scheme": {
+ "const": "https"
}
}
},
"then": {
+ "properties": {
+ "no_tls_verify": {
+ "description": "Disable TLS verification for https proxy",
+ "type": "boolean",
+ "default": false
+ }
+ }
+ },
+ "else": {
"properties": {
"no_tls_verify": {
"not": true
diff --git a/src/api/v1/checkhealth.go b/src/api/v1/checkhealth.go
index cead95fe..ab730a7c 100644
--- a/src/api/v1/checkhealth.go
+++ b/src/api/v1/checkhealth.go
@@ -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),
diff --git a/src/docker/label_parser.go b/src/docker/label_parser.go
index 7a102270..cb490db9 100644
--- a/src/docker/label_parser.go
+++ b/src/docker/label_parser.go
@@ -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
diff --git a/src/docker/label_parser_test.go b/src/docker/label_parser_test.go
index 266049a4..a7ab312b 100644
--- a/src/docker/label_parser_test.go
+++ b/src/docker/label_parser_test.go
@@ -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)
}
}
diff --git a/src/main.go b/src/main.go
index fb35c2bb..ee7fd930 100755
--- a/src/main.go
+++ b/src/main.go
@@ -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()
})
diff --git a/src/models/proxy_entry.go b/src/models/proxy_entry.go
index e884a87d..993ff1cf 100644
--- a/src/models/proxy_entry.go
+++ b/src/models/proxy_entry.go
@@ -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":
diff --git a/src/proxy/entry.go b/src/proxy/entry.go
index 2f119548..08359c75 100644
--- a/src/proxy/entry.go
+++ b/src/proxy/entry.go
@@ -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()
}
diff --git a/src/proxy/fields/headers.go b/src/proxy/fields/headers.go
new file mode 100644
index 00000000..173767f8
--- /dev/null
+++ b/src/proxy/fields/headers.go
@@ -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()
+}
diff --git a/src/proxy/fields/path.go b/src/proxy/fields/path.go
deleted file mode 100644
index ef384443..00000000
--- a/src/proxy/fields/path.go
+++ /dev/null
@@ -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 '/'")
-}
diff --git a/src/proxy/fields/path_pattern.go b/src/proxy/fields/path_pattern.go
new file mode 100644
index 00000000..114e9853
--- /dev/null
+++ b/src/proxy/fields/path_pattern.go
@@ -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*)+/?$")
diff --git a/src/proxy/provider/provider.go b/src/proxy/provider/provider.go
index 55ed41a5..44e76a55 100644
--- a/src/proxy/provider/provider.go
+++ b/src/proxy/provider/provider.go
@@ -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 {
diff --git a/src/route/http_route.go b/src/route/http_route.go
index 740062e2..b5fd3d35 100755
--- a/src/route/http_route.go
+++ b/src/route/http_route.go
@@ -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()
}
diff --git a/src/watcher/docker_watcher.go b/src/watcher/docker_watcher.go
index 63433a93..3f1484eb 100644
--- a/src/watcher/docker_watcher.go
+++ b/src/watcher/docker_watcher.go
@@ -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'
)}
diff --git a/src/watcher/event.go b/src/watcher/event.go
index ec0a6fa4..ba27d2e2 100644
--- a/src/watcher/event.go
+++ b/src/watcher/event.go
@@ -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
-}
diff --git a/src/watcher/file_watcher.go b/src/watcher/file_watcher.go
index f8f633ce..a871a115 100644
--- a/src/watcher/file_watcher.go
+++ b/src/watcher/file_watcher.go
@@ -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)
}
diff --git a/src/watcher/file_watcher_helper.go b/src/watcher/file_watcher_helper.go
index a98fcc5e..58492790 100644
--- a/src/watcher/file_watcher_helper.go
+++ b/src/watcher/file_watcher_helper.go
@@ -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()