mirror of
https://github.com/yusing/godoxy.git
synced 2025-07-24 13:04:03 +02:00
v0.5.0-rc4: fixing autocert issue, cache ACME registration, added ls-config option
This commit is contained in:
parent
04fd6543fd
commit
82f06374f7
33 changed files with 301 additions and 221 deletions
|
@ -4,7 +4,7 @@ ENV GOCACHE=/root/.cache/go-build
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
RUN --mount=type=cache,target="/go/pkg/mod" \
|
RUN --mount=type=cache,target="/go/pkg/mod" \
|
||||||
--mount=type=cache,target="/root/.cache/go-build" \
|
--mount=type=cache,target="/root/.cache/go-build" \
|
||||||
go mod download
|
go mod download && \
|
||||||
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o go-proxy github.com/yusing/go-proxy
|
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o go-proxy github.com/yusing/go-proxy
|
||||||
|
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -27,7 +27,7 @@ get:
|
||||||
cd src && go get -u && go mod tidy && cd ..
|
cd src && go get -u && go mod tidy && cd ..
|
||||||
|
|
||||||
debug:
|
debug:
|
||||||
make build && GOPROXY_DEBUG=1 bin/go-proxy
|
make build && sudo GOPROXY_DEBUG=1 bin/go-proxy
|
||||||
|
|
||||||
archive:
|
archive:
|
||||||
git archive HEAD -o ../go-proxy-$$(date +"%Y%m%d%H%M").zip
|
git archive HEAD -o ../go-proxy-$$(date +"%Y%m%d%H%M").zip
|
||||||
|
|
19
README.md
19
README.md
|
@ -26,8 +26,10 @@ A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse pr
|
||||||
## Key Points
|
## Key Points
|
||||||
|
|
||||||
- Easy to use
|
- 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 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
|
- Auto hot-reload on container state / config file changes
|
||||||
- Support HTTP(s), TCP and UDP
|
- Support HTTP(s), TCP and UDP
|
||||||
- Web UI for configuration and monitoring (See [screenshots](https://github.com/yusing/go-proxy-frontend?tab=readme-ov-file#screenshots))
|
- Web UI for configuration and monitoring (See [screenshots](https://github.com/yusing/go-proxy-frontend?tab=readme-ov-file#screenshots))
|
||||||
|
@ -37,7 +39,7 @@ A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse pr
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
1. Setup DNS Records
|
1. Setup DNS Records, e.g.
|
||||||
|
|
||||||
- A Record: `*.y.z` -> `10.0.10.1`
|
- A Record: `*.y.z` -> `10.0.10.1`
|
||||||
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
||||||
|
@ -45,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)
|
2. Setup `go-proxy` [See here](docs/docker.md)
|
||||||
|
|
||||||
3. Configure `go-proxy`
|
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`
|
- or with web config editor via `http://gp.y.z`
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
[🔼Back to top](#table-of-content)
|
||||||
|
|
||||||
### Commands line arguments
|
### Commands line arguments
|
||||||
|
|
||||||
| Argument | Description |
|
| Argument | Description | Example |
|
||||||
| ---------- | -------------------------------- |
|
| ----------- | -------------------------------- | -------------------------- |
|
||||||
| empty | start proxy server |
|
| empty | start proxy server | |
|
||||||
| `validate` | validate config and exit |
|
| `validate` | validate config and exit | |
|
||||||
| `reload` | trigger a force reload of config |
|
| `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>`**
|
**run with `docker exec <container_name> /app/go-proxy <command>`**
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
```go
|
```go
|
||||||
var providersGenMap = map[string]ProviderGenerator{
|
var providersGenMap = map[string]ProviderGenerator{
|
||||||
"cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
|
"cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
|
||||||
// add here, i.e.
|
// add here, e.g.
|
||||||
"clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
|
"clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
@ -172,7 +172,7 @@ service_a:
|
||||||
|
|
||||||
- Container not showing up in proxies list
|
- Container not showing up in proxies list
|
||||||
|
|
||||||
Please check that either `ports` or label `proxy.<alias>.port` is declared, i.e.
|
Please check that either `ports` or label `proxy.<alias>.port` is declared, e.g.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
|
|
|
@ -33,7 +33,7 @@ func SetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
content, err := E.Check(io.ReadAll(r.Body))
|
content, err := E.Check(io.ReadAll(r.Body))
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
U.HandleErr(w, r, err)
|
U.HandleErr(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -44,13 +44,13 @@ func SetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||||
err = provider.Validate(content)
|
err = provider.Validate(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
U.HandleErr(w, r, err, http.StatusBadRequest)
|
U.HandleErr(w, r, err, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = E.From(os.WriteFile(path.Join(common.ConfigBasePath, filename), content, 0644))
|
err = E.From(os.WriteFile(path.Join(common.ConfigBasePath, filename), content, 0644))
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
U.HandleErr(w, r, err)
|
U.HandleErr(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func Reload(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
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)
|
U.HandleErr(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,9 @@ func NewConfig(cfg *M.AutoCertConfig) *Config {
|
||||||
if cfg.KeyPath == "" {
|
if cfg.KeyPath == "" {
|
||||||
cfg.KeyPath = KeyFileDefault
|
cfg.KeyPath = KeyFileDefault
|
||||||
}
|
}
|
||||||
|
if cfg.Provider == "" {
|
||||||
|
cfg.Provider = ProviderLocal
|
||||||
|
}
|
||||||
return (*Config)(cfg)
|
return (*Config)(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,43 +39,35 @@ func (cfg *Config) GetProvider() (*Provider, E.NestedError) {
|
||||||
if cfg.Email == "" {
|
if cfg.Email == "" {
|
||||||
errors.Addf("no email specified")
|
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 err := errors.Build(); err.HasError() {
|
||||||
if !ok {
|
|
||||||
errors.Addf("unknown provider: %q", cfg.Provider)
|
|
||||||
}
|
|
||||||
if err := errors.Build(); err.IsNotNil() {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
privKey, err := E.Check(ecdsa.GenerateKey(elliptic.P256(), rand.Reader))
|
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)
|
return nil, E.Failure("generate private key").With(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
user := &User{
|
user := &User{
|
||||||
Email: cfg.Email,
|
Email: cfg.Email,
|
||||||
key: privKey,
|
key: privKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
legoCfg := lego.NewConfig(user)
|
legoCfg := lego.NewConfig(user)
|
||||||
legoCfg.Certificate.KeyType = certcrypto.RSA2048
|
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{
|
base := &Provider{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
user: user,
|
user: user,
|
||||||
legoCfg: legoCfg,
|
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()
|
return base, E.Nil()
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
certBasePath = "certs/"
|
certBasePath = "certs/"
|
||||||
CertFileDefault = certBasePath + "cert.crt"
|
CertFileDefault = certBasePath + "cert.crt"
|
||||||
KeyFileDefault = certBasePath + "priv.key"
|
KeyFileDefault = certBasePath + "priv.key"
|
||||||
|
RegistrationFile = certBasePath + "registration.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -21,11 +22,10 @@ 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),
|
||||||
ProviderDuckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
|
ProviderDuckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
var Logger = logrus.WithField("module", "autocert")
|
var logger = logrus.WithField("module", "autocert")
|
||||||
|
|
|
@ -5,18 +5,17 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"reflect"
|
||||||
"sync"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/certificate"
|
"github.com/go-acme/lego/v4/certificate"
|
||||||
"github.com/go-acme/lego/v4/challenge"
|
"github.com/go-acme/lego/v4/challenge"
|
||||||
"github.com/go-acme/lego/v4/lego"
|
"github.com/go-acme/lego/v4/lego"
|
||||||
"github.com/go-acme/lego/v4/registration"
|
"github.com/go-acme/lego/v4/registration"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
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"
|
||||||
"github.com/yusing/go-proxy/utils"
|
U "github.com/yusing/go-proxy/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
|
@ -27,10 +26,9 @@ type Provider struct {
|
||||||
|
|
||||||
tlsCert *tls.Certificate
|
tlsCert *tls.Certificate
|
||||||
certExpiries CertExpiries
|
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
|
type CertExpiries map[string]time.Time
|
||||||
|
|
||||||
func (p *Provider) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
func (p *Provider) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
@ -57,59 +55,72 @@ func (p *Provider) GetExpiries() CertExpiries {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) ObtainCert() E.NestedError {
|
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")
|
ne := E.Failure("obtain certificate")
|
||||||
|
|
||||||
client := p.client
|
client := p.client
|
||||||
if p.user.Registration == nil {
|
if p.user.Registration == nil {
|
||||||
reg, err := E.Check(client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}))
|
if err := p.loadRegistration(); err.HasError() {
|
||||||
if err.IsNotNil() {
|
ne = ne.With(err)
|
||||||
return ne.With(E.Failure("register account").With(err))
|
if err := p.registerACME(); err.HasError() {
|
||||||
|
return ne.With(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
p.user.Registration = reg
|
|
||||||
}
|
}
|
||||||
req := certificate.ObtainRequest{
|
req := certificate.ObtainRequest{
|
||||||
Domains: p.cfg.Domains,
|
Domains: p.cfg.Domains,
|
||||||
Bundle: true,
|
Bundle: true,
|
||||||
}
|
}
|
||||||
cert, err := E.Check(client.Certificate.Obtain(req))
|
cert, err := E.Check(client.Certificate.Obtain(req))
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return ne.With(err)
|
return ne.With(err)
|
||||||
}
|
}
|
||||||
err = p.saveCert(cert)
|
err = p.saveCert(cert)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return ne.With(E.Failure("save certificate").With(err))
|
return ne.With(E.Failure("save certificate").With(err))
|
||||||
}
|
}
|
||||||
tlsCert, err := E.Check(tls.X509KeyPair(cert.Certificate, cert.PrivateKey))
|
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))
|
return ne.With(E.Failure("parse obtained certificate").With(err))
|
||||||
}
|
}
|
||||||
expiries, err := getCertExpiries(&tlsCert)
|
expiries, err := getCertExpiries(&tlsCert)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return ne.With(E.Failure("get certificate expiry").With(err))
|
return ne.With(E.Failure("get certificate expiry").With(err))
|
||||||
}
|
}
|
||||||
p.tlsCert = &tlsCert
|
p.tlsCert = &tlsCert
|
||||||
p.certExpiries = expiries
|
p.certExpiries = expiries
|
||||||
|
|
||||||
return E.Nil()
|
return E.Nil()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) LoadCert() E.NestedError {
|
func (p *Provider) LoadCert() E.NestedError {
|
||||||
cert, err := E.Check(tls.LoadX509KeyPair(p.cfg.CertPath, p.cfg.KeyPath))
|
cert, err := E.Check(tls.LoadX509KeyPair(p.cfg.CertPath, p.cfg.KeyPath))
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
expiries, err := getCertExpiries(&cert)
|
expiries, err := getCertExpiries(&cert)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.tlsCert = &cert
|
p.tlsCert = &cert
|
||||||
p.certExpiries = expiries
|
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 {
|
func (p *Provider) ShouldRenewOn() time.Time {
|
||||||
for _, expiry := range p.certExpiries {
|
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
|
// this line should never be reached
|
||||||
panic("no certificate available")
|
panic("no certificate available")
|
||||||
|
@ -120,117 +131,151 @@ func (p *Provider) ScheduleRenewal(ctx context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug("starting renewal scheduler")
|
logger.Debug("started renewal scheduler")
|
||||||
defer logger.Debug("renewal scheduler stopped")
|
defer logger.Debug("renewal scheduler stopped")
|
||||||
|
|
||||||
stop := make(chan struct{})
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
default:
|
case <-ticker.C: // check every 5 seconds
|
||||||
t := time.Until(p.ShouldRenewOn())
|
if err := p.renewIfNeeded(); err.HasError() {
|
||||||
Logger.Infof("next renewal in %v", t.Round(time.Second))
|
logger.Warn(err)
|
||||||
go func() {
|
|
||||||
<-time.After(t)
|
|
||||||
close(stop)
|
|
||||||
}()
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-stop:
|
|
||||||
if err := p.renewIfNeeded(); err.IsNotNil() {
|
|
||||||
Logger.Fatal(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 {
|
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 {
|
if err != nil {
|
||||||
return E.Failure("write key file").With(err)
|
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 {
|
if err != nil {
|
||||||
return E.Failure("write cert file").With(err)
|
return E.Failure("write cert file").With(err)
|
||||||
}
|
}
|
||||||
return E.Nil()
|
return E.Nil()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) needRenewal() bool {
|
func (p *Provider) certState() CertState {
|
||||||
expired := time.Now().After(p.ShouldRenewOn())
|
if time.Now().After(p.ShouldRenewOn()) {
|
||||||
if expired {
|
return CertStateExpired
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
if len(p.cfg.Domains) != len(p.certExpiries) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
wantedDomains := make([]string, len(p.cfg.Domains))
|
|
||||||
certDomains := make([]string, len(p.certExpiries))
|
certDomains := make([]string, len(p.certExpiries))
|
||||||
copy(wantedDomains, p.cfg.Domains)
|
wantedDomains := make([]string, len(p.cfg.Domains))
|
||||||
i := 0
|
i := 0
|
||||||
for domain := range p.certExpiries {
|
for domain := range p.certExpiries {
|
||||||
certDomains[i] = domain
|
certDomains[i] = domain
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
slices.Sort(wantedDomains)
|
copy(wantedDomains, p.cfg.Domains)
|
||||||
slices.Sort(certDomains)
|
sort.Strings(wantedDomains)
|
||||||
for i, domain := range certDomains {
|
sort.Strings(certDomains)
|
||||||
if domain != wantedDomains[i] {
|
|
||||||
return true
|
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 {
|
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()
|
return E.Nil()
|
||||||
}
|
}
|
||||||
|
|
||||||
p.mutex.Lock()
|
if err := p.ObtainCert(); err.HasError() {
|
||||||
defer p.mutex.Unlock()
|
return E.Failure("renew certificate").With(err)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
return E.Nil()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCertExpiries(cert *tls.Certificate) (CertExpiries, E.NestedError) {
|
func getCertExpiries(cert *tls.Certificate) (CertExpiries, E.NestedError) {
|
||||||
r := make(CertExpiries, len(cert.Certificate))
|
r := make(CertExpiries, len(cert.Certificate))
|
||||||
for _, cert := range cert.Certificate {
|
for _, cert := range cert.Certificate {
|
||||||
x509Cert, err := E.Check(x509.ParseCertificate(cert))
|
x509Cert, err := E.Check(x509.ParseCertificate(cert))
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, E.Failure("parse certificate").With(err)
|
return nil, E.Failure("parse certificate").With(err)
|
||||||
}
|
}
|
||||||
if x509Cert.IsCA {
|
if x509Cert.IsCA {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
r[x509Cert.Subject.CommonName] = x509Cert.NotAfter
|
r[x509Cert.Subject.CommonName] = x509Cert.NotAfter
|
||||||
|
for i := range x509Cert.DNSNames {
|
||||||
|
r[x509Cert.DNSNames[i]] = x509Cert.NotAfter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return r, E.Nil()
|
return r, E.Nil()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setOptions[T interface{}](cfg *T, opt M.AutocertProviderOpt) E.NestedError {
|
func setOptions[T interface{}](cfg *T, opt M.AutocertProviderOpt) E.NestedError {
|
||||||
for k, v := range opt {
|
for k, v := range opt {
|
||||||
err := utils.SetFieldFromSnake(cfg, k, v)
|
err := U.SetFieldFromSnake(cfg, k, v)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return E.Failure("set autocert option").Subject(k).With(err)
|
return E.Failure("set autocert option").Subject(k).With(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -241,18 +286,16 @@ func providerGenerator[CT any, PT challenge.Provider](
|
||||||
defaultCfg func() *CT,
|
defaultCfg func() *CT,
|
||||||
newProvider func(*CT) (PT, error),
|
newProvider func(*CT) (PT, error),
|
||||||
) ProviderGenerator {
|
) ProviderGenerator {
|
||||||
return func(opt M.AutocertProviderOpt) (challenge.Provider, error) {
|
return func(opt M.AutocertProviderOpt) (challenge.Provider, E.NestedError) {
|
||||||
cfg := defaultCfg()
|
cfg := defaultCfg()
|
||||||
err := setOptions(cfg, opt)
|
err := setOptions(cfg, opt)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
p, err := E.Check(newProvider(cfg))
|
p, err := E.Check(newProvider(cfg))
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return p, nil
|
return p, E.Nil()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var logger = logrus.WithField("module", "autocert")
|
|
||||||
|
|
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 (
|
const (
|
||||||
CommandStart = ""
|
CommandStart = ""
|
||||||
CommandValidate = "validate"
|
CommandValidate = "validate"
|
||||||
CommandReload = "reload"
|
CommandListConfigs = "ls-config"
|
||||||
|
CommandReload = "reload"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ValidCommands = []string{CommandStart, CommandValidate, CommandReload}
|
var ValidCommands = []string{CommandStart, CommandValidate, CommandListConfigs, CommandReload}
|
||||||
|
|
||||||
func GetArgs() Args {
|
func GetArgs() Args {
|
||||||
var args Args
|
var args Args
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
args.Command = flag.Arg(0)
|
args.Command = flag.Arg(0)
|
||||||
if err := validateArgs(args.Command, ValidCommands); err.IsNotNil() {
|
if err := validateArgs(args.Command, ValidCommands); err.HasError() {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
return args
|
return args
|
||||||
|
|
|
@ -3,21 +3,16 @@ package common
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var NoSchemaValidation = getEnvBool("GOPROXY_NO_SCHEMA_VALIDATION")
|
var NoSchemaValidation = getEnvBool("GOPROXY_NO_SCHEMA_VALIDATION")
|
||||||
var IsDebug = getEnvBool("GOPROXY_DEBUG")
|
var IsDebug = getEnvBool("GOPROXY_DEBUG")
|
||||||
|
|
||||||
var LogLevel = func() logrus.Level {
|
|
||||||
if IsDebug {
|
|
||||||
logrus.SetLevel(logrus.DebugLevel)
|
|
||||||
}
|
|
||||||
return logrus.GetLevel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
func getEnvBool(key string) bool {
|
func getEnvBool(key string) bool {
|
||||||
v := os.Getenv(key)
|
switch strings.ToLower(os.Getenv(key)) {
|
||||||
return v == "1" || strings.ToLower(v) == "true" || strings.ToLower(v) == "yes" || strings.ToLower(v) == "on"
|
case "1", "true", "yes", "on":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ func New() (*Config, E.NestedError) {
|
||||||
watcher: W.NewFileWatcher(common.ConfigFileName),
|
watcher: W.NewFileWatcher(common.ConfigFileName),
|
||||||
reloadReq: make(chan struct{}, 1),
|
reloadReq: make(chan struct{}, 1),
|
||||||
}
|
}
|
||||||
if err := cfg.load(); err.IsNotNil() {
|
if err := cfg.load(); err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
cfg.startProviders()
|
cfg.startProviders()
|
||||||
|
@ -66,7 +66,7 @@ func (cfg *Config) Dispose() {
|
||||||
|
|
||||||
func (cfg *Config) Reload() E.NestedError {
|
func (cfg *Config) Reload() E.NestedError {
|
||||||
cfg.stopProviders()
|
cfg.stopProviders()
|
||||||
if err := cfg.load(); err.IsNotNil() {
|
if err := cfg.load(); err.HasError() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cfg.startProviders()
|
cfg.startProviders()
|
||||||
|
@ -156,7 +156,7 @@ func (cfg *Config) watchChanges() {
|
||||||
case <-cfg.watcherCtx.Done():
|
case <-cfg.watcherCtx.Done():
|
||||||
return
|
return
|
||||||
case <-cfg.reloadReq:
|
case <-cfg.reloadReq:
|
||||||
if err := cfg.Reload(); err.IsNotNil() {
|
if err := cfg.Reload(); err.HasError() {
|
||||||
cfg.l.Error(err)
|
cfg.l.Error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -186,29 +186,29 @@ func (cfg *Config) load() E.NestedError {
|
||||||
cfg.l.Debug("loading config")
|
cfg.l.Debug("loading config")
|
||||||
|
|
||||||
data, err := cfg.reader.Read()
|
data, err := cfg.reader.Read()
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return E.Failure("read config").With(err)
|
return E.Failure("read config").With(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
model := M.DefaultConfig()
|
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)
|
return E.Failure("parse config").With(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !common.NoSchemaValidation {
|
if !common.NoSchemaValidation {
|
||||||
if err = Validate(data); err.IsNotNil() {
|
if err = Validate(data); err.HasError() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
warnings := E.NewBuilder("errors loading config")
|
warnings := E.NewBuilder("errors loading config")
|
||||||
|
|
||||||
cfg.l.Debug("starting autocert")
|
cfg.l.Debug("initializing autocert")
|
||||||
ap, err := autocert.NewConfig(&model.AutoCert).GetProvider()
|
ap, err := autocert.NewConfig(&model.AutoCert).GetProvider()
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
warnings.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("initialized autocert")
|
||||||
}
|
}
|
||||||
cfg.autocertProvider = ap
|
cfg.autocertProvider = ap
|
||||||
|
|
||||||
|
@ -226,7 +226,7 @@ func (cfg *Config) load() E.NestedError {
|
||||||
|
|
||||||
cfg.value = model
|
cfg.value = model
|
||||||
|
|
||||||
if err := warnings.Build(); err.IsNotNil() {
|
if err := warnings.Build(); err.HasError() {
|
||||||
cfg.l.Warn(err)
|
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)
|
errors := E.NewBuilder("cannot %s these providers", action)
|
||||||
|
|
||||||
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.HasError() {
|
||||||
errors.Add(E.From(err).Subject(p))
|
errors.Add(E.From(err).Subject(p))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := errors.Build(); err.IsNotNil() {
|
if err := errors.Build(); err.HasError() {
|
||||||
cfg.l.Error(err)
|
cfg.l.Error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ func ConnectClient(host string) (Client, E.NestedError) {
|
||||||
opt = clientOptEnvHost
|
opt = clientOptEnvHost
|
||||||
default:
|
default:
|
||||||
helper, err := E.Check(connhelper.GetConnectionHelper(host))
|
helper, err := E.Check(connhelper.GetConnectionHelper(host))
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
logger.Fatalf("unexpected error: %s", err)
|
logger.Fatalf("unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
if helper != nil {
|
if helper != nil {
|
||||||
|
@ -65,7 +65,7 @@ func ConnectClient(host string) (Client, E.NestedError) {
|
||||||
|
|
||||||
client, err := E.Check(client.NewClientWithOpts(opt...))
|
client, err := E.Check(client.NewClientWithOpts(opt...))
|
||||||
|
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ type ClientInfo struct {
|
||||||
|
|
||||||
func GetClientInfo(clientHost string) (*ClientInfo, E.NestedError) {
|
func GetClientInfo(clientHost string) (*ClientInfo, E.NestedError) {
|
||||||
dockerClient, err := ConnectClient(clientHost)
|
dockerClient, err := ConnectClient(clientHost)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, E.Failure("create docker client").With(err)
|
return nil, E.Failure("create docker client").With(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ func GetClientInfo(clientHost string) (*ClientInfo, E.NestedError) {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
containers, err := E.Check(dockerClient.ContainerList(ctx, container.ListOptions{}))
|
containers, err := E.Check(dockerClient.ContainerList(ctx, container.ListOptions{}))
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, E.Failure("list containers").With(err)
|
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
|
// since the services being proxied to
|
||||||
// should have the same IP as the docker client
|
// should have the same IP as the docker client
|
||||||
url, err := E.Check(client.ParseHostURL(dockerClient.DaemonHost()))
|
url, err := E.Check(client.ParseHostURL(dockerClient.DaemonHost()))
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, E.Invalid("host url", dockerClient.DaemonHost()).With(err)
|
return nil, E.Invalid("host url", dockerClient.DaemonHost()).With(err)
|
||||||
}
|
}
|
||||||
if url.Scheme == "unix" {
|
if url.Scheme == "unix" {
|
||||||
|
|
|
@ -63,7 +63,7 @@ func ParseLabel(label string, value string) (*Label, E.NestedError) {
|
||||||
}
|
}
|
||||||
// try to parse value
|
// try to parse value
|
||||||
v, err := p(value)
|
v, err := p(value)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
l.Value = v
|
l.Value = v
|
||||||
|
|
|
@ -118,11 +118,11 @@ func (ne NestedError) Subjectf(format string, args ...any) NestedError {
|
||||||
return ne
|
return ne
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ne NestedError) IsNil() bool {
|
func (ne NestedError) NoError() bool {
|
||||||
return ne.err == nil
|
return ne.err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ne NestedError) IsNotNil() bool {
|
func (ne NestedError) HasError() bool {
|
||||||
return ne.err != nil
|
return ne.err != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ 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.IsNil() {
|
if ne.NoError() {
|
||||||
sb.WriteString("nil")
|
sb.WriteString("nil")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,8 @@ func TestErrorIs(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNil(t *testing.T) {
|
func TestNil(t *testing.T) {
|
||||||
ExpectTrue(t, Nil().IsNil())
|
ExpectTrue(t, Nil().NoError())
|
||||||
ExpectFalse(t, Nil().IsNotNil())
|
ExpectFalse(t, Nil().HasError())
|
||||||
ExpectEqual(t, Nil().Error(), "nil")
|
ExpectEqual(t, Nil().Error(), "nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
61
src/main.go
61
src/main.go
|
@ -2,6 +2,8 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
@ -33,14 +35,15 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.SetFormatter(&logrus.TextFormatter{
|
logrus.SetFormatter(&logrus.TextFormatter{
|
||||||
DisableSorting: true,
|
DisableSorting: true,
|
||||||
FullTimestamp: true,
|
DisableLevelTruncation: true,
|
||||||
ForceColors: true,
|
FullTimestamp: true,
|
||||||
TimestampFormat: "01-02 15:04:05",
|
ForceColors: 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.HasError() {
|
||||||
l.Fatal(err)
|
l.Fatal(err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -52,10 +55,10 @@ func main() {
|
||||||
if args.Command == common.CommandValidate {
|
if args.Command == common.CommandValidate {
|
||||||
var err E.NestedError
|
var err E.NestedError
|
||||||
data, err := E.Check(os.ReadFile(common.ConfigPath))
|
data, err := E.Check(os.ReadFile(common.ConfigPath))
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
l.WithError(err).Fatalf("config error")
|
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.WithError(err).Fatalf("config error")
|
||||||
}
|
}
|
||||||
l.Printf("config OK")
|
l.Printf("config OK")
|
||||||
|
@ -63,10 +66,20 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.New()
|
cfg, err := config.New()
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
l.Fatalf("config error: %s", err)
|
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() {
|
onShutdown.Add(func() {
|
||||||
docker.CloseAllClients()
|
docker.CloseAllClients()
|
||||||
cfg.Dispose()
|
cfg.Dispose()
|
||||||
|
@ -80,23 +93,27 @@ func main() {
|
||||||
autocert := cfg.GetAutoCertProvider()
|
autocert := cfg.GetAutoCertProvider()
|
||||||
|
|
||||||
if autocert != nil {
|
if autocert != nil {
|
||||||
err = autocert.LoadCert()
|
if err = autocert.LoadCert(); err.HasError() {
|
||||||
|
if !err.Is(os.ErrNotExist) { // ignore if cert doesn't exist
|
||||||
if err.IsNotNil() {
|
l.Error(err)
|
||||||
l.Error(err)
|
}
|
||||||
l.Info("Now attempting to obtain a new certificate...")
|
l.Debug("obtaining cert due to error loading cert")
|
||||||
if err = autocert.ObtainCert(); err.IsNotNil() {
|
if err = autocert.ObtainCert(); err.HasError() {
|
||||||
ctx, certRenewalCancel := context.WithCancel(context.Background())
|
|
||||||
go autocert.ScheduleRenewal(ctx)
|
|
||||||
onShutdown.Add(certRenewalCancel)
|
|
||||||
} else {
|
|
||||||
l.Warn(err)
|
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{
|
proxyServer := server.InitProxyServer(server.Options{
|
||||||
|
|
|
@ -33,7 +33,7 @@ type (
|
||||||
func NewEntry(m *M.ProxyEntry) (any, E.NestedError) {
|
func NewEntry(m *M.ProxyEntry) (any, E.NestedError) {
|
||||||
m.SetDefaults()
|
m.SetDefaults()
|
||||||
scheme, err := T.NewScheme(m.Scheme)
|
scheme, err := T.NewScheme(m.Scheme)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if scheme.IsStream() {
|
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) {
|
func validateEntry(m *M.ProxyEntry, s T.Scheme) (*Entry, E.NestedError) {
|
||||||
host, err := T.NewHost(m.Host)
|
host, err := T.NewHost(m.Host)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
port, err := T.NewPort(m.Port)
|
port, err := T.NewPort(m.Port)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pathPatterns, err := T.NewPathPatterns(m.PathPatterns)
|
pathPatterns, err := T.NewPathPatterns(m.PathPatterns)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
setHeaders, err := T.NewHTTPHeaders(m.SetHeaders)
|
setHeaders, err := T.NewHTTPHeaders(m.SetHeaders)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
url, err := E.Check(url.Parse(fmt.Sprintf("%s://%s:%d", s, host, port)))
|
url, err := E.Check(url.Parse(fmt.Sprintf("%s://%s:%d", s, host, port)))
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Entry{
|
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) {
|
func validateStreamEntry(m *M.ProxyEntry) (*StreamEntry, E.NestedError) {
|
||||||
host, err := T.NewHost(m.Host)
|
host, err := T.NewHost(m.Host)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
port, err := T.NewStreamPort(m.Port)
|
port, err := T.NewStreamPort(m.Port)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
scheme, err := T.NewStreamScheme(m.Scheme)
|
scheme, err := T.NewStreamScheme(m.Scheme)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &StreamEntry{
|
return &StreamEntry{
|
||||||
|
|
|
@ -25,7 +25,7 @@ func NewPathPatterns(s []string) (PathPatterns, E.NestedError) {
|
||||||
}
|
}
|
||||||
pp := make(PathPatterns, len(s))
|
pp := make(PathPatterns, len(s))
|
||||||
for i, v := range s {
|
for i, v := range s {
|
||||||
if pattern, err := NewPathPattern(v); err.IsNotNil() {
|
if pattern, err := NewPathPattern(v); err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
pp[i] = pattern
|
pp[i] = pattern
|
||||||
|
|
|
@ -18,7 +18,7 @@ func NewPort(v string) (Port, E.NestedError) {
|
||||||
|
|
||||||
func NewPortInt[Int int | uint16](v Int) (Port, E.NestedError) {
|
func NewPortInt[Int int | uint16](v Int) (Port, E.NestedError) {
|
||||||
pp := Port(v)
|
pp := Port(v)
|
||||||
if err := pp.boundCheck(); err.IsNotNil() {
|
if err := pp.boundCheck(); err.HasError() {
|
||||||
return ErrPort, err
|
return ErrPort, err
|
||||||
}
|
}
|
||||||
return pp, E.Nil()
|
return pp, E.Nil()
|
||||||
|
|
|
@ -19,21 +19,21 @@ func NewStreamPort(p string) (StreamPort, E.NestedError) {
|
||||||
}
|
}
|
||||||
|
|
||||||
listeningPort, err := NewPort(split[0])
|
listeningPort, err := NewPort(split[0])
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return StreamPort{}, err
|
return StreamPort{}, err
|
||||||
}
|
}
|
||||||
if err = listeningPort.boundCheck(); err.IsNotNil() {
|
if err = listeningPort.boundCheck(); err.HasError() {
|
||||||
return StreamPort{}, err
|
return StreamPort{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyPort, err := NewPort(split[1])
|
proxyPort, err := NewPort(split[1])
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
proxyPort, err = parseNameToPort(split[1])
|
proxyPort, err = parseNameToPort(split[1])
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return StreamPort{}, err
|
return StreamPort{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = proxyPort.boundCheck(); err.IsNotNil() {
|
if err = proxyPort.boundCheck(); err.HasError() {
|
||||||
return StreamPort{}, err
|
return StreamPort{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,11 +21,11 @@ func NewStreamScheme(s string) (ss *StreamScheme, err E.NestedError) {
|
||||||
return nil, E.Invalid("stream scheme", s)
|
return nil, E.Invalid("stream scheme", s)
|
||||||
}
|
}
|
||||||
ss.ListeningScheme, err = NewScheme(parts[0])
|
ss.ListeningScheme, err = NewScheme(parts[0])
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ss.ProxyScheme, err = NewScheme(parts[1])
|
ss.ProxyScheme, err = NewScheme(parts[1])
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return ss, E.Nil()
|
return ss, E.Nil()
|
||||||
|
|
|
@ -39,7 +39,7 @@ func (p DockerProvider) GetProxyEntries() (M.ProxyEntries, E.NestedError) {
|
||||||
entries := M.NewProxyEntries()
|
entries := M.NewProxyEntries()
|
||||||
|
|
||||||
info, err := D.GetClientInfo(p.dockerHost)
|
info, err := D.GetClientInfo(p.dockerHost)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return entries, err
|
return entries, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ func (p DockerProvider) GetProxyEntries() (M.ProxyEntries, E.NestedError) {
|
||||||
|
|
||||||
for _, container := range info.Containers {
|
for _, container := range info.Containers {
|
||||||
en, err := p.getEntriesFromLabels(&container, info.Host)
|
en, err := p.getEntriesFromLabels(&container, info.Host)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
errors.Add(err)
|
errors.Add(err)
|
||||||
}
|
}
|
||||||
// although err is not nil
|
// although err is not nil
|
||||||
|
@ -95,7 +95,7 @@ func (p *DockerProvider) getEntriesFromLabels(container *types.Container, client
|
||||||
|
|
||||||
// find first port, return if no port exposed
|
// find first port, return if no port exposed
|
||||||
defaultPort, err := findFirstPort(container)
|
defaultPort, err := findFirstPort(container)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
logrus.Debug(mainAlias, " ", err.Error())
|
logrus.Debug(mainAlias, " ", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ func (p *DockerProvider) getEntriesFromLabels(container *types.Container, client
|
||||||
errors := E.NewBuilder("failed to apply label for %q", mainAlias)
|
errors := E.NewBuilder("failed to apply label for %q", mainAlias)
|
||||||
for key, val := range container.Labels {
|
for key, val := range container.Labels {
|
||||||
lbl, err := D.ParseLabel(key, val)
|
lbl, err := D.ParseLabel(key, val)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
errors.Add(E.From(err).Subject(key))
|
errors.Add(E.From(err).Subject(key))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,7 @@ func (p *DockerProvider) getEntriesFromLabels(container *types.Container, client
|
||||||
if lbl.Target == wildcardAlias {
|
if lbl.Target == wildcardAlias {
|
||||||
// apply label for all aliases
|
// apply label for all aliases
|
||||||
entries.EachKV(func(a string, e *M.ProxyEntry) {
|
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))
|
errors.Add(E.From(err).Subject(lbl.Target))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -131,7 +131,7 @@ func (p *DockerProvider) getEntriesFromLabels(container *types.Container, client
|
||||||
errors.Add(E.NotExists("alias", lbl.Target))
|
errors.Add(E.NotExists("alias", lbl.Target))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err = D.ApplyLabel(config, lbl); err.IsNotNil() {
|
if err = D.ApplyLabel(config, lbl); err.HasError() {
|
||||||
errors.Add(err.Subject(lbl.Target))
|
errors.Add(err.Subject(lbl.Target))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,16 +34,16 @@ func (p *FileProvider) String() string {
|
||||||
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.HasError() {
|
||||||
return entries, E.Failure("read file").Subject(p).With(err)
|
return entries, E.Failure("read file").Subject(p).With(err)
|
||||||
}
|
}
|
||||||
ne := E.Failure("validation").Subject(p)
|
ne := E.Failure("validation").Subject(p)
|
||||||
if !common.NoSchemaValidation {
|
if !common.NoSchemaValidation {
|
||||||
if err = Validate(data); err.IsNotNil() {
|
if err = Validate(data); err.HasError() {
|
||||||
return entries, ne.With(err)
|
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, ne.With(err)
|
||||||
}
|
}
|
||||||
return entries, E.Nil()
|
return entries, E.Nil()
|
||||||
|
|
|
@ -92,12 +92,12 @@ func (p *Provider) StartAllRoutes() E.NestedError {
|
||||||
nStarted := 0
|
nStarted := 0
|
||||||
nFailed := 0
|
nFailed := 0
|
||||||
|
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
errors.Add(err)
|
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.HasError() {
|
||||||
errors.Add(err.Subject(r))
|
errors.Add(err.Subject(r))
|
||||||
nFailed++
|
nFailed++
|
||||||
} else {
|
} else {
|
||||||
|
@ -118,7 +118,7 @@ func (p *Provider) StopAllRoutes() E.NestedError {
|
||||||
nStopped := 0
|
nStopped := 0
|
||||||
nFailed := 0
|
nFailed := 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.HasError() {
|
||||||
errors.Add(err.Subject(r))
|
errors.Add(err.Subject(r))
|
||||||
nFailed++
|
nFailed++
|
||||||
} else {
|
} else {
|
||||||
|
@ -195,7 +195,7 @@ func (p *Provider) processReloadRequests() {
|
||||||
func (p *Provider) loadRoutes() E.NestedError {
|
func (p *Provider) loadRoutes() E.NestedError {
|
||||||
entries, err := p.GetProxyEntries()
|
entries, err := p.GetProxyEntries()
|
||||||
|
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
p.l.Warn(err.Subject(p))
|
p.l.Warn(err.Subject(p))
|
||||||
}
|
}
|
||||||
p.routes = R.NewRoutes()
|
p.routes = R.NewRoutes()
|
||||||
|
@ -204,7 +204,7 @@ func (p *Provider) loadRoutes() E.NestedError {
|
||||||
entries.EachKV(func(a string, e *M.ProxyEntry) {
|
entries.EachKV(func(a string, e *M.ProxyEntry) {
|
||||||
e.Alias = a
|
e.Alias = a
|
||||||
r, err := R.NewRoute(e)
|
r, err := R.NewRoute(e)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
errors.Add(err.Subject(a))
|
errors.Add(err.Subject(a))
|
||||||
} else {
|
} else {
|
||||||
p.routes.Set(a, r)
|
p.routes.Set(a, r)
|
||||||
|
|
|
@ -21,7 +21,7 @@ var NewRoutes = F.NewMap[string, Route]
|
||||||
|
|
||||||
func NewRoute(en *M.ProxyEntry) (Route, E.NestedError) {
|
func NewRoute(en *M.ProxyEntry) (Route, E.NestedError) {
|
||||||
entry, err := P.NewEntry(en)
|
entry, err := P.NewEntry(en)
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
switch e := entry.(type) {
|
switch e := entry.(type) {
|
||||||
|
|
|
@ -78,7 +78,7 @@ func (route *TCPRoute) CloseListeners() {
|
||||||
route.listener.Close()
|
route.listener.Close()
|
||||||
route.listener = nil
|
route.listener = nil
|
||||||
for _, pipe := range route.pipe {
|
for _, pipe := range route.pipe {
|
||||||
if err := pipe.Stop(); err.IsNotNil() {
|
if err := pipe.Stop(); err.HasError() {
|
||||||
route.l.Error(err)
|
route.l.Error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
@ -135,7 +136,7 @@ func (p *BidirectionalPipe) Start() E.NestedError {
|
||||||
errCh <- p.pDstSrc.Start()
|
errCh <- p.pDstSrc.Start()
|
||||||
}()
|
}()
|
||||||
for err := range errCh {
|
for err := range errCh {
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
return err
|
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 {
|
func Copy(ctx context.Context, dst io.WriteCloser, src io.ReadCloser) E.NestedError {
|
||||||
_, err := io.Copy(dst, StdReadCloser{&ReadCloser{ctx: ctx, r: src}})
|
_, err := io.Copy(dst, StdReadCloser{&ReadCloser{ctx: ctx, r: src}})
|
||||||
return E.From(err)
|
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))
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
func ExpectErrNil(t *testing.T, err E.NestedError) {
|
func ExpectErrNil(t *testing.T, err E.NestedError) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
t.Errorf("expected err=nil, got %s", err.Error())
|
t.Errorf("expected err=nil, got %s", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,13 +31,13 @@ func (w *DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Nest
|
||||||
var err E.NestedError
|
var err E.NestedError
|
||||||
for range 3 {
|
for range 3 {
|
||||||
cl, err = D.ConnectClient(w.host)
|
cl, err = D.ConnectClient(w.host)
|
||||||
if err.IsNil() {
|
if err.NoError() {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
errCh <- E.From(err)
|
errCh <- E.From(err)
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
}
|
}
|
||||||
if err.IsNotNil() {
|
if err.HasError() {
|
||||||
errCh <- E.Failure("connecting to docker")
|
errCh <- E.Failure("connecting to docker")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue