diff --git a/Makefile b/Makefile index 73ea05e1..51a1f8fd 100755 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ build: CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy github.com/yusing/go-proxy test: - cd src && go test ./... && cd .. + go test ./src/... up: docker compose up -d diff --git a/docs/docker.md b/docs/docker.md index 9bc50818..f2fbd5d0 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -85,17 +85,18 @@ ### Syntax -| Label | Description | Default | Accepted values | -| ----------------------- | --------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------- | -| `proxy.aliases` | comma separated aliases for subdomain and label matching | `container_name` | any | -| `proxy.exclude` | to be excluded from `go-proxy` | false | boolean | -| `proxy.idle_timeout` | time for idle (no traffic) before put it into sleep **(http/s only)** | empty **(disabled)** | `number[unit]...`, e.g. `1m30s` | -| `proxy.wake_timeout` | time to wait for container to start before responding a loading page | empty | `number[unit]...` | -| `proxy.stop_method` | method to stop after `idle_timeout` | `stop` | `stop`, `pause`, `kill` | -| `proxy.stop_timeout` | time to wait for stop command | `10s` | `number[unit]...` | -| `proxy.stop_signal` | signal sent to container for `stop` and `kill` methods | docker's default | `SIGINT`, `SIGTERM`, `SIGHUP`, `SIGQUIT` and those without **SIG** prefix | -| `proxy..` | set field for specific alias | N/A | N/A | -| `proxy.*.` | set field for all aliases | N/A | N/A | +| Label | Description | Default | Accepted values | +| ------------------------ | --------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------- | +| `proxy.aliases` | comma separated aliases for subdomain and label matching | `container_name` | any | +| `proxy.exclude` | to be excluded from `go-proxy` | false | boolean | +| `proxy.idle_timeout` | time for idle (no traffic) before put it into sleep **(http/s only)** | empty **(disabled)** | `number[unit]...`, e.g. `1m30s` | +| `proxy.wake_timeout` | time to wait for container to start before responding a loading page | empty | `number[unit]...` | +| `proxy.stop_method` | method to stop after `idle_timeout` | `stop` | `stop`, `pause`, `kill` | +| `proxy.stop_timeout` | time to wait for stop command | `10s` | `number[unit]...` | +| `proxy.stop_signal` | signal sent to container for `stop` and `kill` methods | docker's default | `SIGINT`, `SIGTERM`, `SIGHUP`, `SIGQUIT` and those without **SIG** prefix | +| `proxy..` | set field for specific alias | N/A | N/A | +| `proxy.$.` | set field for specific alias at index (started from **0**) | N/A | N/A | +| `proxy.*.` | set field for all aliases | N/A | N/A | ### Fields @@ -226,10 +227,10 @@ services: restart: unless-stopped labels: - proxy.aliases=adg,adg-dns,adg-setup - - proxy.adg.port=80 - - proxy.adg-setup.port=3000 - - proxy.adg-dns.scheme=udp - - proxy.adg-dns.port=20000:dns + - proxy.$1.port=80 + - proxy.$2.scheme=udp + - proxy.$2.port=20000:dns + - proxy.$3.port=3000 volumes: - adg-work:/opt/adguardhome/work - adg-conf:/opt/adguardhome/conf @@ -263,8 +264,8 @@ services: labels: - proxy.aliases=pal1,pal2 - proxy.*.scheme=udp - - proxy.pal1.port=20002:8211 - - proxy.pal2.port=20003:27015 + - proxy.$1.port=20002:8211 + - proxy.$2.port=20003:27015 environment: ... volumes: - palworld:/palworld diff --git a/src/common/args.go b/src/common/args.go index c01f336d..806d2102 100644 --- a/src/common/args.go +++ b/src/common/args.go @@ -12,11 +12,12 @@ type Args struct { } const ( - CommandStart = "" - CommandValidate = "validate" - CommandListConfigs = "ls-config" - CommandListRoutes = "ls-routes" - CommandReload = "reload" + CommandStart = "" + CommandValidate = "validate" + CommandListConfigs = "ls-config" + CommandListRoutes = "ls-routes" + CommandReload = "reload" + CommandDebugListEntries = "debug-ls-entries" ) var ValidCommands = []string{ @@ -25,6 +26,7 @@ var ValidCommands = []string{ CommandListConfigs, CommandListRoutes, CommandReload, + CommandDebugListEntries, } func GetArgs() Args { diff --git a/src/common/constants.go b/src/common/constants.go index 08c1b3c1..3308f7dc 100644 --- a/src/common/constants.go +++ b/src/common/constants.go @@ -53,14 +53,15 @@ var WellKnownHTTPPorts = map[uint16]bool{ var ( ServiceNamePortMapTCP = map[string]int{ - "postgres": 5432, - "mysql": 3306, - "mariadb": 3306, - "redis": 6379, - "mssql": 1433, - "memcached": 11211, - "rabbitmq": 5672, - "mongo": 27017, + "postgres": 5432, + "mysql": 3306, + "mariadb": 3306, + "redis": 6379, + "mssql": 1433, + "memcached": 11211, + "rabbitmq": 5672, + "mongo": 27017, + "minecraft-server": 25565, "dns": 53, "ssh": 22, diff --git a/src/config/config.go b/src/config/config.go index 17a53b50..1e17ac77 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -52,11 +52,6 @@ func (cfg *Config) GetAutoCertProvider() *autocert.Provider { return cfg.autocertProvider } -func (cfg *Config) StartProxyProviders() { - cfg.startProviders() - cfg.watchChanges() -} - func (cfg *Config) Dispose() { if cfg.watcherCancel != nil { cfg.watcherCancel() @@ -70,10 +65,48 @@ func (cfg *Config) Reload() E.NestedError { if err := cfg.load(); err.HasError() { return err } - cfg.startProviders() + cfg.StartProxyProviders() return nil } +func (cfg *Config) StartProxyProviders() { + cfg.controlProviders("start", (*PR.Provider).StartAllRoutes) +} + +func (cfg *Config) WatchChanges() { + cfg.watcherCtx, cfg.watcherCancel = context.WithCancel(context.Background()) + go func() { + for { + select { + case <-cfg.watcherCtx.Done(): + return + case <-cfg.reloadReq: + if err := cfg.Reload(); err.HasError() { + cfg.l.Error(err) + } + } + } + }() + go func() { + eventCh, errCh := cfg.watcher.Events(cfg.watcherCtx) + for { + select { + case <-cfg.watcherCtx.Done(): + return + case event := <-eventCh: + if event.Action.IsDelete() { + cfg.stopProviders() + } else { + cfg.reloadReq <- struct{}{} + } + case err := <-errCh: + cfg.l.Error(err) + continue + } + } + }() +} + func (cfg *Config) FindRoute(alias string) R.Route { return F.MapFind(cfg.proxyProviders, func(p *PR.Provider) (R.Route, bool) { @@ -131,6 +164,14 @@ func (cfg *Config) Statistics() map[string]any { } } +func (cfg *Config) DumpEntries() map[string]*M.ProxyEntry { + entries := make(map[string]*M.ProxyEntry) + cfg.forEachRoute(func(alias string, r R.Route, p *PR.Provider) { + entries[alias] = r.Entry() + }) + return entries +} + func (cfg *Config) forEachRoute(do func(alias string, r R.Route, p *PR.Provider)) { cfg.proxyProviders.RangeAll(func(_ string, p *PR.Provider) { p.RangeRoutes(func(a string, r R.Route) { @@ -139,40 +180,6 @@ func (cfg *Config) forEachRoute(do func(alias string, r R.Route, p *PR.Provider) }) } -func (cfg *Config) watchChanges() { - cfg.watcherCtx, cfg.watcherCancel = context.WithCancel(context.Background()) - go func() { - for { - select { - case <-cfg.watcherCtx.Done(): - return - case <-cfg.reloadReq: - if err := cfg.Reload(); err.HasError() { - cfg.l.Error(err) - } - } - } - }() - go func() { - eventCh, errCh := cfg.watcher.Events(cfg.watcherCtx) - for { - select { - case <-cfg.watcherCtx.Done(): - return - case event := <-eventCh: - if event.Action.IsDelete() { - cfg.stopProviders() - } else { - cfg.reloadReq <- struct{}{} - } - case err := <-errCh: - cfg.l.Error(err) - continue - } - } - }() -} - func (cfg *Config) load() (res E.NestedError) { b := E.NewBuilder("errors loading config") defer b.To(&res) @@ -257,10 +264,6 @@ func (cfg *Config) controlProviders(action string, do func(*PR.Provider) E.Neste } } -func (cfg *Config) startProviders() { - cfg.controlProviders("start", (*PR.Provider).StartAllRoutes) -} - func (cfg *Config) stopProviders() { cfg.controlProviders("stop routes", (*PR.Provider).StopAllRoutes) } diff --git a/src/docker/container.go b/src/docker/container.go index 9a7aa3ba..accd211c 100644 --- a/src/docker/container.go +++ b/src/docker/container.go @@ -43,7 +43,7 @@ func FromDocker(c *types.Container, dockerHost string) (res Container) { StopMethod: res.getDeleteLabel(LabelStopMethod), StopTimeout: res.getDeleteLabel(LabelStopTimeout), StopSignal: res.getDeleteLabel(LabelStopSignal), - Running: c.Status == "running", + Running: c.Status == "running" || c.State == "running", } return } @@ -94,7 +94,7 @@ func (c Container) getName() string { func (c Container) getImageName() string { colonSep := strings.Split(c.Image, ":") - slashSep := strings.Split(colonSep[len(colonSep)-1], "/") + slashSep := strings.Split(colonSep[0], "/") return slashSep[len(slashSep)-1] } diff --git a/src/docker/label.go b/src/docker/label.go index b049509e..57746c00 100644 --- a/src/docker/label.go +++ b/src/docker/label.go @@ -23,7 +23,7 @@ type Label struct { // Returns: // - error: an error if the field does not exist. func ApplyLabel[T any](obj *T, l *Label) E.NestedError { - return U.SetFieldFromSnake(obj, l.Attribute, l.Value) + return U.Deserialize(map[string]any{l.Attribute: l.Value}, obj) } type ValueParser func(string) (any, E.NestedError) diff --git a/src/error/builder_test.go b/src/error/builder_test.go index 122a123c..99f0b99c 100644 --- a/src/error/builder_test.go +++ b/src/error/builder_test.go @@ -36,17 +36,17 @@ func TestBuilderNested(t *testing.T) { expected1 := (`error occurred: - Action 1 failed: - - invalid Inner - 1 - - invalid Inner - 2 + - invalid Inner: 1 + - invalid Inner: 2 - Action 2 failed: - - invalid Inner - 3`) + - invalid Inner: 3`) expected2 := (`error occurred: - Action 1 failed: - - invalid Inner - 2 - - invalid Inner - 1 + - invalid Inner: 2 + - invalid Inner: 1 - Action 2 failed: - - invalid Inner - 3`) + - invalid Inner: 3`) if got != expected1 && got != expected2 { t.Errorf("expected \n%s, got \n%s", expected1, got) } diff --git a/src/error/error.go b/src/error/error.go index 15587ab1..cc3934ac 100644 --- a/src/error/error.go +++ b/src/error/error.go @@ -10,13 +10,10 @@ type ( NestedError = *nestedError nestedError struct { subject string - err error // can be nil + err error extras []nestedError severity Severity } - errorInterface struct { - *nestedError - } Severity uint8 ) @@ -25,20 +22,11 @@ const ( SeverityWarning ) -func (e errorInterface) Error() string { - return e.String() -} - func From(err error) NestedError { if IsNil(err) { return nil } - switch err := err.(type) { - case errorInterface: - return err.nestedError - default: - return &nestedError{err: err} - } + return &nestedError{err: err} } // Check is a helper function that @@ -112,7 +100,7 @@ func (ne NestedError) Error() error { if ne == nil { return nil } - return errorInterface{ne} + return ne.buildError(0, "") } func (ne NestedError) With(s any) NestedError { @@ -123,10 +111,10 @@ func (ne NestedError) With(s any) NestedError { switch ss := s.(type) { case nil: return ne - case *nestedError: - return ne.withError(ss.Error()) - case error: + case NestedError: return ne.withError(ss) + case error: + return ne.withError(From(ss)) case string: msg = ss case fmt.Stringer: @@ -134,7 +122,7 @@ func (ne NestedError) With(s any) NestedError { default: msg = fmt.Sprint(s) } - return ne.withError(errors.New(msg)) + return ne.withError(From(errors.New(msg))) } func (ne NestedError) Extraf(format string, args ...any) NestedError { @@ -206,15 +194,17 @@ func errorf(format string, args ...any) NestedError { return From(fmt.Errorf(format, args...)) } -func (ne NestedError) withError(err error) NestedError { - if ne != nil && IsNotNil(err) { - ne.extras = append(ne.extras, *From(err)) +func (ne NestedError) withError(err NestedError) NestedError { + if ne != nil && err != nil { + ne.extras = append(ne.extras, *err) } return ne } func (ne NestedError) writeToSB(sb *strings.Builder, level int, prefix string) { - ne.writeIndents(sb, level) + for i := 0; i < level; i++ { + sb.WriteString(" ") + } sb.WriteString(prefix) if ne.NoError() { @@ -224,11 +214,7 @@ func (ne NestedError) writeToSB(sb *strings.Builder, level int, prefix string) { sb.WriteString(ne.err.Error()) if ne.subject != "" { - if IsNotNil(ne.err) { - sb.WriteString(fmt.Sprintf(" for %q", ne.subject)) - } else { - sb.WriteString(fmt.Sprint(ne.subject)) - } + sb.WriteString(fmt.Sprintf(" for %q", ne.subject)) } if len(ne.extras) > 0 { sb.WriteRune(':') @@ -239,8 +225,32 @@ func (ne NestedError) writeToSB(sb *strings.Builder, level int, prefix string) { } } -func (ne NestedError) writeIndents(sb *strings.Builder, level int) { +func (ne NestedError) buildError(level int, prefix string) error { + var res error + var sb strings.Builder + for i := 0; i < level; i++ { sb.WriteString(" ") } + sb.WriteString(prefix) + + if ne.NoError() { + sb.WriteString("nil") + return errors.New(sb.String()) + } + + res = fmt.Errorf("%s%w", sb.String(), ne.err) + sb.Reset() + + if ne.subject != "" { + sb.WriteString(fmt.Sprintf(" for %q", ne.subject)) + } + if len(ne.extras) > 0 { + sb.WriteRune(':') + res = fmt.Errorf("%w%s", res, sb.String()) + for _, extra := range ne.extras { + res = errors.Join(res, extra.buildError(level+1, "- ")) + } + } + return res } diff --git a/src/error/error_test.go b/src/error/error_test.go index 9fa2f3ec..7e465af0 100644 --- a/src/error/error_test.go +++ b/src/error/error_test.go @@ -1,6 +1,7 @@ package error_test import ( + "errors" "testing" . "github.com/yusing/go-proxy/error" @@ -17,6 +18,11 @@ func TestErrorIs(t *testing.T) { ExpectFalse(t, Invalid("foo", "bar").Is(ErrFailure)) ExpectFalse(t, Invalid("foo", "bar").Is(nil)) + + ExpectTrue(t, errors.Is(Failure("foo").Error(), ErrFailure)) + ExpectTrue(t, errors.Is(Failure("foo").With(Invalid("bar", "baz")).Error(), ErrInvalid)) + ExpectTrue(t, errors.Is(Failure("foo").With(Invalid("bar", "baz")).Error(), ErrFailure)) + ExpectFalse(t, errors.Is(Failure("foo").With(Invalid("bar", "baz")).Error(), ErrNotExists)) } func TestErrorNestedIs(t *testing.T) { @@ -99,4 +105,5 @@ func TestErrorNested(t *testing.T) { - 3 - 3` ExpectEqual(t, ne.String(), want) + ExpectEqual(t, ne.Error().Error(), want) } diff --git a/src/main.go b/src/main.go index 2a6a80c2..60245ee9 100755 --- a/src/main.go +++ b/src/main.go @@ -23,6 +23,7 @@ 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() { @@ -82,10 +83,18 @@ func main() { return } + if args.Command == common.CommandDebugListEntries { + printJSON(cfg.DumpEntries()) + return + } + if err.HasError() { l.Warn(err) } + W.InitFileWatcherHelper() + cfg.WatchChanges() + onShutdown.Add(docker.CloseAllClients) onShutdown.Add(cfg.Dispose) diff --git a/src/models/proxy_entry.go b/src/models/proxy_entry.go index 4f41dcd1..dabb9713 100644 --- a/src/models/proxy_entry.go +++ b/src/models/proxy_entry.go @@ -21,7 +21,7 @@ type ( HideHeaders []string `yaml:"hide_headers" json:"hide_headers"` // http(s) proxy only /* Docker only */ - *D.ProxyProperties `yaml:"-" json:"-"` + *D.ProxyProperties `yaml:"-" json:"proxy_properties"` } ProxyEntries = F.Map[string, *ProxyEntry] diff --git a/src/proxy/constants.go b/src/proxy/constants.go index 8acaaa8c..12783227 100644 --- a/src/proxy/constants.go +++ b/src/proxy/constants.go @@ -1,10 +1,5 @@ package proxy -var ( - PathMode_Forward = "forward" - PathMode_RemovedPath = "" -) - const ( StreamType_UDP string = "udp" StreamType_TCP string = "tcp" @@ -19,4 +14,3 @@ var ( HTTPSchemes = []string{"http", "https"} ValidSchemes = append(StreamSchemes, HTTPSchemes...) ) - diff --git a/src/proxy/provider/docker_provider.go b/src/proxy/provider/docker_provider.go index 71284974..64b88ed0 100755 --- a/src/proxy/provider/docker_provider.go +++ b/src/proxy/provider/docker_provider.go @@ -1,7 +1,9 @@ package provider import ( + "regexp" "strconv" + "strings" D "github.com/yusing/go-proxy/docker" E "github.com/yusing/go-proxy/error" @@ -14,6 +16,8 @@ type DockerProvider struct { dockerHost, hostname string } +var AliasRefRegex = regexp.MustCompile(`\$\d+`) + func DockerProviderImpl(dockerHost string) ProviderImpl { return &DockerProvider{dockerHost: dockerHost} } @@ -120,7 +124,7 @@ func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (M.Pr errors := E.NewBuilder("failed to apply label") for key, val := range container.Labels { - errors.Add(p.applyLabel(entries, key, val)) + errors.Add(p.applyLabel(container, entries, key, val)) } // selecting correct host port @@ -132,9 +136,14 @@ func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (M.Pr } for _, p := range container.Ports { containerPort := strconv.Itoa(int(p.PrivatePort)) - if containerPort == entry.Port { - entry.Port = strconv.Itoa(int(p.PublicPort)) + publicPort := strconv.Itoa(int(p.PublicPort)) + entryPortSplit := strings.Split(entry.Port, ":") + if len(entryPortSplit) == 2 && entryPortSplit[1] == containerPort { + entryPortSplit[1] = publicPort + } else if entryPortSplit[0] == containerPort { + entryPortSplit[0] = publicPort } + entry.Port = strings.Join(entryPortSplit, ":") } } } @@ -142,7 +151,7 @@ func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (M.Pr return entries, errors.Build().Subject(container.ContainerName) } -func (p *DockerProvider) applyLabel(entries M.ProxyEntries, key, val string) (res E.NestedError) { +func (p *DockerProvider) applyLabel(container D.Container, entries M.ProxyEntries, key, val string) (res E.NestedError) { b := E.NewBuilder("errors in label %s", key) defer b.To(&res) @@ -161,6 +170,23 @@ func (p *DockerProvider) applyLabel(entries M.ProxyEntries, key, val string) (re } }) } else { + refErr := E.NewBuilder("errors parsing alias references") + lbl.Target = AliasRefRegex.ReplaceAllStringFunc(lbl.Target, func(ref string) string { + index, err := strconv.Atoi(ref[1:]) + if err != nil { + refErr.Add(E.Invalid("integer", ref)) + return ref + } + if index < 1 || index > len(container.Aliases) { + refErr.Add(E.Invalid("index", ref).Extraf("index out of range")) + return ref + } + return container.Aliases[index-1] + }) + if refErr.HasError() { + b.Add(refErr.Build()) + return + } config, ok := entries.Load(lbl.Target) if !ok { b.Add(E.NotExist("alias", lbl.Target)) diff --git a/src/proxy/provider/docker_provider_test.go b/src/proxy/provider/docker_provider_test.go new file mode 100644 index 00000000..d4f5d86e --- /dev/null +++ b/src/proxy/provider/docker_provider_test.go @@ -0,0 +1,145 @@ +package provider + +import ( + "strings" + "testing" + + "github.com/docker/docker/api/types" + D "github.com/yusing/go-proxy/docker" + E "github.com/yusing/go-proxy/error" + F "github.com/yusing/go-proxy/utils/functional" + . "github.com/yusing/go-proxy/utils/testing" +) + +func get[KT comparable, VT any](m F.Map[KT, VT], key KT) VT { + v, _ := m.Load(key) + return v +} + +var dummyNames = []string{"/a"} + +func TestApplyLabelFieldValidity(t *testing.T) { + pathPatterns := ` +- / +- POST /upload/{$} +- GET /static +`[1:] + pathPatternsExpect := []string{ + "/", + "POST /upload/{$}", + "GET /static", + } + setHeaders := ` +X_Custom_Header1: value1 +X_Custom_Header1: value2 +X_Custom_Header2: value3 +`[1:] + setHeadersExpect := map[string]string{ + "X_Custom_Header1": "value1, value2", + "X_Custom_Header2": "value3", + } + hideHeaders := ` +- X-Custom-Header1 +- X-Custom-Header2 +`[1:] + hideHeadersExpect := []string{ + "X-Custom-Header1", + "X-Custom-Header2", + } + var p DockerProvider + var c = D.FromDocker(&types.Container{ + Names: dummyNames, + Labels: map[string]string{ + D.LableAliases: "a,b", + "proxy.*.scheme": "https", + "proxy.*.host": "app", + "proxy.*.port": "4567", + "proxy.a.no_tls_verify": "true", + "proxy.a.path_patterns": pathPatterns, + "proxy.a.set_headers": setHeaders, + "proxy.a.hide_headers": hideHeaders, + }}, "") + entries, err := p.entriesFromContainerLabels(c) + ExpectNoError(t, err.Error()) + a := get(entries, "a") + b := get(entries, "b") + + ExpectEqual(t, a.Scheme, "https") + ExpectEqual(t, b.Scheme, "https") + + ExpectEqual(t, a.Host, "app") + ExpectEqual(t, b.Host, "app") + + ExpectEqual(t, a.Port, "4567") + ExpectEqual(t, b.Port, "4567") + + ExpectEqual(t, a.NoTLSVerify, true) + ExpectEqual(t, b.NoTLSVerify, false) + + ExpectDeepEqual(t, a.PathPatterns, pathPatternsExpect) + ExpectEqual(t, len(b.PathPatterns), 0) + + ExpectDeepEqual(t, a.SetHeaders, setHeadersExpect) + ExpectEqual(t, len(b.SetHeaders), 0) + + ExpectDeepEqual(t, a.HideHeaders, hideHeadersExpect) + ExpectEqual(t, len(b.HideHeaders), 0) +} + +func TestApplyLabel(t *testing.T) { + var p DockerProvider + var c = D.FromDocker(&types.Container{ + Names: dummyNames, + Labels: map[string]string{ + D.LableAliases: "a,b,c", + "proxy.a.no_tls_verify": "true", + "proxy.b.port": "1234", + "proxy.c.scheme": "https", + }}, "") + entries, err := p.entriesFromContainerLabels(c) + ExpectNoError(t, err.Error()) + ExpectEqual(t, get(entries, "a").NoTLSVerify, true) + ExpectEqual(t, get(entries, "b").Port, "1234") + ExpectEqual(t, get(entries, "c").Scheme, "https") +} + +func TestApplyLabelWithRef(t *testing.T) { + var p DockerProvider + var c = D.FromDocker(&types.Container{ + Names: dummyNames, + Labels: map[string]string{ + D.LableAliases: "a,b,c", + "proxy.$1.host": "localhost", + "proxy.$2.port": "1234", + "proxy.$3.scheme": "https", + }}, "") + entries, err := p.entriesFromContainerLabels(c) + ExpectNoError(t, err.Error()) + ExpectEqual(t, get(entries, "a").Host, "localhost") + ExpectEqual(t, get(entries, "b").Port, "1234") + ExpectEqual(t, get(entries, "c").Scheme, "https") +} + +func TestApplyLabelWithRefIndexError(t *testing.T) { + var p DockerProvider + var c = D.FromDocker(&types.Container{ + Names: dummyNames, + Labels: map[string]string{ + D.LableAliases: "a,b", + "proxy.$1.host": "localhost", + "proxy.$4.scheme": "https", + }}, "") + _, err := p.entriesFromContainerLabels(c) + ExpectError(t, E.ErrInvalid, err.Error()) + ExpectTrue(t, strings.Contains(err.String(), "index out of range")) + + c = D.FromDocker(&types.Container{ + Names: dummyNames, + Labels: map[string]string{ + D.LableAliases: "a,b", + "proxy.$0.host": "localhost", + }}, "") + _, err = p.entriesFromContainerLabels(c) + ExpectError(t, E.ErrInvalid, err.Error()) + ExpectTrue(t, strings.Contains(err.String(), "index out of range")) +} diff --git a/src/route/http_route.go b/src/route/http_route.go index c8b2b72b..5f796c87 100755 --- a/src/route/http_route.go +++ b/src/route/http_route.go @@ -168,4 +168,5 @@ var ( httpRoutes = F.NewMapOf[SubdomainKey, *HTTPRoute]() httpRoutesMu sync.Mutex + globalMux = http.NewServeMux() ) diff --git a/src/utils/reflection.go b/src/utils/reflection.go deleted file mode 100644 index 8eb525ca..00000000 --- a/src/utils/reflection.go +++ /dev/null @@ -1,24 +0,0 @@ -package utils - -import ( - "net/http" - "reflect" - "strings" - - E "github.com/yusing/go-proxy/error" -) - -func snakeToPascal(s string) string { - toHyphenCamel := http.CanonicalHeaderKey(strings.ReplaceAll(s, "_", "-")) - return strings.ReplaceAll(toHyphenCamel, "-", "") -} - -func SetFieldFromSnake[T, VT any](obj *T, field string, value VT) E.NestedError { - field = snakeToPascal(field) - prop := reflect.ValueOf(obj).Elem().FieldByName(field) - if prop.Kind() == 0 { - return E.Invalid("field", field) - } - prop.Set(reflect.ValueOf(value)) - return nil -} diff --git a/src/utils/testing/testing.go b/src/utils/testing/testing.go index f15726c1..98d559e7 100644 --- a/src/utils/testing/testing.go +++ b/src/utils/testing/testing.go @@ -1,6 +1,7 @@ package utils import ( + "errors" "reflect" "testing" ) @@ -12,6 +13,13 @@ func ExpectNoError(t *testing.T, err error) { } } +func ExpectError(t *testing.T, expected error, err error) { + t.Helper() + if !errors.Is(err, expected) { + t.Errorf("expected err %s, got nil", expected.Error()) + } +} + func ExpectEqual[T comparable](t *testing.T, got T, want T) { t.Helper() if got != want { diff --git a/src/watcher/file_watcher.go b/src/watcher/file_watcher.go index f4cda622..f4dbc704 100644 --- a/src/watcher/file_watcher.go +++ b/src/watcher/file_watcher.go @@ -23,4 +23,8 @@ func (f *fileWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Nested return fwHelper.Add(ctx, f) } -var fwHelper = newFileWatcherHelper(common.ConfigBasePath) +func InitFileWatcherHelper() { + fwHelper = newFileWatcherHelper(common.ConfigBasePath) +} + +var fwHelper *fileWatcherHelper