mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-24 01:38:50 +02:00
Fixed a few issues:
- Incorrect name being shown on dashboard "Proxies page" - Apps being shown when homepage.show is false - Load balanced routes are shown on homepage instead of the load balancer - Route with idlewatcher will now be removed on container destroy - Idlewatcher panic - Performance improvement - Idlewatcher infinitely loading - Reload stucked / not working properly - Streams stuck on shutdown / reload - etc... Added: - support idlewatcher for loadbalanced routes - partial implementation for stream type idlewatcher Issues: - graceful shutdown
This commit is contained in:
172
internal/route/provider/docker.go
Executable file
172
internal/route/provider/docker.go
Executable file
@@ -0,0 +1,172 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/proxy/entry"
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
)
|
||||
|
||||
type DockerProvider struct {
|
||||
name, dockerHost string
|
||||
ExplicitOnly bool
|
||||
}
|
||||
|
||||
var (
|
||||
AliasRefRegex = regexp.MustCompile(`#\d+`)
|
||||
AliasRefRegexOld = regexp.MustCompile(`\$\d+`)
|
||||
)
|
||||
|
||||
func DockerProviderImpl(name, dockerHost string, explicitOnly bool) (ProviderImpl, E.NestedError) {
|
||||
if dockerHost == common.DockerHostFromEnv {
|
||||
dockerHost = common.GetEnv("DOCKER_HOST", client.DefaultDockerHost)
|
||||
}
|
||||
return &DockerProvider{name, dockerHost, explicitOnly}, nil
|
||||
}
|
||||
|
||||
func (p *DockerProvider) String() string {
|
||||
return "docker: " + p.name
|
||||
}
|
||||
|
||||
func (p *DockerProvider) NewWatcher() W.Watcher {
|
||||
return W.NewDockerWatcher(p.dockerHost)
|
||||
}
|
||||
|
||||
func (p *DockerProvider) LoadRoutesImpl() (routes R.Routes, err E.NestedError) {
|
||||
routes = R.NewRoutes()
|
||||
entries := entry.NewProxyEntries()
|
||||
|
||||
info, err := D.GetClientInfo(p.dockerHost, true)
|
||||
if err != nil {
|
||||
return routes, E.FailWith("connect to docker", err)
|
||||
}
|
||||
|
||||
errors := E.NewBuilder("errors in docker labels")
|
||||
|
||||
for _, c := range info.Containers {
|
||||
container := D.FromDocker(&c, p.dockerHost)
|
||||
if container.IsExcluded {
|
||||
continue
|
||||
}
|
||||
|
||||
newEntries, err := p.entriesFromContainerLabels(container)
|
||||
if err != nil {
|
||||
errors.Add(err)
|
||||
}
|
||||
// although err is not nil
|
||||
// there may be some valid entries in `en`
|
||||
dups := entries.MergeFrom(newEntries)
|
||||
// add the duplicate proxy entries to the error
|
||||
dups.RangeAll(func(k string, v *entry.RawEntry) {
|
||||
errors.Addf("duplicate alias %s", k)
|
||||
})
|
||||
}
|
||||
|
||||
entries.RangeAll(func(_ string, e *entry.RawEntry) {
|
||||
e.Container.DockerHost = p.dockerHost
|
||||
})
|
||||
|
||||
routes, err = R.FromEntries(entries)
|
||||
errors.Add(err)
|
||||
|
||||
return routes, errors.Build()
|
||||
}
|
||||
|
||||
func (p *DockerProvider) shouldIgnore(container *D.Container) bool {
|
||||
return container.IsExcluded ||
|
||||
!container.IsExplicit && p.ExplicitOnly ||
|
||||
!container.IsExplicit && container.IsDatabase ||
|
||||
strings.HasSuffix(container.ContainerName, "-old")
|
||||
}
|
||||
|
||||
// Returns a list of proxy entries for a container.
|
||||
// Always non-nil.
|
||||
func (p *DockerProvider) entriesFromContainerLabels(container *D.Container) (entries entry.RawEntries, _ E.NestedError) {
|
||||
entries = entry.NewProxyEntries()
|
||||
|
||||
if p.shouldIgnore(container) {
|
||||
return
|
||||
}
|
||||
|
||||
// init entries map for all aliases
|
||||
for _, a := range container.Aliases {
|
||||
entries.Store(a, &entry.RawEntry{
|
||||
Alias: a,
|
||||
Container: container,
|
||||
})
|
||||
}
|
||||
|
||||
errors := E.NewBuilder("failed to apply label")
|
||||
for key, val := range container.Labels {
|
||||
errors.Add(p.applyLabel(container, entries, key, val))
|
||||
}
|
||||
|
||||
// remove all entries that failed to fill in missing fields
|
||||
entries.RangeAll(func(_ string, re *entry.RawEntry) {
|
||||
re.FillMissingFields()
|
||||
})
|
||||
|
||||
return entries, errors.Build().Subject(container.ContainerName)
|
||||
}
|
||||
|
||||
func (p *DockerProvider) applyLabel(container *D.Container, entries entry.RawEntries, key, val string) (res E.NestedError) {
|
||||
b := E.NewBuilder("errors in label %s", key)
|
||||
defer b.To(&res)
|
||||
|
||||
refErr := E.NewBuilder("errors in alias references")
|
||||
replaceIndexRef := 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.OutOfRange("index", ref))
|
||||
return ref
|
||||
}
|
||||
return container.Aliases[index-1]
|
||||
}
|
||||
|
||||
lbl, err := D.ParseLabel(key, val)
|
||||
if err != nil {
|
||||
b.Add(err.Subject(key))
|
||||
}
|
||||
if lbl.Namespace != D.NSProxy {
|
||||
return
|
||||
}
|
||||
if lbl.Target == D.WildcardAlias {
|
||||
// apply label for all aliases
|
||||
entries.RangeAll(func(a string, e *entry.RawEntry) {
|
||||
if err = D.ApplyLabel(e, lbl); err != nil {
|
||||
b.Add(err)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
lbl.Target = AliasRefRegex.ReplaceAllStringFunc(lbl.Target, replaceIndexRef)
|
||||
lbl.Target = AliasRefRegexOld.ReplaceAllStringFunc(lbl.Target, func(s string) string {
|
||||
logrus.Warnf("%q should now be %q, old syntax will be removed in a future version", lbl, strings.ReplaceAll(lbl.String(), "$", "#"))
|
||||
return replaceIndexRef(s)
|
||||
})
|
||||
if refErr.HasError() {
|
||||
b.Add(refErr.Build())
|
||||
return
|
||||
}
|
||||
config, ok := entries.Load(lbl.Target)
|
||||
if !ok {
|
||||
b.Add(E.NotExist("alias", lbl.Target))
|
||||
return
|
||||
}
|
||||
if err = D.ApplyLabel(config, lbl); err != nil {
|
||||
b.Add(err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
374
internal/route/provider/docker_test.go
Normal file
374
internal/route/provider/docker_test.go
Normal file
@@ -0,0 +1,374 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/proxy/entry"
|
||||
T "github.com/yusing/go-proxy/internal/proxy/fields"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
var (
|
||||
dummyNames = []string{"/a"}
|
||||
p DockerProvider
|
||||
)
|
||||
|
||||
func TestApplyLabelWildcard(t *testing.T) {
|
||||
pathPatterns := `
|
||||
- /
|
||||
- POST /upload/{$}
|
||||
- GET /static
|
||||
`[1:]
|
||||
pathPatternsExpect := []string{
|
||||
"/",
|
||||
"POST /upload/{$}",
|
||||
"GET /static",
|
||||
}
|
||||
middlewaresExpect := D.NestedLabelMap{
|
||||
"middleware1": {
|
||||
"prop1": "value1",
|
||||
"prop2": "value2",
|
||||
},
|
||||
"middleware2": {
|
||||
"prop3": "value3",
|
||||
"prop4": "value4",
|
||||
},
|
||||
}
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b",
|
||||
D.LabelIdleTimeout: "",
|
||||
D.LabelStopMethod: common.StopMethodDefault,
|
||||
D.LabelStopSignal: "SIGTERM",
|
||||
D.LabelStopTimeout: common.StopTimeoutDefault,
|
||||
D.LabelWakeTimeout: common.WakeTimeoutDefault,
|
||||
"proxy.*.no_tls_verify": "true",
|
||||
"proxy.*.scheme": "https",
|
||||
"proxy.*.host": "app",
|
||||
"proxy.*.port": "4567",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
"proxy.a.path_patterns": pathPatterns,
|
||||
"proxy.a.middlewares.middleware1.prop1": "value1",
|
||||
"proxy.a.middlewares.middleware1.prop2": "value2",
|
||||
"proxy.a.middlewares.middleware2.prop3": "value3",
|
||||
"proxy.a.middlewares.middleware2.prop4": "value4",
|
||||
},
|
||||
}, client.DefaultDockerHost))
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
a, ok := entries.Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
b, ok := entries.Load("b")
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
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")
|
||||
|
||||
ExpectTrue(t, a.NoTLSVerify)
|
||||
ExpectTrue(t, b.NoTLSVerify)
|
||||
|
||||
ExpectDeepEqual(t, a.PathPatterns, pathPatternsExpect)
|
||||
ExpectEqual(t, len(b.PathPatterns), 0)
|
||||
|
||||
ExpectDeepEqual(t, a.Middlewares, middlewaresExpect)
|
||||
ExpectEqual(t, len(b.Middlewares), 0)
|
||||
|
||||
ExpectEqual(t, a.Container.IdleTimeout, "")
|
||||
ExpectEqual(t, b.Container.IdleTimeout, "")
|
||||
|
||||
ExpectEqual(t, a.Container.StopTimeout, common.StopTimeoutDefault)
|
||||
ExpectEqual(t, b.Container.StopTimeout, common.StopTimeoutDefault)
|
||||
|
||||
ExpectEqual(t, a.Container.StopMethod, common.StopMethodDefault)
|
||||
ExpectEqual(t, b.Container.StopMethod, common.StopMethodDefault)
|
||||
|
||||
ExpectEqual(t, a.Container.WakeTimeout, common.WakeTimeoutDefault)
|
||||
ExpectEqual(t, b.Container.WakeTimeout, common.WakeTimeoutDefault)
|
||||
|
||||
ExpectEqual(t, a.Container.StopSignal, "SIGTERM")
|
||||
ExpectEqual(t, b.Container.StopSignal, "SIGTERM")
|
||||
}
|
||||
|
||||
func TestApplyLabelWithAlias(t *testing.T) {
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
State: "running",
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b,c",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
"proxy.a.port": "3333",
|
||||
"proxy.b.port": "1234",
|
||||
"proxy.c.scheme": "https",
|
||||
},
|
||||
}, client.DefaultDockerHost))
|
||||
a, ok := entries.Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
b, ok := entries.Load("b")
|
||||
ExpectTrue(t, ok)
|
||||
c, ok := entries.Load("c")
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
ExpectNoError(t, err.Error())
|
||||
ExpectEqual(t, a.Scheme, "http")
|
||||
ExpectEqual(t, a.Port, "3333")
|
||||
ExpectEqual(t, a.NoTLSVerify, true)
|
||||
ExpectEqual(t, b.Scheme, "http")
|
||||
ExpectEqual(t, b.Port, "1234")
|
||||
ExpectEqual(t, c.Scheme, "https")
|
||||
}
|
||||
|
||||
func TestApplyLabelWithRef(t *testing.T) {
|
||||
entries := Must(p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
State: "running",
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b,c",
|
||||
"proxy.#1.host": "localhost",
|
||||
"proxy.#1.port": "4444",
|
||||
"proxy.#2.port": "9999",
|
||||
"proxy.#3.port": "1111",
|
||||
"proxy.#3.scheme": "https",
|
||||
},
|
||||
}, client.DefaultDockerHost)))
|
||||
a, ok := entries.Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
b, ok := entries.Load("b")
|
||||
ExpectTrue(t, ok)
|
||||
c, ok := entries.Load("c")
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
ExpectEqual(t, a.Scheme, "http")
|
||||
ExpectEqual(t, a.Host, "localhost")
|
||||
ExpectEqual(t, a.Port, "4444")
|
||||
ExpectEqual(t, b.Port, "9999")
|
||||
ExpectEqual(t, c.Scheme, "https")
|
||||
ExpectEqual(t, c.Port, "1111")
|
||||
}
|
||||
|
||||
func TestApplyLabelWithRefIndexError(t *testing.T) {
|
||||
c := D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
State: "running",
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b",
|
||||
"proxy.#1.host": "localhost",
|
||||
"proxy.#4.scheme": "https",
|
||||
},
|
||||
}, "")
|
||||
_, err := p.entriesFromContainerLabels(c)
|
||||
ExpectError(t, E.ErrOutOfRange, err.Error())
|
||||
ExpectTrue(t, strings.Contains(err.String(), "index out of range"))
|
||||
|
||||
_, err = p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
State: "running",
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b",
|
||||
"proxy.#0.host": "localhost",
|
||||
},
|
||||
}, ""))
|
||||
ExpectError(t, E.ErrOutOfRange, err.Error())
|
||||
ExpectTrue(t, strings.Contains(err.String(), "index out of range"))
|
||||
}
|
||||
|
||||
func TestPublicIPLocalhost(t *testing.T) {
|
||||
c := D.FromDocker(&types.Container{Names: dummyNames, State: "running"}, client.DefaultDockerHost)
|
||||
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
ExpectEqual(t, raw.Container.PublicIP, "127.0.0.1")
|
||||
ExpectEqual(t, raw.Host, raw.Container.PublicIP)
|
||||
}
|
||||
|
||||
func TestPublicIPRemote(t *testing.T) {
|
||||
c := D.FromDocker(&types.Container{Names: dummyNames, State: "running"}, "tcp://1.2.3.4:2375")
|
||||
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
ExpectEqual(t, raw.Container.PublicIP, "1.2.3.4")
|
||||
ExpectEqual(t, raw.Host, raw.Container.PublicIP)
|
||||
}
|
||||
|
||||
func TestPrivateIPLocalhost(t *testing.T) {
|
||||
c := D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
NetworkSettings: &types.SummaryNetworkSettings{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"network": {
|
||||
IPAddress: "172.17.0.123",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, client.DefaultDockerHost)
|
||||
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
ExpectEqual(t, raw.Container.PrivateIP, "172.17.0.123")
|
||||
ExpectEqual(t, raw.Host, raw.Container.PrivateIP)
|
||||
}
|
||||
|
||||
func TestPrivateIPRemote(t *testing.T) {
|
||||
c := D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
State: "running",
|
||||
NetworkSettings: &types.SummaryNetworkSettings{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"network": {
|
||||
IPAddress: "172.17.0.123",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, "tcp://1.2.3.4:2375")
|
||||
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
ExpectEqual(t, raw.Container.PrivateIP, "")
|
||||
ExpectEqual(t, raw.Container.PublicIP, "1.2.3.4")
|
||||
ExpectEqual(t, raw.Host, raw.Container.PublicIP)
|
||||
}
|
||||
|
||||
func TestStreamDefaultValues(t *testing.T) {
|
||||
privPort := uint16(1234)
|
||||
pubPort := uint16(4567)
|
||||
privIP := "172.17.0.123"
|
||||
cont := &types.Container{
|
||||
Names: []string{"a"},
|
||||
State: "running",
|
||||
NetworkSettings: &types.SummaryNetworkSettings{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"network": {
|
||||
IPAddress: privIP,
|
||||
},
|
||||
},
|
||||
},
|
||||
Ports: []types.Port{
|
||||
{Type: "udp", PrivatePort: privPort, PublicPort: pubPort},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("local", func(t *testing.T) {
|
||||
c := D.FromDocker(cont, client.DefaultDockerHost)
|
||||
|
||||
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
en := Must(entry.ValidateEntry(raw))
|
||||
a := ExpectType[*entry.StreamEntry](t, en)
|
||||
ExpectEqual(t, a.Scheme.ListeningScheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, a.Scheme.ProxyScheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, a.Host, T.Host(privIP))
|
||||
ExpectEqual(t, a.Port.ListeningPort, 0)
|
||||
ExpectEqual(t, a.Port.ProxyPort, T.Port(privPort))
|
||||
})
|
||||
|
||||
t.Run("remote", func(t *testing.T) {
|
||||
c := D.FromDocker(cont, "tcp://1.2.3.4:2375")
|
||||
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
en := Must(entry.ValidateEntry(raw))
|
||||
a := ExpectType[*entry.StreamEntry](t, en)
|
||||
ExpectEqual(t, a.Scheme.ListeningScheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, a.Scheme.ProxyScheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, a.Host, "1.2.3.4")
|
||||
ExpectEqual(t, a.Port.ListeningPort, 0)
|
||||
ExpectEqual(t, a.Port.ProxyPort, T.Port(pubPort))
|
||||
})
|
||||
}
|
||||
|
||||
func TestExplicitExclude(t *testing.T) {
|
||||
_, ok := Must(p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a",
|
||||
D.LabelExclude: "true",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
},
|
||||
}, ""))).Load("a")
|
||||
ExpectFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestImplicitExcludeDatabase(t *testing.T) {
|
||||
t.Run("mount path detection", func(t *testing.T) {
|
||||
_, ok := Must(p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Mounts: []types.MountPoint{
|
||||
{Source: "/data", Destination: "/var/lib/postgresql/data"},
|
||||
},
|
||||
}, ""))).Load("a")
|
||||
ExpectFalse(t, ok)
|
||||
})
|
||||
t.Run("exposed port detection", func(t *testing.T) {
|
||||
_, ok := Must(p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 5432, PublicPort: 5432},
|
||||
},
|
||||
}, ""))).Load("a")
|
||||
ExpectFalse(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
// func TestImplicitExcludeNoExposedPort(t *testing.T) {
|
||||
// var p DockerProvider
|
||||
// entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
// Image: "redis",
|
||||
// Names: []string{"redis"},
|
||||
// Ports: []types.Port{
|
||||
// {Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
// },
|
||||
// State: "running",
|
||||
// }, ""))
|
||||
// ExpectNoError(t, err.Error())
|
||||
|
||||
// _, ok := entries.Load("redis")
|
||||
// ExpectFalse(t, ok)
|
||||
// }
|
||||
|
||||
// func TestNotExcludeSpecifiedPort(t *testing.T) {
|
||||
// var p DockerProvider
|
||||
// entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
// Image: "redis",
|
||||
// Names: []string{"redis"},
|
||||
// Ports: []types.Port{
|
||||
// {Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
// },
|
||||
// Labels: map[string]string{
|
||||
// "proxy.redis.port": "6379:6379", // but specified in label
|
||||
// },
|
||||
// }, ""))
|
||||
// ExpectNoError(t, err.Error())
|
||||
|
||||
// _, ok := entries.Load("redis")
|
||||
// ExpectTrue(t, ok)
|
||||
// }
|
||||
|
||||
// func TestNotExcludeNonExposedPortHostNetwork(t *testing.T) {
|
||||
// var p DockerProvider
|
||||
// cont := &types.Container{
|
||||
// Image: "redis",
|
||||
// Names: []string{"redis"},
|
||||
// Ports: []types.Port{
|
||||
// {Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
// },
|
||||
// Labels: map[string]string{
|
||||
// "proxy.redis.port": "6379:6379",
|
||||
// },
|
||||
// }
|
||||
// cont.HostConfig.NetworkMode = "host"
|
||||
|
||||
// entries, err := p.entriesFromContainerLabels(D.FromDocker(cont, ""))
|
||||
// ExpectNoError(t, err.Error())
|
||||
|
||||
// _, ok := entries.Load("redis")
|
||||
// ExpectTrue(t, ok)
|
||||
// }
|
||||
109
internal/route/provider/event_handler.go
Normal file
109
internal/route/provider/event_handler.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/watcher"
|
||||
)
|
||||
|
||||
type EventHandler struct {
|
||||
provider *Provider
|
||||
|
||||
added []string
|
||||
removed []string
|
||||
paused []string
|
||||
updated []string
|
||||
errs E.Builder
|
||||
}
|
||||
|
||||
func (provider *Provider) newEventHandler() *EventHandler {
|
||||
return &EventHandler{
|
||||
provider: provider,
|
||||
errs: E.NewBuilder("event errors"),
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *EventHandler) Handle(parent task.Task, events []watcher.Event) {
|
||||
oldRoutes := handler.provider.routes
|
||||
newRoutes, err := handler.provider.LoadRoutesImpl()
|
||||
if err != nil {
|
||||
handler.errs.Add(err.Subject("load routes"))
|
||||
return
|
||||
}
|
||||
|
||||
oldRoutes.RangeAll(func(k string, v *route.Route) {
|
||||
if !newRoutes.Has(k) {
|
||||
handler.Remove(v)
|
||||
}
|
||||
})
|
||||
newRoutes.RangeAll(func(k string, newr *route.Route) {
|
||||
if oldRoutes.Has(k) {
|
||||
for _, ev := range events {
|
||||
if handler.match(ev, newr) {
|
||||
old, ok := oldRoutes.Load(k)
|
||||
if !ok { // should not happen
|
||||
panic("race condition")
|
||||
}
|
||||
handler.Update(parent, old, newr)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handler.Add(parent, newr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (handler *EventHandler) match(event watcher.Event, route *route.Route) bool {
|
||||
switch handler.provider.t {
|
||||
case ProviderTypeDocker:
|
||||
return route.Entry.Container.ContainerID == event.ActorID ||
|
||||
route.Entry.Container.ContainerName == event.ActorName
|
||||
case ProviderTypeFile:
|
||||
return true
|
||||
}
|
||||
// should never happen
|
||||
return false
|
||||
}
|
||||
|
||||
func (handler *EventHandler) Add(parent task.Task, route *route.Route) {
|
||||
err := handler.provider.startRoute(parent, route)
|
||||
if err != nil {
|
||||
handler.errs.Add(err)
|
||||
} else {
|
||||
handler.added = append(handler.added, route.Entry.Alias)
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *EventHandler) Remove(route *route.Route) {
|
||||
route.Finish("route removal")
|
||||
handler.removed = append(handler.removed, route.Entry.Alias)
|
||||
}
|
||||
|
||||
func (handler *EventHandler) Update(parent task.Task, oldRoute *route.Route, newRoute *route.Route) {
|
||||
oldRoute.Finish("route update")
|
||||
err := handler.provider.startRoute(parent, newRoute)
|
||||
if err != nil {
|
||||
handler.errs.Add(err)
|
||||
} else {
|
||||
handler.updated = append(handler.updated, newRoute.Entry.Alias)
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *EventHandler) Log() {
|
||||
results := E.NewBuilder("event occured")
|
||||
for _, alias := range handler.added {
|
||||
results.Addf("added %s", alias)
|
||||
}
|
||||
for _, alias := range handler.removed {
|
||||
results.Addf("removed %s", alias)
|
||||
}
|
||||
for _, alias := range handler.updated {
|
||||
results.Addf("updated %s", alias)
|
||||
}
|
||||
results.Add(handler.errs.Build())
|
||||
if result := results.Build(); result != nil {
|
||||
handler.provider.l.Info(result)
|
||||
}
|
||||
}
|
||||
69
internal/route/provider/file.go
Normal file
69
internal/route/provider/file.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/proxy/entry"
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
)
|
||||
|
||||
type FileProvider struct {
|
||||
fileName string
|
||||
path string
|
||||
}
|
||||
|
||||
func FileProviderImpl(filename string) (ProviderImpl, E.NestedError) {
|
||||
impl := &FileProvider{
|
||||
fileName: filename,
|
||||
path: path.Join(common.ConfigBasePath, filename),
|
||||
}
|
||||
_, err := os.Stat(impl.path)
|
||||
switch {
|
||||
case err == nil:
|
||||
return impl, nil
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
return nil, E.NotExist("file", impl.path)
|
||||
default:
|
||||
return nil, E.UnexpectedError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Validate(data []byte) E.NestedError {
|
||||
return U.ValidateYaml(U.GetSchema(common.FileProviderSchemaPath), data)
|
||||
}
|
||||
|
||||
func (p FileProvider) String() string {
|
||||
return p.fileName
|
||||
}
|
||||
|
||||
func (p *FileProvider) LoadRoutesImpl() (routes R.Routes, res E.NestedError) {
|
||||
routes = R.NewRoutes()
|
||||
|
||||
b := E.NewBuilder("file %q validation failure", p.fileName)
|
||||
defer b.To(&res)
|
||||
|
||||
entries := entry.NewProxyEntries()
|
||||
|
||||
data, err := E.Check(os.ReadFile(p.path))
|
||||
if err != nil {
|
||||
b.Add(E.FailWith("read file", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = entries.UnmarshalFromYAML(data); err != nil {
|
||||
b.Add(err)
|
||||
return
|
||||
}
|
||||
|
||||
return R.FromEntries(entries)
|
||||
}
|
||||
|
||||
func (p *FileProvider) NewWatcher() W.Watcher {
|
||||
return W.NewConfigFileWatcher(p.fileName)
|
||||
}
|
||||
188
internal/route/provider/provider.go
Normal file
188
internal/route/provider/provider.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||
)
|
||||
|
||||
type (
|
||||
Provider struct {
|
||||
ProviderImpl `json:"-"`
|
||||
|
||||
name string
|
||||
t ProviderType
|
||||
routes R.Routes
|
||||
|
||||
watcher W.Watcher
|
||||
|
||||
l *logrus.Entry
|
||||
}
|
||||
ProviderImpl interface {
|
||||
fmt.Stringer
|
||||
NewWatcher() W.Watcher
|
||||
LoadRoutesImpl() (R.Routes, E.NestedError)
|
||||
}
|
||||
ProviderType string
|
||||
ProviderStats struct {
|
||||
NumRPs int `json:"num_reverse_proxies"`
|
||||
NumStreams int `json:"num_streams"`
|
||||
Type ProviderType `json:"type"`
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
ProviderTypeDocker ProviderType = "docker"
|
||||
ProviderTypeFile ProviderType = "file"
|
||||
|
||||
providerEventFlushInterval = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
func newProvider(name string, t ProviderType) *Provider {
|
||||
p := &Provider{
|
||||
name: name,
|
||||
t: t,
|
||||
routes: R.NewRoutes(),
|
||||
}
|
||||
p.l = logrus.WithField("provider", p)
|
||||
return p
|
||||
}
|
||||
|
||||
func NewFileProvider(filename string) (p *Provider, err E.NestedError) {
|
||||
name := path.Base(filename)
|
||||
if name == "" {
|
||||
return nil, E.Invalid("file name", "empty")
|
||||
}
|
||||
p = newProvider(name, ProviderTypeFile)
|
||||
p.ProviderImpl, err = FileProviderImpl(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.watcher = p.NewWatcher()
|
||||
return
|
||||
}
|
||||
|
||||
func NewDockerProvider(name string, dockerHost string) (p *Provider, err E.NestedError) {
|
||||
if name == "" {
|
||||
return nil, E.Invalid("provider name", "empty")
|
||||
}
|
||||
|
||||
p = newProvider(name, ProviderTypeDocker)
|
||||
p.ProviderImpl, err = DockerProviderImpl(name, dockerHost, p.IsExplicitOnly())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.watcher = p.NewWatcher()
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Provider) IsExplicitOnly() bool {
|
||||
return p.name[len(p.name)-1] == '!'
|
||||
}
|
||||
|
||||
func (p *Provider) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p *Provider) GetType() ProviderType {
|
||||
return p.t
|
||||
}
|
||||
|
||||
// to work with json marshaller.
|
||||
func (p *Provider) MarshalText() ([]byte, error) {
|
||||
return []byte(p.String()), nil
|
||||
}
|
||||
|
||||
func (p *Provider) startRoute(parent task.Task, r *R.Route) E.NestedError {
|
||||
subtask := parent.Subtask("route %s", r.Entry.Alias)
|
||||
err := r.Start(subtask)
|
||||
if err != nil {
|
||||
p.routes.Delete(r.Entry.Alias)
|
||||
subtask.Finish(err.String()) // just to ensure
|
||||
return err
|
||||
} else {
|
||||
subtask.OnComplete("del from provider", func() {
|
||||
p.routes.Delete(r.Entry.Alias)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start implements task.TaskStarter.
|
||||
func (p *Provider) Start(configSubtask task.Task) (res E.NestedError) {
|
||||
errors := E.NewBuilder("errors starting routes")
|
||||
defer errors.To(&res)
|
||||
|
||||
// routes and event queue will stop on parent cancel
|
||||
providerTask := configSubtask
|
||||
|
||||
p.routes.RangeAllParallel(func(alias string, r *R.Route) {
|
||||
errors.Add(p.startRoute(providerTask, r))
|
||||
})
|
||||
|
||||
eventQueue := events.NewEventQueue(
|
||||
providerTask,
|
||||
providerEventFlushInterval,
|
||||
func(flushTask task.Task, events []events.Event) {
|
||||
handler := p.newEventHandler()
|
||||
// routes' lifetime should follow the provider's lifetime
|
||||
handler.Handle(providerTask, events)
|
||||
handler.Log()
|
||||
flushTask.Finish("events flushed")
|
||||
},
|
||||
func(err E.NestedError) {
|
||||
p.l.Error(err)
|
||||
},
|
||||
)
|
||||
eventQueue.Start(p.watcher.Events(providerTask.Context()))
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Provider) RangeRoutes(do func(string, *R.Route)) {
|
||||
p.routes.RangeAll(do)
|
||||
}
|
||||
|
||||
func (p *Provider) GetRoute(alias string) (*R.Route, bool) {
|
||||
return p.routes.Load(alias)
|
||||
}
|
||||
|
||||
func (p *Provider) LoadRoutes() E.NestedError {
|
||||
var err E.NestedError
|
||||
p.routes, err = p.LoadRoutesImpl()
|
||||
if p.routes.Size() > 0 {
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return E.FailWith("loading routes", err)
|
||||
}
|
||||
|
||||
func (p *Provider) NumRoutes() int {
|
||||
return p.routes.Size()
|
||||
}
|
||||
|
||||
func (p *Provider) Statistics() ProviderStats {
|
||||
numRPs := 0
|
||||
numStreams := 0
|
||||
p.routes.RangeAll(func(_ string, r *R.Route) {
|
||||
switch r.Type {
|
||||
case R.RouteTypeReverseProxy:
|
||||
numRPs++
|
||||
case R.RouteTypeStream:
|
||||
numStreams++
|
||||
}
|
||||
})
|
||||
return ProviderStats{
|
||||
NumRPs: numRPs,
|
||||
NumStreams: numStreams,
|
||||
Type: p.t,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user