correcting some behaviors for $DOCKER_HOST, now uses container's private IP instead of localhost

This commit is contained in:
yusing
2024-10-11 09:13:38 +08:00
parent d91b66ae87
commit 7184c9cfe9
68 changed files with 925 additions and 570 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

@@ -7,7 +7,6 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
E "github.com/yusing/go-proxy/internal/error"
)

View File

@@ -1,45 +1,78 @@
package docker
import (
"fmt"
"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 {
@@ -65,79 +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 ""
if strings.HasPrefix(c.DockerHost, "unix://") {
c.PublicIP = "127.0.0.1"
return
}
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
}
c.PublicIP = url.Hostname()
}
func (c Container) getAliases() []string {
if l := c.getDeleteLabel(LabelAliases); l != "" {
return U.CommaSeperatedList(l)
} else {
return []string{c.getName()}
func (c *Container) setPrivateIP(helper containerHelper) {
if !strings.HasPrefix(c.DockerHost, "unix://") {
return
}
if helper.NetworkSettings == nil {
return
}
for _, v := range helper.NetworkSettings.Networks {
c.PrivateIP = v.IPAddress
return
}
}
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[fmt.Sprint(v.PublicPort)] = v
}
return res
}
func (c Container) getPrivatePortMapping() PortMapping {
res := make(PortMapping)
for _, v := range c.Ports {
res[fmt.Sprint(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 Container) isDatabase() bool {
for _, m := range c.Container.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

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

@@ -18,9 +18,9 @@ type templateData struct {
var loadingPage []byte
var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage)))
const headerCheckRedirect = "X-GoProxy-Check-Redirect"
const headerCheckRedirect = "X-Goproxy-Check-Redirect"
func (w *watcher) makeRespBody(format string, args ...any) []byte {
func (w *Watcher) makeRespBody(format string, args ...any) []byte {
msg := fmt.Sprintf(format, args...)
data := new(templateData)

View File

@@ -11,13 +11,13 @@ import (
)
type Waker struct {
*watcher
*Watcher
client *http.Client
rp *gphttp.ReverseProxy
}
func NewWaker(w *watcher, rp *gphttp.ReverseProxy) *Waker {
func NewWaker(w *Watcher, rp *gphttp.ReverseProxy) *Waker {
orig := rp.ServeHTTP
// workaround for stopped containers port become zero
rp.ServeHTTP = func(rw http.ResponseWriter, r *http.Request) {
@@ -33,7 +33,7 @@ func NewWaker(w *watcher, rp *gphttp.ReverseProxy) *Waker {
orig(rw, r)
}
return &Waker{
watcher: w,
Watcher: w,
client: &http.Client{
Timeout: 1 * time.Second,
Transport: rp.Transport,
@@ -70,7 +70,9 @@ func (w *Waker) wake(next http.HandlerFunc, rw http.ResponseWriter, r *http.Requ
rw.Header().Add("Cache-Control", "no-cache")
rw.Header().Add("Cache-Control", "no-store")
rw.Header().Add("Cache-Control", "must-revalidate")
rw.Write(body)
if _, err := rw.Write(body); err != nil {
w.l.Errorf("error writing http response: %s", err)
}
return
}

View File

@@ -17,7 +17,7 @@ import (
)
type (
watcher struct {
Watcher struct {
*P.ReverseProxyEntry
client D.Client
@@ -46,17 +46,17 @@ var (
mainLoopCancel context.CancelFunc
mainLoopWg sync.WaitGroup
watcherMap = F.NewMapOf[string, *watcher]()
watcherMap = F.NewMapOf[string, *Watcher]()
watcherMapMu sync.Mutex
portHistoryMap = F.NewMapOf[PT.Alias, string]()
newWatcherCh = make(chan *watcher)
newWatcherCh = make(chan *Watcher)
logger = logrus.WithField("module", "idle_watcher")
)
func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
func Register(entry *P.ReverseProxyEntry) (*Watcher, E.NestedError) {
failure := E.Failure("idle_watcher register")
if entry.IdleTimeout == 0 {
@@ -83,7 +83,7 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
return nil, failure.With(err)
}
w := &watcher{
w := &Watcher{
ReverseProxyEntry: entry,
client: client,
refCount: &sync.WaitGroup{},
@@ -104,7 +104,7 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
return w, nil
}
func (w *watcher) Unregister() {
func (w *Watcher) Unregister() {
w.refCount.Add(-1)
}
@@ -138,29 +138,30 @@ func Stop() {
mainLoopWg.Wait()
}
func (w *watcher) containerStop() error {
func (w *Watcher) containerStop() error {
return w.client.ContainerStop(w.ctx, w.ContainerID, container.StopOptions{
Signal: string(w.StopSignal),
Timeout: &w.StopTimeout})
Timeout: &w.StopTimeout,
})
}
func (w *watcher) containerPause() error {
func (w *Watcher) containerPause() error {
return w.client.ContainerPause(w.ctx, w.ContainerID)
}
func (w *watcher) containerKill() error {
func (w *Watcher) containerKill() error {
return w.client.ContainerKill(w.ctx, w.ContainerID, string(w.StopSignal))
}
func (w *watcher) containerUnpause() error {
func (w *Watcher) containerUnpause() error {
return w.client.ContainerUnpause(w.ctx, w.ContainerID)
}
func (w *watcher) containerStart() error {
func (w *Watcher) containerStart() error {
return w.client.ContainerStart(w.ctx, w.ContainerID, container.StartOptions{})
}
func (w *watcher) containerStatus() (string, E.NestedError) {
func (w *Watcher) containerStatus() (string, E.NestedError) {
json, err := w.client.ContainerInspect(w.ctx, w.ContainerID)
if err != nil {
return "", E.FailWith("inspect container", err)
@@ -168,7 +169,7 @@ func (w *watcher) containerStatus() (string, E.NestedError) {
return json.State.Status, nil
}
func (w *watcher) wakeIfStopped() E.NestedError {
func (w *Watcher) wakeIfStopped() E.NestedError {
if w.ready.Load() || w.ContainerRunning {
return nil
}
@@ -191,7 +192,7 @@ func (w *watcher) wakeIfStopped() E.NestedError {
}
}
func (w *watcher) getStopCallback() StopCallback {
func (w *Watcher) getStopCallback() StopCallback {
var cb func() error
switch w.StopMethod {
case PT.StopMethodPause:
@@ -215,11 +216,11 @@ func (w *watcher) getStopCallback() StopCallback {
}
}
func (w *watcher) resetIdleTimer() {
func (w *Watcher) resetIdleTimer() {
w.ticker.Reset(w.IdleTimeout)
}
func (w *watcher) watchUntilCancel() {
func (w *Watcher) watchUntilCancel() {
defer close(w.wakeCh)
w.ctx, w.cancel = context.WithCancel(mainLoopCtx)

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
return FromJSON(json, c.key), nil
}

View File

@@ -47,7 +47,7 @@ func ApplyLabel[T any](obj *T, l *Label) E.NestedError {
case *Label:
var field reflect.Value
objType := reflect.TypeFor[T]()
for i := 0; i < reflect.TypeFor[T]().NumField(); i++ {
for i := range reflect.TypeFor[T]().NumField() {
if objType.Field(i).Tag.Get("yaml") == l.Attribute {
field = reflect.ValueOf(obj).Elem().Field(i)
break

View File

@@ -8,14 +8,18 @@ import (
. "github.com/yusing/go-proxy/internal/utils/testing"
)
const (
mName = "middleware1"
mAttr = "prop1"
v = "value1"
)
func makeLabel(ns, name, attr string) string {
return fmt.Sprintf("%s.%s.%s", ns, name, attr)
}
func TestNestedLabel(t *testing.T) {
mName := "middleware1"
mAttr := "prop1"
v := "value1"
pl, err := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v)
ExpectNoError(t, err.Error())
sGot := ExpectType[*Label](t, pl.Value)
@@ -28,9 +32,6 @@ func TestApplyNestedLabel(t *testing.T) {
entry := new(struct {
Middlewares NestedLabelMap `yaml:"middlewares"`
})
mName := "middleware1"
mAttr := "prop1"
v := "value1"
pl, err := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v)
ExpectNoError(t, err.Error())
err = ApplyLabel(entry, pl)
@@ -42,10 +43,6 @@ func TestApplyNestedLabel(t *testing.T) {
}
func TestApplyNestedLabelExisting(t *testing.T) {
mName := "middleware1"
mAttr := "prop1"
v := "value1"
checkAttr := "prop2"
checkV := "value2"
entry := new(struct {
@@ -71,9 +68,6 @@ func TestApplyNestedLabelExisting(t *testing.T) {
}
func TestApplyNestedLabelNoAttr(t *testing.T) {
mName := "middleware1"
v := "value1"
entry := new(struct {
Middlewares NestedLabelMap `yaml:"middlewares"`
})

View File

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

View File

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