Compare commits

..

3 Commits

Author SHA1 Message Date
yusing
64e30f59e8 added missing json tags 2024-10-11 10:38:38 +08:00
yusing
cef7b3d396 updated tests for new behavior 2024-10-11 10:00:10 +08:00
yusing
7184c9cfe9 correcting some behaviors for $DOCKER_HOST, now uses container's private IP instead of localhost 2024-10-11 09:13:38 +08:00
15 changed files with 404 additions and 278 deletions

View File

@@ -21,29 +21,21 @@ type Client struct {
l logrus.FieldLogger
}
func ParseDockerHostname(host string) (string, E.NestedError) {
switch host {
case common.DockerHostFromEnv, "":
return "localhost", nil
}
url, err := E.Check(client.ParseHostURL(host))
if err != nil {
return "", E.Invalid("host", host).With(err)
}
return url.Hostname(), nil
}
var (
clientMap F.Map[string, Client] = F.NewMapOf[string, Client]()
clientMapMu sync.Mutex
func (c Client) DaemonHostname() string {
// DaemonHost should always return a valid host
hostname, _ := ParseDockerHostname(c.DaemonHost())
return hostname
}
clientOptEnvHost = []client.Opt{
client.WithHostFromEnv(),
client.WithAPIVersionNegotiation(),
}
)
func (c Client) Connected() bool {
return c.Client != nil
}
// if the client is still referenced, this is no-op
// if the client is still referenced, this is no-op.
func (c *Client) Close() error {
if c.refCount.Add(-1) > 0 {
return nil
@@ -86,6 +78,8 @@ func ConnectClient(host string) (Client, E.NestedError) {
var opt []client.Opt
switch host {
case "":
return Client{}, E.Invalid("docker host", "empty")
case common.DockerHostFromEnv:
opt = clientOptEnvHost
default:
@@ -139,15 +133,3 @@ func CloseAllClients() {
clientMap.Clear()
logger.Debug("closed all clients")
}
var (
clientMap F.Map[string, Client] = F.NewMapOf[string, Client]()
clientMapMu sync.Mutex
clientOptEnvHost = []client.Opt{
client.WithHostFromEnv(),
client.WithAPIVersionNegotiation(),
}
logger = logrus.WithField("module", "docker")
)

View File

@@ -1,44 +1,78 @@
package docker
import (
"net/url"
"strconv"
"strings"
"github.com/docker/docker/api/types"
"github.com/sirupsen/logrus"
U "github.com/yusing/go-proxy/internal/utils"
)
type Container struct {
*types.Container
*ProxyProperties
}
type (
PortMapping = map[string]types.Port
Container struct {
_ U.NoCopy
func FromDocker(c *types.Container, dockerHost string) (res Container) {
res.Container = c
isExplicit := c.Labels[LabelAliases] != ""
res.ProxyProperties = &ProxyProperties{
DockerHost: dockerHost,
ContainerName: res.getName(),
ContainerID: c.ID,
ImageName: res.getImageName(),
PublicPortMapping: res.getPublicPortMapping(),
PrivatePortMapping: res.getPrivatePortMapping(),
NetworkMode: c.HostConfig.NetworkMode,
Aliases: res.getAliases(),
IsExcluded: U.ParseBool(res.getDeleteLabel(LabelExclude)),
IsExplicit: isExplicit,
IsDatabase: res.isDatabase(),
IdleTimeout: res.getDeleteLabel(LabelIdleTimeout),
WakeTimeout: res.getDeleteLabel(LabelWakeTimeout),
StopMethod: res.getDeleteLabel(LabelStopMethod),
StopTimeout: res.getDeleteLabel(LabelStopTimeout),
StopSignal: res.getDeleteLabel(LabelStopSignal),
Running: c.Status == "running" || c.State == "running",
DockerHost string `json:"docker_host" yaml:"-"`
ContainerName string `json:"container_name" yaml:"-"`
ContainerID string `json:"container_id" yaml:"-"`
ImageName string `json:"image_name" yaml:"-"`
Labels map[string]string `json:"labels" yaml:"-"`
PublicPortMapping PortMapping `json:"public_ports" yaml:"-"` // non-zero publicPort:types.Port
PrivatePortMapping PortMapping `json:"private_ports" yaml:"-"` // privatePort:types.Port
PublicIP string `json:"public_ip" yaml:"-"`
PrivateIP string `json:"private_ip" yaml:"-"`
NetworkMode string `json:"network_mode" yaml:"-"`
Aliases []string `json:"aliases" yaml:"-"`
IsExcluded bool `json:"is_excluded" yaml:"-"`
IsExplicit bool `json:"is_explicit" yaml:"-"`
IsDatabase bool `json:"is_database" yaml:"-"`
IdleTimeout string `json:"idle_timeout" yaml:"-"`
WakeTimeout string `json:"wake_timeout" yaml:"-"`
StopMethod string `json:"stop_method" yaml:"-"`
StopTimeout string `json:"stop_timeout" yaml:"-"` // stop_method = "stop" only
StopSignal string `json:"stop_signal" yaml:"-"` // stop_method = "stop" | "kill" only
Running bool `json:"running" yaml:"-"`
}
)
func FromDocker(c *types.Container, dockerHost string) (res *Container) {
isExplicit := c.Labels[LabelAliases] != ""
helper := containerHelper{c}
res = &Container{
DockerHost: dockerHost,
ContainerName: helper.getName(),
ContainerID: c.ID,
ImageName: helper.getImageName(),
Labels: c.Labels,
PublicPortMapping: helper.getPublicPortMapping(),
PrivatePortMapping: helper.getPrivatePortMapping(),
NetworkMode: c.HostConfig.NetworkMode,
Aliases: helper.getAliases(),
IsExcluded: U.ParseBool(helper.getDeleteLabel(LabelExclude)),
IsExplicit: isExplicit,
IsDatabase: helper.isDatabase(),
IdleTimeout: helper.getDeleteLabel(LabelIdleTimeout),
WakeTimeout: helper.getDeleteLabel(LabelWakeTimeout),
StopMethod: helper.getDeleteLabel(LabelStopMethod),
StopTimeout: helper.getDeleteLabel(LabelStopTimeout),
StopSignal: helper.getDeleteLabel(LabelStopSignal),
Running: c.Status == "running" || c.State == "running",
}
res.setPrivateIP(helper)
res.setPublicIP()
return
}
func FromJSON(json types.ContainerJSON, dockerHost string) Container {
func FromJSON(json types.ContainerJSON, dockerHost string) *Container {
ports := make([]types.Port, 0)
for k, bindings := range json.NetworkSettings.Ports {
for _, v := range bindings {
@@ -64,78 +98,32 @@ func FromJSON(json types.ContainerJSON, dockerHost string) Container {
return cont
}
func (c Container) getDeleteLabel(label string) string {
if l, ok := c.Labels[label]; ok {
delete(c.Labels, label)
return l
func (c *Container) setPublicIP() {
if c.PublicPortMapping == nil {
return
}
return ""
}
func (c Container) getAliases() []string {
if l := c.getDeleteLabel(LabelAliases); l != "" {
return U.CommaSeperatedList(l)
if strings.HasPrefix(c.DockerHost, "unix://") {
c.PublicIP = "127.0.0.1"
return
}
return []string{c.getName()}
}
func (c Container) getName() string {
return strings.TrimPrefix(c.Names[0], "/")
}
func (c Container) getImageName() string {
colonSep := strings.Split(c.Image, ":")
slashSep := strings.Split(colonSep[0], "/")
return slashSep[len(slashSep)-1]
}
func (c Container) getPublicPortMapping() PortMapping {
res := make(PortMapping)
for _, v := range c.Ports {
if v.PublicPort == 0 {
continue
}
res[U.PortString(v.PublicPort)] = v
url, err := url.Parse(c.DockerHost)
if err != nil {
logrus.Errorf("invalid docker host %q: %v\nfalling back to 127.0.0.1", c.DockerHost, err)
c.PublicIP = "127.0.0.1"
return
}
return res
c.PublicIP = url.Hostname()
}
func (c Container) getPrivatePortMapping() PortMapping {
res := make(PortMapping)
for _, v := range c.Ports {
res[U.PortString(v.PrivatePort)] = v
func (c *Container) setPrivateIP(helper containerHelper) {
if !strings.HasPrefix(c.DockerHost, "unix://") {
return
}
return res
}
var databaseMPs = map[string]struct{}{
"/var/lib/postgresql/data": {},
"/var/lib/mysql": {},
"/var/lib/mongodb": {},
"/var/lib/mariadb": {},
"/var/lib/memcached": {},
"/var/lib/rabbitmq": {},
}
var databasePrivPorts = map[uint16]struct{}{
5432: {}, // postgres
3306: {}, // mysql, mariadb
6379: {}, // redis
11211: {}, // memcached
27017: {}, // mongodb
}
func (c Container) isDatabase() bool {
for _, m := range c.Container.Mounts {
if _, ok := databaseMPs[m.Destination]; ok {
return true
}
if helper.NetworkSettings == nil {
return
}
for _, v := range c.Ports {
if _, ok := databasePrivPorts[v.PrivatePort]; ok {
return true
}
for _, v := range helper.NetworkSettings.Networks {
c.PrivateIP = v.IPAddress
return
}
return false
}

View File

@@ -0,0 +1,90 @@
package docker
import (
"strings"
"github.com/docker/docker/api/types"
U "github.com/yusing/go-proxy/internal/utils"
)
type containerHelper struct {
*types.Container
}
// getDeleteLabel gets the value of a label and then deletes it from the container.
// If the label does not exist, an empty string is returned.
func (c containerHelper) getDeleteLabel(label string) string {
if l, ok := c.Labels[label]; ok {
delete(c.Labels, label)
return l
}
return ""
}
func (c containerHelper) getAliases() []string {
if l := c.getDeleteLabel(LabelAliases); l != "" {
return U.CommaSeperatedList(l)
}
return []string{c.getName()}
}
func (c containerHelper) getName() string {
return strings.TrimPrefix(c.Names[0], "/")
}
func (c containerHelper) getImageName() string {
colonSep := strings.Split(c.Image, ":")
slashSep := strings.Split(colonSep[0], "/")
return slashSep[len(slashSep)-1]
}
func (c containerHelper) getPublicPortMapping() PortMapping {
res := make(PortMapping)
for _, v := range c.Ports {
if v.PublicPort == 0 {
continue
}
res[U.PortString(v.PublicPort)] = v
}
return res
}
func (c containerHelper) getPrivatePortMapping() PortMapping {
res := make(PortMapping)
for _, v := range c.Ports {
res[U.PortString(v.PrivatePort)] = v
}
return res
}
var databaseMPs = map[string]struct{}{
"/var/lib/postgresql/data": {},
"/var/lib/mysql": {},
"/var/lib/mongodb": {},
"/var/lib/mariadb": {},
"/var/lib/memcached": {},
"/var/lib/rabbitmq": {},
}
var databasePrivPorts = map[uint16]struct{}{
5432: {}, // postgres
3306: {}, // mysql, mariadb
6379: {}, // redis
11211: {}, // memcached
27017: {}, // mongodb
}
func (c containerHelper) isDatabase() bool {
for _, m := range c.Mounts {
if _, ok := databaseMPs[m.Destination]; ok {
return true
}
}
for _, v := range c.Ports {
if _, ok := databasePrivPorts[v.PrivatePort]; ok {
return true
}
}
return false
}

View File

@@ -7,13 +7,13 @@ import (
E "github.com/yusing/go-proxy/internal/error"
)
func (c Client) Inspect(containerID string) (Container, E.NestedError) {
func (c Client) Inspect(containerID string) (*Container, E.NestedError) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
json, err := c.ContainerInspect(ctx, containerID)
if err != nil {
return Container{}, E.From(err)
return nil, E.From(err)
}
return FromJSON(json, c.key), nil
}

View File

@@ -0,0 +1,5 @@
package docker
import "github.com/sirupsen/logrus"
var logger = logrus.WithField("module", "docker")

View File

@@ -1,27 +0,0 @@
package docker
import "github.com/docker/docker/api/types"
type (
PortMapping = map[string]types.Port
ProxyProperties struct {
DockerHost string `json:"docker_host" yaml:"-"`
ContainerName string `json:"container_name" yaml:"-"`
ContainerID string `json:"container_id" yaml:"-"`
ImageName string `json:"image_name" yaml:"-"`
PublicPortMapping PortMapping `json:"public_ports" yaml:"-"` // non-zero publicPort:types.Port
PrivatePortMapping PortMapping `json:"private_ports" yaml:"-"` // privatePort:types.Port
NetworkMode string `json:"network_mode" yaml:"-"`
Aliases []string `json:"aliases" yaml:"-"`
IsExcluded bool `json:"is_excluded" yaml:"-"`
IsExplicit bool `json:"is_explicit" yaml:"-"`
IsDatabase bool `json:"is_database" yaml:"-"`
IdleTimeout string `json:"idle_timeout" yaml:"-"`
WakeTimeout string `json:"wake_timeout" yaml:"-"`
StopMethod string `json:"stop_method" yaml:"-"`
StopTimeout string `json:"stop_timeout" yaml:"-"` // stop_method = "stop" only
StopSignal string `json:"stop_signal" yaml:"-"` // stop_method = "stop" | "kill" only
Running bool `json:"running" yaml:"-"`
}
)

View File

@@ -1,8 +1,9 @@
package error
package error_test
import (
"testing"
. "github.com/yusing/go-proxy/internal/error"
. "github.com/yusing/go-proxy/internal/utils/testing"
)

View File

@@ -1,9 +1,10 @@
package error
package error_test
import (
"errors"
"testing"
. "github.com/yusing/go-proxy/internal/error"
. "github.com/yusing/go-proxy/internal/utils/testing"
)

View File

@@ -15,24 +15,24 @@ import (
type (
ReverseProxyEntry struct { // real model after validation
Alias T.Alias
Scheme T.Scheme
URL net.URL
NoTLSVerify bool
PathPatterns T.PathPatterns
LoadBalance loadbalancer.Config
Middlewares D.NestedLabelMap
Alias T.Alias `json:"alias"`
Scheme T.Scheme `json:"scheme"`
URL net.URL `json:"url"`
NoTLSVerify bool `json:"no_tls_verify"`
PathPatterns T.PathPatterns `json:"path_patterns"`
LoadBalance loadbalancer.Config `json:"load_balance"`
Middlewares D.NestedLabelMap `json:"middlewares"`
/* Docker only */
IdleTimeout time.Duration
WakeTimeout time.Duration
StopMethod T.StopMethod
StopTimeout int
StopSignal T.Signal
DockerHost string
ContainerName string
ContainerID string
ContainerRunning bool
IdleTimeout time.Duration `json:"idle_timeout"`
WakeTimeout time.Duration `json:"wake_timeout"`
StopMethod T.StopMethod `json:"stop_method"`
StopTimeout int `json:"stop_timeout"`
StopSignal T.Signal `json:"stop_signal"`
DockerHost string `json:"docker_host"`
ContainerName string `json:"container_name"`
ContainerID string `json:"container_id"`
ContainerRunning bool `json:"container_running"`
}
StreamEntry struct {
Alias T.Alias `json:"alias"`

View File

@@ -5,7 +5,9 @@ import (
"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"
R "github.com/yusing/go-proxy/internal/route"
@@ -15,8 +17,8 @@ import (
)
type DockerProvider struct {
name, dockerHost, hostname string
ExplicitOnly bool
name, dockerHost string
ExplicitOnly bool
}
var (
@@ -25,11 +27,10 @@ var (
)
func DockerProviderImpl(name, dockerHost string, explicitOnly bool) (ProviderImpl, E.NestedError) {
hostname, err := D.ParseDockerHostname(dockerHost)
if err.HasError() {
return nil, err
if dockerHost == common.DockerHostFromEnv {
dockerHost = common.GetEnv("DOCKER_HOST", client.DefaultDockerHost)
}
return &DockerProvider{name, dockerHost, hostname, explicitOnly}, nil
return &DockerProvider{name, dockerHost, explicitOnly}, nil
}
func (p *DockerProvider) String() string {
@@ -80,7 +81,7 @@ func (p *DockerProvider) LoadRoutesImpl() (routes R.Routes, err E.NestedError) {
return routes, errors.Build()
}
func (p *DockerProvider) shouldIgnore(container D.Container) bool {
func (p *DockerProvider) shouldIgnore(container *D.Container) bool {
return container.IsExcluded ||
!container.IsExplicit && p.ExplicitOnly ||
!container.IsExplicit && container.IsDatabase ||
@@ -173,7 +174,7 @@ func (p *DockerProvider) OnEvent(event W.Event, routes R.Routes) (res EventResul
// Returns a list of proxy entries for a container.
// Always non-nil.
func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (entries types.RawEntries, _ E.NestedError) {
func (p *DockerProvider) entriesFromContainerLabels(container *D.Container) (entries types.RawEntries, _ E.NestedError) {
entries = types.NewProxyEntries()
if p.shouldIgnore(container) {
@@ -183,9 +184,8 @@ func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (entr
// init entries map for all aliases
for _, a := range container.Aliases {
entries.Store(a, &types.RawEntry{
Alias: a,
Host: p.hostname,
ProxyProperties: container.ProxyProperties,
Alias: a,
Container: container,
})
}
@@ -202,7 +202,7 @@ func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (entr
return entries, errors.Build().Subject(container.ContainerName)
}
func (p *DockerProvider) applyLabel(container D.Container, entries types.RawEntries, key, val string) (res E.NestedError) {
func (p *DockerProvider) applyLabel(container *D.Container, entries types.RawEntries, key, val string) (res E.NestedError) {
b := E.NewBuilder("errors in label %s", key)
defer b.To(&res)

View File

@@ -5,18 +5,20 @@ import (
"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"
P "github.com/yusing/go-proxy/internal/proxy"
T "github.com/yusing/go-proxy/internal/proxy/fields"
. "github.com/yusing/go-proxy/internal/utils/testing"
)
var dummyNames = []string{"/a"}
var p DockerProvider
func TestApplyLabelFieldValidity(t *testing.T) {
func TestApplyLabelWildcard(t *testing.T) {
pathPatterns := `
- /
- POST /upload/{$}
@@ -58,9 +60,7 @@ func TestApplyLabelFieldValidity(t *testing.T) {
"proxy.a.middlewares.middleware2.prop3": "value3",
"proxy.a.middlewares.middleware2.prop4": "value4",
},
Ports: []types.Port{
{Type: "tcp", PrivatePort: 4567, PublicPort: 8888},
}}, ""))
}, ""))
ExpectNoError(t, err.Error())
a, ok := entries.Load("a")
@@ -74,8 +74,8 @@ func TestApplyLabelFieldValidity(t *testing.T) {
ExpectEqual(t, a.Host, "app")
ExpectEqual(t, b.Host, "app")
ExpectEqual(t, a.Port, "8888")
ExpectEqual(t, b.Port, "8888")
ExpectEqual(t, a.Port, "4567")
ExpectEqual(t, b.Port, "4567")
ExpectTrue(t, a.NoTLSVerify)
ExpectTrue(t, b.NoTLSVerify)
@@ -102,8 +102,7 @@ func TestApplyLabelFieldValidity(t *testing.T) {
ExpectEqual(t, b.StopSignal, "SIGTERM")
}
func TestApplyLabel(t *testing.T) {
var p DockerProvider
func TestApplyLabelWithAlias(t *testing.T) {
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
Names: dummyNames,
Labels: map[string]string{
@@ -113,11 +112,7 @@ func TestApplyLabel(t *testing.T) {
"proxy.b.port": "1234",
"proxy.c.scheme": "https",
},
Ports: []types.Port{
{Type: "tcp", PrivatePort: 3333, PublicPort: 1111},
{Type: "tcp", PrivatePort: 4444, PublicPort: 1234},
}}, "",
))
}, ""))
a, ok := entries.Load("a")
ExpectTrue(t, ok)
b, ok := entries.Load("b")
@@ -127,18 +122,15 @@ func TestApplyLabel(t *testing.T) {
ExpectNoError(t, err.Error())
ExpectEqual(t, a.Scheme, "http")
ExpectEqual(t, a.Port, "1111")
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")
// map does not necessary follow the order above
ExpectEqualAny(t, c.Port, []string{"1111", "1234"})
}
func TestApplyLabelWithRef(t *testing.T) {
var p DockerProvider
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
entries := Must(p.entriesFromContainerLabels(D.FromDocker(&types.Container{
Names: dummyNames,
Labels: map[string]string{
D.LabelAliases: "a,b,c",
@@ -148,11 +140,7 @@ func TestApplyLabelWithRef(t *testing.T) {
"proxy.#3.port": "1111",
"proxy.#3.scheme": "https",
},
Ports: []types.Port{
{Type: "tcp", PrivatePort: 3333, PublicPort: 9999},
{Type: "tcp", PrivatePort: 4444, PublicPort: 5555},
{Type: "tcp", PrivatePort: 1111, PublicPort: 2222},
}}, ""))
}, "")))
a, ok := entries.Load("a")
ExpectTrue(t, ok)
b, ok := entries.Load("b")
@@ -160,24 +148,23 @@ func TestApplyLabelWithRef(t *testing.T) {
c, ok := entries.Load("c")
ExpectTrue(t, ok)
ExpectNoError(t, err.Error())
ExpectEqual(t, a.Scheme, "http")
ExpectEqual(t, a.Host, "localhost")
ExpectEqual(t, a.Port, "5555")
ExpectEqual(t, a.Port, "4444")
ExpectEqual(t, b.Port, "9999")
ExpectEqual(t, c.Scheme, "https")
ExpectEqual(t, c.Port, "2222")
ExpectEqual(t, c.Port, "1111")
}
func TestApplyLabelWithRefIndexError(t *testing.T) {
var p DockerProvider
var c = D.FromDocker(&types.Container{
c := D.FromDocker(&types.Container{
Names: dummyNames,
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"))
@@ -187,71 +174,143 @@ func TestApplyLabelWithRefIndexError(t *testing.T) {
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 TestStreamDefaultValues(t *testing.T) {
var p DockerProvider
var c = D.FromDocker(&types.Container{
func TestPublicIPLocalhost(t *testing.T) {
c := D.FromDocker(&types.Container{Names: dummyNames}, client.DefaultDockerHost)
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
ExpectTrue(t, ok)
ExpectEqual(t, raw.PublicIP, "127.0.0.1")
ExpectEqual(t, raw.Host, raw.PublicIP)
}
func TestPublicIPRemote(t *testing.T) {
c := D.FromDocker(&types.Container{Names: dummyNames}, "tcp://1.2.3.4:2375")
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
ExpectTrue(t, ok)
ExpectEqual(t, raw.PublicIP, "1.2.3.4")
ExpectEqual(t, raw.Host, raw.PublicIP)
}
func TestPrivateIPLocalhost(t *testing.T) {
c := D.FromDocker(&types.Container{
Names: dummyNames,
Labels: map[string]string{
D.LabelAliases: "a",
"proxy.*.no_tls_verify": "true",
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.PrivateIP, "172.17.0.123")
ExpectEqual(t, raw.Host, raw.PrivateIP)
}
func TestPrivateIPRemote(t *testing.T) {
c := D.FromDocker(&types.Container{
Names: dummyNames,
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.PrivateIP, "")
ExpectEqual(t, raw.PublicIP, "1.2.3.4")
ExpectEqual(t, raw.Host, raw.PublicIP)
}
func TestStreamDefaultValues(t *testing.T) {
privPort := uint16(1234)
pubPort := uint16(4567)
privIP := "172.17.0.123"
cont := &types.Container{
Names: []string{"a"},
NetworkSettings: &types.SummaryNetworkSettings{
Networks: map[string]*network.EndpointSettings{
"network": {
IPAddress: privIP,
},
},
},
Ports: []types.Port{
{Type: "udp", PrivatePort: 1234, PublicPort: 5678},
}}, "",
)
entries, err := p.entriesFromContainerLabels(c)
ExpectNoError(t, err.Error())
{Type: "udp", PrivatePort: privPort, PublicPort: pubPort},
},
}
raw, ok := entries.Load("a")
ExpectTrue(t, ok)
t.Run("local", func(t *testing.T) {
c := D.FromDocker(cont, client.DefaultDockerHost)
entry, err := P.ValidateEntry(raw)
ExpectNoError(t, err.Error())
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
ExpectTrue(t, ok)
entry := Must(P.ValidateEntry(raw))
a := ExpectType[*P.StreamEntry](t, entry)
ExpectEqual(t, a.Scheme.ListeningScheme, T.Scheme("udp"))
ExpectEqual(t, a.Scheme.ProxyScheme, T.Scheme("udp"))
ExpectEqual(t, a.Port.ListeningPort, 0)
ExpectEqual(t, a.Port.ProxyPort, 5678)
a := ExpectType[*P.StreamEntry](t, entry)
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)
entry := Must(P.ValidateEntry(raw))
a := ExpectType[*P.StreamEntry](t, entry)
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) {
var p DockerProvider
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
_, 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",
}}, ""))
ExpectNoError(t, err.Error())
_, ok := entries.Load("a")
},
}, ""))).Load("a")
ExpectFalse(t, ok)
}
//! Now nothing will be implicit excluded
//
// func TestImplicitExclude(t *testing.T) {
// var p DockerProvider
// entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
// Names: dummyNames,
// Labels: map[string]string{
// D.LabelAliases: "a",
// "proxy.a.no_tls_verify": "true",
// },
// State: "running",
// }, ""))
// ExpectNoError(t, err.Error())
// _, ok := entries.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

View File

@@ -14,6 +14,8 @@ import (
type (
RawEntry struct {
_ U.NoCopy
// raw entry object before validation
// loaded from docker labels or yaml file
Alias string `json:"-" yaml:"-"`
@@ -27,7 +29,7 @@ type (
Homepage *H.HomePageItem `json:"homepage,omitempty" yaml:"homepage"`
/* Docker only */
*D.ProxyProperties `json:"proxy_properties" yaml:"-"`
*D.Container `json:"container" yaml:"-"`
}
RawEntries = F.Map[string, *RawEntry]
@@ -36,9 +38,20 @@ type (
var NewProxyEntries = F.NewMapOf[string, *RawEntry]
func (e *RawEntry) FillMissingFields() {
isDocker := e.ProxyProperties != nil
isDocker := e.Container != nil
if !isDocker {
e.ProxyProperties = &D.ProxyProperties{}
e.Container = &D.Container{}
}
if e.Host == "" {
switch {
case e.PrivateIP != "":
e.Host = e.PrivateIP
case e.PublicIP != "":
e.Host = e.PublicIP
default:
e.Host = "localhost"
}
}
lp, pp, extra := e.splitPorts()
@@ -67,21 +80,24 @@ func (e *RawEntry) FillMissingFields() {
}
}
// replace private port with public port (if any)
if isDocker && e.NetworkMode != "host" {
// replace private port with public port if using public IP.
if e.Host == e.PublicIP {
if p, ok := e.PrivatePortMapping[pp]; ok {
pp = U.PortString(p.PublicPort)
}
if _, ok := e.PublicPortMapping[pp]; !ok { // port is not exposed, but specified
// try to fallback to first public port
if p, ok := F.FirstValueOf(e.PublicPortMapping); ok {
pp = U.PortString(p.PublicPort)
}
}
// replace public port with private port if using private IP.
if e.Host == e.PrivateIP {
if p, ok := e.PublicPortMapping[pp]; ok {
pp = U.PortString(p.PrivatePort)
}
}
if e.Scheme == "" && isDocker {
if p, ok := e.PublicPortMapping[pp]; ok && p.Type == "udp" {
switch {
case e.Host == e.PublicIP && e.PublicPortMapping[pp].Type == "udp":
e.Scheme = "udp"
case e.Host == e.PrivateIP && e.PrivatePortMapping[pp].Type == "udp":
e.Scheme = "udp"
}
}
@@ -92,15 +108,11 @@ func (e *RawEntry) FillMissingFields() {
e.Scheme = "tcp"
case strings.HasSuffix(pp, "443"):
e.Scheme = "https"
default:
// assume its http
default: // assume its http
e.Scheme = "http"
}
}
if e.Host == "" {
e.Host = "localhost"
}
if e.IdleTimeout == "" {
e.IdleTimeout = common.IdleTimeoutDefault
}

View File

@@ -1,8 +0,0 @@
package utils
func Must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}

8
internal/utils/nocopy.go Normal file
View File

@@ -0,0 +1,8 @@
package utils
// empty struct that implements Locker interface
// for hinting that no copy should be performed.
type NoCopy struct{}
func (*NoCopy) Lock() {}
func (*NoCopy) Unlock() {}

View File

@@ -7,6 +7,7 @@ import (
"testing"
"github.com/yusing/go-proxy/internal/common"
E "github.com/yusing/go-proxy/internal/error"
)
func init() {
@@ -96,3 +97,17 @@ func ExpectType[T any](t *testing.T, got any) (_ T) {
}
return got.(T)
}
func Must[T any](v T, err E.NestedError) T {
if err != nil {
panic(err)
}
return v
}
func Must2[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}