Compare commits

..

3 Commits

36 changed files with 421 additions and 560 deletions

View File

@@ -1,4 +1,4 @@
FROM golang:1.22.6-alpine as builder FROM golang:1.22.6-alpine AS builder
COPY src /src COPY src /src
ENV GOCACHE=/root/.cache/go-build ENV GOCACHE=/root/.cache/go-build
WORKDIR /src WORKDIR /src

View File

@@ -12,7 +12,7 @@ build:
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy github.com/yusing/go-proxy CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy github.com/yusing/go-proxy
test: test:
cd src && go test && cd .. cd src && go test ./... && cd ..
up: up:
docker compose up -d docker compose up -d

View File

@@ -16,7 +16,6 @@ A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse pr
- [Provider File](#provider-file) - [Provider File](#provider-file)
- [Known issues](#known-issues) - [Known issues](#known-issues)
- [Build it yourself](#build-it-yourself) - [Build it yourself](#build-it-yourself)
<!-- /TOC -->
## Key Points ## Key Points
@@ -81,12 +80,14 @@ autocert:
- ... - ...
# reverse proxy providers configuration # reverse proxy providers configuration
providers: providers:
entry_1: include:
kind: docker - providers.yml
value: # `FROM_ENV` or full url to docker host - other_file_1.yml
entry_2: - ...
kind: file docker:
value: # relative path of file to `config/` local: $DOCKER_HOST
remote-1: tcp://10.0.2.1:2375
remote-2: ssh://root:1234@10.0.2.2
``` ```
[🔼Back to top](#table-of-content) [🔼Back to top](#table-of-content)

View File

@@ -11,22 +11,26 @@
# provider: cloudflare # provider: cloudflare
# email: # ACME Email # email: # ACME Email
# domains: # a list of domains for cert registration # domains: # a list of domains for cert registration
# - # - x.y.z
# options: # options:
# - auth_token: # your zone API token # - auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
# 3. other providers, check readme for more # 3. other providers, check readme for more
providers: providers:
local: include:
kind: docker - providers.yml # config/providers.yml
# add some more below if you want
# - file1.yml # config/file_1.yml
# - file2.yml
docker:
# for value format, see https://docs.docker.com/reference/cli/dockerd/ # for value format, see https://docs.docker.com/reference/cli/dockerd/
# i.e. ssh://user@10.0.1.1:22, tcp://10.0.2.1:2375 # $DOCKER_HOST implies unix:///var/run/docker.sock by default
# use FROM_ENV if you have binded docker socket to /var/run/docker.sock local: $DOCKER_HOST
value: FROM_ENV # add more docker providers if needed
providers: # remote-1: tcp://10.0.2.1:2375
kind: file # remote-2: ssh://root:1234@10.0.2.2
value: providers.yml
# Fixed options (optional, non hot-reloadable) # Fixed options (optional, non hot-reloadable)
# timeout_shutdown: 5 # timeout_shutdown: 5

View File

@@ -200,25 +200,22 @@ services:
volumes: volumes:
- nginx:/usr/share/nginx/html - nginx:/usr/share/nginx/html
go-proxy: go-proxy:
image: ghcr.io/yusing/go-proxy image: ghcr.io/yusing/go-proxy:latest
container_name: go-proxy container_name: go-proxy
restart: always restart: always
ports: network_mode: host
- 80:80 # http
- 443:443 # optional, https
- 8080:8080 # http panel
- 8443:8443 # optional, https panel
- 53:20000/udp # adguardhome
- 25565:20001/tcp # minecraft
- 8211:20002/udp # palworld
- 27015:20003/udp # palworld
volumes: volumes:
- ./config:/app/config - ./config:/app/config
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
go-proxy-frontend:
image: ghcr.io/yusing/go-proxy-frontend:latest
container_name: go-proxy-frontend
restart: unless-stopped
network_mode: host
labels: labels:
- proxy.aliases=gp - proxy.*.aliases=gp
- proxy.gp.port=8080 depends_on:
- go-proxy
``` ```
[🔼Back to top](#table-of-content) [🔼Back to top](#table-of-content)

View File

@@ -34,12 +34,7 @@
"provider": { "provider": {
"title": "DNS Challenge Provider", "title": "DNS Challenge Provider",
"type": "string", "type": "string",
"enum": [ "enum": ["local", "cloudflare", "clouddns", "duckdns"]
"local",
"cloudflare",
"clouddns",
"duckdns"
]
}, },
"options": { "options": {
"title": "Provider specific options", "title": "Provider specific options",
@@ -57,12 +52,7 @@
} }
}, },
"then": { "then": {
"required": [ "required": ["email", "domains", "provider", "options"]
"email",
"domains",
"provider",
"options"
]
} }
}, },
{ {
@@ -76,9 +66,7 @@
"then": { "then": {
"properties": { "properties": {
"options": { "options": {
"required": [ "required": ["auth_token"],
"auth_token"
],
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"auth_token": { "auth_token": {
@@ -101,11 +89,7 @@
"then": { "then": {
"properties": { "properties": {
"options": { "options": {
"required": [ "required": ["client_id", "email", "password"],
"client_id",
"email",
"password"
],
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"client_id": { "client_id": {
@@ -136,9 +120,7 @@
"then": { "then": {
"properties": { "properties": {
"options": { "options": {
"required": [ "required": ["token"],
"token"
],
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"token": { "token": {
@@ -155,73 +137,54 @@
"providers": { "providers": {
"title": "Proxy providers configuration", "title": "Proxy providers configuration",
"type": "object", "type": "object",
"patternProperties": { "additionalProperties": false,
"^[a-zA-Z0-9_-]+$": { "properties": {
"description": "Proxy provider", "include": {
"title": "Proxy providers configuration files",
"description": "relative path to 'config'",
"type": "array",
"items": {
"type": "string",
"pattern": "^[a-zA-Z0-9_-]+\\.(yml|yaml)$",
"patternErrorMessage": "Invalid file name"
}
},
"docker": {
"title": "Docker provider configuration",
"description": "docker clients (name: address)",
"type": "object", "type": "object",
"properties": { "patternProperties": {
"kind": { "^[a-zA-Z0-9-_]+$": {
"description": "Proxy provider kind",
"type": "string", "type": "string",
"enum": [ "examples": [
"docker", "unix:///var/run/docker.sock",
"file" "tcp://127.0.0.1:2375",
"ssh://user@host:port"
],
"oneOf": [
{
"const": "$DOCKER_HOST",
"description": "Use DOCKER_HOST environment variable"
},
{
"pattern": "^unix://.+$",
"description": "A Unix socket for local Docker communication."
},
{
"pattern": "^ssh://.+$",
"description": "An SSH connection to a remote Docker host."
},
{
"pattern": "^fd://.+$",
"description": "A file descriptor for Docker communication."
},
{
"pattern": "^tcp://.+$",
"description": "A TCP connection to a remote Docker host."
}
] ]
},
"value": {
"type": "string"
} }
}, }
"required": [
"kind",
"value"
],
"allOf": [
{
"if": {
"properties": {
"kind": {
"const": "docker"
}
}
},
"then": {
"if": {
"properties": {
"value": {
"const": "FROM_ENV"
}
}
},
"then": {
"properties": {
"value": {
"description": "use docker client from environment"
}
}
},
"else": {
"properties": {
"value": {
"description": "docker client URL",
"examples": [
"unix:///var/run/docker.sock",
"tcp://127.0.0.1:2375",
"ssh://user@host:port"
]
}
}
}
},
"else": {
"properties": {
"value": {
"description": "file path"
}
}
}
}
]
} }
} }
}, },
@@ -236,7 +199,5 @@
} }
}, },
"additionalProperties": false, "additionalProperties": false,
"required": [ "required": ["providers"]
"providers" }
]
}

View File

@@ -37,7 +37,7 @@
] ]
}, },
"host": { "host": {
"anyOf": [ "oneOf": [
{ {
"type": "string", "type": "string",
"format": "ipv4", "format": "ipv4",
@@ -69,9 +69,7 @@
"set_headers": {}, "set_headers": {},
"hide_headers": {} "hide_headers": {}
}, },
"required": [ "required": ["host"],
"host"
],
"additionalProperties": false, "additionalProperties": false,
"allOf": [ "allOf": [
{ {
@@ -80,10 +78,7 @@
{ {
"properties": { "properties": {
"scheme": { "scheme": {
"enum": [ "enum": ["http", "https"]
"http",
"https"
]
} }
} }
}, },
@@ -171,9 +166,7 @@
"not": true "not": true
} }
}, },
"required": [ "required": ["port"]
"port"
]
} }
}, },
{ {
@@ -198,4 +191,4 @@
} }
}, },
"additionalProperties": false "additionalProperties": false
} }

View File

@@ -1,114 +0,0 @@
#!/bin/bash
set -e
REPO_URL=https://github.com/yusing/go-proxy
BIN_URL="${REPO_URL}/releases/download/${VERSION}/go-proxy"
SRC_URL="${REPO_URL}/archive/refs/tags/${VERSION}.tar.gz"
APP_ROOT="/opt/go-proxy/${VERSION}"
LOG_FILE="/tmp/go-proxy-setup.log"
if [ -z "$VERSION" ] || [ "$VERSION" = "latest" ]; then
VERSION_URL="${REPO_URL}/raw/main/version.txt"
VERSION=$(wget -qO- "$VERSION_URL")
fi
if [ -d "$APP_ROOT" ]; then
echo "$APP_ROOT already exists"
exit 1
fi
# check if wget exists
if ! [ -x "$(command -v wget)" ]; then
echo "wget is not installed"
exit 1
fi
# check if make exists
if ! [ -x "$(command -v make)" ]; then
echo "make is not installed"
exit 1
fi
dl_source() {
cd /tmp
echo "Downloading go-proxy source ${VERSION}"
wget -c "${SRC_URL}" -O go-proxy.tar.gz &> $LOG_FILE
if [ $? -gt 0 ]; then
echo "Source download failed, check your internet connection and version number"
exit 1
fi
echo "Done"
echo "Extracting go-proxy source ${VERSION}"
tar xzf go-proxy.tar.gz &> $LOG_FILE
if [ $? -gt 0 ]; then
echo "failed to untar go-proxy.tar.gz"
exit 1
fi
rm go-proxy.tar.gz
mkdir -p "$(dirname "${APP_ROOT}")"
mv "go-proxy-${VERSION}" "$APP_ROOT"
cd "$APP_ROOT"
echo "Done"
}
dl_binary() {
mkdir -p bin
echo "Downloading go-proxy binary ${VERSION}"
wget -c "${BIN_URL}" -O bin/go-proxy &> $LOG_FILE
if [ $? -gt 0 ]; then
echo "Binary download failed, check your internet connection and version number"
exit 1
fi
chmod +x bin/go-proxy
echo "Done"
}
setup() {
make setup &> $LOG_FILE
if [ $? -gt 0 ]; then
echo "make setup failed"
exit 1
fi
# SETUP_CODEMIRROR = 1
if [ "$SETUP_CODEMIRROR" != "0" ]; then
make setup-codemirror &> $LOG_FILE || echo "make setup-codemirror failed, ignored"
fi
}
dl_source
dl_binary
setup
# setup systemd
# check if systemctl exists
if ! command -v systemctl is-system-running > /dev/null 2>&1; then
echo "systemctl not found, skipping systemd setup"
exit 0
fi
systemctl_failed() {
echo "Failed to enable and start go-proxy"
systemctl status go-proxy
exit 1
}
echo "Setting up systemd service"
cat <<EOF > /etc/systemd/system/go-proxy.service
[Unit]
Description=go-proxy reverse proxy
After=network-online.target
Wants=network-online.target systemd-networkd-wait-online.service
[Service]
Type=simple
ExecStart=${APP_ROOT}/bin/go-proxy
WorkingDirectory=${APP_ROOT}
Environment="GOPROXY_IS_SYSTEMD=1"
Restart=on-failure
RestartSec=1s
KillMode=process
KillSignal=SIGINT
TimeoutStartSec=5s
TimeoutStopSec=5s
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload &>$LOG_FILE || systemctl_failed
systemctl enable --now go-proxy &>$LOG_FILE || systemctl_failed
echo "Done"
echo "Setup complete"

View File

@@ -21,6 +21,7 @@ const (
) )
var providersGenMap = map[string]ProviderGenerator{ var providersGenMap = map[string]ProviderGenerator{
"": providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
ProviderLocal: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig), ProviderLocal: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
ProviderCloudflare: providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig), ProviderCloudflare: providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
ProviderClouddns: providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig), ProviderClouddns: providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),

View File

@@ -35,7 +35,7 @@ const (
ProvidersSchemaPath = SchemaBasePath + "providers.schema.json" ProvidersSchemaPath = SchemaBasePath + "providers.schema.json"
) )
const DockerHostFromEnv = "FROM_ENV" const DockerHostFromEnv = "$DOCKER_HOST"
const ( const (
ProxyHTTPPort = ":80" ProxyHTTPPort = ":80"

View File

@@ -7,7 +7,6 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var IsRunningAsService = getEnvBool("GOPROXY_IS_SYSTEMD")
var NoSchemaValidation = getEnvBool("GOPROXY_NO_SCHEMA_VALIDATION") var NoSchemaValidation = getEnvBool("GOPROXY_NO_SCHEMA_VALIDATION")
var IsDebug = getEnvBool("GOPROXY_DEBUG") var IsDebug = getEnvBool("GOPROXY_DEBUG")

View File

@@ -134,16 +134,9 @@ func (cfg *Config) Statistics() map[string]interface{} {
panic("bug: should not reach here") panic("bug: should not reach here")
} }
}) })
stats["type"] = p.GetType()
stats["num_streams"] = nStreams stats["num_streams"] = nStreams
stats["num_reverse_proxies"] = nRPs stats["num_reverse_proxies"] = nRPs
switch p.ProviderImpl.(type) {
case *PR.DockerProvider:
stats["type"] = "docker"
case *PR.FileProvider:
stats["type"] = "file"
default:
panic("bug: should not reach here")
}
providerStats[p.GetName()] = stats providerStats[p.GetName()] = stats
}) })
@@ -202,17 +195,17 @@ func (cfg *Config) load() E.NestedError {
} }
if !common.NoSchemaValidation { if !common.NoSchemaValidation {
if err := Validate(data); err.IsNotNil() { if err = Validate(data); err.IsNotNil() {
return err return err
} }
} }
errors := E.NewBuilder("errors validating config") warnings := E.NewBuilder("errors validating config")
cfg.l.Debug("starting autocert") cfg.l.Debug("starting autocert")
ap, err := autocert.NewConfig(&model.AutoCert).GetProvider() ap, err := autocert.NewConfig(&model.AutoCert).GetProvider()
if err.IsNotNil() { if err.IsNotNil() {
errors.Add(E.Failure("autocert provider").With(err)) warnings.Add(E.Failure("autocert provider").With(err))
} else { } else {
cfg.l.Debug("started autocert") cfg.l.Debug("started autocert")
} }
@@ -220,18 +213,24 @@ func (cfg *Config) load() E.NestedError {
cfg.l.Debug("starting providers") cfg.l.Debug("starting providers")
cfg.proxyProviders = F.NewMap[string, *PR.Provider]() cfg.proxyProviders = F.NewMap[string, *PR.Provider]()
for name, pm := range model.Providers { for _, filename := range model.Providers.Files {
p := PR.NewProvider(name, pm) p := PR.NewFileProvider(filename)
cfg.proxyProviders.Set(name, p) cfg.proxyProviders.Set(p.GetName(), p)
if err := p.StartAllRoutes(); err.IsNotNil() {
errors.Add(E.Failure("start routes").Subjectf("provider %s", name).With(err))
}
} }
for name, dockerHost := range model.Providers.Docker {
p := PR.NewDockerProvider(name, dockerHost)
cfg.proxyProviders.Set(p.GetName(), p)
}
cfg.proxyProviders.EachKV(func(name string, p *PR.Provider) {
if err := p.StartAllRoutes(); err.IsNotNil() {
warnings.Add(E.Failure("start routes").Subject(p).With(err))
}
})
cfg.l.Debug("started providers") cfg.l.Debug("started providers")
cfg.value = model cfg.value = model
if err := errors.Build(); err.IsNotNil() { if err := warnings.Build(); err.IsNotNil() {
cfg.l.Warn(err) cfg.l.Warn(err)
} }
@@ -244,7 +243,7 @@ func (cfg *Config) controlProviders(action string, do func(*PR.Provider) E.Neste
cfg.proxyProviders.EachKVParallel(func(name string, p *PR.Provider) { cfg.proxyProviders.EachKVParallel(func(name string, p *PR.Provider) {
if err := do(p); err.IsNotNil() { if err := do(p); err.IsNotNil() {
errors.Add(E.From(err).Subjectf("provider %s", name)) errors.Add(E.From(err).Subject(p))
} }
}) })

View File

@@ -1,7 +1,6 @@
package docker package docker
import ( import (
"errors"
"strings" "strings"
E "github.com/yusing/go-proxy/error" E "github.com/yusing/go-proxy/error"
@@ -77,5 +76,3 @@ func RegisterNamespace(namespace string, pm ValueParserMap) {
// namespace:target.attribute -> func(string) (any, error) // namespace:target.attribute -> func(string) (any, error)
var labelValueParserMap = make(map[string]ValueParserMap) var labelValueParserMap = make(map[string]ValueParserMap)
var ErrInvalidLabel = errors.New("invalid label")

View File

@@ -1,7 +1,6 @@
package docker package docker
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"reflect" "reflect"
@@ -14,27 +13,13 @@ func makeLabel(namespace string, alias string, field string) string {
return fmt.Sprintf("%s.%s.%s", namespace, alias, field) return fmt.Sprintf("%s.%s.%s", namespace, alias, field)
} }
func TestInvalidLabel(t *testing.T) {
pl, err := ParseLabel("foo.bar", "1234")
if !errors.Is(err, ErrInvalidLabel) {
t.Errorf("expected errInvalidLabel, got %s", err)
}
if pl != nil {
t.Errorf("expected nil, got %v", pl)
}
_, err = ParseLabel("proxy.foo", "bar")
if !errors.Is(err, ErrInvalidLabel) {
t.Errorf("expected errInvalidLabel, got %s", err)
}
}
func TestHomePageLabel(t *testing.T) { func TestHomePageLabel(t *testing.T) {
alias := "foo" alias := "foo"
field := "ip" field := "ip"
v := "bar" v := "bar"
pl, err := ParseLabel(makeLabel(NSHomePage, alias, field), v) pl, err := ParseLabel(makeLabel(NSHomePage, alias, field), v)
if err.IsNotNil() { if err.IsNotNil() {
t.Errorf("expected err=nil, got %s", err) t.Errorf("expected err=nil, got %s", err.Error())
} }
if pl.Target != alias { if pl.Target != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Target) t.Errorf("expected alias=%s, got %s", alias, pl.Target)
@@ -53,7 +38,7 @@ func TestStringProxyLabel(t *testing.T) {
v := "bar" v := "bar"
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v) pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v)
if err.IsNotNil() { if err.IsNotNil() {
t.Errorf("expected err=nil, got %s", err) t.Errorf("expected err=nil, got %s", err.Error())
} }
if pl.Target != alias { if pl.Target != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Target) t.Errorf("expected alias=%s, got %s", alias, pl.Target)
@@ -83,7 +68,7 @@ func TestBoolProxyLabelValid(t *testing.T) {
for k, v := range tests { for k, v := range tests {
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), k) pl, err := ParseLabel(makeLabel(NSProxy, alias, field), k)
if err.IsNotNil() { if err.IsNotNil() {
t.Errorf("expected err=nil, got %s", err) t.Errorf("expected err=nil, got %s", err.Error())
} }
if pl.Target != alias { if pl.Target != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Target) t.Errorf("expected alias=%s, got %s", alias, pl.Target)
@@ -101,8 +86,8 @@ func TestBoolProxyLabelInvalid(t *testing.T) {
alias := "foo" alias := "foo"
field := "no_tls_verify" field := "no_tls_verify"
_, err := ParseLabel(makeLabel(NSProxy, alias, field), "invalid") _, err := ParseLabel(makeLabel(NSProxy, alias, field), "invalid")
if !errors.Is(err, E.ErrInvalid) { if !err.Is(E.ErrInvalid) {
t.Errorf("expected err InvalidProxyLabel, got %s", err) t.Errorf("expected err InvalidProxyLabel, got %v", reflect.TypeOf(err))
} }
} }
@@ -121,7 +106,7 @@ func TestHeaderProxyLabelValid(t *testing.T) {
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v) pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v)
if err.IsNotNil() { if err.IsNotNil() {
t.Errorf("expected err=nil, got %s", err) t.Errorf("expected err=nil, got %s", err.Error())
} }
if pl.Target != alias { if pl.Target != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Target) t.Errorf("expected alias=%s, got %s", alias, pl.Target)
@@ -152,7 +137,7 @@ func TestHeaderProxyLabelInvalid(t *testing.T) {
for _, v := range tests { for _, v := range tests {
_, err := ParseLabel(makeLabel(NSProxy, alias, field), v) _, err := ParseLabel(makeLabel(NSProxy, alias, field), v)
if !errors.Is(err, E.ErrInvalid) { if !err.Is(E.ErrInvalid) {
t.Errorf("expected err InvalidProxyLabel for %q, got %v", v, err) t.Errorf("expected err InvalidProxyLabel for %q, got %v", v, err)
} }
} }
@@ -164,7 +149,7 @@ func TestCommaSepProxyLabelSingle(t *testing.T) {
v := "X-Custom-Header1" v := "X-Custom-Header1"
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v) pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v)
if err.IsNotNil() { if err.IsNotNil() {
t.Errorf("expected err=nil, got %s", err) t.Errorf("expected err=nil, got %s", err.Error())
} }
if pl.Target != alias { if pl.Target != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Target) t.Errorf("expected alias=%s, got %s", alias, pl.Target)
@@ -188,7 +173,7 @@ func TestCommaSepProxyLabelMulti(t *testing.T) {
v := "X-Custom-Header1, X-Custom-Header2,X-Custom-Header3" v := "X-Custom-Header1, X-Custom-Header2,X-Custom-Header3"
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v) pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v)
if err.IsNotNil() { if err.IsNotNil() {
t.Errorf("expected err=nil, got %s", err) t.Errorf("expected err=nil, got %s", err.Error())
} }
if pl.Target != alias { if pl.Target != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Target) t.Errorf("expected alias=%s, got %s", alias, pl.Target)

28
src/error/builder_test.go Normal file
View File

@@ -0,0 +1,28 @@
package error
import "testing"
func TestBuilder(t *testing.T) {
eb := NewBuilder("error occurred")
eb.Add(Failure("Action 1").With(Invalid("Inner", "1")).With(Invalid("Inner", "2")))
eb.Add(Failure("Action 2").With(Invalid("Inner", "3")))
got := eb.Build().Error()
expected1 :=
(`error occurred:
- Action 1 failed:
- invalid Inner - 1
- invalid Inner - 2
- Action 2 failed:
- invalid Inner - 3`)
expected2 :=
(`error occurred:
- Action 1 failed:
- invalid Inner - 2
- invalid Inner - 1
- Action 2 failed:
- invalid Inner - 3`)
if got != expected1 && got != expected2 {
t.Errorf("expected \n%s, got \n%s", expected1, got)
}
}

View File

@@ -4,7 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"sync"
) )
type ( type (
@@ -20,27 +19,24 @@ type (
// You should return (Slice/Map, NestedError). // You should return (Slice/Map, NestedError).
// Caller then should handle the nested error, // Caller then should handle the nested error,
// and continue with the valid values. // and continue with the valid values.
NestedError struct{ *nestedError } NestedError struct {
nestedError struct { subject string
neBase
sync.Mutex
}
neBase struct {
subject any
err error // can be nil err error // can be nil
extras []neBase extras []NestedError
inner *neBase // can be nil
level int
} }
) )
func Nil() NestedError { return NestedError{} } func Nil() NestedError { return NestedError{} }
func From(err error) NestedError { func From(err error) NestedError {
if err == nil { switch err := err.(type) {
case nil:
return Nil() return Nil()
case NestedError:
return err
default:
return NestedError{err: err}
} }
return NestedError{&nestedError{neBase: *copyFrom(err)}}
} }
// Check is a helper function that // Check is a helper function that
@@ -50,73 +46,41 @@ func Check[T any](obj T, err error) (T, NestedError) {
} }
func Join(message string, err ...error) NestedError { func Join(message string, err ...error) NestedError {
extras := make([]neBase, 0, len(err)) extras := make([]NestedError, 0, len(err))
nErr := 0 nErr := 0
for _, e := range err { for _, e := range err {
if err == nil { if err == nil {
continue continue
} }
extras = append(extras, *copyFrom(e)) extras = append(extras, From(e))
nErr += 1 nErr += 1
} }
if nErr == 0 { if nErr == 0 {
return Nil() return Nil()
} }
return NestedError{&nestedError{ return NestedError{
neBase: neBase{ err: errors.New(message),
err: errors.New(message), extras: extras,
extras: extras,
},
}}
}
func copyFrom(err error) *neBase {
if err == nil {
return nil
} }
switch base := err.(type) {
case *neBase:
copy := *base
return &copy
}
return &neBase{err: err}
} }
func new(message ...string) NestedError { func (ne NestedError) Error() string {
if len(message) == 0 {
return From(nil)
}
return From(errors.New(strings.Join(message, " ")))
}
func errorf(format string, args ...any) NestedError {
return From(fmt.Errorf(format, args...))
}
func (ne *neBase) Error() string {
var buf strings.Builder var buf strings.Builder
ne.writeToSB(&buf, ne.level, "") ne.writeToSB(&buf, 0, "")
return buf.String() return buf.String()
} }
func (ne NestedError) ExtraError(err error) NestedError { func (ne NestedError) Is(err error) bool {
if err != nil { return errors.Is(ne.err, err)
ne.Lock()
ne.extras = append(ne.extras, From(err).addLevel(ne.Level()+1))
ne.Unlock()
}
return ne
} }
func (ne NestedError) Extra(s string) NestedError { func (ne NestedError) With(s any) NestedError {
return ne.ExtraError(errors.New(s))
}
func (ne NestedError) ExtraAny(s any) NestedError {
var msg string var msg string
switch ss := s.(type) { switch ss := s.(type) {
case nil:
return ne
case error: case error:
return ne.ExtraError(ss) return ne.withError(ss)
case string: case string:
msg = ss msg = ss
case fmt.Stringer: case fmt.Stringer:
@@ -124,15 +88,22 @@ func (ne NestedError) ExtraAny(s any) NestedError {
default: default:
msg = fmt.Sprint(s) msg = fmt.Sprint(s)
} }
return ne.ExtraError(errors.New(msg)) return ne.withError(errors.New(msg))
} }
func (ne NestedError) Extraf(format string, args ...any) NestedError { func (ne NestedError) Extraf(format string, args ...any) NestedError {
return ne.ExtraError(fmt.Errorf(format, args...)) return ne.With(fmt.Errorf(format, args...))
} }
func (ne NestedError) Subject(s any) NestedError { func (ne NestedError) Subject(s any) NestedError {
ne.subject = s switch ss := s.(type) {
case string:
ne.subject = ss
case fmt.Stringer:
ne.subject = ss.String()
default:
ne.subject = fmt.Sprint(s)
}
return ne return ne
} }
@@ -143,74 +114,51 @@ func (ne NestedError) Subjectf(format string, args ...any) NestedError {
if strings.Contains(format, "%w") { if strings.Contains(format, "%w") {
panic("Subjectf format should not contain %w") panic("Subjectf format should not contain %w")
} }
return ne.Subject(fmt.Sprintf(format, args...)) ne.subject = fmt.Sprintf(format, args...)
}
func (ne NestedError) Level() int {
return ne.level
}
func (ne *nestedError) IsNil() bool {
return ne == nil
}
func (ne *nestedError) IsNotNil() bool {
return ne != nil
}
func (ne NestedError) With(inner error) NestedError {
ne.Lock()
defer ne.Unlock()
if ne.inner == nil {
ne.inner = copyFrom(inner)
} else {
ne.ExtraError(inner)
}
root := &ne.neBase
for root.inner != nil {
root.inner.level = root.level + 1
root = root.inner
}
return ne return ne
} }
func (ne *neBase) addLevel(level int) neBase { func (ne NestedError) IsNil() bool {
ret := *ne return ne.err == nil
ret.level += level
if ret.inner != nil {
inner := ret.inner.addLevel(level)
ret.inner = &inner
}
return ret
} }
func (ne *neBase) writeToSB(sb *strings.Builder, level int, prefix string) { func (ne NestedError) IsNotNil() bool {
return ne.err != nil
}
func errorf(format string, args ...any) NestedError {
return From(fmt.Errorf(format, args...))
}
func (ne NestedError) withError(err error) NestedError {
ne.extras = append(ne.extras, From(err))
return ne
}
func (ne *NestedError) writeToSB(sb *strings.Builder, level int, prefix string) {
ne.writeIndents(sb, level) ne.writeIndents(sb, level)
sb.WriteString(prefix) sb.WriteString(prefix)
if ne.err != nil { if ne.err != nil {
sb.WriteString(ne.err.Error()) sb.WriteString(ne.err.Error())
sb.WriteRune(' ')
} }
if ne.subject != nil { if ne.subject != "" {
sb.WriteString(fmt.Sprintf("for %q", ne.subject)) if ne.err != nil {
sb.WriteString(fmt.Sprintf(" for %q", ne.subject))
} else {
sb.WriteString(fmt.Sprint(ne.subject))
}
} }
if ne.inner != nil || len(ne.extras) > 0 { if len(ne.extras) > 0 {
sb.WriteString(":\n") sb.WriteRune(':')
} for _, extra := range ne.extras {
level += 1 sb.WriteRune('\n')
for _, extra := range ne.extras { extra.writeToSB(sb, level+1, "- ")
extra.writeToSB(sb, level, "- ") }
sb.WriteRune('\n')
}
if ne.inner != nil {
ne.inner.writeToSB(sb, level, "- ")
} }
} }
func (ne *neBase) writeIndents(sb *strings.Builder, level int) { func (ne *NestedError) writeIndents(sb *strings.Builder, level int) {
for i := 0; i < level; i++ { for i := 0; i < level; i++ {
sb.WriteString(" ") sb.WriteString(" ")
} }

View File

@@ -4,63 +4,67 @@ import (
"testing" "testing"
) )
func AssertEq(t *testing.T, got, want string) { func AssertEq[T comparable](t *testing.T, got, want T) {
t.Helper() t.Helper()
if got != want { if got != want {
t.Errorf("expected %q, got %q", want, got) t.Errorf("expected:\n%v, got\n%v", want, got)
} }
} }
func TestErrorIs(t *testing.T) {
AssertEq(t, Failure("foo").Is(ErrFailure), true)
AssertEq(t, Failure("foo").With("bar").Is(ErrFailure), true)
AssertEq(t, Failure("foo").With("bar").Is(ErrInvalid), false)
AssertEq(t, Failure("foo").With("bar").With("baz").Is(ErrInvalid), false)
AssertEq(t, Invalid("foo", "bar").Is(ErrInvalid), true)
AssertEq(t, Invalid("foo", "bar").Is(ErrFailure), false)
}
func TestErrorSimple(t *testing.T) { func TestErrorSimple(t *testing.T) {
ne := new("foo bar") ne := Failure("foo bar")
AssertEq(t, ne.Error(), "foo bar") AssertEq(t, ne.Error(), "foo bar failed")
ne.Subject("baz") ne = ne.Subject("baz")
AssertEq(t, ne.Error(), "baz: foo bar") AssertEq(t, ne.Error(), "foo bar failed for \"baz\"")
} }
func TestErrorSubjectOnly(t *testing.T) { func TestErrorWith(t *testing.T) {
ne := new().Subject("bar") ne := Failure("foo").With("bar").With("baz")
AssertEq(t, ne.Error(), "bar") AssertEq(t, ne.Error(), "foo failed:\n - bar\n - baz")
}
func TestErrorExtra(t *testing.T) {
ne := new("foo").Extra("bar").Extra("baz")
AssertEq(t, ne.Error(), "foo:\n - bar\n - baz\n")
} }
func TestErrorNested(t *testing.T) { func TestErrorNested(t *testing.T) {
inner := new("inner"). inner := Failure("inner").
Extra("123"). With("1").
Extra("456") With("1")
inner2 := new("inner"). inner2 := Failure("inner2").
Subject("2"). Subject("action 2").
Extra("456"). With("2").
Extra("789") With("2")
inner3 := new("inner"). inner3 := Failure("inner3").
Subject("3"). Subject("action 3").
Extra("456"). With("3").
Extra("789") With("3")
ne := new("foo"). ne := Failure("foo").
Extra("bar"). With("bar").
Extra("baz"). With("baz").
ExtraError(inner). With(inner).
With(inner.With(inner2.With(inner3))) With(inner.With(inner2.With(inner3)))
want := want :=
`foo: `foo failed:
- bar - bar
- baz - baz
- inner: - inner failed:
- 123 - 1
- 456 - 1
- inner: - inner failed:
- 123 - 1
- 456 - 1
- 2: inner: - inner2 failed for "action 2":
- 456 - 2
- 789 - 2
- 3: inner: - inner3 failed for "action 3":
- 456 - 3
- 789 - 3`
`
AssertEq(t, ne.Error(), want) AssertEq(t, ne.Error(), want)
} }

View File

@@ -1,30 +1,33 @@
package error package error
import (
stderrors "errors"
)
var ( var (
ErrAlreadyStarted = new("already started") ErrFailure = stderrors.New("failed")
ErrNotStarted = new("not started") ErrInvalid = stderrors.New("invalid")
ErrInvalid = new("invalid") ErrUnsupported = stderrors.New("unsupported")
ErrUnsupported = new("unsupported") ErrNotExists = stderrors.New("does not exist")
ErrNotExists = new("does not exist") ErrDuplicated = stderrors.New("duplicated")
ErrDuplicated = new("duplicated")
) )
func Failure(what string) NestedError { func Failure(what string) NestedError {
return errorf("%s failed", what) return errorf("%s %w", what, ErrFailure)
} }
func Invalid(subject, what any) NestedError { func Invalid(subject, what any) NestedError {
return errorf("%w %s: %q", ErrInvalid, subject, what) return errorf("%w %v - %v", ErrInvalid, subject, what)
} }
func Unsupported(subject, what any) NestedError { func Unsupported(subject, what any) NestedError {
return errorf("%w %s: %q", ErrUnsupported, subject, what) return errorf("%w %v - %v", ErrUnsupported, subject, what)
} }
func NotExists(subject, what any) NestedError { func NotExists(subject, what any) NestedError {
return errorf("%s %w: %q", subject, ErrNotExists, what) return errorf("%s %v - %v", subject, ErrNotExists, what)
} }
func Duplicated(subject, what any) NestedError { func Duplicated(subject, what any) NestedError {
return errorf("%w %s: %q", ErrDuplicated, subject, what) return errorf("%w %v: %v", ErrDuplicated, subject, what)
} }

View File

@@ -3,8 +3,8 @@ module github.com/yusing/go-proxy
go 1.22 go 1.22
require ( require (
github.com/docker/cli v27.1.1+incompatible github.com/docker/cli v27.1.2+incompatible
github.com/docker/docker v27.1.1+incompatible github.com/docker/docker v27.1.2+incompatible
github.com/fsnotify/fsnotify v1.7.0 github.com/fsnotify/fsnotify v1.7.0
github.com/go-acme/lego/v4 v4.17.4 github.com/go-acme/lego/v4 v4.17.4
github.com/santhosh-tekuri/jsonschema v1.2.4 github.com/santhosh-tekuri/jsonschema v1.2.4
@@ -44,7 +44,7 @@ require (
golang.org/x/crypto v0.26.0 // indirect golang.org/x/crypto v0.26.0 // indirect
golang.org/x/mod v0.20.0 // indirect golang.org/x/mod v0.20.0 // indirect
golang.org/x/sync v0.8.0 // indirect golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.23.0 // indirect golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.17.0 // indirect golang.org/x/text v0.17.0 // indirect
golang.org/x/time v0.6.0 // indirect golang.org/x/time v0.6.0 // indirect
golang.org/x/tools v0.24.0 // indirect golang.org/x/tools v0.24.0 // indirect

View File

@@ -13,10 +13,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE= github.com/docker/cli v27.1.2+incompatible h1:nYviRv5Y+YAKx3dFrTvS1ErkyVVunKOhoweCTE1BsnI=
github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v27.1.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= github.com/docker/docker v27.1.2+incompatible h1:AhGzR1xaQIy53qCkxARaFluI00WPGtXn0AJuoQsVYTY=
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v27.1.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -119,8 +119,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=

View File

@@ -16,6 +16,7 @@ import (
"github.com/yusing/go-proxy/common" "github.com/yusing/go-proxy/common"
"github.com/yusing/go-proxy/config" "github.com/yusing/go-proxy/config"
"github.com/yusing/go-proxy/docker" "github.com/yusing/go-proxy/docker"
E "github.com/yusing/go-proxy/error"
R "github.com/yusing/go-proxy/route" R "github.com/yusing/go-proxy/route"
"github.com/yusing/go-proxy/server" "github.com/yusing/go-proxy/server"
F "github.com/yusing/go-proxy/utils/functional" F "github.com/yusing/go-proxy/utils/functional"
@@ -31,19 +32,12 @@ func main() {
logrus.SetLevel(logrus.DebugLevel) logrus.SetLevel(logrus.DebugLevel)
} }
if common.IsRunningAsService { logrus.SetFormatter(&logrus.TextFormatter{
logrus.SetFormatter(&logrus.TextFormatter{ DisableSorting: true,
DisableColors: true, FullTimestamp: true,
DisableTimestamp: true, ForceColors: true,
DisableSorting: true, TimestampFormat: "01-02 15:04:05",
}) })
} else {
logrus.SetFormatter(&logrus.TextFormatter{
DisableSorting: true,
FullTimestamp: true,
TimestampFormat: "01-02 15:04:05",
})
}
if args.Command == common.CommandReload { if args.Command == common.CommandReload {
if err := apiUtils.ReloadServer(); err.IsNotNil() { if err := apiUtils.ReloadServer(); err.IsNotNil() {
@@ -53,18 +47,26 @@ func main() {
} }
onShutdown := F.NewSlice[func()]() onShutdown := F.NewSlice[func()]()
// exit if only validate config
if args.Command == common.CommandValidate {
var err E.NestedError
data, err := E.Check(os.ReadFile(common.ConfigPath))
if err.IsNotNil() {
l.WithError(err).Fatalf("config error")
}
if err = config.Validate(data); err.IsNotNil() {
l.WithError(err).Fatalf("config error")
}
l.Printf("config OK")
return
}
cfg, err := config.New() cfg, err := config.New()
if err.IsNotNil() { if err.IsNotNil() {
l.Fatalf("config error: %s", err) l.Fatalf("config error: %s", err)
} }
// exit if only validate config
// TODO: validate without load
if args.Command == common.CommandValidate {
l.Printf("config OK")
return
}
onShutdown.Add(func() { onShutdown.Add(func() {
docker.CloseAllClients() docker.CloseAllClients()
cfg.Dispose() cfg.Dispose()
@@ -76,22 +78,27 @@ func main() {
signal.Notify(sig, syscall.SIGHUP) signal.Notify(sig, syscall.SIGHUP)
autocert := cfg.GetAutoCertProvider() autocert := cfg.GetAutoCertProvider()
err = autocert.LoadCert()
if err.IsNotNil() { if autocert != nil {
l.Infof("error loading certificate: %s\nNow attempting to obtain a new certificate", err) err = autocert.LoadCert()
if err = autocert.ObtainCert(); err.IsNotNil() {
ctx, certRenewalCancel := context.WithCancel(context.Background()) if err.IsNotNil() {
go autocert.ScheduleRenewal(ctx) l.Error(err)
onShutdown.Add(certRenewalCancel) l.Info("Now attempting to obtain a new certificate...")
if err = autocert.ObtainCert(); err.IsNotNil() {
ctx, certRenewalCancel := context.WithCancel(context.Background())
go autocert.ScheduleRenewal(ctx)
onShutdown.Add(certRenewalCancel)
} else {
l.Warn(err)
}
} else { } else {
l.Warn(err) for name, expiry := range autocert.GetExpiries() {
} l.Infof("certificate %q: expire on %s", name, expiry)
} else { }
for name, expiry := range autocert.GetExpiries() {
l.Infof("certificate %q: expire on %s", name, expiry)
} }
} }
proxyServer := server.InitProxyServer(server.Options{ proxyServer := server.InitProxyServer(server.Options{
Name: "proxy", Name: "proxy",
CertProvider: autocert, CertProvider: autocert,

View File

@@ -40,4 +40,10 @@ func (e *ProxyEntry) SetDefaults() {
if e.Path == "" { if e.Path == "" {
e.Path = "/" e.Path = "/"
} }
switch e.Scheme {
case "http":
e.Port = "80"
case "https":
e.Port = "443"
}
} }

View File

@@ -1,9 +0,0 @@
package model
type (
ProxyProvider struct {
Kind string `json:"kind"` // docker, file
Value string `json:"value"`
}
ProxyProviders = map[string]ProxyProvider
)

View File

@@ -0,0 +1,6 @@
package model
type ProxyProviders struct {
Files []string `yaml:"include" json:"include"` // docker, file
Docker map[string]string `yaml:"docker" json:"docker"`
}

View File

@@ -11,5 +11,5 @@ func NewPath(s string) (Path, E.NestedError) {
if s == "" || s[0] == '/' { if s == "" || s[0] == '/' {
return Path{F.NewStringable(s)}, E.Nil() return Path{F.NewStringable(s)}, E.Nil()
} }
return Path{}, E.Invalid("path", s).Extra("must be empty or start with '/'") return Path{}, E.Invalid("path", s).With("must be empty or start with '/'")
} }

View File

@@ -11,7 +11,7 @@ type Port int
func NewPort(v string) (Port, E.NestedError) { func NewPort(v string) (Port, E.NestedError) {
p, err := strconv.Atoi(v) p, err := strconv.Atoi(v)
if err != nil { if err != nil {
return ErrPort, E.From(err) return ErrPort, E.Invalid("port number", v).With(err)
} }
return NewPortInt(p) return NewPortInt(p)
} }

View File

@@ -15,7 +15,7 @@ type StreamPort struct {
func NewStreamPort(p string) (StreamPort, E.NestedError) { func NewStreamPort(p string) (StreamPort, E.NestedError) {
split := strings.Split(p, ":") split := strings.Split(p, ":")
if len(split) != 2 { if len(split) != 2 {
return StreamPort{}, E.Invalid("stream port", p).Extra("should be in 'x:y' format") return StreamPort{}, E.Invalid("stream port", p).With("should be in 'x:y' format")
} }
listeningPort, err := NewPort(split[0]) listeningPort, err := NewPort(split[0])

View File

@@ -16,8 +16,8 @@ type DockerProvider struct {
dockerHost string dockerHost string
} }
func DockerProviderImpl(model *M.ProxyProvider) ProviderImpl { func DockerProviderImpl(dockerHost string) ProviderImpl {
return &DockerProvider{dockerHost: model.Value} return &DockerProvider{dockerHost: dockerHost}
} }
// GetProxyEntries returns proxy entries from a docker client. // GetProxyEntries returns proxy entries from a docker client.
@@ -32,15 +32,16 @@ func DockerProviderImpl(model *M.ProxyProvider) ProviderImpl {
// - p: A pointer to the DockerProvider struct. // - p: A pointer to the DockerProvider struct.
// //
// Returns: // Returns:
// - P.EntryModelSlice: A slice of EntryModel structs representing the proxy entries. // - P.EntryModelSlice: (non-nil) A slice of EntryModel structs representing the proxy entries.
// - error: An error object if there was an error retrieving the docker client information or parsing the labels. // - error: An error object if there was an error retrieving the docker client information or parsing the labels.
func (p DockerProvider) GetProxyEntries() (M.ProxyEntries, E.NestedError) { func (p DockerProvider) GetProxyEntries() (M.ProxyEntries, E.NestedError) {
entries := M.NewProxyEntries()
info, err := D.GetClientInfo(p.dockerHost) info, err := D.GetClientInfo(p.dockerHost)
if err.IsNotNil() { if err.IsNotNil() {
return nil, E.From(err) return entries, E.From(err)
} }
entries := M.NewProxyEntries()
errors := E.NewBuilder("errors when parse docker labels for %q", p.dockerHost) errors := E.NewBuilder("errors when parse docker labels for %q", p.dockerHost)
for _, container := range info.Containers { for _, container := range info.Containers {

View File

@@ -16,10 +16,10 @@ type FileProvider struct {
path string path string
} }
func FileProviderImpl(m *M.ProxyProvider) ProviderImpl { func FileProviderImpl(filename string) ProviderImpl {
return &FileProvider{ return &FileProvider{
fileName: m.Value, fileName: filename,
path: path.Join(common.ConfigBasePath, m.Value), path: path.Join(common.ConfigBasePath, filename),
} }
} }
@@ -27,13 +27,17 @@ func Validate(data []byte) E.NestedError {
return U.ValidateYaml(U.GetSchema(common.ProvidersSchemaPath), data) return U.ValidateYaml(U.GetSchema(common.ProvidersSchemaPath), data)
} }
func (p *FileProvider) String() string {
return p.fileName
}
func (p *FileProvider) GetProxyEntries() (M.ProxyEntries, E.NestedError) { func (p *FileProvider) GetProxyEntries() (M.ProxyEntries, E.NestedError) {
entries := M.NewProxyEntries() entries := M.NewProxyEntries()
data, err := E.Check(os.ReadFile(p.path)) data, err := E.Check(os.ReadFile(p.path))
if err.IsNotNil() { if err.IsNotNil() {
return entries, E.Failure("read file").Subject(p.fileName).With(err) return entries, E.Failure("read file").Subject(p).With(err)
} }
ne := E.Failure("validation").Subject(p.fileName) ne := E.Failure("validation").Subject(p)
if !common.NoSchemaValidation { if !common.NoSchemaValidation {
if err = Validate(data); err.IsNotNil() { if err = Validate(data); err.IsNotNil() {
return entries, ne.With(err) return entries, ne.With(err)

View File

@@ -2,9 +2,10 @@ package provider
import ( import (
"context" "context"
"fmt"
"path"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/yusing/go-proxy/common"
E "github.com/yusing/go-proxy/error" E "github.com/yusing/go-proxy/error"
M "github.com/yusing/go-proxy/models" M "github.com/yusing/go-proxy/models"
R "github.com/yusing/go-proxy/route" R "github.com/yusing/go-proxy/route"
@@ -20,6 +21,7 @@ type Provider struct {
ProviderImpl ProviderImpl
name string name string
t ProviderType
routes *R.Routes routes *R.Routes
reloadReqCh chan struct{} reloadReqCh chan struct{}
@@ -30,27 +32,49 @@ type Provider struct {
l *logrus.Entry l *logrus.Entry
} }
func NewProvider(name string, model M.ProxyProvider) (p *Provider) { type ProviderType string
p = &Provider{
const (
ProviderTypeDocker ProviderType = "docker"
ProviderTypeFile ProviderType = "file"
)
func newProvider(name string, t ProviderType) *Provider {
return &Provider{
name: name, name: name,
t: t,
routes: R.NewRoutes(), routes: R.NewRoutes(),
reloadReqCh: make(chan struct{}, 1), reloadReqCh: make(chan struct{}, 1),
l: logrus.WithField("provider", name), l: logrus.WithField("provider", name),
} }
switch model.Kind { }
case common.ProviderKind_Docker: func NewFileProvider(filename string) *Provider {
p.ProviderImpl = DockerProviderImpl(&model) name := path.Base(filename)
case common.ProviderKind_File: p := newProvider(name, ProviderTypeFile)
p.ProviderImpl = FileProviderImpl(&model) p.ProviderImpl = FileProviderImpl(filename)
}
p.watcher = p.NewWatcher() p.watcher = p.NewWatcher()
return return p
}
func NewDockerProvider(name string, dockerHost string) *Provider {
p := newProvider(name, ProviderTypeDocker)
p.ProviderImpl = DockerProviderImpl(dockerHost)
p.watcher = p.NewWatcher()
return p
} }
func (p *Provider) GetName() string { func (p *Provider) GetName() string {
return p.name return p.name
} }
func (p *Provider) GetType() ProviderType {
return p.t
}
func (p *Provider) String() string {
return fmt.Sprintf("%s (%s provider)", p.name, p.t)
}
func (p *Provider) StartAllRoutes() E.NestedError { func (p *Provider) StartAllRoutes() E.NestedError {
err := p.loadRoutes() err := p.loadRoutes()
@@ -58,23 +82,24 @@ func (p *Provider) StartAllRoutes() E.NestedError {
p.watcherCtx, p.watcherCancel = context.WithCancel(context.Background()) p.watcherCtx, p.watcherCancel = context.WithCancel(context.Background())
go p.watchEvents() go p.watchEvents()
if err.IsNotNil() { errors := E.NewBuilder("errors in routes")
return err
}
errors := E.NewBuilder("errors starting routes for provider %q", p.name)
nStarted := 0 nStarted := 0
nFailed := 0
if err.IsNotNil() {
errors.Add(err)
}
p.routes.EachKVParallel(func(alias string, r R.Route) { p.routes.EachKVParallel(func(alias string, r R.Route) {
if err := r.Start(); err.IsNotNil() { if err := r.Start(); err.IsNotNil() {
errors.Add(err.Subject(alias)) errors.Add(err.Subject(r))
nFailed++
} else { } else {
nStarted++ nStarted++
} }
}) })
if err := errors.Build(); err.IsNotNil() { p.l.Infof("%d routes started, %d failed", nStarted, nFailed)
return err return errors.Build()
}
p.l.Infof("%d routes started", nStarted)
return E.Nil()
} }
func (p *Provider) StopAllRoutes() E.NestedError { func (p *Provider) StopAllRoutes() E.NestedError {
@@ -87,7 +112,7 @@ func (p *Provider) StopAllRoutes() E.NestedError {
nStopped := 0 nStopped := 0
p.routes.EachKVParallel(func(alias string, r R.Route) { p.routes.EachKVParallel(func(alias string, r R.Route) {
if err := r.Stop(); err.IsNotNil() { if err := r.Stop(); err.IsNotNil() {
errors.Add(err.Subject(alias)) errors.Add(err.Subject(r))
} else { } else {
nStopped++ nStopped++
} }
@@ -146,7 +171,7 @@ func (p *Provider) loadRoutes() E.NestedError {
entries, err := p.GetProxyEntries() entries, err := p.GetProxyEntries()
if err.IsNotNil() { if err.IsNotNil() {
p.l.Warn(err.Subjectf("provider %s", p.name)) p.l.Warn(err.Subject(p))
} }
p.routes = R.NewRoutes() p.routes = R.NewRoutes()
@@ -155,7 +180,7 @@ func (p *Provider) loadRoutes() E.NestedError {
e.Alias = a e.Alias = a
r, err := R.NewRoute(e) r, err := R.NewRoute(e)
if err.IsNotNil() { if err.IsNotNil() {
errors.Addf("%s: %w", a, err) errors.Add(err.Subject(a))
p.l.Debugf("failed to load route: %s, %s", a, err) p.l.Debugf("failed to load route: %s, %s", a, err)
} else { } else {
p.routes.Set(a, r) p.routes.Set(a, r)

View File

@@ -67,7 +67,7 @@ func NewHTTPRoute(entry *P.Entry) (*HTTPRoute, E.NestedError) {
path := entry.Path.String() path := entry.Path.String()
if _, exists := r.Subroutes[path]; exists { if _, exists := r.Subroutes[path]; exists {
httpRoutes.Unlock() httpRoutes.Unlock()
return nil, E.Duplicated("path", path).Subject(entry.Alias) return nil, E.Duplicated("path", path)
} }
r.mux.HandleFunc(path, rp.ServeHTTP) r.mux.HandleFunc(path, rp.ServeHTTP)
if err := recover(); err != nil { if err := recover(); err != nil {
@@ -75,9 +75,9 @@ func NewHTTPRoute(entry *P.Entry) (*HTTPRoute, E.NestedError) {
switch t := err.(type) { switch t := err.(type) {
case error: case error:
// NOTE: likely path pattern error // NOTE: likely path pattern error
return nil, E.From(t).Subject(entry.Alias) return nil, E.From(t)
default: default:
return nil, E.From(fmt.Errorf("%v", t)).Subject(entry.Alias) return nil, E.From(fmt.Errorf("%v", t))
} }
} }
@@ -106,6 +106,10 @@ func NewHTTPRoute(entry *P.Entry) (*HTTPRoute, E.NestedError) {
return r, E.Nil() return r, E.Nil()
} }
func (r *HTTPRoute) String() string {
return fmt.Sprintf("%s (reverse proxy)", r.Alias)
}
func (r *HTTPRoute) Start() E.NestedError { func (r *HTTPRoute) Start() E.NestedError {
httpRoutes.Set(r.Alias.String(), r) httpRoutes.Set(r.Alias.String(), r)
return E.Nil() return E.Nil()

View File

@@ -11,6 +11,7 @@ type (
Route interface { Route interface {
Start() E.NestedError Start() E.NestedError
Stop() E.NestedError Stop() E.NestedError
String() string
} }
Routes = F.Map[string, Route] Routes = F.Map[string, Route]
) )

View File

@@ -49,9 +49,13 @@ func NewStreamRoute(entry *P.StreamEntry) (*StreamRoute, E.NestedError) {
return base, E.Nil() return base, E.Nil()
} }
func (r *StreamRoute) String() string {
return fmt.Sprintf("%s (%v stream)", r.Alias, r.Scheme)
}
func (r *StreamRoute) Start() E.NestedError { func (r *StreamRoute) Start() E.NestedError {
if r.started.Load() { if r.started.Load() {
return E.ErrAlreadyStarted return E.Invalid("state", "already started")
} }
r.wg.Wait() r.wg.Wait()
if err := r.Setup(); err != nil { if err := r.Setup(); err != nil {
@@ -66,7 +70,7 @@ func (r *StreamRoute) Start() E.NestedError {
func (r *StreamRoute) Stop() E.NestedError { func (r *StreamRoute) Stop() E.NestedError {
if !r.started.Load() { if !r.started.Load() {
return E.ErrNotStarted return E.Invalid("state", "not started")
} }
l := r.l l := r.l
close(r.stopCh) close(r.stopCh)

View File

@@ -49,8 +49,12 @@ func NewServer(opt Options) (s *server) {
logrus.WithFields(logrus.Fields{"?": "server", "name": opt.Name}), logrus.WithFields(logrus.Fields{"?": "server", "name": opt.Name}),
}) })
_, err := opt.CertProvider.GetCert(nil) certAvailable := false
certAvailable := err == nil if opt.CertProvider != nil {
_, err := opt.CertProvider.GetCert(nil)
certAvailable = err == nil
}
if certAvailable && opt.RedirectToHTTPS && opt.HTTPSPort != "" { if certAvailable && opt.RedirectToHTTPS && opt.HTTPSPort != "" {
httpHandler = redirectToTLSHandler(opt.HTTPSPort) httpHandler = redirectToTLSHandler(opt.HTTPSPort)
} else { } else {

View File

@@ -37,11 +37,13 @@ func (s *Slice[T]) Set(i int, v T) {
} }
func (s *Slice[T]) Add(e T) *Slice[T] { func (s *Slice[T]) Add(e T) *Slice[T] {
return &Slice[T]{append(s.s, e)} s.s = append(s.s, e)
return s
} }
func (s *Slice[T]) AddRange(other *Slice[T]) *Slice[T] { func (s *Slice[T]) AddRange(other *Slice[T]) *Slice[T] {
return &Slice[T]{append(s.s, other.s...)} s.s = append(s.s, other.s...)
return s
} }
func (s *Slice[T]) ForEach(do func(T)) { func (s *Slice[T]) ForEach(do func(T)) {

View File

@@ -1 +1 @@
0.5.0-beta 0.5.0-beta2