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`, `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`
| -| `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`, `https`, `tcp`, `udp` | +| `host` | proxy host | | 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`
| +| `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()