This commit is contained in:
yusing w 2024-04-08 05:07:27 +00:00
parent 8694987ef9
commit 52549b6446
36 changed files with 1268 additions and 554 deletions

View file

@ -8,7 +8,13 @@ jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Build and Push Container to ghcr.io
uses: GlueOps/github-actions-build-push-containers@v0.3.7
with:
tags: latest,${{ github.ref_name }}
- name: Set tags (latest)
if: "!endsWith(github.ref, '-dev')"
run: echo "::set-output name=tag::latest,${{ github.ref_name }}"
- name: Set tags (dev)
if: "endsWith(github.ref, '-dev')"
run: echo "::set-output name=tag::dev,${{ github.ref_name }}"
- name: Build and Push Container to ghcr.io
uses: GlueOps/github-actions-build-push-containers@v0.3.7
with:
tags: ${{ steps.build_and_push.outputs.tag }}

3
.gitignore vendored
View file

@ -6,4 +6,5 @@ bin/
templates/codemirror/
logs/
log/
log/
.vscode/settings.json

15
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,15 @@
build-image:
image: docker
rules:
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
variables:
CI_REGISTRY_IMAGE: $CI_REGISTRY_IMAGE:latest
- if: $CI_COMMIT_REF_NAME != $CI_DEFAULT_BRANCH
variables:
CI_REGISTRY_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH
before_script:
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
script:
- echo building $CI_REGISTRY_IMAGE
- docker build --pull -t $CI_REGISTRY_IMAGE .
- docker push $CI_REGISTRY_IMAGE

12
.vscode/settings.example.json vendored Normal file
View file

@ -0,0 +1,12 @@
{
"yaml.schemas": {
"https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
"config.example.yml",
"config.yml"
],
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
"providers.example.yml",
"*.providers.yml"
]
}
}

16
.vscode/settings.json vendored
View file

@ -1,16 +0,0 @@
{
"go.inferGopath": false,
"yaml.schemas": {
// "https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
// "config.example.yml",
// "config.yml"
// ],
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
"providers.example.yml",
"*.providers.yml"
],
"file:///config/workspace/go-proxy/schema/config.schema.json": [
"file:///config/workspace/go-proxy/config.example.yml"
]
}
}

View file

@ -3,7 +3,7 @@ RUN apk add --no-cache unzip wget make
COPY Makefile .
RUN make setup-codemirror
FROM golang:1.22.1-alpine as builder
FROM golang:1.22.2-alpine as builder
COPY src/ /src
COPY go.mod go.sum /src/go-proxy
WORKDIR /src/go-proxy

View file

