mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-15 14:53:35 +01:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
109c2460fa | ||
|
|
71e8e4a462 | ||
|
|
8e2cc56afb | ||
|
|
6728bc39d2 | ||
|
|
daca4b7735 | ||
|
|
3b597eea29 | ||
|
|
090b73d287 | ||
|
|
96bce79e4b | ||
|
|
d9fd399e43 | ||
|
|
46281aa3b0 | ||
|
|
d39b68bfd8 | ||
|
|
a11ce46028 | ||
|
|
6388d9d44d | ||
|
|
69361aea1b | ||
|
|
26e2154c64 | ||
|
|
a29bf880bc | ||
|
|
1f6d03bdbb | ||
|
|
4a7d898b8e | ||
|
|
521b694aec | ||
|
|
a351de7441 | ||
|
|
ab2dc26b76 | ||
|
|
9a81b13b67 | ||
|
|
626bd9666b | ||
|
|
d7eab2ebcd | ||
|
|
e48b9bbb0a |
2
.github/workflows/docker-image.yml
vendored
2
.github/workflows/docker-image.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
tags: ${{ github.ref_name }}
|
||||
|
||||
- name: Tag as latest
|
||||
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
||||
if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, '-')
|
||||
run: |
|
||||
docker tag ghcr.io/${{ github.repository }}:${{ github.ref_name }} ghcr.io/${{ github.repository }}:latest
|
||||
docker push ghcr.io/${{ github.repository }}:latest
|
||||
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,7 +1,7 @@
|
||||
compose.yml
|
||||
|
||||
config/
|
||||
certs/
|
||||
config*/
|
||||
certs*/
|
||||
bin/
|
||||
|
||||
templates/codemirror/
|
||||
@@ -13,6 +13,8 @@ log/
|
||||
|
||||
go.work.sum
|
||||
|
||||
!src/config/
|
||||
!src/**/
|
||||
|
||||
todo.md
|
||||
todo.md
|
||||
|
||||
.*.swp
|
||||
@@ -1,4 +1,5 @@
|
||||
FROM golang:1.23.1-alpine AS builder
|
||||
RUN apk add --no-cache tzdata
|
||||
COPY src /src
|
||||
ENV GOCACHE=/root/.cache/go-build
|
||||
WORKDIR /src
|
||||
@@ -7,16 +8,17 @@ RUN --mount=type=cache,target="/go/pkg/mod" \
|
||||
go mod download && \
|
||||
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o go-proxy github.com/yusing/go-proxy
|
||||
|
||||
FROM alpine:3.20
|
||||
FROM scratch
|
||||
|
||||
LABEL maintainer="yusing@6uo.me"
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
# copy timezone data
|
||||
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
|
||||
# copy binary
|
||||
COPY --from=builder /src/go-proxy /app/
|
||||
COPY schema/ /app/schema
|
||||
|
||||
RUN chmod +x /app/go-proxy
|
||||
ENV DOCKER_HOST=unix:///var/run/docker.sock
|
||||
ENV GOPROXY_DEBUG=0
|
||||
|
||||
|
||||
5
Makefile
5
Makefile
@@ -12,7 +12,7 @@ build:
|
||||
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy github.com/yusing/go-proxy
|
||||
|
||||
test:
|
||||
cd src && go test ./... && cd ..
|
||||
go test ./src/...
|
||||
|
||||
up:
|
||||
docker compose up -d
|
||||
@@ -42,3 +42,6 @@ rapid-crash:
|
||||
sudo docker run --restart=always --name test_crash debian:bookworm-slim /bin/cat &&\
|
||||
sleep 3 &&\
|
||||
sudo docker rm -f test_crash
|
||||
|
||||
debug-list-containers:
|
||||
bash -c 'echo -e "GET /containers/json HTTP/1.0\r\n" | sudo netcat -U /var/run/docker.sock | tail -n +9 | jq'
|
||||
88
README.md
88
README.md
@@ -6,13 +6,16 @@
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
|
||||
A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse proxy and load balancer with a web UI.
|
||||
[繁體中文文檔請看此](README_CHT.md)
|
||||
|
||||
**Table of content**
|
||||
A lightweight, easy-to-use, and [performant](docs/benchmark_result.md) reverse proxy with a web UI.
|
||||
|
||||
## Table of content
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
- [go-proxy](#go-proxy)
|
||||
- [Table of content](#table-of-content)
|
||||
- [Key Points](#key-points)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Setup](#setup)
|
||||
@@ -21,21 +24,23 @@ A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse pr
|
||||
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
|
||||
- [Config File](#config-file)
|
||||
- [Provider File](#provider-file)
|
||||
- [Known issues](#known-issues)
|
||||
- [Showcase](#showcase)
|
||||
- [idlesleeper](#idlesleeper)
|
||||
- [Build it yourself](#build-it-yourself)
|
||||
|
||||
## Key Points
|
||||
|
||||
- Easy to use
|
||||
- Effortless configuration
|
||||
- Error messages is clear and detailed, easy troubleshooting
|
||||
- Auto certificate obtaining and renewal (See [Supported DNS Challenge Providers](docs/dns_providers.md))
|
||||
- Auto configuration for docker containers
|
||||
- Auto hot-reload on container state / config file changes
|
||||
- Stop containers on idle, wake it up on traffic _(optional)_
|
||||
- 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))
|
||||
- Written in **[Go](https://go.dev)**
|
||||
- Easy to use
|
||||
- Effortless configuration
|
||||
- Error messages is clear and detailed, easy troubleshooting
|
||||
- Auto certificate obtaining and renewal (See [Supported DNS Challenge Providers](docs/dns_providers.md))
|
||||
- Auto configuration for docker containers
|
||||
- Auto hot-reload on container state / config file changes
|
||||
- Stop containers on idle, wake it up on traffic _(optional, see [showcase](#idlesleeper))_
|
||||
- HTTP(s) reserve proxy
|
||||
- TCP and UDP port forwarding
|
||||
- Web UI for configuration and monitoring (See [screenshots](https://github.com/yusing/go-proxy-frontend?tab=readme-ov-file#screenshots))
|
||||
- Written in **[Go](https://go.dev)**
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
@@ -45,14 +50,16 @@ A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse pr
|
||||
|
||||
1. Setup DNS Records, e.g.
|
||||
|
||||
- A Record: `*.y.z` -> `10.0.10.1`
|
||||
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
||||
- A Record: `*.y.z` -> `10.0.10.1`
|
||||
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
||||
|
||||
2. Setup `go-proxy` [See here](docs/docker.md)
|
||||
|
||||
3. Configure `go-proxy`
|
||||
- with text editor (e.g. Visual Studio Code)
|
||||
- or with web config editor via `http://gp.y.z`
|
||||
3. Setup `docker-socket-proxy` (see [example](docs/docker_socket_proxy.md) other machine that is running docker (if any)
|
||||
|
||||
4. Configure `go-proxy`
|
||||
- with text editor (e.g. Visual Studio Code)
|
||||
- or with web config editor via `http://gp.y.z`
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
@@ -70,10 +77,13 @@ A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse pr
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Environment Variable | Description | Default | Values |
|
||||
| ------------------------------ | ------------------------- | ------- | ------- |
|
||||
| `GOPROXY_NO_SCHEMA_VALIDATION` | disable schema validation | `false` | boolean |
|
||||
| `GOPROXY_DEBUG` | enable debug behaviors | `false` | boolean |
|
||||
| Environment Variable | Description | Default | Values |
|
||||
| ------------------------------ | ------------------------------------------- | ---------------- | ------------- |
|
||||
| `GOPROXY_NO_SCHEMA_VALIDATION` | disable schema validation | `false` | boolean |
|
||||
| `GOPROXY_DEBUG` | enable debug behaviors | `false` | boolean |
|
||||
| `GOPROXY_HTTP_ADDR` | http server listening address | `:80` | `[host]:port` |
|
||||
| `GOPROXY_HTTPS_ADDR` | https server listening address (if enabled) | `:443` | `[host]:port` |
|
||||
| `GOPROXY_API_ADDR` | api server listening address | `127.0.0.1:8888` | `[host]:port` |
|
||||
|
||||
### Use JSON Schema in VSCode
|
||||
|
||||
@@ -88,21 +98,21 @@ See [config.example.yml](config.example.yml) for more
|
||||
```yaml
|
||||
# autocert configuration
|
||||
autocert:
|
||||
email: # ACME Email
|
||||
domains: # a list of domains for cert registration
|
||||
provider: # DNS Challenge provider
|
||||
options: # provider specific options
|
||||
- ...
|
||||
email: # ACME Email
|
||||
domains: # a list of domains for cert registration
|
||||
provider: # DNS Challenge provider
|
||||
options: # provider specific options
|
||||
- ...
|
||||
# reverse proxy providers configuration
|
||||
providers:
|
||||
include:
|
||||
- providers.yml
|
||||
- other_file_1.yml
|
||||
- ...
|
||||
docker:
|
||||
local: $DOCKER_HOST
|
||||
remote-1: tcp://10.0.2.1:2375
|
||||
remote-2: ssh://root:1234@10.0.2.2
|
||||
include:
|
||||
- providers.yml
|
||||
- other_file_1.yml
|
||||
- ...
|
||||
docker:
|
||||
local: $DOCKER_HOST
|
||||
remote-1: tcp://10.0.2.1:2375
|
||||
remote-2: ssh://root:1234@10.0.2.2
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
@@ -115,11 +125,11 @@ See [providers.example.yml](providers.example.yml) for examples
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Known issues
|
||||
## Showcase
|
||||
|
||||
- Cert "renewal" is actually obtaining a new cert instead of renewing the existing one
|
||||
### idlesleeper
|
||||
|
||||
- `autocert` config is not hot-reloadable
|
||||

|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
@@ -135,6 +145,4 @@ See [providers.example.yml](providers.example.yml) for examples
|
||||
|
||||
5. build binary with `make build`
|
||||
|
||||
6. start your container with `make up` (docker) or `bin/go-proxy` (binary)
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
144
README_CHT.md
Normal file
144
README_CHT.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# go-proxy
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
|
||||
一個輕量化、易用且[高效](docs/benchmark_result.md)的反向代理和端口轉發工具
|
||||
|
||||
## 目錄
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
- [go-proxy](#go-proxy)
|
||||
- [目錄](#目錄)
|
||||
- [重點](#重點)
|
||||
- [入門指南](#入門指南)
|
||||
- [安裝](#安裝)
|
||||
- [命令行參數](#命令行參數)
|
||||
- [環境變量](#環境變量)
|
||||
- [VSCode 中使用 JSON Schema](#vscode-中使用-json-schema)
|
||||
- [配置文件](#配置文件)
|
||||
- [透過文件配置](#透過文件配置)
|
||||
- [展示](#展示)
|
||||
- [idlesleeper](#idlesleeper)
|
||||
- [源碼編譯](#源碼編譯)
|
||||
|
||||
## 重點
|
||||
|
||||
- 易用
|
||||
- 不需花費太多時間就能輕鬆配置
|
||||
- 除錯簡單
|
||||
- 自動處理 HTTPS 證書(參見[可用的 DNS 供應商](docs/dns_providers.md))
|
||||
- 透過 Docker 容器自動配置
|
||||
- 容器狀態變更時自動熱重載
|
||||
- 容器閒置時自動暫停/停止,入站時自動喚醒
|
||||
- HTTP(s)反向代理
|
||||
- TCP/UDP 端口轉發
|
||||
- 用於配置和監控的前端 Web 面板([截圖](https://github.com/yusing/go-proxy-frontend?tab=readme-ov-file#screenshots))
|
||||
- 使用 **[Go](https://go.dev)** 編寫
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
## 入門指南
|
||||
|
||||
### 安裝
|
||||
|
||||
1. 設置 DNS 記錄,例如:
|
||||
|
||||
- A 記錄: `*.y.z` -> `10.0.10.1`
|
||||
- AAAA 記錄: `*.y.z` -> `::ffff:a00:a01`
|
||||
|
||||
2. 安裝 `go-proxy` [參見這裡](docs/docker.md)
|
||||
|
||||
3. 配置 `go-proxy`
|
||||
- 使用文本編輯器 (推薦 Visual Studio Code [參見 VSCode 使用 schema](#vscode-中使用-json-schema))
|
||||
- 或通過 `http://gp.y.z` 使用網頁配置編輯器
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
### 命令行參數
|
||||
|
||||
| 參數 | 描述 | 示例 |
|
||||
| ----------- | -------------- | -------------------------- |
|
||||
| 空 | 啟動代理服務器 | |
|
||||
| `validate` | 驗證配置並退出 | |
|
||||
| `reload` | 強制刷新配置 | |
|
||||
| `ls-config` | 列出配置並退出 | `go-proxy ls-config \| jq` |
|
||||
| `ls-route` | 列出路由並退出 | `go-proxy ls-route \| jq` |
|
||||
|
||||
**使用 `docker exec <容器名稱> /app/go-proxy <參數>` 運行**
|
||||
|
||||
### 環境變量
|
||||
|
||||
| 環境變量 | 描述 | 默認 | 格式 |
|
||||
| ------------------------------ | ---------------- | ---------------- | ------------- |
|
||||
| `GOPROXY_NO_SCHEMA_VALIDATION` | 禁用 schema 驗證 | `false` | boolean |
|
||||
| `GOPROXY_DEBUG` | 啟用調試輸出 | `false` | boolean |
|
||||
| `GOPROXY_HTTP_ADDR` | http 收聽地址 | `:80` | `[host]:port` |
|
||||
| `GOPROXY_HTTPS_ADDR` | https 收聽地址 | `:443` | `[host]:port` |
|
||||
| `GOPROXY_API_ADDR` | api 收聽地址 | `127.0.0.1:8888` | `[host]:port` |
|
||||
|
||||
### VSCode 中使用 JSON Schema
|
||||
|
||||
複製 [`.vscode/settings.example.json`](.vscode/settings.example.json) 到 `.vscode/settings.json` 並根據需求修改
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
### 配置文件
|
||||
|
||||
參見 [config.example.yml](config.example.yml) 了解更多
|
||||
|
||||
```yaml
|
||||
# autocert 配置
|
||||
autocert:
|
||||
email: # ACME 電子郵件
|
||||
domains: # 域名列表
|
||||
provider: # DNS 供應商
|
||||
options: # 供應商個別配置
|
||||
- ...
|
||||
# 配置文件 / docker
|
||||
providers:
|
||||
include:
|
||||
- providers.yml
|
||||
- other_file_1.yml
|
||||
- ...
|
||||
docker:
|
||||
local: $DOCKER_HOST
|
||||
remote-1: tcp://10.0.2.1:2375
|
||||
remote-2: ssh://root:1234@10.0.2.2
|
||||
```
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
### 透過文件配置
|
||||
|
||||
參見 [Fields](docs/docker.md#fields)
|
||||
|
||||
參見範例 [providers.example.yml](providers.example.yml)
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
## 展示
|
||||
|
||||
### idlesleeper
|
||||
|
||||

|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
## 源碼編譯
|
||||
|
||||
1. 獲取源碼 `git clone https://github.com/yusing/go-proxy --depth=1`
|
||||
|
||||
2. 安裝/升級 [go 版本 (>=1.22)](https://go.dev/doc/install) 和 `make`(如果尚未安裝)
|
||||
|
||||
3. 如果之前編譯過(go 版本 < 1.22),請使用 `go clean -cache` 清除緩存
|
||||
|
||||
4. 使用 `make get` 獲取依賴項
|
||||
|
||||
5. 使用 `make build` 編譯
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
@@ -18,7 +18,7 @@ services:
|
||||
# (Optional) change this to your timezone to get correct log timestamp
|
||||
TZ: ETC/UTC
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./config:/app/config
|
||||
|
||||
# (Optional) choose one of below to enable https
|
||||
|
||||
307
docs/docker.md
307
docs/docker.md
@@ -20,9 +20,9 @@
|
||||
|
||||
1. Install `wget` if not already
|
||||
|
||||
- Ubuntu based: `sudo apt install -y wget`
|
||||
- Fedora based: `sudo yum install -y wget`
|
||||
- Arch based: `sudo pacman -Sy wget`
|
||||
- Ubuntu based: `sudo apt install -y wget`
|
||||
- Fedora based: `sudo yum install -y wget`
|
||||
- Arch based: `sudo pacman -Sy wget`
|
||||
|
||||
2. Run setup script
|
||||
|
||||
@@ -57,20 +57,20 @@
|
||||
|
||||
```yaml
|
||||
autocert:
|
||||
email: john.doe@x.y.z # ACME Email
|
||||
domains: # a list of domains for cert registration
|
||||
- x.y.z
|
||||
provider: cloudflare
|
||||
options:
|
||||
- auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||
email: john.doe@x.y.z # ACME Email
|
||||
domains: # a list of domains for cert registration
|
||||
- x.y.z
|
||||
provider: cloudflare
|
||||
options:
|
||||
- auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||
```
|
||||
|
||||
To use **existing certificate**, set path for cert and key in `config.yml`, e.g.
|
||||
|
||||
```yaml
|
||||
autocert:
|
||||
cert_path: /app/certs/cert.crt
|
||||
key_path: /app/certs/priv.key
|
||||
cert_path: /app/certs/cert.crt
|
||||
key_path: /app/certs/priv.key
|
||||
```
|
||||
|
||||
5. Modify `compose.yml` to fit your needs
|
||||
@@ -85,30 +85,31 @@
|
||||
|
||||
### Syntax
|
||||
|
||||
| Label | Description | Default | Accepted values |
|
||||
| ----------------------- | --------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------- |
|
||||
| `proxy.aliases` | comma separated aliases for subdomain and label matching | `container_name` | any |
|
||||
| `proxy.exclude` | to be excluded from `go-proxy` | false | boolean |
|
||||
| `proxy.idle_timeout` | time for idle (no traffic) before put it into sleep **(http/s only)** | empty **(disabled)** | `number[unit]...`, e.g. `1m30s` |
|
||||
| `proxy.wake_timeout` | time to wait for container to start before responding a loading page | empty | `number[unit]...` |
|
||||
| `proxy.stop_method` | method to stop after `idle_timeout` | `stop` | `stop`, `pause`, `kill` |
|
||||
| `proxy.stop_timeout` | time to wait for stop command | `10s` | `number[unit]...` |
|
||||
| `proxy.stop_signal` | signal sent to container for `stop` and `kill` methods | docker's default | `SIGINT`, `SIGTERM`, `SIGHUP`, `SIGQUIT` and those without **SIG** prefix |
|
||||
| `proxy.<alias>.<field>` | set field for specific alias | N/A | N/A |
|
||||
| `proxy.*.<field>` | set field for all aliases | N/A | N/A |
|
||||
| Label | Description | Example | Default | Accepted values |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | --------------------------- | ------------------------------------------------------------------------- |
|
||||
| `proxy.aliases` | comma separated aliases for subdomain and label matching | `gitlab,gitlab-reg,gitlab-ssh` | `container_name` | any |
|
||||
| `proxy.exclude` | to be excluded from `go-proxy` | | false | boolean |
|
||||
| `proxy.idle_timeout` | time for idle (no traffic) before put it into sleep **(http/s only)**<br> _**NOTE: idlewatcher will only be enabled containers that has non-empty `idle_timeout`**_ | `1h` | empty or `0` **(disabled)** | `number[unit]...`, e.g. `1m30s` |
|
||||
| `proxy.wake_timeout` | time to wait for target site to be ready | | `10s` | `number[unit]...` |
|
||||
| `proxy.stop_method` | method to stop after `idle_timeout` | | `stop` | `stop`, `pause`, `kill` |
|
||||
| `proxy.stop_timeout` | time to wait for stop command | | `10s` | `number[unit]...` |
|
||||
| `proxy.stop_signal` | signal sent to container for `stop` and `kill` methods | | docker's default | `SIGINT`, `SIGTERM`, `SIGHUP`, `SIGQUIT` and those without **SIG** prefix |
|
||||
| `proxy.<alias>.<field>` | set field for specific alias | `proxy.gitlab-ssh.scheme` | N/A | N/A |
|
||||
| `proxy.$<index>.<field>` | set field for specific alias at index (starting from **1**) | `proxy.$3.port` | N/A | N/A |
|
||||
| `proxy.*.<field>` | set field for all aliases | `proxy.*.set_headers` | N/A | N/A |
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Description | Default | Allowed Values / Syntax |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `scheme` | proxy protocol | <ul><li>`http` for numeric port</li><li>`tcp` for `x:y` port</li></ul> | `http`, `https`, `tcp`, `udp` |
|
||||
| `host` | proxy host | <ul><li>Docker: docker client IP / hostname </li><li>File: `localhost`</li></ul> | IP address, hostname |
|
||||
| `port` | proxy port **(http/s)** | first port in `ports:` | number in range of `1 - 65535` |
|
||||
| `port` **(required)** | proxy port **(tcp/udp)** | N/A | `x:y` <br><ul><li>x: port for `go-proxy` to listen on</li><li>y: port or [_service name_](../src/common/constants.go#L55) of target container</li></ul> |
|
||||
| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean |
|
||||
| `path_patterns` | proxy path patterns **(http/s only)**<br> only requests that matched a pattern will be proxied | empty **(proxy all requests)** | yaml style list[<sup>1</sup>](#list-example) of path patterns ([syntax](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)) |
|
||||
| `set_headers` | header to set **(http/s only)** | empty | yaml style key-value mapping[<sup>2</sup>](#key-value-mapping-example) of header-value pairs |
|
||||
| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[<sup>1</sup>](#list-example) of headers |
|
||||
| Field | Description | Default | Allowed Values / Syntax |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `scheme` | proxy protocol | <ul><li>`http` for numeric port</li><li>`tcp` for `x:y` port</li></ul> | `http`, `https`, `tcp`, `udp` |
|
||||
| `host` | proxy host | <ul><li>Docker: docker client IP / hostname </li><li>File: `localhost`</li></ul> | IP address, hostname |
|
||||
| `port` | proxy port **(http/s)** | first port returned from docker | number in range of `1 - 65535` |
|
||||
| `port` **(required)** | proxy port **(tcp/udp)** | N/A | `x:y` <br><ul><li>**x**: port for `go-proxy` to listen on.<br>**x** can be 0, which means listen on a random port</li><li>**y**: port or [_service name_](../src/common/constants.go#L55) of target container</li></ul> |
|
||||
| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean |
|
||||
| `path_patterns` | proxy path patterns **(http/s only)**<br> only requests that matched a pattern will be proxied | empty **(proxy all requests)** | yaml style list[<sup>1</sup>](#list-example) of ([path patterns](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)) |
|
||||
| `set_headers` | header to set **(http/s only)** | empty | yaml style key-value mapping[<sup>2</sup>](#key-value-mapping-example) of header-value pairs |
|
||||
| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[<sup>1</sup>](#list-example) of headers |
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
@@ -133,11 +134,11 @@ File Provider
|
||||
|
||||
```yaml
|
||||
service_a:
|
||||
host: service_a.internal
|
||||
set_headers:
|
||||
# do not duplicate header keys, as it is not allowed in YAML
|
||||
X-Custom-Header1: value1, value2
|
||||
X-Custom-Header2: value3
|
||||
host: service_a.internal
|
||||
set_headers:
|
||||
# do not duplicate header keys, as it is not allowed in YAML
|
||||
X-Custom-Header1: value1, value2
|
||||
X-Custom-Header2: value3
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
@@ -163,150 +164,152 @@ File Provider
|
||||
|
||||
```yaml
|
||||
service_a:
|
||||
host: service_a.internal
|
||||
path_patterns:
|
||||
- GET /
|
||||
- POST /auth
|
||||
hide_headers:
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
host: service_a.internal
|
||||
path_patterns:
|
||||
- GET /
|
||||
- POST /auth
|
||||
hide_headers:
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- 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, e.g.
|
||||
Please check that either `ports` or label `proxy.<alias>.port` is declared, e.g.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
nginx-1: # Option 1
|
||||
...
|
||||
ports:
|
||||
- 80
|
||||
nginx-2: # Option 2
|
||||
...
|
||||
container_name: nginx-2
|
||||
labels:
|
||||
proxy.nginx-2.port: 80
|
||||
```
|
||||
```yaml
|
||||
services:
|
||||
nginx-1: # Option 1
|
||||
...
|
||||
ports:
|
||||
- 80
|
||||
nginx-2: # Option 2
|
||||
...
|
||||
container_name: nginx-2
|
||||
network_mode: host
|
||||
labels:
|
||||
proxy.nginx-2.port: 80
|
||||
```
|
||||
|
||||
- Firewall issues
|
||||
- Firewall issues
|
||||
|
||||
If you are using `ufw` with vpn that drop all inbound traffic except vpn, run below:
|
||||
If you are using `ufw` with vpn that drop all inbound traffic except vpn, run below:
|
||||
|
||||
`sudo ufw allow from 172.16.0.0/16 to 100.64.0.0/10`
|
||||
`sudo ufw allow from 172.16.0.0/16 to 100.64.0.0/10`
|
||||
|
||||
Explaination:
|
||||
Explaination:
|
||||
|
||||
Docker network is usually `172.16.0.0/16`
|
||||
Docker network is usually `172.16.0.0/16`
|
||||
|
||||
Tailscale is used as an example, `100.64.0.0/10` will be the CIDR
|
||||
Tailscale is used as an example, `100.64.0.0/10` will be the CIDR
|
||||
|
||||
You can also list CIDRs of all docker bridge networks by:
|
||||
You can also list CIDRs of all docker bridge networks by:
|
||||
|
||||
`docker network inspect $(docker network ls | awk '$3 == "bridge" { print $1}') | jq -r '.[] | .Name + " " + .IPAM.Config[0].Subnet' -`
|
||||
`docker network inspect $(docker network ls | awk '$3 == "bridge" { print $1}') | jq -r '.[] | .Name + " " + .IPAM.Config[0].Subnet' -`
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Docker compose examples
|
||||
|
||||
More examples in [here](examples/)
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
adg-work:
|
||||
adg-conf:
|
||||
mc-data:
|
||||
palworld:
|
||||
nginx:
|
||||
adg-work:
|
||||
adg-conf:
|
||||
mc-data:
|
||||
palworld:
|
||||
nginx:
|
||||
services:
|
||||
adg:
|
||||
image: adguard/adguardhome
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- proxy.aliases=adg,adg-dns,adg-setup
|
||||
- proxy.adg.port=80
|
||||
- proxy.adg-setup.port=3000
|
||||
- proxy.adg-dns.scheme=udp
|
||||
- proxy.adg-dns.port=20000:dns
|
||||
volumes:
|
||||
- adg-work:/opt/adguardhome/work
|
||||
- adg-conf:/opt/adguardhome/conf
|
||||
ports:
|
||||
- 80
|
||||
- 3000
|
||||
- 53
|
||||
mc:
|
||||
image: itzg/minecraft-server
|
||||
tty: true
|
||||
stdin_open: true
|
||||
container_name: mc
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 25565
|
||||
labels:
|
||||
- proxy.mc.scheme=tcp
|
||||
- proxy.mc.port=20001:25565
|
||||
environment:
|
||||
- EULA=TRUE
|
||||
volumes:
|
||||
- mc-data:/data
|
||||
palworld:
|
||||
image: thijsvanloef/palworld-server-docker:latest
|
||||
restart: unless-stopped
|
||||
container_name: pal
|
||||
stop_grace_period: 30s
|
||||
ports:
|
||||
- 8211
|
||||
- 27015
|
||||
labels:
|
||||
- proxy.aliases=pal1,pal2
|
||||
- proxy.*.scheme=udp
|
||||
- proxy.pal1.port=20002:8211
|
||||
- proxy.pal2.port=20003:27015
|
||||
environment: ...
|
||||
volumes:
|
||||
- palworld:/palworld
|
||||
nginx:
|
||||
image: nginx
|
||||
container_name: nginx
|
||||
volumes:
|
||||
- nginx:/usr/share/nginx/html
|
||||
ports:
|
||||
- 80
|
||||
labels:
|
||||
proxy.idle_timeout: 1m
|
||||
go-proxy:
|
||||
image: ghcr.io/yusing/go-proxy:latest
|
||||
container_name: go-proxy
|
||||
restart: always
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
go-proxy-frontend:
|
||||
image: ghcr.io/yusing/go-proxy-frontend:latest
|
||||
container_name: go-proxy-frontend
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
labels:
|
||||
- proxy.aliases=gp
|
||||
- proxy.gp.port=8888
|
||||
depends_on:
|
||||
- go-proxy
|
||||
adg:
|
||||
image: adguard/adguardhome
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- proxy.aliases=adg,adg-dns,adg-setup
|
||||
- proxy.$1.port=80
|
||||
- proxy.$2.scheme=udp
|
||||
- proxy.$2.port=20000:dns
|
||||
- proxy.$3.port=3000
|
||||
volumes:
|
||||
- adg-work:/opt/adguardhome/work
|
||||
- adg-conf:/opt/adguardhome/conf
|
||||
ports:
|
||||
- 80
|
||||
- 3000
|
||||
- 53/udp
|
||||
mc:
|
||||
image: itzg/minecraft-server
|
||||
tty: true
|
||||
stdin_open: true
|
||||
container_name: mc
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 25565
|
||||
labels:
|
||||
- proxy.mc.port=20001:25565
|
||||
environment:
|
||||
- EULA=TRUE
|
||||
volumes:
|
||||
- mc-data:/data
|
||||
palworld:
|
||||
image: thijsvanloef/palworld-server-docker:latest
|
||||
restart: unless-stopped
|
||||
container_name: pal
|
||||
stop_grace_period: 30s
|
||||
ports:
|
||||
- 8211/udp
|
||||
- 27015/udp
|
||||
labels:
|
||||
- proxy.aliases=pal1,pal2
|
||||
- proxy.*.scheme=udp
|
||||
- proxy.$1.port=20002:8211
|
||||
- proxy.$2.port=20003:27015
|
||||
environment: ...
|
||||
volumes:
|
||||
- palworld:/palworld
|
||||
nginx:
|
||||
image: nginx
|
||||
container_name: nginx
|
||||
volumes:
|
||||
- nginx:/usr/share/nginx/html
|
||||
ports:
|
||||
- 80
|
||||
labels:
|
||||
proxy.idle_timeout: 1m
|
||||
go-proxy:
|
||||
image: ghcr.io/yusing/go-proxy:latest
|
||||
container_name: go-proxy
|
||||
restart: always
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
go-proxy-frontend:
|
||||
image: ghcr.io/yusing/go-proxy-frontend:latest
|
||||
container_name: go-proxy-frontend
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
labels:
|
||||
- proxy.aliases=gp
|
||||
- proxy.gp.port=3000
|
||||
depends_on:
|
||||
- go-proxy
|
||||
```
|
||||
|
||||
[🔼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
|
||||
- `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:2000`: adguard dns (udp)
|
||||
- `yourdomain.com:20001`: minecraft server
|
||||
- `yourdomain.com:20002`: palworld server
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
40
docs/docker_socket_proxy.md
Normal file
40
docs/docker_socket_proxy.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## Docker Socket Proxy
|
||||
|
||||
For docker client on other machine, set this up, then add `name: tcp://<machine_ip>:2375` to `config.yml` under `docker` section
|
||||
|
||||
```yml
|
||||
# compose.yml on remote machine (e.g. server1)
|
||||
docker-proxy:
|
||||
container_name: docker-proxy
|
||||
image: tecnativa/docker-socket-proxy
|
||||
privileged: true
|
||||
environment:
|
||||
- ALLOW_START=1
|
||||
- ALLOW_STOP=1
|
||||
- ALLOW_RESTARTS=1
|
||||
- CONTAINERS=1
|
||||
- EVENTS=1
|
||||
- PING=1
|
||||
- POST=1
|
||||
- VERSION=1
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
restart: always
|
||||
ports:
|
||||
- 2375:2375
|
||||
# or more secure
|
||||
- <machine_ip>:2375:2375
|
||||
```
|
||||
|
||||
```yml
|
||||
# config.yml on go-proxy machine
|
||||
autocert:
|
||||
... # your config
|
||||
|
||||
providers:
|
||||
include:
|
||||
...
|
||||
docker:
|
||||
...
|
||||
server1: tcp://<machine_ip>:2375
|
||||
```
|
||||
16
examples/microbin.yml
Normal file
16
examples/microbin.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
services:
|
||||
app:
|
||||
container_name: microbin
|
||||
cpu_shares: 10
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
env_file: .env
|
||||
image: docker.i.sh/danielszabo99/microbin:latest
|
||||
ports:
|
||||
- 8080
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data:/app/microbin_data
|
||||
# microbin.domain.tld
|
||||
16
examples/siyuan.yml
Normal file
16
examples/siyuan.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
services:
|
||||
main:
|
||||
image: b3log/siyuan:v3.1.0
|
||||
container_name: siyuan
|
||||
command:
|
||||
- --workspace=/siyuan/workspace/
|
||||
- --accessAuthCode=<some password>
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- ./workspace:/siyuan/workspace
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Asia/Hong_Kong
|
||||
ports:
|
||||
- 6806
|
||||
# siyuan.domain.tld
|
||||
2
frontend
2
frontend
Submodule frontend updated: d0e59630d6...441fd708db
@@ -1,22 +1,22 @@
|
||||
example: # matching `app.y.z`
|
||||
scheme: https
|
||||
host: 10.0.0.1
|
||||
port: 80
|
||||
path_patterns: # Check https://pkg.go.dev/net/http#hdr-Patterns-ServeMux for syntax
|
||||
- GET / # accept any GET request
|
||||
- POST /auth # for /auth and /auth/* accept only POST
|
||||
- GET /home/{$}
|
||||
- /b/{bucket}/o/{any}
|
||||
no_tls_verify: false
|
||||
set_headers:
|
||||
HEADER_A: VALUE_A, VALUE_B
|
||||
HEADER_B: VALUE_C
|
||||
hide_headers:
|
||||
- HEADER_C
|
||||
- HEADER_D
|
||||
app1:
|
||||
host: some_host
|
||||
scheme: https
|
||||
host: 10.0.0.1
|
||||
port: 80
|
||||
path_patterns: # Check https://pkg.go.dev/net/http#hdr-Patterns-ServeMux for syntax
|
||||
- GET / # accept any GET request
|
||||
- POST /auth # for /auth and /auth/* accept only POST
|
||||
- GET /home/{$}
|
||||
- /b/{bucket}/o/{any}
|
||||
no_tls_verify: false
|
||||
set_headers:
|
||||
HEADER_A: VALUE_A, VALUE_B
|
||||
HEADER_B: VALUE_C
|
||||
hide_headers:
|
||||
- HEADER_C
|
||||
- HEADER_D
|
||||
app1: # app1 -> localhost:8080
|
||||
port: 8080
|
||||
app2:
|
||||
scheme: tcp
|
||||
host: 10.0.0.2
|
||||
port: 20000:tcp
|
||||
scheme: tcp
|
||||
host: 10.0.0.2
|
||||
port: 20000:tcp
|
||||
|
||||
@@ -75,7 +75,10 @@
|
||||
"scheme": {
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": ["http", "https"]
|
||||
"enum": [
|
||||
"http",
|
||||
"https"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -87,7 +90,7 @@
|
||||
"then": {
|
||||
"properties": {
|
||||
"port": {
|
||||
"markdownDescription": "Proxy port from **1** to **65535**",
|
||||
"markdownDescription": "Proxy port from **0** to **65535**",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -96,7 +99,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"maximum": 65535
|
||||
}
|
||||
]
|
||||
@@ -160,7 +163,9 @@
|
||||
"not": true
|
||||
}
|
||||
},
|
||||
"required": ["port"]
|
||||
"required": [
|
||||
"port"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -192,4 +197,4 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
BIN
showcase/idlesleeper.webp
Normal file
BIN
showcase/idlesleeper.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 831 KiB |
@@ -36,7 +36,7 @@ func IsStreamHealthy(scheme, address string) bool {
|
||||
}
|
||||
|
||||
func ReloadServer() E.NestedError {
|
||||
resp, err := HttpClient.Post(fmt.Sprintf("http://localhost%v/reload", common.APIHTTPPort), "", nil)
|
||||
resp, err := HttpClient.Post(fmt.Sprintf("http://localhost%v/reload", common.APIHTTPAddr), "", nil)
|
||||
if err != nil {
|
||||
return E.From(err)
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func (p *Provider) ObtainCert() (res E.NestedError) {
|
||||
defer b.To(&res)
|
||||
|
||||
if p.cfg.Provider == ProviderLocal {
|
||||
b.Addf("provider is set to %q", ProviderLocal)
|
||||
b.Addf("provider is set to %q", ProviderLocal).WithSeverity(E.SeverityWarning)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
29
src/autocert/setup.go
Normal file
29
src/autocert/setup.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
func (p *Provider) Setup(ctx context.Context) (err E.NestedError) {
|
||||
if err = p.LoadCert(); err != nil {
|
||||
if !err.Is(os.ErrNotExist) { // ignore if cert doesn't exist
|
||||
return err
|
||||
}
|
||||
logger.Debug("obtaining cert due to error loading cert")
|
||||
if err = p.ObtainCert(); err != nil {
|
||||
return err.Warn()
|
||||
}
|
||||
}
|
||||
|
||||
go p.ScheduleRenewal(ctx)
|
||||
|
||||
for _, expiry := range p.GetExpiries() {
|
||||
logger.Infof("certificate expire on %s", expiry)
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -12,11 +12,13 @@ type Args struct {
|
||||
}
|
||||
|
||||
const (
|
||||
CommandStart = ""
|
||||
CommandValidate = "validate"
|
||||
CommandListConfigs = "ls-config"
|
||||
CommandListRoutes = "ls-routes"
|
||||
CommandReload = "reload"
|
||||
CommandStart = ""
|
||||
CommandValidate = "validate"
|
||||
CommandListConfigs = "ls-config"
|
||||
CommandListRoutes = "ls-routes"
|
||||
CommandReload = "reload"
|
||||
CommandDebugListEntries = "debug-ls-entries"
|
||||
CommandDebugListProviders = "debug-ls-providers"
|
||||
)
|
||||
|
||||
var ValidCommands = []string{
|
||||
@@ -25,6 +27,8 @@ var ValidCommands = []string{
|
||||
CommandListConfigs,
|
||||
CommandListRoutes,
|
||||
CommandReload,
|
||||
CommandDebugListEntries,
|
||||
CommandDebugListProviders,
|
||||
}
|
||||
|
||||
func GetArgs() Args {
|
||||
|
||||
@@ -30,65 +30,13 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
SchemaBasePath = "schema/"
|
||||
ConfigSchemaPath = SchemaBasePath + "config.schema.json"
|
||||
ProvidersSchemaPath = SchemaBasePath + "providers.schema.json"
|
||||
SchemaBasePath = "schema/"
|
||||
ConfigSchemaPath = SchemaBasePath + "config.schema.json"
|
||||
FileProviderSchemaPath = SchemaBasePath + "providers.schema.json"
|
||||
)
|
||||
|
||||
const DockerHostFromEnv = "$DOCKER_HOST"
|
||||
|
||||
const (
|
||||
ProxyHTTPPort = ":80"
|
||||
ProxyHTTPSPort = ":443"
|
||||
APIHTTPPort = ":8888"
|
||||
)
|
||||
|
||||
var WellKnownHTTPPorts = map[uint16]bool{
|
||||
80: true,
|
||||
8000: true,
|
||||
8008: true,
|
||||
8080: true,
|
||||
3000: true,
|
||||
}
|
||||
|
||||
var (
|
||||
ServiceNamePortMapTCP = map[string]int{
|
||||
"postgres": 5432,
|
||||
"mysql": 3306,
|
||||
"mariadb": 3306,
|
||||
"redis": 6379,
|
||||
"mssql": 1433,
|
||||
"memcached": 11211,
|
||||
"rabbitmq": 5672,
|
||||
"mongo": 27017,
|
||||
|
||||
"dns": 53,
|
||||
"ssh": 22,
|
||||
"ftp": 21,
|
||||
"smtp": 25,
|
||||
"pop3": 110,
|
||||
"imap": 143,
|
||||
}
|
||||
)
|
||||
|
||||
var ImageNamePortMapHTTP = map[string]int{
|
||||
"nginx": 80,
|
||||
"httpd": 80,
|
||||
"adguardhome": 3000,
|
||||
"gogs": 3000,
|
||||
"gitea": 3000,
|
||||
"portainer": 9000,
|
||||
"portainer-ce": 9000,
|
||||
"home-assistant": 8123,
|
||||
"homebridge": 8581,
|
||||
"uptime-kuma": 3001,
|
||||
"changedetection.io": 3000,
|
||||
"prometheus": 9090,
|
||||
"grafana": 3000,
|
||||
"dockge": 5001,
|
||||
"nginx-proxy-manager": 81,
|
||||
}
|
||||
|
||||
const (
|
||||
IdleTimeoutDefault = "0"
|
||||
WakeTimeoutDefault = "10s"
|
||||
|
||||
@@ -6,9 +6,22 @@ import (
|
||||
U "github.com/yusing/go-proxy/utils"
|
||||
)
|
||||
|
||||
var NoSchemaValidation = getEnvBool("GOPROXY_NO_SCHEMA_VALIDATION")
|
||||
var IsDebug = getEnvBool("GOPROXY_DEBUG")
|
||||
var (
|
||||
NoSchemaValidation = getEnvBool("GOPROXY_NO_SCHEMA_VALIDATION")
|
||||
IsDebug = getEnvBool("GOPROXY_DEBUG")
|
||||
ProxyHTTPAddr = getEnv("GOPROXY_HTTP_ADDR", ":80")
|
||||
ProxyHTTPSAddr = getEnv("GOPROXY_HTTPS_ADDR", ":443")
|
||||
APIHTTPAddr = getEnv("GOPROXY_API_ADDR", "127.0.0.1:8888")
|
||||
)
|
||||
|
||||
func getEnvBool(key string) bool {
|
||||
return U.ParseBool(os.Getenv(key))
|
||||
}
|
||||
|
||||
func getEnv(key string, defaultValue string) string {
|
||||
value, ok := os.LookupEnv(key)
|
||||
if !ok {
|
||||
value = defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
74
src/common/ports.go
Normal file
74
src/common/ports.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package common
|
||||
|
||||
var (
|
||||
WellKnownHTTPPorts = map[string]bool{
|
||||
"80": true,
|
||||
"8000": true,
|
||||
"8008": true,
|
||||
"8080": true,
|
||||
"3000": true,
|
||||
}
|
||||
|
||||
ServiceNamePortMapTCP = map[string]int{
|
||||
"mssql": 1433,
|
||||
"mysql": 3306,
|
||||
"mariadb": 3306,
|
||||
"postgres": 5432,
|
||||
"rabbitmq": 5672,
|
||||
"redis": 6379,
|
||||
"memcached": 11211,
|
||||
"mongo": 27017,
|
||||
|
||||
"ssh": 22,
|
||||
"ftp": 21,
|
||||
"smtp": 25,
|
||||
"dns": 53,
|
||||
"pop3": 110,
|
||||
"imap": 143,
|
||||
}
|
||||
|
||||
ImageNamePortMap = func() (m map[string]int) {
|
||||
m = make(map[string]int, len(ServiceNamePortMapTCP)+len(imageNamePortMap))
|
||||
for k, v := range ServiceNamePortMapTCP {
|
||||
m[k] = v
|
||||
}
|
||||
for k, v := range imageNamePortMap {
|
||||
m[k] = v
|
||||
}
|
||||
return
|
||||
}()
|
||||
|
||||
imageNamePortMap = map[string]int{
|
||||
"adguardhome": 3000,
|
||||
"bazarr": 6767,
|
||||
"calibre-web": 8083,
|
||||
"changedetection.io": 3000,
|
||||
"dockge": 5001,
|
||||
"gitea": 3000,
|
||||
"gogs": 3000,
|
||||
"grafana": 3000,
|
||||
"home-assistant": 8123,
|
||||
"homebridge": 8581,
|
||||
"httpd": 80,
|
||||
"immich": 3001,
|
||||
"jellyfin": 8096,
|
||||
"lidarr": 8686,
|
||||
"minecraft-server": 25565,
|
||||
"nginx": 80,
|
||||
"nginx-proxy-manager": 81,
|
||||
"open-webui": 8080,
|
||||
"plex": 32400,
|
||||
"portainer": 9000,
|
||||
"portainer-ce": 9000,
|
||||
"prometheus": 9090,
|
||||
"prowlarr": 9696,
|
||||
"radarr": 7878,
|
||||
"radarr-sma": 7878,
|
||||
"rsshub": 1200,
|
||||
"rss-bridge": 80,
|
||||
"sonarr": 8989,
|
||||
"sonarr-sma": 8989,
|
||||
"uptime-kuma": 3001,
|
||||
"whisparr": 6969,
|
||||
}
|
||||
)
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
U "github.com/yusing/go-proxy/utils"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
W "github.com/yusing/go-proxy/watcher"
|
||||
"github.com/yusing/go-proxy/watcher/events"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -52,11 +53,6 @@ func (cfg *Config) GetAutoCertProvider() *autocert.Provider {
|
||||
return cfg.autocertProvider
|
||||
}
|
||||
|
||||
func (cfg *Config) StartProxyProviders() {
|
||||
cfg.startProviders()
|
||||
cfg.watchChanges()
|
||||
}
|
||||
|
||||
func (cfg *Config) Dispose() {
|
||||
if cfg.watcherCancel != nil {
|
||||
cfg.watcherCancel()
|
||||
@@ -70,76 +66,15 @@ func (cfg *Config) Reload() E.NestedError {
|
||||
if err := cfg.load(); err.HasError() {
|
||||
return err
|
||||
}
|
||||
cfg.startProviders()
|
||||
cfg.StartProxyProviders()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) FindRoute(alias string) R.Route {
|
||||
return F.MapFind(cfg.proxyProviders,
|
||||
func(p *PR.Provider) (R.Route, bool) {
|
||||
if route, ok := p.GetRoute(alias); ok {
|
||||
return route, true
|
||||
}
|
||||
return nil, false
|
||||
},
|
||||
)
|
||||
func (cfg *Config) StartProxyProviders() {
|
||||
cfg.controlProviders("start", (*PR.Provider).StartAllRoutes)
|
||||
}
|
||||
|
||||
func (cfg *Config) RoutesByAlias() map[string]U.SerializedObject {
|
||||
routes := make(map[string]U.SerializedObject)
|
||||
cfg.forEachRoute(func(alias string, r R.Route, p *PR.Provider) {
|
||||
obj, err := U.Serialize(r)
|
||||
if err.HasError() {
|
||||
cfg.l.Error(err)
|
||||
return
|
||||
}
|
||||
obj["provider"] = p.GetName()
|
||||
obj["type"] = string(r.Type())
|
||||
routes[alias] = obj
|
||||
})
|
||||
return routes
|
||||
}
|
||||
|
||||
func (cfg *Config) Statistics() map[string]any {
|
||||
nTotalStreams := 0
|
||||
nTotalRPs := 0
|
||||
providerStats := make(map[string]any)
|
||||
|
||||
cfg.forEachRoute(func(alias string, r R.Route, p *PR.Provider) {
|
||||
s, ok := providerStats[p.GetName()]
|
||||
if !ok {
|
||||
s = make(map[string]int)
|
||||
}
|
||||
|
||||
stats := s.(map[string]int)
|
||||
switch r.Type() {
|
||||
case R.RouteTypeStream:
|
||||
stats["num_streams"]++
|
||||
nTotalStreams++
|
||||
case R.RouteTypeReverseProxy:
|
||||
stats["num_reverse_proxies"]++
|
||||
nTotalRPs++
|
||||
default:
|
||||
panic("bug: should not reach here")
|
||||
}
|
||||
})
|
||||
|
||||
return map[string]any{
|
||||
"num_total_streams": nTotalStreams,
|
||||
"num_total_reverse_proxies": nTotalRPs,
|
||||
"providers": providerStats,
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) forEachRoute(do func(alias string, r R.Route, p *PR.Provider)) {
|
||||
cfg.proxyProviders.RangeAll(func(_ string, p *PR.Provider) {
|
||||
p.RangeRoutes(func(a string, r R.Route) {
|
||||
do(a, r, p)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (cfg *Config) watchChanges() {
|
||||
func (cfg *Config) WatchChanges() {
|
||||
cfg.watcherCtx, cfg.watcherCancel = context.WithCancel(context.Background())
|
||||
go func() {
|
||||
for {
|
||||
@@ -160,7 +95,7 @@ func (cfg *Config) watchChanges() {
|
||||
case <-cfg.watcherCtx.Done():
|
||||
return
|
||||
case event := <-eventCh:
|
||||
if event.Action.IsDelete() {
|
||||
if event.Action == events.ActionFileDeleted {
|
||||
cfg.stopProviders()
|
||||
} else {
|
||||
cfg.reloadReq <- struct{}{}
|
||||
@@ -173,6 +108,14 @@ func (cfg *Config) watchChanges() {
|
||||
}()
|
||||
}
|
||||
|
||||
func (cfg *Config) forEachRoute(do func(alias string, r R.Route, p *PR.Provider)) {
|
||||
cfg.proxyProviders.RangeAll(func(_ string, p *PR.Provider) {
|
||||
p.RangeRoutes(func(a string, r R.Route) {
|
||||
do(a, r, p)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (cfg *Config) load() (res E.NestedError) {
|
||||
b := E.NewBuilder("errors loading config")
|
||||
defer b.To(&res)
|
||||
@@ -231,20 +174,28 @@ func (cfg *Config) loadProviders(providers *M.ProxyProviders) (res E.NestedError
|
||||
defer b.To(&res)
|
||||
|
||||
for _, filename := range providers.Files {
|
||||
p := PR.NewFileProvider(filename)
|
||||
p, err := PR.NewFileProvider(filename)
|
||||
if err != nil {
|
||||
b.Add(err.Subject(filename))
|
||||
continue
|
||||
}
|
||||
cfg.proxyProviders.Store(p.GetName(), p)
|
||||
b.Add(p.LoadRoutes())
|
||||
b.Add(p.LoadRoutes().Subject(filename))
|
||||
}
|
||||
for name, dockerHost := range providers.Docker {
|
||||
p := PR.NewDockerProvider(name, dockerHost)
|
||||
p, err := PR.NewDockerProvider(name, dockerHost)
|
||||
if err != nil {
|
||||
b.Add(err.Subjectf("%s (%s)", name, dockerHost))
|
||||
continue
|
||||
}
|
||||
cfg.proxyProviders.Store(p.GetName(), p)
|
||||
b.Add(p.LoadRoutes())
|
||||
b.Add(p.LoadRoutes().Subject(dockerHost))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (cfg *Config) controlProviders(action string, do func(*PR.Provider) E.NestedError) {
|
||||
errors := E.NewBuilder("cannot %s these providers", action)
|
||||
errors := E.NewBuilder("errors in %s these providers", action)
|
||||
|
||||
cfg.proxyProviders.RangeAll(func(name string, p *PR.Provider) {
|
||||
if err := do(p); err.HasError() {
|
||||
@@ -257,10 +208,6 @@ func (cfg *Config) controlProviders(action string, do func(*PR.Provider) E.Neste
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) startProviders() {
|
||||
cfg.controlProviders("start", (*PR.Provider).StartAllRoutes)
|
||||
}
|
||||
|
||||
func (cfg *Config) stopProviders() {
|
||||
cfg.controlProviders("stop routes", (*PR.Provider).StopAllRoutes)
|
||||
}
|
||||
|
||||
82
src/config/query.go
Normal file
82
src/config/query.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
M "github.com/yusing/go-proxy/models"
|
||||
PR "github.com/yusing/go-proxy/proxy/provider"
|
||||
R "github.com/yusing/go-proxy/route"
|
||||
U "github.com/yusing/go-proxy/utils"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
)
|
||||
|
||||
func (cfg *Config) DumpEntries() map[string]*M.RawEntry {
|
||||
entries := make(map[string]*M.RawEntry)
|
||||
cfg.forEachRoute(func(alias string, r R.Route, p *PR.Provider) {
|
||||
entries[alias] = r.Entry()
|
||||
})
|
||||
return entries
|
||||
}
|
||||
|
||||
func (cfg *Config) DumpProviders() map[string]*PR.Provider {
|
||||
entries := make(map[string]*PR.Provider)
|
||||
cfg.proxyProviders.RangeAll(func(name string, p *PR.Provider) {
|
||||
entries[name] = p
|
||||
})
|
||||
return entries
|
||||
}
|
||||
|
||||
func (cfg *Config) RoutesByAlias() map[string]U.SerializedObject {
|
||||
routes := make(map[string]U.SerializedObject)
|
||||
cfg.forEachRoute(func(alias string, r R.Route, p *PR.Provider) {
|
||||
obj, err := U.Serialize(r)
|
||||
if err.HasError() {
|
||||
cfg.l.Error(err)
|
||||
return
|
||||
}
|
||||
obj["provider"] = p.GetName()
|
||||
obj["type"] = string(r.Type())
|
||||
routes[alias] = obj
|
||||
})
|
||||
return routes
|
||||
}
|
||||
|
||||
func (cfg *Config) Statistics() map[string]any {
|
||||
nTotalStreams := 0
|
||||
nTotalRPs := 0
|
||||
providerStats := make(map[string]any)
|
||||
|
||||
cfg.forEachRoute(func(alias string, r R.Route, p *PR.Provider) {
|
||||
s, ok := providerStats[p.GetName()]
|
||||
if !ok {
|
||||
s = make(map[string]int)
|
||||
}
|
||||
|
||||
stats := s.(map[string]int)
|
||||
switch r.Type() {
|
||||
case R.RouteTypeStream:
|
||||
stats["num_streams"]++
|
||||
nTotalStreams++
|
||||
case R.RouteTypeReverseProxy:
|
||||
stats["num_reverse_proxies"]++
|
||||
nTotalRPs++
|
||||
default:
|
||||
panic("bug: should not reach here")
|
||||
}
|
||||
})
|
||||
|
||||
return map[string]any{
|
||||
"num_total_streams": nTotalStreams,
|
||||
"num_total_reverse_proxies": nTotalRPs,
|
||||
"providers": providerStats,
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) FindRoute(alias string) R.Route {
|
||||
return F.MapFind(cfg.proxyProviders,
|
||||
func(p *PR.Provider) (R.Route, bool) {
|
||||
if route, ok := p.GetRoute(alias); ok {
|
||||
return route, true
|
||||
}
|
||||
return nil, false
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -10,31 +10,56 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/common"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
key string
|
||||
refCount *atomic.Int32
|
||||
*client.Client
|
||||
|
||||
l logrus.FieldLogger
|
||||
}
|
||||
|
||||
func ParseDockerHostname(host string) (string, E.NestedError) {
|
||||
switch host {
|
||||
case common.DockerHostFromEnv, "":
|
||||
return "localhost", nil
|
||||
}
|
||||
url, err := E.Check(client.ParseHostURL(host))
|
||||
if err != nil {
|
||||
return "", E.Invalid("host", host).With(err)
|
||||
}
|
||||
return url.Hostname(), nil
|
||||
}
|
||||
|
||||
func (c Client) DaemonHostname() string {
|
||||
url, _ := client.ParseHostURL(c.DaemonHost())
|
||||
return url.Hostname()
|
||||
// DaemonHost should always return a valid host
|
||||
hostname, _ := ParseDockerHostname(c.DaemonHost())
|
||||
return hostname
|
||||
}
|
||||
|
||||
func (c Client) Connected() bool {
|
||||
return c.Client != nil
|
||||
}
|
||||
|
||||
// if the client is still referenced, this is no-op
|
||||
func (c Client) Close() error {
|
||||
if c.refCount.Load() > 0 {
|
||||
c.refCount.Add(-1)
|
||||
func (c *Client) Close() error {
|
||||
if c.refCount.Add(-1) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
clientMapMu.Lock()
|
||||
defer clientMapMu.Unlock()
|
||||
delete(clientMap, c.key)
|
||||
clientMap.Delete(c.key)
|
||||
|
||||
return c.Client.Close()
|
||||
client := c.Client
|
||||
c.Client = nil
|
||||
|
||||
c.l.Debugf("client closed")
|
||||
|
||||
if client != nil {
|
||||
return client.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConnectClient creates a new Docker client connection to the specified host.
|
||||
@@ -52,7 +77,7 @@ func ConnectClient(host string) (Client, E.NestedError) {
|
||||
defer clientMapMu.Unlock()
|
||||
|
||||
// check if client exists
|
||||
if client, ok := clientMap[host]; ok {
|
||||
if client, ok := clientMap.Load(host); ok {
|
||||
client.refCount.Add(1)
|
||||
return client, nil
|
||||
}
|
||||
@@ -94,28 +119,31 @@ func ConnectClient(host string) (Client, E.NestedError) {
|
||||
return Client{}, err
|
||||
}
|
||||
|
||||
clientMap[host] = Client{
|
||||
c := Client{
|
||||
Client: client,
|
||||
key: host,
|
||||
refCount: &atomic.Int32{},
|
||||
l: logger.WithField("docker_client", client.DaemonHost()),
|
||||
}
|
||||
clientMap[host].refCount.Add(1)
|
||||
return clientMap[host], nil
|
||||
c.refCount.Add(1)
|
||||
c.l.Debugf("client connected")
|
||||
|
||||
clientMap.Store(host, c)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func CloseAllClients() {
|
||||
clientMapMu.Lock()
|
||||
defer clientMapMu.Unlock()
|
||||
for _, client := range clientMap {
|
||||
client.Close()
|
||||
}
|
||||
clientMap = make(map[string]Client)
|
||||
clientMap.RangeAll(func(_ string, c Client) {
|
||||
c.Client.Close()
|
||||
})
|
||||
clientMap.Clear()
|
||||
logger.Debug("closed all clients")
|
||||
}
|
||||
|
||||
var (
|
||||
clientMap map[string]Client = make(map[string]Client)
|
||||
clientMapMu sync.Mutex
|
||||
clientMap F.Map[string, Client] = F.NewMapOf[string, Client]()
|
||||
clientMapMu sync.Mutex
|
||||
|
||||
clientOptEnvHost = []client.Opt{
|
||||
client.WithHostFromEnv(),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
|
||||
@@ -10,17 +10,21 @@ import (
|
||||
)
|
||||
|
||||
type ProxyProperties struct {
|
||||
DockerHost string `yaml:"docker_host" json:"docker_host"`
|
||||
ContainerName string `yaml:"container_name" json:"container_name"`
|
||||
ImageName string `yaml:"image_name" json:"image_name"`
|
||||
Aliases []string `yaml:"aliases" json:"aliases"`
|
||||
IsExcluded bool `yaml:"is_excluded" json:"is_excluded"`
|
||||
FirstPort string `yaml:"first_port" json:"first_port"`
|
||||
IdleTimeout string `yaml:"idle_timeout" json:"idle_timeout"`
|
||||
WakeTimeout string `yaml:"wake_timeout" json:"wake_timeout"`
|
||||
StopMethod string `yaml:"stop_method" json:"stop_method"`
|
||||
StopTimeout string `yaml:"stop_timeout" json:"stop_timeout"` // stop_method = "stop" only
|
||||
StopSignal string `yaml:"stop_signal" json:"stop_signal"` // stop_method = "stop" | "kill" only
|
||||
DockerHost string `yaml:"-" json:"docker_host"`
|
||||
ContainerName string `yaml:"-" json:"container_name"`
|
||||
ImageName string `yaml:"-" json:"image_name"`
|
||||
PublicPortMapping PortMapping `yaml:"-" json:"public_port_mapping"` // non-zero publicPort:types.Port
|
||||
PrivatePortMapping PortMapping `yaml:"-" json:"private_port_mapping"` // privatePort:types.Port
|
||||
NetworkMode string `yaml:"-" json:"network_mode"`
|
||||
|
||||
Aliases []string `yaml:"-" json:"aliases"`
|
||||
IsExcluded bool `yaml:"-" json:"is_excluded"`
|
||||
IdleTimeout string `yaml:"-" json:"idle_timeout"`
|
||||
WakeTimeout string `yaml:"-" json:"wake_timeout"`
|
||||
StopMethod string `yaml:"-" json:"stop_method"`
|
||||
StopTimeout string `yaml:"-" json:"stop_timeout"` // stop_method = "stop" only
|
||||
StopSignal string `yaml:"-" json:"stop_signal"` // stop_method = "stop" | "kill" only
|
||||
Running bool `yaml:"-" json:"running"`
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
@@ -28,20 +32,25 @@ type Container struct {
|
||||
*ProxyProperties
|
||||
}
|
||||
|
||||
type PortMapping = map[string]types.Port
|
||||
|
||||
func FromDocker(c *types.Container, dockerHost string) (res Container) {
|
||||
res.Container = c
|
||||
res.ProxyProperties = &ProxyProperties{
|
||||
DockerHost: dockerHost,
|
||||
ContainerName: res.getName(),
|
||||
ImageName: res.getImageName(),
|
||||
Aliases: res.getAliases(),
|
||||
IsExcluded: U.ParseBool(res.getDeleteLabel(LableExclude)),
|
||||
FirstPort: res.firstPortOrEmpty(),
|
||||
IdleTimeout: res.getDeleteLabel(LabelIdleTimeout),
|
||||
WakeTimeout: res.getDeleteLabel(LabelWakeTimeout),
|
||||
StopMethod: res.getDeleteLabel(LabelStopMethod),
|
||||
StopTimeout: res.getDeleteLabel(LabelStopTimeout),
|
||||
StopSignal: res.getDeleteLabel(LabelStopSignal),
|
||||
DockerHost: dockerHost,
|
||||
ContainerName: res.getName(),
|
||||
ImageName: res.getImageName(),
|
||||
PublicPortMapping: res.getPublicPortMapping(),
|
||||
PrivatePortMapping: res.getPrivatePortMapping(),
|
||||
NetworkMode: c.HostConfig.NetworkMode,
|
||||
Aliases: res.getAliases(),
|
||||
IsExcluded: U.ParseBool(res.getDeleteLabel(LabelExclude)),
|
||||
IdleTimeout: res.getDeleteLabel(LabelIdleTimeout),
|
||||
WakeTimeout: res.getDeleteLabel(LabelWakeTimeout),
|
||||
StopMethod: res.getDeleteLabel(LabelStopMethod),
|
||||
StopTimeout: res.getDeleteLabel(LabelStopTimeout),
|
||||
StopSignal: res.getDeleteLabel(LabelStopSignal),
|
||||
Running: c.Status == "running" || c.State == "running",
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -79,7 +88,7 @@ func (c Container) getDeleteLabel(label string) string {
|
||||
}
|
||||
|
||||
func (c Container) getAliases() []string {
|
||||
if l := c.getDeleteLabel(LableAliases); l != "" {
|
||||
if l := c.getDeleteLabel(LabelAliases); l != "" {
|
||||
return U.CommaSeperatedList(l)
|
||||
} else {
|
||||
return []string{c.getName()}
|
||||
@@ -92,18 +101,28 @@ func (c Container) getName() string {
|
||||
|
||||
func (c Container) getImageName() string {
|
||||
colonSep := strings.Split(c.Image, ":")
|
||||
slashSep := strings.Split(colonSep[len(colonSep)-1], "/")
|
||||
slashSep := strings.Split(colonSep[0], "/")
|
||||
return slashSep[len(slashSep)-1]
|
||||
}
|
||||
|
||||
func (c Container) firstPortOrEmpty() string {
|
||||
if len(c.Ports) == 0 {
|
||||
return ""
|
||||
}
|
||||
for _, p := range c.Ports {
|
||||
if p.PublicPort != 0 {
|
||||
return fmt.Sprint(p.PublicPort)
|
||||
func (c Container) getPublicPortMapping() PortMapping {
|
||||
res := make(PortMapping)
|
||||
for _, v := range c.Ports {
|
||||
if v.PublicPort == 0 {
|
||||
continue
|
||||
}
|
||||
res[fmt.Sprint(v.PublicPort)] = v
|
||||
}
|
||||
return ""
|
||||
return res
|
||||
}
|
||||
|
||||
func (c Container) getPrivatePortMapping() PortMapping {
|
||||
res := make(PortMapping)
|
||||
for _, v := range c.Ports {
|
||||
if v.PublicPort == 0 {
|
||||
continue
|
||||
}
|
||||
res[fmt.Sprint(v.PrivatePort)] = v
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
87
src/docker/idlewatcher/html/loading_page.html
Normal file
87
src/docker/idlewatcher/html/loading_page.html
Normal file
@@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{.Title}}</title>
|
||||
<style>
|
||||
/* Global Styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
font-family: Inter, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: #fff;
|
||||
background-color: #212121;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Spinner Styles */
|
||||
.spinner {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 16px solid #333;
|
||||
border-radius: 50%;
|
||||
border-top: 16px solid #66d9ef;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Error Styles */
|
||||
.error {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.error::before {
|
||||
content: "\26A0"; /* Unicode for warning symbol */
|
||||
font-size: 40px;
|
||||
color: #ff9900;
|
||||
}
|
||||
|
||||
/* Message Styles */
|
||||
.message {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
padding-left: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.onload = async function () {
|
||||
let result = await fetch(window.location.href, {
|
||||
headers: {
|
||||
{{ range $key, $value := .RequestHeaders }}
|
||||
'{{ $key }}' : {{ $value }}
|
||||
{{ end }}
|
||||
},
|
||||
}).then((resp) => resp.text())
|
||||
.catch((err) => {
|
||||
document.getElementById("message").innerText = err;
|
||||
});
|
||||
if (result) {
|
||||
document.documentElement.innerHTML = result
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<div class="{{.SpinnerClass}}"></div>
|
||||
<div class="message">{{.Message}}</div>
|
||||
</body>
|
||||
</html>
|
||||
93
src/docker/idlewatcher/http.go
Normal file
93
src/docker/idlewatcher/http.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type templateData struct {
|
||||
Title string
|
||||
Message string
|
||||
RequestHeaders http.Header
|
||||
SpinnerClass string
|
||||
}
|
||||
|
||||
//go:embed html/loading_page.html
|
||||
var loadingPage []byte
|
||||
var loadingPageTmpl = func() *template.Template {
|
||||
tmpl, err := template.New("loading").Parse(string(loadingPage))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return tmpl
|
||||
}()
|
||||
|
||||
const (
|
||||
htmlContentType = "text/html; charset=utf-8"
|
||||
|
||||
errPrefix = "\u1000"
|
||||
|
||||
headerGoProxyTargetURL = "X-GoProxy-Target"
|
||||
headerContentType = "Content-Type"
|
||||
|
||||
spinnerClassSpinner = "spinner"
|
||||
spinnerClassErrorSign = "error"
|
||||
)
|
||||
|
||||
func (w *watcher) makeSuccResp(redirectURL string, resp *http.Response) (*http.Response, error) {
|
||||
h := make(http.Header)
|
||||
h.Set("Location", redirectURL)
|
||||
h.Set("Content-Length", "0")
|
||||
h.Set(headerContentType, htmlContentType)
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusTemporaryRedirect,
|
||||
Header: h,
|
||||
Body: http.NoBody,
|
||||
TLS: resp.TLS,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *watcher) makeErrResp(errFmt string, args ...any) (*http.Response, error) {
|
||||
return w.makeResp(errPrefix+errFmt, args...)
|
||||
}
|
||||
|
||||
func (w *watcher) makeResp(format string, args ...any) (*http.Response, error) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
|
||||
data := new(templateData)
|
||||
data.Title = w.ContainerName
|
||||
data.Message = strings.ReplaceAll(msg, "\n", "<br>")
|
||||
data.Message = strings.ReplaceAll(data.Message, " ", " ")
|
||||
data.RequestHeaders = make(http.Header)
|
||||
data.RequestHeaders.Add(headerGoProxyTargetURL, "window.location.href")
|
||||
if strings.HasPrefix(data.Message, errPrefix) {
|
||||
data.Message = strings.TrimLeft(data.Message, errPrefix)
|
||||
data.SpinnerClass = spinnerClassErrorSign
|
||||
} else {
|
||||
data.SpinnerClass = spinnerClassSpinner
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(make([]byte, 128)) // more than enough
|
||||
err := loadingPageTmpl.Execute(buf, data)
|
||||
if err != nil { // should never happen
|
||||
panic(err)
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
Header: http.Header{
|
||||
headerContentType: {htmlContentType},
|
||||
"Cache-Control": {
|
||||
"no-cache",
|
||||
"no-store",
|
||||
"must-revalidate",
|
||||
},
|
||||
},
|
||||
Body: io.NopCloser(buf),
|
||||
ContentLength: int64(buf.Len()),
|
||||
}, nil
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package idlewatcher
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
roundTripper struct {
|
||||
@@ -12,3 +16,63 @@ type (
|
||||
func (rt roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return rt.patched(req)
|
||||
}
|
||||
|
||||
func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*http.Response, error) {
|
||||
// target site is ready, passthrough
|
||||
if w.ready.Load() {
|
||||
return origRoundTrip(req)
|
||||
}
|
||||
|
||||
// wake the container
|
||||
w.wakeCh <- struct{}{}
|
||||
|
||||
// initial request
|
||||
targetUrl := req.Header.Get(headerGoProxyTargetURL)
|
||||
if targetUrl == "" {
|
||||
return w.makeResp(
|
||||
"%s is starting... Please wait",
|
||||
w.ContainerName,
|
||||
)
|
||||
}
|
||||
|
||||
w.l.Debug("serving event")
|
||||
|
||||
// stream request
|
||||
rtDone := make(chan *http.Response, 1)
|
||||
ctx, cancel := context.WithTimeout(req.Context(), w.WakeTimeout)
|
||||
defer cancel()
|
||||
|
||||
// loop original round trip until success in a goroutine
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
default:
|
||||
resp, err := origRoundTrip(req)
|
||||
if err == nil {
|
||||
w.ready.Store(true)
|
||||
rtDone <- resp
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case resp := <-rtDone:
|
||||
return w.makeSuccResp(targetUrl, resp)
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return w.makeErrResp("Timed out waiting for %s to fully wake", w.ContainerName)
|
||||
}
|
||||
return w.makeErrResp("idlewatcher has stopped\n%s", w.ctx.Err().Error())
|
||||
case <-w.ctx.Done():
|
||||
return w.makeErrResp("idlewatcher has stopped\n%s", w.ctx.Err().Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -15,28 +13,44 @@ import (
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
P "github.com/yusing/go-proxy/proxy"
|
||||
PT "github.com/yusing/go-proxy/proxy/fields"
|
||||
W "github.com/yusing/go-proxy/watcher"
|
||||
)
|
||||
|
||||
type watcher struct {
|
||||
*P.ReverseProxyEntry
|
||||
client D.Client
|
||||
|
||||
refCount atomic.Int32
|
||||
|
||||
stopByMethod StopCallback
|
||||
wakeCh chan struct{}
|
||||
wakeDone chan E.NestedError
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
l logrus.FieldLogger
|
||||
}
|
||||
|
||||
type (
|
||||
watcher struct {
|
||||
*P.ReverseProxyEntry
|
||||
|
||||
client D.Client
|
||||
|
||||
ready atomic.Bool // whether the site is ready to accept connection
|
||||
stopByMethod StopCallback // send a docker command w.r.t. `stop_method`
|
||||
|
||||
wakeCh chan struct{}
|
||||
wakeDone chan E.NestedError
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
refCount *sync.WaitGroup
|
||||
|
||||
l logrus.FieldLogger
|
||||
}
|
||||
|
||||
WakeDone <-chan error
|
||||
WakeFunc func() WakeDone
|
||||
StopCallback func() (bool, E.NestedError)
|
||||
StopCallback func() E.NestedError
|
||||
)
|
||||
|
||||
var (
|
||||
mainLoopCtx context.Context
|
||||
mainLoopCancel context.CancelFunc
|
||||
mainLoopWg sync.WaitGroup
|
||||
|
||||
watcherMap = make(map[string]*watcher)
|
||||
watcherMapMu sync.Mutex
|
||||
|
||||
newWatcherCh = make(chan *watcher)
|
||||
|
||||
logger = logrus.WithField("module", "idle_watcher")
|
||||
)
|
||||
|
||||
func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
|
||||
@@ -51,6 +65,7 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
|
||||
|
||||
if w, ok := watcherMap[entry.ContainerName]; ok {
|
||||
w.refCount.Add(1)
|
||||
w.ReverseProxyEntry = entry
|
||||
return w, nil
|
||||
}
|
||||
|
||||
@@ -62,13 +77,14 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
|
||||
w := &watcher{
|
||||
ReverseProxyEntry: entry,
|
||||
client: client,
|
||||
refCount: &sync.WaitGroup{},
|
||||
wakeCh: make(chan struct{}, 1),
|
||||
wakeDone: make(chan E.NestedError, 1),
|
||||
l: logger.WithField("container", entry.ContainerName),
|
||||
}
|
||||
w.refCount.Add(1)
|
||||
|
||||
w.stopByMethod = w.getStopCallback()
|
||||
|
||||
watcherMap[w.ContainerName] = w
|
||||
|
||||
go func() {
|
||||
@@ -78,19 +94,9 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// If the container is not registered, this is no-op
|
||||
func Unregister(containerName string) {
|
||||
watcherMapMu.Lock()
|
||||
defer watcherMapMu.Unlock()
|
||||
|
||||
if w, ok := watcherMap[containerName]; ok {
|
||||
if w.refCount.Load() == 0 {
|
||||
w.cancel()
|
||||
close(w.wakeCh)
|
||||
delete(watcherMap, containerName)
|
||||
} else {
|
||||
w.refCount.Add(-1)
|
||||
}
|
||||
w.refCount.Add(-1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,8 +106,6 @@ func Start() {
|
||||
|
||||
mainLoopCtx, mainLoopCancel = context.WithCancel(context.Background())
|
||||
|
||||
defer mainLoopWg.Wait()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-mainLoopCtx.Done():
|
||||
@@ -110,8 +114,11 @@ func Start() {
|
||||
w.l.Debug("registered")
|
||||
mainLoopWg.Add(1)
|
||||
go func() {
|
||||
w.watch()
|
||||
Unregister(w.ContainerName)
|
||||
w.watchUntilCancel()
|
||||
w.refCount.Wait() // wait for 0 ref count
|
||||
|
||||
w.client.Close()
|
||||
delete(watcherMap, w.ContainerName)
|
||||
w.l.Debug("unregistered")
|
||||
mainLoopWg.Done()
|
||||
}()
|
||||
@@ -130,24 +137,6 @@ func (w *watcher) PatchRoundTripper(rtp http.RoundTripper) roundTripper {
|
||||
}}
|
||||
}
|
||||
|
||||
func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*http.Response, error) {
|
||||
timeout := time.After(w.WakeTimeout)
|
||||
w.wakeCh <- struct{}{}
|
||||
for {
|
||||
select {
|
||||
case err := <-w.wakeDone:
|
||||
if err != nil {
|
||||
return nil, err.Error()
|
||||
}
|
||||
return origRoundTrip(req)
|
||||
case <-timeout:
|
||||
resp := loadingResponse
|
||||
resp.TLS = req.TLS
|
||||
return &resp, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) containerStop() error {
|
||||
return w.client.ContainerStop(w.ctx, w.ContainerName, container.StopOptions{
|
||||
Signal: string(w.StopSignal),
|
||||
@@ -178,36 +167,22 @@ func (w *watcher) containerStatus() (string, E.NestedError) {
|
||||
return json.State.Status, nil
|
||||
}
|
||||
|
||||
func (w *watcher) wakeIfStopped() (bool, E.NestedError) {
|
||||
failure := E.Failure("wake")
|
||||
func (w *watcher) wakeIfStopped() E.NestedError {
|
||||
status, err := w.containerStatus()
|
||||
|
||||
if err.HasError() {
|
||||
return false, failure.With(err)
|
||||
return err
|
||||
}
|
||||
// "created", "running", "paused", "restarting", "removing", "exited", or "dead"
|
||||
switch status {
|
||||
case "exited", "dead":
|
||||
err = E.From(w.containerStart())
|
||||
return E.From(w.containerStart())
|
||||
case "paused":
|
||||
err = E.From(w.containerUnpause())
|
||||
return E.From(w.containerUnpause())
|
||||
case "running":
|
||||
return false, nil
|
||||
return nil
|
||||
default:
|
||||
return false, failure.With(E.Unexpected("container state", status))
|
||||
}
|
||||
|
||||
if err.HasError() {
|
||||
return false, failure.With(err)
|
||||
}
|
||||
|
||||
status, err = w.containerStatus()
|
||||
if err.HasError() {
|
||||
return false, failure.With(err)
|
||||
} else if status != "running" {
|
||||
return false, failure.With(E.Unexpected("container state", status))
|
||||
} else {
|
||||
return true, nil
|
||||
return E.Unexpected("container state", status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,26 +198,36 @@ func (w *watcher) getStopCallback() StopCallback {
|
||||
default:
|
||||
panic("should not reach here")
|
||||
}
|
||||
return func() (bool, E.NestedError) {
|
||||
return func() E.NestedError {
|
||||
status, err := w.containerStatus()
|
||||
if err.HasError() {
|
||||
return false, E.FailWith("stop", err)
|
||||
return err
|
||||
}
|
||||
if status != "running" {
|
||||
return false, nil
|
||||
return nil
|
||||
}
|
||||
err = E.From(cb())
|
||||
if err.HasError() {
|
||||
return false, E.FailWith("stop", err)
|
||||
}
|
||||
return true, nil
|
||||
return E.From(cb())
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) watch() {
|
||||
watcherCtx, watcherCancel := context.WithCancel(context.Background())
|
||||
w.ctx = watcherCtx
|
||||
w.cancel = watcherCancel
|
||||
func (w *watcher) watchUntilCancel() {
|
||||
defer close(w.wakeCh)
|
||||
|
||||
w.ctx, w.cancel = context.WithCancel(context.Background())
|
||||
|
||||
dockerWatcher := W.NewDockerWatcherWithClient(w.client)
|
||||
dockerEventCh, dockerEventErrCh := dockerWatcher.EventsWithOptions(w.ctx, W.DockerListOptions{
|
||||
Filters: W.NewDockerFilter(
|
||||
W.DockerFilterContainer,
|
||||
W.DockerrFilterContainerName(w.ContainerName),
|
||||
W.DockerFilterStart,
|
||||
W.DockerFilterStop,
|
||||
W.DockerFilterDie,
|
||||
W.DockerFilterKill,
|
||||
W.DockerFilterPause,
|
||||
W.DockerFilterUnpause,
|
||||
),
|
||||
})
|
||||
|
||||
ticker := time.NewTicker(w.IdleTimeout)
|
||||
defer ticker.Stop()
|
||||
@@ -250,80 +235,42 @@ func (w *watcher) watch() {
|
||||
for {
|
||||
select {
|
||||
case <-mainLoopCtx.Done():
|
||||
watcherCancel()
|
||||
case <-watcherCtx.Done():
|
||||
w.cancel()
|
||||
case <-w.ctx.Done():
|
||||
w.l.Debug("stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
w.l.Debug("timeout")
|
||||
stopped, err := w.stopByMethod()
|
||||
if err.HasError() {
|
||||
w.l.Error(err.Extraf("stop method: %s", w.StopMethod))
|
||||
} else if stopped {
|
||||
w.l.Infof("%s: ok", w.StopMethod)
|
||||
} else {
|
||||
case err := <-dockerEventErrCh:
|
||||
if err != nil && err.IsNot(context.Canceled) {
|
||||
w.l.Error(E.FailWith("docker watcher", err))
|
||||
}
|
||||
case e := <-dockerEventCh:
|
||||
switch {
|
||||
// create / start / unpause
|
||||
case e.Action.IsContainerWake():
|
||||
ticker.Reset(w.IdleTimeout)
|
||||
w.l.Info(e)
|
||||
default: // stop / pause / kill
|
||||
ticker.Stop()
|
||||
w.ready.Store(false)
|
||||
w.l.Info(e)
|
||||
}
|
||||
case <-ticker.C:
|
||||
w.l.Debug("idle timeout")
|
||||
ticker.Stop()
|
||||
if err := w.stopByMethod(); err != nil && err.IsNot(context.Canceled) {
|
||||
w.l.Error(E.FailWith("stop", err).Extraf("stop method: %s", w.StopMethod))
|
||||
}
|
||||
case <-w.wakeCh:
|
||||
w.l.Debug("wake received")
|
||||
go func() {
|
||||
started, err := w.wakeIfStopped()
|
||||
if err != nil {
|
||||
w.l.Error(err)
|
||||
} else if started {
|
||||
w.l.Infof("awaken")
|
||||
ticker.Reset(w.IdleTimeout)
|
||||
}
|
||||
w.wakeDone <- err // this is passed to roundtrip
|
||||
}()
|
||||
w.l.Debug("wake signal received")
|
||||
ticker.Reset(w.IdleTimeout)
|
||||
err := w.wakeIfStopped()
|
||||
if err != nil && err.IsNot(context.Canceled) {
|
||||
w.l.Error(E.FailWith("wake", err))
|
||||
}
|
||||
select {
|
||||
case w.wakeDone <- err: // this is passed to roundtrip
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
mainLoopCtx context.Context
|
||||
mainLoopCancel context.CancelFunc
|
||||
mainLoopWg sync.WaitGroup
|
||||
|
||||
watcherMap = make(map[string]*watcher)
|
||||
watcherMapMu sync.Mutex
|
||||
|
||||
newWatcherCh = make(chan *watcher)
|
||||
|
||||
logger = logrus.WithField("module", "idle_watcher")
|
||||
|
||||
loadingResponse = http.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
Header: http.Header{
|
||||
"Content-Type": {"text/html"},
|
||||
"Cache-Control": {
|
||||
"no-cache",
|
||||
"no-store",
|
||||
"must-revalidate",
|
||||
},
|
||||
},
|
||||
Body: io.NopCloser(bytes.NewReader((loadingPage))),
|
||||
ContentLength: int64(len(loadingPage)),
|
||||
}
|
||||
|
||||
loadingPage = []byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Loading...</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 1000); // 1000 milliseconds = 1 second
|
||||
};
|
||||
</script>
|
||||
<p>Container is starting... Please wait</p>
|
||||
</body>
|
||||
</html>
|
||||
`[1:])
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ type Label struct {
|
||||
// Returns:
|
||||
// - error: an error if the field does not exist.
|
||||
func ApplyLabel[T any](obj *T, l *Label) E.NestedError {
|
||||
return U.SetFieldFromSnake(obj, l.Attribute, l.Value)
|
||||
return U.Deserialize(map[string]any{l.Attribute: l.Value}, obj)
|
||||
}
|
||||
|
||||
type ValueParser func(string) (any, E.NestedError)
|
||||
|
||||
@@ -3,8 +3,8 @@ package docker
|
||||
const (
|
||||
WildcardAlias = "*"
|
||||
|
||||
LableAliases = NSProxy + ".aliases"
|
||||
LableExclude = NSProxy + ".exclude"
|
||||
LabelAliases = NSProxy + ".aliases"
|
||||
LabelExclude = NSProxy + ".exclude"
|
||||
LabelIdleTimeout = NSProxy + ".idle_timeout"
|
||||
LabelWakeTimeout = NSProxy + ".wake_timeout"
|
||||
LabelStopMethod = NSProxy + ".stop_method"
|
||||
|
||||
@@ -25,6 +25,7 @@ func NewBuilder(format string, args ...any) Builder {
|
||||
func (b Builder) Add(err NestedError) Builder {
|
||||
if err != nil {
|
||||
b.Lock()
|
||||
// TODO: if err severity is higher than b.severity, update b.severity
|
||||
b.errors = append(b.errors, err)
|
||||
b.Unlock()
|
||||
}
|
||||
@@ -54,15 +55,19 @@ func (b Builder) WithSeverity(s Severity) Builder {
|
||||
func (b Builder) Build() NestedError {
|
||||
if len(b.errors) == 0 {
|
||||
return nil
|
||||
} else if len(b.errors) == 1 {
|
||||
return b.errors[0]
|
||||
}
|
||||
return Join(b.message, b.errors...).Severity(b.severity)
|
||||
}
|
||||
|
||||
func (b Builder) To(ptr *NestedError) {
|
||||
if *ptr == nil {
|
||||
if ptr == nil {
|
||||
return
|
||||
} else if *ptr == nil {
|
||||
*ptr = b.Build()
|
||||
} else {
|
||||
**ptr = *b.Build()
|
||||
(*ptr).With(b.Build())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,17 +36,17 @@ func TestBuilderNested(t *testing.T) {
|
||||
expected1 :=
|
||||
(`error occurred:
|
||||
- Action 1 failed:
|
||||
- invalid Inner - 1
|
||||
- invalid Inner - 2
|
||||
- invalid Inner: 1
|
||||
- invalid Inner: 2
|
||||
- Action 2 failed:
|
||||
- invalid Inner - 3`)
|
||||
- invalid Inner: 3`)
|
||||
expected2 :=
|
||||
(`error occurred:
|
||||
- Action 1 failed:
|
||||
- invalid Inner - 2
|
||||
- invalid Inner - 1
|
||||
- invalid Inner: "1"
|
||||
- invalid Inner: "2"
|
||||
- Action 2 failed:
|
||||
- invalid Inner - 3`)
|
||||
- invalid Inner: "3"`)
|
||||
if got != expected1 && got != expected2 {
|
||||
t.Errorf("expected \n%s, got \n%s", expected1, got)
|
||||
}
|
||||
|
||||
@@ -10,35 +10,23 @@ type (
|
||||
NestedError = *nestedError
|
||||
nestedError struct {
|
||||
subject string
|
||||
err error // can be nil
|
||||
err error
|
||||
extras []nestedError
|
||||
severity Severity
|
||||
}
|
||||
errorInterface struct {
|
||||
*nestedError
|
||||
}
|
||||
Severity uint8
|
||||
)
|
||||
|
||||
const (
|
||||
SeverityFatal Severity = iota
|
||||
SeverityWarning
|
||||
SeverityWarning Severity = iota
|
||||
SeverityFatal
|
||||
)
|
||||
|
||||
func (e errorInterface) Error() string {
|
||||
return e.String()
|
||||
}
|
||||
|
||||
func From(err error) NestedError {
|
||||
if IsNil(err) {
|
||||
return nil
|
||||
}
|
||||
switch err := err.(type) {
|
||||
case errorInterface:
|
||||
return err.nestedError
|
||||
default:
|
||||
return &nestedError{err: err}
|
||||
}
|
||||
return &nestedError{err: err}
|
||||
}
|
||||
|
||||
// Check is a helper function that
|
||||
@@ -112,7 +100,7 @@ func (ne NestedError) Error() error {
|
||||
if ne == nil {
|
||||
return nil
|
||||
}
|
||||
return errorInterface{ne}
|
||||
return ne.buildError(0, "")
|
||||
}
|
||||
|
||||
func (ne NestedError) With(s any) NestedError {
|
||||
@@ -123,18 +111,18 @@ func (ne NestedError) With(s any) NestedError {
|
||||
switch ss := s.(type) {
|
||||
case nil:
|
||||
return ne
|
||||
case *nestedError:
|
||||
return ne.withError(ss.Error())
|
||||
case error:
|
||||
case NestedError:
|
||||
return ne.withError(ss)
|
||||
case error:
|
||||
return ne.withError(From(ss))
|
||||
case string:
|
||||
msg = ss
|
||||
case fmt.Stringer:
|
||||
msg = ss.String()
|
||||
return ne.append(ss.String())
|
||||
default:
|
||||
msg = fmt.Sprint(s)
|
||||
return ne.append(fmt.Sprint(s))
|
||||
}
|
||||
return ne.withError(errors.New(msg))
|
||||
return ne.withError(From(errors.New(msg)))
|
||||
}
|
||||
|
||||
func (ne NestedError) Extraf(format string, args ...any) NestedError {
|
||||
@@ -206,15 +194,25 @@ func errorf(format string, args ...any) NestedError {
|
||||
return From(fmt.Errorf(format, args...))
|
||||
}
|
||||
|
||||
func (ne NestedError) withError(err error) NestedError {
|
||||
if ne != nil && IsNotNil(err) {
|
||||
ne.extras = append(ne.extras, *From(err))
|
||||
func (ne NestedError) withError(err NestedError) NestedError {
|
||||
if ne != nil && err != nil {
|
||||
ne.extras = append(ne.extras, *err)
|
||||
}
|
||||
return ne
|
||||
}
|
||||
|
||||
func (ne NestedError) append(msg string) NestedError {
|
||||
if ne == nil {
|
||||
return nil
|
||||
}
|
||||
ne.err = fmt.Errorf("%w %s", ne.err, msg)
|
||||
return ne
|
||||
}
|
||||
|
||||
func (ne NestedError) writeToSB(sb *strings.Builder, level int, prefix string) {
|
||||
ne.writeIndents(sb, level)
|
||||
for i := 0; i < level; i++ {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
sb.WriteString(prefix)
|
||||
|
||||
if ne.NoError() {
|
||||
@@ -224,11 +222,7 @@ func (ne NestedError) writeToSB(sb *strings.Builder, level int, prefix string) {
|
||||
|
||||
sb.WriteString(ne.err.Error())
|
||||
if ne.subject != "" {
|
||||
if IsNotNil(ne.err) {
|
||||
sb.WriteString(fmt.Sprintf(" for %q", ne.subject))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprint(ne.subject))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" for %q", ne.subject))
|
||||
}
|
||||
if len(ne.extras) > 0 {
|
||||
sb.WriteRune(':')
|
||||
@@ -239,8 +233,32 @@ func (ne NestedError) writeToSB(sb *strings.Builder, level int, prefix string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (ne NestedError) writeIndents(sb *strings.Builder, level int) {
|
||||
func (ne NestedError) buildError(level int, prefix string) error {
|
||||
var res error
|
||||
var sb strings.Builder
|
||||
|
||||
for i := 0; i < level; i++ {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
sb.WriteString(prefix)
|
||||
|
||||
if ne.NoError() {
|
||||
sb.WriteString("nil")
|
||||
return errors.New(sb.String())
|
||||
}
|
||||
|
||||
res = fmt.Errorf("%s%w", sb.String(), ne.err)
|
||||
sb.Reset()
|
||||
|
||||
if ne.subject != "" {
|
||||
sb.WriteString(fmt.Sprintf(" for %q", ne.subject))
|
||||
}
|
||||
if len(ne.extras) > 0 {
|
||||
sb.WriteRune(':')
|
||||
res = fmt.Errorf("%w%s", res, sb.String())
|
||||
for _, extra := range ne.extras {
|
||||
res = errors.Join(res, extra.buildError(level+1, "- "))
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package error_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/go-proxy/error"
|
||||
@@ -17,6 +18,11 @@ func TestErrorIs(t *testing.T) {
|
||||
ExpectFalse(t, Invalid("foo", "bar").Is(ErrFailure))
|
||||
|
||||
ExpectFalse(t, Invalid("foo", "bar").Is(nil))
|
||||
|
||||
ExpectTrue(t, errors.Is(Failure("foo").Error(), ErrFailure))
|
||||
ExpectTrue(t, errors.Is(Failure("foo").With(Invalid("bar", "baz")).Error(), ErrInvalid))
|
||||
ExpectTrue(t, errors.Is(Failure("foo").With(Invalid("bar", "baz")).Error(), ErrFailure))
|
||||
ExpectFalse(t, errors.Is(Failure("foo").With(Invalid("bar", "baz")).Error(), ErrNotExists))
|
||||
}
|
||||
|
||||
func TestErrorNestedIs(t *testing.T) {
|
||||
@@ -99,4 +105,5 @@ func TestErrorNested(t *testing.T) {
|
||||
- 3
|
||||
- 3`
|
||||
ExpectEqual(t, ne.String(), want)
|
||||
ExpectEqual(t, ne.Error().Error(), want)
|
||||
}
|
||||
|
||||
@@ -10,17 +10,19 @@ var (
|
||||
ErrUnsupported = stderrors.New("unsupported")
|
||||
ErrUnexpected = stderrors.New("unexpected")
|
||||
ErrNotExists = stderrors.New("does not exist")
|
||||
ErrMissing = stderrors.New("missing")
|
||||
ErrAlreadyExist = stderrors.New("already exist")
|
||||
ErrOutOfRange = stderrors.New("out of range")
|
||||
)
|
||||
|
||||
const fmtSubjectWhat = "%w %v: %v"
|
||||
const fmtSubjectWhat = "%w %v: %q"
|
||||
|
||||
func Failure(what string) NestedError {
|
||||
return errorf("%s %w", what, ErrFailure)
|
||||
}
|
||||
|
||||
func FailedWhy(what string, why string) NestedError {
|
||||
return errorf("%s %w because %s", what, ErrFailure, why)
|
||||
return Failure(what).With(why)
|
||||
}
|
||||
|
||||
func FailWith(what string, err any) NestedError {
|
||||
@@ -47,6 +49,14 @@ func NotExist(subject, what any) NestedError {
|
||||
return errorf("%v %w: %v", subject, ErrNotExists, what)
|
||||
}
|
||||
|
||||
func Missing(subject any) NestedError {
|
||||
return errorf("%w %v", ErrMissing, subject)
|
||||
}
|
||||
|
||||
func AlreadyExist(subject, what any) NestedError {
|
||||
return errorf("%v %w: %v", subject, ErrAlreadyExist, what)
|
||||
}
|
||||
|
||||
func OutOfRange(subject string, value any) NestedError {
|
||||
return errorf("%v %w: %v", subject, ErrOutOfRange, value)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ module github.com/yusing/go-proxy
|
||||
go 1.22.0
|
||||
|
||||
require (
|
||||
github.com/docker/cli v27.2.1+incompatible
|
||||
github.com/docker/docker v27.2.1+incompatible
|
||||
github.com/docker/cli v27.3.1+incompatible
|
||||
github.com/docker/docker v27.3.1+incompatible
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/go-acme/lego/v4 v4.18.0
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
||||
|
||||
@@ -13,10 +13,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v27.2.1+incompatible h1:U5BPtiD0viUzjGAjV1p0MGB8eVA3L3cbIrnyWmSJI70=
|
||||
github.com/docker/cli v27.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI=
|
||||
github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ=
|
||||
github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
|
||||
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
|
||||
100
src/main.go
100
src/main.go
@@ -3,11 +3,14 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -26,32 +29,34 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
args := common.GetArgs()
|
||||
l := logrus.WithField("module", "main")
|
||||
onShutdown := F.NewSlice[func()]()
|
||||
|
||||
if common.IsDebug {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
}
|
||||
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
DisableSorting: true,
|
||||
DisableLevelTruncation: true,
|
||||
FullTimestamp: true,
|
||||
ForceColors: true,
|
||||
TimestampFormat: "01-02 15:04:05",
|
||||
})
|
||||
if args.Command != common.CommandStart {
|
||||
logrus.SetOutput(io.Discard)
|
||||
} else {
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
DisableSorting: true,
|
||||
DisableLevelTruncation: true,
|
||||
FullTimestamp: true,
|
||||
ForceColors: true,
|
||||
TimestampFormat: "01-02 15:04:05",
|
||||
})
|
||||
}
|
||||
|
||||
if args.Command == common.CommandReload {
|
||||
if err := apiUtils.ReloadServer(); err.HasError() {
|
||||
l.Fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Print("ok")
|
||||
return
|
||||
}
|
||||
|
||||
onShutdown := F.NewSlice[func()]()
|
||||
|
||||
// exit if only validate config
|
||||
if args.Command == common.CommandValidate {
|
||||
data, err := os.ReadFile(common.ConfigPath)
|
||||
@@ -59,33 +64,40 @@ func main() {
|
||||
err = config.Validate(data).Error()
|
||||
}
|
||||
if err != nil {
|
||||
l.Fatal("config error: ", err)
|
||||
log.Fatal("config error: ", err)
|
||||
}
|
||||
l.Printf("config OK")
|
||||
log.Print("config OK")
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err.IsFatal() {
|
||||
l.Fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if args.Command == common.CommandListConfigs {
|
||||
switch args.Command {
|
||||
case common.CommandListConfigs:
|
||||
printJSON(cfg.Value())
|
||||
return
|
||||
case common.CommandListRoutes:
|
||||
printJSON(cfg.RoutesByAlias())
|
||||
return
|
||||
case common.CommandDebugListEntries:
|
||||
printJSON(cfg.DumpEntries())
|
||||
return
|
||||
case common.CommandDebugListProviders:
|
||||
printJSON(cfg.DumpProviders())
|
||||
return
|
||||
}
|
||||
|
||||
cfg.StartProxyProviders()
|
||||
|
||||
if args.Command == common.CommandListRoutes {
|
||||
printJSON(cfg.RoutesByAlias())
|
||||
return
|
||||
}
|
||||
|
||||
if err.HasError() {
|
||||
l.Warn(err)
|
||||
}
|
||||
|
||||
cfg.WatchChanges()
|
||||
|
||||
onShutdown.Add(docker.CloseAllClients)
|
||||
onShutdown.Add(cfg.Dispose)
|
||||
|
||||
@@ -97,25 +109,14 @@ func main() {
|
||||
autocert := cfg.GetAutoCertProvider()
|
||||
|
||||
if autocert != nil {
|
||||
if err = autocert.LoadCert(); err.HasError() {
|
||||
if !err.Is(os.ErrNotExist) { // ignore if cert doesn't exist
|
||||
l.Error(err)
|
||||
}
|
||||
l.Debug("obtaining cert due to error loading cert")
|
||||
if err = autocert.ObtainCert(); err.HasError() {
|
||||
l.Warn(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err.NoError() {
|
||||
ctx, certRenewalCancel := context.WithCancel(context.Background())
|
||||
go autocert.ScheduleRenewal(ctx)
|
||||
onShutdown.Add(certRenewalCancel)
|
||||
}
|
||||
|
||||
for _, expiry := range autocert.GetExpiries() {
|
||||
l.Infof("certificate expire on %s", expiry)
|
||||
break
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
if err = autocert.Setup(ctx); err != nil && err.IsWarning() {
|
||||
cancel()
|
||||
l.Warn(err)
|
||||
} else if err.IsFatal() {
|
||||
l.Fatal(err)
|
||||
} else {
|
||||
onShutdown.Add(cancel)
|
||||
}
|
||||
} else {
|
||||
l.Info("autocert not configured")
|
||||
@@ -124,15 +125,15 @@ func main() {
|
||||
proxyServer := server.InitProxyServer(server.Options{
|
||||
Name: "proxy",
|
||||
CertProvider: autocert,
|
||||
HTTPPort: common.ProxyHTTPPort,
|
||||
HTTPSPort: common.ProxyHTTPSPort,
|
||||
HTTPAddr: common.ProxyHTTPAddr,
|
||||
HTTPSAddr: common.ProxyHTTPSAddr,
|
||||
Handler: http.HandlerFunc(R.ProxyHandler),
|
||||
RedirectToHTTPS: cfg.Value().RedirectToHTTPS,
|
||||
})
|
||||
apiServer := server.InitAPIServer(server.Options{
|
||||
Name: "api",
|
||||
CertProvider: autocert,
|
||||
HTTPPort: common.APIHTTPPort,
|
||||
HTTPAddr: common.APIHTTPAddr,
|
||||
Handler: api.NewHandler(cfg),
|
||||
RedirectToHTTPS: cfg.Value().RedirectToHTTPS,
|
||||
})
|
||||
@@ -156,7 +157,9 @@ func main() {
|
||||
wg.Add(onShutdown.Size())
|
||||
onShutdown.ForEach(func(f func()) {
|
||||
go func() {
|
||||
l.Debugf("waiting for %s to complete...", funcName(f))
|
||||
f()
|
||||
l.Debugf("%s done", funcName(f))
|
||||
wg.Done()
|
||||
}()
|
||||
})
|
||||
@@ -165,14 +168,23 @@ func main() {
|
||||
close(done)
|
||||
}()
|
||||
|
||||
timeout := time.After(time.Duration(cfg.Value().TimeoutShutdown) * time.Second)
|
||||
select {
|
||||
case <-done:
|
||||
logrus.Info("shutdown complete")
|
||||
case <-time.After(time.Duration(cfg.Value().TimeoutShutdown) * time.Second):
|
||||
case <-timeout:
|
||||
logrus.Info("timeout waiting for shutdown")
|
||||
onShutdown.ForEach(func(f func()) {
|
||||
l.Warnf("%s() is still running", funcName(f))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func funcName(f func()) string {
|
||||
parts := strings.Split(runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name(), "/go-proxy/")
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
|
||||
func printJSON(obj any) {
|
||||
j, err := E.Check(json.Marshal(obj))
|
||||
if err.HasError() {
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
. "github.com/yusing/go-proxy/common"
|
||||
D "github.com/yusing/go-proxy/docker"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
)
|
||||
|
||||
type (
|
||||
ProxyEntry struct { // raw entry object before validation
|
||||
Alias string `yaml:"-" json:"-"`
|
||||
Scheme string `yaml:"scheme" json:"scheme"`
|
||||
Host string `yaml:"host" json:"host"`
|
||||
Port string `yaml:"port" json:"port"`
|
||||
NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // https proxy only
|
||||
PathPatterns []string `yaml:"path_patterns" json:"path_patterns"` // http(s) proxy only
|
||||
SetHeaders map[string]string `yaml:"set_headers" json:"set_headers"` // http(s) proxy only
|
||||
HideHeaders []string `yaml:"hide_headers" json:"hide_headers"` // http(s) proxy only
|
||||
|
||||
/* Docker only */
|
||||
*D.ProxyProperties `yaml:"-" json:"-"`
|
||||
}
|
||||
|
||||
ProxyEntries = F.Map[string, *ProxyEntry]
|
||||
)
|
||||
|
||||
var NewProxyEntries = F.NewMapOf[string, *ProxyEntry]
|
||||
|
||||
func (e *ProxyEntry) SetDefaults() {
|
||||
if e.Scheme == "" {
|
||||
switch {
|
||||
case strings.ContainsRune(e.Port, ':'):
|
||||
e.Scheme = "tcp"
|
||||
case e.ProxyProperties != nil:
|
||||
if _, ok := ServiceNamePortMapTCP[e.ImageName]; ok {
|
||||
e.Scheme = "tcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if e.Scheme == "" {
|
||||
switch e.Port {
|
||||
case "443", "8443":
|
||||
e.Scheme = "https"
|
||||
default:
|
||||
e.Scheme = "http"
|
||||
}
|
||||
}
|
||||
if e.Host == "" {
|
||||
e.Host = "localhost"
|
||||
}
|
||||
if e.Port == "" {
|
||||
e.Port = e.FirstPort
|
||||
}
|
||||
if e.Port == "" {
|
||||
if port, ok := ServiceNamePortMapTCP[e.Port]; ok {
|
||||
e.Port = strconv.Itoa(port)
|
||||
} else if port, ok := ImageNamePortMapHTTP[e.Port]; ok {
|
||||
e.Port = strconv.Itoa(port)
|
||||
} else {
|
||||
switch e.Scheme {
|
||||
case "http":
|
||||
e.Port = "80"
|
||||
case "https":
|
||||
e.Port = "443"
|
||||
}
|
||||
}
|
||||
}
|
||||
if e.IdleTimeout == "" {
|
||||
e.IdleTimeout = IdleTimeoutDefault
|
||||
}
|
||||
if e.WakeTimeout == "" {
|
||||
e.WakeTimeout = WakeTimeoutDefault
|
||||
}
|
||||
if e.StopTimeout == "" {
|
||||
e.StopTimeout = StopTimeoutDefault
|
||||
}
|
||||
if e.StopMethod == "" {
|
||||
e.StopMethod = StopMethodDefault
|
||||
}
|
||||
}
|
||||
115
src/models/raw_entry.go
Normal file
115
src/models/raw_entry.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
. "github.com/yusing/go-proxy/common"
|
||||
D "github.com/yusing/go-proxy/docker"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
)
|
||||
|
||||
type (
|
||||
RawEntry struct {
|
||||
// raw entry object before validation
|
||||
// loaded from docker labels or yaml file
|
||||
Alias string `yaml:"-" json:"-"`
|
||||
Scheme string `yaml:"scheme" json:"scheme"`
|
||||
Host string `yaml:"host" json:"host"`
|
||||
Port string `yaml:"port" json:"port"`
|
||||
NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // https proxy only
|
||||
PathPatterns []string `yaml:"path_patterns" json:"path_patterns"` // http(s) proxy only
|
||||
SetHeaders map[string]string `yaml:"set_headers" json:"set_headers"` // http(s) proxy only
|
||||
HideHeaders []string `yaml:"hide_headers" json:"hide_headers"` // http(s) proxy only
|
||||
|
||||
/* Docker only */
|
||||
*D.ProxyProperties `yaml:"-" json:"proxy_properties"`
|
||||
}
|
||||
|
||||
RawEntries = F.Map[string, *RawEntry]
|
||||
)
|
||||
|
||||
var NewProxyEntries = F.NewMapOf[string, *RawEntry]
|
||||
|
||||
func (e *RawEntry) FillMissingFields() bool {
|
||||
isDocker := e.ProxyProperties != nil
|
||||
|
||||
if !isDocker {
|
||||
e.ProxyProperties = &D.ProxyProperties{}
|
||||
}
|
||||
|
||||
if e.Port == "" {
|
||||
if port, ok := ServiceNamePortMapTCP[e.ImageName]; ok {
|
||||
e.Port = strconv.Itoa(port)
|
||||
} else if port, ok := ImageNamePortMap[e.ImageName]; ok {
|
||||
e.Port = strconv.Itoa(port)
|
||||
} else {
|
||||
switch {
|
||||
case e.Scheme == "https":
|
||||
e.Port = "443"
|
||||
case !isDocker:
|
||||
e.Port = "80"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if e.PublicPortMapping != nil && e.NetworkMode != "host" {
|
||||
if _, ok := e.PublicPortMapping[e.Port]; !ok { // port is not exposed, but specified
|
||||
// try to fallback to first public port
|
||||
if len(e.PublicPortMapping) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, p := range e.PublicPortMapping {
|
||||
e.Port = fmt.Sprint(p.PublicPort)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if e.Scheme == "" {
|
||||
if _, ok := ServiceNamePortMapTCP[e.ImageName]; ok {
|
||||
e.Scheme = "tcp"
|
||||
} else if strings.ContainsRune(e.Port, ':') {
|
||||
e.Scheme = "tcp"
|
||||
} else if _, ok := WellKnownHTTPPorts[e.Port]; ok {
|
||||
e.Scheme = "http"
|
||||
} else if e.Port == "443" {
|
||||
e.Scheme = "https"
|
||||
} else if isDocker {
|
||||
if e.Port == "" {
|
||||
return false
|
||||
}
|
||||
if p, ok := e.PublicPortMapping[e.Port]; ok {
|
||||
if p.Type == "udp" {
|
||||
e.Scheme = "udp"
|
||||
} else {
|
||||
e.Scheme = "http"
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
e.Scheme = "http"
|
||||
}
|
||||
}
|
||||
|
||||
if e.Host == "" {
|
||||
e.Host = "localhost"
|
||||
}
|
||||
|
||||
if e.IdleTimeout == "" {
|
||||
e.IdleTimeout = IdleTimeoutDefault
|
||||
}
|
||||
if e.WakeTimeout == "" {
|
||||
e.WakeTimeout = WakeTimeoutDefault
|
||||
}
|
||||
if e.StopTimeout == "" {
|
||||
e.StopTimeout = StopTimeoutDefault
|
||||
}
|
||||
if e.StopMethod == "" {
|
||||
e.StopMethod = StopMethodDefault
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package proxy
|
||||
|
||||
var (
|
||||
PathMode_Forward = "forward"
|
||||
PathMode_RemovedPath = ""
|
||||
)
|
||||
|
||||
const (
|
||||
StreamType_UDP string = "udp"
|
||||
StreamType_TCP string = "tcp"
|
||||
// StreamType_UDP_TCP Scheme = "udp-tcp"
|
||||
// StreamType_TCP_UDP Scheme = "tcp-udp"
|
||||
// StreamType_TLS Scheme = "tls"
|
||||
)
|
||||
|
||||
var (
|
||||
// TODO: support "tcp-udp", "udp-tcp", etc.
|
||||
StreamSchemes = []string{StreamType_TCP, StreamType_UDP}
|
||||
HTTPSchemes = []string{"http", "https"}
|
||||
ValidSchemes = append(StreamSchemes, HTTPSchemes...)
|
||||
)
|
||||
|
||||
@@ -22,13 +22,14 @@ type (
|
||||
HideHeaders []string
|
||||
|
||||
/* Docker only */
|
||||
IdleTimeout time.Duration
|
||||
WakeTimeout time.Duration
|
||||
StopMethod T.StopMethod
|
||||
StopTimeout int
|
||||
StopSignal T.Signal
|
||||
DockerHost string
|
||||
ContainerName string
|
||||
IdleTimeout time.Duration
|
||||
WakeTimeout time.Duration
|
||||
StopMethod T.StopMethod
|
||||
StopTimeout int
|
||||
StopSignal T.Signal
|
||||
DockerHost string
|
||||
ContainerName string
|
||||
ContainerRunning bool
|
||||
}
|
||||
StreamEntry struct {
|
||||
Alias T.Alias `json:"alias"`
|
||||
@@ -42,8 +43,11 @@ func (rp *ReverseProxyEntry) UseIdleWatcher() bool {
|
||||
return rp.IdleTimeout > 0 && rp.DockerHost != ""
|
||||
}
|
||||
|
||||
func ValidateEntry(m *M.ProxyEntry) (any, E.NestedError) {
|
||||
m.SetDefaults()
|
||||
func ValidateEntry(m *M.RawEntry) (any, E.NestedError) {
|
||||
if !m.FillMissingFields() {
|
||||
return nil, E.Missing("fields")
|
||||
}
|
||||
|
||||
scheme, err := T.NewScheme(m.Scheme)
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
@@ -62,7 +66,7 @@ func ValidateEntry(m *M.ProxyEntry) (any, E.NestedError) {
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func validateRPEntry(m *M.ProxyEntry, s T.Scheme, b E.Builder) *ReverseProxyEntry {
|
||||
func validateRPEntry(m *M.RawEntry, s T.Scheme, b E.Builder) *ReverseProxyEntry {
|
||||
var stopTimeOut time.Duration
|
||||
|
||||
host, err := T.ValidateHost(m.Host)
|
||||
@@ -102,24 +106,25 @@ func validateRPEntry(m *M.ProxyEntry, s T.Scheme, b E.Builder) *ReverseProxyEntr
|
||||
}
|
||||
|
||||
return &ReverseProxyEntry{
|
||||
Alias: T.NewAlias(m.Alias),
|
||||
Scheme: s,
|
||||
URL: url,
|
||||
NoTLSVerify: m.NoTLSVerify,
|
||||
PathPatterns: pathPatterns,
|
||||
SetHeaders: setHeaders,
|
||||
HideHeaders: m.HideHeaders,
|
||||
IdleTimeout: idleTimeout,
|
||||
WakeTimeout: wakeTimeout,
|
||||
StopMethod: stopMethod,
|
||||
StopTimeout: int(stopTimeOut.Seconds()), // docker api takes integer seconds for timeout argument
|
||||
StopSignal: stopSignal,
|
||||
DockerHost: m.DockerHost,
|
||||
ContainerName: m.ContainerName,
|
||||
Alias: T.NewAlias(m.Alias),
|
||||
Scheme: s,
|
||||
URL: url,
|
||||
NoTLSVerify: m.NoTLSVerify,
|
||||
PathPatterns: pathPatterns,
|
||||
SetHeaders: setHeaders,
|
||||
HideHeaders: m.HideHeaders,
|
||||
IdleTimeout: idleTimeout,
|
||||
WakeTimeout: wakeTimeout,
|
||||
StopMethod: stopMethod,
|
||||
StopTimeout: int(stopTimeOut.Seconds()), // docker api takes integer seconds for timeout argument
|
||||
StopSignal: stopSignal,
|
||||
DockerHost: m.DockerHost,
|
||||
ContainerName: m.ContainerName,
|
||||
ContainerRunning: m.Running,
|
||||
}
|
||||
}
|
||||
|
||||
func validateStreamEntry(m *M.ProxyEntry, b E.Builder) *StreamEntry {
|
||||
func validateStreamEntry(m *M.RawEntry, b E.Builder) *StreamEntry {
|
||||
host, err := T.ValidateHost(m.Host)
|
||||
b.Add(err)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ func NewPathPattern(s string) (PathPattern, E.NestedError) {
|
||||
if len(s) == 0 {
|
||||
return "", E.Invalid("path", "must not be empty")
|
||||
}
|
||||
if !pathPattern.MatchString(string(s)) {
|
||||
if !pathPattern.MatchString(s) {
|
||||
return "", E.Invalid("path pattern", s)
|
||||
}
|
||||
return PathPattern(s), nil
|
||||
@@ -34,4 +34,4 @@ func ValidatePathPatterns(s []string) (PathPatterns, E.NestedError) {
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
var pathPattern = regexp.MustCompile("^((GET|POST|DELETE|PUT|PATCH|HEAD|OPTIONS|CONNECT)\\s)?(/\\w*)+/?$")
|
||||
var pathPattern = regexp.MustCompile(`^(/[-\w./]*({\$\})?|((GET|POST|DELETE|PUT|HEAD|OPTION) /[-\w./]*({\$\})?))$`)
|
||||
|
||||
47
src/proxy/fields/path_pattern_test.go
Normal file
47
src/proxy/fields/path_pattern_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
U "github.com/yusing/go-proxy/utils/testing"
|
||||
)
|
||||
|
||||
var validPatterns = []string{
|
||||
"/",
|
||||
"/index.html",
|
||||
"/somepage/",
|
||||
"/drive/abc.mp4",
|
||||
"/{$}",
|
||||
"/some-page/{$}",
|
||||
"GET /",
|
||||
"GET /static/{$}",
|
||||
"GET /drive/abc.mp4",
|
||||
"GET /drive/abc.mp4/{$}",
|
||||
"POST /auth",
|
||||
"DELETE /user/",
|
||||
"PUT /storage/id/",
|
||||
}
|
||||
|
||||
var invalidPatterns = []string{
|
||||
"/$",
|
||||
"/{$}{$}",
|
||||
"/{$}/{$}",
|
||||
"/index.html$",
|
||||
"get /",
|
||||
"GET/",
|
||||
"GET /$",
|
||||
"GET /drive/{$}/abc.mp4/",
|
||||
"OPTION /config/{$}/abc.conf/{$}",
|
||||
}
|
||||
|
||||
func TestPathPatternRegex(t *testing.T) {
|
||||
for _, pattern := range validPatterns {
|
||||
_, err := NewPathPattern(pattern)
|
||||
U.ExpectNoError(t, err.Error())
|
||||
}
|
||||
for _, pattern := range invalidPatterns {
|
||||
_, err := NewPathPattern(pattern)
|
||||
U.ExpectError2(t, pattern, E.ErrInvalid, err.Error())
|
||||
}
|
||||
}
|
||||
@@ -13,22 +13,23 @@ func ValidatePort(v string) (Port, E.NestedError) {
|
||||
if err != nil {
|
||||
return ErrPort, E.Invalid("port number", v).With(err)
|
||||
}
|
||||
return NewPortInt(p)
|
||||
return ValidatePortInt(p)
|
||||
}
|
||||
|
||||
func NewPortInt[Int int | uint16](v Int) (Port, E.NestedError) {
|
||||
pp := Port(v)
|
||||
if err := pp.boundCheck(); err.HasError() {
|
||||
return ErrPort, err
|
||||
func ValidatePortInt[Int int | uint16](v Int) (Port, E.NestedError) {
|
||||
p := Port(v)
|
||||
if !p.inBound() {
|
||||
return ErrPort, E.OutOfRange("port", p)
|
||||
}
|
||||
return pp, nil
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p Port) boundCheck() E.NestedError {
|
||||
if p < MinPort || p > MaxPort {
|
||||
return E.Invalid("port", p)
|
||||
}
|
||||
return nil
|
||||
func (p Port) inBound() bool {
|
||||
return p >= MinPort && p <= MaxPort
|
||||
}
|
||||
|
||||
func (p Port) String() string {
|
||||
return strconv.Itoa(int(p))
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -14,36 +14,42 @@ type StreamPort struct {
|
||||
|
||||
func ValidateStreamPort(p string) (StreamPort, E.NestedError) {
|
||||
split := strings.Split(p, ":")
|
||||
if len(split) != 2 {
|
||||
return StreamPort{}, E.Invalid("stream port", p).With("should be in 'x:y' format")
|
||||
|
||||
switch len(split) {
|
||||
case 1:
|
||||
split = []string{"0", split[0]}
|
||||
case 2:
|
||||
break
|
||||
default:
|
||||
return ErrStreamPort, E.Invalid("stream port", p).With("too many colons")
|
||||
}
|
||||
|
||||
listeningPort, err := ValidatePort(split[0])
|
||||
if err.HasError() {
|
||||
return StreamPort{}, err
|
||||
}
|
||||
if err = listeningPort.boundCheck(); err.HasError() {
|
||||
return StreamPort{}, err
|
||||
if err != nil {
|
||||
return ErrStreamPort, err
|
||||
}
|
||||
|
||||
proxyPort, err := ValidatePort(split[1])
|
||||
if err.HasError() {
|
||||
if err.Is(E.ErrOutOfRange) {
|
||||
return ErrStreamPort, err
|
||||
} else if proxyPort == 0 {
|
||||
return ErrStreamPort, E.Invalid("stream port", p).With("proxy port cannot be 0")
|
||||
} else if err != nil {
|
||||
proxyPort, err = parseNameToPort(split[1])
|
||||
if err.HasError() {
|
||||
return StreamPort{}, err
|
||||
if err != nil {
|
||||
return ErrStreamPort, E.Invalid("stream port", p).With(proxyPort)
|
||||
}
|
||||
}
|
||||
if err = proxyPort.boundCheck(); err.HasError() {
|
||||
return StreamPort{}, err
|
||||
}
|
||||
|
||||
return StreamPort{ListeningPort: listeningPort, ProxyPort: proxyPort}, nil
|
||||
return StreamPort{listeningPort, proxyPort}, nil
|
||||
}
|
||||
|
||||
func parseNameToPort(name string) (Port, E.NestedError) {
|
||||
port, ok := common.ServiceNamePortMapTCP[name]
|
||||
if !ok {
|
||||
return -1, E.Unsupported("service", name)
|
||||
return ErrPort, E.Invalid("service", name)
|
||||
}
|
||||
return Port(port), nil
|
||||
}
|
||||
|
||||
var ErrStreamPort = StreamPort{ErrPort, ErrPort}
|
||||
|
||||
48
src/proxy/fields/stream_port_test.go
Normal file
48
src/proxy/fields/stream_port_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
. "github.com/yusing/go-proxy/utils/testing"
|
||||
)
|
||||
|
||||
var validPorts = []string{
|
||||
"1234:5678",
|
||||
"0:2345",
|
||||
"2345",
|
||||
"1234:postgres",
|
||||
}
|
||||
|
||||
var invalidPorts = []string{
|
||||
"",
|
||||
"123:",
|
||||
"0:",
|
||||
":1234",
|
||||
"1234:1234:1234",
|
||||
"qwerty",
|
||||
"asdfgh:asdfgh",
|
||||
"1234:asdfgh",
|
||||
}
|
||||
|
||||
var outOfRangePorts = []string{
|
||||
"-1:1234",
|
||||
"1234:-1",
|
||||
"65536",
|
||||
"0:65536",
|
||||
}
|
||||
|
||||
func TestStreamPort(t *testing.T) {
|
||||
for _, port := range validPorts {
|
||||
_, err := ValidateStreamPort(port)
|
||||
ExpectNoError(t, err.Error())
|
||||
}
|
||||
for _, port := range invalidPorts {
|
||||
_, err := ValidateStreamPort(port)
|
||||
ExpectError2(t, port, E.ErrInvalid, err.Error())
|
||||
}
|
||||
for _, port := range outOfRangePorts {
|
||||
_, err := ValidateStreamPort(port)
|
||||
ExpectError2(t, port, E.ErrOutOfRange, err.Error())
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func ValidateStreamScheme(s string) (ss *StreamScheme, err E.NestedError) {
|
||||
}
|
||||
|
||||
func (s StreamScheme) String() string {
|
||||
return fmt.Sprintf("%s -> %s", s.ListeningScheme, s.ProxyScheme)
|
||||
return fmt.Sprintf("%s:%s", s.ListeningScheme, s.ProxyScheme)
|
||||
}
|
||||
|
||||
// IsCoherent checks if the ListeningScheme and ProxyScheme of the StreamScheme are equal.
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
D "github.com/yusing/go-proxy/docker"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
M "github.com/yusing/go-proxy/models"
|
||||
R "github.com/yusing/go-proxy/route"
|
||||
W "github.com/yusing/go-proxy/watcher"
|
||||
. "github.com/yusing/go-proxy/watcher/event"
|
||||
)
|
||||
|
||||
type DockerProvider struct {
|
||||
dockerHost, hostname string
|
||||
}
|
||||
|
||||
func DockerProviderImpl(dockerHost string) ProviderImpl {
|
||||
return &DockerProvider{dockerHost: dockerHost}
|
||||
var AliasRefRegex = regexp.MustCompile(`\$\d+`)
|
||||
|
||||
func DockerProviderImpl(dockerHost string) (ProviderImpl, E.NestedError) {
|
||||
hostname, err := D.ParseDockerHostname(dockerHost)
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
return &DockerProvider{dockerHost: dockerHost, hostname: hostname}, nil
|
||||
}
|
||||
|
||||
func (p *DockerProvider) String() string {
|
||||
return fmt.Sprintf("docker:%s", p.dockerHost)
|
||||
}
|
||||
|
||||
func (p *DockerProvider) NewWatcher() W.Watcher {
|
||||
@@ -22,6 +36,7 @@ func (p *DockerProvider) NewWatcher() W.Watcher {
|
||||
}
|
||||
|
||||
func (p *DockerProvider) LoadRoutesImpl() (routes R.Routes, err E.NestedError) {
|
||||
routes = R.NewRoutes()
|
||||
entries := M.NewProxyEntries()
|
||||
|
||||
info, err := D.GetClientInfo(p.dockerHost, true)
|
||||
@@ -45,12 +60,12 @@ func (p *DockerProvider) LoadRoutesImpl() (routes R.Routes, err E.NestedError) {
|
||||
// there may be some valid entries in `en`
|
||||
dups := entries.MergeFrom(newEntries)
|
||||
// add the duplicate proxy entries to the error
|
||||
dups.RangeAll(func(k string, v *M.ProxyEntry) {
|
||||
dups.RangeAll(func(k string, v *M.RawEntry) {
|
||||
errors.Addf("duplicate alias %s", k)
|
||||
})
|
||||
}
|
||||
|
||||
entries.RangeAll(func(_ string, e *M.ProxyEntry) {
|
||||
entries.RangeAll(func(_ string, e *M.RawEntry) {
|
||||
e.DockerHost = p.dockerHost
|
||||
})
|
||||
|
||||
@@ -60,7 +75,7 @@ func (p *DockerProvider) LoadRoutesImpl() (routes R.Routes, err E.NestedError) {
|
||||
return routes, errors.Build()
|
||||
}
|
||||
|
||||
func (p *DockerProvider) OnEvent(event Event, routes R.Routes) (res EventResult) {
|
||||
func (p *DockerProvider) OnEvent(event W.Event, routes R.Routes) (res EventResult) {
|
||||
b := E.NewBuilder("event %s error", event)
|
||||
defer b.To(&res.err)
|
||||
|
||||
@@ -72,48 +87,45 @@ func (p *DockerProvider) OnEvent(event Event, routes R.Routes) (res EventResult)
|
||||
}
|
||||
})
|
||||
|
||||
switch event.Action {
|
||||
case ActionStarted, ActionCreated, ActionModified:
|
||||
client, err := D.ConnectClient(p.dockerHost)
|
||||
if err.HasError() {
|
||||
b.Add(E.FailWith("connect to docker", err))
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
cont, err := client.Inspect(event.ActorID)
|
||||
if err.HasError() {
|
||||
b.Add(E.FailWith("inspect container", err))
|
||||
return
|
||||
}
|
||||
entries, err := p.entriesFromContainerLabels(cont)
|
||||
b.Add(err)
|
||||
|
||||
entries.RangeAll(func(alias string, entry *M.ProxyEntry) {
|
||||
if routes.Has(alias) {
|
||||
b.Add(E.AlreadyExist("alias", alias))
|
||||
} else {
|
||||
if route, err := R.NewRoute(entry); err.HasError() {
|
||||
b.Add(err)
|
||||
} else {
|
||||
routes.Store(alias, route)
|
||||
b.Add(route.Start())
|
||||
res.nAdded++
|
||||
}
|
||||
}
|
||||
})
|
||||
client, err := D.ConnectClient(p.dockerHost)
|
||||
if err.HasError() {
|
||||
b.Add(E.FailWith("connect to docker", err))
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
cont, err := client.Inspect(event.ActorID)
|
||||
if err.HasError() {
|
||||
b.Add(E.FailWith("inspect container", err))
|
||||
return
|
||||
}
|
||||
entries, err := p.entriesFromContainerLabels(cont)
|
||||
b.Add(err)
|
||||
|
||||
entries.RangeAll(func(alias string, entry *M.RawEntry) {
|
||||
if routes.Has(alias) {
|
||||
b.Add(E.AlreadyExist("alias", alias))
|
||||
} else {
|
||||
if route, err := R.NewRoute(entry); err.HasError() {
|
||||
b.Add(err)
|
||||
} else {
|
||||
routes.Store(alias, route)
|
||||
b.Add(route.Start())
|
||||
res.nAdded++
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Returns a list of proxy entries for a container.
|
||||
// Always non-nil
|
||||
func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (M.ProxyEntries, E.NestedError) {
|
||||
func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (M.RawEntries, E.NestedError) {
|
||||
entries := M.NewProxyEntries()
|
||||
|
||||
// init entries map for all aliases
|
||||
for _, a := range container.Aliases {
|
||||
entries.Store(a, &M.ProxyEntry{
|
||||
entries.Store(a, &M.RawEntry{
|
||||
Alias: a,
|
||||
Host: p.hostname,
|
||||
ProxyProperties: container.ProxyProperties,
|
||||
@@ -122,13 +134,37 @@ func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (M.Pr
|
||||
|
||||
errors := E.NewBuilder("failed to apply label")
|
||||
for key, val := range container.Labels {
|
||||
errors.Add(p.applyLabel(entries, key, val))
|
||||
errors.Add(p.applyLabel(container, entries, key, val))
|
||||
}
|
||||
|
||||
// selecting correct host port
|
||||
replacePrivPorts := func() {
|
||||
if container.HostConfig.NetworkMode != "host" {
|
||||
entries.RangeAll(func(_ string, entry *M.RawEntry) {
|
||||
entryPortSplit := strings.Split(entry.Port, ":")
|
||||
n := len(entryPortSplit)
|
||||
// if the port matches the proxy port, replace it with the public port
|
||||
if p, ok := container.PrivatePortMapping[entryPortSplit[n-1]]; ok {
|
||||
entryPortSplit[n-1] = fmt.Sprint(p.PublicPort)
|
||||
entry.Port = strings.Join(entryPortSplit, ":")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
replacePrivPorts()
|
||||
|
||||
// remove all entries that failed to fill in missing fields
|
||||
entries.RemoveAll(func(re *M.RawEntry) bool {
|
||||
return !re.FillMissingFields()
|
||||
})
|
||||
|
||||
// do it again since the port may got filled in
|
||||
replacePrivPorts()
|
||||
|
||||
return entries, errors.Build().Subject(container.ContainerName)
|
||||
}
|
||||
|
||||
func (p *DockerProvider) applyLabel(entries M.ProxyEntries, key, val string) (res E.NestedError) {
|
||||
func (p *DockerProvider) applyLabel(container D.Container, entries M.RawEntries, key, val string) (res E.NestedError) {
|
||||
b := E.NewBuilder("errors in label %s", key)
|
||||
defer b.To(&res)
|
||||
|
||||
@@ -141,12 +177,29 @@ func (p *DockerProvider) applyLabel(entries M.ProxyEntries, key, val string) (re
|
||||
}
|
||||
if lbl.Target == D.WildcardAlias {
|
||||
// apply label for all aliases
|
||||
entries.RangeAll(func(a string, e *M.ProxyEntry) {
|
||||
entries.RangeAll(func(a string, e *M.RawEntry) {
|
||||
if err = D.ApplyLabel(e, lbl); err.HasError() {
|
||||
b.Add(err.Subject(lbl.Target))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
refErr := E.NewBuilder("errors parsing alias references")
|
||||
lbl.Target = AliasRefRegex.ReplaceAllStringFunc(lbl.Target, func(ref string) string {
|
||||
index, err := strconv.Atoi(ref[1:])
|
||||
if err != nil {
|
||||
refErr.Add(E.Invalid("integer", ref))
|
||||
return ref
|
||||
}
|
||||
if index < 1 || index > len(container.Aliases) {
|
||||
refErr.Add(E.OutOfRange("index", ref))
|
||||
return ref
|
||||
}
|
||||
return container.Aliases[index-1]
|
||||
})
|
||||
if refErr.HasError() {
|
||||
b.Add(refErr.Build())
|
||||
return
|
||||
}
|
||||
config, ok := entries.Load(lbl.Target)
|
||||
if !ok {
|
||||
b.Add(E.NotExist("alias", lbl.Target))
|
||||
|
||||
311
src/proxy/provider/docker_provider_test.go
Normal file
311
src/proxy/provider/docker_provider_test.go
Normal file
@@ -0,0 +1,311 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/yusing/go-proxy/common"
|
||||
D "github.com/yusing/go-proxy/docker"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
P "github.com/yusing/go-proxy/proxy"
|
||||
T "github.com/yusing/go-proxy/proxy/fields"
|
||||
|
||||
. "github.com/yusing/go-proxy/utils/testing"
|
||||
)
|
||||
|
||||
var dummyNames = []string{"/a"}
|
||||
|
||||
func TestApplyLabelFieldValidity(t *testing.T) {
|
||||
pathPatterns := `
|
||||
- /
|
||||
- POST /upload/{$}
|
||||
- GET /static
|
||||
`[1:]
|
||||
pathPatternsExpect := []string{
|
||||
"/",
|
||||
"POST /upload/{$}",
|
||||
"GET /static",
|
||||
}
|
||||
setHeaders := `
|
||||
X_Custom_Header1: value1
|
||||
X_Custom_Header1: value2
|
||||
X_Custom_Header2: value3
|
||||
`[1:]
|
||||
setHeadersExpect := map[string]string{
|
||||
"X_Custom_Header1": "value1, value2",
|
||||
"X_Custom_Header2": "value3",
|
||||
}
|
||||
hideHeaders := `
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
`[1:]
|
||||
hideHeadersExpect := []string{
|
||||
"X-Custom-Header1",
|
||||
"X-Custom-Header2",
|
||||
}
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b",
|
||||
D.LabelIdleTimeout: common.IdleTimeoutDefault,
|
||||
D.LabelStopMethod: common.StopMethodDefault,
|
||||
D.LabelStopSignal: "SIGTERM",
|
||||
D.LabelStopTimeout: common.StopTimeoutDefault,
|
||||
D.LabelWakeTimeout: common.WakeTimeoutDefault,
|
||||
"proxy.*.no_tls_verify": "true",
|
||||
"proxy.*.scheme": "https",
|
||||
"proxy.*.host": "app",
|
||||
"proxy.*.port": "4567",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
"proxy.a.path_patterns": pathPatterns,
|
||||
"proxy.a.set_headers": setHeaders,
|
||||
"proxy.a.hide_headers": hideHeaders,
|
||||
},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 4567, PublicPort: 8888},
|
||||
}}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
a, ok := entries.Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
b, ok := entries.Load("b")
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
ExpectEqual(t, a.Scheme, "https")
|
||||
ExpectEqual(t, b.Scheme, "https")
|
||||
|
||||
ExpectEqual(t, a.Host, "app")
|
||||
ExpectEqual(t, b.Host, "app")
|
||||
|
||||
ExpectEqual(t, a.Port, "8888")
|
||||
ExpectEqual(t, b.Port, "8888")
|
||||
|
||||
ExpectTrue(t, a.NoTLSVerify)
|
||||
ExpectTrue(t, b.NoTLSVerify)
|
||||
|
||||
ExpectDeepEqual(t, a.PathPatterns, pathPatternsExpect)
|
||||
ExpectEqual(t, len(b.PathPatterns), 0)
|
||||
|
||||
ExpectDeepEqual(t, a.SetHeaders, setHeadersExpect)
|
||||
ExpectEqual(t, len(b.SetHeaders), 0)
|
||||
|
||||
ExpectDeepEqual(t, a.HideHeaders, hideHeadersExpect)
|
||||
ExpectEqual(t, len(b.HideHeaders), 0)
|
||||
|
||||
ExpectEqual(t, a.IdleTimeout, common.IdleTimeoutDefault)
|
||||
ExpectEqual(t, b.IdleTimeout, common.IdleTimeoutDefault)
|
||||
|
||||
ExpectEqual(t, a.StopTimeout, common.StopTimeoutDefault)
|
||||
ExpectEqual(t, b.StopTimeout, common.StopTimeoutDefault)
|
||||
|
||||
ExpectEqual(t, a.StopMethod, common.StopMethodDefault)
|
||||
ExpectEqual(t, b.StopMethod, common.StopMethodDefault)
|
||||
|
||||
ExpectEqual(t, a.WakeTimeout, common.WakeTimeoutDefault)
|
||||
ExpectEqual(t, b.WakeTimeout, common.WakeTimeoutDefault)
|
||||
|
||||
ExpectEqual(t, a.StopSignal, "SIGTERM")
|
||||
ExpectEqual(t, b.StopSignal, "SIGTERM")
|
||||
}
|
||||
|
||||
func TestApplyLabel(t *testing.T) {
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b,c",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
"proxy.a.port": "3333",
|
||||
"proxy.b.port": "1234",
|
||||
"proxy.c.scheme": "https",
|
||||
},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 3333, PublicPort: 1111},
|
||||
{Type: "tcp", PrivatePort: 4444, PublicPort: 1234},
|
||||
}}, "",
|
||||
))
|
||||
a, ok := entries.Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
b, ok := entries.Load("b")
|
||||
ExpectTrue(t, ok)
|
||||
c, ok := entries.Load("c")
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
ExpectNoError(t, err.Error())
|
||||
ExpectEqual(t, a.Scheme, "http")
|
||||
ExpectEqual(t, a.Port, "1111")
|
||||
ExpectEqual(t, a.NoTLSVerify, true)
|
||||
ExpectEqual(t, b.Scheme, "http")
|
||||
ExpectEqual(t, b.Port, "1234")
|
||||
ExpectEqual(t, c.Scheme, "https")
|
||||
ExpectEqual(t, c.Port, "1111")
|
||||
}
|
||||
|
||||
func TestApplyLabelWithRef(t *testing.T) {
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b,c",
|
||||
"proxy.$1.host": "localhost",
|
||||
"proxy.*.port": "1111",
|
||||
"proxy.$1.port": "4444",
|
||||
"proxy.$2.port": "9999",
|
||||
"proxy.$3.scheme": "https",
|
||||
},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 3333, PublicPort: 9999},
|
||||
{Type: "tcp", PrivatePort: 4444, PublicPort: 5555},
|
||||
{Type: "tcp", PrivatePort: 1111, PublicPort: 2222},
|
||||
}}, ""))
|
||||
a, ok := entries.Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
b, ok := entries.Load("b")
|
||||
ExpectTrue(t, ok)
|
||||
c, ok := entries.Load("c")
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
ExpectNoError(t, err.Error())
|
||||
ExpectEqual(t, a.Scheme, "http")
|
||||
ExpectEqual(t, a.Host, "localhost")
|
||||
ExpectEqual(t, a.Port, "5555")
|
||||
ExpectEqual(t, b.Port, "9999")
|
||||
ExpectEqual(t, c.Scheme, "https")
|
||||
ExpectEqual(t, c.Port, "2222")
|
||||
}
|
||||
|
||||
func TestApplyLabelWithRefIndexError(t *testing.T) {
|
||||
var p DockerProvider
|
||||
var c = D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b",
|
||||
"proxy.$1.host": "localhost",
|
||||
"proxy.$4.scheme": "https",
|
||||
}}, "")
|
||||
_, err := p.entriesFromContainerLabels(c)
|
||||
ExpectError(t, E.ErrOutOfRange, err.Error())
|
||||
ExpectTrue(t, strings.Contains(err.String(), "index out of range"))
|
||||
|
||||
_, err = p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b",
|
||||
"proxy.$0.host": "localhost",
|
||||
}}, ""))
|
||||
ExpectError(t, E.ErrOutOfRange, err.Error())
|
||||
ExpectTrue(t, strings.Contains(err.String(), "index out of range"))
|
||||
}
|
||||
|
||||
func TestStreamDefaultValues(t *testing.T) {
|
||||
var p DockerProvider
|
||||
var c = D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a",
|
||||
"proxy.*.no_tls_verify": "true",
|
||||
},
|
||||
Ports: []types.Port{
|
||||
{Type: "udp", PrivatePort: 1234, PublicPort: 5678},
|
||||
}}, "",
|
||||
)
|
||||
entries, err := p.entriesFromContainerLabels(c)
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
raw, ok := entries.Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
entry, err := P.ValidateEntry(raw)
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
a := ExpectType[*P.StreamEntry](t, entry)
|
||||
ExpectEqual(t, a.Scheme.ListeningScheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, a.Scheme.ProxyScheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, a.Port.ListeningPort, 0)
|
||||
ExpectEqual(t, a.Port.ProxyPort, 5678)
|
||||
}
|
||||
|
||||
func TestExplicitExclude(t *testing.T) {
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a",
|
||||
D.LabelExclude: "true",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
}}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
_, ok := entries.Load("a")
|
||||
ExpectFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestImplicitExclude(t *testing.T) {
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
}}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
_, ok := entries.Load("a")
|
||||
ExpectFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestImplicitExcludeNoExposedPort(t *testing.T) {
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Image: "redis",
|
||||
Names: []string{"redis"},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
},
|
||||
}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
_, ok := entries.Load("redis")
|
||||
ExpectFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestExcludeNonExposedPort(t *testing.T) {
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Image: "redis",
|
||||
Names: []string{"redis"},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"proxy.redis.port": "6379:6379", // should be excluded even specified
|
||||
},
|
||||
}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
_, ok := entries.Load("redis")
|
||||
ExpectFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestNotExcludeNonExposedPortHostNetwork(t *testing.T) {
|
||||
var p DockerProvider
|
||||
cont := &types.Container{
|
||||
Image: "redis",
|
||||
Names: []string{"redis"},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"proxy.redis.port": "6379:6379", // should be excluded even specified
|
||||
},
|
||||
}
|
||||
cont.HostConfig.NetworkMode = "host"
|
||||
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(cont, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
_, ok := entries.Load("redis")
|
||||
ExpectTrue(t, ok)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
@@ -10,7 +11,6 @@ import (
|
||||
R "github.com/yusing/go-proxy/route"
|
||||
U "github.com/yusing/go-proxy/utils"
|
||||
W "github.com/yusing/go-proxy/watcher"
|
||||
. "github.com/yusing/go-proxy/watcher/event"
|
||||
)
|
||||
|
||||
type FileProvider struct {
|
||||
@@ -18,18 +18,31 @@ type FileProvider struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func FileProviderImpl(filename string) ProviderImpl {
|
||||
return &FileProvider{
|
||||
func FileProviderImpl(filename string) (ProviderImpl, E.NestedError) {
|
||||
impl := &FileProvider{
|
||||
fileName: filename,
|
||||
path: path.Join(common.ConfigBasePath, filename),
|
||||
}
|
||||
_, err := os.Stat(impl.path)
|
||||
switch {
|
||||
case err == nil:
|
||||
return impl, nil
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
return nil, E.NotExist("file", impl.path)
|
||||
default:
|
||||
return nil, E.UnexpectedError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Validate(data []byte) E.NestedError {
|
||||
return U.ValidateYaml(U.GetSchema(common.ProvidersSchemaPath), data)
|
||||
return U.ValidateYaml(U.GetSchema(common.FileProviderSchemaPath), data)
|
||||
}
|
||||
|
||||
func (p FileProvider) OnEvent(event Event, routes R.Routes) (res EventResult) {
|
||||
func (p FileProvider) String() string {
|
||||
return p.fileName
|
||||
}
|
||||
|
||||
func (p FileProvider) OnEvent(event W.Event, routes R.Routes) (res EventResult) {
|
||||
b := E.NewBuilder("event %s error", event)
|
||||
defer b.To(&res.err)
|
||||
|
||||
@@ -53,6 +66,8 @@ func (p FileProvider) OnEvent(event Event, routes R.Routes) (res EventResult) {
|
||||
}
|
||||
|
||||
func (p *FileProvider) LoadRoutesImpl() (routes R.Routes, res E.NestedError) {
|
||||
routes = R.NewRoutes()
|
||||
|
||||
b := E.NewBuilder("file %q validation failure", p.fileName)
|
||||
defer b.To(&res)
|
||||
|
||||
|
||||
@@ -2,19 +2,17 @@ package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
R "github.com/yusing/go-proxy/route"
|
||||
W "github.com/yusing/go-proxy/watcher"
|
||||
. "github.com/yusing/go-proxy/watcher/event"
|
||||
)
|
||||
|
||||
type (
|
||||
Provider struct {
|
||||
ProviderImpl
|
||||
ProviderImpl `json:"-"`
|
||||
|
||||
name string
|
||||
t ProviderType
|
||||
@@ -28,8 +26,10 @@ type (
|
||||
}
|
||||
ProviderImpl interface {
|
||||
NewWatcher() W.Watcher
|
||||
// even returns error, routes must be non-nil
|
||||
LoadRoutesImpl() (R.Routes, E.NestedError)
|
||||
OnEvent(event Event, routes R.Routes) EventResult
|
||||
OnEvent(event W.Event, routes R.Routes) EventResult
|
||||
String() string
|
||||
}
|
||||
ProviderType string
|
||||
EventResult struct {
|
||||
@@ -54,19 +54,25 @@ func newProvider(name string, t ProviderType) *Provider {
|
||||
return p
|
||||
}
|
||||
|
||||
func NewFileProvider(filename string) *Provider {
|
||||
func NewFileProvider(filename string) (p *Provider, err E.NestedError) {
|
||||
name := path.Base(filename)
|
||||
p := newProvider(name, ProviderTypeFile)
|
||||
p.ProviderImpl = FileProviderImpl(filename)
|
||||
p = newProvider(name, ProviderTypeFile)
|
||||
p.ProviderImpl, err = FileProviderImpl(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.watcher = p.NewWatcher()
|
||||
return p
|
||||
return
|
||||
}
|
||||
|
||||
func NewDockerProvider(name string, dockerHost string) *Provider {
|
||||
p := newProvider(name, ProviderTypeDocker)
|
||||
p.ProviderImpl = DockerProviderImpl(dockerHost)
|
||||
func NewDockerProvider(name string, dockerHost string) (p *Provider, err E.NestedError) {
|
||||
p = newProvider(name, ProviderTypeDocker)
|
||||
p.ProviderImpl, err = DockerProviderImpl(dockerHost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.watcher = p.NewWatcher()
|
||||
return p
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Provider) GetName() string {
|
||||
@@ -77,8 +83,9 @@ func (p *Provider) GetType() ProviderType {
|
||||
return p.t
|
||||
}
|
||||
|
||||
func (p *Provider) String() string {
|
||||
return fmt.Sprintf("%s-%s", p.t, p.name)
|
||||
// to work with json marshaller
|
||||
func (p *Provider) MarshalText() ([]byte, error) {
|
||||
return []byte(p.String()), nil
|
||||
}
|
||||
|
||||
func (p *Provider) StartAllRoutes() (res E.NestedError) {
|
||||
@@ -86,7 +93,6 @@ func (p *Provider) StartAllRoutes() (res E.NestedError) {
|
||||
defer errors.To(&res)
|
||||
|
||||
// start watcher no matter load success or not
|
||||
p.watcherCtx, p.watcherCancel = context.WithCancel(context.Background())
|
||||
go p.watchEvents()
|
||||
|
||||
nStarted := 0
|
||||
@@ -137,15 +143,17 @@ func (p *Provider) GetRoute(alias string) (R.Route, bool) {
|
||||
}
|
||||
|
||||
func (p *Provider) LoadRoutes() E.NestedError {
|
||||
routes, err := p.LoadRoutesImpl()
|
||||
if err != nil {
|
||||
var err E.NestedError
|
||||
p.routes, err = p.LoadRoutesImpl()
|
||||
if p.routes.Size() > 0 {
|
||||
p.l.Infof("loaded %d routes", p.routes.Size())
|
||||
return err
|
||||
}
|
||||
p.routes = routes
|
||||
return nil
|
||||
return E.FailWith("loading routes", err)
|
||||
}
|
||||
|
||||
func (p *Provider) watchEvents() {
|
||||
p.watcherCtx, p.watcherCancel = context.WithCancel(context.Background())
|
||||
events, errs := p.watcher.Events(p.watcherCtx)
|
||||
l := p.l.WithField("module", "watcher")
|
||||
|
||||
@@ -153,21 +161,15 @@ func (p *Provider) watchEvents() {
|
||||
select {
|
||||
case <-p.watcherCtx.Done():
|
||||
return
|
||||
case event, ok := <-events:
|
||||
if !ok { // channel closed
|
||||
return
|
||||
}
|
||||
case event := <-events:
|
||||
res := p.OnEvent(event, p.routes)
|
||||
l.Infof("%s event %q", event.Type, event)
|
||||
l.Infof("%d route added, %d routes removed", res.nAdded, res.nRemoved)
|
||||
if res.err.HasError() {
|
||||
l.Error(res.err)
|
||||
}
|
||||
case err, ok := <-errs:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err.Is(context.Canceled) {
|
||||
case err := <-errs:
|
||||
if err == nil || err.Is(context.Canceled) {
|
||||
continue
|
||||
}
|
||||
l.Errorf("watcher error: %s", err)
|
||||
|
||||
@@ -232,7 +232,7 @@ func NewReverseProxy(target *url.URL, transport http.RoundTripper, entry *Revers
|
||||
}
|
||||
return &ReverseProxy{Rewrite: func(pr *ProxyRequest) {
|
||||
rewriteRequestURL(pr.Out, target)
|
||||
pr.SetXForwarded()
|
||||
// pr.SetXForwarded()
|
||||
setHeaders(pr.Out)
|
||||
hideHeaders(pr.Out)
|
||||
}, Transport: transport}
|
||||
@@ -348,9 +348,9 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
outreq.Header.Del("Forwarded")
|
||||
// outreq.Header.Del("X-Forwarded-For")
|
||||
// outreq.Header.Del("X-Forwarded-Host")
|
||||
// outreq.Header.Del("X-Forwarded-Proto")
|
||||
outreq.Header.Del("X-Forwarded-For")
|
||||
outreq.Header.Del("X-Forwarded-Host")
|
||||
outreq.Header.Del("X-Forwarded-Proto")
|
||||
|
||||
pr := &ProxyRequest{
|
||||
In: req,
|
||||
|
||||
@@ -4,5 +4,5 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const udpBufferSize = 1500
|
||||
const udpBufferSize = 8192
|
||||
const streamStopListenTimeout = 1 * time.Second
|
||||
|
||||
@@ -37,19 +37,23 @@ type (
|
||||
)
|
||||
|
||||
func NewHTTPRoute(entry *P.ReverseProxyEntry) (*HTTPRoute, E.NestedError) {
|
||||
var trans http.RoundTripper
|
||||
var trans *http.Transport
|
||||
var regIdleWatcher func() E.NestedError
|
||||
var unregIdleWatcher func()
|
||||
|
||||
if entry.NoTLSVerify {
|
||||
trans = transportNoTLS
|
||||
trans = transportNoTLS.Clone()
|
||||
} else {
|
||||
trans = transport
|
||||
trans = transport.Clone()
|
||||
}
|
||||
|
||||
rp := P.NewReverseProxy(entry.URL, trans, entry)
|
||||
|
||||
if entry.UseIdleWatcher() {
|
||||
// allow time for response header up to `WakeTimeout`
|
||||
if entry.WakeTimeout > trans.ResponseHeaderTimeout {
|
||||
trans.ResponseHeaderTimeout = entry.WakeTimeout
|
||||
}
|
||||
regIdleWatcher = func() E.NestedError {
|
||||
watcher, err := idlewatcher.Register(entry)
|
||||
if err.HasError() {
|
||||
@@ -114,6 +118,7 @@ func (r *HTTPRoute) Stop() E.NestedError {
|
||||
|
||||
if r.unregIdleWatcher != nil {
|
||||
r.unregIdleWatcher()
|
||||
r.unregIdleWatcher = nil
|
||||
}
|
||||
|
||||
r.mux = nil
|
||||
@@ -151,13 +156,13 @@ func findMux(host string) (*http.ServeMux, E.NestedError) {
|
||||
}
|
||||
|
||||
var (
|
||||
defaultDialer = net.Dialer{
|
||||
Timeout: 60 * time.Second,
|
||||
KeepAlive: 60 * time.Second,
|
||||
}
|
||||
transport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 60 * time.Second,
|
||||
KeepAlive: 60 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 1000,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: defaultDialer.DialContext,
|
||||
MaxIdleConnsPerHost: 1000,
|
||||
}
|
||||
transportNoTLS = func() *http.Transport {
|
||||
@@ -168,4 +173,5 @@ var (
|
||||
|
||||
httpRoutes = F.NewMapOf[SubdomainKey, *HTTPRoute]()
|
||||
httpRoutesMu sync.Mutex
|
||||
globalMux = http.NewServeMux()
|
||||
)
|
||||
|
||||
@@ -13,22 +13,22 @@ import (
|
||||
type (
|
||||
Route interface {
|
||||
RouteImpl
|
||||
Entry() *M.ProxyEntry
|
||||
Entry() *M.RawEntry
|
||||
Type() RouteType
|
||||
URL() *url.URL
|
||||
}
|
||||
Routes = F.Map[string, Route]
|
||||
RouteType string
|
||||
Routes = F.Map[string, Route]
|
||||
|
||||
RouteImpl interface {
|
||||
Start() E.NestedError
|
||||
Stop() E.NestedError
|
||||
String() string
|
||||
}
|
||||
route struct {
|
||||
RouteType string
|
||||
route struct {
|
||||
RouteImpl
|
||||
type_ RouteType
|
||||
entry *M.ProxyEntry
|
||||
entry *M.RawEntry
|
||||
}
|
||||
)
|
||||
|
||||
@@ -40,9 +40,9 @@ const (
|
||||
// function alias
|
||||
var NewRoutes = F.NewMapOf[string, Route]
|
||||
|
||||
func NewRoute(en *M.ProxyEntry) (Route, E.NestedError) {
|
||||
func NewRoute(en *M.RawEntry) (Route, E.NestedError) {
|
||||
rt, err := P.ValidateEntry(en)
|
||||
if err.HasError() {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -58,10 +58,13 @@ func NewRoute(en *M.ProxyEntry) (Route, E.NestedError) {
|
||||
default:
|
||||
panic("bug: should not reach here")
|
||||
}
|
||||
return &route{RouteImpl: rt.(RouteImpl), entry: en, type_: t}, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &route{RouteImpl: rt.(RouteImpl), entry: en, type_: t}, nil
|
||||
}
|
||||
|
||||
func (rt *route) Entry() *M.ProxyEntry {
|
||||
func (rt *route) Entry() *M.RawEntry {
|
||||
return rt.entry
|
||||
}
|
||||
|
||||
@@ -74,11 +77,11 @@ func (rt *route) URL() *url.URL {
|
||||
return url
|
||||
}
|
||||
|
||||
func FromEntries(entries M.ProxyEntries) (Routes, E.NestedError) {
|
||||
func FromEntries(entries M.RawEntries) (Routes, E.NestedError) {
|
||||
b := E.NewBuilder("errors in routes")
|
||||
|
||||
routes := NewRoutes()
|
||||
entries.RangeAll(func(alias string, entry *M.ProxyEntry) {
|
||||
entries.RangeAll(func(alias string, entry *M.RawEntry) {
|
||||
entry.Alias = alias
|
||||
r, err := NewRoute(entry)
|
||||
if err.HasError() {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -12,11 +14,13 @@ import (
|
||||
)
|
||||
|
||||
type StreamRoute struct {
|
||||
P.StreamEntry
|
||||
*P.StreamEntry
|
||||
StreamImpl `json:"-"`
|
||||
|
||||
wg sync.WaitGroup
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
connCh chan any
|
||||
started atomic.Bool
|
||||
l logrus.FieldLogger
|
||||
@@ -27,6 +31,7 @@ type StreamImpl interface {
|
||||
Accept() (any, error)
|
||||
Handle(any) error
|
||||
CloseListeners()
|
||||
String() string
|
||||
}
|
||||
|
||||
func NewStreamRoute(entry *P.StreamEntry) (*StreamRoute, E.NestedError) {
|
||||
@@ -35,9 +40,8 @@ func NewStreamRoute(entry *P.StreamEntry) (*StreamRoute, E.NestedError) {
|
||||
return nil, E.Unsupported("scheme", fmt.Sprintf("%v -> %v", entry.Scheme.ListeningScheme, entry.Scheme.ProxyScheme))
|
||||
}
|
||||
base := &StreamRoute{
|
||||
StreamEntry: *entry,
|
||||
wg: sync.WaitGroup{},
|
||||
connCh: make(chan any),
|
||||
StreamEntry: entry,
|
||||
connCh: make(chan any, 100),
|
||||
}
|
||||
if entry.Scheme.ListeningScheme.IsTCP() {
|
||||
base.StreamImpl = NewTCPRoute(base)
|
||||
@@ -54,13 +58,14 @@ func (r *StreamRoute) String() string {
|
||||
|
||||
func (r *StreamRoute) Start() E.NestedError {
|
||||
if r.started.Load() {
|
||||
return E.Invalid("state", "already started")
|
||||
return nil
|
||||
}
|
||||
r.stopCh = make(chan struct{}, 1)
|
||||
r.ctx, r.cancel = context.WithCancel(context.Background())
|
||||
r.wg.Wait()
|
||||
if err := r.Setup(); err != nil {
|
||||
return E.FailWith("setup", err)
|
||||
}
|
||||
r.l.Infof("listening on port %d", r.Port.ListeningPort)
|
||||
r.started.Store(true)
|
||||
r.wg.Add(2)
|
||||
go r.grAcceptConnections()
|
||||
@@ -70,10 +75,10 @@ func (r *StreamRoute) Start() E.NestedError {
|
||||
|
||||
func (r *StreamRoute) Stop() E.NestedError {
|
||||
if !r.started.Load() {
|
||||
return E.Invalid("state", "not started")
|
||||
return nil
|
||||
}
|
||||
l := r.l
|
||||
close(r.stopCh)
|
||||
r.cancel()
|
||||
r.CloseListeners()
|
||||
|
||||
done := make(chan struct{}, 1)
|
||||
@@ -82,13 +87,16 @@ func (r *StreamRoute) Stop() E.NestedError {
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
l.Info("stopped listening")
|
||||
case <-time.After(streamStopListenTimeout):
|
||||
l.Error("timed out waiting for connections")
|
||||
timeout := time.After(streamStopListenTimeout)
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
l.Debug("stopped listening")
|
||||
return nil
|
||||
case <-timeout:
|
||||
return E.FailedWhy("stop", "timed out")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *StreamRoute) grAcceptConnections() {
|
||||
@@ -96,13 +104,13 @@ func (r *StreamRoute) grAcceptConnections() {
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.stopCh:
|
||||
case <-r.ctx.Done():
|
||||
return
|
||||
default:
|
||||
conn, err := r.Accept()
|
||||
if err != nil {
|
||||
select {
|
||||
case <-r.stopCh:
|
||||
case <-r.ctx.Done():
|
||||
return
|
||||
default:
|
||||
r.l.Error(err)
|
||||
@@ -119,12 +127,12 @@ func (r *StreamRoute) grHandleConnections() {
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.stopCh:
|
||||
case <-r.ctx.Done():
|
||||
return
|
||||
case conn := <-r.connCh:
|
||||
go func() {
|
||||
err := r.Handle(conn)
|
||||
if err != nil {
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
r.l.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -7,24 +7,26 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
T "github.com/yusing/go-proxy/proxy/fields"
|
||||
U "github.com/yusing/go-proxy/utils"
|
||||
)
|
||||
|
||||
const tcpDialTimeout = 5 * time.Second
|
||||
|
||||
type Pipes []*U.BidirectionalPipe
|
||||
type (
|
||||
Pipes []U.BidirectionalPipe
|
||||
|
||||
type TCPRoute struct {
|
||||
*StreamRoute
|
||||
listener net.Listener
|
||||
pipe Pipes
|
||||
mu sync.Mutex
|
||||
}
|
||||
TCPRoute struct {
|
||||
*StreamRoute
|
||||
listener net.Listener
|
||||
pipe Pipes
|
||||
mu sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
func NewTCPRoute(base *StreamRoute) StreamImpl {
|
||||
return &TCPRoute{
|
||||
StreamRoute: base,
|
||||
listener: nil,
|
||||
pipe: make(Pipes, 0),
|
||||
}
|
||||
}
|
||||
@@ -34,6 +36,8 @@ func (route *TCPRoute) Setup() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//! this read the allocated port from orginal ':0'
|
||||
route.Port.ListeningPort = T.Port(in.Addr().(*net.TCPAddr).Port)
|
||||
route.listener = in
|
||||
return nil
|
||||
}
|
||||
@@ -47,7 +51,7 @@ func (route *TCPRoute) Handle(c any) error {
|
||||
|
||||
defer clientConn.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), tcpDialTimeout)
|
||||
ctx, cancel := context.WithTimeout(route.ctx, tcpDialTimeout)
|
||||
defer cancel()
|
||||
|
||||
serverAddr := fmt.Sprintf("%s:%v", route.Host, route.Port.ProxyPort)
|
||||
@@ -58,17 +62,12 @@ func (route *TCPRoute) Handle(c any) error {
|
||||
return err
|
||||
}
|
||||
|
||||
pipeCtx, pipeCancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
<-route.stopCh
|
||||
pipeCancel()
|
||||
}()
|
||||
|
||||
route.mu.Lock()
|
||||
defer route.mu.Unlock()
|
||||
|
||||
pipe := U.NewBidirectionalPipe(pipeCtx, clientConn, serverConn)
|
||||
pipe := U.NewBidirectionalPipe(route.ctx, clientConn, serverConn)
|
||||
route.pipe = append(route.pipe, pipe)
|
||||
|
||||
route.mu.Unlock()
|
||||
return pipe.Start()
|
||||
}
|
||||
|
||||
@@ -78,9 +77,4 @@ func (route *TCPRoute) CloseListeners() {
|
||||
}
|
||||
route.listener.Close()
|
||||
route.listener = nil
|
||||
for _, pipe := range route.pipe {
|
||||
if err := pipe.Stop(); err != nil {
|
||||
route.l.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,43 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/yusing/go-proxy/utils"
|
||||
T "github.com/yusing/go-proxy/proxy/fields"
|
||||
U "github.com/yusing/go-proxy/utils"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
)
|
||||
|
||||
type UDPRoute struct {
|
||||
*StreamRoute
|
||||
type (
|
||||
UDPRoute struct {
|
||||
*StreamRoute
|
||||
|
||||
connMap UDPConnMap
|
||||
connMapMutex sync.Mutex
|
||||
connMap UDPConnMap
|
||||
|
||||
listeningConn *net.UDPConn
|
||||
targetAddr *net.UDPAddr
|
||||
}
|
||||
listeningConn *net.UDPConn
|
||||
targetAddr *net.UDPAddr
|
||||
}
|
||||
UDPConn struct {
|
||||
src *net.UDPConn
|
||||
dst *net.UDPConn
|
||||
U.BidirectionalPipe
|
||||
}
|
||||
UDPConnMap = F.Map[string, *UDPConn]
|
||||
)
|
||||
|
||||
type UDPConn struct {
|
||||
src *net.UDPConn
|
||||
dst *net.UDPConn
|
||||
*utils.BidirectionalPipe
|
||||
}
|
||||
|
||||
type UDPConnMap map[string]*UDPConn
|
||||
var NewUDPConnMap = F.NewMapOf[string, *UDPConn]
|
||||
|
||||
func NewUDPRoute(base *StreamRoute) StreamImpl {
|
||||
return &UDPRoute{
|
||||
StreamRoute: base,
|
||||
connMap: make(UDPConnMap),
|
||||
connMap: NewUDPConnMap(),
|
||||
}
|
||||
}
|
||||
|
||||
func (route *UDPRoute) Setup() error {
|
||||
laddr, err := net.ResolveUDPAddr(string(route.Scheme.ListeningScheme), fmt.Sprintf(":%v", route.Port.ProxyPort))
|
||||
laddr, err := net.ResolveUDPAddr(string(route.Scheme.ListeningScheme), fmt.Sprintf(":%v", route.Port.ListeningPort))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -50,8 +51,12 @@ func (route *UDPRoute) Setup() error {
|
||||
return err
|
||||
}
|
||||
|
||||
//! this read the allocated listeningPort from orginal ':0'
|
||||
route.Port.ListeningPort = T.Port(source.LocalAddr().(*net.UDPAddr).Port)
|
||||
|
||||
route.listeningConn = source
|
||||
route.targetAddr = raddr
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -70,33 +75,24 @@ func (route *UDPRoute) Accept() (any, error) {
|
||||
}
|
||||
|
||||
key := srcAddr.String()
|
||||
conn, ok := route.connMap[key]
|
||||
conn, ok := route.connMap.Load(key)
|
||||
|
||||
if !ok {
|
||||
route.connMapMutex.Lock()
|
||||
if conn, ok = route.connMap[key]; !ok {
|
||||
srcConn, err := net.DialUDP("udp", nil, srcAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dstConn, err := net.DialUDP("udp", nil, route.targetAddr)
|
||||
if err != nil {
|
||||
srcConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
pipeCtx, pipeCancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
<-route.stopCh
|
||||
pipeCancel()
|
||||
}()
|
||||
conn = &UDPConn{
|
||||
srcConn,
|
||||
dstConn,
|
||||
utils.NewBidirectionalPipe(pipeCtx, sourceRWCloser{in, dstConn}, sourceRWCloser{in, srcConn}),
|
||||
}
|
||||
route.connMap[key] = conn
|
||||
srcConn, err := net.DialUDP("udp", nil, srcAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
route.connMapMutex.Unlock()
|
||||
dstConn, err := net.DialUDP("udp", nil, route.targetAddr)
|
||||
if err != nil {
|
||||
srcConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
conn = &UDPConn{
|
||||
srcConn,
|
||||
dstConn,
|
||||
U.NewBidirectionalPipe(route.ctx, sourceRWCloser{in, dstConn}, sourceRWCloser{in, srcConn}),
|
||||
}
|
||||
route.connMap.Store(key, conn)
|
||||
}
|
||||
|
||||
_, err = conn.dst.Write(buffer[:nRead])
|
||||
@@ -112,15 +108,15 @@ func (route *UDPRoute) CloseListeners() {
|
||||
route.listeningConn.Close()
|
||||
route.listeningConn = nil
|
||||
}
|
||||
for _, conn := range route.connMap {
|
||||
route.connMap.RangeAll(func(_ string, conn *UDPConn) {
|
||||
if err := conn.src.Close(); err != nil {
|
||||
route.l.Errorf("error closing src conn: %s", err)
|
||||
}
|
||||
if err := conn.dst.Close(); err != nil {
|
||||
route.l.Error("error closing dst conn: %s", err)
|
||||
}
|
||||
}
|
||||
route.connMap = make(UDPConnMap)
|
||||
})
|
||||
route.connMap.Clear()
|
||||
}
|
||||
|
||||
type sourceRWCloser struct {
|
||||
|
||||
@@ -22,11 +22,9 @@ type server struct {
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Name string
|
||||
// port (with leading colon)
|
||||
HTTPPort string
|
||||
// port (with leading colon)
|
||||
HTTPSPort string
|
||||
Name string
|
||||
HTTPAddr string
|
||||
HTTPSAddr string
|
||||
CertProvider *autocert.Provider
|
||||
RedirectToHTTPS bool
|
||||
Handler http.Handler
|
||||
@@ -55,22 +53,22 @@ func NewServer(opt Options) (s *server) {
|
||||
certAvailable = err == nil
|
||||
}
|
||||
|
||||
if certAvailable && opt.RedirectToHTTPS && opt.HTTPSPort != "" {
|
||||
httpHandler = redirectToTLSHandler(opt.HTTPSPort)
|
||||
if certAvailable && opt.RedirectToHTTPS && opt.HTTPSAddr != "" {
|
||||
httpHandler = redirectToTLSHandler(opt.HTTPSAddr)
|
||||
} else {
|
||||
httpHandler = opt.Handler
|
||||
}
|
||||
|
||||
if opt.HTTPPort != "" {
|
||||
if opt.HTTPAddr != "" {
|
||||
httpSer = &http.Server{
|
||||
Addr: opt.HTTPPort,
|
||||
Addr: opt.HTTPAddr,
|
||||
Handler: httpHandler,
|
||||
ErrorLog: logger,
|
||||
}
|
||||
}
|
||||
if certAvailable && opt.HTTPSPort != "" {
|
||||
if certAvailable && opt.HTTPSAddr != "" {
|
||||
httpsSer = &http.Server{
|
||||
Addr: opt.HTTPSPort,
|
||||
Addr: opt.HTTPSAddr,
|
||||
Handler: opt.Handler,
|
||||
ErrorLog: logger,
|
||||
TLSConfig: &tls.Config{
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
func FileOK(p string) bool {
|
||||
_, err := os.Stat(p)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func FileName(p string) string {
|
||||
return path.Base(p)
|
||||
}
|
||||
@@ -3,9 +3,10 @@ package utils
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
@@ -16,15 +17,19 @@ type (
|
||||
Path string
|
||||
}
|
||||
|
||||
ReadCloser struct {
|
||||
ctx context.Context
|
||||
r io.ReadCloser
|
||||
closed atomic.Bool
|
||||
ContextReader struct {
|
||||
ctx context.Context
|
||||
io.Reader
|
||||
}
|
||||
|
||||
ContextWriter struct {
|
||||
ctx context.Context
|
||||
io.Writer
|
||||
}
|
||||
|
||||
Pipe struct {
|
||||
r ReadCloser
|
||||
w io.WriteCloser
|
||||
r ContextReader
|
||||
w ContextWriter
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
@@ -35,48 +40,48 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func (r *ReadCloser) Read(p []byte) (int, error) {
|
||||
func (r *ContextReader) Read(p []byte) (int, error) {
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
return 0, r.ctx.Err()
|
||||
default:
|
||||
return r.r.Read(p)
|
||||
return r.Reader.Read(p)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ReadCloser) Close() error {
|
||||
if r.closed.Load() {
|
||||
return nil
|
||||
func (w *ContextWriter) Write(p []byte) (int, error) {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return 0, w.ctx.Err()
|
||||
default:
|
||||
return w.Writer.Write(p)
|
||||
}
|
||||
r.closed.Store(true)
|
||||
return r.r.Close()
|
||||
}
|
||||
|
||||
func NewPipe(ctx context.Context, r io.ReadCloser, w io.WriteCloser) *Pipe {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
_, cancel := context.WithCancel(ctx)
|
||||
return &Pipe{
|
||||
r: ReadCloser{ctx: ctx, r: r},
|
||||
w: w,
|
||||
r: ContextReader{ctx: ctx, Reader: r},
|
||||
w: ContextWriter{ctx: ctx, Writer: w},
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pipe) Start() error {
|
||||
return Copy(p.ctx, p.w, &p.r)
|
||||
func (p *Pipe) Start() (err error) {
|
||||
err = Copy(&p.w, &p.r)
|
||||
switch {
|
||||
case
|
||||
// NOTE: ignoring broken pipe and connection reset by peer
|
||||
errors.Is(err, syscall.EPIPE),
|
||||
errors.Is(err, syscall.ECONNRESET):
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Pipe) Stop() error {
|
||||
p.cancel()
|
||||
return E.JoinE("error stopping pipe", p.r.Close(), p.w.Close()).Error()
|
||||
}
|
||||
|
||||
func (p *Pipe) Write(b []byte) (int, error) {
|
||||
return p.w.Write(b)
|
||||
}
|
||||
|
||||
func NewBidirectionalPipe(ctx context.Context, rw1 io.ReadWriteCloser, rw2 io.ReadWriteCloser) *BidirectionalPipe {
|
||||
return &BidirectionalPipe{
|
||||
func NewBidirectionalPipe(ctx context.Context, rw1 io.ReadWriteCloser, rw2 io.ReadWriteCloser) BidirectionalPipe {
|
||||
return BidirectionalPipe{
|
||||
pSrcDst: NewPipe(ctx, rw1, rw2),
|
||||
pDstSrc: NewPipe(ctx, rw2, rw1),
|
||||
}
|
||||
@@ -89,7 +94,7 @@ func NewBidirectionalPipeIntermediate(ctx context.Context, listener io.ReadClose
|
||||
}
|
||||
}
|
||||
|
||||
func (p *BidirectionalPipe) Start() error {
|
||||
func (p BidirectionalPipe) Start() error {
|
||||
errCh := make(chan error, 2)
|
||||
go func() {
|
||||
errCh <- p.pSrcDst.Start()
|
||||
@@ -97,20 +102,11 @@ func (p *BidirectionalPipe) Start() error {
|
||||
go func() {
|
||||
errCh <- p.pDstSrc.Start()
|
||||
}()
|
||||
for err := range errCh {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return E.JoinE("bidirectional pipe error", <-errCh, <-errCh).Error()
|
||||
}
|
||||
|
||||
func (p *BidirectionalPipe) Stop() error {
|
||||
return E.JoinE("error stopping pipe", p.pSrcDst.Stop(), p.pDstSrc.Stop()).Error()
|
||||
}
|
||||
|
||||
func Copy(ctx context.Context, dst io.WriteCloser, src io.ReadCloser) error {
|
||||
_, err := io.Copy(dst, &ReadCloser{ctx: ctx, r: src})
|
||||
func Copy(dst *ContextWriter, src *ContextReader) error {
|
||||
_, err := io.Copy(dst, src)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
func snakeToPascal(s string) string {
|
||||
toHyphenCamel := http.CanonicalHeaderKey(strings.ReplaceAll(s, "_", "-"))
|
||||
return strings.ReplaceAll(toHyphenCamel, "-", "")
|
||||
}
|
||||
|
||||
func SetFieldFromSnake[T, VT any](obj *T, field string, value VT) E.NestedError {
|
||||
field = snakeToPascal(field)
|
||||
prop := reflect.ValueOf(obj).Elem().FieldByName(field)
|
||||
if prop.Kind() == 0 {
|
||||
return E.Invalid("field", field)
|
||||
}
|
||||
prop.Set(reflect.ValueOf(value))
|
||||
return nil
|
||||
}
|
||||
@@ -4,13 +4,10 @@ import (
|
||||
"github.com/santhosh-tekuri/jsonschema"
|
||||
)
|
||||
|
||||
var schemaCompiler = func() *jsonschema.Compiler {
|
||||
c := jsonschema.NewCompiler()
|
||||
c.Draft = jsonschema.Draft7
|
||||
return c
|
||||
}()
|
||||
|
||||
var schemaStorage = make(map[string]*jsonschema.Schema)
|
||||
var (
|
||||
schemaCompiler = jsonschema.NewCompiler()
|
||||
schemaStorage = make(map[string]*jsonschema.Schema)
|
||||
)
|
||||
|
||||
func GetSchema(path string) *jsonschema.Schema {
|
||||
if schema, ok := schemaStorage[path]; ok {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package utils
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CommaSeperatedList(s string) []string {
|
||||
res := strings.Split(s, ",")
|
||||
@@ -9,3 +13,11 @@ func CommaSeperatedList(s string) []string {
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func ExtractPort(fullURL string) (int, error) {
|
||||
url, err := url.Parse(fullURL)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.Atoi(url.Port())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
@@ -9,6 +10,23 @@ func ExpectNoError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err != nil && !reflect.ValueOf(err).IsNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectError(t *testing.T, expected error, err error) {
|
||||
t.Helper()
|
||||
if !errors.Is(err, expected) {
|
||||
t.Errorf("expected err %s, got %s", expected.Error(), err.Error())
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectError2(t *testing.T, input any, expected error, err error) {
|
||||
t.Helper()
|
||||
if !errors.Is(err, expected) {
|
||||
t.Errorf("%v: expected err %s, got %s", input, expected.Error(), err.Error())
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +34,7 @@ func ExpectEqual[T comparable](t *testing.T, got T, want T) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("expected:\n%v, got\n%v", want, got)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,29 +42,34 @@ func ExpectDeepEqual[T any](t *testing.T, got T, want T) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("expected:\n%v, got\n%v", want, got)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectTrue(t *testing.T, got bool) {
|
||||
t.Helper()
|
||||
if !got {
|
||||
t.Errorf("expected true, got false")
|
||||
t.Error("expected true")
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectFalse(t *testing.T, got bool) {
|
||||
t.Helper()
|
||||
if got {
|
||||
t.Errorf("expected false, got true")
|
||||
t.Error("expected false")
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectType[T any](t *testing.T, got any) T {
|
||||
func ExpectType[T any](t *testing.T, got any) (_ T) {
|
||||
t.Helper()
|
||||
tExpect := reflect.TypeFor[T]()
|
||||
_, ok := got.(T)
|
||||
if !ok {
|
||||
t.Errorf("expected type %s, got %T", tExpect, got)
|
||||
t.Fatalf("expected type %s, got %s", tExpect, reflect.TypeOf(got).Elem())
|
||||
t.FailNow()
|
||||
return
|
||||
}
|
||||
return got.(T)
|
||||
}
|
||||
|
||||
@@ -2,74 +2,127 @@ package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/events"
|
||||
docker_events "github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/sirupsen/logrus"
|
||||
D "github.com/yusing/go-proxy/docker"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
. "github.com/yusing/go-proxy/watcher/event"
|
||||
"github.com/yusing/go-proxy/watcher/events"
|
||||
)
|
||||
|
||||
type DockerWatcher struct {
|
||||
host string
|
||||
type (
|
||||
DockerWatcher struct {
|
||||
host string
|
||||
client D.Client
|
||||
logrus.FieldLogger
|
||||
}
|
||||
DockerListOptions = docker_events.ListOptions
|
||||
)
|
||||
|
||||
// https://docs.docker.com/reference/api/engine/version/v1.47/#tag/System/operation/SystemPingHead
|
||||
var (
|
||||
DockerFilterContainer = filters.Arg("type", string(docker_events.ContainerEventType))
|
||||
DockerFilterStart = filters.Arg("event", string(docker_events.ActionStart))
|
||||
DockerFilterStop = filters.Arg("event", string(docker_events.ActionStop))
|
||||
DockerFilterDie = filters.Arg("event", string(docker_events.ActionDie))
|
||||
DockerFilterKill = filters.Arg("event", string(docker_events.ActionKill))
|
||||
DockerFilterPause = filters.Arg("event", string(docker_events.ActionPause))
|
||||
DockerFilterUnpause = filters.Arg("event", string(docker_events.ActionUnPause))
|
||||
|
||||
NewDockerFilter = filters.NewArgs
|
||||
|
||||
dockerWatcherRetryInterval = 3 * time.Second
|
||||
)
|
||||
|
||||
func DockerrFilterContainerName(name string) filters.KeyValuePair {
|
||||
return filters.Arg("container", name)
|
||||
}
|
||||
|
||||
func NewDockerWatcher(host string) *DockerWatcher {
|
||||
return &DockerWatcher{host: host}
|
||||
func NewDockerWatcher(host string) DockerWatcher {
|
||||
return DockerWatcher{
|
||||
host: host,
|
||||
FieldLogger: (logrus.
|
||||
WithField("module", "docker_watcher").
|
||||
WithField("host", host))}
|
||||
}
|
||||
|
||||
func (w *DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.NestedError) {
|
||||
func NewDockerWatcherWithClient(client D.Client) DockerWatcher {
|
||||
return DockerWatcher{
|
||||
client: client,
|
||||
FieldLogger: (logrus.
|
||||
WithField("module", "docker_watcher").
|
||||
WithField("host", client.DaemonHost()))}
|
||||
}
|
||||
|
||||
func (w DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.NestedError) {
|
||||
return w.EventsWithOptions(ctx, optionsWatchAll)
|
||||
}
|
||||
|
||||
func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerListOptions) (<-chan Event, <-chan E.NestedError) {
|
||||
eventCh := make(chan Event)
|
||||
errCh := make(chan E.NestedError)
|
||||
started := make(chan struct{})
|
||||
|
||||
eventsCtx, eventsCancel := context.WithCancel(ctx)
|
||||
|
||||
go func() {
|
||||
defer close(eventCh)
|
||||
defer close(errCh)
|
||||
|
||||
var cl D.Client
|
||||
var err E.NestedError
|
||||
for range 3 {
|
||||
cl, err = D.ConnectClient(w.host)
|
||||
if err.NoError() {
|
||||
break
|
||||
defer func() {
|
||||
if w.client.Connected() {
|
||||
w.client.Close()
|
||||
}
|
||||
errCh <- err
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
if err.HasError() {
|
||||
errCh <- E.Failure("connecting to docker")
|
||||
return
|
||||
}
|
||||
defer cl.Close()
|
||||
}()
|
||||
|
||||
cEventCh, cErrCh := cl.Events(ctx, dwOptions)
|
||||
started <- struct{}{}
|
||||
if !w.client.Connected() {
|
||||
var err E.NestedError
|
||||
attempts := 0
|
||||
for {
|
||||
w.client, err = D.ConnectClient(w.host)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
attempts++
|
||||
errCh <- E.FailWith(fmt.Sprintf("docker connection attempt #%d", attempts), err)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
time.Sleep(dockerWatcherRetryInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.Debugf("client connected")
|
||||
|
||||
cEventCh, cErrCh := w.client.Events(eventsCtx, options)
|
||||
|
||||
w.Debugf("watcher started")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if err := <-cErrCh; err != nil {
|
||||
errCh <- E.From(err)
|
||||
if err := E.From(ctx.Err()); err != nil && err.IsNot(context.Canceled) {
|
||||
errCh <- err
|
||||
}
|
||||
return
|
||||
case msg := <-cEventCh:
|
||||
var Action Action
|
||||
switch msg.Action {
|
||||
case events.ActionStart:
|
||||
Action = ActionCreated
|
||||
case events.ActionDie:
|
||||
Action = ActionStopped
|
||||
default: // NOTE: should not happen
|
||||
Action = ActionModified
|
||||
action, ok := events.DockerEventMap[msg.Action]
|
||||
if !ok {
|
||||
w.Debugf("ignored unknown docker event: %s for container %s", msg.Action, msg.Actor.Attributes["name"])
|
||||
continue
|
||||
}
|
||||
eventCh <- Event{
|
||||
Type: EventTypeDocker,
|
||||
event := Event{
|
||||
Type: events.EventTypeDocker,
|
||||
ActorID: msg.Actor.ID,
|
||||
ActorAttributes: msg.Actor.Attributes, // labels
|
||||
ActorName: msg.Actor.Attributes["name"],
|
||||
Action: Action,
|
||||
Action: action,
|
||||
}
|
||||
eventCh <- event
|
||||
case err := <-cErrCh:
|
||||
if err == nil {
|
||||
continue
|
||||
@@ -79,21 +132,21 @@ func (w *DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Nest
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
if D.IsErrConnectionFailed(err) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
cEventCh, cErrCh = cl.Events(ctx, dwOptions)
|
||||
}
|
||||
eventsCancel()
|
||||
time.Sleep(dockerWatcherRetryInterval)
|
||||
eventsCtx, eventsCancel = context.WithCancel(ctx)
|
||||
cEventCh, cErrCh = w.client.Events(ctx, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
<-started
|
||||
|
||||
return eventCh, errCh
|
||||
}
|
||||
|
||||
var dwOptions = events.ListOptions{Filters: filters.NewArgs(
|
||||
filters.Arg("type", string(events.ContainerEventType)),
|
||||
filters.Arg("event", string(events.ActionStart)),
|
||||
filters.Arg("event", string(events.ActionDie)), // 'stop' already triggering 'die'
|
||||
var optionsWatchAll = DockerListOptions{Filters: NewDockerFilter(
|
||||
DockerFilterContainer,
|
||||
DockerFilterStart,
|
||||
DockerFilterStop,
|
||||
DockerFilterDie,
|
||||
)}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package event
|
||||
|
||||
import "fmt"
|
||||
|
||||
type (
|
||||
Event struct {
|
||||
Type EventType
|
||||
ActorName string
|
||||
ActorID string
|
||||
ActorAttributes map[string]string
|
||||
Action Action
|
||||
}
|
||||
Action string
|
||||
EventType string
|
||||
)
|
||||
|
||||
const (
|
||||
ActionModified Action = "modified"
|
||||
ActionCreated Action = "created"
|
||||
ActionStarted Action = "started"
|
||||
ActionDeleted Action = "deleted"
|
||||
ActionStopped Action = "stopped"
|
||||
|
||||
EventTypeDocker EventType = "docker"
|
||||
EventTypeFile EventType = "file"
|
||||
)
|
||||
|
||||
func (e Event) String() string {
|
||||
return fmt.Sprintf("%s %s", e.ActorName, e.Action)
|
||||
}
|
||||
|
||||
func (a Action) IsDelete() bool {
|
||||
return a == ActionDeleted
|
||||
}
|
||||
77
src/watcher/events/events.go
Normal file
77
src/watcher/events/events.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
dockerEvents "github.com/docker/docker/api/types/events"
|
||||
)
|
||||
|
||||
type (
|
||||
Event struct {
|
||||
Type EventType
|
||||
ActorName string
|
||||
ActorID string
|
||||
ActorAttributes map[string]string
|
||||
Action Action
|
||||
}
|
||||
Action uint16
|
||||
EventType string
|
||||
)
|
||||
|
||||
const (
|
||||
ActionFileModified Action = (1 << iota)
|
||||
ActionFileCreated
|
||||
ActionFileDeleted
|
||||
|
||||
ActionContainerCreate
|
||||
ActionContainerStart
|
||||
ActionContainerUnpause
|
||||
|
||||
ActionContainerKill
|
||||
ActionContainerStop
|
||||
ActionContainerPause
|
||||
ActionContainerDie
|
||||
|
||||
actionContainerWakeMask = ActionContainerCreate | ActionContainerStart | ActionContainerUnpause
|
||||
actionContainerSleepMask = ActionContainerKill | ActionContainerStop | ActionContainerPause | ActionContainerDie
|
||||
)
|
||||
|
||||
const (
|
||||
EventTypeDocker EventType = "docker"
|
||||
EventTypeFile EventType = "file"
|
||||
)
|
||||
|
||||
var DockerEventMap = map[dockerEvents.Action]Action{
|
||||
dockerEvents.ActionCreate: ActionContainerCreate,
|
||||
dockerEvents.ActionStart: ActionContainerStart,
|
||||
dockerEvents.ActionUnPause: ActionContainerUnpause,
|
||||
|
||||
dockerEvents.ActionKill: ActionContainerKill,
|
||||
dockerEvents.ActionStop: ActionContainerStop,
|
||||
dockerEvents.ActionPause: ActionContainerPause,
|
||||
dockerEvents.ActionDie: ActionContainerDie,
|
||||
}
|
||||
|
||||
var dockerActionNameMap = func() (m map[Action]string) {
|
||||
m = make(map[Action]string, len(DockerEventMap))
|
||||
for k, v := range DockerEventMap {
|
||||
m[v] = string(k)
|
||||
}
|
||||
return
|
||||
}()
|
||||
|
||||
func (e Event) String() string {
|
||||
return fmt.Sprintf("%s %s", e.ActorName, e.Action)
|
||||
}
|
||||
|
||||
func (a Action) String() string {
|
||||
return dockerActionNameMap[a]
|
||||
}
|
||||
|
||||
func (a Action) IsContainerWake() bool {
|
||||
return a&actionContainerWakeMask != 0
|
||||
}
|
||||
|
||||
func (a Action) IsContainerSleep() bool {
|
||||
return a&actionContainerSleepMask != 0
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/yusing/go-proxy/common"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
. "github.com/yusing/go-proxy/watcher/event"
|
||||
)
|
||||
|
||||
type fileWatcher struct {
|
||||
@@ -21,7 +20,10 @@ func NewFileWatcher(filename string) Watcher {
|
||||
}
|
||||
|
||||
func (f *fileWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.NestedError) {
|
||||
if fwHelper == nil {
|
||||
fwHelper = newFileWatcherHelper(common.ConfigBasePath)
|
||||
}
|
||||
return fwHelper.Add(ctx, f)
|
||||
}
|
||||
|
||||
var fwHelper = newFileWatcherHelper(common.ConfigBasePath)
|
||||
var fwHelper *fileWatcherHelper
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/sirupsen/logrus"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
. "github.com/yusing/go-proxy/watcher/event"
|
||||
"github.com/yusing/go-proxy/watcher/events"
|
||||
)
|
||||
|
||||
type fileWatcherHelper struct {
|
||||
@@ -81,30 +81,30 @@ func (h *fileWatcherHelper) start() {
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-h.w.Events:
|
||||
case fsEvent, ok := <-h.w.Events:
|
||||
if !ok {
|
||||
// closed manually?
|
||||
fsLogger.Error("channel closed")
|
||||
return
|
||||
}
|
||||
// retrieve the watcher
|
||||
w, ok := h.m[path.Base(event.Name)]
|
||||
w, ok := h.m[path.Base(fsEvent.Name)]
|
||||
if !ok {
|
||||
// watcher for this file does not exist
|
||||
continue
|
||||
}
|
||||
|
||||
msg := Event{
|
||||
Type: EventTypeFile,
|
||||
Type: events.EventTypeFile,
|
||||
ActorName: w.filename,
|
||||
}
|
||||
switch {
|
||||
case event.Has(fsnotify.Create):
|
||||
msg.Action = ActionCreated
|
||||
case event.Has(fsnotify.Write):
|
||||
msg.Action = ActionModified
|
||||
case event.Has(fsnotify.Remove), event.Has(fsnotify.Rename):
|
||||
msg.Action = ActionDeleted
|
||||
case fsEvent.Has(fsnotify.Create):
|
||||
msg.Action = events.ActionFileCreated
|
||||
case fsEvent.Has(fsnotify.Write):
|
||||
msg.Action = events.ActionFileModified
|
||||
case fsEvent.Has(fsnotify.Remove), fsEvent.Has(fsnotify.Rename):
|
||||
msg.Action = events.ActionFileDeleted
|
||||
default: // ignore other events
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"context"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
. "github.com/yusing/go-proxy/watcher/event"
|
||||
"github.com/yusing/go-proxy/watcher/events"
|
||||
)
|
||||
|
||||
type Event = events.Event
|
||||
|
||||
type Watcher interface {
|
||||
Events(ctx context.Context) (<-chan Event, <-chan E.NestedError)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user