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,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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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...)
)

View File

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

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]()
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
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 {

View File

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