mirror of
https://github.com/yusing/godoxy.git
synced 2025-07-25 21:34:02 +02:00
check release
This commit is contained in:
parent
d7eab2ebcd
commit
626bd9666b
19 changed files with 336 additions and 149 deletions
2
Makefile
2
Makefile
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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...)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
145
src/proxy/provider/docker_provider_test.go
Normal file
145
src/proxy/provider/docker_provider_test.go
Normal 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"))
|
||||||
|
}
|
|
@ -168,4 +168,5 @@ var (
|
||||||
|
|
||||||
httpRoutes = F.NewMapOf[SubdomainKey, *HTTPRoute]()
|
httpRoutes = F.NewMapOf[SubdomainKey, *HTTPRoute]()
|
||||||
httpRoutesMu sync.Mutex
|
httpRoutesMu sync.Mutex
|
||||||
|
globalMux = http.NewServeMux()
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue