check release

This commit is contained in:
yusing 2024-09-21 12:45:56 +08:00
parent d7eab2ebcd
commit 626bd9666b
19 changed files with 336 additions and 149 deletions

View file

@ -12,7 +12,7 @@ build:
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy github.com/yusing/go-proxy CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy github.com/yusing/go-proxy
test: test:
cd src && go test ./... && cd .. go test ./src/...
up: up:
docker compose up -d docker compose up -d

View file

@ -85,17 +85,18 @@
### Syntax ### Syntax
| Label | Description | Default | Accepted values | | Label | Description | Default | Accepted values |
| ----------------------- | --------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------- | | ------------------------ | --------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------- |
| `proxy.aliases` | comma separated aliases for subdomain and label matching | `container_name` | any | | `proxy.aliases` | comma separated aliases for subdomain and label matching | `container_name` | any |
| `proxy.exclude` | to be excluded from `go-proxy` | false | boolean | | `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.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.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_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_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.stop_signal` | signal sent to container for `stop` and `kill` methods | docker's default | `SIGINT`, `SIGTERM`, `SIGHUP`, `SIGQUIT` and those without **SIG** prefix |
| `proxy.<alias>.<field>` | set field for specific alias | N/A | N/A | | `proxy.<alias>.<field>` | set field for specific alias | N/A | N/A |
| `proxy.*.<field>` | set field for all aliases | N/A | N/A | | `proxy.$<index>.<field>` | set field for specific alias at index (started from **0**) | N/A | N/A |
| `proxy.*.<field>` | set field for all aliases | N/A | N/A |
### Fields ### Fields
@ -226,10 +227,10 @@ services:
restart: unless-stopped restart: unless-stopped
labels: labels:
- proxy.aliases=adg,adg-dns,adg-setup - proxy.aliases=adg,adg-dns,adg-setup
- proxy.adg.port=80 - proxy.$1.port=80
- proxy.adg-setup.port=3000 - proxy.$2.scheme=udp
- proxy.adg-dns.scheme=udp - proxy.$2.port=20000:dns
- proxy.adg-dns.port=20000:dns - proxy.$3.port=3000
volumes: volumes:
- adg-work:/opt/adguardhome/work - adg-work:/opt/adguardhome/work
- adg-conf:/opt/adguardhome/conf - adg-conf:/opt/adguardhome/conf
@ -263,8 +264,8 @@ services:
labels: labels:
- proxy.aliases=pal1,pal2 - proxy.aliases=pal1,pal2
- proxy.*.scheme=udp - proxy.*.scheme=udp
- proxy.pal1.port=20002:8211 - proxy.$1.port=20002:8211
- proxy.pal2.port=20003:27015 - proxy.$2.port=20003:27015
environment: ... environment: ...
volumes: volumes:
- palworld:/palworld - palworld:/palworld

View file

