mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-19 01:47:14 +01:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21fcceb391 | ||
|
|
82f06374f7 | ||
|
|
04fd6543fd | ||
|
|
409a18df38 | ||
|
|
4e5a8d0985 | ||
|
|
16b507bc7c |
@@ -2,13 +2,12 @@ FROM golang:1.23.1-alpine AS builder
|
||||
COPY src /src
|
||||
ENV GOCACHE=/root/.cache/go-build
|
||||
WORKDIR /src
|
||||
RUN --mount=type=cache,target="/go/pkg/mod" \
|
||||
go mod download
|
||||
RUN --mount=type=cache,target="/go/pkg/mod" \
|
||||
--mount=type=cache,target="/root/.cache/go-build" \
|
||||
go mod download && \
|
||||
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o go-proxy github.com/yusing/go-proxy
|
||||
|
||||
FROM alpine:latest
|
||||
FROM alpine:3.20
|
||||
|
||||
LABEL maintainer="yusing@6uo.me"
|
||||
|
||||
|
||||
2
Makefile
2
Makefile
@@ -27,7 +27,7 @@ get:
|
||||
cd src && go get -u && go mod tidy && cd ..
|
||||
|
||||
debug:
|
||||
make build && GOPROXY_DEBUG=1 bin/go-proxy
|
||||
make build && sudo GOPROXY_DEBUG=1 bin/go-proxy
|
||||
|
||||
archive:
|
||||
git archive HEAD -o ../go-proxy-$$(date +"%Y%m%d%H%M").zip
|
||||
|
||||
29
README.md
29
README.md
@@ -1,5 +1,11 @@
|
||||
# go-proxy
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
|
||||
A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse proxy and load balancer with a web UI.
|
||||
|
||||
**Table of content**
|
||||
@@ -20,18 +26,20 @@ A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse pr
|
||||
## Key Points
|
||||
|
||||
- Easy to use
|
||||
- Effortless configuration
|
||||
- Error messages is clear and detailed
|
||||
- Auto certificate obtaining and renewal (See [Supported DNS Challenge Providers](docs/dns_providers.md))
|
||||
- Auto configuration for docker contaienrs
|
||||
- Auto configuration for docker containers
|
||||
- Auto hot-reload on container state / config file changes
|
||||
- Support HTTP(s), TCP and UDP
|
||||
- Web UI for configuration and monitoring (See [screenshots](screeenshots))
|
||||
- Web UI for configuration and monitoring (See [screenshots](https://github.com/yusing/go-proxy-frontend?tab=readme-ov-file#screenshots))
|
||||
- Written in **[Go](https://go.dev)**
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Setup DNS Records
|
||||
1. Setup DNS Records, e.g.
|
||||
|
||||
- A Record: `*.y.z` -> `10.0.10.1`
|
||||
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
||||
@@ -39,18 +47,19 @@ A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse pr
|
||||
2. Setup `go-proxy` [See here](docs/docker.md)
|
||||
|
||||
3. Configure `go-proxy`
|
||||
- with text editor (i.e. Visual Studio Code)
|
||||
- with text editor (e.g. Visual Studio Code)
|
||||
- or with web config editor via `http://gp.y.z`
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
### Commands line arguments
|
||||
|
||||
| Argument | Description |
|
||||
| ---------- | -------------------------------- |
|
||||
| empty | start proxy server |
|
||||
| `validate` | validate config and exit |
|
||||
| `reload` | trigger a force reload of config |
|
||||
| Argument | Description | Example |
|
||||
| ----------- | -------------------------------- | -------------------------- |
|
||||
| empty | start proxy server | |
|
||||
| `validate` | validate config and exit | |
|
||||
| `reload` | trigger a force reload of config | |
|
||||
| `ls-config` | list config and exit | `go-proxy ls-config \| jq` |
|
||||
|
||||
**run with `docker exec <container_name> /app/go-proxy <command>`**
|
||||
|
||||
@@ -105,6 +114,8 @@ See [providers.example.yml](providers.example.yml) for examples
|
||||
|
||||
- Cert "renewal" is actually obtaining a new cert instead of renewing the existing one
|
||||
|
||||
- `autocert` config is not hot-reloadable
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Build it yourself
|
||||
|
||||
@@ -5,7 +5,8 @@ services:
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
labels:
|
||||
- proxy.*.aliases=gp
|
||||
- proxy.aliases=gp
|
||||
- proxy.gp.port=8888
|
||||
depends_on:
|
||||
- app
|
||||
app:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
```go
|
||||
var providersGenMap = map[string]ProviderGenerator{
|
||||
"cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
|
||||
// add here, i.e.
|
||||
// add here, e.g.
|
||||
"clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# Supported DNS Providers
|
||||
|
||||
<!-- TOC -->
|
||||
- [Cloudflare](#cloudflare)
|
||||
- [CloudDNS](#clouddns)
|
||||
- [DuckDNS](#duckdns)
|
||||
- [Implement other DNS providers](#implement-other-dns-providers)
|
||||
<!-- /TOC -->
|
||||
|
||||
- [Supported DNS Providers](#supported-dns-providers)
|
||||
- [Cloudflare](#cloudflare)
|
||||
- [CloudDNS](#clouddns)
|
||||
- [DuckDNS](#duckdns)
|
||||
- [OVHCloud](#ovhcloud)
|
||||
- [Implement other DNS providers](#implement-other-dns-providers)
|
||||
|
||||
## Cloudflare
|
||||
|
||||
@@ -23,10 +25,29 @@ Follow [this guide](https://cloudkul.com/blog/automcatic-renew-and-generate-ssl-
|
||||
|
||||
## DuckDNS
|
||||
|
||||
`token`: DuckDNS Token
|
||||
- `token`: DuckDNS Token
|
||||
|
||||
Tested by [earvingad](https://github.com/earvingad)
|
||||
|
||||
## OVHCloud
|
||||
|
||||
_Note, `application_key` and `oauth2_config` **CANNOT** be used together_
|
||||
|
||||
- `api_endpoint`: Endpoint URL, or one of
|
||||
- `ovh-eu`,
|
||||
- `ovh-ca`,
|
||||
- `ovh-us`,
|
||||
- `kimsufi-eu`,
|
||||
- `kimsufi-ca`,
|
||||
- `soyoustart-eu`,
|
||||
- `soyoustart-ca`
|
||||
- `application_secret`
|
||||
- `application_key`
|
||||
- `consumer_key`
|
||||
- `oauth2_config`: Client ID and Client Secret
|
||||
- `client_id`
|
||||
- `client_secret`
|
||||
|
||||
## Implement other DNS providers
|
||||
|
||||
See [add_dns_provider.md](docs/add_dns_provider.md)
|
||||
|
||||
@@ -85,24 +85,26 @@
|
||||
|
||||
### Syntax
|
||||
|
||||
| Label | Description |
|
||||
| ----------------------- | -------------------------------------------------------- |
|
||||
| `proxy.aliases` | comma separated aliases for subdomain and label matching |
|
||||
| `proxy.<alias>.<field>` | set field for specific alias |
|
||||
| `proxy.*.<field>` | set field for all aliases |
|
||||
| Label | Description | Default |
|
||||
| ----------------------- | -------------------------------------------------------- | ---------------- |
|
||||
| `proxy.aliases` | comma separated aliases for subdomain and label matching | `container_name` |
|
||||
| `proxy.<alias>.<field>` | set field for specific alias | N/A |
|
||||
| `proxy.*.<field>` | set field for all aliases | N/A |
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Description | Default | Allowed Values / Syntax |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `scheme` | proxy protocol | <ul><li>`http` for numeric port</li><li>`tcp` for `x:y` port</li></ul> | `http`, `https`, `tcp`, `udp` |
|
||||
| `host` | proxy host | <ul><li>Docker: `container_name`</li><li>File: `localhost`</li></ul> | IP address, hostname |
|
||||
| `port` | proxy port **(http/s)** | first port in `ports:` | number in range of `1 - 65535` |
|
||||
| `port` **(required)** | proxy port **(tcp/udp)** | N/A | `x:y` <br><ul><li>x: port for `go-proxy` to listen on</li><li>y: port or [_service name_](../src/common/constants.go#L55) of target container</li></ul> |
|
||||
| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean |
|
||||
| `path_patterns` | proxy path patterns **(http/s only)**<br> only requests that matched a pattern will be proxied | empty **(proxy all requests)** | yaml style list[<sup>1</sup>](#list-example) of path patterns ([syntax](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)) |
|
||||
| `set_headers` | header to set **(http/s only)** | empty | yaml style key-value mapping[<sup>2</sup>](#key-value-mapping-example) of header-value pairs |
|
||||
| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[<sup>1</sup>](#list-example) of headers |
|
||||
| Field | Description | Default | Allowed Values / Syntax |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `scheme` | proxy protocol | <ul><li>`http` for numeric port</li><li>`tcp` for `x:y` port</li></ul> | `http`, `https`, `tcp`, `udp` |
|
||||
| `host` | proxy host | <ul><li>Docker: docker client IP / hostname </li><li>File: `localhost`</li></ul> | IP address, hostname |
|
||||
| `port` | proxy port **(http/s)** | first port in `ports:` | number in range of `1 - 65535` |
|
||||
| `port` **(required)** | proxy port **(tcp/udp)** | N/A | `x:y` <br><ul><li>x: port for `go-proxy` to listen on</li><li>y: port or [_service name_](../src/common/constants.go#L55) of target container</li></ul> |
|
||||
| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean |
|
||||
| `path_patterns` | proxy path patterns **(http/s only)**<br> only requests that matched a pattern will be proxied | empty **(proxy all requests)** | yaml style list[<sup>1</sup>](#list-example) of path patterns ([syntax](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)) |
|
||||
| `set_headers` | header to set **(http/s only)** | empty | yaml style key-value mapping[<sup>2</sup>](#key-value-mapping-example) of header-value pairs |
|
||||
| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[<sup>1</sup>](#list-example) of headers |
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
#### Key-value mapping example
|
||||
|
||||
@@ -132,6 +134,8 @@ service_a:
|
||||
X-Custom-Header2: value3
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
#### List example
|
||||
|
||||
Docker Compose
|
||||
@@ -166,6 +170,23 @@ service_a:
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Container not showing up in proxies list
|
||||
|
||||
Please check that either `ports` or label `proxy.<alias>.port` is declared, e.g.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
nginx-1: # Option 1
|
||||
...
|
||||
ports:
|
||||
- 80
|
||||
nginx-2: # Option 2
|
||||
...
|
||||
container_name: nginx-2
|
||||
labels:
|
||||
proxy.nginx-2.port: 80
|
||||
```
|
||||
|
||||
- Firewall issues
|
||||
|
||||
If you are using `ufw` with vpn that drop all inbound traffic except vpn, run below:
|
||||
@@ -237,6 +258,8 @@ services:
|
||||
container_name: nginx
|
||||
volumes:
|
||||
- nginx:/usr/share/nginx/html
|
||||
ports:
|
||||
- 80
|
||||
go-proxy:
|
||||
image: ghcr.io/yusing/go-proxy:latest
|
||||
container_name: go-proxy
|
||||
@@ -251,7 +274,8 @@ services:
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
labels:
|
||||
- proxy.*.aliases=gp
|
||||
- proxy.aliases=gp
|
||||
- proxy.gp.port=8888
|
||||
depends_on:
|
||||
- go-proxy
|
||||
```
|
||||
|
||||
2
frontend
2
frontend
Submodule frontend updated: 8cdf9eaa10...d0e59630d6
@@ -37,7 +37,7 @@
|
||||
"title": "DNS Challenge Provider",
|
||||
"default": "local",
|
||||
"type": "string",
|
||||
"enum": ["local", "cloudflare", "clouddns", "duckdns"]
|
||||
"enum": ["local", "cloudflare", "clouddns", "duckdns", "ovh"]
|
||||
},
|
||||
"options": {
|
||||
"title": "Provider specific options",
|
||||
@@ -135,6 +135,82 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "ovh"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"options": {
|
||||
"required": ["application_secret", "consumer_key"],
|
||||
"additionalProperties": false,
|
||||
"oneOf": [
|
||||
{
|
||||
"required": ["application_key"]
|
||||
},
|
||||
{
|
||||
"required": ["oauth2_config"]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"api_endpoint": {
|
||||
"description": "OVH API endpoint",
|
||||
"default": "ovh-eu",
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": [
|
||||
"ovh-eu",
|
||||
"ovh-ca",
|
||||
"ovh-us",
|
||||
"kimsufi-eu",
|
||||
"kimsufi-ca",
|
||||
"soyoustart-eu",
|
||||
"soyoustart-ca"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
}
|
||||
]
|
||||
},
|
||||
"application_secret": {
|
||||
"description": "OVH Application Secret",
|
||||
"type": "string"
|
||||
},
|
||||
"consumer_key": {
|
||||
"description": "OVH Consumer Key",
|
||||
"type": "string"
|
||||
},
|
||||
"application_key": {
|
||||
"description": "OVH Application Key",
|
||||
"type": "string"
|
||||
},
|
||||
"oauth2_config": {
|
||||
"description": "OVH OAuth2 config",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"client_id": {
|
||||
"description": "OVH Client ID",
|
||||
"type": "string"
|
||||
},
|
||||
"client_secret": {
|
||||
"description": "OVH Client Secret",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["client_id", "client_secret"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
set -e
|
||||
if [ -z "$BRANCH" ]; then
|
||||
BRANCH="main"
|
||||
BRANCH="v0.5"
|
||||
fi
|
||||
BASE_URL="https://github.com/yusing/go-proxy/raw/${BRANCH}"
|
||||
mkdir -p go-proxy
|
||||
|
||||
@@ -33,7 +33,7 @@ func SetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
content, err := E.Check(io.ReadAll(r.Body))
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
U.HandleErr(w, r, err)
|
||||
return
|
||||
}
|
||||
@@ -44,13 +44,13 @@ func SetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||
err = provider.Validate(content)
|
||||
}
|
||||
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
U.HandleErr(w, r, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = E.From(os.WriteFile(path.Join(common.ConfigBasePath, filename), content, 0644))
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
U.HandleErr(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -29,10 +29,10 @@ func List(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func listRoutes(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
routes := cfg.RoutesByAlias()
|
||||
type_filter := r.FormValue("type")
|
||||
if type_filter != "" {
|
||||
typeFilter := r.FormValue("type")
|
||||
if typeFilter != "" {
|
||||
for k, v := range routes {
|
||||
if v["type"] != type_filter {
|
||||
if v["type"] != typeFilter {
|
||||
delete(routes, k)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func Reload(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
if err := cfg.Reload(); err.IsNotNil() {
|
||||
if err := cfg.Reload(); err.HasError() {
|
||||
U.HandleErr(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func Stats(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
stats := map[string]interface{}{
|
||||
stats := map[string]any{
|
||||
"proxies": cfg.Statistics(),
|
||||
"uptime": utils.FormatDuration(server.GetProxyServer().Uptime()),
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ func NewConfig(cfg *M.AutoCertConfig) *Config {
|
||||
if cfg.KeyPath == "" {
|
||||
cfg.KeyPath = KeyFileDefault
|
||||
}
|
||||
if cfg.Provider == "" {
|
||||
cfg.Provider = ProviderLocal
|
||||
}
|
||||
return (*Config)(cfg)
|
||||
}
|
||||
|
||||
@@ -36,43 +39,35 @@ func (cfg *Config) GetProvider() (*Provider, E.NestedError) {
|
||||
if cfg.Email == "" {
|
||||
errors.Addf("no email specified")
|
||||
}
|
||||
// check if provider is implemented
|
||||
_, ok := providersGenMap[cfg.Provider]
|
||||
if !ok {
|
||||
errors.Addf("unknown provider: %q", cfg.Provider)
|
||||
}
|
||||
}
|
||||
|
||||
gen, ok := providersGenMap[cfg.Provider]
|
||||
if !ok {
|
||||
errors.Addf("unknown provider: %q", cfg.Provider)
|
||||
}
|
||||
if err := errors.Build(); err.IsNotNil() {
|
||||
if err := errors.Build(); err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privKey, err := E.Check(ecdsa.GenerateKey(elliptic.P256(), rand.Reader))
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, E.Failure("generate private key").With(err)
|
||||
}
|
||||
|
||||
user := &User{
|
||||
Email: cfg.Email,
|
||||
key: privKey,
|
||||
}
|
||||
|
||||
legoCfg := lego.NewConfig(user)
|
||||
legoCfg.Certificate.KeyType = certcrypto.RSA2048
|
||||
legoClient, err := E.Check(lego.NewClient(legoCfg))
|
||||
if err.IsNotNil() {
|
||||
return nil, E.Failure("create lego client").With(err)
|
||||
}
|
||||
|
||||
base := &Provider{
|
||||
cfg: cfg,
|
||||
user: user,
|
||||
legoCfg: legoCfg,
|
||||
client: legoClient,
|
||||
}
|
||||
legoProvider, err := E.Check(gen(cfg.Options))
|
||||
if err.IsNotNil() {
|
||||
return nil, E.Failure("create lego provider").With(err)
|
||||
}
|
||||
err = E.From(legoClient.Challenge.SetDNS01Provider(legoProvider))
|
||||
if err.IsNotNil() {
|
||||
return nil, E.Failure("set challenge provider").With(err)
|
||||
}
|
||||
|
||||
return base, E.Nil()
|
||||
}
|
||||
|
||||
@@ -4,13 +4,15 @@ import (
|
||||
"github.com/go-acme/lego/v4/providers/dns/clouddns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
"github.com/go-acme/lego/v4/providers/dns/duckdns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
certBasePath = "certs/"
|
||||
CertFileDefault = certBasePath + "cert.crt"
|
||||
KeyFileDefault = certBasePath + "priv.key"
|
||||
certBasePath = "certs/"
|
||||
CertFileDefault = certBasePath + "cert.crt"
|
||||
KeyFileDefault = certBasePath + "priv.key"
|
||||
RegistrationFile = certBasePath + "registration.json"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,14 +20,15 @@ const (
|
||||
ProviderCloudflare = "cloudflare"
|
||||
ProviderClouddns = "clouddns"
|
||||
ProviderDuckdns = "duckdns"
|
||||
ProviderOVH = "ovh"
|
||||
)
|
||||
|
||||
var providersGenMap = map[string]ProviderGenerator{
|
||||
"": providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
|
||||
ProviderLocal: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
|
||||
ProviderCloudflare: providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
|
||||
ProviderClouddns: providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
|
||||
ProviderDuckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
|
||||
ProviderOVH: providerGenerator(ovh.NewDefaultConfig, ovh.NewDNSProviderConfig),
|
||||
}
|
||||
|
||||
var Logger = logrus.WithField("module", "autocert")
|
||||
var logger = logrus.WithField("module", "autocert")
|
||||
|
||||
@@ -5,18 +5,17 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"github.com/sirupsen/logrus"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
M "github.com/yusing/go-proxy/models"
|
||||
"github.com/yusing/go-proxy/utils"
|
||||
U "github.com/yusing/go-proxy/utils"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
@@ -27,10 +26,9 @@ type Provider struct {
|
||||
|
||||
tlsCert *tls.Certificate
|
||||
certExpiries CertExpiries
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
type ProviderGenerator func(M.AutocertProviderOpt) (challenge.Provider, error)
|
||||
type ProviderGenerator func(M.AutocertProviderOpt) (challenge.Provider, E.NestedError)
|
||||
type CertExpiries map[string]time.Time
|
||||
|
||||
func (p *Provider) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
@@ -57,59 +55,72 @@ func (p *Provider) GetExpiries() CertExpiries {
|
||||
}
|
||||
|
||||
func (p *Provider) ObtainCert() E.NestedError {
|
||||
if p.cfg.Provider == ProviderLocal {
|
||||
return E.FailureWhy("obtain cert", "provider is set to \"local\"")
|
||||
}
|
||||
|
||||
if p.client == nil {
|
||||
if err := p.initClient(); err.HasError() {
|
||||
return E.Failure("obtain cert").With(err)
|
||||
}
|
||||
}
|
||||
|
||||
ne := E.Failure("obtain certificate")
|
||||
|
||||
client := p.client
|
||||
if p.user.Registration == nil {
|
||||
reg, err := E.Check(client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}))
|
||||
if err.IsNotNil() {
|
||||
return ne.With(E.Failure("register account").With(err))
|
||||
if err := p.loadRegistration(); err.HasError() {
|
||||
ne = ne.With(err)
|
||||
if err := p.registerACME(); err.HasError() {
|
||||
return ne.With(err)
|
||||
}
|
||||
}
|
||||
p.user.Registration = reg
|
||||
}
|
||||
req := certificate.ObtainRequest{
|
||||
Domains: p.cfg.Domains,
|
||||
Bundle: true,
|
||||
}
|
||||
cert, err := E.Check(client.Certificate.Obtain(req))
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return ne.With(err)
|
||||
}
|
||||
err = p.saveCert(cert)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return ne.With(E.Failure("save certificate").With(err))
|
||||
}
|
||||
tlsCert, err := E.Check(tls.X509KeyPair(cert.Certificate, cert.PrivateKey))
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return ne.With(E.Failure("parse obtained certificate").With(err))
|
||||
}
|
||||
expiries, err := getCertExpiries(&tlsCert)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return ne.With(E.Failure("get certificate expiry").With(err))
|
||||
}
|
||||
p.tlsCert = &tlsCert
|
||||
p.certExpiries = expiries
|
||||
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
func (p *Provider) LoadCert() E.NestedError {
|
||||
cert, err := E.Check(tls.LoadX509KeyPair(p.cfg.CertPath, p.cfg.KeyPath))
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return err
|
||||
}
|
||||
expiries, err := getCertExpiries(&cert)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return err
|
||||
}
|
||||
p.tlsCert = &cert
|
||||
p.certExpiries = expiries
|
||||
p.renewIfNeeded()
|
||||
return E.Nil()
|
||||
|
||||
logger.Infof("next renewal in %v", time.Until(p.ShouldRenewOn()))
|
||||
return p.renewIfNeeded()
|
||||
}
|
||||
|
||||
func (p *Provider) ShouldRenewOn() time.Time {
|
||||
for _, expiry := range p.certExpiries {
|
||||
return expiry.AddDate(0, -1, 0)
|
||||
return expiry.AddDate(0, -1, 0) // 1 month before
|
||||
}
|
||||
// this line should never be reached
|
||||
panic("no certificate available")
|
||||
@@ -120,139 +131,161 @@ func (p *Provider) ScheduleRenewal(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("starting renewal scheduler")
|
||||
logger.Debug("started renewal scheduler")
|
||||
defer logger.Debug("renewal scheduler stopped")
|
||||
|
||||
stop := make(chan struct{})
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
t := time.Until(p.ShouldRenewOn())
|
||||
Logger.Infof("next renewal in %v", t.Round(time.Second))
|
||||
go func() {
|
||||
<-time.After(t)
|
||||
close(stop)
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-stop:
|
||||
if err := p.renewIfNeeded(); err.IsNotNil() {
|
||||
Logger.Fatal(err)
|
||||
}
|
||||
case <-ticker.C: // check every 5 seconds
|
||||
if err := p.renewIfNeeded(); err.HasError() {
|
||||
logger.Warn(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) initClient() E.NestedError {
|
||||
legoClient, err := E.Check(lego.NewClient(p.legoCfg))
|
||||
if err.HasError() {
|
||||
return E.Failure("create lego client").With(err)
|
||||
}
|
||||
|
||||
legoProvider, err := providersGenMap[p.cfg.Provider](p.cfg.Options)
|
||||
if err.HasError() {
|
||||
return E.Failure("create lego provider").With(err)
|
||||
}
|
||||
|
||||
err = E.From(legoClient.Challenge.SetDNS01Provider(legoProvider))
|
||||
if err.HasError() {
|
||||
return E.Failure("set challenge provider").With(err)
|
||||
}
|
||||
|
||||
p.client = legoClient
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
func (p *Provider) registerACME() E.NestedError {
|
||||
if p.user.Registration != nil {
|
||||
return E.Nil()
|
||||
}
|
||||
reg, err := E.Check(p.client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}))
|
||||
if err.HasError() {
|
||||
return E.Failure("register ACME").With(err)
|
||||
}
|
||||
p.user.Registration = reg
|
||||
|
||||
if err := p.saveRegistration(); err.HasError() {
|
||||
logger.Warn(err)
|
||||
}
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
func (p *Provider) loadRegistration() E.NestedError {
|
||||
if p.user.Registration != nil {
|
||||
return E.Nil()
|
||||
}
|
||||
reg := ®istration.Resource{}
|
||||
err := U.LoadJson(RegistrationFile, reg)
|
||||
if err.HasError() {
|
||||
return E.Failure("parse registration file").With(err)
|
||||
}
|
||||
p.user.Registration = reg
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
func (p *Provider) saveRegistration() E.NestedError {
|
||||
return U.SaveJson(RegistrationFile, p.user.Registration, 0o600)
|
||||
}
|
||||
|
||||
func (p *Provider) saveCert(cert *certificate.Resource) E.NestedError {
|
||||
err := os.WriteFile(p.cfg.KeyPath, cert.PrivateKey, 0600) // -rw-------
|
||||
err := os.WriteFile(p.cfg.KeyPath, cert.PrivateKey, 0o600) // -rw-------
|
||||
if err != nil {
|
||||
return E.Failure("write key file").With(err)
|
||||
}
|
||||
err = os.WriteFile(p.cfg.CertPath, cert.Certificate, 0644) // -rw-r--r--
|
||||
err = os.WriteFile(p.cfg.CertPath, cert.Certificate, 0o644) // -rw-r--r--
|
||||
if err != nil {
|
||||
return E.Failure("write cert file").With(err)
|
||||
}
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
func (p *Provider) needRenewal() bool {
|
||||
expired := time.Now().After(p.ShouldRenewOn())
|
||||
if expired {
|
||||
return true
|
||||
func (p *Provider) certState() CertState {
|
||||
if time.Now().After(p.ShouldRenewOn()) {
|
||||
return CertStateExpired
|
||||
}
|
||||
if len(p.cfg.Domains) != len(p.certExpiries) {
|
||||
return true
|
||||
}
|
||||
wantedDomains := make([]string, len(p.cfg.Domains))
|
||||
|
||||
certDomains := make([]string, len(p.certExpiries))
|
||||
copy(wantedDomains, p.cfg.Domains)
|
||||
wantedDomains := make([]string, len(p.cfg.Domains))
|
||||
i := 0
|
||||
for domain := range p.certExpiries {
|
||||
certDomains[i] = domain
|
||||
i++
|
||||
}
|
||||
slices.Sort(wantedDomains)
|
||||
slices.Sort(certDomains)
|
||||
for i, domain := range certDomains {
|
||||
if domain != wantedDomains[i] {
|
||||
return true
|
||||
}
|
||||
copy(wantedDomains, p.cfg.Domains)
|
||||
sort.Strings(wantedDomains)
|
||||
sort.Strings(certDomains)
|
||||
|
||||
if !reflect.DeepEqual(certDomains, wantedDomains) {
|
||||
logger.Debugf("cert domains mismatch: %v != %v", certDomains, p.cfg.Domains)
|
||||
return CertStateMismatch
|
||||
}
|
||||
return false
|
||||
|
||||
return CertStateValid
|
||||
}
|
||||
|
||||
func (p *Provider) renewIfNeeded() E.NestedError {
|
||||
if !p.needRenewal() {
|
||||
switch p.certState() {
|
||||
case CertStateExpired:
|
||||
logger.Info("certs expired, renewing")
|
||||
case CertStateMismatch:
|
||||
logger.Info("cert domains mismatch with config, renewing")
|
||||
default:
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
if !p.needRenewal() {
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
trials := 0
|
||||
for {
|
||||
err := p.ObtainCert()
|
||||
if err.IsNotNil() {
|
||||
return E.Nil()
|
||||
}
|
||||
trials++
|
||||
if trials > 3 {
|
||||
return E.Failure("renew certificate").With(err)
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
if err := p.ObtainCert(); err.HasError() {
|
||||
return E.Failure("renew certificate").With(err)
|
||||
}
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
func getCertExpiries(cert *tls.Certificate) (CertExpiries, E.NestedError) {
|
||||
r := make(CertExpiries, len(cert.Certificate))
|
||||
for _, cert := range cert.Certificate {
|
||||
x509Cert, err := E.Check(x509.ParseCertificate(cert))
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, E.Failure("parse certificate").With(err)
|
||||
}
|
||||
if x509Cert.IsCA {
|
||||
continue
|
||||
}
|
||||
r[x509Cert.Subject.CommonName] = x509Cert.NotAfter
|
||||
}
|
||||
return r, E.Nil()
|
||||
}
|
||||
|
||||
func setOptions[T interface{}](cfg *T, opt M.AutocertProviderOpt) E.NestedError {
|
||||
for k, v := range opt {
|
||||
err := utils.SetFieldFromSnake(cfg, k, v)
|
||||
if err.IsNotNil() {
|
||||
return E.Failure("set autocert option").Subject(k).With(err)
|
||||
for i := range x509Cert.DNSNames {
|
||||
r[x509Cert.DNSNames[i]] = x509Cert.NotAfter
|
||||
}
|
||||
}
|
||||
return E.Nil()
|
||||
return r, E.Nil()
|
||||
}
|
||||
|
||||
func providerGenerator[CT any, PT challenge.Provider](
|
||||
defaultCfg func() *CT,
|
||||
newProvider func(*CT) (PT, error),
|
||||
) ProviderGenerator {
|
||||
return func(opt M.AutocertProviderOpt) (challenge.Provider, error) {
|
||||
return func(opt M.AutocertProviderOpt) (challenge.Provider, E.NestedError) {
|
||||
cfg := defaultCfg()
|
||||
err := setOptions(cfg, opt)
|
||||
if err.IsNotNil() {
|
||||
err := U.Deserialize(opt, cfg)
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
p, err := E.Check(newProvider(cfg))
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
return p, E.Nil()
|
||||
}
|
||||
}
|
||||
|
||||
var logger = logrus.WithField("module", "autocert")
|
||||
|
||||
49
src/autocert/provider_test/ovh_test.go
Normal file
49
src/autocert/provider_test/ovh_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package provider_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
||||
. "github.com/yusing/go-proxy/utils"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// type Config struct {
|
||||
// APIEndpoint string
|
||||
|
||||
// ApplicationKey string
|
||||
// ApplicationSecret string
|
||||
// ConsumerKey string
|
||||
|
||||
// OAuth2Config *OAuth2Config
|
||||
|
||||
// PropagationTimeout time.Duration
|
||||
// PollingInterval time.Duration
|
||||
// TTL int
|
||||
// HTTPClient *http.Client
|
||||
// }
|
||||
|
||||
func TestOVH(t *testing.T) {
|
||||
cfg := &ovh.Config{}
|
||||
testYaml := `
|
||||
api_endpoint: https://eu.api.ovh.com
|
||||
application_key: <application_key>
|
||||
application_secret: <application_secret>
|
||||
consumer_key: <consumer_key>
|
||||
oauth2_config:
|
||||
client_id: <client_id>
|
||||
client_secret: <client_secret>
|
||||
`
|
||||
cfgExpected := &ovh.Config{
|
||||
APIEndpoint: "https://eu.api.ovh.com",
|
||||
ApplicationKey: "<application_key>",
|
||||
ApplicationSecret: "<application_secret>",
|
||||
ConsumerKey: "<consumer_key>",
|
||||
OAuth2Config: &ovh.OAuth2Config{ClientID: "<client_id>", ClientSecret: "<client_secret>"},
|
||||
}
|
||||
testYaml = testYaml[1:] // remove first \n
|
||||
opt := make(map[string]any)
|
||||
ExpectNoError(t, yaml.Unmarshal([]byte(testYaml), opt))
|
||||
ExpectNoError(t, Deserialize(opt, cfg))
|
||||
ExpectEqual(t, cfg, cfgExpected)
|
||||
}
|
||||
9
src/autocert/state.go
Normal file
9
src/autocert/state.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package autocert
|
||||
|
||||
type CertState int
|
||||
|
||||
const (
|
||||
CertStateValid CertState = 0
|
||||
CertStateExpired CertState = iota
|
||||
CertStateMismatch CertState = iota
|
||||
)
|
||||
@@ -12,18 +12,19 @@ type Args struct {
|
||||
}
|
||||
|
||||
const (
|
||||
CommandStart = ""
|
||||
CommandValidate = "validate"
|
||||
CommandReload = "reload"
|
||||
CommandStart = ""
|
||||
CommandValidate = "validate"
|
||||
CommandListConfigs = "ls-config"
|
||||
CommandReload = "reload"
|
||||
)
|
||||
|
||||
var ValidCommands = []string{CommandStart, CommandValidate, CommandReload}
|
||||
var ValidCommands = []string{CommandStart, CommandValidate, CommandListConfigs, CommandReload}
|
||||
|
||||
func GetArgs() Args {
|
||||
var args Args
|
||||
flag.Parse()
|
||||
args.Command = flag.Arg(0)
|
||||
if err := validateArgs(args.Command, ValidCommands); err.IsNotNil() {
|
||||
if err := validateArgs(args.Command, ValidCommands); err.HasError() {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
return args
|
||||
|
||||
@@ -3,21 +3,16 @@ package common
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var NoSchemaValidation = getEnvBool("GOPROXY_NO_SCHEMA_VALIDATION")
|
||||
var IsDebug = getEnvBool("GOPROXY_DEBUG")
|
||||
|
||||
var LogLevel = func() logrus.Level {
|
||||
if IsDebug {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
}
|
||||
return logrus.GetLevel()
|
||||
}()
|
||||
|
||||
func getEnvBool(key string) bool {
|
||||
v := os.Getenv(key)
|
||||
return v == "1" || strings.ToLower(v) == "true" || strings.ToLower(v) == "yes" || strings.ToLower(v) == "on"
|
||||
switch strings.ToLower(os.Getenv(key)) {
|
||||
case "1", "true", "yes", "on":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func New() (*Config, E.NestedError) {
|
||||
watcher: W.NewFileWatcher(common.ConfigFileName),
|
||||
reloadReq: make(chan struct{}, 1),
|
||||
}
|
||||
if err := cfg.load(); err.IsNotNil() {
|
||||
if err := cfg.load(); err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
cfg.startProviders()
|
||||
@@ -66,7 +66,7 @@ func (cfg *Config) Dispose() {
|
||||
|
||||
func (cfg *Config) Reload() E.NestedError {
|
||||
cfg.stopProviders()
|
||||
if err := cfg.load(); err.IsNotNil() {
|
||||
if err := cfg.load(); err.HasError() {
|
||||
return err
|
||||
}
|
||||
cfg.startProviders()
|
||||
@@ -95,7 +95,7 @@ func (cfg *Config) RoutesByAlias() map[string]U.SerializedObject {
|
||||
prName := p.GetName()
|
||||
p.GetCurrentRoutes().EachKV(func(a string, r R.Route) {
|
||||
obj, err := U.Serialize(r)
|
||||
if err != nil {
|
||||
if err.HasError() {
|
||||
cfg.l.Error(err)
|
||||
return
|
||||
}
|
||||
@@ -114,13 +114,13 @@ func (cfg *Config) RoutesByAlias() map[string]U.SerializedObject {
|
||||
return routes
|
||||
}
|
||||
|
||||
func (cfg *Config) Statistics() map[string]interface{} {
|
||||
func (cfg *Config) Statistics() map[string]any {
|
||||
nTotalStreams := 0
|
||||
nTotalRPs := 0
|
||||
providerStats := make(map[string]interface{})
|
||||
providerStats := make(map[string]any)
|
||||
|
||||
cfg.proxyProviders.Each(func(p *PR.Provider) {
|
||||
stats := make(map[string]interface{})
|
||||
stats := make(map[string]any)
|
||||
nStreams := 0
|
||||
nRPs := 0
|
||||
p.GetCurrentRoutes().EachKV(func(a string, r R.Route) {
|
||||
@@ -141,7 +141,7 @@ func (cfg *Config) Statistics() map[string]interface{} {
|
||||
providerStats[p.GetName()] = stats
|
||||
})
|
||||
|
||||
return map[string]interface{}{
|
||||
return map[string]any{
|
||||
"num_total_streams": nTotalStreams,
|
||||
"num_total_reverse_proxies": nTotalRPs,
|
||||
"providers": providerStats,
|
||||
@@ -156,7 +156,7 @@ func (cfg *Config) watchChanges() {
|
||||
case <-cfg.watcherCtx.Done():
|
||||
return
|
||||
case <-cfg.reloadReq:
|
||||
if err := cfg.Reload(); err.IsNotNil() {
|
||||
if err := cfg.Reload(); err.HasError() {
|
||||
cfg.l.Error(err)
|
||||
}
|
||||
}
|
||||
@@ -186,29 +186,29 @@ func (cfg *Config) load() E.NestedError {
|
||||
cfg.l.Debug("loading config")
|
||||
|
||||
data, err := cfg.reader.Read()
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return E.Failure("read config").With(err)
|
||||
}
|
||||
|
||||
model := M.DefaultConfig()
|
||||
if err := E.From(yaml.Unmarshal(data, model)); err.IsNotNil() {
|
||||
if err := E.From(yaml.Unmarshal(data, model)); err.HasError() {
|
||||
return E.Failure("parse config").With(err)
|
||||
}
|
||||
|
||||
if !common.NoSchemaValidation {
|
||||
if err = Validate(data); err.IsNotNil() {
|
||||
if err = Validate(data); err.HasError() {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
warnings := E.NewBuilder("errors loading config")
|
||||
|
||||
cfg.l.Debug("starting autocert")
|
||||
cfg.l.Debug("initializing autocert")
|
||||
ap, err := autocert.NewConfig(&model.AutoCert).GetProvider()
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
warnings.Add(E.Failure("autocert provider").With(err))
|
||||
} else {
|
||||
cfg.l.Debug("started autocert")
|
||||
cfg.l.Debug("initialized autocert")
|
||||
}
|
||||
cfg.autocertProvider = ap
|
||||
|
||||
@@ -226,7 +226,7 @@ func (cfg *Config) load() E.NestedError {
|
||||
|
||||
cfg.value = model
|
||||
|
||||
if err := warnings.Build(); err.IsNotNil() {
|
||||
if err := warnings.Build(); err.HasError() {
|
||||
cfg.l.Warn(err)
|
||||
}
|
||||
|
||||
@@ -238,12 +238,12 @@ func (cfg *Config) controlProviders(action string, do func(*PR.Provider) E.Neste
|
||||
errors := E.NewBuilder("cannot %s these providers", action)
|
||||
|
||||
cfg.proxyProviders.EachKVParallel(func(name string, p *PR.Provider) {
|
||||
if err := do(p); err.IsNotNil() {
|
||||
if err := do(p); err.HasError() {
|
||||
errors.Add(E.From(err).Subject(p))
|
||||
}
|
||||
})
|
||||
|
||||
if err := errors.Build(); err.IsNotNil() {
|
||||
if err := errors.Build(); err.HasError() {
|
||||
cfg.l.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func ConnectClient(host string) (Client, E.NestedError) {
|
||||
opt = clientOptEnvHost
|
||||
default:
|
||||
helper, err := E.Check(connhelper.GetConnectionHelper(host))
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
logger.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if helper != nil {
|
||||
@@ -65,7 +65,7 @@ func ConnectClient(host string) (Client, E.NestedError) {
|
||||
|
||||
client, err := E.Check(client.NewClientWithOpts(opt...))
|
||||
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -18,15 +18,15 @@ type ClientInfo struct {
|
||||
|
||||
func GetClientInfo(clientHost string) (*ClientInfo, E.NestedError) {
|
||||
dockerClient, err := ConnectClient(clientHost)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, E.Failure("create docker client").With(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
containers, err := E.Check(dockerClient.ContainerList(ctx, container.ListOptions{All: true}))
|
||||
if err.IsNotNil() {
|
||||
containers, err := E.Check(dockerClient.ContainerList(ctx, container.ListOptions{}))
|
||||
if err.HasError() {
|
||||
return nil, E.Failure("list containers").With(err)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func GetClientInfo(clientHost string) (*ClientInfo, E.NestedError) {
|
||||
// since the services being proxied to
|
||||
// should have the same IP as the docker client
|
||||
url, err := E.Check(client.ParseHostURL(dockerClient.DaemonHost()))
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, E.Invalid("host url", dockerClient.DaemonHost()).With(err)
|
||||
}
|
||||
if url.Scheme == "unix" {
|
||||
|
||||
@@ -9,7 +9,7 @@ type (
|
||||
Icon string
|
||||
Category string
|
||||
Description string
|
||||
WidgetConfig map[string]interface{}
|
||||
WidgetConfig map[string]any
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ func ParseLabel(label string, value string) (*Label, E.NestedError) {
|
||||
}
|
||||
// try to parse value
|
||||
v, err := p(value)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
l.Value = v
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
. "github.com/yusing/go-proxy/utils"
|
||||
)
|
||||
|
||||
func makeLabel(namespace string, alias string, field string) string {
|
||||
@@ -18,29 +19,23 @@ func TestHomePageLabel(t *testing.T) {
|
||||
field := "ip"
|
||||
v := "bar"
|
||||
pl, err := ParseLabel(makeLabel(NSHomePage, alias, field), v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
ExpectNoError(t, err)
|
||||
if pl.Target != alias {
|
||||
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
|
||||
t.Errorf("Expected alias=%s, got %s", alias, pl.Target)
|
||||
}
|
||||
if pl.Attribute != field {
|
||||
t.Errorf("expected field=%s, got %s", field, pl.Target)
|
||||
t.Errorf("Expected field=%s, got %s", field, pl.Target)
|
||||
}
|
||||
if pl.Value != v {
|
||||
t.Errorf("expected value=%q, got %s", v, pl.Value)
|
||||
t.Errorf("Expected value=%q, got %s", v, pl.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringProxyLabel(t *testing.T) {
|
||||
v := "bar"
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "ip"), v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
if pl.Value != v {
|
||||
t.Errorf("expected value=%q, got %s", v, pl.Value)
|
||||
}
|
||||
ExpectNoError(t, err)
|
||||
ExpectEqual(t, pl.Value, v)
|
||||
}
|
||||
|
||||
func TestBoolProxyLabelValid(t *testing.T) {
|
||||
@@ -57,12 +52,8 @@ func TestBoolProxyLabelValid(t *testing.T) {
|
||||
|
||||
for k, v := range tests {
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "no_tls_verify"), k)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
if pl.Value != v {
|
||||
t.Errorf("expected value=%v, got %v", v, pl.Value)
|
||||
}
|
||||
ExpectNoError(t, err)
|
||||
ExpectEqual(t, pl.Value, v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +62,7 @@ func TestBoolProxyLabelInvalid(t *testing.T) {
|
||||
field := "no_tls_verify"
|
||||
_, err := ParseLabel(makeLabel(NSProxy, alias, field), "invalid")
|
||||
if !err.Is(E.ErrInvalid) {
|
||||
t.Errorf("expected err InvalidProxyLabel, got %s", err.Error())
|
||||
t.Errorf("Expected err InvalidProxyLabel, got %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,17 +78,12 @@ X-Custom-Header2: boo`
|
||||
}
|
||||
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "set_headers"), v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
hGot, ok := pl.Value.(map[string]string)
|
||||
if !ok {
|
||||
t.Errorf("value is not a map[string]string, but %T", pl.Value)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(h, hGot) {
|
||||
t.Errorf("expected %v, got %v", h, hGot)
|
||||
ExpectNoError(t, err)
|
||||
hGot := ExpectType[map[string]string](t, pl.Value)
|
||||
if hGot != nil && !reflect.DeepEqual(h, hGot) {
|
||||
t.Errorf("Expected %v, got %v", h, hGot)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSetHeaderProxyLabelInvalid(t *testing.T) {
|
||||
@@ -110,7 +96,7 @@ func TestSetHeaderProxyLabelInvalid(t *testing.T) {
|
||||
for _, v := range tests {
|
||||
_, err := ParseLabel(makeLabel(NSProxy, "foo", "set_headers"), v)
|
||||
if !err.Is(E.ErrInvalid) {
|
||||
t.Errorf("expected invalid err for %q, got %s", v, err.Error())
|
||||
t.Errorf("Expected invalid err for %q, got %s", v, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,47 +109,33 @@ func TestHideHeadersProxyLabel(t *testing.T) {
|
||||
`
|
||||
v = strings.TrimPrefix(v, "\n")
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "hide_headers"), v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
sGot, ok := pl.Value.([]string)
|
||||
ExpectNoError(t, err)
|
||||
sGot := ExpectType[[]string](t, pl.Value)
|
||||
sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"}
|
||||
if !ok {
|
||||
t.Errorf("value is not []string, but %T", pl.Value)
|
||||
}
|
||||
if !reflect.DeepEqual(sGot, sWant) {
|
||||
t.Errorf("expected %q, got %q", sWant, sGot)
|
||||
if sGot != nil {
|
||||
ExpectEqual(t, sGot, sWant)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommaSepProxyLabelSingle(t *testing.T) {
|
||||
v := "a"
|
||||
pl, err := ParseLabel("proxy.aliases", v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
sGot, ok := pl.Value.([]string)
|
||||
ExpectNoError(t, err)
|
||||
sGot := ExpectType[[]string](t, pl.Value)
|
||||
sWant := []string{"a"}
|
||||
if !ok {
|
||||
t.Errorf("value is not []string, but %T", pl.Value)
|
||||
}
|
||||
if !reflect.DeepEqual(sGot, sWant) {
|
||||
t.Errorf("expected %q, got %q", sWant, sGot)
|
||||
if sGot != nil {
|
||||
ExpectEqual(t, sGot, sWant)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCommaSepProxyLabelMulti(t *testing.T) {
|
||||
v := "X-Custom-Header1, X-Custom-Header2,X-Custom-Header3"
|
||||
pl, err := ParseLabel("proxy.aliases", v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
sGot, ok := pl.Value.([]string)
|
||||
ExpectNoError(t, err)
|
||||
sGot := ExpectType[[]string](t, pl.Value)
|
||||
sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"}
|
||||
if !ok {
|
||||
t.Errorf("value is not []string, but %T", pl.Value)
|
||||
}
|
||||
if !reflect.DeepEqual(sGot, sWant) {
|
||||
t.Errorf("expected %q, got %q", sWant, sGot)
|
||||
if sGot != nil {
|
||||
ExpectEqual(t, sGot, sWant)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,11 +118,11 @@ func (ne NestedError) Subjectf(format string, args ...any) NestedError {
|
||||
return ne
|
||||
}
|
||||
|
||||
func (ne NestedError) IsNil() bool {
|
||||
func (ne NestedError) NoError() bool {
|
||||
return ne.err == nil
|
||||
}
|
||||
|
||||
func (ne NestedError) IsNotNil() bool {
|
||||
func (ne NestedError) HasError() bool {
|
||||
return ne.err != nil
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ func (ne *NestedError) writeToSB(sb *strings.Builder, level int, prefix string)
|
||||
ne.writeIndents(sb, level)
|
||||
sb.WriteString(prefix)
|
||||
|
||||
if ne.IsNil() {
|
||||
if ne.NoError() {
|
||||
sb.WriteString("nil")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,46 +1,42 @@
|
||||
package error
|
||||
package error_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/go-proxy/error"
|
||||
. "github.com/yusing/go-proxy/utils"
|
||||
)
|
||||
|
||||
func AssertEq[T comparable](t *testing.T, got, want T) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
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)
|
||||
ExpectTrue(t, Failure("foo").Is(ErrFailure))
|
||||
ExpectTrue(t, Failure("foo").With("bar").Is(ErrFailure))
|
||||
ExpectFalse(t, Failure("foo").With("bar").Is(ErrInvalid))
|
||||
ExpectFalse(t, Failure("foo").With("bar").With("baz").Is(ErrInvalid))
|
||||
|
||||
AssertEq(t, Invalid("foo", "bar").Is(ErrInvalid), true)
|
||||
AssertEq(t, Invalid("foo", "bar").Is(ErrFailure), false)
|
||||
ExpectTrue(t, Invalid("foo", "bar").Is(ErrInvalid))
|
||||
ExpectFalse(t, Invalid("foo", "bar").Is(ErrFailure))
|
||||
|
||||
AssertEq(t, Nil().Is(nil), true)
|
||||
AssertEq(t, Nil().Is(ErrInvalid), false)
|
||||
AssertEq(t, Invalid("foo", "bar").Is(nil), false)
|
||||
ExpectTrue(t, Nil().Is(nil))
|
||||
ExpectFalse(t, Nil().Is(ErrInvalid))
|
||||
ExpectFalse(t, Invalid("foo", "bar").Is(nil))
|
||||
}
|
||||
|
||||
func TestNil(t *testing.T) {
|
||||
AssertEq(t, Nil().IsNil(), true)
|
||||
AssertEq(t, Nil().IsNotNil(), false)
|
||||
AssertEq(t, Nil().Error(), "nil")
|
||||
ExpectTrue(t, Nil().NoError())
|
||||
ExpectFalse(t, Nil().HasError())
|
||||
ExpectEqual(t, Nil().Error(), "nil")
|
||||
}
|
||||
|
||||
func TestErrorSimple(t *testing.T) {
|
||||
ne := Failure("foo bar")
|
||||
AssertEq(t, ne.Error(), "foo bar failed")
|
||||
ExpectEqual(t, ne.Error(), "foo bar failed")
|
||||
ne = ne.Subject("baz")
|
||||
AssertEq(t, ne.Error(), "foo bar failed for \"baz\"")
|
||||
ExpectEqual(t, ne.Error(), "foo bar failed for \"baz\"")
|
||||
}
|
||||
|
||||
func TestErrorWith(t *testing.T) {
|
||||
ne := Failure("foo").With("bar").With("baz")
|
||||
AssertEq(t, ne.Error(), "foo failed:\n - bar\n - baz")
|
||||
ExpectEqual(t, ne.Error(), "foo failed:\n - bar\n - baz")
|
||||
}
|
||||
|
||||
func TestErrorNested(t *testing.T) {
|
||||
@@ -76,5 +72,5 @@ func TestErrorNested(t *testing.T) {
|
||||
- inner3 failed for "action 3":
|
||||
- 3
|
||||
- 3`
|
||||
AssertEq(t, ne.Error(), want)
|
||||
ExpectEqual(t, ne.Error(), want)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ func Failure(what string) NestedError {
|
||||
return errorf("%s %w", what, ErrFailure)
|
||||
}
|
||||
|
||||
func FailureWhy(what string, why string) NestedError {
|
||||
return errorf("%s %w because %s", what, ErrFailure, why)
|
||||
}
|
||||
|
||||
func Invalid(subject, what any) NestedError {
|
||||
return errorf("%w %v - %v", ErrInvalid, subject, what)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
module github.com/yusing/go-proxy
|
||||
|
||||
go 1.22
|
||||
|
||||
toolchain go1.23.1
|
||||
go 1.22.0
|
||||
|
||||
require (
|
||||
github.com/docker/cli v27.2.1+incompatible
|
||||
@@ -36,6 +34,7 @@ require (
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/ovh/go-ovh v1.6.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
|
||||
go.opentelemetry.io/otel v1.30.0 // indirect
|
||||
@@ -45,10 +44,12 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.30.0 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
golang.org/x/tools v0.25.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
)
|
||||
|
||||
10
src/go.sum
10
src/go.sum
@@ -45,12 +45,16 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
|
||||
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
||||
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
|
||||
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
@@ -63,6 +67,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI=
|
||||
github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -110,6 +116,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -149,6 +157,8 @@ google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
61
src/main.go
61
src/main.go
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -33,14 +35,15 @@ func main() {
|
||||
}
|
||||
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
DisableSorting: true,
|
||||
FullTimestamp: true,
|
||||
ForceColors: true,
|
||||
TimestampFormat: "01-02 15:04:05",
|
||||
DisableSorting: true,
|
||||
DisableLevelTruncation: true,
|
||||
FullTimestamp: true,
|
||||
ForceColors: true,
|
||||
TimestampFormat: "01-02 15:04:05",
|
||||
})
|
||||
|
||||
if args.Command == common.CommandReload {
|
||||
if err := apiUtils.ReloadServer(); err.IsNotNil() {
|
||||
if err := apiUtils.ReloadServer(); err.HasError() {
|
||||
l.Fatal(err)
|
||||
}
|
||||
return
|
||||
@@ -52,10 +55,10 @@ func main() {
|
||||
if args.Command == common.CommandValidate {
|
||||
var err E.NestedError
|
||||
data, err := E.Check(os.ReadFile(common.ConfigPath))
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
l.WithError(err).Fatalf("config error")
|
||||
}
|
||||
if err = config.Validate(data); err.IsNotNil() {
|
||||
if err = config.Validate(data); err.HasError() {
|
||||
l.WithError(err).Fatalf("config error")
|
||||
}
|
||||
l.Printf("config OK")
|
||||
@@ -63,10 +66,20 @@ func main() {
|
||||
}
|
||||
|
||||
cfg, err := config.New()
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
l.Fatalf("config error: %s", err)
|
||||
}
|
||||
|
||||
if args.Command == common.CommandListConfigs {
|
||||
yml, err := E.Check(json.Marshal(cfg.Value()))
|
||||
if err.HasError() {
|
||||
panic(err)
|
||||
}
|
||||
rawLogger := log.New(os.Stdout, "", 0)
|
||||
rawLogger.Printf("%s", yml) // raw output for convenience using "jq"
|
||||
return
|
||||
}
|
||||
|
||||
onShutdown.Add(func() {
|
||||
docker.CloseAllClients()
|
||||
cfg.Dispose()
|
||||
@@ -80,23 +93,27 @@ func main() {
|
||||
autocert := cfg.GetAutoCertProvider()
|
||||
|
||||
if autocert != nil {
|
||||
err = autocert.LoadCert()
|
||||
|
||||
if err.IsNotNil() {
|
||||
l.Error(err)
|
||||
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 {
|
||||
if err = autocert.LoadCert(); err.HasError() {
|
||||
if !err.Is(os.ErrNotExist) { // ignore if cert doesn't exist
|
||||
l.Error(err)
|
||||
}
|
||||
l.Debug("obtaining cert due to error loading cert")
|
||||
if err = autocert.ObtainCert(); err.HasError() {
|
||||
l.Warn(err)
|
||||
}
|
||||
} else {
|
||||
for name, expiry := range autocert.GetExpiries() {
|
||||
l.Infof("certificate %q: expire on %s", name, expiry)
|
||||
}
|
||||
}
|
||||
|
||||
if err.NoError() {
|
||||
ctx, certRenewalCancel := context.WithCancel(context.Background())
|
||||
go autocert.ScheduleRenewal(ctx)
|
||||
onShutdown.Add(certRenewalCancel)
|
||||
}
|
||||
|
||||
for name, expiry := range autocert.GetExpiries() {
|
||||
l.Infof("certificate %q: expire on %s", name, expiry)
|
||||
}
|
||||
} else {
|
||||
l.Info("autocert not configured")
|
||||
}
|
||||
|
||||
proxyServer := server.InitProxyServer(server.Options{
|
||||
|
||||
@@ -9,5 +9,5 @@ type (
|
||||
Provider string `json:"provider"`
|
||||
Options AutocertProviderOpt `yaml:",flow" json:"options"`
|
||||
}
|
||||
AutocertProviderOpt map[string]string
|
||||
AutocertProviderOpt map[string]any
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@ type (
|
||||
func NewEntry(m *M.ProxyEntry) (any, E.NestedError) {
|
||||
m.SetDefaults()
|
||||
scheme, err := T.NewScheme(m.Scheme)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
if scheme.IsStream() {
|
||||
@@ -44,23 +44,23 @@ func NewEntry(m *M.ProxyEntry) (any, E.NestedError) {
|
||||
|
||||
func validateEntry(m *M.ProxyEntry, s T.Scheme) (*Entry, E.NestedError) {
|
||||
host, err := T.NewHost(m.Host)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
port, err := T.NewPort(m.Port)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
pathPatterns, err := T.NewPathPatterns(m.PathPatterns)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
setHeaders, err := T.NewHTTPHeaders(m.SetHeaders)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
url, err := E.Check(url.Parse(fmt.Sprintf("%s://%s:%d", s, host, port)))
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
return &Entry{
|
||||
@@ -78,15 +78,15 @@ func validateEntry(m *M.ProxyEntry, s T.Scheme) (*Entry, E.NestedError) {
|
||||
|
||||
func validateStreamEntry(m *M.ProxyEntry) (*StreamEntry, E.NestedError) {
|
||||
host, err := T.NewHost(m.Host)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
port, err := T.NewStreamPort(m.Port)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
scheme, err := T.NewStreamScheme(m.Scheme)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
return &StreamEntry{
|
||||
|
||||
@@ -25,7 +25,7 @@ func NewPathPatterns(s []string) (PathPatterns, E.NestedError) {
|
||||
}
|
||||
pp := make(PathPatterns, len(s))
|
||||
for i, v := range s {
|
||||
if pattern, err := NewPathPattern(v); err.IsNotNil() {
|
||||
if pattern, err := NewPathPattern(v); err.HasError() {
|
||||
return nil, err
|
||||
} else {
|
||||
pp[i] = pattern
|
||||
|
||||
@@ -16,9 +16,9 @@ func NewPort(v string) (Port, E.NestedError) {
|
||||
return NewPortInt(p)
|
||||
}
|
||||
|
||||
func NewPortInt(v int) (Port, E.NestedError) {
|
||||
func NewPortInt[Int int | uint16](v Int) (Port, E.NestedError) {
|
||||
pp := Port(v)
|
||||
if err := pp.boundCheck(); err.IsNotNil() {
|
||||
if err := pp.boundCheck(); err.HasError() {
|
||||
return ErrPort, err
|
||||
}
|
||||
return pp, E.Nil()
|
||||
|
||||
@@ -19,21 +19,21 @@ func NewStreamPort(p string) (StreamPort, E.NestedError) {
|
||||
}
|
||||
|
||||
listeningPort, err := NewPort(split[0])
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return StreamPort{}, err
|
||||
}
|
||||
if err = listeningPort.boundCheck(); err.IsNotNil() {
|
||||
if err = listeningPort.boundCheck(); err.HasError() {
|
||||
return StreamPort{}, err
|
||||
}
|
||||
|
||||
proxyPort, err := NewPort(split[1])
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
proxyPort, err = parseNameToPort(split[1])
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return StreamPort{}, err
|
||||
}
|
||||
}
|
||||
if err = proxyPort.boundCheck(); err.IsNotNil() {
|
||||
if err = proxyPort.boundCheck(); err.HasError() {
|
||||
return StreamPort{}, err
|
||||
}
|
||||
|
||||
|
||||
@@ -21,11 +21,11 @@ func NewStreamScheme(s string) (ss *StreamScheme, err E.NestedError) {
|
||||
return nil, E.Invalid("stream scheme", s)
|
||||
}
|
||||
ss.ListeningScheme, err = NewScheme(parts[0])
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
ss.ProxyScheme, err = NewScheme(parts[1])
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
return ss, E.Nil()
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
D "github.com/yusing/go-proxy/docker"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
M "github.com/yusing/go-proxy/models"
|
||||
@@ -38,15 +39,15 @@ func (p DockerProvider) GetProxyEntries() (M.ProxyEntries, E.NestedError) {
|
||||
entries := M.NewProxyEntries()
|
||||
|
||||
info, err := D.GetClientInfo(p.dockerHost)
|
||||
if err.IsNotNil() {
|
||||
return entries, E.From(err)
|
||||
if err.HasError() {
|
||||
return entries, err
|
||||
}
|
||||
|
||||
errors := E.NewBuilder("errors when parse docker labels for %q", p.dockerHost)
|
||||
errors := E.NewBuilder("errors when parse docker labels")
|
||||
|
||||
for _, container := range info.Containers {
|
||||
en, err := p.getEntriesFromLabels(&container, info.Host)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
errors.Add(err)
|
||||
}
|
||||
// although err is not nil
|
||||
@@ -93,9 +94,9 @@ func (p *DockerProvider) getEntriesFromLabels(container *types.Container, client
|
||||
entries := M.NewProxyEntries()
|
||||
|
||||
// find first port, return if no port exposed
|
||||
defaultPort := findFirstPort(container)
|
||||
if defaultPort == PT.NoPort {
|
||||
return entries, E.Nil()
|
||||
defaultPort, err := findFirstPort(container)
|
||||
if err.HasError() {
|
||||
logrus.Debug(mainAlias, " ", err.Error())
|
||||
}
|
||||
|
||||
// init entries map for all aliases
|
||||
@@ -103,14 +104,14 @@ func (p *DockerProvider) getEntriesFromLabels(container *types.Container, client
|
||||
entries.Set(string(a), &M.ProxyEntry{
|
||||
Alias: string(a),
|
||||
Host: clientHost,
|
||||
Port: fmt.Sprint(defaultPort),
|
||||
Port: defaultPort,
|
||||
})
|
||||
})
|
||||
|
||||
errors := E.NewBuilder("failed to apply label for %q", mainAlias)
|
||||
for key, val := range container.Labels {
|
||||
lbl, err := D.ParseLabel(key, val)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
errors.Add(E.From(err).Subject(key))
|
||||
continue
|
||||
}
|
||||
@@ -120,7 +121,7 @@ func (p *DockerProvider) getEntriesFromLabels(container *types.Container, client
|
||||
if lbl.Target == wildcardAlias {
|
||||
// apply label for all aliases
|
||||
entries.EachKV(func(a string, e *M.ProxyEntry) {
|
||||
if err = D.ApplyLabel(e, lbl); err.IsNotNil() {
|
||||
if err = D.ApplyLabel(e, lbl); err.HasError() {
|
||||
errors.Add(E.From(err).Subject(lbl.Target))
|
||||
}
|
||||
})
|
||||
@@ -130,21 +131,29 @@ func (p *DockerProvider) getEntriesFromLabels(container *types.Container, client
|
||||
errors.Add(E.NotExists("alias", lbl.Target))
|
||||
continue
|
||||
}
|
||||
if err = D.ApplyLabel(config, lbl); err.IsNotNil() {
|
||||
if err = D.ApplyLabel(config, lbl); err.HasError() {
|
||||
errors.Add(err.Subject(lbl.Target))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entries.EachKV(func(a string, e *M.ProxyEntry) {
|
||||
if e.Port == "" {
|
||||
entries.UnsafeDelete(a)
|
||||
}
|
||||
})
|
||||
|
||||
return entries, errors.Build()
|
||||
}
|
||||
|
||||
func findFirstPort(c *types.Container) (pp PT.Port) {
|
||||
func findFirstPort(c *types.Container) (string, E.NestedError) {
|
||||
if len(c.Ports) == 0 {
|
||||
return "", E.FailureWhy("findFirstPort", "no port exposed")
|
||||
}
|
||||
for _, p := range c.Ports {
|
||||
if p.PublicPort != 0 || c.HostConfig.NetworkMode == "host" {
|
||||
pp, _ = PT.NewPortInt(int(p.PublicPort))
|
||||
return
|
||||
if p.PublicPort != 0 {
|
||||
return fmt.Sprint(p.PublicPort), E.Nil()
|
||||
}
|
||||
}
|
||||
return PT.NoPort
|
||||
return "", E.Failure("findFirstPort")
|
||||
}
|
||||
|
||||
@@ -34,16 +34,16 @@ func (p *FileProvider) String() string {
|
||||
func (p *FileProvider) GetProxyEntries() (M.ProxyEntries, E.NestedError) {
|
||||
entries := M.NewProxyEntries()
|
||||
data, err := E.Check(os.ReadFile(p.path))
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return entries, E.Failure("read file").Subject(p).With(err)
|
||||
}
|
||||
ne := E.Failure("validation").Subject(p)
|
||||
if !common.NoSchemaValidation {
|
||||
if err = Validate(data); err.IsNotNil() {
|
||||
if err = Validate(data); err.HasError() {
|
||||
return entries, ne.With(err)
|
||||
}
|
||||
}
|
||||
if err = entries.UnmarshalFromYAML(data); err.IsNotNil() {
|
||||
if err = entries.UnmarshalFromYAML(data); err.HasError() {
|
||||
return entries, ne.With(err)
|
||||
}
|
||||
return entries, E.Nil()
|
||||
|
||||
@@ -92,12 +92,12 @@ func (p *Provider) StartAllRoutes() E.NestedError {
|
||||
nStarted := 0
|
||||
nFailed := 0
|
||||
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
errors.Add(err)
|
||||
}
|
||||
|
||||
p.routes.EachKVParallel(func(alias string, r R.Route) {
|
||||
if err := r.Start(); err.IsNotNil() {
|
||||
if err := r.Start(); err.HasError() {
|
||||
errors.Add(err.Subject(r))
|
||||
nFailed++
|
||||
} else {
|
||||
@@ -118,7 +118,7 @@ func (p *Provider) StopAllRoutes() E.NestedError {
|
||||
nStopped := 0
|
||||
nFailed := 0
|
||||
p.routes.EachKVParallel(func(alias string, r R.Route) {
|
||||
if err := r.Stop(); err.IsNotNil() {
|
||||
if err := r.Stop(); err.HasError() {
|
||||
errors.Add(err.Subject(r))
|
||||
nFailed++
|
||||
} else {
|
||||
@@ -175,12 +175,13 @@ func (p *Provider) processReloadRequests() {
|
||||
select {
|
||||
case p.cooldownCh <- struct{}{}:
|
||||
p.l.Info("Starting to reload routes")
|
||||
nRoutes := p.routes.Size()
|
||||
|
||||
p.StopAllRoutes()
|
||||
p.loadRoutes()
|
||||
p.StartAllRoutes()
|
||||
|
||||
p.l.Info("Routes reloaded")
|
||||
p.l.Infof("Routes reloaded (%d -> %d)", nRoutes, p.routes.Size())
|
||||
|
||||
go func() {
|
||||
time.Sleep(reloadCooldown)
|
||||
@@ -194,7 +195,7 @@ func (p *Provider) processReloadRequests() {
|
||||
func (p *Provider) loadRoutes() E.NestedError {
|
||||
entries, err := p.GetProxyEntries()
|
||||
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
p.l.Warn(err.Subject(p))
|
||||
}
|
||||
p.routes = R.NewRoutes()
|
||||
@@ -203,7 +204,7 @@ func (p *Provider) loadRoutes() E.NestedError {
|
||||
entries.EachKV(func(a string, e *M.ProxyEntry) {
|
||||
e.Alias = a
|
||||
r, err := R.NewRoute(e)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
errors.Add(err.Subject(a))
|
||||
} else {
|
||||
p.routes.Set(a, r)
|
||||
@@ -212,4 +213,4 @@ func (p *Provider) loadRoutes() E.NestedError {
|
||||
return errors.Build()
|
||||
}
|
||||
|
||||
const reloadCooldown = 300 * time.Millisecond
|
||||
const reloadCooldown = 50 * time.Millisecond
|
||||
|
||||
@@ -21,7 +21,7 @@ var NewRoutes = F.NewMap[string, Route]
|
||||
|
||||
func NewRoute(en *M.ProxyEntry) (Route, E.NestedError) {
|
||||
entry, err := P.NewEntry(en)
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
switch e := entry.(type) {
|
||||
|
||||
@@ -37,7 +37,6 @@ func NewStreamRoute(entry *P.StreamEntry) (*StreamRoute, E.NestedError) {
|
||||
base := &StreamRoute{
|
||||
StreamEntry: entry,
|
||||
wg: sync.WaitGroup{},
|
||||
stopCh: make(chan struct{}, 1),
|
||||
connCh: make(chan any),
|
||||
}
|
||||
if entry.Scheme.ListeningScheme.IsTCP() {
|
||||
@@ -57,6 +56,7 @@ func (r *StreamRoute) Start() E.NestedError {
|
||||
if r.started.Load() {
|
||||
return E.Invalid("state", "already started")
|
||||
}
|
||||
r.stopCh = make(chan struct{}, 1)
|
||||
r.wg.Wait()
|
||||
if err := r.Setup(); err != nil {
|
||||
return E.Failure("setup").With(err)
|
||||
|
||||
@@ -38,11 +38,11 @@ func (route *TCPRoute) Setup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (route *TCPRoute) Accept() (interface{}, error) {
|
||||
func (route *TCPRoute) Accept() (any, error) {
|
||||
return route.listener.Accept()
|
||||
}
|
||||
|
||||
func (route *TCPRoute) Handle(c interface{}) error {
|
||||
func (route *TCPRoute) Handle(c any) error {
|
||||
clientConn := c.(net.Conn)
|
||||
|
||||
defer clientConn.Close()
|
||||
@@ -78,7 +78,7 @@ func (route *TCPRoute) CloseListeners() {
|
||||
route.listener.Close()
|
||||
route.listener = nil
|
||||
for _, pipe := range route.pipe {
|
||||
if err := pipe.Stop(); err.IsNotNil() {
|
||||
if err := pipe.Stop(); err.HasError() {
|
||||
route.l.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func (route *UDPRoute) Setup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (route *UDPRoute) Accept() (interface{}, error) {
|
||||
func (route *UDPRoute) Accept() (any, error) {
|
||||
in := route.listeningConn
|
||||
|
||||
buffer := make([]byte, udpBufferSize)
|
||||
@@ -103,7 +103,7 @@ func (route *UDPRoute) Accept() (interface{}, error) {
|
||||
return conn, err
|
||||
}
|
||||
|
||||
func (route *UDPRoute) Handle(c interface{}) error {
|
||||
func (route *UDPRoute) Handle(c any) error {
|
||||
return c.(*UDPConn).Start()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,25 +2,25 @@ package functional
|
||||
|
||||
import "sync"
|
||||
|
||||
func ForEachKey[K comparable, V interface{}](obj map[K]V, do func(K)) {
|
||||
func ForEachKey[K comparable, V any](obj map[K]V, do func(K)) {
|
||||
for k := range obj {
|
||||
do(k)
|
||||
}
|
||||
}
|
||||
|
||||
func ForEachValue[K comparable, V interface{}](obj map[K]V, do func(V)) {
|
||||
func ForEachValue[K comparable, V any](obj map[K]V, do func(V)) {
|
||||
for _, v := range obj {
|
||||
do(v)
|
||||
}
|
||||
}
|
||||
|
||||
func ForEachKV[K comparable, V interface{}](obj map[K]V, do func(K, V)) {
|
||||
func ForEachKV[K comparable, V any](obj map[K]V, do func(K, V)) {
|
||||
for k, v := range obj {
|
||||
do(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func ParallelForEach[T interface{}](obj []T, do func(T)) {
|
||||
func ParallelForEach[T any](obj []T, do func(T)) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(obj))
|
||||
for _, v := range obj {
|
||||
@@ -32,7 +32,7 @@ func ParallelForEach[T interface{}](obj []T, do func(T)) {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func ParallelForEachKey[K comparable, V interface{}](obj map[K]V, do func(K)) {
|
||||
func ParallelForEachKey[K comparable, V any](obj map[K]V, do func(K)) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(obj))
|
||||
for k := range obj {
|
||||
@@ -44,7 +44,7 @@ func ParallelForEachKey[K comparable, V interface{}](obj map[K]V, do func(K)) {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func ParallelForEachValue[K comparable, V interface{}](obj map[K]V, do func(V)) {
|
||||
func ParallelForEachValue[K comparable, V any](obj map[K]V, do func(V)) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(obj))
|
||||
for _, v := range obj {
|
||||
@@ -56,7 +56,7 @@ func ParallelForEachValue[K comparable, V interface{}](obj map[K]V, do func(V))
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func ParallelForEachKV[K comparable, V interface{}](obj map[K]V, do func(K, V)) {
|
||||
func ParallelForEachKV[K comparable, V any](obj map[K]V, do func(K, V)) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(obj))
|
||||
for k, v := range obj {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
type Map[KT comparable, VT interface{}] struct {
|
||||
type Map[KT comparable, VT any] struct {
|
||||
m map[KT]VT
|
||||
defVals map[KT]VT
|
||||
sync.RWMutex
|
||||
@@ -22,7 +22,7 @@ type Map[KT comparable, VT interface{}] struct {
|
||||
//
|
||||
// Return:
|
||||
// - *Map[KT, VT]: a pointer to the newly created Map.
|
||||
func NewMap[KT comparable, VT interface{}](dv ...map[KT]VT) *Map[KT, VT] {
|
||||
func NewMap[KT comparable, VT any](dv ...map[KT]VT) *Map[KT, VT] {
|
||||
return NewMapFrom(make(map[KT]VT), dv...)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func NewMap[KT comparable, VT interface{}](dv ...map[KT]VT) *Map[KT, VT] {
|
||||
//
|
||||
// Return:
|
||||
// - *Map[KT, VT]: a pointer to the newly created Map.
|
||||
func NewMapOf[M Map[KT, VT], KT comparable, VT interface{}](dv ...map[KT]VT) *Map[KT, VT] {
|
||||
func NewMapOf[M Map[KT, VT], KT comparable, VT any](dv ...map[KT]VT) *Map[KT, VT] {
|
||||
return NewMapFrom(make(map[KT]VT), dv...)
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func NewMapOf[M Map[KT, VT], KT comparable, VT interface{}](dv ...map[KT]VT) *Ma
|
||||
//
|
||||
// Return:
|
||||
// - *Map[KT, VT]: a pointer to the newly created Map.
|
||||
func NewMapFrom[KT comparable, VT interface{}](from map[KT]VT, dv ...map[KT]VT) *Map[KT, VT] {
|
||||
func NewMapFrom[KT comparable, VT any](from map[KT]VT, dv ...map[KT]VT) *Map[KT, VT] {
|
||||
if len(dv) > 0 {
|
||||
return &Map[KT, VT]{m: from, defVals: dv[0]}
|
||||
}
|
||||
@@ -135,6 +135,10 @@ func (m *Map[KT, VT]) Delete(key KT) {
|
||||
m.Unlock()
|
||||
}
|
||||
|
||||
func (m *Map[KT, VT]) UnsafeDelete(key KT) {
|
||||
delete(m.m, key)
|
||||
}
|
||||
|
||||
// MergeWith merges the contents of another Map[KT, VT]
|
||||
// into the current Map[KT, VT] and
|
||||
// returns a map that were duplicated.
|
||||
|
||||
@@ -2,6 +2,7 @@ package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
@@ -135,7 +136,7 @@ func (p *BidirectionalPipe) Start() E.NestedError {
|
||||
errCh <- p.pDstSrc.Start()
|
||||
}()
|
||||
for err := range errCh {
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -149,4 +150,20 @@ func (p *BidirectionalPipe) Stop() E.NestedError {
|
||||
func Copy(ctx context.Context, dst io.WriteCloser, src io.ReadCloser) E.NestedError {
|
||||
_, err := io.Copy(dst, StdReadCloser{&ReadCloser{ctx: ctx, r: src}})
|
||||
return E.From(err)
|
||||
}
|
||||
}
|
||||
|
||||
func LoadJson[T any](path string, pointer *T) E.NestedError {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return E.From(err)
|
||||
}
|
||||
return E.From(json.Unmarshal(data, pointer))
|
||||
}
|
||||
|
||||
func SaveJson[T any](path string, pointer *T, perm os.FileMode) E.NestedError {
|
||||
data, err := json.Marshal(pointer)
|
||||
if err != nil {
|
||||
return E.From(err)
|
||||
}
|
||||
return E.From(os.WriteFile(path, data, perm))
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/santhosh-tekuri/jsonschema"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
@@ -12,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func ValidateYaml(schema *jsonschema.Schema, data []byte) E.NestedError {
|
||||
var i interface{}
|
||||
var i any
|
||||
|
||||
err := yaml.Unmarshal(data, &i)
|
||||
if err != nil {
|
||||
@@ -55,7 +56,7 @@ func TryJsonStringify(o any) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Serialize converts the given data into a map[string]interface{} representation.
|
||||
// Serialize converts the given data into a map[string]any representation.
|
||||
//
|
||||
// It uses reflection to inspect the data type and handle different kinds of data.
|
||||
// For a struct, it extracts the fields using the json tag if present, or the field name if not.
|
||||
@@ -66,9 +67,9 @@ func TryJsonStringify(o any) string {
|
||||
// - data: The data to be converted into a map.
|
||||
//
|
||||
// Returns:
|
||||
// - result: The resulting map[string]interface{} representation of the data.
|
||||
// - result: The resulting map[string]any representation of the data.
|
||||
// - error: An error if the data type is unsupported or if there is an error during conversion.
|
||||
func Serialize(data interface{}) (SerializedObject, error) {
|
||||
func Serialize(data any) (SerializedObject, E.NestedError) {
|
||||
result := make(map[string]any)
|
||||
|
||||
// Use reflection to inspect the data type
|
||||
@@ -76,7 +77,7 @@ func Serialize(data interface{}) (SerializedObject, error) {
|
||||
|
||||
// Check if the value is valid
|
||||
if !value.IsValid() {
|
||||
return nil, fmt.Errorf("invalid data")
|
||||
return nil, E.Invalid("data", fmt.Sprintf("type: %T", data))
|
||||
}
|
||||
|
||||
// Dereference pointers if necessary
|
||||
@@ -107,7 +108,7 @@ func Serialize(data interface{}) (SerializedObject, error) {
|
||||
} else if field.Anonymous {
|
||||
// If the field is an embedded struct, add its fields to the result
|
||||
fieldMap, err := Serialize(value.Field(i).Interface())
|
||||
if err != nil {
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range fieldMap {
|
||||
@@ -118,10 +119,72 @@ func Serialize(data interface{}) (SerializedObject, error) {
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type: %s", value.Kind())
|
||||
// return nil, fmt.Errorf("unsupported type: %s", value.Kind())
|
||||
return nil, E.Unsupported("type", value.Kind())
|
||||
}
|
||||
|
||||
return result, nil
|
||||
return result, E.Nil()
|
||||
}
|
||||
|
||||
type SerializedObject map[string]any
|
||||
func Deserialize(src map[string]any, target any) E.NestedError {
|
||||
// convert data fields to lower no-snake
|
||||
// convert target fields to lower
|
||||
// then check if the field of data is in the target
|
||||
mapping := make(map[string]string)
|
||||
t := reflect.TypeOf(target).Elem()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
snakeCaseField := strings.ToLower(field.Name)
|
||||
mapping[snakeCaseField] = field.Name
|
||||
}
|
||||
for k, v := range src {
|
||||
kCleaned := toLowerNoSnake(k)
|
||||
if fieldName, ok := mapping[kCleaned]; ok {
|
||||
prop := reflect.ValueOf(target).Elem().FieldByName(fieldName)
|
||||
propType := prop.Type()
|
||||
isPtr := prop.Kind() == reflect.Ptr
|
||||
if prop.CanSet() {
|
||||
val := reflect.ValueOf(v)
|
||||
vType := val.Type()
|
||||
switch {
|
||||
case isPtr && vType.ConvertibleTo(propType.Elem()):
|
||||
ptr := reflect.New(propType.Elem())
|
||||
ptr.Elem().Set(val.Convert(propType.Elem()))
|
||||
prop.Set(ptr)
|
||||
case vType.ConvertibleTo(propType):
|
||||
prop.Set(val.Convert(propType))
|
||||
case isPtr:
|
||||
var vSerialized SerializedObject
|
||||
vSerialized, ok = v.(SerializedObject)
|
||||
if !ok {
|
||||
if vType.ConvertibleTo(reflect.TypeFor[SerializedObject]()) {
|
||||
vSerialized = val.Convert(reflect.TypeFor[SerializedObject]()).Interface().(SerializedObject)
|
||||
} else {
|
||||
return E.Failure(fmt.Sprintf("convert %s (%T) to %s", k, v, reflect.TypeFor[SerializedObject]()))
|
||||
}
|
||||
}
|
||||
propNew := reflect.New(propType.Elem())
|
||||
err := Deserialize(vSerialized, propNew.Interface())
|
||||
if err.HasError() {
|
||||
return E.Failure("set field").With(k).With(err)
|
||||
}
|
||||
prop.Set(propNew)
|
||||
default:
|
||||
return E.Unsupported("field", k).Extraf("type=%s", propType)
|
||||
}
|
||||
} else {
|
||||
return E.Unsupported("field", k).Extraf("type=%s", propType)
|
||||
}
|
||||
} else {
|
||||
return E.Failure("unknown field").With(k)
|
||||
}
|
||||
}
|
||||
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
func toLowerNoSnake(s string) string {
|
||||
return strings.ToLower(strings.ReplaceAll(s, "_", ""))
|
||||
}
|
||||
|
||||
type SerializedObject = map[string]any
|
||||
|
||||
53
src/utils/testing.go
Normal file
53
src/utils/testing.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
func ExpectNoError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
var noError bool
|
||||
switch t := err.(type) {
|
||||
case E.NestedError:
|
||||
noError = t.NoError()
|
||||
default:
|
||||
noError = err == nil
|
||||
}
|
||||
if !noError {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectEqual(t *testing.T, got, want any) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("expected:\n%v, got\n%v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectTrue(t *testing.T, got bool) {
|
||||
t.Helper()
|
||||
if !got {
|
||||
t.Errorf("expected true, got false")
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectFalse(t *testing.T, got bool) {
|
||||
t.Helper()
|
||||
if got {
|
||||
t.Errorf("expected false, got true")
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectType[T any](t *testing.T, got any) T {
|
||||
t.Helper()
|
||||
tExpect := reflect.TypeFor[T]()
|
||||
_, ok := got.(T)
|
||||
if !ok {
|
||||
t.Errorf("expected type %T, got %T", tExpect, got)
|
||||
}
|
||||
return got.(T)
|
||||
}
|
||||
@@ -31,13 +31,13 @@ func (w *DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Nest
|
||||
var err E.NestedError
|
||||
for range 3 {
|
||||
cl, err = D.ConnectClient(w.host)
|
||||
if err.IsNil() {
|
||||
if err.NoError() {
|
||||
break
|
||||
}
|
||||
errCh <- E.From(err)
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
if err.IsNotNil() {
|
||||
if err.HasError() {
|
||||
errCh <- E.Failure("connecting to docker")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
0.5.0-rc2
|
||||
Reference in New Issue
Block a user