mirror of
https://github.com/yusing/godoxy.git
synced 2026-02-14 14:37:41 +01:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64e30f59e8 | ||
|
|
cef7b3d396 | ||
|
|
7184c9cfe9 |
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
90
internal/docker/container_helper.go
Normal file
90
internal/docker/container_helper.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
5
internal/docker/logger.go
Normal file
5
internal/docker/logger.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package docker
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
var logger = logrus.WithField("module", "docker")
|
||||
@@ -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:"-"`
|
||||
}
|
||||
)
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
8
internal/utils/nocopy.go
Normal 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() {}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user