@ -12,11 +12,12 @@ type Args struct {
} }
const ( const (
CommandStart = "" CommandStart = ""
CommandValidate = "validate" CommandValidate = "validate"
CommandListConfigs = "ls-config" CommandListConfigs = "ls-config"
CommandListRoutes = "ls-routes" CommandListRoutes = "ls-routes"
CommandReload = "reload" CommandReload = "reload"
CommandDebugListEntries = "debug-ls-entries"
) )
var ValidCommands = []string{ var ValidCommands = []string{
@ -25,6 +26,7 @@ var ValidCommands = []string{
CommandListConfigs, CommandListConfigs,
CommandListRoutes, CommandListRoutes,
CommandReload, CommandReload,
CommandDebugListEntries,
} }
func GetArgs() Args { func GetArgs() Args {

View file

@ -53,14 +53,15 @@ var WellKnownHTTPPorts = map[uint16]bool{
var ( var (
ServiceNamePortMapTCP = map[string]int{ ServiceNamePortMapTCP = map[string]int{
"postgres": 5432, "postgres": 5432,
"mysql": 3306, "mysql": 3306,
"mariadb": 3306, "mariadb": 3306,
"redis": 6379, "redis": 6379,
"mssql": 1433, "mssql": 1433,
"memcached": 11211, "memcached": 11211,
"rabbitmq": 5672, "rabbitmq": 5672,
"mongo": 27017, "mongo": 27017,
"minecraft-server": 25565,
"dns": 53, "dns": 53,
"ssh": 22, "ssh": 22,

View file

@ -52,11 +52,6 @@ func (cfg *Config) GetAutoCertProvider() *autocert.Provider {
return cfg.autocertProvider return cfg.autocertProvider
} }
func (cfg *Config) StartProxyProviders() {
cfg.startProviders()
cfg.watchChanges()
}
func (cfg *Config) Dispose() { func (cfg *Config) Dispose() {
if cfg.watcherCancel != nil { if cfg.watcherCancel != nil {
cfg.watcherCancel() cfg.watcherCancel()
@ -70,10 +65,48 @@ func (cfg *Config) Reload() E.NestedError {
if err := cfg.load(); err.HasError() { if err := cfg.load(); err.HasError() {
return err return err
} }
cfg.startProviders() cfg.StartProxyProviders()
return nil 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 { func (cfg *Config) FindRoute(alias string) R.Route {
return F.MapFind(cfg.proxyProviders, return F.MapFind(cfg.proxyProviders,
func(p *PR.Provider) (R.Route, bool) { 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)) { func (cfg *Config) forEachRoute(do func(alias string, r R.Route, p *PR.Provider)) {
cfg.proxyProviders.RangeAll(func(_ string, p *PR.Provider) { cfg.proxyProviders.RangeAll(func(_ string, p *PR.Provider) {
p.RangeRoutes(func(a string, r R.Route) { 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) { func (cfg *Config) load() (res E.NestedError) {
b := E.NewBuilder("errors loading config") b := E.NewBuilder("errors loading config")
defer b.To(&res) 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() { func (cfg *Config) stopProviders() {
cfg.controlProviders("stop routes", (*PR.Provider).StopAllRoutes) cfg.controlProviders("stop routes", (*PR.Provider).StopAllRoutes)
} }

View file

@ -43,7 +43,7 @@ func FromDocker(c *types.Container, dockerHost string) (res Container) {
StopMethod: res.getDeleteLabel(LabelStopMethod), StopMethod: res.getDeleteLabel(LabelStopMethod),
StopTimeout: res.getDeleteLabel(LabelStopTimeout), StopTimeout: res.getDeleteLabel(LabelStopTimeout),
StopSignal: res.getDeleteLabel(LabelStopSignal), StopSignal: res.getDeleteLabel(LabelStopSignal),
Running: c.Status == "running", Running: c.Status == "running" || c.State == "running",
} }
return return
} }
@ -94,7 +94,7 @@ func (c Container) getName() string {
func (c Container) getImageName() string { func (c Container) getImageName() string {
colonSep := strings.Split(c.Image, ":") colonSep := strings.Split(c.Image, ":")
slashSep := strings.Split(colonSep[len(colonSep)-1], "/") slashSep := strings.Split(colonSep[0], "/")
return slashSep[len(slashSep)-1] return slashSep[len(slashSep)-1]
} }

View file

@ -23,7 +23,7 @@ type Label struct {
// Returns: // Returns:
// - error: an error if the field does not exist. // - error: an error if the field does not exist.
func ApplyLabel[T any](obj *T, l *Label) E.NestedError { 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) type ValueParser func(string) (any, E.NestedError)

View file

@ -36,17 +36,17 @@ func TestBuilderNested(t *testing.T) {
expected1 := expected1 :=
(`error occurred: (`error occurred:
- Action 1 failed: - Action 1 failed:
- invalid Inner - 1 - invalid Inner: 1
- invalid Inner - 2 - invalid Inner: 2
- Action 2 failed: - Action 2 failed:
- invalid Inner - 3`) - invalid Inner: 3`)
expected2 := expected2 :=
(`error occurred: (`error occurred:
- Action 1 failed: - Action 1 failed:
- invalid Inner - 2 - invalid Inner: 2
- invalid Inner - 1 - invalid Inner: 1
- Action 2 failed: - Action 2 failed:
- invalid Inner - 3`) - invalid Inner: 3`)
if got != expected1 && got != expected2 { if got != expected1 && got != expected2 {
t.Errorf("expected \n%s, got \n%s", expected1, got) t.Errorf("expected \n%s, got \n%s", expected1, got)
} }

View file

@ -10,13 +10,10 @@ type (
NestedError = *nestedError NestedError = *nestedError
nestedError struct { nestedError struct {
subject string subject string
err error // can be nil err error
extras []nestedError extras []nestedError
severity Severity severity Severity
} }
errorInterface struct {
*nestedError
}
Severity uint8 Severity uint8
) )
@ -25,20 +22,11 @@ const (
SeverityWarning SeverityWarning
) )
func (e errorInterface) Error() string {
return e.String()
}
func From(err error) NestedError { func From(err error) NestedError {
if IsNil(err) { if IsNil(err) {
return nil return nil
} }
switch err := err.(type) { return &nestedError{err: err}
case errorInterface:
return err.nestedError
default:
return &nestedError{err: err}
}
} }
// Check is a helper function that // Check is a helper function that
@ -112,7 +100,7 @@ func (ne NestedError) Error() error {
if ne == nil { if ne == nil {
return nil return nil
} }
return errorInterface{ne} return ne.buildError(0, "")
} }
func (ne NestedError) With(s any) NestedError { func (ne NestedError) With(s any) NestedError {
@ -123,10 +111,10 @@ func (ne NestedError) With(s any) NestedError {
switch ss := s.(type) { switch ss := s.(type) {
case nil: case nil:
return ne return ne
case *nestedError: case NestedError:
return ne.withError(ss.Error())
case error:
return ne.withError(ss) return ne.withError(ss)
case error:
return ne.withError(From(ss))
case string: case string:
msg = ss msg = ss
case fmt.Stringer: case fmt.Stringer:
@ -134,7 +122,7 @@ func (ne NestedError) With(s any) NestedError {
default: default:
msg = fmt.Sprint(s) 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 { 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...)) return From(fmt.Errorf(format, args...))
} }
func (ne NestedError) withError(err error) NestedError { func (ne NestedError) withError(err NestedError) NestedError {
if ne != nil && IsNotNil(err) { if ne != nil && err != nil {
ne.extras = append(ne.extras, *From(err)) ne.extras = append(ne.extras, *err)
} }
return ne return ne
} }
func (ne NestedError) writeToSB(sb *strings.Builder, level int, prefix string) { 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) sb.WriteString(prefix)
if ne.NoError() { if ne.NoError() {
@ -224,11 +214,7 @@ func (ne NestedError) writeToSB(sb *strings.Builder, level int, prefix string) {
sb.WriteString(ne.err.Error()) sb.WriteString(ne.err.Error())
if ne.subject != "" { if ne.subject != "" {
if IsNotNil(ne.err) { sb.WriteString(fmt.Sprintf(" for %q", ne.subject))
sb.WriteString(fmt.Sprintf(" for %q", ne.subject))
} else {
sb.WriteString(fmt.Sprint(ne.subject))
}
} }
if len(ne.extras) > 0 { if len(ne.extras) > 0 {
sb.WriteRune(':') 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++ { for i := 0; i < level; i++ {
sb.WriteString(" ") 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
} }

View file

@ -1,6 +1,7 @@
package error_test package error_test
import ( import (
"errors"
"testing" "testing"
. "github.com/yusing/go-proxy/error" . "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(ErrFailure))
ExpectFalse(t, Invalid("foo", "bar").Is(nil)) 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) { func TestErrorNestedIs(t *testing.T) {
@ -99,4 +105,5 @@ func TestErrorNested(t *testing.T) {
- 3 - 3
- 3` - 3`
ExpectEqual(t, ne.String(), want) ExpectEqual(t, ne.String(), want)
ExpectEqual(t, ne.Error().Error(), want)
} }

View file

@ -23,6 +23,7 @@ import (
R "github.com/yusing/go-proxy/route" R "github.com/yusing/go-proxy/route"
"github.com/yusing/go-proxy/server" "github.com/yusing/go-proxy/server"
F "github.com/yusing/go-proxy/utils/functional" F "github.com/yusing/go-proxy/utils/functional"
W "github.com/yusing/go-proxy/watcher"
) )
func main() { func main() {
@ -82,10 +83,18 @@ func main() {
return return
} }
if args.Command == common.CommandDebugListEntries {
printJSON(cfg.DumpEntries())
return
}
if err.HasError() { if err.HasError() {
l.Warn(err) l.Warn(err)
} }
W.InitFileWatcherHelper()
cfg.WatchChanges()
onShutdown.Add(docker.CloseAllClients) onShutdown.Add(docker.CloseAllClients)
onShutdown.Add(cfg.Dispose) onShutdown.Add(cfg.Dispose)

View file

@ -21,7 +21,7 @@ type (
HideHeaders []string `yaml:"hide_headers" json:"hide_headers"` // http(s) proxy only HideHeaders []string `yaml:"hide_headers" json:"hide_headers"` // http(s) proxy only
/* Docker only */ /* Docker only */
*D.ProxyProperties `yaml:"-" json:"-"` *D.ProxyProperties `yaml:"-" json:"proxy_properties"`
} }
ProxyEntries = F.Map[string, *ProxyEntry] ProxyEntries = F.Map[string, *ProxyEntry]

View file

@ -1,10 +1,5 @@
package proxy package proxy
var (
PathMode_Forward = "forward"
PathMode_RemovedPath = ""
)
const ( const (
StreamType_UDP string = "udp" StreamType_UDP string = "udp"
StreamType_TCP string = "tcp" StreamType_TCP string = "tcp"
@ -19,4 +14,3 @@ var (
HTTPSchemes = []string{"http", "https"} HTTPSchemes = []string{"http", "https"}
ValidSchemes = append(StreamSchemes, HTTPSchemes...) ValidSchemes = append(StreamSchemes, HTTPSchemes...)
) )

View file

@ -1,7 +1,9 @@
package provider package provider
import ( import (
"regexp"
"strconv" "strconv"
"strings"
D "github.com/yusing/go-proxy/docker" D "github.com/yusing/go-proxy/docker"
E "github.com/yusing/go-proxy/error" E "github.com/yusing/go-proxy/error"
@ -14,6 +16,8 @@ type DockerProvider struct {
dockerHost, hostname string dockerHost, hostname string
} }
var AliasRefRegex = regexp.MustCompile(`\$\d+`)
func DockerProviderImpl(dockerHost string) ProviderImpl { func DockerProviderImpl(dockerHost string) ProviderImpl {
return &DockerProvider{dockerHost: dockerHost} return &DockerProvider{dockerHost: dockerHost}
} }
@ -120,7 +124,7 @@ func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (M.Pr
errors := E.NewBuilder("failed to apply label") errors := E.NewBuilder("failed to apply label")
for key, val := range container.Labels { 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 // selecting correct host port
@ -132,9 +136,14 @@ func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (M.Pr
} }
for _, p := range container.Ports { for _, p := range container.Ports {
containerPort := strconv.Itoa(int(p.PrivatePort)) containerPort := strconv.Itoa(int(p.PrivatePort))
if containerPort == entry.Port { publicPort := strconv.Itoa(int(p.PublicPort))
entry.Port = 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) 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) b := E.NewBuilder("errors in label %s", key)
defer b.To(&res) defer b.To(&res)
@ -161,6 +170,23 @@ func (p *DockerProvider) applyLabel(entries M.ProxyEntries, key, val string) (re
} }
}) })
} else { } 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) config, ok := entries.Load(lbl.Target)
if !ok { if !ok {
b.Add(E.NotExist("alias", lbl.Target)) b.Add(E.NotExist("alias", lbl.Target))

View file

@ -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"))
}

View file

@ -168,4 +168,5 @@ var (
httpRoutes = F.NewMapOf[SubdomainKey, *HTTPRoute]() httpRoutes = F.NewMapOf[SubdomainKey, *HTTPRoute]()
httpRoutesMu sync.Mutex httpRoutesMu sync.Mutex
globalMux = http.NewServeMux()
) )

View file

@ -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
}

View file

@ -1,6 +1,7 @@
package utils package utils
import ( import (
"errors"
"reflect" "reflect"
"testing" "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) { func ExpectEqual[T comparable](t *testing.T, got T, want T) {
t.Helper() t.Helper()
if got != want { if got != want {

View file

@ -23,4 +23,8 @@ func (f *fileWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Nested
return fwHelper.Add(ctx, f) return fwHelper.Add(ctx, f)
} }
var fwHelper = newFileWatcherHelper(common.ConfigBasePath) func InitFileWatcherHelper() {
fwHelper = newFileWatcherHelper(common.ConfigBasePath)
}
var fwHelper *fileWatcherHelper