@ -18,12 +18,14 @@ build:
mkdir -p bin
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy src/go-proxy/*.go
test:
go test src/go-proxy/*.go
up:
docker compose up -d --build app
docker compose up -d
restart:
docker kill go-proxy
docker compose up -d app
docker compose restart -t 0
logs:
tail -f log/go-proxy.log
@ -31,6 +33,12 @@ logs:
get:
go get -d -u ./src/go-proxy
repush:
git reset --soft HEAD^
git add -A
git commit -m "repush"
git push gitlab dev --force
udp-server:
docker run -it --rm \
-p 9999:9999/udp \

243
README.md
View file

@ -6,35 +6,29 @@ In the examples domain `x.y.z` is used, replace them with your domain
## Table of content
- [go-proxy](#go-proxy)
- [Table of content](#table-of-content)
- [Key Points](#key-points)
- [How to use](#how-to-use)
- [Tested Services](#tested-services)
- [HTTP/HTTPs Reverse Proxy](#httphttps-reverse-proxy)
- [TCP Proxy](#tcp-proxy)
- [UDP Proxy](#udp-proxy)
- [Command-line args](#command-line-args)
- [Commands](#commands)
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
- [Configuration](#configuration)
- [Labels (docker)](#labels-docker)
- [Environment variables](#environment-variables)
- [Config File](#config-file)
- [Fields](#fields)
- [Provider Kinds](#provider-kinds)
- [Provider File](#provider-file)
- [Supported DNS Challenge Providers](#supported-dns-challenge-providers)
- [Examples](#examples)
- [Single port configuration example](#single-port-configuration-example)
- [Multiple ports configuration example](#multiple-ports-configuration-example)
- [TCP/UDP configuration example](#tcpudp-configuration-example)
- [Load balancing Configuration Example](#load-balancing-configuration-example)
- [Troubleshooting](#troubleshooting)
- [Benchmarks](#benchmarks)
- [Known issues](#known-issues)
- [Memory usage](#memory-usage)
- [Build it yourself](#build-it-yourself)
<!-- TOC -->
- [Table of content](#table-of-content)
- [Key Points](#key-points)
- [How to use](#how-to-use)
- [Tested Services](#tested-services)
- [HTTP/HTTPs Reverse Proxy](#httphttps-reverse-proxy)
- [TCP Proxy](#tcp-proxy)
- [UDP Proxy](#udp-proxy)
- [Command-line args](#command-line-args)
- [Commands](#commands)
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
- [Environment variables](#environment-variables)
- [Config File](#config-file)
- [Fields](#fields)
- [Provider Kinds](#provider-kinds)
- [Provider File](#provider-file)
- [Supported DNS Challenge Providers](#supported-dns-challenge-providers)
- [Troubleshooting](#troubleshooting)
- [Benchmarks](#benchmarks)
- [Known issues](#known-issues)
- [Memory usage](#memory-usage)
- [Build it yourself](#build-it-yourself)
<!-- /TOC -->
## Key Points
@ -58,6 +52,8 @@ In the examples domain `x.y.z` is used, replace them with your domain
![config editor screenshot](screenshots/config_editor.png)
[🔼Back to top](#table-of-content)
## How to use
1. Setup DNS Records to your machine's IP address
@ -65,18 +61,23 @@ In the examples domain `x.y.z` is used, replace them with your domain
- A Record: `*.y.z` -> `10.0.10.1`
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
2. Start `go-proxy` (see [Binary](docs/binary.md) or [docker](docs/docker.md))
2. Start `go-proxy` by
- [Running from binary or as a system service](docs/binary.md)
- [Running as a docker container](docs/docker.md)
3. Start editing config files
- with text editor (i.e. Visual Studio Code)
- or with web config editor by navigate to `ip:8080`
- or with web config editor by navigate to `http://ip:8080`
[🔼Back to top](#table-of-content)
## Tested Services
### HTTP/HTTPs Reverse Proxy
- nginx
- minio
- Nginx
- Minio
- AdguardHome Dashboard
- etc.
@ -91,6 +92,8 @@ In the examples domain `x.y.z` is used, replace them with your domain
- Adguardhome DNS
- Palworld Dedicated Server
[🔼Back to top](#table-of-content)
## Command-line args
`go-proxy [command]`
@ -106,9 +109,11 @@ Examples:
- Binary: `go-proxy reload`
- Docker: `docker exec -it go-proxy /app/go-proxy reload`
[🔼Back to top](#table-of-content)
## Use JSON Schema in VSCode
Modify `.vscode/settings.json` to fit your needs
Copy [`.vscode/settings.example.json`](.vscode/settings.example.json) to `.vscode/settings.json` and modify to fit your needs
```json
{
@ -125,78 +130,36 @@ Modify `.vscode/settings.json` to fit your needs
}
```
## Configuration
[🔼Back to top](#table-of-content)
With container name, no label needs to be added _(most of the time)_.
### Labels (docker)
See [docker.md](docs/docker.md#docker-compose-example) for examples
When `go-proxy` is running in `host` network mode, see [here](docs/docker.md#docker-compose-example-host-network) for extra instructions
- `proxy.aliases`: comma separated aliases for subdomain matching
- default: container name
- `proxy.*.<field>`: wildcard label for all aliases
Below labels has a **`proxy.<alias>.`** prefix (i.e. `proxy.nginx.scheme: http`)
- `scheme`: proxy protocol
- default: `http`
- allowed: `http`, `https`, `tcp`, `udp`
- `host`: proxy host
- default: `container_name`
- `port`: proxy port
- default: first expose port (declared in `Dockerfile` or `docker-compose.yml`)
- `http(s)`: number in range og `0 - 65535`
- `tcp/udp`: `[<listeningPort>:]<targetPort>`
- `listeningPort`: number, when it is omitted (not suggested), a free port starting from 20000 will be used.
- `targetPort`: number, or predefined names (see [constants.go:14](src/go-proxy/constants.go#L14))
- `no_tls_verify`: whether skip tls verify when scheme is https
- default: `false`
- `path`: proxy path _(http(s) proxy only)_
- default: empty
- `path_mode`: mode for path handling
- default: empty
- allowed: empty, `forward`, `sub`
- `empty`: remove path prefix from URL when proxying
1. apps.y.z/webdav -> webdav:80
2. apps.y.z./webdav/path/to/file -> webdav:80/path/to/file
- `forward`: path remain unchanged
1. apps.y.z/webdav -> webdav:80/webdav
2. apps.y.z./webdav/path/to/file -> webdav:80/webdav/path/to/file
- `sub`: (experimental) remove path prefix from URL and also append path to HTML link attributes (`src`, `href` and `action`) and Javascript `fetch(url)` by response body substitution
e.g. apps.y.z/app1 -> webdav:80, `href="/app1/path/to/file"` -> `href="/path/to/file"`
- `load_balance`: _(Docker only)_ enable load balance
- allowed: `1`, `true`
### Environment variables
## Environment variables
- `GOPROXY_DEBUG`: set to `1` or `true` to enable debug behaviors (i.e. output, etc.)
- `GOPROXY_HOST_NETWORK`: _(Docker only)_ set to `1` when `network_mode: host`
- `GOPROXY_NO_SCHEMA_VALIDATION`: disable schema validation on config load / reload **(for testing new DNS Challenge providers)**
### Config File
[🔼Back to top](#table-of-content)
## Config File
See [config.example.yml](config.example.yml) for more
#### Fields
### Fields
- `autocert`: autocert configuration
- `email`: ACME Email
- `domains`: a list of domains for cert registration
- `provider`: DNS Challenge provider, see [Supported DNS Challenge Providers](#supported-dns-challenge-providers)
- `options`: provider specific options
- `options`: [provider specific options](#supported-dns-challenge-providers)
- `providers`: reverse proxy providers configuration
- `kind`: provider kind (string), see [Provider Kinds](#provider-kinds)
- `value`: provider specific value
#### Provider Kinds
[🔼Back to top](#table-of-content)
### Provider Kinds
- `docker`: load reverse proxies from docker
@ -209,12 +172,16 @@ See [config.example.yml](config.example.yml) for more
value: relative path of file to `config/`
[🔼Back to top](#table-of-content)
### Provider File
Fields are same as [docker labels](#labels-docker) starting from `scheme`
Fields are same as [docker labels](docs/docker.md#labels) starting from `scheme`
See [providers.example.yml](providers.example.yml) for examples
[🔼Back to top](#table-of-content)
### Supported DNS Challenge Providers
- Cloudflare
@ -223,85 +190,19 @@ See [providers.example.yml](providers.example.yml) for examples
Follow [this guide](https://cloudkul.com/blog/automcatic-renew-and-generate-ssl-on-your-website-using-lego-client/) to create a new token with `Zone.DNS` read and edit permissions
- CloudDNS
- `client_id`
- `email`
- `password`
- DuckDNS (thanks [earvingad](https://github.com/earvingad))
- `token`: DuckDNS Token
To add more provider support, see [this](docs/add_dns_provider.md)
## Examples
See [docker.md](docs/docker.md#docker-compose-example) for complete examples
### Single port configuration example
```yaml
# (default) https://<container_name>.y.z
whoami:
image: traefik/whoami
container_name: whoami # => whoami.y.z
# enable both subdomain and path matching:
whoami:
image: traefik/whoami
container_name: whoami
labels:
- proxy.aliases=whoami,apps
- proxy.apps.path=/whoami
# 1. visit https://whoami.y.z
# 2. visit https://apps.y.z/whoami
```
### Multiple ports configuration example
```yaml
minio:
image: quay.io/minio/minio
container_name: minio
...
labels:
- proxy.aliases=minio,minio-console
- proxy.minio.port=9000
- proxy.minio-console.port=9001
# visit https://minio.y.z to access minio
# visit https://minio-console.y.z/whoami to access minio console
```
### TCP/UDP configuration example
```yaml
# In the app
app-db:
image: postgres:15
container_name: app-db
...
labels:
# Optional (postgres is in the known image map)
- proxy.app-db.scheme=tcp
# Optional (first free port will be used for listening port)
- proxy.app-db.port=20000:postgres
# In go-proxy
go-proxy:
...
ports:
- 80:80
...
- <your desired port>:20000/tcp
# or 20000-20010:20000-20010/tcp to declare large range at once
# access app-db via <*>.y.z:20000
```
## Load balancing Configuration Example
```yaml
nginx:
...
deploy:
mode: replicated
replicas: 3
labels:
- proxy.nginx.load_balance=1 # allowed: [1, true]
```
[🔼Back to top](#table-of-content)
## Troubleshooting
@ -309,6 +210,8 @@ Q: How to fix when it shows "no matching route for subdomain \<subdomain>"?
A: Make sure the container is running, and \<subdomain> matches any container name / alias
[🔼Back to top](#table-of-content)
## Benchmarks
Benchmarked with `wrk` connecting `traefik/whoami`'s `/bench` endpoint
@ -414,14 +317,20 @@ Local benchmark (client running wrk and `go-proxy` server are under same proxmox
Transfer/sec: 10.94MB
```
[🔼Back to top](#table-of-content)
## Known issues
None
- Cert "renewal" is actually obtaining a new cert instead of renewing the existing one
[🔼Back to top](#table-of-content)
## Memory usage
It takes ~15 MB for 50 proxy entries
[🔼Back to top](#table-of-content)
## Build it yourself
1. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already
@ -433,3 +342,5 @@ It takes ~15 MB for 50 proxy entries
4. build binary with `make build`
5. start your container with `make up` (docker) or `bin/go-proxy` (binary)
[🔼Back to top](#table-of-content)

View file

@ -37,5 +37,5 @@
password: b9841238feb177a84330f
```
5. Run and test if it works
5. Run with `GOPROXY_NO_SCHEMA_VALIDATION=1` and test if it works
6. Commit and create pull request

View file

@ -1,4 +1,24 @@
# Getting started with `go-proxy` docker container
# Docker container guide
## Table of content
<!-- TOC -->
- [Table of content](#table-of-content)
- [Setup](#setup)
- [Labels](#labels)
- [Labels (docker specific)](#labels-docker-specific)
- [Troubleshooting](#troubleshooting)
- [Docker compose examples](#docker-compose-examples)
- [Local docker provider in bridge network](#local-docker-provider-in-bridge-network)
- [Remote docker provider](#remote-docker-provider)
- [Explaination](#explaination)
- [Remote setup](#remote-setup)
- [Proxy setup](#proxy-setup)
- [Local docker provider in host network](#local-docker-provider-in-host-network)
- [Proxy setup](#proxy-setup)
- [Services URLs for above examples](#services-urls-for-above-examples)
<!-- /TOC -->
## Setup
@ -46,6 +66,77 @@
7. Start editing config files in `http://<ip>:8080`
[🔼Back to top](#table-of-content)
## Labels
- `proxy.aliases`: comma separated aliases for subdomain matching
- default: container name
- `proxy.*.<field>`: wildcard label for all aliases
Below labels has a **`proxy.<alias>.`** prefix (i.e. `proxy.nginx.scheme: http`)
- `scheme`: proxy protocol
- default: `http`
- allowed: `http`, `https`, `tcp`, `udp`
- `host`: proxy host
- default: `container_name`
- `port`: proxy port
- default: first expose port (declared in `Dockerfile` or `docker-compose.yml`)
- `http(s)`: number in range og `0 - 65535`
- `tcp/udp`: `[<listeningPort>:]<targetPort>`
- `listeningPort`: number, when it is omitted (not suggested), a free port starting from 20000 will be used.
- `targetPort`: number, or predefined names (see [constants.go:14](src/go-proxy/constants.go#L14))
- `no_tls_verify`: whether skip tls verify when scheme is https
- default: `false`
- `path`: proxy path _(http(s) proxy only)_
- default: empty
- `path_mode`: mode for path handling
- default: empty
- allowed: empty, `forward`, `sub`
- `empty`: remove path prefix from URL when proxying
1. apps.y.z/webdav -> webdav:80
2. apps.y.z./webdav/path/to/file -> webdav:80/path/to/file
- `forward`: path remain unchanged
1. apps.y.z/webdav -> webdav:80/webdav
2. apps.y.z./webdav/path/to/file -> webdav:80/webdav/path/to/file
- `sub`: **(experimental)** remove path prefix from URL and also append path to HTML link attributes (`src`, `href` and `action`) and Javascript `fetch(url)` by response body substitution
e.g. apps.y.z/app1 -> webdav:80, `href="/app1/path/to/file"` -> `href="/path/to/file"`
- `set_headers`: a list of header to set, (key:value, one by line)
Duplicated keys will be treated as multiple-value headers
```yaml
labels:
- |
proxy.app.set_headers=
X-Custom-Header1: value1
X-Custom-Header1: value2
X-Custom-Header2: value2
```
- `hide_headers`: comma seperated list of headers to hide
[🔼Back to top](#table-of-content)
## Labels (docker specific)
Below labels has a **`proxy.<alias>.`** prefix (i.e. `proxy.app.headers.hide: X-Powered-By,X-Custom-Header`)
- `headers.set.<header>`: value of header to set
- `headers.hide`: comma seperated list of headers to hide
- `load_balance`: enable load balance
- allowed: `1`, `true`
[🔼Back to top](#table-of-content)
## Troubleshooting
- Firewall issues
@ -64,7 +155,11 @@
`docker network inspect $(docker network ls | awk '$3 == "bridge" { print $1}') | jq -r '.[] | .Name + " " + .IPAM.Config[0].Subnet' -`
## Docker compose example (bridge network)
[🔼Back to top](#table-of-content)
## Docker compose examples
### Local docker provider in bridge network
```yaml
volumes:
@ -136,28 +231,19 @@ services:
- /var/run/docker.sock:/var/run/docker.sock:ro
labels:
- proxy.aliases=gp
- proxy.panel.port=8080
- proxy.gp.port=8080
```
### Services URLs
[🔼Back to top](#table-of-content)
- `gp.yourdomain.com`: go-proxy web panel
- `adg-setup.yourdomain.com`: adguard setup (first time setup)
- `adg.yourdomain.com`: adguard dashboard
- `nginx.yourdomain.com`: nginx
- `yourdomain.com:53`: adguard dns
- `yourdomain.com:25565`: minecraft server
- `yourdomain.com:8211`: palworld server
### Remote docker provider
## Docker compose example (host network)
#### Explaination
### Notice
- Expose container ports to random port in remote host
- Use container port with an asterisk sign **(\*)** before to find remote port automatically
When `go-proxy` is running in `host` network mode, you must:
- set `GOPROXY_HOST_NETWORK=1`
- map ports to host explicitly
- add an asterisk sign **(*)** before `port` number under `labels`
#### Remote setup
```yaml
volumes:
@ -170,7 +256,7 @@ services:
adg:
image: adguard/adguardhome
restart: unless-stopped
ports: # map random ports to container ports
ports: # map container ports
- 80
- 3000
- 53/udp
@ -224,19 +310,62 @@ services:
- 80
volumes:
- nginx:/usr/share/nginx/html
go-proxy:
image: ghcr.io/yusing/go-proxy
container_name: go-proxy
restart: always
network_mode: host # no port mapping needed for host network mode
environment:
- GOPROXY_HOST_NETWORK=1 # required for host network mode
volumes:
- ./config:/app/config
- /var/run/docker.sock:/var/run/docker.sock:ro
labels:
- proxy.aliases=gp
- proxy.panel.port=808
```
**Same services URLs as [`bridge`](#services-urls) example!**
[🔼Back to top](#table-of-content)
#### Proxy setup
```yaml
go-proxy:
image: ghcr.io/yusing/go-proxy
container_name: go-proxy
restart: always
network_mode: host
volumes:
- ./config:/app/config
- /var/run/docker.sock:/var/run/docker.sock:ro
labels:
- proxy.aliases=gp
- proxy.gp.port=8080
```
[🔼Back to top](#table-of-content)
### Local docker provider in host network
Mostly as remote docker setup, see [remote setup](#remote-setup)
With `GOPROXY_HOST_NETWORK=1` to treat it as remote docker provider
#### Proxy setup
```yaml
go-proxy:
image: ghcr.io/yusing/go-proxy
container_name: go-proxy
restart: always
network_mode: host
environment: # this part is needed for local docker in host mode
- GOPROXY_HOST_NETWORK=1
volumes:
- ./config:/app/config
- /var/run/docker.sock:/var/run/docker.sock:ro
labels:
- proxy.aliases=gp
- proxy.gp.port=8080
```
[🔼Back to top](#table-of-content)
### Services URLs for above examples
- `gp.yourdomain.com`: go-proxy web panel
- `adg-setup.yourdomain.com`: adguard setup (first time setup)
- `adg.yourdomain.com`: adguard dashboard
- `nginx.yourdomain.com`: nginx
- `yourdomain.com:53`: adguard dns
- `yourdomain.com:25565`: minecraft server
- `yourdomain.com:8211`: palworld server
[🔼Back to top](#table-of-content)

25
go.mod
View file

@ -9,14 +9,14 @@ require (
github.com/go-acme/lego/v4 v4.16.1
github.com/santhosh-tekuri/jsonschema v1.2.4
github.com/sirupsen/logrus v1.9.3
golang.org/x/net v0.22.0
golang.org/x/net v0.24.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cloudflare/cloudflare-go v0.86.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cloudflare/cloudflare-go v0.92.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
@ -37,17 +37,18 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 // indirect
go.opentelemetry.io/otel v1.25.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.25.0 // indirect
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/sys v0.18.0 // indirect
go.opentelemetry.io/otel/trace v1.25.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.17.0 // indirect
golang.org/x/tools v0.20.0 // indirect
gotest.tools/v3 v3.5.1 // indirect
)

62
go.sum
View file

@ -1,11 +1,11 @@
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cloudflare/cloudflare-go v0.86.0 h1:jEKN5VHNYNYtfDL2lUFLTRo+nOVNPFxpXTstVx0rqHI=
github.com/cloudflare/cloudflare-go v0.86.0/go.mod h1:wYW/5UP02TUfBToa/yKbQHV+r6h1NnJ1Je7XjuGM4Jw=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cloudflare/cloudflare-go v0.92.0 h1:ltJvGvqZ4G6Fm2hHOYZ5RWpJQcrM0oDrsjjZydZhFJQ=
github.com/cloudflare/cloudflare-go v0.92.0/go.mod h1:nUqvBUUDRxNzsDSQjbqUNWHEIYAoUlgRmcAzMKlFdKs=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -58,7 +58,6 @@ github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
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=
@ -79,7 +78,6 @@ 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/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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=
@ -88,61 +86,57 @@ github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XF
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 h1:cEPbyTSEHlQR89XVlyo78gqluF8Y3oMeBkXGWzQsfXY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0/go.mod h1:DKdbWcT4GH1D0Y3Sqt/PFXt2naRKDWtU+eE6oLdFNA8=
go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k=
go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA=
go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM=
go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I=
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
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=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
@ -153,8 +147,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -1,16 +1,26 @@
example: # matching `app.y.z`
# optional, defaults to http
scheme:
scheme: http
# required, proxy target
host: 10.0.0.1
# optional, defaults to 80 for http, 443 for https
port: 80
port: "80"
# optional, defaults to empty
path:
# optional, defaults to sub
# optional, defaults to empty
path_mode:
# optional (https only)
# no_tls_verify: false
# optional headers to set / override (http(s) only)
set_headers:
HEADER_A:
- VALUE_1
- VALUE_2
HEADER_B: [VALUE_3]
# optional headers to hide (http(s) only)
hide_headers:
- HEADER_C
- HEADER_D
app1: # matching `app1.y.z` -> http://x.y.z
host: x.y.z
app2: # `app2` has no effect for tcp / udp, but still has to be unique across files
@ -22,4 +32,7 @@ app3: # matching `app3.y.z` -> https://10.0.0.1/app3
host: 10.0.0.1
path: /app3
path_mode: forward
no_tls_verify: false
no_tls_verify: false
set_headers:
X-Forwarded-Proto: [https]
X-Forwarded-Host: [app3.y.z]

View file

@ -24,28 +24,89 @@
"provider": {
"description": "DNS Challenge Provider",
"type": "string",
"enum": ["cloudflare"]
"enum": ["cloudflare", "clouddns", "duckdns"]
},
"options": {
"description": "Provider specific options",
"type": "object",
"properties": {
"auth_token": {
"description": "Cloudflare API Token with Zone Scope",
"type": "string"
}
}
"type": "object"
}
},
"required": ["email", "domains", "provider", "options"],
"anyOf": [
"allOf": [
{
"properties": {
"provider": {
"const": "cloudflare"
},
"options": {
"required": ["auth_token"]
"if": {
"properties": {
"provider": {
"const": "cloudflare"
}
}
},
"then": {
"properties": {
"options": {
"required": ["auth_token"],
"additionalProperties": false,
"properties": {
"auth_token": {
"description": "Cloudflare API Token with Zone Scope",
"type": "string"
}
}
}
}
}
},
{
"if": {
"properties": {
"provider": {
"const": "clouddns"
}
}
},
"then": {
"properties": {
"options": {
"required": ["client_id", "email", "password"],
"additionalProperties": false,
"properties": {
"client_id": {
"description": "CloudDNS Client ID",
"type": "string"
},
"email": {
"description": "CloudDNS Email",
"type": "string"
},
"password": {
"description": "CloudDNS Password",
"type": "string"
}
}
}
}
}
},
{
"if": {
"properties": {
"provider": {
"const": "duckdns"
}
}
},
"then": {
"properties": {
"options": {
"required": ["token"],
"additionalProperties": false,
"properties": {
"token": {
"description": "DuckDNS Token",
"type": "string"
}
}
}
}
}
}

View file

@ -55,7 +55,9 @@
"no_tls_verify": {
"description": "Disable TLS verification for https proxy",
"type": "boolean"
}
},
"set_headers": {},
"hide_headers": {}
},
"required": ["host"],
"additionalProperties": false,
@ -129,6 +131,23 @@
"type": "null"
}
]
},
"set_headers": {
"type": "object",
"description": "Proxy headers to set",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"hide_headers": {
"type":"array",
"description": "Proxy headers to hide",
"items": {
"type": "string"
}
}
}
},
@ -145,6 +164,12 @@
},
"path_mode": {
"not": true
},
"set_headers": {
"not": true
},
"hide_headers": {
"not": true
}
},
"required": ["port"]

View file

@ -98,7 +98,7 @@ Wants=network-online.target systemd-networkd-wait-online.service
Type=simple
ExecStart=${APP_ROOT}/bin/go-proxy
WorkingDirectory=${APP_ROOT}
Environment="IS_SYSTEMD=1"
Environment="GOPROXY_IS_SYSTEMD=1"
Restart=on-failure
RestartSec=1s
KillMode=process

View file

@ -9,6 +9,7 @@ import (
"crypto/x509"
"os"
"path"
"slices"
"sync"
"time"
@ -18,6 +19,7 @@ import (
"github.com/go-acme/lego/v4/lego"
"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/registration"
)
@ -54,7 +56,7 @@ type AutoCertProvider interface {
GetExpiries() CertExpiries
LoadCert() bool
ObtainCert() NestedErrorLike
RenewalOn() time.Time
ShouldRenewOn() time.Time
ScheduleRenewal()
}
@ -72,7 +74,7 @@ func (cfg AutoCertConfig) GetProvider() (AutoCertProvider, error) {
}
gen, ok := providersGenMap[cfg.Provider]
if !ok {
ne.Extraf("unknown provider: %s", cfg.Provider)
ne.Extraf("unknown provider: %q", cfg.Provider)
}
if ne.HasExtras() {
return nil, ne
@ -189,13 +191,9 @@ func (p *autoCertProvider) LoadCert() bool {
return true
}
func (p *autoCertProvider) RenewalOn() time.Time {
t := time.Now().AddDate(0, 0, 3)
func (p *autoCertProvider) ShouldRenewOn() time.Time {
for _, expiry := range p.certExpiries {
if expiry.Before(t) {
return time.Now()
}
return t
return expiry.AddDate(0, -1, 0)
}
// this line should never be reached
panic("no certificate available")
@ -203,8 +201,8 @@ func (p *autoCertProvider) RenewalOn() time.Time {
func (p *autoCertProvider) ScheduleRenewal() {
for {
t := time.Until(p.RenewalOn())
aclog.Infof("next renewal in %v", t)
t := time.Until(p.ShouldRenewOn())
aclog.Infof("next renewal in %v", t.Round(time.Second))
time.Sleep(t)
err := p.renewIfNeeded()
if err != nil {
@ -230,7 +228,29 @@ func (p *autoCertProvider) saveCert(cert *certificate.Resource) NestedErrorLike
}
func (p *autoCertProvider) needRenewal() bool {
return time.Now().After(p.RenewalOn())
expired := time.Now().After(p.ShouldRenewOn())
if expired {
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))
copy(wantedDomains, 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
}
}
return false
}
func (p *autoCertProvider) renewIfNeeded() NestedErrorLike {
@ -249,6 +269,7 @@ func (p *autoCertProvider) renewIfNeeded() NestedErrorLike {
for {
err := p.ObtainCert()
if err == nil {
aclog.Info("renewed certificate")
return nil
}
trials++
@ -305,5 +326,6 @@ func setOptions[T interface{}](cfg *T, opt ProviderOptions) error {
var providersGenMap = map[string]ProviderGenerator{
"cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
"clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
"clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
"duckdns": providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
}

View file

@ -1,10 +1,10 @@
package main
import (
"os"
"sync"
"time"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
)
@ -25,12 +25,10 @@ type Config interface {
func NewConfig(path string) Config {
cfg := &config{
reader: &FileReader{Path: path},
l: cfgl,
}
cfg.watcher = NewFileWatcher(
path,
cfg.MustReload, // OnChange
func() { os.Exit(1) }, // OnDelete
)
// must init fields above before creating watcher
cfg.watcher = cfg.NewFileWatcher()
return cfg
}
@ -43,10 +41,7 @@ func (cfg *config) Value() configModel {
return *cfg.m
}
func (cfg *config) Load(reader ...Reader) error {
cfg.mutex.Lock()
defer cfg.mutex.Unlock()
func (cfg *config) Load() error {
if cfg.reader == nil {
panic("config reader not set")
}
@ -68,7 +63,7 @@ func (cfg *config) Load(reader ...Reader) error {
ne.With(err)
}
pErrs := NewNestedError("errors in these providers")
pErrs := NewNestedError("these providers have errors")
for name, p := range model.Providers {
if p.Kind != ProviderKind_File {
@ -90,13 +85,16 @@ func (cfg *config) Load(reader ...Reader) error {
return ne
}
cfg.mutex.Lock()
defer cfg.mutex.Unlock()
cfg.m = model
return nil
}
func (cfg *config) MustLoad() {
if err := cfg.Load(); err != nil {
cfgl.Fatal(err)
cfg.l.Fatal(err)
}
}
@ -115,7 +113,7 @@ func (cfg *config) Reload() error {
func (cfg *config) MustReload() {
if err := cfg.Reload(); err != nil {
cfgl.Fatal(err)
cfg.l.Fatal(err)
}
}
@ -144,7 +142,7 @@ func (cfg *config) StartProviders() {
cfg.providerInitialized = true
if pErrs.HasExtras() {
cfgl.Error(pErrs)
cfg.l.Error(pErrs)
}
}
@ -194,6 +192,7 @@ func defaultConfig() *configModel {
type config struct {
m *configModel
l logrus.FieldLogger
reader Reader
watcher Watcher
mutex sync.Mutex

View file

@ -123,7 +123,7 @@ var (
}
)
const wildcardLabelPrefix = "proxy.*."
const wildcardAlias = "*"
const clientUrlFromEnv = "FROM_ENV"
@ -147,18 +147,6 @@ const (
var (
configSchema *jsonschema.Schema
providersSchema *jsonschema.Schema
_ = func() *jsonschema.Compiler {
c := jsonschema.NewCompiler()
c.Draft = jsonschema.Draft7
var err error
if configSchema, err = c.Compile(configSchemaPath); err != nil {
panic(err)
}
if providersSchema, err = c.Compile(providersSchemaPath); err != nil {
panic(err)
}
return c
}()
)
const (
@ -168,17 +156,36 @@ const (
const udpBufferSize = 1500
var isHostNetworkMode = os.Getenv("GOPROXY_HOST_NETWORK") == "1"
var isHostNetworkMode = getEnvBool("GOPROXY_HOST_NETWORK")
var logLevel = func() logrus.Level {
switch os.Getenv("GOPROXY_DEBUG") {
case "1", "true":
if getEnvBool("GOPROXY_DEBUG") {
logrus.SetLevel(logrus.DebugLevel)
}
return logrus.GetLevel()
}()
var isRunningAsService = func() bool {
v := os.Getenv("IS_SYSTEMD")
return v == "1"
}()
var isRunningAsService = getEnvBool("IS_SYSTEMD") || getEnvBool("GOPROXY_IS_SYSTEMD") // IS_SYSTEMD is deprecated
var noSchemaValidation = getEnvBool("GOPROXY_NO_SCHEMA_VALIDATION")
func getEnvBool(key string) bool {
v := os.Getenv(key)
return v == "1" || v == "true"
}
func initSchema() {
if noSchemaValidation {
return
}
c := jsonschema.NewCompiler()
c.Draft = jsonschema.Draft7
var err error
if configSchema, err = c.Compile(configSchemaPath); err != nil {
panic(err)
}
if providersSchema, err = c.Compile(providersSchemaPath); err != nil {
panic(err)
}
}

View file

@ -1,6 +1,7 @@
package main
import (
"errors"
"fmt"
"net/http"
"strconv"
@ -14,20 +15,15 @@ import (
"golang.org/x/net/context"
)
func (p *Provider) setConfigField(c *ProxyConfig, label string, value string, prefix string) error {
if strings.HasPrefix(label, prefix) {
field := strings.TrimPrefix(label, prefix)
if err := setFieldFromSnake(c, field, value); err != nil {
return err
}
}
return nil
func setConfigField(pl *ProxyLabel, c *ProxyConfig) error {
return setFieldFromSnake(c, pl.Field, pl.Value)
}
func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP string) ProxyConfigSlice {
func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP string) (ProxyConfigSlice, error) {
var aliases []string
cfgs := make(ProxyConfigSlice, 0)
cfgMap := make(map[string]*ProxyConfig)
containerName := strings.TrimPrefix(container.Names[0], "/")
aliasesLabel, ok := container.Labels["proxy.aliases"]
@ -35,7 +31,8 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP
if !ok {
aliases = []string{containerName}
} else {
aliases = strings.Split(aliasesLabel, ",")
v, _ := commaSepParser(aliasesLabel)
aliases = v.([]string)
}
if clientIP == "" && isHostNetworkMode {
@ -44,21 +41,42 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP
isRemote := clientIP != ""
for _, alias := range aliases {
ne := NewNestedError("invalid label config").Subjectf("container %s", containerName)
cfgMap[alias] = &ProxyConfig{}
}
l := p.l.WithField("container", containerName).WithField("alias", alias)
config := NewProxyConfig(p)
prefix := fmt.Sprintf("proxy.%s.", alias)
for label, value := range container.Labels {
err := p.setConfigField(&config, label, value, prefix)
if err != nil {
ne.ExtraError(NewNestedErrorFrom(err).Subjectf("alias %s", alias))
}
err = p.setConfigField(&config, label, value, wildcardLabelPrefix)
if err != nil {
ne.ExtraError(NewNestedErrorFrom(err).Subjectf("alias %s", alias))
ne := NewNestedError("these labels have errors").Subject(containerName)
for label, value := range container.Labels {
pl, err := parseProxyLabel(label, value)
if err != nil {
if !errors.Is(err, errNotProxyLabel) {
ne.ExtraError(NewNestedErrorFrom(err).Subject(label))
}
continue
}
if pl.Alias == wildcardAlias {
for alias := range cfgMap {
pl.Alias = alias
err = setConfigField(pl, cfgMap[alias])
if err != nil {
ne.ExtraError(NewNestedErrorFrom(err).Subject(pl.Alias))
}
}
continue
}
config, ok := cfgMap[pl.Alias]
if !ok {
ne.ExtraError(NewNestedError("unknown alias").Subject(pl.Alias))
continue
}
err = setConfigField(pl, config)
if err != nil {
ne.ExtraError(NewNestedErrorFrom(err).Subject(pl.Alias))
}
}
for alias, config := range cfgMap {
l := p.l.WithField("alias", alias)
if config.Port == "" {
config.Port = fmt.Sprintf("%d", selectPort(container, isRemote))
}
@ -70,8 +88,6 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP
switch {
case strings.HasSuffix(config.Port, "443"):
config.Scheme = "https"
case strings.HasPrefix(container.Image, "sha256:"):
config.Scheme = "http"
default:
imageName := getImageName(container)
_, isKnownImage := ImageNamePortMapTCP[imageName]
@ -90,7 +106,7 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP
var err error
// find matching port
srcPort := config.Port[1:]
config.Port, err = findMatchingContainerPort(container,srcPort)
config.Port, err = findMatchingContainerPort(container, srcPort)
if err != nil {
ne.ExtraError(NewNestedErrorFrom(err).Subjectf("alias %s", alias))
}
@ -98,8 +114,7 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP
config.Port = fmt.Sprintf("%s:%s", srcPort, config.Port)
}
}
if config.Host == "" {
switch {
case isRemote:
@ -126,12 +141,15 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP
config.Alias = alias
if ne.HasExtras() {
l.Error(ne)
continue
}
cfgs = append(cfgs, config)
cfgs = append(cfgs, *config)
}
return cfgs
if ne.HasExtras() {
return nil, ne
}
return cfgs, nil
}
func (p *Provider) getDockerClient() (*client.Client, error) {
@ -196,8 +214,19 @@ func (p *Provider) getDockerProxyConfigs() (ProxyConfigSlice, error) {
cfgs := make(ProxyConfigSlice, 0)
ne := NewNestedError("these containers have errors")
for _, container := range containerSlice {
cfgs = append(cfgs, p.getContainerProxyConfigs(&container, clientIP)...)
ccfgs, err := p.getContainerProxyConfigs(&container, clientIP)
if err != nil {
ne.ExtraError(err)
continue
}
cfgs = append(cfgs, ccfgs...)
}
if ne.HasExtras() {
// print but ignore
p.l.Error(ne)
}
return cfgs, nil

View file

@ -1,6 +1,7 @@
package main
import (
"errors"
"fmt"
"strings"
"sync"
@ -46,6 +47,10 @@ func NewNestedErrorFrom(err error) NestedErrorLike {
if err == nil {
panic("cannot convert nil error to NestedError")
}
errUnwrap := errors.Unwrap(err)
if errUnwrap != nil {
return NewNestedErrorFrom(errUnwrap)
}
return NewNestedError(err.Error())
}
@ -92,23 +97,23 @@ func (ne *NestedError) Level() int {
return ne.level
}
func (ef *NestedError) Error() string {
func (ne *NestedError) Error() string {
var buf strings.Builder
ef.writeToSB(&buf, "")
ne.writeToSB(&buf, ne.level, "")
return buf.String()
}
func (ef *NestedError) HasInner() bool {
return ef.inner != nil
func (ne *NestedError) HasInner() bool {
return ne.inner != nil
}
func (ef *NestedError) HasExtras() bool {
return len(ef.extras) > 0
func (ne *NestedError) HasExtras() bool {
return len(ne.extras) > 0
}
func (ef *NestedError) With(inner error) NestedErrorLike {
ef.Lock()
defer ef.Unlock()
func (ne *NestedError) With(inner error) NestedErrorLike {
ne.Lock()
defer ne.Unlock()
var in *NestedError
@ -116,79 +121,75 @@ func (ef *NestedError) With(inner error) NestedErrorLike {
case NestedErrorLike:
in = t.copy()
default:
in = &NestedError{extras: []string{t.Error()}}
in = &NestedError{message: t.Error()}
}
if ef.inner == nil {
ef.inner = in
if ne.inner == nil {
ne.inner = in
} else {
ef.inner.ExtraError(in)
ne.inner.ExtraError(in)
}
root := ef
root := ne
for root.inner != nil {
root.inner.level = root.level + 1
root = root.inner
}
return ef
return ne
}
func (ef *NestedError) addLevel(level int) NestedErrorLike {
ef.level += level
if ef.inner != nil {
ef.inner.addLevel(level)
func (ne *NestedError) addLevel(level int) NestedErrorLike {
ne.level += level
if ne.inner != nil {
ne.inner.addLevel(level)
}
return ef
return ne
}
func (ef *NestedError) copy() *NestedError {
func (ne *NestedError) copy() *NestedError {
var inner *NestedError
if ef.inner != nil {
inner = ef.inner.copy()
if ne.inner != nil {
inner = ne.inner.copy()
}
return &NestedError{
subject: ef.subject,
message: ef.message,
extras: ef.extras,
subject: ne.subject,
message: ne.message,
extras: ne.extras,
inner: inner,
level: ef.level,
}
}
func (ef *NestedError) writeIndents(sb *strings.Builder, level int) {
func (ne *NestedError) writeIndents(sb *strings.Builder, level int) {
for i := 0; i < level; i++ {
sb.WriteString(" ")
}
}
func (ef *NestedError) writeToSB(sb *strings.Builder, prefix string) {
ef.writeIndents(sb, ef.level)
func (ne *NestedError) writeToSB(sb *strings.Builder, level int, prefix string) {
ne.writeIndents(sb, level)
sb.WriteString(prefix)
if ef.subject != "" {
sb.WriteRune('"')
sb.WriteString(ef.subject)
sb.WriteRune('"')
if ef.message != "" {
sb.WriteString(":\n")
} else {
sb.WriteRune('\n')
if ne.subject != "" {
sb.WriteString(ne.subject)
if ne.message != "" {
sb.WriteString(": ")
}
}
if ef.message != "" {
ef.writeIndents(sb, ef.level)
sb.WriteString(ef.message)
sb.WriteRune('\n')
if ne.message != "" {
sb.WriteString(ne.message)
}
for _, l := range ef.extras {
l = strings.TrimSpace(l)
if ne.HasExtras() || ne.HasInner() {
sb.WriteString(":\n")
}
level += 1
for _, l := range ne.extras {
if l == "" {
continue
}
ef.writeIndents(sb, ef.level)
ne.writeIndents(sb, level)
sb.WriteString("- ")
sb.WriteString(l)
sb.WriteRune('\n')
}
if ef.inner != nil {
ef.inner.writeToSB(sb, "- ")
if ne.inner != nil {
ne.inner.writeToSB(sb, level, "- ")
}
}

View file

@ -0,0 +1,66 @@
package main
import (
"testing"
)
func AssertEq(t *testing.T, got, want string) {
t.Helper()
if got != want {
t.Errorf("expected %q, got %q", want, got)
}
}
func TestErrorSimple(t *testing.T) {
ne := NewNestedError("foo bar")
AssertEq(t, ne.Error(), "foo bar")
ne.Subject("baz")
AssertEq(t, ne.Error(), "baz: foo bar")
}
func TestErrorSubjectOnly(t *testing.T) {
ne := NewNestedError("").Subject("bar")
AssertEq(t, ne.Error(), "bar")
}
func TestErrorExtra(t *testing.T) {
ne := NewNestedError("foo").Extra("bar").Extra("baz")
AssertEq(t, ne.Error(), "foo:\n - bar\n - baz\n")
}
func TestErrorNested(t *testing.T) {
inner := NewNestedError("inner").
Extra("123").
Extra("456")
inner2 := NewNestedError("inner").
Subject("2").
Extra("456").
Extra("789")
inner3 := NewNestedError("inner").
Subject("3").
Extra("456").
Extra("789")
ne := NewNestedError("foo").
Extra("bar").
Extra("baz").
ExtraError(inner).
With(inner.With(inner2.With(inner3)))
want :=
`foo:
- bar
- baz
- inner:
- 123
- 456
- inner:
- 123
- 456
- 2: inner:
- 456
- 789
- 3: inner:
- 456
- 789
`
AssertEq(t, ne.Error(), want)
}

View file

@ -34,7 +34,7 @@ func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) {
tr = transport
}
proxy := NewSingleHostReverseProxy(url, tr)
proxy := NewReverseProxy(url, tr, config)
route := &HTTPRoute{
Alias: config.Alias,
@ -42,36 +42,35 @@ func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) {
Path: config.Path,
Proxy: proxy,
PathMode: config.PathMode,
l: hrlog.WithFields(logrus.Fields{
"alias": config.Alias,
// "path": config.Path,
// "path_mode": config.PathMode,
}),
l: logrus.WithField("alias", config.Alias),
}
var rewriteBegin = proxy.Rewrite
var rewrite func(*ProxyRequest)
var modifyResponse func(*http.Response) error
switch {
case config.Path == "", config.PathMode == ProxyPathMode_Forward:
// no path or forward path
if config.Path == "" || config.PathMode == ProxyPathMode_Forward {
rewrite = rewriteBegin
case config.PathMode == ProxyPathMode_RemovedPath:
rewrite = func(pr *ProxyRequest) {
rewriteBegin(pr)
pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path)
} else {
switch config.PathMode {
case ProxyPathMode_RemovedPath:
rewrite = func(pr *ProxyRequest) {
rewriteBegin(pr)
pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path)
}
case ProxyPathMode_Sub:
rewrite = func(pr *ProxyRequest) {
rewriteBegin(pr)
// disable compression
pr.Out.Header.Set("Accept-Encoding", "identity")
// remove path prefix
pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path)
}
modifyResponse = config.pathSubModResp
default:
return nil, NewNestedError("invalid path mode").Subject(config.PathMode)
}
case config.PathMode == ProxyPathMode_Sub:
rewrite = func(pr *ProxyRequest) {
rewriteBegin(pr)
// disable compression
pr.Out.Header.Set("Accept-Encoding", "identity")
// remove path prefix
pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path)
}
modifyResponse = config.pathSubModResp
default:
return nil, NewNestedError("invalid path mode").Subject(config.PathMode)
}
if logLevel == logrus.DebugLevel {
@ -96,8 +95,9 @@ func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) {
}
func (r *HTTPRoute) Start() {
// dummy
httpRoutes.Get(r.Alias).Add(r.Path, r)
}
func (r *HTTPRoute) Stop() {
httpRoutes.Delete(r.Alias)
}

View file

@ -2,10 +2,9 @@ package main
import "github.com/sirupsen/logrus"
var palog = logrus.WithField("component", "panel")
var prlog = logrus.WithField("component", "provider")
var cfgl = logrus.WithField("component", "config")
var hrlog = logrus.WithField("component", "http_proxy")
var srlog = logrus.WithField("component", "stream")
var wlog = logrus.WithField("component", "watcher")
var aclog = logrus.WithField("component", "autocert")
var palog = logrus.WithField("?", "panel")
var cfgl = logrus.WithField("?", "config")
var hrlog = logrus.WithField("?", "http")
var srlog = logrus.WithField("?", "stream")
var wlog = logrus.WithField("?", "watcher")
var aclog = logrus.WithField("?", "autocert")

View file

@ -43,6 +43,8 @@ func main() {
return
}
initSchema()
cfg = NewConfig(configPath)
cfg.MustLoad()

View file

@ -1,8 +1,6 @@
package main
import (
"sync"
"github.com/sirupsen/logrus"
)
@ -10,15 +8,17 @@ type Provider struct {
Kind string `json:"kind"` // docker, file
Value string `json:"value"`
watcher Watcher
routes map[string]Route // id -> Route
mutex sync.Mutex
l logrus.FieldLogger
watcher Watcher
routes map[string]Route // id -> Route
l logrus.FieldLogger
reloadReqCh chan struct{}
}
// Init is called after LoadProxyConfig
func (p *Provider) Init(name string) error {
p.l = prlog.WithFields(logrus.Fields{"kind": p.Kind, "name": name})
p.l = logrus.WithField("provider", name)
p.reloadReqCh = make(chan struct{}, 1)
defer p.initWatcher()
if err := p.loadProxyConfig(); err != nil {
@ -40,16 +40,23 @@ func (p *Provider) StopAllRoutes() {
}
func (p *Provider) ReloadRoutes() {
p.mutex.Lock()
defer p.mutex.Unlock()
select {
case p.reloadReqCh <- struct{}{}:
defer func() {
<-p.reloadReqCh
}()
p.StopAllRoutes()
err := p.loadProxyConfig()
if err != nil {
p.l.Error("failed to reload routes: ", err)
p.StopAllRoutes()
err := p.loadProxyConfig()
if err != nil {
p.l.Error("failed to reload routes: ", err)
return
}
p.StartAllRoutes()
default:
p.l.Info("reload request already in progress")
return
}
p.StartAllRoutes()
}
func (p *Provider) loadProxyConfig() error {
@ -97,9 +104,9 @@ func (p *Provider) initWatcher() error {
if err != nil {
return NewNestedError("unable to create docker client").With(err)
}
p.watcher = NewDockerWatcher(dockerClient, p.ReloadRoutes)
p.watcher = p.NewDockerWatcher(dockerClient)
case ProviderKind_File:
p.watcher = NewFileWatcher(p.GetFilePath(), p.ReloadRoutes, p.StopAllRoutes)
p.watcher = p.NewFileWatcher()
}
return nil
}

View file

@ -1,29 +1,26 @@
package main
import "fmt"
import (
"fmt"
"net/http"
)
type ProxyConfig struct {
Alias string `yaml:"-" json:"-"`
Scheme string `yaml:"scheme" json:"scheme"`
Host string `yaml:"host" json:"host"`
Port string `yaml:"port" json:"port"`
LoadBalance string `yaml:"-" json:"-"` // docker provider only
NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // http proxy only
Path string `yaml:"path" json:"path"` // http proxy only
PathMode string `yaml:"path_mode" json:"path_mode"` // http proxy only
provider *Provider
Alias string `yaml:"-" json:"-"`
Scheme string `yaml:"scheme" json:"scheme"`
Host string `yaml:"host" json:"host"`
Port string `yaml:"port" json:"port"`
LoadBalance string `yaml:"-" json:"-"` // docker provider only
NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // http proxy only
Path string `yaml:"path" json:"path"` // http proxy only
PathMode string `yaml:"path_mode" json:"path_mode"` // http proxy only
SetHeaders http.Header `yaml:"set_headers" json:"set_headers"` // http proxy only
HideHeaders []string `yaml:"hide_headers" json:"hide_headers"` // http proxy only
}
type ProxyConfigMap map[string]ProxyConfig
type ProxyConfigSlice []ProxyConfig
func NewProxyConfig(provider *Provider) ProxyConfig {
return ProxyConfig{
provider: provider,
}
}
// used by `GetFileProxyConfigs`
func (cfg *ProxyConfig) SetDefaults() error {
err := NewNestedError("invalid proxy config").Subject(cfg.Alias)
@ -55,4 +52,4 @@ func (cfg *ProxyConfig) SetDefaults() error {
func (cfg *ProxyConfig) GetID() string {
return fmt.Sprintf("%s-%s-%s-%s-%s", cfg.Alias, cfg.Scheme, cfg.Host, cfg.Port, cfg.Path)
}
}

View file

@ -0,0 +1,92 @@
package main
import (
"errors"
"fmt"
"net/http"
"strings"
)
type ProxyLabel struct {
Alias string
Field string
Value any
}
var errNotProxyLabel = errors.New("not a proxy label")
var errInvalidSetHeaderLine = errors.New("invalid set header line")
var errInvalidBoolean = errors.New("invalid boolean")
const proxyLabelNamespace = "proxy"
func parseProxyLabel(label string, value string) (*ProxyLabel, error) {
ns := strings.Split(label, ".")
var v any = value
if len(ns) != 3 {
return nil, errNotProxyLabel
}
if ns[0] != proxyLabelNamespace {
return nil, errNotProxyLabel
}
field := ns[2]
var err error
parser, ok := valueParser[field]
if ok {
v, err = parser(v.(string))
if err != nil {
return nil, err
}
}
return &ProxyLabel{
Alias: ns[1],
Field: field,
Value: v,
}, nil
}
func setHeadersParser(value string) (any, error) {
value = strings.TrimSpace(value)
lines := strings.Split(value, "\n")
h := make(http.Header)
for _, line := range lines {
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("%w: %q", errInvalidSetHeaderLine, line)
}
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
h.Add(key, val)
}
return h, nil
}
func commaSepParser(value string) (any, error) {
v := strings.Split(value, ",")
for i := range v {
v[i] = strings.TrimSpace(v[i])
}
return v, nil
}
func boolParser(value string) (any, error) {
switch strings.ToLower(value) {
case "true", "yes", "1":
return true, nil
case "false", "no", "0":
return false, nil
default:
return nil, fmt.Errorf("%w: %q", errInvalidBoolean, value)
}
}
var valueParser = map[string]func(string) (any, error){
"set_headers": setHeadersParser,
"hide_headers": commaSepParser,
"no_tls_verify": boolParser,
}

View file

@ -0,0 +1,186 @@
package main
import (
"errors"
"fmt"
"net/http"
"reflect"
"testing"
)
func makeLabel(alias string, field string) string {
return fmt.Sprintf("proxy.%s.%s", alias, field)
}
func TestNotProxyLabel(t *testing.T) {
pl, err := parseProxyLabel("foo.bar", "1234")
if !errors.Is(err, errNotProxyLabel) {
t.Errorf("expected err NotProxyLabel, got %v", err)
}
if pl != nil {
t.Errorf("expected nil, got %v", pl)
}
_, err = parseProxyLabel("proxy.foo", "bar")
if !errors.Is(err, errNotProxyLabel) {
t.Errorf("expected err InvalidProxyLabel, got %v", err)
}
}
func TestStringProxyLabel(t *testing.T) {
alias := "foo"
field := "ip"
v := "bar"
pl, err := parseProxyLabel(makeLabel(alias, field), v)
if err != nil {
t.Errorf("expected err=nil, got %v", err)
}
if pl.Alias != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Alias)
}
if pl.Field != field {
t.Errorf("expected field=%s, got %s", field, pl.Field)
}
if pl.Value != v {
t.Errorf("expected value=%q, got %s", v, pl.Value)
}
}
func TestBoolProxyLabelValid(t *testing.T) {
alias := "foo"
field := "no_tls_verify"
tests := map[string]bool{
"true": true,
"TRUE": true,
"yes": true,
"1": true,
"false": false,
"FALSE": false,
"no": false,
"0": false,
}
for k, v := range tests {
pl, err := parseProxyLabel(makeLabel(alias, field), k)
if err != nil {
t.Errorf("expected err=nil, got %v", err)
}
if pl.Alias != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Alias)
}
if pl.Field != field {
t.Errorf("expected field=%s, got %s", field, pl.Field)
}
if pl.Value != v {
t.Errorf("expected value=%v, got %v", v, pl.Value)
}
}
}
func TestBoolProxyLabelInvalid(t *testing.T) {
alias := "foo"
field := "no_tls_verify"
_, err := parseProxyLabel(makeLabel(alias, field), "invalid")
if !errors.Is(err, errInvalidBoolean) {
t.Errorf("expected err InvalidProxyLabel, got %v", err)
}
}
func TestHeaderProxyLabelValid(t *testing.T) {
alias := "foo"
field := "set_headers"
v := `
X-Custom-Header1: foo
X-Custom-Header1: bar
X-Custom-Header2: baz
`
h := make(http.Header, 0)
h.Set("X-Custom-Header1", "foo")
h.Add("X-Custom-Header1", "bar")
h.Set("X-Custom-Header2", "baz")
pl, err := parseProxyLabel(makeLabel(alias, field), v)
if err != nil {
t.Errorf("expected err=nil, got %v", err)
}
if pl.Alias != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Alias)
}
if pl.Field != field {
t.Errorf("expected field=%s, got %s", field, pl.Field)
}
hGot, ok := pl.Value.(http.Header)
if !ok {
t.Error("value is not http.Header")
return
}
for k, vWant := range h {
vGot := hGot[k]
if !reflect.DeepEqual(vGot, vWant) {
t.Errorf("expected %s=%q, got %q", k, vWant, vGot)
}
}
}
func TestHeaderProxyLabelInvalid(t *testing.T) {
alias := "foo"
field := "set_headers"
tests := []string{
"X-Custom-Header1 = bar",
"X-Custom-Header1",
}
for _, v := range tests {
_, err := parseProxyLabel(makeLabel(alias, field), v)
if !errors.Is(err, errInvalidSetHeaderLine) {
t.Errorf("expected err InvalidProxyLabel for %q, got %v", v, err)
}
}
}
func TestCommaSepProxyLabelSingle(t *testing.T) {
alias := "foo"
field := "hide_headers"
v := "X-Custom-Header1"
pl, err := parseProxyLabel(makeLabel(alias, field), v)
if err != nil {
t.Errorf("expected err=nil, got %v", err)
}
if pl.Alias != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Alias)
}
if pl.Field != field {
t.Errorf("expected field=%s, got %s", field, pl.Field)
}
sGot, ok := pl.Value.([]string)
sWant := []string{"X-Custom-Header1"}
if !ok {
t.Error("value is not []string")
}
if !reflect.DeepEqual(sGot, sWant) {
t.Errorf("expected %q, got %q", sWant, sGot)
}
}
func TestCommaSepProxyLabelMulti(t *testing.T) {
alias := "foo"
field := "hide_headers"
v := "X-Custom-Header1, X-Custom-Header2,X-Custom-Header3"
pl, err := parseProxyLabel(makeLabel(alias, field), v)
if err != nil {
t.Errorf("expected err=nil, got %v", err)
}
if pl.Alias != alias {
t.Errorf("expected alias=%s, got %s", alias, pl.Alias)
}
if pl.Field != field {
t.Errorf("expected field=%s, got %s", field, pl.Field)
}
sGot, ok := pl.Value.([]string)
sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"}
if !ok {
t.Error("value is not []string")
}
if !reflect.DeepEqual(sGot, sWant) {
t.Errorf("expected %q, got %q", sWant, sGot)
}
}

View file

@ -1,6 +1,6 @@
package main
// A small mod on net/http/httputils
// A small mod on net/http/httputil/reverseproxy.go
// that doubled the performance
import (
@ -8,14 +8,12 @@ import (
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httptrace"
"net/textproto"
"net/url"
"strings"
"time"
"golang.org/x/net/http/httpguts"
)
@ -39,16 +37,16 @@ type ProxyRequest struct {
//
// SetURL rewrites the outbound Host header to match the target's host.
// To preserve the inbound request's Host header (the default behavior
// of [NewSingleHostReverseProxy]):
// of [NewReverseProxy]):
//
// rewriteFunc := func(r *httputil.ProxyRequest) {
// r.SetURL(url)
// r.Out.Host = r.In.Host
// }
func (r *ProxyRequest) SetURL(target *url.URL) {
rewriteRequestURL(r.Out, target)
r.Out.Host = ""
}
// func (r *ProxyRequest) SetURL(target *url.URL) {
// rewriteRequestURL(r.Out, target)
// r.Out.Host = ""
// }
// SetXForwarded sets the X-Forwarded-For, X-Forwarded-Host, and
// X-Forwarded-Proto headers of the outbound request.
@ -132,17 +130,17 @@ type ReverseProxy struct {
// recognizes a response as a streaming response, or
// if its ContentLength is -1; for such responses, writes
// are flushed to the client immediately.
FlushInterval time.Duration
// FlushInterval time.Duration
// ErrorLog specifies an optional logger for errors
// that occur when attempting to proxy the request.
// If nil, logging is done via the log package's standard logger.
ErrorLog *log.Logger
// ErrorLog *log.Logger
// BufferPool optionally specifies a buffer pool to
// get byte slices for use by io.CopyBuffer when
// copying HTTP response bodies.
BufferPool BufferPool
// BufferPool BufferPool
// ModifyResponse is an optional function that modifies the
// Response from the backend. It is called if the backend
@ -203,18 +201,18 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) {
return a.Path + b.Path, apath + bpath
}
// NewSingleHostReverseProxy returns a new [ReverseProxy] that routes
// NewReverseProxy returns a new [ReverseProxy] that routes
// URLs to the scheme, host, and base path provided in target. If the
// target's path is "/base" and the incoming request was for "/dir",
// the target request will be for /base/dir.
//
// NewSingleHostReverseProxy does not rewrite the Host header.
// NewReverseProxy does not rewrite the Host header.
//
// To customize the ReverseProxy behavior beyond what
// NewSingleHostReverseProxy provides, use ReverseProxy directly
// NewReverseProxy provides, use ReverseProxy directly
// with a Rewrite function. The ProxyRequest SetURL method
// may be used to route the outbound request. (Note that SetURL,
// unlike NewSingleHostReverseProxy, rewrites the Host header
// unlike NewReverseProxy, rewrites the Host header
// of the outbound request by default.)
//
// proxy := &ReverseProxy{
@ -223,9 +221,34 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) {
// r.Out.Host = r.In.Host // if desired
// },
// }
func NewSingleHostReverseProxy(target *url.URL, transport *http.Transport) *ReverseProxy {
func NewReverseProxy(target *url.URL, transport *http.Transport, config *ProxyConfig) *ReverseProxy {
// check on init rather than on request
var setHeaders = func(r *http.Request) {}
var hideHeaders = func(r *http.Request) {}
if len(config.SetHeaders) > 0 {
setHeaders = func(r *http.Request) {
h := config.SetHeaders.Clone()
for k, vv := range h {
if k == "Host" {
r.Host = vv[0]
} else {
r.Header[k] = vv
}
}
}
}
if len(config.HideHeaders) > 0 {
hideHeaders = func(r *http.Request) {
for _, k := range config.HideHeaders {
r.Header.Del(k)
}
}
}
return &ReverseProxy{Rewrite: func(pr *ProxyRequest) {
rewriteRequestURL(pr.Out, target)
pr.SetXForwarded()
setHeaders(pr.Out)
hideHeaders(pr.Out)
}, Transport: transport}
}
@ -380,7 +403,7 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Strip client-provided forwarding headers.
// The Rewrite func may use SetXForwarded to set new values
// for these or copy the previous values from the inbound request.
// outreq.Header.Del("Forwarded")
outreq.Header.Del("Forwarded")
// outreq.Header.Del("X-Forwarded-For")
// outreq.Header.Del("X-Forwarded-Host")
// outreq.Header.Del("X-Forwarded-Proto")
@ -388,29 +411,27 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// NOTE: removed
// Remove unparsable query parameters from the outbound request.
// outreq.URL.RawQuery = cleanQueryParams(outreq.URL.RawQuery)
pr := &ProxyRequest{
In: req,
Out: outreq,
}
pr.SetXForwarded() // NOTE: added
p.Rewrite(pr)
outreq = pr.Out
// NOTE: removed
// } else {
// if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
// // If we aren't the first proxy retain prior
// // X-Forwarded-For information as a comma+space
// // separated list and fold multiple headers into one.
// prior, ok := outreq.Header["X-Forwarded-For"]
// omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
// if len(prior) > 0 {
// clientIP = strings.Join(prior, ", ") + ", " + clientIP
// }
// if !omit {
// outreq.Header.Set("X-Forwarded-For", clientIP)
// }
// if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
// // If we aren't the first proxy retain prior
// // X-Forwarded-For information as a comma+space
// // separated list and fold multiple headers into one.
// prior, ok := outreq.Header["X-Forwarded-For"]
// omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
// if len(prior) > 0 {
// clientIP = strings.Join(prior, ", ") + ", " + clientIP
// }
// if !omit {
// outreq.Header.Set("X-Forwarded-For", clientIP)
// }
// }
// }
if _, ok := outreq.Header["User-Agent"]; !ok {
@ -637,11 +658,11 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// }
func (p *ReverseProxy) logf(format string, args ...any) {
if p.ErrorLog != nil {
p.ErrorLog.Printf(format, args...)
} else {
hrlog.Printf(format, args...)
}
// if p.ErrorLog != nil {
// p.ErrorLog.Printf(format, args...)
// } else {
hrlog.Errorf(format, args...)
// }
}
// NOTE: removed

View file

@ -0,0 +1,102 @@
package main
import (
"net/http"
"net/url"
"os"
"reflect"
"testing"
"time"
)
var proxyCfg ProxyConfig
var proxyUrl, _ = url.Parse("http://127.0.0.1:8181")
var proxyServer = NewServer(ServerOptions{
Name: "proxy",
HTTPAddr: ":8080",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
NewReverseProxy(proxyUrl, &http.Transport{}, &proxyCfg).ServeHTTP(w, r)
}),
})
var testServer = NewServer(ServerOptions{
Name: "test",
HTTPAddr: ":8181",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := r.Header
for k, vv := range h {
for _, v := range vv {
w.Header().Add(k, v)
}
}
w.WriteHeader(http.StatusOK)
}),
})
var httpClient = http.DefaultClient
func TestMain(m *testing.M) {
proxyServer.Start()
testServer.Start()
time.Sleep(100 * time.Millisecond)
code := m.Run()
proxyServer.Stop()
testServer.Stop()
os.Exit(code)
}
func TestSetHeader(t *testing.T) {
hWant := http.Header{"X-Test": []string{"foo", "bar"}, "X-Test2": []string{"baz"}}
proxyCfg = ProxyConfig{
Alias: "test",
Scheme: "http",
Host: "127.0.0.1",
Port: "8181",
SetHeaders: hWant,
}
req, err := http.NewRequest("HEAD", "http://127.0.0.1:8080", nil)
if err != nil {
t.Fatal(err)
}
resp, err := httpClient.Do(req)
if err != nil {
t.Fatal(err)
}
hGot := resp.Header
t.Log("headers: ", hGot)
for k, v := range hWant {
if !reflect.DeepEqual(hGot[k], v) {
t.Errorf("header %s: expected %v, got %v", k, v, hGot[k])
}
}
}
func TestHideHeader(t *testing.T) {
hHide := []string{"X-Test", "X-Test2"}
proxyCfg = ProxyConfig{
Alias: "test",
Scheme: "http",
Host: "127.0.0.1",
Port: "8181",
HideHeaders: hHide,
}
req, err := http.NewRequest("HEAD", "http://127.0.0.1:8080", nil)
for _, k := range hHide {
req.Header.Set(k, "foo")
}
if err != nil {
t.Fatal(err)
}
resp, err := httpClient.Do(req)
if err != nil {
t.Fatal(err)
}
hGot := resp.Header
t.Log("headers: ", hGot)
for _, v := range hHide {
_, ok := hGot[v]
if ok {
t.Errorf("header %s: expected hidden, got %v", v, hGot[v])
}
}
}

View file

@ -22,7 +22,6 @@ func NewRoute(cfg *ProxyConfig) (Route, error) {
if err != nil {
return nil, NewNestedErrorFrom(err).Subject(cfg.Alias)
}
httpRoutes.Get(cfg.Alias).Add(cfg.Path, route)
return route, nil
}
}
@ -43,4 +42,4 @@ func isStreamScheme(s string) bool {
}
}
return false
}
}

View file

@ -48,10 +48,18 @@ func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) {
var srcPort, dstPort string
var srcScheme, dstScheme string
l := srlog.WithFields(logrus.Fields{
"alias": config.Alias,
})
portSplit := strings.Split(config.Port, ":")
if len(portSplit) != 2 {
cfgl.Warnf("invalid port %s, assuming it is target port", config.Port)
srcPort = "0"
l.Warnf(
`%s: invalid port %s,
assuming it is target port`,
config.Alias,
config.Port,
)
srcPort = "0" // will assign later
dstPort = config.Port
} else {
srcPort = portSplit[0]
@ -101,11 +109,7 @@ func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) {
stopCh: make(chan struct{}, 1),
connCh: make(chan interface{}),
started: false,
l: srlog.WithFields(logrus.Fields{
"alias": config.Alias,
// "src": fmt.Sprintf("%s://:%d", srcScheme, srcPortInt),
// "dst": fmt.Sprintf("%s://%s:%d", dstScheme, config.Host, dstPortInt),
}),
l: l,
}, nil
}
@ -235,4 +239,4 @@ func (route *StreamRouteBase) grHandleConnections() {
// id -> target
type StreamRoutes SafeMap[string, StreamRoute]
var streamRoutes StreamRoutes = NewSafeMapOf[StreamRoutes]()
var streamRoutes StreamRoutes = NewSafeMapOf[StreamRoutes]()

View file

@ -3,6 +3,7 @@ package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net"
@ -212,13 +213,17 @@ func setFieldFromSnake[T interface{}, VT interface{}](obj *T, field string, valu
field = utils.snakeToPascal(field)
prop := reflect.ValueOf(obj).Elem().FieldByName(field)
if prop.Kind() == 0 {
return NewNestedError("unknown field").Subject(field)
return errors.New("unknown field")
}
prop.Set(reflect.ValueOf(value))
return nil
}
func validateYaml(schema *jsonschema.Schema, data []byte) error {
if noSchemaValidation {
return nil
}
var i interface{}
err := yaml.Unmarshal(data, &i)

View file

@ -1,7 +1,7 @@
package main
import (
"path"
"strings"
"sync"
"time"
@ -22,8 +22,6 @@ type Watcher interface {
}
type watcherBase struct {
name string // for log / error output
kind string // for log / error output
onChange func()
l logrus.FieldLogger
sync.Mutex
@ -42,30 +40,44 @@ type dockerWatcher struct {
wg sync.WaitGroup
}
func newWatcher(kind string, name string, onChange func()) *watcherBase {
func (p *Provider) newWatcher() *watcherBase {
return &watcherBase{
kind: kind,
name: name,
onChange: onChange,
l: wlog.WithFields(logrus.Fields{"kind": kind, "name": name}),
}
}
func NewFileWatcher(p string, onChange func(), onDelete func()) Watcher {
return &fileWatcher{
watcherBase: newWatcher("File", path.Base(p), onChange),
path: p,
onDelete: onDelete,
onChange: p.ReloadRoutes,
l: p.l,
}
}
func NewDockerWatcher(c *client.Client, onChange func()) Watcher {
func (p *Provider) NewFileWatcher() Watcher {
return &fileWatcher{
watcherBase: p.newWatcher(),
path: p.GetFilePath(),
onDelete: p.StopAllRoutes,
}
}
func (p *Provider) NewDockerWatcher(c *client.Client) Watcher {
return &dockerWatcher{
watcherBase: newWatcher("Docker", c.DaemonHost(), onChange),
watcherBase: p.newWatcher(),
client: c,
stopCh: make(chan struct{}, 1),
}
}
func (c *config) newWatcher() *watcherBase {
return &watcherBase{
onChange: c.MustReload,
l: c.l,
}
}
func (c *config) NewFileWatcher() Watcher {
return &fileWatcher{
watcherBase: c.newWatcher(),
path: c.reader.(*FileReader).Path,
onDelete: func() { c.l.Fatal("config file deleted") },
}
}
func (w *fileWatcher) Start() {
w.Lock()
defer w.Unlock()
@ -100,7 +112,7 @@ func (w *fileWatcher) Dispose() {
func (w *dockerWatcher) Start() {
w.Lock()
defer w.Unlock()
dockerWatchMap.Set(w.name, w)
dockerWatchMap.Set(w.client.DaemonHost(), w)
w.wg.Add(1)
go w.watch()
}
@ -114,7 +126,7 @@ func (w *dockerWatcher) Stop() {
close(w.stopCh)
w.wg.Wait()
w.stopCh = nil
dockerWatchMap.Delete(w.name)
dockerWatchMap.Delete(w.client.DaemonHost())
}
func (w *dockerWatcher) Dispose() {
@ -164,10 +176,10 @@ func watchFiles() {
}
switch {
case event.Has(fsnotify.Write):
w.l.Info("file changed")
w.l.Info("file changed: ", event.Name)
go w.onChange()
case event.Has(fsnotify.Remove), event.Has(fsnotify.Rename):
w.l.Info("file renamed / deleted")
w.l.Info("file renamed / deleted: ", event.Name)
go w.onDelete()
}
case err := <-fsWatcher.Errors:
@ -194,16 +206,20 @@ func (w *dockerWatcher) watch() {
case <-w.stopCh:
return
case msg := <-msgChan:
w.l.Infof("container %s %s", msg.Actor.Attributes["name"], msg.Action)
containerName := msg.Actor.Attributes["name"]
if strings.HasPrefix(containerName, "buildx_buildkit_builder-") {
continue
}
w.l.Infof("container %s %s", containerName, msg.Action)
go w.onChange()
case err := <-errChan:
switch {
case client.IsErrConnectionFailed(err):
w.l.Error(NewNestedError("connection failed").Subject(w.name))
w.l.Error("watcher: connection failed")
case client.IsErrNotFound(err):
w.l.Error(NewNestedError("endpoint not found").Subject(w.name))
w.l.Error("watcher: endpoint not found")
default:
w.l.Error(NewNestedErrorFrom(err).Subject(w.name))
w.l.Errorf("watcher: %v", err)
}
time.Sleep(1 * time.Second)
msgChan, errChan = listen()

View file

@ -1 +1 @@
0.4.7
0.4.8