mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-08 10:23:54 +02:00
Compare commits
96 Commits
dev
...
compat-old
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15b9635ee1 | ||
|
|
33d512f550 | ||
|
|
c7a70f630a | ||
|
|
12a8b9a1b4 | ||
|
|
4c62a4b365 | ||
|
|
5c30f4a859 | ||
|
|
c6e4e83fcd | ||
|
|
c54741aab6 | ||
|
|
fb6b692f55 | ||
|
|
1319f72805 | ||
|
|
52511f1618 | ||
|
|
ca2cbd3332 | ||
|
|
57f7b72923 | ||
|
|
54b3008cce | ||
|
|
d9abd65471 | ||
|
|
d5b9d2e6bc | ||
|
|
ec9cab05c7 | ||
|
|
6ff1346675 | ||
|
|
a75441aa8a | ||
|
|
32971b45c3 | ||
|
|
4bc7ce09c7 | ||
|
|
b5946b34b8 | ||
|
|
442e4a0972 | ||
|
|
d1c79acf87 | ||
|
|
df70f7f63c | ||
|
|
6707143aaf | ||
|
|
742a86d079 | ||
|
|
45f856b8d1 | ||
|
|
044c7bf8a0 | ||
|
|
e7b19c472b | ||
|
|
1ace5e641d | ||
|
|
83976646db | ||
|
|
8e1d75abd6 | ||
|
|
5c341d4745 | ||
|
|
9f245a62f2 | ||
|
|
ecc2547eb1 | ||
|
|
e2a48fcff9 | ||
|
|
6b09f54793 | ||
|
|
7b5ad3ab5e | ||
|
|
48790b4756 | ||
|
|
727a3e9452 | ||
|
|
f8f1b54aaf | ||
|
|
cb8f405e76 | ||
|
|
51704829c6 | ||
|
|
a1cc1d844d | ||
|
|
633deb85ca | ||
|
|
ee1c375fd9 | ||
|
|
2713f5282e | ||
|
|
f70cca0d00 | ||
|
|
3adefe4202 | ||
|
|
966c873013 | ||
|
|
b8c61c37dc | ||
|
|
22582cd32f | ||
|
|
a46573cab3 | ||
|
|
64e380cc40 | ||
|
|
2f68c7c386 | ||
|
|
e3001a70ed | ||
|
|
ea6543b3f9 | ||
|
|
1793dd629f | ||
|
|
edf8c6ea32 | ||
|
|
60cdffcf3c | ||
|
|
717ed04b38 | ||
|
|
80a6b21ff9 | ||
|
|
e48c3f57dd | ||
|
|
39f427555f | ||
|
|
280e455198 | ||
|
|
082991694b | ||
|
|
4ec9d818bf | ||
|
|
258d8921ad | ||
|
|
2b8d416625 | ||
|
|
b4e9613efe | ||
|
|
25605208e4 | ||
|
|
29036bed18 | ||
|
|
6d78f14cc7 | ||
|
|
4730753cb3 | ||
|
|
4fa2ef41aa | ||
|
|
732376328c | ||
|
|
110fe4b0aa | ||
|
|
45ba8d447a | ||
|
|
108417ca9d | ||
|
|
bd1ff9731d | ||
|
|
235af71343 | ||
|
|
059306c93b | ||
|
|
d938e24cf5 | ||
|
|
ab1881d02e | ||
|
|
90a4922b79 | ||
|
|
2022a0db82 | ||
|
|
4f8bb40d3d | ||
|
|
8ea296c99f | ||
|
|
ccefaf003d | ||
|
|
61236e0ace | ||
|
|
6a89ab77c8 | ||
|
|
9f1c279698 | ||
|
|
89cbcfee8c | ||
|
|
2da4bbedbf | ||
|
|
d73272b8e0 |
20
.github/workflows/docker-image-compat.yml
vendored
Normal file
20
.github/workflows/docker-image-compat.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: Docker Image CI (compat)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- compat
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-compat:
|
||||||
|
uses: ./.github/workflows/docker-image.yml
|
||||||
|
with:
|
||||||
|
image_name: ${{ github.repository_owner }}/godoxy
|
||||||
|
tag: latest-compat
|
||||||
|
target: main
|
||||||
|
build-compat-agent:
|
||||||
|
uses: ./.github/workflows/docker-image.yml
|
||||||
|
with:
|
||||||
|
image_name: ${{ github.repository_owner }}/godoxy-agent
|
||||||
|
tag: latest-compat
|
||||||
|
target: agent
|
||||||
1
.github/workflows/docker-image-nightly.yml
vendored
1
.github/workflows/docker-image-nightly.yml
vendored
@@ -8,6 +8,7 @@ on:
|
|||||||
- "**" # matches every branch
|
- "**" # matches every branch
|
||||||
- "!dependabot/*"
|
- "!dependabot/*"
|
||||||
- "!main" # excludes main
|
- "!main" # excludes main
|
||||||
|
- "!compat" # excludes compat branch
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-nightly:
|
build-nightly:
|
||||||
|
|||||||
37
.github/workflows/docker-image.yml
vendored
37
.github/workflows/docker-image.yml
vendored
@@ -45,11 +45,37 @@ jobs:
|
|||||||
attestations: write
|
attestations: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout (for tag resolution)
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
platform=${{ matrix.platform }}
|
platform=${{ matrix.platform }}
|
||||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Compute VERSION for build
|
||||||
|
run: |
|
||||||
|
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
|
||||||
|
version="${GITHUB_REF_NAME}"
|
||||||
|
cache_variant="release"
|
||||||
|
elif [ "${GITHUB_REF_NAME}" = "main" ] || [ "${GITHUB_REF_NAME}" = "compat" ]; then
|
||||||
|
git fetch --tags origin main
|
||||||
|
version="$(git describe --tags --abbrev=0 origin/main 2>/dev/null || git describe --tags --abbrev=0 main 2>/dev/null || echo v0.0.0)"
|
||||||
|
cache_variant="${GITHUB_REF_NAME}"
|
||||||
|
else
|
||||||
|
version="v$(date -u +'%Y%m%d-%H%M')"
|
||||||
|
cache_variant="nightly"
|
||||||
|
fi
|
||||||
|
echo "VERSION_FOR_BUILD=$version" >> $GITHUB_ENV
|
||||||
|
echo "CACHE_VARIANT=$cache_variant" >> $GITHUB_ENV
|
||||||
|
if [ "${GITHUB_REF_TYPE}" = "branch" ]; then
|
||||||
|
echo "BRANCH_FOR_BUILD=${GITHUB_REF_NAME}" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "BRANCH_FOR_BUILD=" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
@@ -80,14 +106,15 @@ jobs:
|
|||||||
file: ${{ env.DOCKERFILE }}
|
file: ${{ env.DOCKERFILE }}
|
||||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ inputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,name=${{ env.REGISTRY }}/${{ inputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||||
cache-from: |
|
cache-from: |
|
||||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}
|
type=gha,scope=${{ github.workflow }}-${{ env.CACHE_VARIANT }}-${{ env.PLATFORM_PAIR }}
|
||||||
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }}
|
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.CACHE_VARIANT }}-${{ env.PLATFORM_PAIR }}
|
||||||
cache-to: |
|
cache-to: |
|
||||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max
|
type=gha,scope=${{ github.workflow }}-${{ env.CACHE_VARIANT }}-${{ env.PLATFORM_PAIR }},mode=max
|
||||||
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }},mode=max
|
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.CACHE_VARIANT }}-${{ env.PLATFORM_PAIR }},mode=max
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ github.ref_name }}
|
VERSION=${{ env.VERSION_FOR_BUILD }}
|
||||||
MAKE_ARGS=${{ env.MAKE_ARGS }}
|
MAKE_ARGS=${{ env.MAKE_ARGS }}
|
||||||
|
BRANCH=${{ env.BRANCH_FOR_BUILD }}
|
||||||
|
|
||||||
- name: Generate artifact attestation
|
- name: Generate artifact attestation
|
||||||
uses: actions/attest-build-provenance@v1
|
uses: actions/attest-build-provenance@v1
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -40,4 +40,8 @@ tsconfig.tsbuildinfo
|
|||||||
|
|
||||||
!agent.compose.yml
|
!agent.compose.yml
|
||||||
!agent/pkg/**
|
!agent/pkg/**
|
||||||
dev-data/
|
dev-data/
|
||||||
|
|
||||||
|
RELEASE_NOTES.md
|
||||||
|
CLAUDE.md
|
||||||
|
.kilocode/**
|
||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -7,3 +7,6 @@
|
|||||||
[submodule "goutils"]
|
[submodule "goutils"]
|
||||||
path = goutils
|
path = goutils
|
||||||
url = https://github.com/yusing/goutils.git
|
url = https://github.com/yusing/goutils.git
|
||||||
|
[submodule "internal/go-proxmox"]
|
||||||
|
path = internal/go-proxmox
|
||||||
|
url = https://github.com/yusing/go-proxmox
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ WORKDIR /src
|
|||||||
COPY goutils/go.mod goutils/go.sum ./goutils/
|
COPY goutils/go.mod goutils/go.sum ./goutils/
|
||||||
COPY internal/go-oidc/go.mod internal/go-oidc/go.sum ./internal/go-oidc/
|
COPY internal/go-oidc/go.mod internal/go-oidc/go.sum ./internal/go-oidc/
|
||||||
COPY internal/gopsutil/go.mod internal/gopsutil/go.sum ./internal/gopsutil/
|
COPY internal/gopsutil/go.mod internal/gopsutil/go.sum ./internal/gopsutil/
|
||||||
|
COPY internal/go-proxmox/go.mod internal/go-proxmox/go.sum ./internal/go-proxmox/
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
# remove godoxy stuff from go.mod first
|
# remove godoxy stuff from go.mod first
|
||||||
@@ -43,6 +44,9 @@ ENV VERSION=${VERSION}
|
|||||||
ARG MAKE_ARGS
|
ARG MAKE_ARGS
|
||||||
ENV MAKE_ARGS=${MAKE_ARGS}
|
ENV MAKE_ARGS=${MAKE_ARGS}
|
||||||
|
|
||||||
|
ARG BRANCH
|
||||||
|
ENV BRANCH=${BRANCH}
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
--mount=type=cache,target=/root/go/pkg/mod \
|
--mount=type=cache,target=/root/go/pkg/mod \
|
||||||
make ${MAKE_ARGS} docker=1 build
|
make ${MAKE_ARGS} docker=1 build
|
||||||
|
|||||||
15
Makefile
15
Makefile
@@ -1,5 +1,6 @@
|
|||||||
shell := /bin/sh
|
shell := /bin/sh
|
||||||
export VERSION ?= $(shell git describe --tags --abbrev=0)
|
export VERSION ?= $(shell git describe --tags --abbrev=0)
|
||||||
|
export BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
|
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
|
||||||
export GOOS = linux
|
export GOOS = linux
|
||||||
|
|
||||||
@@ -8,7 +9,12 @@ REPO_URL ?= https://github.com/yusing/godoxy
|
|||||||
WEBUI_DIR ?= ../godoxy-webui
|
WEBUI_DIR ?= ../godoxy-webui
|
||||||
DOCS_DIR ?= ${WEBUI_DIR}/wiki
|
DOCS_DIR ?= ${WEBUI_DIR}/wiki
|
||||||
|
|
||||||
GO_TAGS = sonic
|
ifneq ($(BRANCH), compat)
|
||||||
|
GO_TAGS = sonic
|
||||||
|
else
|
||||||
|
GO_TAGS =
|
||||||
|
endif
|
||||||
|
|
||||||
LDFLAGS = -X github.com/yusing/goutils/version.version=${VERSION} -checklinkname=0
|
LDFLAGS = -X github.com/yusing/goutils/version.version=${VERSION} -checklinkname=0
|
||||||
|
|
||||||
ifeq ($(agent), 1)
|
ifeq ($(agent), 1)
|
||||||
@@ -86,7 +92,7 @@ docker-build-test:
|
|||||||
|
|
||||||
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2)
|
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2)
|
||||||
files := $(shell find . -name go.mod -type f -or -name Dockerfile -type f)
|
files := $(shell find . -name go.mod -type f -or -name Dockerfile -type f)
|
||||||
gomod_paths := $(shell find . -name go.mod -type f | xargs dirname)
|
gomod_paths := $(shell find . -name go.mod -type f | grep -vE '^./internal/(go-oidc|go-proxmox|gopsutil)/' | xargs dirname)
|
||||||
|
|
||||||
update-go:
|
update-go:
|
||||||
for file in ${files}; do \
|
for file in ${files}; do \
|
||||||
@@ -137,9 +143,6 @@ benchmark:
|
|||||||
dev-run: build
|
dev-run: build
|
||||||
cd dev-data && ${BIN_PATH}
|
cd dev-data && ${BIN_PATH}
|
||||||
|
|
||||||
mtrace:
|
|
||||||
${BIN_PATH} debug-ls-mtrace > mtrace.json
|
|
||||||
|
|
||||||
rapid-crash:
|
rapid-crash:
|
||||||
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
|
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
|
||||||
sleep 3 &&\
|
sleep 3 &&\
|
||||||
@@ -156,7 +159,7 @@ cloc:
|
|||||||
scc -w -i go --not-match '_test.go$$'
|
scc -w -i go --not-match '_test.go$$'
|
||||||
|
|
||||||
push-github:
|
push-github:
|
||||||
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
git push origin $(BRANCH)
|
||||||
|
|
||||||
gen-swagger:
|
gen-swagger:
|
||||||
# go install github.com/swaggo/swag/cmd/swag@latest
|
# go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
|
|||||||
61
README.md
61
README.md
@@ -33,6 +33,10 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
|||||||
- [Prerequisites](#prerequisites)
|
- [Prerequisites](#prerequisites)
|
||||||
- [Setup](#setup)
|
- [Setup](#setup)
|
||||||
- [How does GoDoxy work](#how-does-godoxy-work)
|
- [How does GoDoxy work](#how-does-godoxy-work)
|
||||||
|
- [Proxmox Integration](#proxmox-integration)
|
||||||
|
- [Automatic Route Binding](#automatic-route-binding)
|
||||||
|
- [WebUI Management](#webui-management)
|
||||||
|
- [API Endpoints](#api-endpoints)
|
||||||
- [Update / Uninstall system agent](#update--uninstall-system-agent)
|
- [Update / Uninstall system agent](#update--uninstall-system-agent)
|
||||||
- [Screenshots](#screenshots)
|
- [Screenshots](#screenshots)
|
||||||
- [idlesleeper](#idlesleeper)
|
- [idlesleeper](#idlesleeper)
|
||||||
@@ -67,7 +71,11 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
|||||||
- Podman
|
- Podman
|
||||||
- **Idle-sleep**: stop and wake containers based on traffic _(see [screenshots](#idlesleeper))_
|
- **Idle-sleep**: stop and wake containers based on traffic _(see [screenshots](#idlesleeper))_
|
||||||
- Docker containers
|
- Docker containers
|
||||||
- Proxmox LXCs
|
- Proxmox LXC containers
|
||||||
|
- **Proxmox Integration**
|
||||||
|
- **Automatic route binding**: Routes automatically bind to Proxmox nodes or LXC containers by matching hostname, IP, or alias
|
||||||
|
- **LXC lifecycle control**: Start, stop, restart containers directly from WebUI
|
||||||
|
- **Real-time logs**: Stream journalctl logs from nodes and LXC containers via WebSocket
|
||||||
- **Traffic Management**
|
- **Traffic Management**
|
||||||
- HTTP reserve proxy
|
- HTTP reserve proxy
|
||||||
- TCP/UDP port forwarding
|
- TCP/UDP port forwarding
|
||||||
@@ -80,7 +88,12 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
|||||||
- App Dashboard
|
- App Dashboard
|
||||||
- Config Editor
|
- Config Editor
|
||||||
- Uptime and System Metrics
|
- Uptime and System Metrics
|
||||||
- Docker Logs Viewer
|
- **Docker**
|
||||||
|
- Container lifecycle management (start, stop, restart)
|
||||||
|
- Real-time container logs via WebSocket
|
||||||
|
- **Proxmox**
|
||||||
|
- LXC container lifecycle management (start, stop, restart)
|
||||||
|
- Real-time node and LXC journalctl logs via WebSocket
|
||||||
- **Cross-Platform support**
|
- **Cross-Platform support**
|
||||||
- Supports **linux/amd64** and **linux/arm64**
|
- Supports **linux/amd64** and **linux/arm64**
|
||||||
- **Efficient and Performant**
|
- **Efficient and Performant**
|
||||||
@@ -128,6 +141,50 @@ Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
|
|||||||
>
|
>
|
||||||
> For example, with the label `proxy.aliases: qbt` you can access your app via `qbt.domain.com`.
|
> For example, with the label `proxy.aliases: qbt` you can access your app via `qbt.domain.com`.
|
||||||
|
|
||||||
|
## Proxmox Integration
|
||||||
|
|
||||||
|
GoDoxy can automatically discover and manage Proxmox nodes and LXC containers through configured providers.
|
||||||
|
|
||||||
|
### Automatic Route Binding
|
||||||
|
|
||||||
|
Routes are automatically linked to Proxmox resources through reverse lookup:
|
||||||
|
|
||||||
|
1. **Node-level routes** (VMID = 0): When hostname, IP, or alias matches a Proxmox node name or IP
|
||||||
|
2. **Container-level routes** (VMID > 0): When hostname, IP, or alias matches an LXC container
|
||||||
|
|
||||||
|
This enables seamless proxy configuration without manual binding:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
routes:
|
||||||
|
pve-node-01:
|
||||||
|
host: pve-node-01.internal
|
||||||
|
port: 8006
|
||||||
|
# Automatically links to Proxmox node pve-node-01
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebUI Management
|
||||||
|
|
||||||
|
From the WebUI, you can:
|
||||||
|
|
||||||
|
- **LXC Lifecycle Control**: Start, stop, restart containers
|
||||||
|
- **Node Logs**: Stream real-time journalctl output from nodes
|
||||||
|
- **LXC Logs**: Stream real-time journalctl output from containers
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
```http
|
||||||
|
# Node journalctl (WebSocket)
|
||||||
|
GET /api/v1/proxmox/journalctl/:node
|
||||||
|
|
||||||
|
# LXC journalctl (WebSocket)
|
||||||
|
GET /api/v1/proxmox/journalctl/:node/:vmid
|
||||||
|
|
||||||
|
# LXC lifecycle control
|
||||||
|
POST /api/v1/proxmox/lxc/:node/:vmid/start
|
||||||
|
POST /api/v1/proxmox/lxc/:node/:vmid/stop
|
||||||
|
POST /api/v1/proxmox/lxc/:node/:vmid/restart
|
||||||
|
```
|
||||||
|
|
||||||
## Update / Uninstall system agent
|
## Update / Uninstall system agent
|
||||||
|
|
||||||
Update:
|
Update:
|
||||||
|
|||||||
@@ -34,6 +34,10 @@
|
|||||||
- [安裝](#安裝)
|
- [安裝](#安裝)
|
||||||
- [手動安裝](#手動安裝)
|
- [手動安裝](#手動安裝)
|
||||||
- [資料夾結構](#資料夾結構)
|
- [資料夾結構](#資料夾結構)
|
||||||
|
- [Proxmox 整合](#proxmox-整合)
|
||||||
|
- [自動路由綁定](#自動路由綁定)
|
||||||
|
- [WebUI 管理](#webui-管理)
|
||||||
|
- [API 端點](#api-端點)
|
||||||
- [更新 / 卸載系統代理 (System Agent)](#更新--卸載系統代理-system-agent)
|
- [更新 / 卸載系統代理 (System Agent)](#更新--卸載系統代理-system-agent)
|
||||||
- [截圖](#截圖)
|
- [截圖](#截圖)
|
||||||
- [閒置休眠](#閒置休眠)
|
- [閒置休眠](#閒置休眠)
|
||||||
@@ -67,6 +71,10 @@
|
|||||||
- **閒置休眠**:根據流量停止和喚醒容器 _(參見[截圖](#閒置休眠))_
|
- **閒置休眠**:根據流量停止和喚醒容器 _(參見[截圖](#閒置休眠))_
|
||||||
- Docker 容器
|
- Docker 容器
|
||||||
- Proxmox LXC 容器
|
- Proxmox LXC 容器
|
||||||
|
- **Proxmox 整合**
|
||||||
|
- **自動路由綁定**:透過比對主機名稱、IP 或別名自動將路由綁定至 Proxmox 節點或 LXC 容器
|
||||||
|
- **LXC 生命週期控制**:可直接從 WebUI 啟動、停止、重新啟動容器
|
||||||
|
- **即時日誌**:透過 WebSocket 串流節點和 LXC 容器的 journalctl 日誌
|
||||||
- **流量管理**
|
- **流量管理**
|
||||||
- HTTP 反向代理
|
- HTTP 反向代理
|
||||||
- TCP/UDP 連接埠轉送
|
- TCP/UDP 連接埠轉送
|
||||||
@@ -79,7 +87,12 @@
|
|||||||
- 應用程式一覽
|
- 應用程式一覽
|
||||||
- 設定編輯器
|
- 設定編輯器
|
||||||
- 執行時間與系統指標
|
- 執行時間與系統指標
|
||||||
- Docker 日誌檢視器
|
- **Docker**
|
||||||
|
- 容器生命週期管理 (啟動、停止、重新啟動)
|
||||||
|
- 透過 WebSocket 即時串流容器日誌
|
||||||
|
- **Proxmox**
|
||||||
|
- LXC 容器生命週期管理 (啟動、停止、重新啟動)
|
||||||
|
- 透過 WebSocket 即時串流節點和 LXC 容器 journalctl 日誌
|
||||||
- **跨平台支援**
|
- **跨平台支援**
|
||||||
- 支援 **linux/amd64** 與 **linux/arm64**
|
- 支援 **linux/amd64** 與 **linux/arm64**
|
||||||
- **高效能**
|
- **高效能**
|
||||||
@@ -144,6 +157,50 @@
|
|||||||
└── .env
|
└── .env
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Proxmox 整合
|
||||||
|
|
||||||
|
GoDoxy 可透過配置的提供者自動探索和管理 Proxmox 節點和 LXC 容器。
|
||||||
|
|
||||||
|
### 自動路由綁定
|
||||||
|
|
||||||
|
路由透過反向查詢自動連結至 Proxmox 資源:
|
||||||
|
|
||||||
|
1. **節點級路由** (VMID = 0):當主機名稱、IP 或別名符合 Proxmox 節點名稱或 IP 時
|
||||||
|
2. **容器級路由** (VMID > 0):當主機名稱、IP 或別名符合 LXC 容器時
|
||||||
|
|
||||||
|
這可實現無需手動綁定的無縫代理配置:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
routes:
|
||||||
|
pve-node-01:
|
||||||
|
host: pve-node-01.internal
|
||||||
|
port: 8006
|
||||||
|
# 自動連結至 Proxmox 節點 pve-node-01
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebUI 管理
|
||||||
|
|
||||||
|
您可以從 WebUI:
|
||||||
|
|
||||||
|
- **LXC 生命週期控制**:啟動、停止、重新啟動容器
|
||||||
|
- **節點日誌**:串流來自節點的即時 journalctl 輸出
|
||||||
|
- **LXC 日誌**:串流來自容器的即時 journalctl 輸出
|
||||||
|
|
||||||
|
### API 端點
|
||||||
|
|
||||||
|
```http
|
||||||
|
# 節點 journalctl (WebSocket)
|
||||||
|
GET /api/v1/proxmox/journalctl/:node
|
||||||
|
|
||||||
|
# LXC journalctl (WebSocket)
|
||||||
|
GET /api/v1/proxmox/journalctl/:node/:vmid
|
||||||
|
|
||||||
|
# LXC 生命週期控制
|
||||||
|
POST /api/v1/proxmox/lxc/:node/:vmid/start
|
||||||
|
POST /api/v1/proxmox/lxc/:node/:vmid/stop
|
||||||
|
POST /api/v1/proxmox/lxc/:node/:vmid/restart
|
||||||
|
```
|
||||||
|
|
||||||
## 更新 / 卸載系統代理 (System Agent)
|
## 更新 / 卸載系統代理 (System Agent)
|
||||||
|
|
||||||
更新:
|
更新:
|
||||||
|
|||||||
25
agent/go.mod
25
agent/go.mod
@@ -14,15 +14,16 @@ replace (
|
|||||||
|
|
||||||
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
|
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
|
||||||
|
|
||||||
|
exclude github.com/yusing/godoxy/internal/utils v0.0.0-20250927032450-e2aeef3a863f
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.14.2
|
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/pion/dtls/v3 v3.0.10
|
github.com/pion/dtls/v3 v3.0.10
|
||||||
github.com/pion/transport/v3 v3.1.1
|
github.com/pion/transport/v3 v3.1.1
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/yusing/godoxy v0.24.1
|
github.com/yusing/godoxy v0.25.2
|
||||||
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
||||||
github.com/yusing/goutils v0.7.0
|
github.com/yusing/goutils v0.7.0
|
||||||
)
|
)
|
||||||
@@ -31,14 +32,16 @@ require (
|
|||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/cli v29.1.4+incompatible // indirect
|
github.com/docker/cli v29.2.0+incompatible // indirect
|
||||||
|
github.com/docker/docker v28.5.2+incompatible // indirect
|
||||||
github.com/docker/go-connections v0.6.0 // indirect
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/ebitengine/purego v0.9.1 // indirect
|
github.com/ebitengine/purego v0.9.1 // indirect
|
||||||
@@ -62,8 +65,9 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/moby/api v1.52.0 // indirect
|
github.com/moby/moby/api v1.53.0 // indirect
|
||||||
github.com/moby/moby/client v0.2.1 // indirect
|
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||||
|
github.com/moby/term v0.5.2 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
@@ -71,9 +75,10 @@ require (
|
|||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pion/logging v0.2.4 // indirect
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.3.0 // indirect
|
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
|
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
|
||||||
@@ -86,12 +91,13 @@ require (
|
|||||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||||
github.com/yusing/ds v0.4.1 // indirect
|
github.com/yusing/ds v0.4.1 // indirect
|
||||||
github.com/yusing/gointernals v0.1.16 // indirect
|
github.com/yusing/gointernals v0.1.16 // indirect
|
||||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260116021320-b12ef77f3743 // indirect
|
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468 // indirect
|
||||||
github.com/yusing/goutils/http/websocket v0.0.0-20260116021320-b12ef77f3743 // indirect
|
github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||||
golang.org/x/arch v0.23.0 // indirect
|
golang.org/x/arch v0.23.0 // indirect
|
||||||
@@ -99,6 +105,7 @@ require (
|
|||||||
golang.org/x/net v0.49.0 // indirect
|
golang.org/x/net v0.49.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
65
agent/go.sum
65
agent/go.sum
@@ -1,3 +1,5 @@
|
|||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||||
@@ -10,10 +12,10 @@ github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
|||||||
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
|
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
|
||||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
@@ -24,6 +26,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
|
|||||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
@@ -37,8 +41,10 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
|||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||||
github.com/docker/cli v29.1.4+incompatible h1:AI8fwZhqsAsrqZnVv9h6lbexeW/LzNTasf6A4vcNN8M=
|
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
|
||||||
github.com/docker/cli v29.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
|
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||||
|
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
@@ -82,8 +88,8 @@ github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU
|
|||||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
@@ -95,6 +101,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
|||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
|
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
|
||||||
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA=
|
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
||||||
@@ -124,19 +132,25 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
|||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
|
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||||
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
|
||||||
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||||
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
|
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||||
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||||
|
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
|
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||||
|
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||||
|
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
@@ -153,16 +167,17 @@ github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkY
|
|||||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
|
||||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.3.0 h1:w/bWkEJdYuRNYhHn5eXnIT8LzDM1O629X1I9MJSkD7Q=
|
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.3.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
@@ -220,6 +235,10 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGN
|
|||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
@@ -228,6 +247,8 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||||
@@ -258,6 +279,12 @@ golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
|||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -268,5 +295,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
|
||||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -16,6 +15,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||||
@@ -150,7 +150,7 @@ func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte)
|
|||||||
// test stream server connection
|
// test stream server connection
|
||||||
const fakeAddress = "localhost:8080" // it won't be used, just for testing
|
const fakeAddress = "localhost:8080" // it won't be used, just for testing
|
||||||
// test TCP stream support
|
// test TCP stream support
|
||||||
err := agentstream.TCPHealthCheck(cfg.Addr, cfg.caCert, cfg.clientCert)
|
err := agentstream.TCPHealthCheck(ctx, cfg.Addr, cfg.caCert, cfg.clientCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
streamUnsupportedErrs.Addf("failed to connect to stream server via TCP: %w", err)
|
streamUnsupportedErrs.Addf("failed to connect to stream server via TCP: %w", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -158,7 +158,7 @@ func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// test UDP stream support
|
// test UDP stream support
|
||||||
err = agentstream.UDPHealthCheck(cfg.Addr, cfg.caCert, cfg.clientCert)
|
err = agentstream.UDPHealthCheck(ctx, cfg.Addr, cfg.caCert, cfg.clientCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
streamUnsupportedErrs.Addf("failed to connect to stream server via UDP: %w", err)
|
streamUnsupportedErrs.Addf("failed to connect to stream server via UDP: %w", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -313,8 +313,18 @@ func (cfg *AgentConfig) do(ctx context.Context, method, endpoint string, body io
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timeout := 5 * time.Second
|
||||||
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
|
remaining := time.Until(deadline)
|
||||||
|
if remaining > 0 {
|
||||||
|
timeout = remaining
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
client := http.Client{
|
client := http.Client{
|
||||||
Transport: cfg.Transport(),
|
Transport: cfg.Transport(),
|
||||||
|
Timeout: timeout,
|
||||||
}
|
}
|
||||||
return client.Do(req)
|
return client.Do(req)
|
||||||
}
|
}
|
||||||
@@ -356,7 +366,7 @@ func (cfg *AgentConfig) fetchJSON(ctx context.Context, endpoint string, out any)
|
|||||||
return resp.StatusCode, nil
|
return resp.StatusCode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(data, out)
|
err = sonic.Unmarshal(data, out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package stream
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"net"
|
"net"
|
||||||
@@ -34,13 +35,13 @@ func NewTCPClient(serverAddr, targetAddress string, caCert *x509.Certificate, cl
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return newTCPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
return newTCPClientWIthHeader(context.Background(), serverAddr, header, caCert, clientCert)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TCPHealthCheck(serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
func TCPHealthCheck(ctx context.Context, serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
||||||
header := NewStreamHealthCheckHeader()
|
header := NewStreamHealthCheckHeader()
|
||||||
|
|
||||||
conn, err := newTCPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
conn, err := newTCPClientWIthHeader(ctx, serverAddr, header, caCert, clientCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -49,7 +50,7 @@ func TCPHealthCheck(serverAddr string, caCert *x509.Certificate, clientCert *tls
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTCPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
func newTCPClientWIthHeader(ctx context.Context, serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||||
// Setup TLS configuration
|
// Setup TLS configuration
|
||||||
caCertPool := x509.NewCertPool()
|
caCertPool := x509.NewCertPool()
|
||||||
caCertPool.AddCert(caCert)
|
caCertPool.AddCert(caCert)
|
||||||
@@ -62,17 +63,43 @@ func newTCPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCe
|
|||||||
ServerName: common.CertsDNSName,
|
ServerName: common.CertsDNSName,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialer := &net.Dialer{
|
||||||
|
Timeout: dialTimeout,
|
||||||
|
}
|
||||||
|
tlsDialer := &tls.Dialer{
|
||||||
|
NetDialer: dialer,
|
||||||
|
Config: tlsConfig,
|
||||||
|
}
|
||||||
|
|
||||||
// Establish TLS connection
|
// Establish TLS connection
|
||||||
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: dialTimeout}, "tcp", serverAddr, tlsConfig)
|
conn, err := tlsDialer.DialContext(ctx, "tcp", serverAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deadline, hasDeadline := ctx.Deadline()
|
||||||
|
if hasDeadline {
|
||||||
|
err := conn.SetWriteDeadline(deadline)
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
// Send the stream header once as a handshake.
|
// Send the stream header once as a handshake.
|
||||||
if _, err := conn.Write(header.Bytes()); err != nil {
|
if _, err := conn.Write(header.Bytes()); err != nil {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasDeadline {
|
||||||
|
// reset write deadline
|
||||||
|
err = conn.SetWriteDeadline(time.Time{})
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &TCPClient{
|
return &TCPClient{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func TestTCPHealthCheck(t *testing.T) {
|
|||||||
|
|
||||||
srv := startTCPServer(t, certs)
|
srv := startTCPServer(t, certs)
|
||||||
|
|
||||||
err := stream.TCPHealthCheck(srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
err := stream.TCPHealthCheck(t.Context(), srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
||||||
require.NoError(t, err, "health check")
|
require.NoError(t, err, "health check")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,6 +21,6 @@ func TestUDPHealthCheck(t *testing.T) {
|
|||||||
|
|
||||||
srv := startUDPServer(t, certs)
|
srv := startUDPServer(t, certs)
|
||||||
|
|
||||||
err := stream.UDPHealthCheck(srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
err := stream.UDPHealthCheck(t.Context(), srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
||||||
require.NoError(t, err, "health check")
|
require.NoError(t, err, "health check")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ func TestUDPServer_RejectInvalidClient(t *testing.T) {
|
|||||||
|
|
||||||
srv := startUDPServer(t, certs)
|
srv := startUDPServer(t, certs)
|
||||||
|
|
||||||
|
|
||||||
// Try to connect with a client cert from a different CA
|
// Try to connect with a client cert from a different CA
|
||||||
_, err = stream.NewUDPClient(srv.Addr.String(), dstAddr, certs.CaCert, invalidClientCert)
|
_, err = stream.NewUDPClient(srv.Addr.String(), dstAddr, certs.CaCert, invalidClientCert)
|
||||||
require.Error(t, err, "expected error when connecting with client cert from different CA")
|
require.Error(t, err, "expected error when connecting with client cert from different CA")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package stream
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"net"
|
"net"
|
||||||
@@ -35,10 +36,10 @@ func NewUDPClient(serverAddr, targetAddress string, caCert *x509.Certificate, cl
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return newUDPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
return newUDPClientWIthHeader(context.Background(), serverAddr, header, caCert, clientCert)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUDPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
func newUDPClientWIthHeader(ctx context.Context, serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||||
// Setup DTLS configuration
|
// Setup DTLS configuration
|
||||||
caCertPool := x509.NewCertPool()
|
caCertPool := x509.NewCertPool()
|
||||||
caCertPool.AddCert(caCert)
|
caCertPool.AddCert(caCert)
|
||||||
@@ -62,21 +63,40 @@ func newUDPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCe
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deadline, hasDeadline := ctx.Deadline()
|
||||||
|
if hasDeadline {
|
||||||
|
err := conn.SetWriteDeadline(deadline)
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Send the stream header once as a handshake.
|
// Send the stream header once as a handshake.
|
||||||
if _, err := conn.Write(header.Bytes()); err != nil {
|
if _, err := conn.Write(header.Bytes()); err != nil {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasDeadline {
|
||||||
|
// reset write deadline
|
||||||
|
err = conn.SetWriteDeadline(time.Time{})
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &UDPClient{
|
return &UDPClient{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UDPHealthCheck(serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
func UDPHealthCheck(ctx context.Context, serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
||||||
header := NewStreamHealthCheckHeader()
|
header := NewStreamHealthCheckHeader()
|
||||||
|
|
||||||
conn, err := newUDPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
conn, err := newUDPClientWIthHeader(ctx, serverAddr, header, caCert, clientCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ package agentproxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
route "github.com/yusing/godoxy/internal/route/types"
|
route "github.com/yusing/godoxy/internal/route/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ func proxyConfigFromHeaders(h http.Header) (cfg Config, err error) {
|
|||||||
return cfg, err
|
return cfg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = sonic.Unmarshal(cfgJSON, &cfg)
|
err = json.Unmarshal(cfgJSON, &cfg)
|
||||||
return cfg, err
|
return cfg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ func (cfg *Config) SetAgentProxyConfigHeadersLegacy(h http.Header) {
|
|||||||
func (cfg *Config) SetAgentProxyConfigHeaders(h http.Header) {
|
func (cfg *Config) SetAgentProxyConfigHeaders(h http.Header) {
|
||||||
h.Set(HeaderXProxyHost, cfg.Host)
|
h.Set(HeaderXProxyHost, cfg.Host)
|
||||||
h.Set(HeaderXProxyScheme, string(cfg.Scheme))
|
h.Set(HeaderXProxyScheme, string(cfg.Scheme))
|
||||||
cfgJSON, _ := sonic.Marshal(cfg.HTTPConfig)
|
cfgJSON, _ := json.Marshal(cfg.HTTPConfig)
|
||||||
cfgBase64 := base64.StdEncoding.EncodeToString(cfgJSON)
|
cfgBase64 := base64.StdEncoding.EncodeToString(cfgJSON)
|
||||||
h.Set(HeaderXProxyConfig, cfgBase64)
|
h.Set(HeaderXProxyConfig, cfgBase64)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -8,7 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
healthcheck "github.com/yusing/godoxy/internal/health/check"
|
healthcheck "github.com/yusing/godoxy/internal/health/check"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
)
|
)
|
||||||
@@ -73,7 +73,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
sonic.ConfigDefault.NewEncoder(w).Encode(result)
|
json.NewEncoder(w).Encode(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseMsOrDefault(msStr string) time.Duration {
|
func parseMsOrDefault(msStr string) time.Duration {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
@@ -51,7 +51,7 @@ func NewAgentHandler() http.Handler {
|
|||||||
Runtime: env.Runtime,
|
Runtime: env.Runtime,
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
sonic.ConfigDefault.NewEncoder(w).Encode(agentInfo)
|
json.NewEncoder(w).Encode(agentInfo)
|
||||||
})
|
})
|
||||||
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
|
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
|
||||||
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)
|
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)
|
||||||
|
|||||||
24
cmd/main.go
24
cmd/main.go
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/api"
|
"github.com/yusing/godoxy/internal/api"
|
||||||
@@ -32,6 +33,16 @@ func parallel(fns ...func()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
done := make(chan struct{}, 1)
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
case <-time.After(common.InitTimeout):
|
||||||
|
log.Fatal().Msgf("timeout waiting for initialization to complete, exiting...")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
initProfiling()
|
initProfiling()
|
||||||
|
|
||||||
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
|
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
|
||||||
@@ -69,14 +80,25 @@ func main() {
|
|||||||
server.StartServer(task.RootTask("api_server", false), server.Options{
|
server.StartServer(task.RootTask("api_server", false), server.Options{
|
||||||
Name: "api",
|
Name: "api",
|
||||||
HTTPAddr: common.APIHTTPAddr,
|
HTTPAddr: common.APIHTTPAddr,
|
||||||
Handler: api.NewHandler(),
|
Handler: api.NewHandler(true),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Local API Handler is used for unauthenticated access.
|
||||||
|
if common.LocalAPIHTTPAddr != "" {
|
||||||
|
server.StartServer(task.RootTask("local_api_server", false), server.Options{
|
||||||
|
Name: "local_api",
|
||||||
|
HTTPAddr: common.LocalAPIHTTPAddr,
|
||||||
|
Handler: api.NewHandler(false),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
listenDebugServer()
|
listenDebugServer()
|
||||||
|
|
||||||
uptime.Poller.Start()
|
uptime.Poller.Start()
|
||||||
config.WatchChanges()
|
config.WatchChanges()
|
||||||
|
|
||||||
|
close(done)
|
||||||
|
|
||||||
task.WaitExit(config.Value().TimeoutShutdown)
|
task.WaitExit(config.Value().TimeoutShutdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
57
go.mod
57
go.mod
@@ -4,6 +4,7 @@ go 1.25.6
|
|||||||
|
|
||||||
replace (
|
replace (
|
||||||
github.com/coreos/go-oidc/v3 => ./internal/go-oidc
|
github.com/coreos/go-oidc/v3 => ./internal/go-oidc
|
||||||
|
github.com/luthermonson/go-proxmox => ./internal/go-proxmox
|
||||||
github.com/shirou/gopsutil/v4 => ./internal/gopsutil
|
github.com/shirou/gopsutil/v4 => ./internal/gopsutil
|
||||||
github.com/yusing/godoxy/agent => ./agent
|
github.com/yusing/godoxy/agent => ./agent
|
||||||
github.com/yusing/godoxy/internal/dnsproviders => ./internal/dnsproviders
|
github.com/yusing/godoxy/internal/dnsproviders => ./internal/dnsproviders
|
||||||
@@ -13,9 +14,12 @@ replace (
|
|||||||
github.com/yusing/goutils/server => ./goutils/server
|
github.com/yusing/goutils/server => ./goutils/server
|
||||||
)
|
)
|
||||||
|
|
||||||
|
exclude github.com/luthermonson/go-proxmox v0.3.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon; modify_html middleware
|
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon; modify_html middleware
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
|
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
|
||||||
|
github.com/docker/docker v28.5.2+incompatible // docker daemon
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||||
github.com/gin-gonic/gin v1.11.0 // api server
|
github.com/gin-gonic/gin v1.11.0 // api server
|
||||||
github.com/go-acme/lego/v4 v4.31.0 // acme client
|
github.com/go-acme/lego/v4 v4.31.0 // acme client
|
||||||
@@ -24,8 +28,8 @@ require (
|
|||||||
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
||||||
github.com/gotify/server/v2 v2.8.0 // reference the Message struct for json response
|
github.com/gotify/server/v2 v2.8.0 // reference the Message struct for json response
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
||||||
github.com/pires/go-proxyproto v0.8.1 // proxy protocol support
|
github.com/pires/go-proxyproto v0.9.2 // proxy protocol support
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.3.0 // lock free map for concurrent operations
|
github.com/puzpuzpuz/xsync/v4 v4.4.0 // lock free map for concurrent operations
|
||||||
github.com/rs/zerolog v1.34.0 // logging
|
github.com/rs/zerolog v1.34.0 // logging
|
||||||
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
||||||
golang.org/x/crypto v0.47.0 // encrypting password with bcrypt
|
golang.org/x/crypto v0.47.0 // encrypting password with bcrypt
|
||||||
@@ -37,31 +41,29 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
|
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
|
||||||
github.com/bytedance/sonic v1.14.2 // fast json parsing
|
github.com/bytedance/sonic v1.15.0 // indirect; fast json parsing
|
||||||
github.com/docker/cli v29.1.4+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
github.com/docker/cli v29.2.0+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
||||||
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
|
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 // jwt authentication
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/luthermonson/go-proxmox v0.3.2 // proxmox API client
|
github.com/luthermonson/go-proxmox v0.3.2
|
||||||
github.com/moby/moby/api v1.52.0 // docker API
|
github.com/oschwald/maxminddb-golang v1.13.1
|
||||||
github.com/moby/moby/client v0.2.1 // docker client
|
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
|
|
||||||
github.com/quic-go/quic-go v0.59.0 // http3 support
|
github.com/quic-go/quic-go v0.59.0 // http3 support
|
||||||
github.com/shirou/gopsutil/v4 v4.25.12 // system information
|
github.com/shirou/gopsutil/v4 v4.25.12 // system information
|
||||||
github.com/spf13/afero v1.15.0 // afero for file system operations
|
github.com/spf13/afero v1.15.0 // afero for file system operations
|
||||||
github.com/stretchr/testify v1.11.1 // testing framework
|
github.com/stretchr/testify v1.11.1 // testing framework
|
||||||
github.com/valyala/fasthttp v1.69.0 // fast http for health check
|
github.com/valyala/fasthttp v1.69.0 // fast http for health check
|
||||||
github.com/yusing/ds v0.4.1 // data structures and algorithms
|
github.com/yusing/ds v0.4.1 // data structures and algorithms
|
||||||
github.com/yusing/godoxy/agent v0.0.0-20260116020954-edcde00dcc3a
|
github.com/yusing/godoxy/agent v0.0.0-20260129101716-0f13004ad6ba
|
||||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260116020954-edcde00dcc3a
|
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260129101716-0f13004ad6ba
|
||||||
github.com/yusing/gointernals v0.1.16
|
github.com/yusing/gointernals v0.1.16
|
||||||
github.com/yusing/goutils v0.7.0
|
github.com/yusing/goutils v0.7.0
|
||||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260116021320-b12ef77f3743
|
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468
|
||||||
github.com/yusing/goutils/http/websocket v0.0.0-20260116021320-b12ef77f3743
|
github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468
|
||||||
github.com/yusing/goutils/server v0.0.0-20260116021320-b12ef77f3743
|
github.com/yusing/goutils/server v0.0.0-20260129081554-24e52ede7468
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/auth v0.18.0 // indirect
|
cloud.google.com/go/auth v0.18.1 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
||||||
@@ -103,7 +105,7 @@ require (
|
|||||||
github.com/magefile/mage v1.15.0 // indirect
|
github.com/magefile/mage v1.15.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/miekg/dns v1.1.70 // indirect
|
github.com/miekg/dns v1.1.72 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
@@ -115,11 +117,11 @@ require (
|
|||||||
github.com/ovh/go-ovh v1.9.0 // indirect
|
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/samber/lo v1.52.0 // indirect
|
github.com/samber/lo v1.52.0 // indirect
|
||||||
github.com/samber/slog-common v0.19.0 // indirect
|
github.com/samber/slog-common v0.19.0 // indirect
|
||||||
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
|
||||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
github.com/sony/gobreaker v1.0.0 // indirect
|
github.com/sony/gobreaker v1.0.0 // indirect
|
||||||
@@ -135,8 +137,8 @@ require (
|
|||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
golang.org/x/tools v0.41.0 // indirect
|
golang.org/x/tools v0.41.0 // indirect
|
||||||
google.golang.org/api v0.260.0 // indirect
|
google.golang.org/api v0.263.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
||||||
google.golang.org/grpc v1.78.0 // indirect
|
google.golang.org/grpc v1.78.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||||
@@ -144,16 +146,20 @@ require (
|
|||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require github.com/moby/moby/api v1.53.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/boombuler/barcode v1.1.0 // indirect
|
github.com/boombuler/barcode v1.1.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/fatih/structs v1.1.0 // indirect
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
@@ -163,28 +169,33 @@ require (
|
|||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/google/go-querystring v1.2.0 // indirect
|
github.com/google/go-querystring v1.2.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||||
github.com/klauspost/compress v1.18.3 // indirect
|
github.com/klauspost/compress v1.18.3 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||||
github.com/linode/linodego v1.64.0 // indirect
|
github.com/linode/linodego v1.64.0 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||||
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/nrdcg/goinwx v0.12.0 // indirect
|
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.0 // indirect
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.0 // indirect
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||||
github.com/pion/dtls/v3 v3.0.10 // indirect
|
github.com/pion/dtls/v3 v3.0.10 // indirect
|
||||||
github.com/pion/logging v0.2.4 // indirect
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/pquerna/otp v1.5.0 // indirect
|
github.com/pquerna/otp v1.5.0 // indirect
|
||||||
|
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.3 // indirect
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/vultr/govultr/v3 v3.26.1 // indirect
|
github.com/vultr/govultr/v3 v3.26.1 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||||
golang.org/x/arch v0.23.0 // indirect
|
golang.org/x/arch v0.23.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
90
go.sum
90
go.sum
@@ -1,5 +1,5 @@
|
|||||||
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
|
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
|
||||||
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
|
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
@@ -23,6 +23,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourceg
|
|||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||||
@@ -51,10 +53,10 @@ github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
|||||||
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
|
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
|
||||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
@@ -65,6 +67,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
|
|||||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -76,8 +80,10 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
|||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||||
github.com/docker/cli v29.1.4+incompatible h1:AI8fwZhqsAsrqZnVv9h6lbexeW/LzNTasf6A4vcNN8M=
|
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
|
||||||
github.com/docker/cli v29.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
|
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||||
|
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
@@ -137,8 +143,8 @@ github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk
|
|||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
@@ -159,6 +165,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
|||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
|
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
|
||||||
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA=
|
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||||
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
||||||
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
||||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||||
@@ -197,8 +205,6 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8
|
|||||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
github.com/luthermonson/go-proxmox v0.3.2 h1:/zUg6FCl9cAABx0xU3OIgtDtClY0gVXxOCsrceDNylc=
|
|
||||||
github.com/luthermonson/go-proxmox v0.3.2/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
|
||||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
@@ -210,29 +216,35 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
|
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
|
||||||
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||||
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
|
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||||
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
|
||||||
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||||
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
|
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||||
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||||
|
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
|
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||||
|
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||||
|
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
|
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
|
||||||
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
||||||
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
||||||
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.0 h1:4MRzV6spwPHKct+4/ETqkEtr39Hq+0KvxhsgqbgQ2Bo=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 h1:eMzyN+jGJbxG4ut278uwIsUo9XacXc711lFjhKnaUso=
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.0 h1:RxraLVYX3eMUfQ1pDtJVvykEFGheky2YsrUt2HHRDcw=
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 h1:t34IpOa+8NfmjkU8bdWtYrLrmr346/FGhu8FlpJDQok=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.0/go.mod h1:JLMEKMX8IYPZ1TUSVHAVAbtnNSfP/I8OZQkAnfEMA0I=
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0/go.mod h1:p95/OxVsdx71I2Qrck1GtIS87sRxcTRKXzUi5nWm9NY=
|
||||||
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
||||||
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
@@ -253,10 +265,11 @@ github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
|||||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
|
||||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
|
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
|
||||||
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
||||||
@@ -267,8 +280,8 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
|
|||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.3.0 h1:w/bWkEJdYuRNYhHn5eXnIT8LzDM1O629X1I9MJSkD7Q=
|
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.3.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
@@ -314,8 +327,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||||
@@ -343,6 +356,10 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGN
|
|||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
@@ -351,6 +368,8 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||||
@@ -402,6 +421,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -449,14 +469,14 @@ golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/api v0.260.0 h1:XbNi5E6bOVEj/uLXQRlt6TKuEzMD7zvW/6tNwltE4P4=
|
google.golang.org/api v0.263.0 h1:UFs7qn8gInIdtk1ZA6eXRXp5JDAnS4x9VRsRVCeKdbk=
|
||||||
google.golang.org/api v0.260.0/go.mod h1:Shj1j0Phr/9sloYrKomICzdYgsSDImpTxME8rGLaZ/o=
|
google.golang.org/api v0.263.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8=
|
||||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 h1:C4WAdL+FbjnGlpp2S+HMVhBeCq2Lcib4xZqfPNF6OoQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
@@ -474,5 +494,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
|
||||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
|
||||||
|
|||||||
2
goutils
2
goutils
Submodule goutils updated: 326c1f1eb3...e5fba76994
@@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/puzpuzpuz/xsync/v4"
|
"github.com/puzpuzpuz/xsync/v4"
|
||||||
@@ -27,9 +26,9 @@ type Config struct {
|
|||||||
Log *accesslog.ACLLoggerConfig `json:"log"`
|
Log *accesslog.ACLLoggerConfig `json:"log"`
|
||||||
|
|
||||||
Notify struct {
|
Notify struct {
|
||||||
To []string `json:"to"` // list of notification providers
|
To []string `json:"to,omitempty"` // list of notification providers
|
||||||
Interval time.Duration `json:"interval"` // interval between notifications
|
Interval time.Duration `json:"interval,omitempty"` // interval between notifications
|
||||||
IncludeAllowed *bool `json:"include_allowed"` // default: false
|
IncludeAllowed *bool `json:"include_allowed,omitzero"` // default: false
|
||||||
} `json:"notify"`
|
} `json:"notify"`
|
||||||
|
|
||||||
config
|
config
|
||||||
@@ -75,8 +74,7 @@ type ipLog struct {
|
|||||||
allowed bool
|
allowed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// could be nil
|
type ContextKey struct{}
|
||||||
var ActiveConfig atomic.Pointer[Config]
|
|
||||||
|
|
||||||
const cacheTTL = 1 * time.Minute
|
const cacheTTL = 1 * time.Minute
|
||||||
|
|
||||||
@@ -108,7 +106,7 @@ func (c *Config) Validate() gperr.Error {
|
|||||||
c.allowLocal = true
|
c.allowLocal = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Notify.Interval < 0 {
|
if c.Notify.Interval <= 0 {
|
||||||
c.Notify.Interval = defaultNotifyInterval
|
c.Notify.Interval = defaultNotifyInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,16 +290,16 @@ func (c *Config) IPAllowed(ip net.IP) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
|
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
|
||||||
if c.Allow.Match(ipAndStr) {
|
|
||||||
c.logAndNotify(ipAndStr, true)
|
|
||||||
c.cacheRecord(ipAndStr, true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if c.Deny.Match(ipAndStr) {
|
if c.Deny.Match(ipAndStr) {
|
||||||
c.logAndNotify(ipAndStr, false)
|
c.logAndNotify(ipAndStr, false)
|
||||||
c.cacheRecord(ipAndStr, false)
|
c.cacheRecord(ipAndStr, false)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if c.Allow.Match(ipAndStr) {
|
||||||
|
c.logAndNotify(ipAndStr, true)
|
||||||
|
c.cacheRecord(ipAndStr, true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
c.logAndNotify(ipAndStr, c.defaultAllow)
|
c.logAndNotify(ipAndStr, c.defaultAllow)
|
||||||
c.cacheRecord(ipAndStr, c.defaultAllow)
|
c.cacheRecord(ipAndStr, c.defaultAllow)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package acl
|
package acl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ type MatcherFunc func(*maxmind.IPInfo) bool
|
|||||||
|
|
||||||
type Matcher struct {
|
type Matcher struct {
|
||||||
match MatcherFunc
|
match MatcherFunc
|
||||||
|
raw string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Matchers []Matcher
|
type Matchers []Matcher
|
||||||
@@ -46,6 +48,7 @@ func (matcher *Matcher) Parse(s string) error {
|
|||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return errSyntax
|
return errSyntax
|
||||||
}
|
}
|
||||||
|
matcher.raw = s
|
||||||
|
|
||||||
switch parts[0] {
|
switch parts[0] {
|
||||||
case MatcherTypeIP:
|
case MatcherTypeIP:
|
||||||
@@ -79,6 +82,18 @@ func (matchers Matchers) Match(ip *maxmind.IPInfo) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (matchers Matchers) MarshalText() ([]byte, error) {
|
||||||
|
if len(matchers) == 0 {
|
||||||
|
return []byte("[]"), nil
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for _, m := range matchers {
|
||||||
|
buf.WriteString(m.raw)
|
||||||
|
buf.WriteByte('\n')
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func matchIP(ip net.IP) MatcherFunc {
|
func matchIP(ip net.IP) MatcherFunc {
|
||||||
return func(ip2 *maxmind.IPInfo) bool {
|
return func(ip2 *maxmind.IPInfo) bool {
|
||||||
return ip.Equal(ip2.IP)
|
return ip.Equal(ip2.IP)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func newAgent(cfg *agent.AgentConfig) *Agent {
|
|||||||
AgentConfig: cfg,
|
AgentConfig: cfg,
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
},
|
},
|
||||||
fasthttpHcClient: &fasthttp.Client{
|
fasthttpHcClient: &fasthttp.Client{
|
||||||
DialTimeout: func(addr string, timeout time.Duration) (net.Conn, error) {
|
DialTimeout: func(addr string, timeout time.Duration) (net.Conn, error) {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package agentpool
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
@@ -63,7 +63,7 @@ func (cfg *Agent) DoHealthCheck(timeout time.Duration, query string) (ret Health
|
|||||||
ret.Detail = fmt.Sprintf("HTTP %d %s", status, resp.Body())
|
ret.Detail = fmt.Sprintf("HTTP %d %s", status, resp.Body())
|
||||||
return ret, nil
|
return ret, nil
|
||||||
} else {
|
} else {
|
||||||
err = sonic.Unmarshal(resp.Body(), &ret)
|
err = json.Unmarshal(resp.Body(), &ret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ret, err
|
return ret, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gin-gonic/gin/codec/json"
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
apiV1 "github.com/yusing/godoxy/internal/api/v1"
|
apiV1 "github.com/yusing/godoxy/internal/api/v1"
|
||||||
@@ -16,6 +14,7 @@ import (
|
|||||||
fileApi "github.com/yusing/godoxy/internal/api/v1/file"
|
fileApi "github.com/yusing/godoxy/internal/api/v1/file"
|
||||||
homepageApi "github.com/yusing/godoxy/internal/api/v1/homepage"
|
homepageApi "github.com/yusing/godoxy/internal/api/v1/homepage"
|
||||||
metricsApi "github.com/yusing/godoxy/internal/api/v1/metrics"
|
metricsApi "github.com/yusing/godoxy/internal/api/v1/metrics"
|
||||||
|
proxmoxApi "github.com/yusing/godoxy/internal/api/v1/proxmox"
|
||||||
routeApi "github.com/yusing/godoxy/internal/api/v1/route"
|
routeApi "github.com/yusing/godoxy/internal/api/v1/route"
|
||||||
"github.com/yusing/godoxy/internal/auth"
|
"github.com/yusing/godoxy/internal/auth"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
@@ -38,7 +37,7 @@ import (
|
|||||||
|
|
||||||
// @externalDocs.description GoDoxy Docs
|
// @externalDocs.description GoDoxy Docs
|
||||||
// @externalDocs.url https://docs.godoxy.dev
|
// @externalDocs.url https://docs.godoxy.dev
|
||||||
func NewHandler() *gin.Engine {
|
func NewHandler(requireAuth bool) *gin.Engine {
|
||||||
if !common.IsDebug {
|
if !common.IsDebug {
|
||||||
gin.SetMode("release")
|
gin.SetMode("release")
|
||||||
}
|
}
|
||||||
@@ -47,11 +46,9 @@ func NewHandler() *gin.Engine {
|
|||||||
r.Use(ErrorLoggingMiddleware())
|
r.Use(ErrorLoggingMiddleware())
|
||||||
r.Use(NoCache())
|
r.Use(NoCache())
|
||||||
|
|
||||||
log.Debug().Msg("gin codec json.API: " + reflect.TypeOf(json.API).Name())
|
|
||||||
|
|
||||||
r.GET("/api/v1/version", apiV1.Version)
|
r.GET("/api/v1/version", apiV1.Version)
|
||||||
|
|
||||||
if auth.IsEnabled() {
|
if auth.IsEnabled() && requireAuth {
|
||||||
v1Auth := r.Group("/api/v1/auth")
|
v1Auth := r.Group("/api/v1/auth")
|
||||||
{
|
{
|
||||||
v1Auth.HEAD("/check", authApi.Check)
|
v1Auth.HEAD("/check", authApi.Check)
|
||||||
@@ -64,7 +61,7 @@ func NewHandler() *gin.Engine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
v1 := r.Group("/api/v1")
|
v1 := r.Group("/api/v1")
|
||||||
if auth.IsEnabled() {
|
if auth.IsEnabled() && requireAuth {
|
||||||
v1.Use(AuthMiddleware())
|
v1.Use(AuthMiddleware())
|
||||||
}
|
}
|
||||||
if common.APISkipOriginCheck {
|
if common.APISkipOriginCheck {
|
||||||
@@ -85,6 +82,8 @@ func NewHandler() *gin.Engine {
|
|||||||
route.GET("/providers", routeApi.Providers)
|
route.GET("/providers", routeApi.Providers)
|
||||||
route.GET("/by_provider", routeApi.ByProvider)
|
route.GET("/by_provider", routeApi.ByProvider)
|
||||||
route.POST("/playground", routeApi.Playground)
|
route.POST("/playground", routeApi.Playground)
|
||||||
|
route.GET("/validate", routeApi.Validate) // websocket
|
||||||
|
route.POST("/validate", routeApi.Validate)
|
||||||
}
|
}
|
||||||
|
|
||||||
file := v1.Group("/file")
|
file := v1.Group("/file")
|
||||||
@@ -140,6 +139,21 @@ func NewHandler() *gin.Engine {
|
|||||||
docker.POST("/start", dockerApi.Start)
|
docker.POST("/start", dockerApi.Start)
|
||||||
docker.POST("/stop", dockerApi.Stop)
|
docker.POST("/stop", dockerApi.Stop)
|
||||||
docker.POST("/restart", dockerApi.Restart)
|
docker.POST("/restart", dockerApi.Restart)
|
||||||
|
docker.GET("/stats/:id", dockerApi.Stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxmox := v1.Group("/proxmox")
|
||||||
|
{
|
||||||
|
proxmox.GET("/tail", proxmoxApi.Tail)
|
||||||
|
proxmox.GET("/journalctl", proxmoxApi.Journalctl)
|
||||||
|
proxmox.GET("/journalctl/:node", proxmoxApi.Journalctl)
|
||||||
|
proxmox.GET("/journalctl/:node/:vmid", proxmoxApi.Journalctl)
|
||||||
|
proxmox.GET("/journalctl/:node/:vmid/:service", proxmoxApi.Journalctl)
|
||||||
|
proxmox.GET("/stats/:node", proxmoxApi.NodeStats)
|
||||||
|
proxmox.GET("/stats/:node/:vmid", proxmoxApi.VMStats)
|
||||||
|
proxmox.POST("/lxc/:node/:vmid/start", proxmoxApi.Start)
|
||||||
|
proxmox.POST("/lxc/:node/:vmid/stop", proxmoxApi.Stop)
|
||||||
|
proxmox.POST("/lxc/:node/:vmid/restart", proxmoxApi.Restart)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ Types are defined in `goutils/apitypes`:
|
|||||||
| `file` | Configuration file read/write operations |
|
| `file` | Configuration file read/write operations |
|
||||||
| `auth` | Authentication and session management |
|
| `auth` | Authentication and session management |
|
||||||
| `agent` | Remote agent creation and management |
|
| `agent` | Remote agent creation and management |
|
||||||
|
| `proxmox` | Proxmox API management and monitoring |
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -77,15 +78,16 @@ API listening address is configured with `GODOXY_API_ADDR` environment variable.
|
|||||||
|
|
||||||
### Internal Dependencies
|
### Internal Dependencies
|
||||||
|
|
||||||
| Package | Purpose |
|
| Package | Purpose |
|
||||||
| ----------------------- | --------------------------- |
|
| ----------------------- | ------------------------------------- |
|
||||||
| `internal/route/routes` | Route storage and iteration |
|
| `internal/route/routes` | Route storage and iteration |
|
||||||
| `internal/docker` | Docker client management |
|
| `internal/docker` | Docker client management |
|
||||||
| `internal/config` | Configuration access |
|
| `internal/config` | Configuration access |
|
||||||
| `internal/metrics` | System metrics collection |
|
| `internal/metrics` | System metrics collection |
|
||||||
| `internal/homepage` | Homepage item generation |
|
| `internal/homepage` | Homepage item generation |
|
||||||
| `internal/agentpool` | Remote agent management |
|
| `internal/agentpool` | Remote agent management |
|
||||||
| `internal/auth` | Authentication services |
|
| `internal/auth` | Authentication services |
|
||||||
|
| `internal/proxmox` | Proxmox API management and monitoring |
|
||||||
|
|
||||||
### External Dependencies
|
### External Dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package agentapi
|
package agentapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -36,6 +37,9 @@ type VerifyNewAgentRequest struct {
|
|||||||
// @Failure 500 {object} ErrorResponse
|
// @Failure 500 {object} ErrorResponse
|
||||||
// @Router /agent/verify [post]
|
// @Router /agent/verify [post]
|
||||||
func Verify(c *gin.Context) {
|
func Verify(c *gin.Context) {
|
||||||
|
// avoid timeout waiting for response headers
|
||||||
|
c.Status(http.StatusContinue)
|
||||||
|
|
||||||
var request VerifyNewAgentRequest
|
var request VerifyNewAgentRequest
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||||
@@ -60,7 +64,7 @@ func Verify(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nRoutesAdded, err := verifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
|
nRoutesAdded, err := verifyNewAgent(c.Request.Context(), request.Host, ca, client, request.ContainerRuntime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||||
return
|
return
|
||||||
@@ -82,7 +86,7 @@ func Verify(c *gin.Context) {
|
|||||||
|
|
||||||
var errAgentAlreadyExists = gperr.New("agent already exists")
|
var errAgentAlreadyExists = gperr.New("agent already exists")
|
||||||
|
|
||||||
func verifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
|
func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
|
||||||
var agentCfg agent.AgentConfig
|
var agentCfg agent.AgentConfig
|
||||||
agentCfg.Addr = host
|
agentCfg.Addr = host
|
||||||
agentCfg.Runtime = containerRuntime
|
agentCfg.Runtime = containerRuntime
|
||||||
@@ -99,7 +103,7 @@ func verifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, contain
|
|||||||
return 0, errAgentAlreadyExists
|
return 0, errAgentAlreadyExists
|
||||||
}
|
}
|
||||||
|
|
||||||
err := agentCfg.InitWithCerts(cfgState.Context(), ca.Cert, client.Cert, client.Key)
|
err := agentCfg.InitWithCerts(ctx, ca.Cert, client.Cert, client.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, gperr.Wrap(err, "failed to initialize agent config")
|
return 0, gperr.Wrap(err, "failed to initialize agent config")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
@@ -43,22 +42,22 @@ func GetContainer(c *gin.Context) {
|
|||||||
|
|
||||||
defer dockerClient.Close()
|
defer dockerClient.Close()
|
||||||
|
|
||||||
cont, err := dockerClient.ContainerInspect(c.Request.Context(), id, client.ContainerInspectOptions{})
|
cont, err := dockerClient.ContainerInspect(c.Request.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to inspect container"))
|
c.Error(apitypes.InternalServerError(err, "failed to inspect container"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var state ContainerState
|
var state ContainerState
|
||||||
if cont.Container.State != nil {
|
if cont.State != nil {
|
||||||
state = cont.Container.State.Status
|
state = cont.State.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &Container{
|
c.JSON(http.StatusOK, &Container{
|
||||||
Server: dockerCfg.URL,
|
Server: dockerCfg.URL,
|
||||||
Name: cont.Container.Name,
|
Name: cont.Name,
|
||||||
ID: cont.Container.ID,
|
ID: cont.ID,
|
||||||
Image: cont.Container.Image,
|
Image: cont.Image,
|
||||||
State: state,
|
State: state,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/moby/moby/api/types/container"
|
|
||||||
"github.com/moby/moby/client"
|
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
|
||||||
_ "github.com/yusing/goutils/apitypes"
|
_ "github.com/yusing/goutils/apitypes"
|
||||||
@@ -40,12 +39,12 @@ func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Containe
|
|||||||
errs := gperr.NewBuilder("failed to get containers")
|
errs := gperr.NewBuilder("failed to get containers")
|
||||||
containers := make([]Container, 0)
|
containers := make([]Container, 0)
|
||||||
for server, dockerClient := range dockerClients {
|
for server, dockerClient := range dockerClients {
|
||||||
conts, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{All: true})
|
conts, err := dockerClient.ContainerList(ctx, container.ListOptions{All: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.Add(err)
|
errs.Add(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, cont := range conts.Items {
|
for _, cont := range conts {
|
||||||
containers = append(containers, Container{
|
containers = append(containers, Container{
|
||||||
Server: server,
|
Server: server,
|
||||||
Name: cont.Names[0],
|
Name: cont.Names[0],
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
dockerSystem "github.com/docker/docker/api/types/system"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
dockerSystem "github.com/moby/moby/api/types/system"
|
|
||||||
"github.com/moby/moby/client"
|
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
|
|
||||||
@@ -65,13 +64,13 @@ func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerIn
|
|||||||
|
|
||||||
i := 0
|
i := 0
|
||||||
for name, dockerClient := range dockerClients {
|
for name, dockerClient := range dockerClients {
|
||||||
info, err := dockerClient.Info(ctx, client.InfoOptions{})
|
info, err := dockerClient.Info(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.Add(err)
|
errs.Add(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
info.Info.Name = name
|
info.Name = name
|
||||||
dockerInfos[i] = toDockerInfo(info.Info)
|
dockerInfos[i] = toDockerInfo(info)
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/moby/moby/api/pkg/stdcopy"
|
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
@@ -22,6 +23,7 @@ type LogsQueryParams struct {
|
|||||||
Since string `form:"from"`
|
Since string `form:"from"`
|
||||||
Until string `form:"to"`
|
Until string `form:"to"`
|
||||||
Levels string `form:"levels"`
|
Levels string `form:"levels"`
|
||||||
|
Limit int `form:"limit,default=100" binding:"min=1,max=1000"`
|
||||||
} // @name LogsQueryParams
|
} // @name LogsQueryParams
|
||||||
|
|
||||||
// @x-id "logs"
|
// @x-id "logs"
|
||||||
@@ -34,9 +36,10 @@ type LogsQueryParams struct {
|
|||||||
// @Param id path string true "container id"
|
// @Param id path string true "container id"
|
||||||
// @Param stdout query bool false "show stdout"
|
// @Param stdout query bool false "show stdout"
|
||||||
// @Param stderr query bool false "show stderr"
|
// @Param stderr query bool false "show stderr"
|
||||||
// @Param from query string false "from timestamp"
|
// @Param from query string false "from timestamp"
|
||||||
// @Param to query string false "to timestamp"
|
// @Param to query string false "to timestamp"
|
||||||
// @Param levels query string false "levels"
|
// @Param levels query string false "levels"
|
||||||
|
// @Param limit query int false "limit"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse
|
// @Failure 400 {object} apitypes.ErrorResponse
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
// @Failure 403 {object} apitypes.ErrorResponse
|
||||||
@@ -70,14 +73,14 @@ func Logs(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
defer dockerClient.Close()
|
defer dockerClient.Close()
|
||||||
|
|
||||||
opts := client.ContainerLogsOptions{
|
opts := container.LogsOptions{
|
||||||
ShowStdout: queryParams.Stdout,
|
ShowStdout: queryParams.Stdout,
|
||||||
ShowStderr: queryParams.Stderr,
|
ShowStderr: queryParams.Stderr,
|
||||||
Since: queryParams.Since,
|
Since: queryParams.Since,
|
||||||
Until: queryParams.Until,
|
Until: queryParams.Until,
|
||||||
Timestamps: true,
|
Timestamps: true,
|
||||||
Follow: true,
|
Follow: true,
|
||||||
Tail: "100",
|
Tail: strconv.Itoa(queryParams.Limit),
|
||||||
}
|
}
|
||||||
if queryParams.Levels != "" {
|
if queryParams.Levels != "" {
|
||||||
opts.Details = true
|
opts.Details = true
|
||||||
|
|||||||
@@ -4,23 +4,17 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RestartRequest struct {
|
|
||||||
ID string `json:"id" binding:"required"`
|
|
||||||
client.ContainerRestartOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// @x-id "restart"
|
// @x-id "restart"
|
||||||
// @BasePath /api/v1
|
// @BasePath /api/v1
|
||||||
// @Summary Restart container
|
// @Summary Restart container
|
||||||
// @Description Restart container by container id
|
// @Description Restart container by container id
|
||||||
// @Tags docker
|
// @Tags docker
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param request body RestartRequest true "Request"
|
// @Param request body StopRequest true "Request"
|
||||||
// @Success 200 {object} apitypes.SuccessResponse
|
// @Success 200 {object} apitypes.SuccessResponse
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
// @Failure 403 {object} apitypes.ErrorResponse
|
||||||
@@ -28,7 +22,7 @@ type RestartRequest struct {
|
|||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
// @Router /docker/restart [post]
|
// @Router /docker/restart [post]
|
||||||
func Restart(c *gin.Context) {
|
func Restart(c *gin.Context) {
|
||||||
var req RestartRequest
|
var req StopRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||||
return
|
return
|
||||||
@@ -48,7 +42,7 @@ func Restart(c *gin.Context) {
|
|||||||
|
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
_, err = client.ContainerRestart(c.Request.Context(), req.ID, req.ContainerRestartOptions)
|
err = client.ContainerRestart(c.Request.Context(), req.ID, req.StopOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to restart container"))
|
c.Error(apitypes.InternalServerError(err, "failed to restart container"))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ package dockerapi
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StartRequest struct {
|
type StartRequest struct {
|
||||||
ID string `json:"id" binding:"required"`
|
ID string `json:"id" binding:"required"`
|
||||||
client.ContainerStartOptions
|
container.StartOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
// @x-id "start"
|
// @x-id "start"
|
||||||
@@ -48,7 +48,7 @@ func Start(c *gin.Context) {
|
|||||||
|
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
_, err = client.ContainerStart(c.Request.Context(), req.ID, req.ContainerStartOptions)
|
err = client.ContainerStart(c.Request.Context(), req.ID, req.StartOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to start container"))
|
c.Error(apitypes.InternalServerError(err, "failed to start container"))
|
||||||
return
|
return
|
||||||
|
|||||||
116
internal/api/v1/docker/stats.go
Normal file
116
internal/api/v1/docker/stats.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package dockerapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/moby/moby/api/types/container"
|
||||||
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
|
"github.com/yusing/goutils/synk"
|
||||||
|
"github.com/yusing/goutils/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContainerStatsResponse container.StatsResponse // @name ContainerStatsResponse
|
||||||
|
|
||||||
|
// @x-id "stats"
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @Summary Get container stats
|
||||||
|
// @Description Get container stats by container id
|
||||||
|
// @Tags docker,websocket
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Container ID or route alias"
|
||||||
|
// @Success 200 {object} ContainerStatsResponse
|
||||||
|
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request: id is required or route is not a docker container"
|
||||||
|
// @Failure 403 {object} apitypes.ErrorResponse
|
||||||
|
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
|
||||||
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
|
// @Router /docker/stats/{id} [get]
|
||||||
|
func Stats(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, apitypes.Error("id is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
|
||||||
|
if !ok {
|
||||||
|
var route types.Route
|
||||||
|
route, ok = routes.GetIncludeExcluded(id)
|
||||||
|
if ok {
|
||||||
|
cont := route.ContainerInfo()
|
||||||
|
if cont == nil {
|
||||||
|
c.JSON(http.StatusBadRequest, apitypes.Error("route is not a docker container"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dockerCfg = cont.DockerCfg
|
||||||
|
id = cont.ContainerID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusNotFound, apitypes.Error("container or route not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dockerClient, err := docker.NewClient(dockerCfg)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dockerClient.Close()
|
||||||
|
|
||||||
|
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||||
|
stats, err := dockerClient.ContainerStats(c.Request.Context(), id, true)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to get container stats"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer stats.Body.Close()
|
||||||
|
|
||||||
|
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to create websocket manager"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer manager.Close()
|
||||||
|
|
||||||
|
buf := synk.GetSizedBytesPool().GetSized(4096)
|
||||||
|
defer synk.GetSizedBytesPool().Put(buf)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-manager.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
_, err = io.CopyBuffer(manager.NewWriter(websocket.TextMessage), stats.Body, buf)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) || errors.Is(err, task.ErrProgramExiting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to copy container stats"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := dockerClient.ContainerStats(c.Request.Context(), id, false)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to get container stats"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer stats.Body.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(c.Writer, stats.Body)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to copy container stats"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,15 +3,15 @@ package dockerapi
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StopRequest struct {
|
type StopRequest struct {
|
||||||
ID string `json:"id" binding:"required"`
|
ID string `json:"id" binding:"required"`
|
||||||
client.ContainerStopOptions
|
container.StopOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
// @x-id "stop"
|
// @x-id "stop"
|
||||||
@@ -48,7 +48,7 @@ func Stop(c *gin.Context) {
|
|||||||
|
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
_, err = client.ContainerStop(c.Request.Context(), req.ID, req.ContainerStopOptions)
|
err = client.ContainerStop(c.Request.Context(), req.ID, req.StopOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to stop container"))
|
c.Error(apitypes.InternalServerError(err, "failed to stop container"))
|
||||||
return
|
return
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ type ValidateFileRequest struct {
|
|||||||
// @Summary Validate file
|
// @Summary Validate file
|
||||||
// @Description Validate file
|
// @Description Validate file
|
||||||
// @Tags file
|
// @Tags file
|
||||||
// @Accept text/plain
|
// @Accept application/yaml
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param type query FileType true "Type"
|
// @Param type query FileType true "Type"
|
||||||
// @Param file body string true "File content"
|
// @Param file body string true "File content"
|
||||||
@@ -29,7 +29,7 @@ type ValidateFileRequest struct {
|
|||||||
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden"
|
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden"
|
||||||
// @Failure 417 {object} any "Validation failed"
|
// @Failure 417 {object} any "Validation failed"
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||||
// @Router /file/validate [post]
|
// @Router /file/validate [post]
|
||||||
func Validate(c *gin.Context) {
|
func Validate(c *gin.Context) {
|
||||||
var request ValidateFileRequest
|
var request ValidateFileRequest
|
||||||
if err := c.ShouldBindQuery(&request); err != nil {
|
if err := c.ShouldBindQuery(&request); err != nil {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
@@ -237,7 +236,7 @@ func marshalSystemInfo(ws *websocket.Manager, agentName string, systemInfo any)
|
|||||||
defer bufFromPool.release(bufFromPool.RawMessage)
|
defer bufFromPool.release(bufFromPool.RawMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := sonic.ConfigDefault.NewEncoder(buf).Encode(map[string]any{
|
err := json.NewEncoder(buf).Encode(map[string]any{
|
||||||
agentName: systemInfo,
|
agentName: systemInfo,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
6
internal/api/v1/proxmox/common.go
Normal file
6
internal/api/v1/proxmox/common.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package proxmoxapi
|
||||||
|
|
||||||
|
type ActionRequest struct {
|
||||||
|
Node string `uri:"node" binding:"required"`
|
||||||
|
VMID int `uri:"vmid" binding:"required"`
|
||||||
|
} // @name ProxmoxVMActionRequest
|
||||||
85
internal/api/v1/proxmox/journalctl.go
Normal file
85
internal/api/v1/proxmox/journalctl.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package proxmoxapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yusing/godoxy/internal/proxmox"
|
||||||
|
"github.com/yusing/goutils/apitypes"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
// e.g. ws://localhost:8889/api/v1/proxmox/journalctl?node=pve&vmid=127&service=pveproxy&service=pvedaemon&limit=10
|
||||||
|
// e.g. ws://localhost:8889/api/v1/proxmox/journalctl/pve/127?service=pveproxy&service=pvedaemon&limit=10
|
||||||
|
|
||||||
|
type JournalctlRequest struct {
|
||||||
|
Node string `form:"node" uri:"node" binding:"required"` // Node name
|
||||||
|
VMID *int `form:"vmid" uri:"vmid"` // Container VMID (optional - if not provided, streams node journalctl)
|
||||||
|
Services []string `form:"service" uri:"service"` // Service names
|
||||||
|
Limit *int `form:"limit" uri:"limit" default:"100" binding:"min=1,max=1000"` // Limit output lines (1-1000)
|
||||||
|
} // @name ProxmoxJournalctlRequest
|
||||||
|
|
||||||
|
// @x-id "journalctl"
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @Summary Get journalctl output
|
||||||
|
// @Description Get journalctl output for node or LXC container. If vmid is not provided, streams node journalctl.
|
||||||
|
// @Tags proxmox,websocket
|
||||||
|
// @Accept json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param query query JournalctlRequest true "Request"
|
||||||
|
// @Param path path JournalctlRequest true "Request"
|
||||||
|
// @Success 200 string plain "Journalctl output"
|
||||||
|
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||||
|
// @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
|
||||||
|
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||||
|
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||||
|
// @Router /proxmox/journalctl [get]
|
||||||
|
// @Router /proxmox/journalctl/{node} [get]
|
||||||
|
// @Router /proxmox/journalctl/{node}/{vmid} [get]
|
||||||
|
// @Router /proxmox/journalctl/{node}/{vmid}/{service} [get]
|
||||||
|
func Journalctl(c *gin.Context) {
|
||||||
|
var request JournalctlRequest
|
||||||
|
uriErr := c.ShouldBindUri(&request)
|
||||||
|
queryErr := c.ShouldBindQuery(&request)
|
||||||
|
if uriErr != nil && queryErr != nil { // allow both uri and query parameters to be set
|
||||||
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", errors.Join(uriErr, queryErr)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node, ok := proxmox.Nodes.Get(request.Node)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusContinue)
|
||||||
|
|
||||||
|
var reader io.ReadCloser
|
||||||
|
var err error
|
||||||
|
if request.VMID == nil {
|
||||||
|
reader, err = node.NodeJournalctl(c.Request.Context(), request.Services, *request.Limit)
|
||||||
|
} else {
|
||||||
|
reader, err = node.LXCJournalctl(c.Request.Context(), *request.VMID, request.Services, *request.Limit)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to get journalctl output"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer manager.Close()
|
||||||
|
|
||||||
|
writer := manager.NewWriter(websocket.TextMessage)
|
||||||
|
_, err = io.Copy(writer, reader)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to copy journalctl output"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
42
internal/api/v1/proxmox/restart.go
Normal file
42
internal/api/v1/proxmox/restart.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package proxmoxapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yusing/godoxy/internal/proxmox"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @x-id "lxcRestart"
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @Summary Restart LXC container
|
||||||
|
// @Description Restart LXC container by node and vmid
|
||||||
|
// @Tags proxmox
|
||||||
|
// @Produce json
|
||||||
|
// @Param path path ActionRequest true "Request"
|
||||||
|
// @Success 200 {object} apitypes.SuccessResponse
|
||||||
|
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||||
|
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||||
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
|
// @Router /proxmox/lxc/:node/:vmid/restart [post]
|
||||||
|
func Restart(c *gin.Context) {
|
||||||
|
var req ActionRequest
|
||||||
|
if err := c.ShouldBindUri(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node, ok := proxmox.Nodes.Get(req.Node)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := node.LXCAction(c.Request.Context(), req.VMID, proxmox.LXCReboot); err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to restart container"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, apitypes.Success("container restarted"))
|
||||||
|
}
|
||||||
42
internal/api/v1/proxmox/start.go
Normal file
42
internal/api/v1/proxmox/start.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package proxmoxapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yusing/godoxy/internal/proxmox"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @x-id "lxcStart"
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @Summary Start LXC container
|
||||||
|
// @Description Start LXC container by node and vmid
|
||||||
|
// @Tags proxmox
|
||||||
|
// @Produce json
|
||||||
|
// @Param path path ActionRequest true "Request"
|
||||||
|
// @Success 200 {object} apitypes.SuccessResponse
|
||||||
|
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||||
|
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||||
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
|
// @Router /proxmox/lxc/:node/:vmid/start [post]
|
||||||
|
func Start(c *gin.Context) {
|
||||||
|
var req ActionRequest
|
||||||
|
if err := c.ShouldBindUri(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node, ok := proxmox.Nodes.Get(req.Node)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := node.LXCAction(c.Request.Context(), req.VMID, proxmox.LXCStart); err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to start container"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, apitypes.Success("container started"))
|
||||||
|
}
|
||||||
139
internal/api/v1/proxmox/stats.go
Normal file
139
internal/api/v1/proxmox/stats.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package proxmoxapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yusing/godoxy/internal/proxmox"
|
||||||
|
"github.com/yusing/goutils/apitypes"
|
||||||
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatsRequest struct {
|
||||||
|
Node string `uri:"node" binding:"required"`
|
||||||
|
VMID int `uri:"vmid" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// @x-id "nodeStats"
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @Summary Get proxmox node stats
|
||||||
|
// @Description Get proxmox node stats in json
|
||||||
|
// @Tags proxmox,websocket
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param node path string true "Node name"
|
||||||
|
// @Success 200 {object} proxmox.NodeStats "Stats output"
|
||||||
|
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||||
|
// @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
|
||||||
|
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||||
|
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||||
|
// @Router /proxmox/stats/{node} [get]
|
||||||
|
func NodeStats(c *gin.Context) {
|
||||||
|
nodeName := c.Param("node")
|
||||||
|
if nodeName == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, apitypes.Error("node name is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node, ok := proxmox.Nodes.Get(nodeName)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isWs := httpheaders.IsWebsocket(c.Request.Header)
|
||||||
|
|
||||||
|
reader, err := node.NodeStats(c.Request.Context(), isWs)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to get stats"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
if !isWs {
|
||||||
|
var line [512]byte
|
||||||
|
n, err := reader.Read(line[:])
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to copy stats"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Data(http.StatusOK, "application/json", line[:n])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer manager.Close()
|
||||||
|
|
||||||
|
writer := manager.NewWriter(websocket.TextMessage)
|
||||||
|
_, err = io.Copy(writer, reader)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to copy stats"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @x-id "vmStats"
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @Summary Get proxmox VM stats
|
||||||
|
// @Description Get proxmox VM stats in format of "STATUS|CPU%%|MEM USAGE/LIMIT|MEM%%|NET I/O|BLOCK I/O"
|
||||||
|
// @Tags proxmox,websocket
|
||||||
|
// @Produce text/plain
|
||||||
|
// @Param path path StatsRequest true "Request"
|
||||||
|
// @Success 200 string plain "Stats output"
|
||||||
|
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||||
|
// @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
|
||||||
|
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||||
|
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||||
|
// @Router /proxmox/stats/{node}/{vmid} [get]
|
||||||
|
func VMStats(c *gin.Context) {
|
||||||
|
var request StatsRequest
|
||||||
|
if err := c.ShouldBindUri(&request); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node, ok := proxmox.Nodes.Get(request.Node)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isWs := httpheaders.IsWebsocket(c.Request.Header)
|
||||||
|
|
||||||
|
reader, err := node.LXCStats(c.Request.Context(), request.VMID, isWs)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to get stats"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
if !isWs {
|
||||||
|
var line [128]byte
|
||||||
|
n, err := reader.Read(line[:])
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to copy stats"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Data(http.StatusOK, "text/plain; charset=utf-8", line[:n])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer manager.Close()
|
||||||
|
|
||||||
|
writer := manager.NewWriter(websocket.TextMessage)
|
||||||
|
_, err = io.Copy(writer, reader)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to copy stats"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
42
internal/api/v1/proxmox/stop.go
Normal file
42
internal/api/v1/proxmox/stop.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package proxmoxapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yusing/godoxy/internal/proxmox"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @x-id "lxcStop"
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @Summary Stop LXC container
|
||||||
|
// @Description Stop LXC container by node and vmid
|
||||||
|
// @Tags proxmox
|
||||||
|
// @Produce json
|
||||||
|
// @Param path path ActionRequest true "Request"
|
||||||
|
// @Success 200 {object} apitypes.SuccessResponse
|
||||||
|
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||||
|
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||||
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
|
// @Router /proxmox/lxc/:node/:vmid/stop [post]
|
||||||
|
func Stop(c *gin.Context) {
|
||||||
|
var req ActionRequest
|
||||||
|
if err := c.ShouldBindUri(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node, ok := proxmox.Nodes.Get(req.Node)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := node.LXCAction(c.Request.Context(), req.VMID, proxmox.LXCShutdown); err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to stop container"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, apitypes.Success("container stopped"))
|
||||||
|
}
|
||||||
77
internal/api/v1/proxmox/tail.go
Normal file
77
internal/api/v1/proxmox/tail.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package proxmoxapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yusing/godoxy/internal/proxmox"
|
||||||
|
"github.com/yusing/goutils/apitypes"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
// e.g. ws://localhost:8889/api/v1/proxmox/tail?node=pve&vmid=127&file=/var/log/immich/web.log&file=/var/log/immich/ml.log&limit=10
|
||||||
|
|
||||||
|
type TailRequest struct {
|
||||||
|
Node string `form:"node" binding:"required"` // Node name
|
||||||
|
VMID *int `form:"vmid"` // Container VMID (optional - if not provided, streams node journalctl)
|
||||||
|
Files []string `form:"file" binding:"required,dive,filepath"` // File paths
|
||||||
|
Limit int `form:"limit" default:"100" binding:"min=1,max=1000"` // Limit output lines (1-1000)
|
||||||
|
} // @name ProxmoxTailRequest
|
||||||
|
|
||||||
|
// @x-id "tail"
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @Summary Get tail output
|
||||||
|
// @Description Get tail output for node or LXC container. If vmid is not provided, streams node tail.
|
||||||
|
// @Tags proxmox,websocket
|
||||||
|
// @Accept json
|
||||||
|
// @Produce application/json
|
||||||
|
// @Param query query TailRequest true "Request"
|
||||||
|
// @Success 200 string plain "Tail output"
|
||||||
|
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||||
|
// @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
|
||||||
|
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||||
|
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||||
|
// @Router /proxmox/tail [get]
|
||||||
|
func Tail(c *gin.Context) {
|
||||||
|
var request TailRequest
|
||||||
|
if err := c.ShouldBindQuery(&request); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node, ok := proxmox.Nodes.Get(request.Node)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusContinue)
|
||||||
|
|
||||||
|
var reader io.ReadCloser
|
||||||
|
var err error
|
||||||
|
if request.VMID == nil {
|
||||||
|
reader, err = node.NodeTail(c.Request.Context(), request.Files, request.Limit)
|
||||||
|
} else {
|
||||||
|
reader, err = node.LXCTail(c.Request.Context(), *request.VMID, request.Files, request.Limit)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to get journalctl output"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer manager.Close()
|
||||||
|
|
||||||
|
writer := manager.NewWriter(websocket.TextMessage)
|
||||||
|
_, err = io.Copy(writer, reader)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to copy journalctl output"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
statequery "github.com/yusing/godoxy/internal/config/query"
|
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
@@ -33,17 +32,10 @@ func Route(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
route, ok := routes.Get(request.Which)
|
route, ok := routes.GetIncludeExcluded(request.Which)
|
||||||
if ok {
|
if ok {
|
||||||
c.JSON(http.StatusOK, route)
|
c.JSON(http.StatusOK, route)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.JSON(http.StatusNotFound, apitypes.Error("route not found"))
|
||||||
// also search for excluded routes
|
|
||||||
route = statequery.SearchRoute(request.Which)
|
|
||||||
if route != nil {
|
|
||||||
c.JSON(http.StatusOK, route)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusNotFound, nil)
|
|
||||||
}
|
}
|
||||||
|
|||||||
69
internal/api/v1/route/validate.go
Normal file
69
internal/api/v1/route/validate.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package routeApi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
|
"github.com/yusing/godoxy/internal/route"
|
||||||
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type _ = route.Route
|
||||||
|
|
||||||
|
// @x-id "validate"
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @Summary Validate route
|
||||||
|
// @Description Validate route,
|
||||||
|
// @Tags route,websocket
|
||||||
|
// @Accept application/yaml
|
||||||
|
// @Produce json
|
||||||
|
// @Param route body route.Route true "Route"
|
||||||
|
// @Success 200 {object} apitypes.SuccessResponse "Route validated"
|
||||||
|
// @Failure 400 {object} apitypes.ErrorResponse "Bad request"
|
||||||
|
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden"
|
||||||
|
// @Failure 417 {object} any "Validation failed"
|
||||||
|
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||||
|
// @Router /route/validate [get]
|
||||||
|
// @Router /route/validate [post]
|
||||||
|
func Validate(c *gin.Context) {
|
||||||
|
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||||
|
ValidateWS(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var request route.Route
|
||||||
|
if err := c.ShouldBindWith(&request, serialization.GinYAMLBinding{}); err != nil {
|
||||||
|
c.JSON(http.StatusExpectationFailed, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, apitypes.Success("route validated"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateWS(c *gin.Context) {
|
||||||
|
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer manager.Close()
|
||||||
|
|
||||||
|
const writeTimeout = 5 * time.Second
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-manager.Done():
|
||||||
|
return
|
||||||
|
case msg := <-manager.ReadCh():
|
||||||
|
var request route.Route
|
||||||
|
if err := serialization.UnmarshalValidate(msg, &request, yaml.Unmarshal); err != nil {
|
||||||
|
manager.WriteJSON(gin.H{"error": err}, writeTimeout)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
manager.WriteJSON(gin.H{"message": "route validated"}, writeTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
@@ -109,7 +109,7 @@ type UserPassAuthCallbackRequest struct {
|
|||||||
|
|
||||||
func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
var creds UserPassAuthCallbackRequest
|
var creds UserPassAuthCallbackRequest
|
||||||
err := sonic.ConfigDefault.NewDecoder(r.Body).Decode(&creds)
|
err := json.NewDecoder(r.Body).Decode(&creds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import (
|
|||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/certcrypto"
|
"github.com/go-acme/lego/v4/certcrypto"
|
||||||
@@ -27,7 +30,7 @@ type Config struct {
|
|||||||
CertPath string `json:"cert_path,omitempty"`
|
CertPath string `json:"cert_path,omitempty"`
|
||||||
KeyPath string `json:"key_path,omitempty"`
|
KeyPath string `json:"key_path,omitempty"`
|
||||||
Extra []ConfigExtra `json:"extra,omitempty"`
|
Extra []ConfigExtra `json:"extra,omitempty"`
|
||||||
ACMEKeyPath string `json:"acme_key_path,omitempty"` // shared by all extra providers
|
ACMEKeyPath string `json:"acme_key_path,omitempty"` // shared by all extra providers with the same CA directory URL
|
||||||
Provider string `json:"provider,omitempty"`
|
Provider string `json:"provider,omitempty"`
|
||||||
Options map[string]strutils.Redacted `json:"options,omitempty"`
|
Options map[string]strutils.Redacted `json:"options,omitempty"`
|
||||||
|
|
||||||
@@ -88,7 +91,7 @@ func (cfg *Config) validate(seenPaths map[string]int) gperr.Error {
|
|||||||
cfg.KeyPath = KeyFileDefault
|
cfg.KeyPath = KeyFileDefault
|
||||||
}
|
}
|
||||||
if cfg.ACMEKeyPath == "" {
|
if cfg.ACMEKeyPath == "" {
|
||||||
cfg.ACMEKeyPath = ACMEKeyFileDefault
|
cfg.ACMEKeyPath = acmeKeyPath(cfg.CADirURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
b := gperr.NewBuilder("certificate error")
|
b := gperr.NewBuilder("certificate error")
|
||||||
@@ -272,3 +275,16 @@ func (cfg *Config) SaveACMEKey(key *ecdsa.PrivateKey) error {
|
|||||||
}
|
}
|
||||||
return os.WriteFile(cfg.ACMEKeyPath, data, 0o600)
|
return os.WriteFile(cfg.ACMEKeyPath, data, 0o600)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// acmeKeyPath returns the path to the ACME key file based on the CA directory URL.
|
||||||
|
// Different CA directory URLs will use different key files to avoid key conflicts.
|
||||||
|
func acmeKeyPath(caDirURL string) string {
|
||||||
|
// Use a hash of the CA directory URL to create a unique key filename
|
||||||
|
// Default to "acme" if no custom CA is configured (Let's Encrypt default)
|
||||||
|
filename := "acme"
|
||||||
|
if caDirURL != "" {
|
||||||
|
hash := sha256.Sum256([]byte(caDirURL))
|
||||||
|
filename = "acme_" + hex.EncodeToString(hash[:])[:16]
|
||||||
|
}
|
||||||
|
return filepath.Join(certBasePath, filename+".key")
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||||
@@ -25,9 +26,9 @@ func TestEABConfigRequired(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
yaml := fmt.Appendf(nil, "eab_kid: %s\neab_hmac: %s", test.cfg.EABKid, test.cfg.EABHmac)
|
yamlCfg := fmt.Appendf(nil, "eab_kid: %s\neab_hmac: %s", test.cfg.EABKid, test.cfg.EABHmac)
|
||||||
cfg := autocert.Config{}
|
cfg := autocert.Config{}
|
||||||
err := serialization.UnmarshalValidateYAML(yaml, &cfg)
|
err := serialization.UnmarshalValidate(yamlCfg, &cfg, yaml.Unmarshal)
|
||||||
if (err != nil) != test.wantErr {
|
if (err != nil) != test.wantErr {
|
||||||
t.Errorf("Validate() error = %v, wantErr %v", err, test.wantErr)
|
t.Errorf("Validate() error = %v, wantErr %v", err, test.wantErr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package autocert
|
package autocert
|
||||||
|
|
||||||
const (
|
const (
|
||||||
certBasePath = "certs/"
|
certBasePath = "certs/"
|
||||||
CertFileDefault = certBasePath + "cert.crt"
|
CertFileDefault = certBasePath + "cert.crt"
|
||||||
KeyFileDefault = certBasePath + "priv.key"
|
KeyFileDefault = certBasePath + "priv.key"
|
||||||
ACMEKeyFileDefault = certBasePath + "acme.key"
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -222,13 +222,14 @@ func (p *Provider) ObtainCertIfNotExistsAll() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := errs.Wait().Error()
|
||||||
p.rebuildSNIMatcher()
|
p.rebuildSNIMatcher()
|
||||||
return errs.Wait().Error()
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// obtainCertIfNotExists obtains a new certificate for this provider if it does not exist.
|
// obtainCertIfNotExists obtains a new certificate for this provider if it does not exist.
|
||||||
func (p *Provider) obtainCertIfNotExists() error {
|
func (p *Provider) obtainCertIfNotExists() error {
|
||||||
err := p.LoadCert()
|
err := p.loadCert()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -261,7 +262,10 @@ func (p *Provider) ObtainCertAll() error {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return errs.Wait().Error()
|
|
||||||
|
err := errs.Wait().Error()
|
||||||
|
p.rebuildSNIMatcher()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObtainCert renews existing certificate or obtains a new certificate for this provider.
|
// ObtainCert renews existing certificate or obtains a new certificate for this provider.
|
||||||
@@ -346,29 +350,32 @@ func (p *Provider) ObtainCert() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) LoadCert() error {
|
func (p *Provider) LoadCertAll() error {
|
||||||
var errs gperr.Builder
|
var errs gperr.Builder
|
||||||
|
for _, provider := range p.allProviders() {
|
||||||
|
if err := provider.loadCert(); err != nil {
|
||||||
|
errs.Add(provider.fmtError(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.rebuildSNIMatcher()
|
||||||
|
return errs.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) loadCert() error {
|
||||||
cert, err := tls.LoadX509KeyPair(p.cfg.CertPath, p.cfg.KeyPath)
|
cert, err := tls.LoadX509KeyPair(p.cfg.CertPath, p.cfg.KeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.Addf("load SSL certificate: %w", p.fmtError(err))
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
expiries, err := getCertExpiries(&cert)
|
expiries, err := getCertExpiries(&cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.Addf("parse SSL certificate: %w", p.fmtError(err))
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
p.tlsCert = &cert
|
p.tlsCert = &cert
|
||||||
p.certExpiries = expiries
|
p.certExpiries = expiries
|
||||||
|
|
||||||
for _, ep := range p.extraProviders {
|
return nil
|
||||||
if err := ep.LoadCert(); err != nil {
|
|
||||||
errs.Add(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p.rebuildSNIMatcher()
|
|
||||||
return errs.Error()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrintCertExpiriesAll prints the certificate expiries for this provider and all extra providers.
|
// PrintCertExpiriesAll prints the certificate expiries for this provider and all extra providers.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
@@ -41,7 +42,7 @@ func TestMultipleCertificatesLifecycle(t *testing.T) {
|
|||||||
cfg.HTTPClient = acmeServer.httpClient()
|
cfg.HTTPClient = acmeServer.httpClient()
|
||||||
|
|
||||||
/* unmarshal yaml config with multiple certs */
|
/* unmarshal yaml config with multiple certs */
|
||||||
err := error(serialization.UnmarshalValidateYAML(yamlConfig, &cfg))
|
err := error(serialization.UnmarshalValidate(yamlConfig, &cfg, yaml.Unmarshal))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, []string{"main.example.com"}, cfg.Domains)
|
require.Equal(t, []string{"main.example.com"}, cfg.Domains)
|
||||||
require.Len(t, cfg.Extra, 2)
|
require.Len(t, cfg.Extra, 2)
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func TestGetCertBySNI(t *testing.T) {
|
|||||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = p.LoadCert()
|
err = p.LoadCertAll()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "a.internal.example.com"})
|
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "a.internal.example.com"})
|
||||||
@@ -113,7 +113,7 @@ func TestGetCertBySNI(t *testing.T) {
|
|||||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = p.LoadCert()
|
err = p.LoadCertAll()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.example.com"})
|
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.example.com"})
|
||||||
@@ -145,7 +145,7 @@ func TestGetCertBySNI(t *testing.T) {
|
|||||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = p.LoadCert()
|
err = p.LoadCertAll()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "unknown.domain.com"})
|
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "unknown.domain.com"})
|
||||||
@@ -171,7 +171,7 @@ func TestGetCertBySNI(t *testing.T) {
|
|||||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = p.LoadCert()
|
err = p.LoadCertAll()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cert, err := p.GetCert(nil)
|
cert, err := p.GetCert(nil)
|
||||||
@@ -197,7 +197,7 @@ func TestGetCertBySNI(t *testing.T) {
|
|||||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = p.LoadCert()
|
err = p.LoadCertAll()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: ""})
|
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: ""})
|
||||||
@@ -229,7 +229,7 @@ func TestGetCertBySNI(t *testing.T) {
|
|||||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = p.LoadCert()
|
err = p.LoadCertAll()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "FOO.EXAMPLE.COM"})
|
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "FOO.EXAMPLE.COM"})
|
||||||
@@ -261,7 +261,7 @@ func TestGetCertBySNI(t *testing.T) {
|
|||||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = p.LoadCert()
|
err = p.LoadCertAll()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: " foo.example.com. "})
|
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: " foo.example.com. "})
|
||||||
@@ -293,7 +293,7 @@ func TestGetCertBySNI(t *testing.T) {
|
|||||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = p.LoadCert()
|
err = p.LoadCertAll()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.a.example.com"})
|
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.a.example.com"})
|
||||||
@@ -319,7 +319,7 @@ func TestGetCertBySNI(t *testing.T) {
|
|||||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = p.LoadCert()
|
err = p.LoadCertAll()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "bar.example.com"})
|
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "bar.example.com"})
|
||||||
@@ -355,7 +355,7 @@ func TestGetCertBySNI(t *testing.T) {
|
|||||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = p.LoadCert()
|
err = p.LoadCertAll()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cert1, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.test.com"})
|
cert1, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.test.com"})
|
||||||
@@ -392,7 +392,7 @@ func TestGetCertBySNI(t *testing.T) {
|
|||||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = p.LoadCert()
|
err = p.LoadCertAll()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cert1, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.example.com"})
|
cert1, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.example.com"})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package autocert_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||||
@@ -42,7 +43,7 @@ extra:
|
|||||||
`
|
`
|
||||||
|
|
||||||
var cfg autocert.Config
|
var cfg autocert.Config
|
||||||
err := error(serialization.UnmarshalValidateYAML([]byte(cfgYAML), &cfg))
|
err := error(serialization.UnmarshalValidate([]byte(cfgYAML), &cfg, yaml.Unmarshal))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test: extra[0] inherits all fields from main except CertPath and KeyPath.
|
// Test: extra[0] inherits all fields from main except CertPath and KeyPath.
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ var (
|
|||||||
IsDebug = env.GetEnvBool("DEBUG", IsTest)
|
IsDebug = env.GetEnvBool("DEBUG", IsTest)
|
||||||
IsTrace = env.GetEnvBool("TRACE", false) && IsDebug
|
IsTrace = env.GetEnvBool("TRACE", false) && IsDebug
|
||||||
|
|
||||||
|
InitTimeout = env.GetEnvDuation("INIT_TIMEOUT", 1*time.Minute)
|
||||||
|
|
||||||
ShortLinkPrefix = env.GetEnvString("SHORTLINK_PREFIX", "go")
|
ShortLinkPrefix = env.GetEnvString("SHORTLINK_PREFIX", "go")
|
||||||
|
|
||||||
ProxyHTTPAddr,
|
ProxyHTTPAddr,
|
||||||
@@ -30,6 +32,11 @@ var (
|
|||||||
APIHTTPPort,
|
APIHTTPPort,
|
||||||
APIHTTPURL = env.GetAddrEnv("API_ADDR", "127.0.0.1:8888", "http")
|
APIHTTPURL = env.GetAddrEnv("API_ADDR", "127.0.0.1:8888", "http")
|
||||||
|
|
||||||
|
LocalAPIHTTPAddr,
|
||||||
|
LocalAPIHTTPHost,
|
||||||
|
LocalAPIHTTPPort,
|
||||||
|
LocalAPIHTTPURL = env.GetAddrEnv("LOCAL_API_ADDR", "", "http")
|
||||||
|
|
||||||
APIJWTSecure = env.GetEnvBool("API_JWT_SECURE", true)
|
APIJWTSecure = env.GetEnvBool("API_JWT_SECURE", true)
|
||||||
APIJWTSecret = decodeJWTKey(env.GetEnvString("API_JWT_SECRET", ""))
|
APIJWTSecret = decodeJWTKey(env.GetEnvString("API_JWT_SECRET", ""))
|
||||||
APIJWTTokenTTL = env.GetEnvDuation("API_JWT_TOKEN_TTL", 24*time.Hour)
|
APIJWTTokenTTL = env.GetEnvDuation("API_JWT_TOKEN_TTL", 24*time.Hour)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
config "github.com/yusing/godoxy/internal/config/types"
|
||||||
"github.com/yusing/godoxy/internal/notif"
|
"github.com/yusing/godoxy/internal/notif"
|
||||||
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
"github.com/yusing/godoxy/internal/watcher"
|
"github.com/yusing/godoxy/internal/watcher"
|
||||||
"github.com/yusing/godoxy/internal/watcher/events"
|
"github.com/yusing/godoxy/internal/watcher/events"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
@@ -59,6 +60,15 @@ func Load() error {
|
|||||||
|
|
||||||
cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName)
|
cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName)
|
||||||
|
|
||||||
|
// disable pool logging temporary since we already have pretty logging
|
||||||
|
routes.HTTP.DisableLog(true)
|
||||||
|
routes.Stream.DisableLog(true)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
routes.HTTP.DisableLog(false)
|
||||||
|
routes.Stream.DisableLog(false)
|
||||||
|
}()
|
||||||
|
|
||||||
initErr := state.InitFromFile(common.ConfigPath)
|
initErr := state.InitFromFile(common.ConfigPath)
|
||||||
err := errors.Join(initErr, state.StartProviders())
|
err := errors.Join(initErr, state.StartProviders())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -54,12 +54,6 @@ Returns all route providers as a map keyed by their short name. Thread-safe acce
|
|||||||
func RouteProviderList() []RouteProviderListResponse
|
func RouteProviderList() []RouteProviderListResponse
|
||||||
```
|
```
|
||||||
|
|
||||||
Returns a list of route providers with their short and full names. Useful for API responses.
|
|
||||||
|
|
||||||
```go
|
|
||||||
func SearchRoute(alias string) types.Route
|
|
||||||
```
|
|
||||||
|
|
||||||
Searches for a route by alias across all providers. Returns `nil` if not found.
|
Searches for a route by alias across all providers. Returns `nil` if not found.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -179,15 +173,6 @@ for shortName, provider := range providers {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Searching for a route
|
|
||||||
|
|
||||||
```go
|
|
||||||
route := statequery.SearchRoute("my-service")
|
|
||||||
if route != nil {
|
|
||||||
fmt.Printf("Found route: %s\n", route.Alias())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Getting system statistics
|
### Getting system statistics
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -213,14 +198,4 @@ func handleGetStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(stats)
|
json.NewEncoder(w).Encode(stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleFindRoute(w http.ResponseWriter, r *http.Request) {
|
|
||||||
alias := r.URL.Query().Get("alias")
|
|
||||||
route := statequery.SearchRoute(alias)
|
|
||||||
if route == nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
json.NewEncoder(w).Encode(route)
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -30,13 +30,3 @@ func RouteProviderList() []RouteProviderListResponse {
|
|||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
func SearchRoute(alias string) types.Route {
|
|
||||||
state := config.ActiveState.Load()
|
|
||||||
for _, p := range state.IterProviders() {
|
|
||||||
if r, ok := p.GetRoute(alias); ok {
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import (
|
|||||||
"github.com/yusing/godoxy/internal/maxmind"
|
"github.com/yusing/godoxy/internal/maxmind"
|
||||||
"github.com/yusing/godoxy/internal/notif"
|
"github.com/yusing/godoxy/internal/notif"
|
||||||
route "github.com/yusing/godoxy/internal/route/provider"
|
route "github.com/yusing/godoxy/internal/route/provider"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
@@ -74,7 +73,6 @@ func SetState(state config.State) {
|
|||||||
|
|
||||||
cfg := state.Value()
|
cfg := state.Value()
|
||||||
config.ActiveState.Store(state)
|
config.ActiveState.Store(state)
|
||||||
acl.ActiveConfig.Store(cfg.ACL)
|
|
||||||
entrypoint.ActiveConfig.Store(&cfg.Entrypoint)
|
entrypoint.ActiveConfig.Store(&cfg.Entrypoint)
|
||||||
homepage.ActiveConfig.Store(&cfg.Homepage)
|
homepage.ActiveConfig.Store(&cfg.Homepage)
|
||||||
if autocertProvider := state.AutoCertProvider(); autocertProvider != nil {
|
if autocertProvider := state.AutoCertProvider(); autocertProvider != nil {
|
||||||
@@ -105,7 +103,7 @@ func (state *state) InitFromFile(filename string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (state *state) Init(data []byte) error {
|
func (state *state) Init(data []byte) error {
|
||||||
err := serialization.UnmarshalValidateYAML(data, &state.Config)
|
err := serialization.UnmarshalValidate(data, &state.Config, yaml.Unmarshal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -113,14 +111,14 @@ func (state *state) Init(data []byte) error {
|
|||||||
g := gperr.NewGroup("config load error")
|
g := gperr.NewGroup("config load error")
|
||||||
g.Go(state.initMaxMind)
|
g.Go(state.initMaxMind)
|
||||||
g.Go(state.initProxmox)
|
g.Go(state.initProxmox)
|
||||||
g.Go(state.loadRouteProviders)
|
|
||||||
g.Go(state.initAutoCert)
|
g.Go(state.initAutoCert)
|
||||||
|
|
||||||
errs := g.Wait()
|
errs := g.Wait()
|
||||||
// these won't benefit from running on goroutines
|
// these won't benefit from running on goroutines
|
||||||
errs.Add(state.initNotification())
|
errs.Add(state.initNotification())
|
||||||
errs.Add(state.initAccessLogger())
|
errs.Add(state.initACL())
|
||||||
errs.Add(state.initEntrypoint())
|
errs.Add(state.initEntrypoint())
|
||||||
|
errs.Add(state.loadRouteProviders())
|
||||||
return errs.Error()
|
return errs.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,12 +190,17 @@ func (state *state) FlushTmpLog() {
|
|||||||
state.tmpLogBuf.Reset()
|
state.tmpLogBuf.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
// this one is connection level access logger, different from entrypoint access logger
|
// initACL initializes the ACL.
|
||||||
func (state *state) initAccessLogger() error {
|
func (state *state) initACL() error {
|
||||||
if !state.ACL.Valid() {
|
if !state.ACL.Valid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return state.ACL.Start(state.task)
|
err := state.ACL.Start(state.task)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state.task.SetValue(acl.ContextKey{}, state.ACL)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *state) initEntrypoint() error {
|
func (state *state) initEntrypoint() error {
|
||||||
@@ -314,76 +317,50 @@ func (state *state) initProxmox() error {
|
|||||||
return errs.Wait().Error()
|
return errs.Wait().Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *state) storeProvider(p types.RouteProvider) {
|
|
||||||
state.providers.Store(p.String(), p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (state *state) loadRouteProviders() error {
|
func (state *state) loadRouteProviders() error {
|
||||||
// disable pool logging temporary since we will have pretty logging below
|
providers := state.Providers
|
||||||
routes.HTTP.ToggleLog(false)
|
|
||||||
routes.Stream.ToggleLog(false)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
routes.HTTP.ToggleLog(true)
|
|
||||||
routes.Stream.ToggleLog(true)
|
|
||||||
}()
|
|
||||||
|
|
||||||
providers := &state.Providers
|
|
||||||
errs := gperr.NewGroup("route provider errors")
|
errs := gperr.NewGroup("route provider errors")
|
||||||
results := gperr.NewGroup("loaded route providers")
|
|
||||||
|
|
||||||
agentpool.RemoveAll()
|
agentpool.RemoveAll()
|
||||||
|
|
||||||
numProviders := len(providers.Agents) + len(providers.Files) + len(providers.Docker)
|
registerProvider := func(p types.RouteProvider) {
|
||||||
providersCh := make(chan types.RouteProvider, numProviders)
|
if actual, loaded := state.providers.LoadOrStore(p.String(), p); loaded {
|
||||||
|
errs.Addf("provider %s already exists, first: %s, second: %s", p.String(), actual.GetType(), p.GetType())
|
||||||
// start providers concurrently
|
|
||||||
var providersConsumer sync.WaitGroup
|
|
||||||
providersConsumer.Go(func() {
|
|
||||||
for p := range providersCh {
|
|
||||||
if actual, loaded := state.providers.LoadOrStore(p.String(), p); loaded {
|
|
||||||
errs.Add(gperr.Errorf("provider %s already exists, first: %s, second: %s", p.String(), actual.GetType(), p.GetType()))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
state.storeProvider(p)
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
var providersProducer sync.WaitGroup
|
agentErrs := gperr.NewGroup("agent init errors")
|
||||||
for _, a := range providers.Agents {
|
for _, a := range providers.Agents {
|
||||||
providersProducer.Go(func() {
|
agentErrs.Go(func() error {
|
||||||
if err := a.Init(state.task.Context()); err != nil {
|
if err := a.Init(state.task.Context()); err != nil {
|
||||||
errs.Add(gperr.PrependSubject(a.String(), err))
|
return gperr.PrependSubject(a.String(), err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
agentpool.Add(a)
|
agentpool.Add(a)
|
||||||
p := route.NewAgentProvider(a)
|
return nil
|
||||||
providersCh <- p
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := agentErrs.Wait().Error(); err != nil {
|
||||||
|
errs.Add(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range providers.Agents {
|
||||||
|
registerProvider(route.NewAgentProvider(a))
|
||||||
|
}
|
||||||
|
|
||||||
for _, filename := range providers.Files {
|
for _, filename := range providers.Files {
|
||||||
providersProducer.Go(func() {
|
p, err := route.NewFileProvider(filename)
|
||||||
p, err := route.NewFileProvider(filename)
|
if err != nil {
|
||||||
if err != nil {
|
errs.Add(gperr.PrependSubject(filename, err))
|
||||||
errs.Add(gperr.PrependSubject(filename, err))
|
return err
|
||||||
} else {
|
}
|
||||||
providersCh <- p
|
registerProvider(p)
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, dockerCfg := range providers.Docker {
|
for name, dockerCfg := range providers.Docker {
|
||||||
providersProducer.Go(func() {
|
registerProvider(route.NewDockerProvider(name, dockerCfg))
|
||||||
providersCh <- route.NewDockerProvider(name, dockerCfg)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
providersProducer.Wait()
|
|
||||||
|
|
||||||
close(providersCh)
|
|
||||||
providersConsumer.Wait()
|
|
||||||
|
|
||||||
lenLongestName := 0
|
lenLongestName := 0
|
||||||
for k := range state.providers.Range {
|
for k := range state.providers.Range {
|
||||||
if len(k) > lenLongestName {
|
if len(k) > lenLongestName {
|
||||||
@@ -392,18 +369,26 @@ func (state *state) loadRouteProviders() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// load routes concurrently
|
// load routes concurrently
|
||||||
var providersLoader sync.WaitGroup
|
loadErrs := gperr.NewGroup("route load errors")
|
||||||
|
|
||||||
|
results := gperr.NewBuilder("loaded route providers")
|
||||||
|
resultsMu := sync.Mutex{}
|
||||||
for _, p := range state.providers.Range {
|
for _, p := range state.providers.Range {
|
||||||
providersLoader.Go(func() {
|
loadErrs.Go(func() error {
|
||||||
if err := p.LoadRoutes(); err != nil {
|
if err := p.LoadRoutes(); err != nil {
|
||||||
errs.Add(err.Subject(p.String()))
|
return err.Subject(p.String())
|
||||||
}
|
}
|
||||||
|
resultsMu.Lock()
|
||||||
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes())
|
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes())
|
||||||
|
resultsMu.Unlock()
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
providersLoader.Wait()
|
if err := loadErrs.Wait().Error(); err != nil {
|
||||||
|
errs.Add(err)
|
||||||
|
}
|
||||||
|
|
||||||
state.tmpLog.Info().Msg(results.Wait().String())
|
state.tmpLog.Info().Msg(results.String())
|
||||||
state.printRoutesByProvider(lenLongestName)
|
state.printRoutesByProvider(lenLongestName)
|
||||||
state.printState()
|
state.printState()
|
||||||
return errs.Wait().Error()
|
return errs.Wait().Error()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
"github.com/yusing/godoxy/internal/acl"
|
"github.com/yusing/godoxy/internal/acl"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
@@ -36,14 +37,14 @@ type (
|
|||||||
Docker map[string]types.DockerProviderConfig `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys"`
|
Docker map[string]types.DockerProviderConfig `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys"`
|
||||||
Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
|
Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
|
||||||
Notification []*notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"`
|
Notification []*notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"`
|
||||||
Proxmox []proxmox.Config `json:"proxmox" yaml:"proxmox,omitempty"`
|
Proxmox []*proxmox.Config `json:"proxmox" yaml:"proxmox,omitempty"`
|
||||||
MaxMind *maxmind.Config `json:"maxmind" yaml:"maxmind,omitempty"`
|
MaxMind *maxmind.Config `json:"maxmind" yaml:"maxmind,omitempty"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func Validate(data []byte) gperr.Error {
|
func Validate(data []byte) gperr.Error {
|
||||||
var model Config
|
var model Config
|
||||||
return serialization.UnmarshalValidateYAML(data, &model)
|
return serialization.UnmarshalValidate(data, &model, yaml.Unmarshal)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig() Config {
|
func DefaultConfig() Config {
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ replace github.com/yusing/godoxy => ../..
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-acme/lego/v4 v4.31.0
|
github.com/go-acme/lego/v4 v4.31.0
|
||||||
github.com/yusing/godoxy v0.24.1
|
github.com/yusing/godoxy v0.25.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/auth v0.18.0 // indirect
|
cloud.google.com/go/auth v0.18.1 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
||||||
@@ -23,12 +23,8 @@ require (
|
|||||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||||
github.com/boombuler/barcode v1.1.0 // indirect
|
github.com/boombuler/barcode v1.1.0 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
|
||||||
github.com/bytedance/sonic v1.14.2 // indirect
|
|
||||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/fatih/structs v1.1.0 // indirect
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
@@ -44,7 +40,7 @@ require (
|
|||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
github.com/gofrs/flock v0.13.0 // indirect
|
github.com/gofrs/flock v0.13.0 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||||
github.com/google/go-querystring v1.2.0 // indirect
|
github.com/google/go-querystring v1.2.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
@@ -53,7 +49,6 @@ require (
|
|||||||
github.com/gotify/server/v2 v2.8.0 // indirect
|
github.com/gotify/server/v2 v2.8.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
|
||||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
@@ -61,24 +56,23 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/maxatome/go-testdeep v1.14.0 // indirect
|
github.com/maxatome/go-testdeep v1.14.0 // indirect
|
||||||
github.com/miekg/dns v1.1.70 // indirect
|
github.com/miekg/dns v1.1.72 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
||||||
github.com/nrdcg/goinwx v0.12.0 // indirect
|
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.0 // indirect
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.0 // indirect
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 // indirect
|
||||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||||
github.com/ovh/go-ovh v1.9.0 // indirect
|
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/pquerna/otp v1.5.0 // indirect
|
github.com/pquerna/otp v1.5.0 // indirect
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.3.0 // indirect
|
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
|
||||||
github.com/rs/zerolog v1.34.0 // indirect
|
github.com/rs/zerolog v1.34.0 // indirect
|
||||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
|
||||||
github.com/sony/gobreaker v1.0.0 // indirect
|
github.com/sony/gobreaker v1.0.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.3 // indirect
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
|
||||||
github.com/vultr/govultr/v3 v3.26.1 // indirect
|
github.com/vultr/govultr/v3 v3.26.1 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
github.com/yusing/gointernals v0.1.16 // indirect
|
github.com/yusing/gointernals v0.1.16 // indirect
|
||||||
@@ -89,7 +83,6 @@ require (
|
|||||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||||
go.uber.org/ratelimit v0.3.1 // indirect
|
go.uber.org/ratelimit v0.3.1 // indirect
|
||||||
golang.org/x/arch v0.23.0 // indirect
|
|
||||||
golang.org/x/crypto v0.47.0 // indirect
|
golang.org/x/crypto v0.47.0 // indirect
|
||||||
golang.org/x/mod v0.32.0 // indirect
|
golang.org/x/mod v0.32.0 // indirect
|
||||||
golang.org/x/net v0.49.0 // indirect
|
golang.org/x/net v0.49.0 // indirect
|
||||||
@@ -98,8 +91,8 @@ require (
|
|||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
golang.org/x/tools v0.41.0 // indirect
|
golang.org/x/tools v0.41.0 // indirect
|
||||||
google.golang.org/api v0.260.0 // indirect
|
google.golang.org/api v0.263.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
||||||
google.golang.org/grpc v1.78.0 // indirect
|
google.golang.org/grpc v1.78.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
|
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
|
||||||
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
|
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
@@ -37,18 +37,10 @@ github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx
|
|||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||||
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
|
||||||
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
|
||||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
|
||||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
|
||||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -90,8 +82,8 @@ github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk
|
|||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
@@ -119,8 +111,6 @@ github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2
|
|||||||
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
||||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
|
||||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
|
||||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -142,18 +132,18 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
|
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
|
||||||
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||||
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
|
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||||
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
|
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
|
||||||
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
||||||
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
||||||
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.0 h1:4MRzV6spwPHKct+4/ETqkEtr39Hq+0KvxhsgqbgQ2Bo=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 h1:eMzyN+jGJbxG4ut278uwIsUo9XacXc711lFjhKnaUso=
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.0 h1:RxraLVYX3eMUfQ1pDtJVvykEFGheky2YsrUt2HHRDcw=
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 h1:t34IpOa+8NfmjkU8bdWtYrLrmr346/FGhu8FlpJDQok=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.0/go.mod h1:JLMEKMX8IYPZ1TUSVHAVAbtnNSfP/I8OZQkAnfEMA0I=
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0/go.mod h1:p95/OxVsdx71I2Qrck1GtIS87sRxcTRKXzUi5nWm9NY=
|
||||||
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
||||||
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
||||||
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
|
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
|
||||||
@@ -166,8 +156,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
|||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.3.0 h1:w/bWkEJdYuRNYhHn5eXnIT8LzDM1O629X1I9MJSkD7Q=
|
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.3.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
@@ -188,11 +178,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
|||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
|
||||||
github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
|
github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
|
||||||
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
@@ -221,8 +208,6 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
|||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
|
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
|
||||||
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
|
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
|
||||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
|
||||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
|
||||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
@@ -249,14 +234,14 @@ golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
|||||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/api v0.260.0 h1:XbNi5E6bOVEj/uLXQRlt6TKuEzMD7zvW/6tNwltE4P4=
|
google.golang.org/api v0.263.0 h1:UFs7qn8gInIdtk1ZA6eXRXp5JDAnS4x9VRsRVCeKdbk=
|
||||||
google.golang.org/api v0.260.0/go.mod h1:Shj1j0Phr/9sloYrKomICzdYgsSDImpTxME8rGLaZ/o=
|
google.golang.org/api v0.263.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8=
|
||||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 h1:C4WAdL+FbjnGlpp2S+HMVhBeCq2Lcib4xZqfPNF6OoQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/connhelper"
|
"github.com/docker/cli/cli/connhelper"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
"github.com/yusing/godoxy/internal/agentpool"
|
"github.com/yusing/godoxy/internal/agentpool"
|
||||||
@@ -152,7 +152,7 @@ func NewClient(cfg types.DockerProviderConfig, unique ...bool) (*SharedClient, e
|
|||||||
if agent.IsDockerHostAgent(host) {
|
if agent.IsDockerHostAgent(host) {
|
||||||
a, ok := agentpool.Get(host)
|
a, ok := agentpool.Get(host)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(fmt.Errorf("agent %q not found", host))
|
return nil, fmt.Errorf("agent %q not found", host)
|
||||||
}
|
}
|
||||||
opt = []client.Opt{
|
opt = []client.Opt{
|
||||||
client.WithHost(agent.DockerHost),
|
client.WithHost(agent.DockerHost),
|
||||||
@@ -198,7 +198,9 @@ func NewClient(cfg types.DockerProviderConfig, unique ...bool) (*SharedClient, e
|
|||||||
opt = append(opt, client.WithTLSClientConfig(cfg.TLS.CAFile, cfg.TLS.CertFile, cfg.TLS.KeyFile))
|
opt = append(opt, client.WithTLSClientConfig(cfg.TLS.CAFile, cfg.TLS.CertFile, cfg.TLS.KeyFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := client.New(opt...)
|
opt = append(opt, client.WithAPIVersionNegotiation())
|
||||||
|
|
||||||
|
client, err := client.NewClientWithOpts(opt...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
"github.com/moby/moby/api/types/container"
|
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
"github.com/yusing/godoxy/internal/agentpool"
|
"github.com/yusing/godoxy/internal/agentpool"
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
@@ -99,18 +98,18 @@ func UpdatePorts(ctx context.Context, c *types.Container) error {
|
|||||||
}
|
}
|
||||||
defer dockerClient.Close()
|
defer dockerClient.Close()
|
||||||
|
|
||||||
inspect, err := dockerClient.ContainerInspect(ctx, c.ContainerID, client.ContainerInspectOptions{})
|
inspect, err := dockerClient.ContainerInspect(ctx, c.ContainerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for port := range inspect.Container.Config.ExposedPorts {
|
for port := range inspect.Config.ExposedPorts {
|
||||||
proto, portStr := nat.SplitProtoPort(port.String())
|
proto, portStr := nat.SplitProtoPort(string(port))
|
||||||
portInt, _ := nat.ParsePort(portStr)
|
portInt, _ := nat.ParsePort(portStr)
|
||||||
if portInt == 0 {
|
if portInt == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
c.PublicPortMapping[portInt] = container.PortSummary{
|
c.PublicPortMapping[portInt] = container.Port{
|
||||||
PublicPort: uint16(portInt), //nolint:gosec
|
PublicPort: uint16(portInt), //nolint:gosec
|
||||||
PrivatePort: uint16(portInt), //nolint:gosec
|
PrivatePort: uint16(portInt), //nolint:gosec
|
||||||
Type: proto,
|
Type: proto,
|
||||||
@@ -210,32 +209,34 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if c.Network != "" {
|
if c.Network != "" {
|
||||||
v, ok := helper.NetworkSettings.Networks[c.Network]
|
v, hasNetwork := helper.NetworkSettings.Networks[c.Network]
|
||||||
if ok && v.IPAddress.IsValid() {
|
if hasNetwork && v.IPAddress != "" {
|
||||||
c.PrivateHostname = v.IPAddress.String()
|
c.PrivateHostname = v.IPAddress
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
var hasComposeNetwork bool
|
||||||
// try {project_name}_{network_name}
|
// try {project_name}_{network_name}
|
||||||
if proj := DockerComposeProject(c); proj != "" {
|
if proj := DockerComposeProject(c); proj != "" {
|
||||||
oldNetwork, newNetwork := c.Network, fmt.Sprintf("%s_%s", proj, c.Network)
|
newNetwork := fmt.Sprintf("%s_%s", proj, c.Network)
|
||||||
if newNetwork != oldNetwork {
|
v, hasComposeNetwork = helper.NetworkSettings.Networks[newNetwork]
|
||||||
v, ok = helper.NetworkSettings.Networks[newNetwork]
|
if hasComposeNetwork && v.IPAddress != "" {
|
||||||
if ok && v.IPAddress.IsValid() {
|
c.Network = newNetwork // update network to the new one
|
||||||
c.Network = newNetwork // update network to the new one
|
c.PrivateHostname = v.IPAddress
|
||||||
c.PrivateHostname = v.IPAddress.String()
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if hasNetwork || hasComposeNetwork { // network is found, but no IP assigned yet
|
||||||
|
return
|
||||||
|
}
|
||||||
nearest := gperr.DoYouMeanField(c.Network, helper.NetworkSettings.Networks)
|
nearest := gperr.DoYouMeanField(c.Network, helper.NetworkSettings.Networks)
|
||||||
addError(c, fmt.Errorf("network %q not found, %w", c.Network, nearest))
|
addError(c, fmt.Errorf("network %q not found, %w", c.Network, nearest))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// fallback to first network if no network is specified
|
// fallback to first network if no network is specified
|
||||||
for k, v := range helper.NetworkSettings.Networks {
|
for k, v := range helper.NetworkSettings.Networks {
|
||||||
if v.IPAddress.IsValid() {
|
if v.IPAddress != "" {
|
||||||
c.Network = k // update network to the first network
|
c.Network = k // update network to the first network
|
||||||
c.PrivateHostname = v.IPAddress.String()
|
c.PrivateHostname = v.IPAddress
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package docker
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/yusing/ds/ordered"
|
"github.com/yusing/ds/ordered"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package docker
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
expect "github.com/yusing/goutils/testing"
|
expect "github.com/yusing/goutils/testing"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ package docker
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
var listOptions = client.ContainerListOptions{
|
var listOptions = container.ListOptions{
|
||||||
// created|restarting|running|removing|paused|exited|dead
|
// created|restarting|running|removing|paused|exited|dead
|
||||||
// Filters: filters.NewArgs(
|
// Filters: filters.NewArgs(
|
||||||
// filters.Arg("status", "created"),
|
// filters.Arg("status", "created"),
|
||||||
@@ -31,7 +31,7 @@ func ListContainers(ctx context.Context, dockerCfg types.DockerProviderConfig) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return containers.Items, nil
|
return containers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsErrConnectionFailed(err error) bool {
|
func IsErrConnectionFailed(err error) bool {
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
rec := accesslog.GetResponseRecorder(w)
|
rec := accesslog.GetResponseRecorder(w)
|
||||||
w = rec
|
w = rec
|
||||||
defer func() {
|
defer func() {
|
||||||
ep.accessLogger.Log(r, rec.Response())
|
ep.accessLogger.LogRequest(r, rec.Response())
|
||||||
accesslog.PutResponseRecorder(rec)
|
accesslog.PutResponseRecorder(rec)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
1
internal/go-proxmox
Submodule
1
internal/go-proxmox
Submodule
Submodule internal/go-proxmox added at 7a07c21f07
@@ -2,13 +2,12 @@ package healthcheck
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/moby/moby/api/types/container"
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
httputils "github.com/yusing/goutils/http"
|
httputils "github.com/yusing/goutils/http"
|
||||||
@@ -44,7 +43,7 @@ func Docker(ctx context.Context, state *DockerHealthcheckState, timeout time.Dur
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// the actual inspect response is intercepted and returned as RequestInterceptedError
|
// the actual inspect response is intercepted and returned as RequestInterceptedError
|
||||||
_, err := state.client.ContainerInspect(ctx, state.containerId, client.ContainerInspectOptions{})
|
_, err := state.client.ContainerInspect(ctx, state.containerId)
|
||||||
|
|
||||||
var interceptedErr *httputils.RequestInterceptedError
|
var interceptedErr *httputils.RequestInterceptedError
|
||||||
if !httputils.AsRequestInterceptedError(err, &interceptedErr) {
|
if !httputils.AsRequestInterceptedError(err, &interceptedErr) {
|
||||||
@@ -106,7 +105,7 @@ func interceptDockerInspectResponse(resp *http.Response) (intercepted bool, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
var state container.State
|
var state container.State
|
||||||
err = sonic.Unmarshal(body, &state)
|
err = json.Unmarshal(body, &state)
|
||||||
release(body)
|
release(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|||||||
@@ -76,8 +76,11 @@ func H2C(ctx context.Context, url *url.URL, method, path string, timeout time.Du
|
|||||||
|
|
||||||
setCommonHeaders(req.Header.Set)
|
setCommonHeaders(req.Header.Set)
|
||||||
|
|
||||||
|
client := *h2cClient
|
||||||
|
client.Timeout = timeout
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
resp, err := h2cClient.Do(req)
|
resp, err := client.Do(req)
|
||||||
lat := time.Since(start)
|
lat := time.Since(start)
|
||||||
|
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Stream(ctx context.Context, url *url.URL, timeout time.Duration) (types.HealthCheckResult, error) {
|
func Stream(ctx context.Context, url *url.URL, timeout time.Duration) (types.HealthCheckResult, error) {
|
||||||
|
if port := url.Port(); port == "" || port == "0" {
|
||||||
|
return types.HealthCheckResult{
|
||||||
|
Latency: 0,
|
||||||
|
Healthy: false,
|
||||||
|
Detail: "no port specified",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
dialer := net.Dialer{
|
dialer := net.Dialer{
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
FallbackDelay: -1,
|
FallbackDelay: -1,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Homepage
|
# Homepage
|
||||||
|
|
||||||
The homepage package provides the GoDoxy WebUI dashboard with support for categories, favorites, widgets, and dynamic item configuration.
|
The homepage package provides the GoDoxy WebUI dashboard with support for categories, favorites, widgets, dynamic item configuration, and icon management.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -194,18 +194,6 @@ Widgets can display various types of information:
|
|||||||
- **Links**: Quick access links
|
- **Links**: Quick access links
|
||||||
- **Custom**: Provider-specific data
|
- **Custom**: Provider-specific data
|
||||||
|
|
||||||
## Icon Handling
|
|
||||||
|
|
||||||
Icons are handled via `IconURL` type:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type IconURL struct {
|
|
||||||
// Icon URL with various sources
|
|
||||||
}
|
|
||||||
|
|
||||||
// Automatic favicon fetching from item URL
|
|
||||||
```
|
|
||||||
|
|
||||||
## Categories
|
## Categories
|
||||||
|
|
||||||
### Default Categories
|
### Default Categories
|
||||||
|
|||||||
491
internal/homepage/icons/README.md
Normal file
491
internal/homepage/icons/README.md
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
# Icons Package
|
||||||
|
|
||||||
|
Icon URL parsing, fetching, and listing for the homepage dashboard.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The icons package manages icon resources from multiple sources with support for light/dark variants and multiple image formats. It provides a unified API for parsing icon URLs, checking icon availability, fetching icon data, and searching available icons from CDN repositories.
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
- Parse and validate icon URLs from various sources
|
||||||
|
- Fetch icon data with caching and fallback strategies
|
||||||
|
- Maintain a searchable index of available icons from walkxcode and selfh.st CDNs
|
||||||
|
- Support light/dark theme variants and multiple image formats (SVG, PNG, WebP)
|
||||||
|
|
||||||
|
### Primary Consumers
|
||||||
|
|
||||||
|
- `internal/homepage/` - Homepage route management and icon assignment
|
||||||
|
- `internal/api/` - Icon search and listing API endpoints
|
||||||
|
- `internal/route/` - Route icon resolution for proxy targets
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- Icon generation or modification (only fetching)
|
||||||
|
- Authentication for remote icon sources (public CDNs only)
|
||||||
|
- Icon validation beyond format checking
|
||||||
|
|
||||||
|
### Stability
|
||||||
|
|
||||||
|
This package exposes a stable public API. Internal implementations (caching strategies, fetch logic) may change without notice.
|
||||||
|
|
||||||
|
## Concepts and Terminology
|
||||||
|
|
||||||
|
| Term | Definition |
|
||||||
|
| ------------ | ------------------------------------------------------------------------------------- |
|
||||||
|
| **Source** | The origin type of an icon (absolute URL, relative path, walkxcode CDN, selfh.st CDN) |
|
||||||
|
| **Variant** | Theme variant: none, light, or dark |
|
||||||
|
| **Key** | Unique identifier combining source and reference (e.g., `@walkxcode/nginx`) |
|
||||||
|
| **Meta** | Metadata describing available formats and variants for an icon |
|
||||||
|
| **Provider** | Interface for checking icon existence without fetching data |
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
### Exported Types
|
||||||
|
|
||||||
|
#### Source
|
||||||
|
|
||||||
|
Source identifies the origin of an icon. Use the constants defined below.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Source string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SourceAbsolute is a full URL (http:// or https://)
|
||||||
|
SourceAbsolute Source = "https://"
|
||||||
|
|
||||||
|
// SourceRelative is a path relative to the target service (@target or leading /)
|
||||||
|
SourceRelative Source = "@target"
|
||||||
|
|
||||||
|
// SourceWalkXCode is the walkxcode dashboard-icons CDN
|
||||||
|
SourceWalkXCode Source = "@walkxcode"
|
||||||
|
|
||||||
|
// SourceSelfhSt is the selfh.st icons CDN
|
||||||
|
SourceSelfhSt Source = "@selfhst"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Variant
|
||||||
|
|
||||||
|
Variant indicates the theme preference for icons that support light/dark modes.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Variant string
|
||||||
|
|
||||||
|
const (
|
||||||
|
VariantNone Variant = "" // Default, no variant suffix
|
||||||
|
VariantLight Variant = "light" // Light theme variant (-light suffix)
|
||||||
|
VariantDark Variant = "dark" // Dark theme variant (-dark suffix)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### URL
|
||||||
|
|
||||||
|
URL represents a parsed icon URL with its source and metadata.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type URL struct {
|
||||||
|
// Source identifies the icon origin
|
||||||
|
Source `json:"source"`
|
||||||
|
|
||||||
|
// FullURL contains the resolved URL for absolute/relative sources
|
||||||
|
FullURL *string `json:"value,omitempty"`
|
||||||
|
|
||||||
|
// Extra contains metadata for CDN sources (walkxcode/selfhst)
|
||||||
|
Extra *Extra `json:"extra,omitempty"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**URL Methods:**
|
||||||
|
|
||||||
|
- `Parse(v string) error` - Parses an icon URL string (implements `strutils.Parser`)
|
||||||
|
- `URL() string` - Returns the absolute URL for fetching
|
||||||
|
- `HasIcon() bool` - Checks if the icon exists (requires Provider to be set)
|
||||||
|
- `WithVariant(variant Variant) *URL` - Returns a new URL with the specified variant
|
||||||
|
- `String() string` - Returns the original URL representation
|
||||||
|
- `MarshalText() ([]byte, error)` - Serializes to text (implements `encoding.TextMarshaler`)
|
||||||
|
- `UnmarshalText(data []byte) error` - Deserializes from text (implements `encoding.TextUnmarshaler`)
|
||||||
|
|
||||||
|
#### Extra
|
||||||
|
|
||||||
|
Extra contains metadata for icons from CDN sources.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Extra struct {
|
||||||
|
// Key is the unique icon key
|
||||||
|
Key Key `json:"key"`
|
||||||
|
|
||||||
|
// Ref is the icon reference name (without variant suffix)
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
|
||||||
|
// FileType is the image format: "svg", "png", or "webp"
|
||||||
|
FileType string `json:"file_type"`
|
||||||
|
|
||||||
|
// IsLight indicates if this is a light variant
|
||||||
|
IsLight bool `json:"is_light"`
|
||||||
|
|
||||||
|
// IsDark indicates if this is a dark variant
|
||||||
|
IsDark bool `json:"is_dark"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Key
|
||||||
|
|
||||||
|
Key is a unique identifier for an icon from a specific source.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Key string
|
||||||
|
|
||||||
|
// NewKey creates a key from source and reference
|
||||||
|
func NewKey(source Source, reference string) Key
|
||||||
|
|
||||||
|
// SourceRef extracts the source and reference from a key
|
||||||
|
func (k Key) SourceRef() (Source, string)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Meta
|
||||||
|
|
||||||
|
Meta stores availability metadata for an icon.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Meta struct {
|
||||||
|
// Available formats
|
||||||
|
SVG bool `json:"SVG"` // SVG format available
|
||||||
|
PNG bool `json:"PNG"` // PNG format available
|
||||||
|
WebP bool `json:"WebP"` // WebP format available
|
||||||
|
|
||||||
|
// Available variants
|
||||||
|
Light bool `json:"Light"` // Light variant available
|
||||||
|
Dark bool `json:"Dark"` // Dark variant available
|
||||||
|
|
||||||
|
// DisplayName is the human-readable name (selfh.st only)
|
||||||
|
DisplayName string `json:"-"`
|
||||||
|
|
||||||
|
// Tag is the category tag (selfh.st only)
|
||||||
|
Tag string `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filenames returns all available filename variants for this icon
|
||||||
|
func (icon *Meta) Filenames(ref string) []string
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exported Functions
|
||||||
|
|
||||||
|
```go
|
||||||
|
// NewURL creates a URL for a CDN source with the given reference and format
|
||||||
|
func NewURL(source Source, refOrName, format string) *URL
|
||||||
|
|
||||||
|
// ErrInvalidIconURL is returned when icon URL parsing fails
|
||||||
|
var ErrInvalidIconURL = gperr.New("invalid icon url")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Provider Interface
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Provider interface {
|
||||||
|
// HasIcon returns true if the icon exists in the provider's catalog
|
||||||
|
HasIcon(u *URL) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetProvider sets the global icon provider for existence checks
|
||||||
|
func SetProvider(p Provider)
|
||||||
|
```
|
||||||
|
|
||||||
|
The provider pattern allows the icons package to check icon existence without fetching data. The `list` subpackage registers a provider that checks against the cached icon list.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph icons/
|
||||||
|
URL["URL"] --> Parser[URL Parser]
|
||||||
|
URL --> VariantHandler[Variant Handler]
|
||||||
|
Key --> Provider
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph fetch/
|
||||||
|
FetchFavIconFromURL --> FetchIconAbsolute
|
||||||
|
FetchFavIconFromURL --> FindIcon
|
||||||
|
FindIcon --> fetchKnownIcon
|
||||||
|
FindIcon --> findIconSlow
|
||||||
|
fetchKnownIcon --> FetchIconAbsolute
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph list/
|
||||||
|
InitCache --> updateIcons
|
||||||
|
updateIcons --> UpdateWalkxCodeIcons
|
||||||
|
updateIcons --> UpdateSelfhstIcons
|
||||||
|
SearchIcons --> fuzzyRank[Fuzzy Rank Match]
|
||||||
|
HasIcon --> ListAvailableIcons
|
||||||
|
end
|
||||||
|
|
||||||
|
style URL fill:#22553F,color:#fff
|
||||||
|
style list fill:#22553F,color:#fff
|
||||||
|
style fetch fill:#22553F,color:#fff
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Interactions
|
||||||
|
|
||||||
|
1. **URL Parsing** (`url.go`): Parses icon URL strings and validates format
|
||||||
|
2. **Icon Existence** (`provider.go`): Delegates to registered Provider
|
||||||
|
3. **Icon Fetching** (`fetch/fetch.go`): Fetches icon data with caching
|
||||||
|
4. **Icon Listing** (`list/list_icons.go`): Maintains cached index of available icons
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Client
|
||||||
|
participant URLParser
|
||||||
|
participant Provider
|
||||||
|
participant FetchCache
|
||||||
|
participant ExternalCDN
|
||||||
|
|
||||||
|
Client->>URLParser: Parse("@walkxcode/nginx.svg")
|
||||||
|
URLParser->>Provider: HasIcon(icon)
|
||||||
|
Provider->>FetchCache: Check cached list
|
||||||
|
FetchCache-->>Provider: exists
|
||||||
|
Provider-->>URLParser: true
|
||||||
|
URLParser-->>Client: URL object
|
||||||
|
|
||||||
|
Client->>FetchCache: FetchFavIconFromURL(url)
|
||||||
|
FetchCache->>ExternalCDN: GET https://...
|
||||||
|
ExternalCDN-->>FetchCache: icon data
|
||||||
|
FetchCache-->>Client: Result{Icon: [...], StatusCode: 200}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subpackages
|
||||||
|
|
||||||
|
### fetch/
|
||||||
|
|
||||||
|
Icon fetching implementation with caching and fallback strategies.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Result struct {
|
||||||
|
Icon []byte // Raw icon image data
|
||||||
|
StatusCode int // HTTP status code from fetch
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchFavIconFromURL fetches an icon from a parsed URL
|
||||||
|
func FetchFavIconFromURL(ctx context.Context, iconURL *URL) (Result, error)
|
||||||
|
|
||||||
|
// FindIcon finds an icon for a route with variant support
|
||||||
|
func FindIcon(ctx context.Context, r route, uri string, variant Variant) (Result, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key behaviors:**
|
||||||
|
|
||||||
|
- `FetchIconAbsolute` is cached with 200 entries and 4-hour TTL
|
||||||
|
- `findIconSlow` has infinite retries with 15-second backoff
|
||||||
|
- HTML parsing fallback extracts `<link rel=icon>` from target pages
|
||||||
|
|
||||||
|
### list/
|
||||||
|
|
||||||
|
Icon catalog management with search and caching.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type IconMap map[Key]*Meta
|
||||||
|
type IconMetaSearch struct {
|
||||||
|
*Meta
|
||||||
|
Source Source `json:"Source"`
|
||||||
|
Ref string `json:"Ref"`
|
||||||
|
rank int
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitCache loads icon metadata from cache or remote sources
|
||||||
|
func InitCache()
|
||||||
|
|
||||||
|
// ListAvailableIcons returns the current icon catalog
|
||||||
|
func ListAvailableIcons() IconMap
|
||||||
|
|
||||||
|
// SearchIcons performs fuzzy search on icon names
|
||||||
|
func SearchIcons(keyword string, limit int) []*IconMetaSearch
|
||||||
|
|
||||||
|
// HasIcon checks if an icon exists in the catalog
|
||||||
|
func HasIcon(icon *URL) bool
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key behaviors:**
|
||||||
|
|
||||||
|
- Updates from walkxcode and selfh.st CDNs every 2 hours
|
||||||
|
- Persists cache to disk for fast startup
|
||||||
|
- Fuzzy search uses Levenshtein distance ranking
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Cache Location
|
||||||
|
|
||||||
|
Icons cache is stored at the path specified by `common.IconListCachePath`.
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
No direct environment variable configuration. Cache is managed internally.
|
||||||
|
|
||||||
|
### Reloading
|
||||||
|
|
||||||
|
Icon cache updates automatically every 2 hours in the background. Manual refresh requires program restart.
|
||||||
|
|
||||||
|
## Observability
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
- `failed to load icons` - Cache load failure at startup
|
||||||
|
- `icons loaded` - Successful cache load with entry count
|
||||||
|
- `updating icon data` - Background update started
|
||||||
|
- `icons list updated` - Successful cache refresh with entry count
|
||||||
|
- `failed to save icons` - Cache persistence failure
|
||||||
|
|
||||||
|
### Metrics
|
||||||
|
|
||||||
|
No metrics exposed directly. Status codes in `Result` can be monitored via HTTP handlers.
|
||||||
|
|
||||||
|
### Tracing
|
||||||
|
|
||||||
|
Standard `context.Context` propagation is used throughout. Fetch operations respect context cancellation and deadlines.
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- **Input Validation**: Icon URLs are strictly validated for format and source
|
||||||
|
- **SSRF Protection**: Only absolute URLs passed directly; no arbitrary URL construction
|
||||||
|
- **Content-Type**: Detected from response headers or inferred from SVG magic bytes
|
||||||
|
- **Size Limits**: Cache limited to 200 entries; no explicit size limit on icon data
|
||||||
|
- **Timeouts**: 3-second timeout on favicon fetches, 5-second timeout on list updates
|
||||||
|
|
||||||
|
## Performance Characteristics
|
||||||
|
|
||||||
|
- **Parsing**: O(1) string parsing with early validation
|
||||||
|
- **Caching**: LRU-style cache with TTL for fetched icons
|
||||||
|
- **Background Updates**: Non-blocking updates every 2 hours
|
||||||
|
- **Search**: O(n) fuzzy match with early exit at rank > 3
|
||||||
|
- **Memory**: Icon list typically contains ~2000 entries
|
||||||
|
|
||||||
|
## Failure Modes and Recovery
|
||||||
|
|
||||||
|
| Failure | Behavior | Recovery |
|
||||||
|
| ---------------------- | ---------------------------------------- | -------------------------------- |
|
||||||
|
| CDN fetch timeout | Return cached data or fail | Automatic retry with backoff |
|
||||||
|
| Cache load failure | Attempt legacy format, then remote fetch | Manual cache reset if persistent |
|
||||||
|
| Icon not found in list | Return error from Parse | User must select valid icon |
|
||||||
|
| HTML parse failure | Return "icon element not found" | Manual icon selection |
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic: Parse and Generate URL
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Parse a CDN icon URL
|
||||||
|
url := &icons.URL{}
|
||||||
|
err := url.Parse("@walkxcode/nginx.svg")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the actual fetchable URL
|
||||||
|
fmt.Println(url.URL())
|
||||||
|
// Output: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/nginx.svg
|
||||||
|
|
||||||
|
// Get string representation
|
||||||
|
fmt.Println(url.String())
|
||||||
|
// Output: @walkxcode/nginx.svg
|
||||||
|
|
||||||
|
// Create with dark variant
|
||||||
|
darkUrl := url.WithVariant(icons.VariantDark)
|
||||||
|
fmt.Println(darkUrl.URL())
|
||||||
|
// Output: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/nginx-dark.svg
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced: Fetch Icon Data
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/yusing/godoxy/internal/homepage/icons/fetch"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Initialize the icon list cache first
|
||||||
|
iconlist.InitCache()
|
||||||
|
|
||||||
|
// Parse icon URL
|
||||||
|
url := &icons.URL{}
|
||||||
|
if err := url.Parse("@walkxcode/nginx.svg"); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch icon data
|
||||||
|
ctx := context.Background()
|
||||||
|
result, err := fetch.FetchFavIconFromURL(ctx, url)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Fetch failed: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.StatusCode != http.StatusOK {
|
||||||
|
fmt.Printf("HTTP %d\n", result.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Fetched %d bytes, Content-Type: %s\n",
|
||||||
|
len(result.Icon), result.ContentType())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration: Search Available Icons
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Initialize cache
|
||||||
|
list.InitCache()
|
||||||
|
|
||||||
|
// Search for icons matching a keyword
|
||||||
|
results := list.SearchIcons("nginx", 5)
|
||||||
|
|
||||||
|
for _, icon := range results {
|
||||||
|
source, ref := icon.Key.SourceRef()
|
||||||
|
fmt.Printf("[%s] %s - SVG:%v PNG:%v WebP:%v\n",
|
||||||
|
source, ref, icon.SVG, icon.PNG, icon.WebP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Notes
|
||||||
|
|
||||||
|
- Unit tests in `url_test.go` validate parsing and serialization
|
||||||
|
- Test mode (`common.IsTest`) bypasses existence checks
|
||||||
|
- Mock HTTP in list tests via `MockHTTPGet()`
|
||||||
|
- Golden tests not used; test fixtures embedded in test cases
|
||||||
|
|
||||||
|
## Icon URL Formats
|
||||||
|
|
||||||
|
| Format | Example | Output URL |
|
||||||
|
| ------------- | ------------------------------ | --------------------------------------------------------------------- |
|
||||||
|
| Absolute | `https://example.com/icon.png` | `https://example.com/icon.png` |
|
||||||
|
| Relative | `@target/favicon.ico` | `/favicon.ico` |
|
||||||
|
| WalkXCode | `@walkxcode/nginx.svg` | `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/nginx.svg` |
|
||||||
|
| Selfh.st | `@selfhst/adguard-home.webp` | `https://cdn.jsdelivr.net/gh/selfhst/icons/webp/adguard-home.webp` |
|
||||||
|
| Light variant | `@walkxcode/nginx-light.png` | `.../nginx-light.png` |
|
||||||
|
| Dark variant | `@walkxcode/nginx-dark.svg` | `.../nginx-dark.svg` |
|
||||||
@@ -2,12 +2,12 @@ package iconlist
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
@@ -55,20 +55,20 @@ func init() {
|
|||||||
|
|
||||||
func InitCache() {
|
func InitCache() {
|
||||||
m := make(IconMap)
|
m := make(IconMap)
|
||||||
err := serialization.LoadJSONIfExist(common.IconListCachePath, &m)
|
err := serialization.LoadFileIfExist(common.IconListCachePath, &m, json.Unmarshal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// backward compatible
|
// backward compatible
|
||||||
oldFormat := struct {
|
oldFormat := struct {
|
||||||
Icons IconMap
|
Icons IconMap
|
||||||
LastUpdate time.Time
|
LastUpdate time.Time
|
||||||
}{}
|
}{}
|
||||||
err = serialization.LoadJSONIfExist(common.IconListCachePath, &oldFormat)
|
err = serialization.LoadFileIfExist(common.IconListCachePath, &oldFormat, json.Unmarshal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("failed to load icons")
|
log.Error().Err(err).Msg("failed to load icons")
|
||||||
} else {
|
} else {
|
||||||
m = oldFormat.Icons
|
m = oldFormat.Icons
|
||||||
// store it to disk immediately
|
// store it to disk immediately
|
||||||
_ = serialization.SaveJSON(common.IconListCachePath, &m, 0o644)
|
_ = serialization.SaveFile(common.IconListCachePath, &m, 0o644, json.Marshal)
|
||||||
}
|
}
|
||||||
} else if len(m) > 0 {
|
} else if len(m) > 0 {
|
||||||
log.Info().
|
log.Info().
|
||||||
@@ -84,7 +84,7 @@ func InitCache() {
|
|||||||
|
|
||||||
task.OnProgramExit("save_icons_cache", func() {
|
task.OnProgramExit("save_icons_cache", func() {
|
||||||
icons := iconsCache.Load()
|
icons := iconsCache.Load()
|
||||||
_ = serialization.SaveJSON(common.IconListCachePath, &icons, 0o644)
|
_ = serialization.SaveFile(common.IconListCachePath, &icons, 0o644, json.Marshal)
|
||||||
})
|
})
|
||||||
|
|
||||||
go backgroundUpdateIcons()
|
go backgroundUpdateIcons()
|
||||||
@@ -105,7 +105,7 @@ func backgroundUpdateIcons() {
|
|||||||
// swap old cache with new cache
|
// swap old cache with new cache
|
||||||
iconsCache.Store(newCache)
|
iconsCache.Store(newCache)
|
||||||
// save it to disk
|
// save it to disk
|
||||||
err := serialization.SaveJSON(common.IconListCachePath, &newCache, 0o644)
|
err := serialization.SaveFile(common.IconListCachePath, &newCache, 0o644, json.Marshal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("failed to save icons")
|
log.Warn().Err(err).Msg("failed to save icons")
|
||||||
}
|
}
|
||||||
@@ -270,7 +270,7 @@ func UpdateWalkxCodeIcons(m IconMap) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data := make(map[string][]string)
|
data := make(map[string][]string)
|
||||||
err = sonic.Unmarshal(body, &data)
|
err = json.Unmarshal(body, &data)
|
||||||
release(body)
|
release(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -349,7 +349,7 @@ func UpdateSelfhstIcons(m IconMap) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data := make([]SelfhStIcon, 0)
|
data := make([]SelfhStIcon, 0)
|
||||||
err = sonic.Unmarshal(body, &data) //nolint:musttag
|
err = json.Unmarshal(body, &data) //nolint:musttag
|
||||||
release(body)
|
release(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ package qbittorrent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
"github.com/yusing/godoxy/internal/homepage/widgets"
|
"github.com/yusing/godoxy/internal/homepage/widgets"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
)
|
)
|
||||||
@@ -59,7 +59,7 @@ func jsonRequest[T any](ctx context.Context, client *Client, endpoint string, qu
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
err = sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&result)
|
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ package qbittorrent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const endpointLogs = "/api/v2/log/main"
|
const endpointLogs = "/api/v2/log/main"
|
||||||
@@ -45,7 +44,7 @@ func (l *LogEntry) Level() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *LogEntry) MarshalJSON() ([]byte, error) {
|
func (l *LogEntry) MarshalJSON() ([]byte, error) {
|
||||||
return sonic.Marshal(map[string]any{
|
return json.Marshal(map[string]any{
|
||||||
"id": l.ID,
|
"id": l.ID,
|
||||||
"timestamp": l.Timestamp,
|
"timestamp": l.Timestamp,
|
||||||
"level": l.Level(),
|
"level": l.Level(),
|
||||||
|
|||||||
13
internal/homepage/types/README.md
Normal file
13
internal/homepage/types/README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Types Package
|
||||||
|
|
||||||
|
Configuration types for the homepage package.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Config struct {
|
||||||
|
UseDefaultCategories bool `json:"use_default_categories"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var ActiveConfig atomic.Pointer[Config]
|
||||||
|
```
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package idlewatcher
|
package idlewatcher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"iter"
|
"iter"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ type watcherDebug struct {
|
|||||||
|
|
||||||
func (w watcherDebug) MarshalJSON() ([]byte, error) {
|
func (w watcherDebug) MarshalJSON() ([]byte, error) {
|
||||||
state := w.state.Load()
|
state := w.state.Load()
|
||||||
return sonic.Marshal(map[string]any{
|
return json.Marshal(map[string]any{
|
||||||
"name": w.Name(),
|
"name": w.Name(),
|
||||||
"state": map[string]string{
|
"state": map[string]string{
|
||||||
"status": string(state.status),
|
"status": string(state.status),
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
package idlewatcher
|
package idlewatcher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type WakeEvent struct {
|
type WakeEvent struct {
|
||||||
@@ -40,7 +39,7 @@ func (w *Watcher) newWakeEvent(eventType WakeEventType, message string, err erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *WakeEvent) WriteSSE(w io.Writer) error {
|
func (e *WakeEvent) WriteSSE(w io.Writer) error {
|
||||||
data, err := sonic.Marshal(e)
|
data, err := json.Marshal(e)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ package provider
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
|
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
@@ -18,7 +17,7 @@ type DockerProvider struct {
|
|||||||
containerID string
|
containerID string
|
||||||
}
|
}
|
||||||
|
|
||||||
var startOptions = client.ContainerStartOptions{}
|
var startOptions = container.StartOptions{}
|
||||||
|
|
||||||
func NewDockerProvider(dockerCfg types.DockerProviderConfig, containerID string) (idlewatcher.Provider, error) {
|
func NewDockerProvider(dockerCfg types.DockerProviderConfig, containerID string) (idlewatcher.Provider, error) {
|
||||||
client, err := docker.NewClient(dockerCfg)
|
client, err := docker.NewClient(dockerCfg)
|
||||||
@@ -33,41 +32,34 @@ func NewDockerProvider(dockerCfg types.DockerProviderConfig, containerID string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *DockerProvider) ContainerPause(ctx context.Context) error {
|
func (p *DockerProvider) ContainerPause(ctx context.Context) error {
|
||||||
_, err := p.client.ContainerPause(ctx, p.containerID, client.ContainerPauseOptions{})
|
return p.client.ContainerPause(ctx, p.containerID)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *DockerProvider) ContainerUnpause(ctx context.Context) error {
|
func (p *DockerProvider) ContainerUnpause(ctx context.Context) error {
|
||||||
_, err := p.client.ContainerUnpause(ctx, p.containerID, client.ContainerUnpauseOptions{})
|
return p.client.ContainerUnpause(ctx, p.containerID)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *DockerProvider) ContainerStart(ctx context.Context) error {
|
func (p *DockerProvider) ContainerStart(ctx context.Context) error {
|
||||||
_, err := p.client.ContainerStart(ctx, p.containerID, startOptions)
|
return p.client.ContainerStart(ctx, p.containerID, startOptions)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *DockerProvider) ContainerStop(ctx context.Context, signal types.ContainerSignal, timeout int) error {
|
func (p *DockerProvider) ContainerStop(ctx context.Context, signal types.ContainerSignal, timeout int) error {
|
||||||
_, err := p.client.ContainerStop(ctx, p.containerID, client.ContainerStopOptions{
|
return p.client.ContainerStop(ctx, p.containerID, container.StopOptions{
|
||||||
Signal: string(signal),
|
Signal: string(signal),
|
||||||
Timeout: &timeout,
|
Timeout: &timeout,
|
||||||
})
|
})
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *DockerProvider) ContainerKill(ctx context.Context, signal types.ContainerSignal) error {
|
func (p *DockerProvider) ContainerKill(ctx context.Context, signal types.ContainerSignal) error {
|
||||||
_, err := p.client.ContainerKill(ctx, p.containerID, client.ContainerKillOptions{
|
return p.client.ContainerKill(ctx, p.containerID, string(signal))
|
||||||
Signal: string(signal),
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *DockerProvider) ContainerStatus(ctx context.Context) (idlewatcher.ContainerStatus, error) {
|
func (p *DockerProvider) ContainerStatus(ctx context.Context) (idlewatcher.ContainerStatus, error) {
|
||||||
status, err := p.client.ContainerInspect(ctx, p.containerID, client.ContainerInspectOptions{})
|
status, err := p.client.ContainerInspect(ctx, p.containerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return idlewatcher.ContainerStatusError, err
|
return idlewatcher.ContainerStatusError, err
|
||||||
}
|
}
|
||||||
switch status.Container.State.Status {
|
switch status.State.Status {
|
||||||
case container.StateRunning:
|
case container.StateRunning:
|
||||||
return idlewatcher.ContainerStatusRunning, nil
|
return idlewatcher.ContainerStatusRunning, nil
|
||||||
case container.StateExited, container.StateDead, container.StateRestarting:
|
case container.StateExited, container.StateDead, container.StateRestarting:
|
||||||
@@ -75,12 +67,12 @@ func (p *DockerProvider) ContainerStatus(ctx context.Context) (idlewatcher.Conta
|
|||||||
case container.StatePaused:
|
case container.StatePaused:
|
||||||
return idlewatcher.ContainerStatusPaused, nil
|
return idlewatcher.ContainerStatusPaused, nil
|
||||||
}
|
}
|
||||||
return idlewatcher.ContainerStatusError, idlewatcher.ErrUnexpectedContainerStatus.Subject(string(status.Container.State.Status))
|
return idlewatcher.ContainerStatusError, idlewatcher.ErrUnexpectedContainerStatus.Subject(status.State.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *DockerProvider) Watch(ctx context.Context) (eventCh <-chan watcher.Event, errCh <-chan gperr.Error) {
|
func (p *DockerProvider) Watch(ctx context.Context) (eventCh <-chan watcher.Event, errCh <-chan gperr.Error) {
|
||||||
return p.watcher.EventsWithOptions(ctx, watcher.DockerListOptions{
|
return p.watcher.EventsWithOptions(ctx, watcher.DockerListOptions{
|
||||||
Filters: watcher.NewDockerFilters(
|
Filters: watcher.NewDockerFilter(
|
||||||
watcher.DockerFilterContainer,
|
watcher.DockerFilterContainer,
|
||||||
watcher.DockerFilterContainerNameID(p.containerID),
|
watcher.DockerFilterContainerNameID(p.containerID),
|
||||||
watcher.DockerFilterStart,
|
watcher.DockerFilterStart,
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ const proxmoxStateCheckInterval = 1 * time.Second
|
|||||||
var ErrNodeNotFound = gperr.New("node not found in pool")
|
var ErrNodeNotFound = gperr.New("node not found in pool")
|
||||||
|
|
||||||
func NewProxmoxProvider(ctx context.Context, nodeName string, vmid int) (idlewatcher.Provider, error) {
|
func NewProxmoxProvider(ctx context.Context, nodeName string, vmid int) (idlewatcher.Provider, error) {
|
||||||
|
if nodeName == "" || vmid == 0 {
|
||||||
|
return nil, gperr.New("node name and vmid are required")
|
||||||
|
}
|
||||||
|
|
||||||
node, ok := proxmox.Nodes.Get(nodeName)
|
node, ok := proxmox.Nodes.Get(nodeName)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, ErrNodeNotFound.Subject(nodeName).
|
return nil, ErrNodeNotFound.Subject(nodeName).
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
package idlewatcher
|
|
||||||
@@ -173,7 +173,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
depRoute, ok = routes.Get(dep)
|
depRoute, ok = routes.GetIncludeExcluded(dep)
|
||||||
if !ok {
|
if !ok {
|
||||||
depErrors.Addf("dependency %q not found", dep)
|
depErrors.Addf("dependency %q not found", dep)
|
||||||
continue
|
continue
|
||||||
@@ -612,10 +612,6 @@ func (w *Watcher) watchUntilDestroy() (returnCause error) {
|
|||||||
if ready {
|
if ready {
|
||||||
// Container is now ready, notify waiting handlers
|
// Container is now ready, notify waiting handlers
|
||||||
w.healthTicker.Stop()
|
w.healthTicker.Stop()
|
||||||
select {
|
|
||||||
case w.readyNotifyCh <- struct{}{}:
|
|
||||||
default: // channel full, notification already pending
|
|
||||||
}
|
|
||||||
w.resetIdleTimer()
|
w.resetIdleTimer()
|
||||||
}
|
}
|
||||||
// If not ready yet, keep checking on next tick
|
// If not ready yet, keep checking on next tick
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ func loadNS[T store](ns namespace) T {
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
if err := sonic.ConfigDefault.NewDecoder(file).Decode(&store); err != nil {
|
if err := json.NewDecoder(file).Decode(&store); err != nil {
|
||||||
log.Err(err).Msg("failed to decode store")
|
log.Err(err).Msg("failed to decode store")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +316,7 @@ type MapStore[VT any] struct {
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
func (s MapStore[VT]) MarshalJSON() ([]byte, error) {
|
func (s MapStore[VT]) MarshalJSON() ([]byte, error) {
|
||||||
return sonic.Marshal(xsync.ToPlainMap(s.Map))
|
return json.Marshal(xsync.ToPlainMap(s.Map))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -325,7 +325,7 @@ func (s MapStore[VT]) MarshalJSON() ([]byte, error) {
|
|||||||
```go
|
```go
|
||||||
func (s *MapStore[VT]) UnmarshalJSON(data []byte) error {
|
func (s *MapStore[VT]) UnmarshalJSON(data []byte) error {
|
||||||
tmp := make(map[string]VT)
|
tmp := make(map[string]VT)
|
||||||
if err := sonic.Unmarshal(data, &tmp); err != nil {
|
if err := json.Unmarshal(data, &tmp); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.Map = xsync.NewMap[string, VT](xsync.WithPresize(len(tmp)))
|
s.Map = xsync.NewMap[string, VT](xsync.WithPresize(len(tmp)))
|
||||||
@@ -349,7 +349,7 @@ The jsonstore package integrates with:
|
|||||||
Errors are logged but don't prevent store usage:
|
Errors are logged but don't prevent store usage:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
if err := sonic.Unmarshal(data, &tmp); err != nil {
|
if err := json.Unmarshal(data, &tmp); err != nil {
|
||||||
log.Err(err).
|
log.Err(err).
|
||||||
Str("path", path).
|
Str("path", path).
|
||||||
Msg("failed to load store")
|
Msg("failed to load store")
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
"github.com/puzpuzpuz/xsync/v4"
|
"github.com/puzpuzpuz/xsync/v4"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
@@ -66,7 +65,7 @@ func loadNS[T store](ns namespace) T {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
if err := sonic.ConfigDefault.NewDecoder(file).Decode(&store); err != nil {
|
if err := json.NewDecoder(file).Decode(&store); err != nil {
|
||||||
log.Err(err).
|
log.Err(err).
|
||||||
Str("path", path).
|
Str("path", path).
|
||||||
Msg("failed to load store")
|
Msg("failed to load store")
|
||||||
@@ -83,7 +82,8 @@ func loadNS[T store](ns namespace) T {
|
|||||||
func save() error {
|
func save() error {
|
||||||
errs := gperr.NewBuilder("failed to save data stores")
|
errs := gperr.NewBuilder("failed to save data stores")
|
||||||
for ns, store := range stores {
|
for ns, store := range stores {
|
||||||
if err := serialization.SaveJSON(filepath.Join(storesPath, string(ns)+".json"), &store, 0o644); err != nil {
|
path := filepath.Join(storesPath, string(ns)+".json")
|
||||||
|
if err := serialization.SaveFile(path, &store, 0o644, json.Marshal); err != nil {
|
||||||
errs.Add(err)
|
errs.Add(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,12 +113,12 @@ func (s *MapStore[VT]) Initialize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s MapStore[VT]) MarshalJSON() ([]byte, error) {
|
func (s MapStore[VT]) MarshalJSON() ([]byte, error) {
|
||||||
return sonic.Marshal(xsync.ToPlainMap(s.Map))
|
return json.Marshal(xsync.ToPlainMap(s.Map))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MapStore[VT]) UnmarshalJSON(data []byte) error {
|
func (s *MapStore[VT]) UnmarshalJSON(data []byte) error {
|
||||||
tmp := make(map[string]VT)
|
tmp := make(map[string]VT)
|
||||||
if err := sonic.Unmarshal(data, &tmp); err != nil {
|
if err := json.Unmarshal(data, &tmp); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.Map = xsync.NewMap[string, VT](xsync.WithPresize(len(tmp)))
|
s.Map = xsync.NewMap[string, VT](xsync.WithPresize(len(tmp)))
|
||||||
@@ -134,10 +134,10 @@ func (obj *ObjectStore[Ptr]) Initialize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (obj ObjectStore[Ptr]) MarshalJSON() ([]byte, error) {
|
func (obj ObjectStore[Ptr]) MarshalJSON() ([]byte, error) {
|
||||||
return sonic.Marshal(obj.ptr)
|
return json.Marshal(obj.ptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj *ObjectStore[Ptr]) UnmarshalJSON(data []byte) error {
|
func (obj *ObjectStore[Ptr]) UnmarshalJSON(data []byte) error {
|
||||||
obj.Initialize()
|
obj.Initialize()
|
||||||
return sonic.Unmarshal(data, obj.ptr)
|
return json.Unmarshal(data, obj.ptr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,9 @@ type ReaderAtSeeker interface {
|
|||||||
|
|
||||||
// BackScanner provides an interface to read a file backward line by line.
|
// BackScanner provides an interface to read a file backward line by line.
|
||||||
type BackScanner struct {
|
type BackScanner struct {
|
||||||
file ReaderAtSeeker
|
file ReaderAtSeeker
|
||||||
size int64
|
size int64
|
||||||
chunkSize int
|
chunkBuf []byte
|
||||||
chunkBuf []byte
|
|
||||||
|
|
||||||
offset int64
|
offset int64
|
||||||
chunk []byte
|
chunk []byte
|
||||||
@@ -27,16 +26,25 @@ type BackScanner struct {
|
|||||||
// NewBackScanner creates a new Scanner to read the file backward.
|
// NewBackScanner creates a new Scanner to read the file backward.
|
||||||
// chunkSize determines the size of each read chunk from the end of the file.
|
// chunkSize determines the size of each read chunk from the end of the file.
|
||||||
func NewBackScanner(file ReaderAtSeeker, fileSize int64, chunkSize int) *BackScanner {
|
func NewBackScanner(file ReaderAtSeeker, fileSize int64, chunkSize int) *BackScanner {
|
||||||
return newBackScanner(file, fileSize, make([]byte, chunkSize))
|
return newBackScanner(file, fileSize, sizedPool.GetSized(chunkSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBackScanner(file ReaderAtSeeker, fileSize int64, buf []byte) *BackScanner {
|
func newBackScanner(file ReaderAtSeeker, fileSize int64, buf []byte) *BackScanner {
|
||||||
return &BackScanner{
|
return &BackScanner{
|
||||||
file: file,
|
file: file,
|
||||||
size: fileSize,
|
size: fileSize,
|
||||||
offset: fileSize,
|
offset: fileSize,
|
||||||
chunkSize: len(buf),
|
chunkBuf: buf,
|
||||||
chunkBuf: buf,
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release releases the buffer back to the pool.
|
||||||
|
func (s *BackScanner) Release() {
|
||||||
|
sizedPool.Put(s.chunkBuf)
|
||||||
|
s.chunkBuf = nil
|
||||||
|
if s.chunk != nil {
|
||||||
|
sizedPool.Put(s.chunk)
|
||||||
|
s.chunk = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,13 +72,14 @@ func (s *BackScanner) Scan() bool {
|
|||||||
// No more data to read; check remaining buffer
|
// No more data to read; check remaining buffer
|
||||||
if len(s.chunk) > 0 {
|
if len(s.chunk) > 0 {
|
||||||
s.line = s.chunk
|
s.line = s.chunk
|
||||||
|
sizedPool.Put(s.chunk)
|
||||||
s.chunk = nil
|
s.chunk = nil
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
newOffset := max(0, s.offset-int64(s.chunkSize))
|
newOffset := max(0, s.offset-int64(len(s.chunkBuf)))
|
||||||
chunkSize := s.offset - newOffset
|
chunkSize := s.offset - newOffset
|
||||||
chunk := s.chunkBuf[:chunkSize]
|
chunk := s.chunkBuf[:chunkSize]
|
||||||
|
|
||||||
@@ -85,8 +94,19 @@ func (s *BackScanner) Scan() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prepend the chunk to the buffer
|
// Prepend the chunk to the buffer
|
||||||
clone := append([]byte{}, chunk[:n]...)
|
if s.chunk == nil { // first chunk
|
||||||
s.chunk = append(clone, s.chunk...)
|
s.chunk = sizedPool.GetSized(2 * len(s.chunkBuf))
|
||||||
|
copy(s.chunk, chunk[:n])
|
||||||
|
s.chunk = s.chunk[:n]
|
||||||
|
} else {
|
||||||
|
neededSize := n + len(s.chunk)
|
||||||
|
newChunk := sizedPool.GetSized(max(neededSize, 2*len(s.chunkBuf)))
|
||||||
|
copy(newChunk, chunk[:n])
|
||||||
|
copy(newChunk[n:], s.chunk)
|
||||||
|
sizedPool.Put(s.chunk)
|
||||||
|
s.chunk = newChunk[:neededSize]
|
||||||
|
}
|
||||||
|
|
||||||
s.offset = newOffset
|
s.offset = newOffset
|
||||||
|
|
||||||
// Check for newline in the updated buffer
|
// Check for newline in the updated buffer
|
||||||
@@ -111,12 +131,3 @@ func (s *BackScanner) Bytes() []byte {
|
|||||||
func (s *BackScanner) Err() error {
|
func (s *BackScanner) Err() error {
|
||||||
return s.err
|
return s.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BackScanner) Reset() error {
|
|
||||||
_, err := s.file.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*s = *newBackScanner(s.file, s.size, s.chunkBuf)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
package accesslog
|
package accesslog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
expect "github.com/yusing/goutils/testing"
|
|
||||||
|
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
@@ -135,88 +137,40 @@ func TestBackScannerWithVaryingChunkSizes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func logEntry() []byte {
|
var logEntry = func() func() []byte {
|
||||||
accesslog := NewMockAccessLogger(task.RootTask("test", false), &RequestLoggerConfig{
|
accesslog := NewMockAccessLogger(task.RootTask("test", false), &RequestLoggerConfig{
|
||||||
Format: FormatJSON,
|
Format: FormatJSON,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
contentTypes := []string{"application/json", "text/html", "text/plain", "application/xml", "application/x-www-form-urlencoded"}
|
||||||
|
userAgents := []string{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/120.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/120.0", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/120.0"}
|
||||||
|
methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"}
|
||||||
|
paths := []string{"/", "/about", "/contact", "/login", "/logout", "/register", "/profile"}
|
||||||
|
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
_, _ = w.Write([]byte("hello"))
|
allocSize := rand.IntN(8192)
|
||||||
|
w.Header().Set("Content-Type", contentTypes[rand.IntN(len(contentTypes))])
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(allocSize))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
}))
|
}))
|
||||||
srv.URL = "http://localhost:8080"
|
srv.URL = "http://localhost:8080"
|
||||||
defer srv.Close()
|
|
||||||
// make a request to the server
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, srv.URL, nil)
|
|
||||||
res := httptest.NewRecorder()
|
|
||||||
// server the request
|
|
||||||
srv.Config.Handler.ServeHTTP(res, req)
|
|
||||||
b := accesslog.(RequestFormatter).AppendRequestLog(nil, req, res.Result())
|
|
||||||
if b[len(b)-1] != '\n' {
|
|
||||||
b = append(b, '\n')
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReset(t *testing.T) {
|
return func() []byte {
|
||||||
file, err := afero.TempFile(afero.NewOsFs(), "", "accesslog")
|
// make a request to the server
|
||||||
if err != nil {
|
req, _ := http.NewRequest(http.MethodGet, srv.URL, nil)
|
||||||
t.Fatalf("failed to create temp file: %v", err)
|
res := httptest.NewRecorder()
|
||||||
|
req.Header.Set("User-Agent", userAgents[rand.IntN(len(userAgents))])
|
||||||
|
req.Method = methods[rand.IntN(len(methods))]
|
||||||
|
req.URL.Path = paths[rand.IntN(len(paths))]
|
||||||
|
// server the request
|
||||||
|
srv.Config.Handler.ServeHTTP(res, req)
|
||||||
|
b := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||||
|
accesslog.(RequestFormatter).AppendRequestLog(b, req, res.Result())
|
||||||
|
return b.Bytes()
|
||||||
}
|
}
|
||||||
defer os.Remove(file.Name())
|
}()
|
||||||
line := logEntry()
|
|
||||||
nLines := 1000
|
|
||||||
for range nLines {
|
|
||||||
_, err := file.Write(line)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to write to temp file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
linesRead := 0
|
|
||||||
stat, _ := file.Stat()
|
|
||||||
s := NewBackScanner(file, stat.Size(), defaultChunkSize)
|
|
||||||
for s.Scan() {
|
|
||||||
linesRead++
|
|
||||||
}
|
|
||||||
if err := s.Err(); err != nil {
|
|
||||||
t.Errorf("scanner error: %v", err)
|
|
||||||
}
|
|
||||||
expect.Equal(t, linesRead, nLines)
|
|
||||||
err = s.Reset()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to reset scanner: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
linesRead = 0
|
|
||||||
for s.Scan() {
|
|
||||||
linesRead++
|
|
||||||
}
|
|
||||||
if err := s.Err(); err != nil {
|
|
||||||
t.Errorf("scanner error: %v", err)
|
|
||||||
}
|
|
||||||
expect.Equal(t, linesRead, nLines)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 100000 log entries.
|
// 100000 log entries.
|
||||||
func BenchmarkBackScanner(b *testing.B) {
|
|
||||||
mockFile := NewMockFile(false)
|
|
||||||
line := logEntry()
|
|
||||||
for range 100000 {
|
|
||||||
_, _ = mockFile.Write(line)
|
|
||||||
}
|
|
||||||
for i := range 14 {
|
|
||||||
chunkSize := (2 << i) * kilobyte
|
|
||||||
scanner := NewBackScanner(mockFile, mockFile.MustSize(), chunkSize)
|
|
||||||
name := strutils.FormatByteSize(chunkSize)
|
|
||||||
b.ResetTimer()
|
|
||||||
b.Run(name, func(b *testing.B) {
|
|
||||||
for b.Loop() {
|
|
||||||
_ = scanner.Reset()
|
|
||||||
for scanner.Scan() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkBackScannerRealFile(b *testing.B) {
|
func BenchmarkBackScannerRealFile(b *testing.B) {
|
||||||
file, err := afero.TempFile(afero.NewOsFs(), "", "accesslog")
|
file, err := afero.TempFile(afero.NewOsFs(), "", "accesslog")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -224,51 +178,58 @@ func BenchmarkBackScannerRealFile(b *testing.B) {
|
|||||||
}
|
}
|
||||||
defer os.Remove(file.Name())
|
defer os.Remove(file.Name())
|
||||||
|
|
||||||
for range 10000 {
|
buf := bytes.NewBuffer(nil)
|
||||||
_, err = file.Write(logEntry())
|
for range 100000 {
|
||||||
if err != nil {
|
buf.Write(logEntry())
|
||||||
b.Fatalf("failed to write to temp file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stat, _ := file.Stat()
|
fSize := int64(buf.Len())
|
||||||
scanner := NewBackScanner(file, stat.Size(), 256*kilobyte)
|
_, err = file.Write(buf.Bytes())
|
||||||
b.ResetTimer()
|
if err != nil {
|
||||||
for scanner.Scan() {
|
b.Fatalf("failed to write to file: %v", err)
|
||||||
}
|
}
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
b.Errorf("scanner error: %v", err)
|
// file position does not matter, Seek not needed
|
||||||
|
|
||||||
|
for i := range 12 {
|
||||||
|
chunkSize := (2 << i) * kilobyte
|
||||||
|
name := strutils.FormatByteSize(chunkSize)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.Run(name, func(b *testing.B) {
|
||||||
|
for b.Loop() {
|
||||||
|
scanner := NewBackScanner(file, fSize, chunkSize)
|
||||||
|
for scanner.Scan() {
|
||||||
|
}
|
||||||
|
scanner.Release()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
BenchmarkBackScanner
|
BenchmarkBackScannerRealFile
|
||||||
BenchmarkBackScanner/2_KiB
|
BenchmarkBackScannerRealFile/2_KiB
|
||||||
BenchmarkBackScanner/2_KiB-20 52 23254071 ns/op 67596663 B/op 26420 allocs/op
|
BenchmarkBackScannerRealFile/2_KiB-10 21 51796773 ns/op 619 B/op 1 allocs/op
|
||||||
BenchmarkBackScanner/4_KiB
|
BenchmarkBackScannerRealFile/4_KiB
|
||||||
BenchmarkBackScanner/4_KiB-20 55 20961059 ns/op 62529378 B/op 13211 allocs/op
|
BenchmarkBackScannerRealFile/4_KiB-10 36 32081281 ns/op 699 B/op 1 allocs/op
|
||||||
BenchmarkBackScanner/8_KiB
|
BenchmarkBackScannerRealFile/8_KiB
|
||||||
BenchmarkBackScanner/8_KiB-20 64 18242460 ns/op 62951141 B/op 6608 allocs/op
|
BenchmarkBackScannerRealFile/8_KiB-10 57 22155619 ns/op 847 B/op 1 allocs/op
|
||||||
BenchmarkBackScanner/16_KiB
|
BenchmarkBackScannerRealFile/16_KiB
|
||||||
BenchmarkBackScanner/16_KiB-20 52 20162076 ns/op 62940256 B/op 3306 allocs/op
|
BenchmarkBackScannerRealFile/16_KiB-10 62 21323125 ns/op 1449 B/op 1 allocs/op
|
||||||
BenchmarkBackScanner/32_KiB
|
BenchmarkBackScannerRealFile/32_KiB
|
||||||
BenchmarkBackScanner/32_KiB-20 54 19247968 ns/op 67553645 B/op 1656 allocs/op
|
BenchmarkBackScannerRealFile/32_KiB-10 63 17534883 ns/op 2729 B/op 1 allocs/op
|
||||||
BenchmarkBackScanner/64_KiB
|
BenchmarkBackScannerRealFile/64_KiB
|
||||||
BenchmarkBackScanner/64_KiB-20 60 20909046 ns/op 64053342 B/op 827 allocs/op
|
BenchmarkBackScannerRealFile/64_KiB-10 73 17877029 ns/op 4617 B/op 1 allocs/op
|
||||||
BenchmarkBackScanner/128_KiB
|
BenchmarkBackScannerRealFile/128_KiB
|
||||||
BenchmarkBackScanner/128_KiB-20 68 17759890 ns/op 62201945 B/op 414 allocs/op
|
BenchmarkBackScannerRealFile/128_KiB-10 75 17797267 ns/op 8866 B/op 1 allocs/op
|
||||||
BenchmarkBackScanner/256_KiB
|
BenchmarkBackScannerRealFile/256_KiB
|
||||||
BenchmarkBackScanner/256_KiB-20 52 19531877 ns/op 61030487 B/op 208 allocs/op
|
BenchmarkBackScannerRealFile/256_KiB-10 67 16732108 ns/op 19691 B/op 1 allocs/op
|
||||||
BenchmarkBackScanner/512_KiB
|
BenchmarkBackScannerRealFile/512_KiB
|
||||||
BenchmarkBackScanner/512_KiB-20 54 19124656 ns/op 61030485 B/op 208 allocs/op
|
BenchmarkBackScannerRealFile/512_KiB-10 70 17121683 ns/op 37577 B/op 1 allocs/op
|
||||||
BenchmarkBackScanner/1_MiB
|
BenchmarkBackScannerRealFile/1_MiB
|
||||||
BenchmarkBackScanner/1_MiB-20 67 17078936 ns/op 61030495 B/op 208 allocs/op
|
BenchmarkBackScannerRealFile/1_MiB-10 51 19615791 ns/op 102930 B/op 1 allocs/op
|
||||||
BenchmarkBackScanner/2_MiB
|
BenchmarkBackScannerRealFile/2_MiB
|
||||||
BenchmarkBackScanner/2_MiB-20 66 18467421 ns/op 61030492 B/op 208 allocs/op
|
BenchmarkBackScannerRealFile/2_MiB-10 26 41744928 ns/op 77595287 B/op 57 allocs/op
|
||||||
BenchmarkBackScanner/4_MiB
|
BenchmarkBackScannerRealFile/4_MiB
|
||||||
BenchmarkBackScanner/4_MiB-20 68 17214573 ns/op 61030486 B/op 208 allocs/op
|
BenchmarkBackScannerRealFile/4_MiB-10 22 48081521 ns/op 79692224 B/op 49 allocs/op
|
||||||
BenchmarkBackScanner/8_MiB
|
|
||||||
BenchmarkBackScanner/8_MiB-20 57 18235229 ns/op 61030492 B/op 208 allocs/op
|
|
||||||
BenchmarkBackScanner/16_MiB
|
|
||||||
BenchmarkBackScanner/16_MiB-20 57 19343441 ns/op 61030499 B/op 208 allocs/op
|
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package accesslog
|
package accesslog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
@@ -9,16 +10,15 @@ import (
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
ConfigBase struct {
|
ConfigBase struct {
|
||||||
B int `json:"buffer_size"` // Deprecated: buffer size is adjusted dynamically
|
Path string `json:"path,omitempty"`
|
||||||
Path string `json:"path"`
|
|
||||||
Stdout bool `json:"stdout"`
|
Stdout bool `json:"stdout"`
|
||||||
Retention *Retention `json:"retention" aliases:"keep"`
|
Retention *Retention `json:"retention" aliases:"keep"`
|
||||||
RotateInterval time.Duration `json:"rotate_interval,omitempty" swaggertype:"primitive,integer"`
|
RotateInterval time.Duration `json:"rotate_interval,omitempty" swaggertype:"primitive,integer"`
|
||||||
}
|
} // @name AccessLoggerConfigBase
|
||||||
ACLLoggerConfig struct {
|
ACLLoggerConfig struct {
|
||||||
ConfigBase
|
ConfigBase
|
||||||
LogAllowed bool `json:"log_allowed"`
|
LogAllowed bool `json:"log_allowed"`
|
||||||
}
|
} // @name ACLLoggerConfig
|
||||||
RequestLoggerConfig struct {
|
RequestLoggerConfig struct {
|
||||||
ConfigBase
|
ConfigBase
|
||||||
Format Format `json:"format" validate:"oneof=common combined json"`
|
Format Format `json:"format" validate:"oneof=common combined json"`
|
||||||
@@ -32,21 +32,21 @@ type (
|
|||||||
}
|
}
|
||||||
AnyConfig interface {
|
AnyConfig interface {
|
||||||
ToConfig() *Config
|
ToConfig() *Config
|
||||||
Writers() ([]Writer, error)
|
Writers() ([]File, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
Format string
|
Format string
|
||||||
Filters struct {
|
Filters struct {
|
||||||
StatusCodes LogFilter[*StatusCodeRange] `json:"status_codes"`
|
StatusCodes LogFilter[*StatusCodeRange] `json:"status_codes,omitzero"`
|
||||||
Method LogFilter[HTTPMethod] `json:"method"`
|
Method LogFilter[HTTPMethod] `json:"method,omitzero"`
|
||||||
Host LogFilter[Host] `json:"host"`
|
Host LogFilter[Host] `json:"host,omitzero"`
|
||||||
Headers LogFilter[*HTTPHeader] `json:"headers"` // header exists or header == value
|
Headers LogFilter[*HTTPHeader] `json:"headers,omitzero"` // header exists or header == value
|
||||||
CIDR LogFilter[*CIDR] `json:"cidr"`
|
CIDR LogFilter[*CIDR] `json:"cidr,omitzero"`
|
||||||
}
|
}
|
||||||
Fields struct {
|
Fields struct {
|
||||||
Headers FieldConfig `json:"headers" aliases:"header"`
|
Headers FieldConfig `json:"headers,omitzero" aliases:"header"`
|
||||||
Query FieldConfig `json:"query" aliases:"queries"`
|
Query FieldConfig `json:"query,omitzero" aliases:"queries"`
|
||||||
Cookies FieldConfig `json:"cookies" aliases:"cookie"`
|
Cookies FieldConfig `json:"cookies,omitzero" aliases:"cookie"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,17 +66,17 @@ func (cfg *ConfigBase) Validate() gperr.Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Writers returns a list of writers for the config.
|
// Writers returns a list of writers for the config.
|
||||||
func (cfg *ConfigBase) Writers() ([]Writer, error) {
|
func (cfg *ConfigBase) Writers() ([]File, error) {
|
||||||
writers := make([]Writer, 0, 2)
|
writers := make([]File, 0, 2)
|
||||||
if cfg.Path != "" {
|
if cfg.Path != "" {
|
||||||
io, err := NewFileIO(cfg.Path)
|
f, err := OpenFile(cfg.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
writers = append(writers, io)
|
writers = append(writers, f)
|
||||||
}
|
}
|
||||||
if cfg.Stdout {
|
if cfg.Stdout {
|
||||||
writers = append(writers, NewStdout())
|
writers = append(writers, stdout)
|
||||||
}
|
}
|
||||||
return writers, nil
|
return writers, nil
|
||||||
}
|
}
|
||||||
@@ -95,6 +95,16 @@ func (cfg *RequestLoggerConfig) ToConfig() *Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) ShouldLogRequest(req *http.Request, res *http.Response) bool {
|
||||||
|
if cfg.req == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return cfg.req.Filters.StatusCodes.CheckKeep(req, res) &&
|
||||||
|
cfg.req.Filters.Method.CheckKeep(req, res) &&
|
||||||
|
cfg.req.Filters.Headers.CheckKeep(req, res) &&
|
||||||
|
cfg.req.Filters.CIDR.CheckKeep(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
func DefaultRequestLoggerConfig() *RequestLoggerConfig {
|
func DefaultRequestLoggerConfig() *RequestLoggerConfig {
|
||||||
return &RequestLoggerConfig{
|
return &RequestLoggerConfig{
|
||||||
ConfigBase: ConfigBase{
|
ConfigBase: ConfigBase{
|
||||||
|
|||||||
73
internal/logging/accesslog/console_logger.go
Normal file
73
internal/logging/accesslog/console_logger.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package accesslog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConsoleLogger struct {
|
||||||
|
cfg *Config
|
||||||
|
|
||||||
|
formatter ConsoleFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
var stdoutLogger = func() *zerolog.Logger {
|
||||||
|
l := zerolog.New(zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
|
||||||
|
w.Out = os.Stdout
|
||||||
|
w.TimeFormat = zerolog.TimeFieldFormat
|
||||||
|
w.FieldsOrder = []string{
|
||||||
|
"uri", "protocol", "type", "size",
|
||||||
|
"useragent", "query", "headers", "cookies",
|
||||||
|
"error", "iso_code", "time_zone"}
|
||||||
|
})).With().Str("level", zerolog.InfoLevel.String()).Timestamp().Logger()
|
||||||
|
return &l
|
||||||
|
}()
|
||||||
|
|
||||||
|
// placeholder for console logger
|
||||||
|
var stdout File = &sharedFileHandle{}
|
||||||
|
|
||||||
|
func NewConsoleLogger(cfg *Config) AccessLogger {
|
||||||
|
if cfg == nil {
|
||||||
|
panic("accesslog: NewConsoleLogger called with nil config")
|
||||||
|
}
|
||||||
|
l := &ConsoleLogger{
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
if cfg.req != nil {
|
||||||
|
l.formatter = ConsoleFormatter{cfg: &cfg.req.Fields}
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ConsoleLogger) Config() *Config {
|
||||||
|
return l.cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ConsoleLogger) LogRequest(req *http.Request, res *http.Response) {
|
||||||
|
if !l.cfg.ShouldLogRequest(req, res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.formatter.LogRequestZeroLog(stdoutLogger, req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ConsoleLogger) LogError(req *http.Request, err error) {
|
||||||
|
log := stdoutLogger.With().Err(err).Logger()
|
||||||
|
l.formatter.LogRequestZeroLog(&log, req, internalErrorResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ConsoleLogger) LogACL(info *maxmind.IPInfo, blocked bool) {
|
||||||
|
ConsoleACLFormatter{}.LogACLZeroLog(stdoutLogger, info, blocked)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ConsoleLogger) Flush() {
|
||||||
|
// No-op for console logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ConsoleLogger) Close() error {
|
||||||
|
// No-op for console logger
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -20,25 +20,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
AccessLogger interface {
|
File interface {
|
||||||
Log(req *http.Request, res *http.Response)
|
io.WriteCloser
|
||||||
LogError(req *http.Request, err error)
|
supportRotate
|
||||||
LogACL(info *maxmind.IPInfo, blocked bool)
|
Name() string
|
||||||
|
|
||||||
Config() *Config
|
|
||||||
|
|
||||||
Flush()
|
|
||||||
Close() error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
accessLogger struct {
|
fileAccessLogger struct {
|
||||||
task *task.Task
|
task *task.Task
|
||||||
cfg *Config
|
cfg *Config
|
||||||
|
|
||||||
writer BufferedWriter
|
writer BufferedWriter
|
||||||
supportRotate SupportRotate
|
file File
|
||||||
writeLock *sync.Mutex
|
writeLock *sync.Mutex
|
||||||
closed bool
|
closed bool
|
||||||
|
|
||||||
writeCount int64
|
writeCount int64
|
||||||
bufSize int
|
bufSize int
|
||||||
@@ -48,32 +43,7 @@ type (
|
|||||||
logger zerolog.Logger
|
logger zerolog.Logger
|
||||||
|
|
||||||
RequestFormatter
|
RequestFormatter
|
||||||
ACLFormatter
|
ACLLogFormatter
|
||||||
}
|
|
||||||
|
|
||||||
Writer interface {
|
|
||||||
io.WriteCloser
|
|
||||||
ShouldBeBuffered() bool
|
|
||||||
Name() string // file name or path
|
|
||||||
}
|
|
||||||
|
|
||||||
SupportRotate interface {
|
|
||||||
io.Writer
|
|
||||||
supportRotate
|
|
||||||
Name() string
|
|
||||||
}
|
|
||||||
|
|
||||||
AccessLogRotater interface {
|
|
||||||
Rotate(result *RotateResult) (rotated bool, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
RequestFormatter interface {
|
|
||||||
// AppendRequestLog appends a log line to line with or without a trailing newline
|
|
||||||
AppendRequestLog(line []byte, req *http.Request, res *http.Response) []byte
|
|
||||||
}
|
|
||||||
ACLFormatter interface {
|
|
||||||
// AppendACLLog appends a log line to line with or without a trailing newline
|
|
||||||
AppendACLLog(line []byte, info *maxmind.IPInfo, blocked bool) []byte
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -96,112 +66,87 @@ const (
|
|||||||
var bytesPool = synk.GetUnsizedBytesPool()
|
var bytesPool = synk.GetUnsizedBytesPool()
|
||||||
var sizedPool = synk.GetSizedBytesPool()
|
var sizedPool = synk.GetSizedBytesPool()
|
||||||
|
|
||||||
func NewAccessLogger(parent task.Parent, cfg AnyConfig) (AccessLogger, error) {
|
func NewFileAccessLogger(parent task.Parent, file File, anyCfg AnyConfig) AccessLogger {
|
||||||
writers, err := cfg.Writers()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewMultiAccessLogger(parent, cfg, writers), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMockAccessLogger(parent task.Parent, cfg *RequestLoggerConfig) AccessLogger {
|
|
||||||
return NewAccessLoggerWithIO(parent, NewMockFile(true), cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAccessLoggerWithIO(parent task.Parent, writer Writer, anyCfg AnyConfig) AccessLogger {
|
|
||||||
cfg := anyCfg.ToConfig()
|
cfg := anyCfg.ToConfig()
|
||||||
if cfg.RotateInterval == 0 {
|
if cfg.RotateInterval == 0 {
|
||||||
cfg.RotateInterval = defaultRotateInterval
|
cfg.RotateInterval = defaultRotateInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
l := &accessLogger{
|
name := file.Name()
|
||||||
task: parent.Subtask("accesslog."+writer.Name(), true),
|
l := &fileAccessLogger{
|
||||||
|
task: parent.Subtask("accesslog."+name, true),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
bufSize: InitialBufferSize,
|
bufSize: InitialBufferSize,
|
||||||
errRateLimiter: rate.NewLimiter(rate.Every(errRateLimit), errBurst),
|
errRateLimiter: rate.NewLimiter(rate.Every(errRateLimit), errBurst),
|
||||||
logger: log.With().Str("file", writer.Name()).Logger(),
|
logger: log.With().Str("file", name).Logger(),
|
||||||
}
|
}
|
||||||
|
|
||||||
l.writeLock, _ = writerLocks.LoadOrStore(writer.Name(), &sync.Mutex{})
|
l.writeLock, _ = writerLocks.LoadOrStore(name, &sync.Mutex{})
|
||||||
|
|
||||||
if writer.ShouldBeBuffered() {
|
l.writer = ioutils.NewBufferedWriter(file, InitialBufferSize)
|
||||||
l.writer = ioutils.NewBufferedWriter(writer, InitialBufferSize)
|
l.file = file
|
||||||
} else {
|
|
||||||
l.writer = NewUnbufferedWriter(writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
if supportRotate, ok := writer.(SupportRotate); ok {
|
|
||||||
l.supportRotate = supportRotate
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.req != nil {
|
if cfg.req != nil {
|
||||||
fmt := CommonFormatter{cfg: &cfg.req.Fields}
|
|
||||||
switch cfg.req.Format {
|
switch cfg.req.Format {
|
||||||
case FormatCommon:
|
case FormatCommon:
|
||||||
l.RequestFormatter = &fmt
|
l.RequestFormatter = CommonFormatter{cfg: &cfg.req.Fields}
|
||||||
case FormatCombined:
|
case FormatCombined:
|
||||||
l.RequestFormatter = &CombinedFormatter{fmt}
|
l.RequestFormatter = CombinedFormatter{CommonFormatter{cfg: &cfg.req.Fields}}
|
||||||
case FormatJSON:
|
case FormatJSON:
|
||||||
l.RequestFormatter = &JSONFormatter{fmt}
|
l.RequestFormatter = JSONFormatter{cfg: &cfg.req.Fields}
|
||||||
default: // should not happen, validation has done by validate tags
|
default: // should not happen, validation has done by validate tags
|
||||||
panic("invalid access log format")
|
panic("invalid access log format")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
l.ACLFormatter = ACLLogFormatter{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go l.start()
|
go l.start()
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *accessLogger) Config() *Config {
|
func (l *fileAccessLogger) Config() *Config {
|
||||||
return l.cfg
|
return l.cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *accessLogger) shouldLog(req *http.Request, res *http.Response) bool {
|
func (l *fileAccessLogger) LogRequest(req *http.Request, res *http.Response) {
|
||||||
if !l.cfg.req.Filters.StatusCodes.CheckKeep(req, res) ||
|
if !l.cfg.ShouldLogRequest(req, res) {
|
||||||
!l.cfg.req.Filters.Method.CheckKeep(req, res) ||
|
|
||||||
!l.cfg.req.Filters.Headers.CheckKeep(req, res) ||
|
|
||||||
!l.cfg.req.Filters.CIDR.CheckKeep(req, res) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *accessLogger) Log(req *http.Request, res *http.Response) {
|
|
||||||
if !l.shouldLog(req, res) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
line := bytesPool.Get()
|
line := bytesPool.GetBuffer()
|
||||||
line = l.AppendRequestLog(line, req, res)
|
defer bytesPool.PutBuffer(line)
|
||||||
if line[len(line)-1] != '\n' {
|
l.AppendRequestLog(line, req, res)
|
||||||
line = append(line, '\n')
|
// line is never empty
|
||||||
|
if line.Bytes()[line.Len()-1] != '\n' {
|
||||||
|
line.WriteByte('\n')
|
||||||
}
|
}
|
||||||
l.write(line)
|
l.write(line.Bytes())
|
||||||
bytesPool.Put(line)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *accessLogger) LogError(req *http.Request, err error) {
|
var internalErrorResponse = &http.Response{
|
||||||
l.Log(req, &http.Response{StatusCode: http.StatusInternalServerError, Status: err.Error()})
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Status: http.StatusText(http.StatusInternalServerError),
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *accessLogger) LogACL(info *maxmind.IPInfo, blocked bool) {
|
func (l *fileAccessLogger) LogError(req *http.Request, err error) {
|
||||||
line := bytesPool.Get()
|
l.LogRequest(req, internalErrorResponse)
|
||||||
line = l.AppendACLLog(line, info, blocked)
|
}
|
||||||
if line[len(line)-1] != '\n' {
|
|
||||||
line = append(line, '\n')
|
func (l *fileAccessLogger) LogACL(info *maxmind.IPInfo, blocked bool) {
|
||||||
|
line := bytesPool.GetBuffer()
|
||||||
|
defer bytesPool.PutBuffer(line)
|
||||||
|
l.AppendACLLog(line, info, blocked)
|
||||||
|
// line is never empty
|
||||||
|
if line.Bytes()[line.Len()-1] != '\n' {
|
||||||
|
line.WriteByte('\n')
|
||||||
}
|
}
|
||||||
l.write(line)
|
l.write(line.Bytes())
|
||||||
bytesPool.Put(line)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *accessLogger) ShouldRotate() bool {
|
func (l *fileAccessLogger) ShouldRotate() bool {
|
||||||
return l.supportRotate != nil && l.cfg.Retention.IsValid()
|
return l.cfg.Retention.IsValid()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *accessLogger) Rotate(result *RotateResult) (rotated bool, err error) {
|
func (l *fileAccessLogger) Rotate(result *RotateResult) (rotated bool, err error) {
|
||||||
if !l.ShouldRotate() {
|
if !l.ShouldRotate() {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
@@ -210,11 +155,11 @@ func (l *accessLogger) Rotate(result *RotateResult) (rotated bool, err error) {
|
|||||||
l.writeLock.Lock()
|
l.writeLock.Lock()
|
||||||
defer l.writeLock.Unlock()
|
defer l.writeLock.Unlock()
|
||||||
|
|
||||||
rotated, err = rotateLogFile(l.supportRotate, l.cfg.Retention, result)
|
rotated, err = rotateLogFile(l.file, l.cfg.Retention, result)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *accessLogger) handleErr(err error) {
|
func (l *fileAccessLogger) handleErr(err error) {
|
||||||
if l.errRateLimiter.Allow() {
|
if l.errRateLimiter.Allow() {
|
||||||
gperr.LogError("failed to write access log", err, &l.logger)
|
gperr.LogError("failed to write access log", err, &l.logger)
|
||||||
} else {
|
} else {
|
||||||
@@ -223,7 +168,7 @@ func (l *accessLogger) handleErr(err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *accessLogger) start() {
|
func (l *fileAccessLogger) start() {
|
||||||
defer func() {
|
defer func() {
|
||||||
l.Flush()
|
l.Flush()
|
||||||
l.Close()
|
l.Close()
|
||||||
@@ -259,7 +204,7 @@ func (l *accessLogger) start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *accessLogger) Close() error {
|
func (l *fileAccessLogger) Close() error {
|
||||||
l.writeLock.Lock()
|
l.writeLock.Lock()
|
||||||
defer l.writeLock.Unlock()
|
defer l.writeLock.Unlock()
|
||||||
if l.closed {
|
if l.closed {
|
||||||
@@ -270,7 +215,7 @@ func (l *accessLogger) Close() error {
|
|||||||
return l.writer.Close()
|
return l.writer.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *accessLogger) Flush() {
|
func (l *fileAccessLogger) Flush() {
|
||||||
l.writeLock.Lock()
|
l.writeLock.Lock()
|
||||||
defer l.writeLock.Unlock()
|
defer l.writeLock.Unlock()
|
||||||
if l.closed {
|
if l.closed {
|
||||||
@@ -279,7 +224,7 @@ func (l *accessLogger) Flush() {
|
|||||||
l.writer.Flush()
|
l.writer.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *accessLogger) write(data []byte) {
|
func (l *fileAccessLogger) write(data []byte) {
|
||||||
l.writeLock.Lock()
|
l.writeLock.Lock()
|
||||||
defer l.writeLock.Unlock()
|
defer l.writeLock.Unlock()
|
||||||
if l.closed {
|
if l.closed {
|
||||||
@@ -294,7 +239,7 @@ func (l *accessLogger) write(data []byte) {
|
|||||||
atomic.AddInt64(&l.writeCount, int64(n))
|
atomic.AddInt64(&l.writeCount, int64(n))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *accessLogger) adjustBuffer() {
|
func (l *fileAccessLogger) adjustBuffer() {
|
||||||
wps := int(atomic.SwapInt64(&l.writeCount, 0)) / int(bufferAdjustInterval.Seconds())
|
wps := int(atomic.SwapInt64(&l.writeCount, 0)) / int(bufferAdjustInterval.Seconds())
|
||||||
origBufSize := l.bufSize
|
origBufSize := l.bufSize
|
||||||
newBufSize := origBufSize
|
newBufSize := origBufSize
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package accesslog_test
|
package accesslog_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -53,13 +54,13 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func fmtLog(cfg *RequestLoggerConfig) (ts string, line string) {
|
func fmtLog(cfg *RequestLoggerConfig) (ts string, line string) {
|
||||||
buf := make([]byte, 0, 1024)
|
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||||
|
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
logger := NewMockAccessLogger(testTask, cfg)
|
logger := NewMockAccessLogger(testTask, cfg)
|
||||||
mockable.MockTimeNow(t)
|
mockable.MockTimeNow(t)
|
||||||
buf = logger.(RequestFormatter).AppendRequestLog(buf, req, resp)
|
logger.(RequestFormatter).AppendRequestLog(buf, req, resp)
|
||||||
return t.Format(LogTimeFormat), string(buf)
|
return t.Format(LogTimeFormat), buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccessLoggerCommon(t *testing.T) {
|
func TestAccessLoggerCommon(t *testing.T) {
|
||||||
@@ -141,9 +142,6 @@ func TestAccessLoggerJSON(t *testing.T) {
|
|||||||
expect.Equal(t, entry.UserAgent, ua)
|
expect.Equal(t, entry.UserAgent, ua)
|
||||||
expect.Equal(t, len(entry.Headers), 0)
|
expect.Equal(t, len(entry.Headers), 0)
|
||||||
expect.Equal(t, len(entry.Cookies), 0)
|
expect.Equal(t, len(entry.Cookies), 0)
|
||||||
if status >= 400 {
|
|
||||||
expect.Equal(t, entry.Error, http.StatusText(status))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkAccessLoggerJSON(b *testing.B) {
|
func BenchmarkAccessLoggerJSON(b *testing.B) {
|
||||||
@@ -152,7 +150,7 @@ func BenchmarkAccessLoggerJSON(b *testing.B) {
|
|||||||
logger := NewMockAccessLogger(testTask, config)
|
logger := NewMockAccessLogger(testTask, config)
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for b.Loop() {
|
for b.Loop() {
|
||||||
logger.Log(req, resp)
|
logger.LogRequest(req, resp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +160,6 @@ func BenchmarkAccessLoggerCombined(b *testing.B) {
|
|||||||
logger := NewMockAccessLogger(testTask, config)
|
logger := NewMockAccessLogger(testTask, config)
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for b.Loop() {
|
for b.Loop() {
|
||||||
logger.Log(req, resp)
|
logger.LogRequest(req, resp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,9 +16,11 @@ type (
|
|||||||
CommonFormatter struct {
|
CommonFormatter struct {
|
||||||
cfg *Fields
|
cfg *Fields
|
||||||
}
|
}
|
||||||
CombinedFormatter struct{ CommonFormatter }
|
CombinedFormatter struct{ CommonFormatter }
|
||||||
JSONFormatter struct{ CommonFormatter }
|
JSONFormatter struct{ cfg *Fields }
|
||||||
ACLLogFormatter struct{}
|
ConsoleFormatter struct{ cfg *Fields }
|
||||||
|
ACLLogFormatter struct{}
|
||||||
|
ConsoleACLFormatter struct{}
|
||||||
)
|
)
|
||||||
|
|
||||||
const LogTimeFormat = "02/Jan/2006:15:04:05 -0700"
|
const LogTimeFormat = "02/Jan/2006:15:04:05 -0700"
|
||||||
@@ -30,24 +32,26 @@ func scheme(req *http.Request) string {
|
|||||||
return "http"
|
return "http"
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendRequestURI(line []byte, req *http.Request, query iter.Seq2[string, []string]) []byte {
|
func appendRequestURI(line *bytes.Buffer, req *http.Request, query iter.Seq2[string, []string]) {
|
||||||
uri := req.URL.EscapedPath()
|
uri := req.URL.EscapedPath()
|
||||||
line = append(line, uri...)
|
line.WriteString(uri)
|
||||||
isFirst := true
|
isFirst := true
|
||||||
for k, v := range query {
|
for k, v := range query {
|
||||||
if isFirst {
|
if isFirst {
|
||||||
line = append(line, '?')
|
line.WriteByte('?')
|
||||||
isFirst = false
|
isFirst = false
|
||||||
} else {
|
} else {
|
||||||
line = append(line, '&')
|
line.WriteByte('&')
|
||||||
}
|
}
|
||||||
line = append(line, k...)
|
for i, val := range v {
|
||||||
line = append(line, '=')
|
if i > 0 {
|
||||||
for _, v := range v {
|
line.WriteByte('&')
|
||||||
line = append(line, v...)
|
}
|
||||||
|
line.WriteString(k)
|
||||||
|
line.WriteByte('=')
|
||||||
|
line.WriteString(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return line
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func clientIP(req *http.Request) string {
|
func clientIP(req *http.Request) string {
|
||||||
@@ -58,50 +62,51 @@ func clientIP(req *http.Request) string {
|
|||||||
return req.RemoteAddr
|
return req.RemoteAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *CommonFormatter) AppendRequestLog(line []byte, req *http.Request, res *http.Response) []byte {
|
func (f CommonFormatter) AppendRequestLog(line *bytes.Buffer, req *http.Request, res *http.Response) {
|
||||||
query := f.cfg.Query.IterQuery(req.URL.Query())
|
query := f.cfg.Query.IterQuery(req.URL.Query())
|
||||||
|
|
||||||
line = append(line, req.Host...)
|
line.WriteString(req.Host)
|
||||||
line = append(line, ' ')
|
line.WriteByte(' ')
|
||||||
|
|
||||||
line = append(line, clientIP(req)...)
|
line.WriteString(clientIP(req))
|
||||||
line = append(line, " - - ["...)
|
line.WriteString(" - - [")
|
||||||
|
|
||||||
line = mockable.TimeNow().AppendFormat(line, LogTimeFormat)
|
line.WriteString(mockable.TimeNow().Format(LogTimeFormat))
|
||||||
line = append(line, `] "`...)
|
line.WriteString("] \"")
|
||||||
|
|
||||||
line = append(line, req.Method...)
|
line.WriteString(req.Method)
|
||||||
line = append(line, ' ')
|
line.WriteByte(' ')
|
||||||
line = appendRequestURI(line, req, query)
|
appendRequestURI(line, req, query)
|
||||||
line = append(line, ' ')
|
line.WriteByte(' ')
|
||||||
line = append(line, req.Proto...)
|
line.WriteString(req.Proto)
|
||||||
line = append(line, '"')
|
line.WriteByte('"')
|
||||||
line = append(line, ' ')
|
line.WriteByte(' ')
|
||||||
|
|
||||||
line = strconv.AppendInt(line, int64(res.StatusCode), 10)
|
line.WriteString(strconv.FormatInt(int64(res.StatusCode), 10))
|
||||||
line = append(line, ' ')
|
line.WriteByte(' ')
|
||||||
line = strconv.AppendInt(line, res.ContentLength, 10)
|
line.WriteString(strconv.FormatInt(res.ContentLength, 10))
|
||||||
return line
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *CombinedFormatter) AppendRequestLog(line []byte, req *http.Request, res *http.Response) []byte {
|
func (f CombinedFormatter) AppendRequestLog(line *bytes.Buffer, req *http.Request, res *http.Response) {
|
||||||
line = f.CommonFormatter.AppendRequestLog(line, req, res)
|
f.CommonFormatter.AppendRequestLog(line, req, res)
|
||||||
line = append(line, " \""...)
|
line.WriteString(" \"")
|
||||||
line = append(line, req.Referer()...)
|
line.WriteString(req.Referer())
|
||||||
line = append(line, "\" \""...)
|
line.WriteString("\" \"")
|
||||||
line = append(line, req.UserAgent()...)
|
line.WriteString(req.UserAgent())
|
||||||
line = append(line, '"')
|
line.WriteByte('"')
|
||||||
return line
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *JSONFormatter) AppendRequestLog(line []byte, req *http.Request, res *http.Response) []byte {
|
func (f JSONFormatter) AppendRequestLog(line *bytes.Buffer, req *http.Request, res *http.Response) {
|
||||||
|
logger := zerolog.New(line)
|
||||||
|
f.LogRequestZeroLog(&logger, req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f JSONFormatter) LogRequestZeroLog(logger *zerolog.Logger, req *http.Request, res *http.Response) {
|
||||||
query := f.cfg.Query.ZerologQuery(req.URL.Query())
|
query := f.cfg.Query.ZerologQuery(req.URL.Query())
|
||||||
headers := f.cfg.Headers.ZerologHeaders(req.Header)
|
headers := f.cfg.Headers.ZerologHeaders(req.Header)
|
||||||
cookies := f.cfg.Cookies.ZerologCookies(req.Cookies())
|
cookies := f.cfg.Cookies.ZerologCookies(req.Cookies())
|
||||||
contentType := res.Header.Get("Content-Type")
|
contentType := res.Header.Get("Content-Type")
|
||||||
|
|
||||||
writer := bytes.NewBuffer(line)
|
|
||||||
logger := zerolog.New(writer)
|
|
||||||
event := logger.Info().
|
event := logger.Info().
|
||||||
Str("time", mockable.TimeNow().Format(LogTimeFormat)).
|
Str("time", mockable.TimeNow().Format(LogTimeFormat)).
|
||||||
Str("ip", clientIP(req)).
|
Str("ip", clientIP(req)).
|
||||||
@@ -119,22 +124,33 @@ func (f *JSONFormatter) AppendRequestLog(line []byte, req *http.Request, res *ht
|
|||||||
Object("headers", headers).
|
Object("headers", headers).
|
||||||
Object("cookies", cookies)
|
Object("cookies", cookies)
|
||||||
|
|
||||||
if res.StatusCode >= 400 {
|
|
||||||
if res.Status != "" {
|
|
||||||
event.Str("error", res.Status)
|
|
||||||
} else {
|
|
||||||
event.Str("error", http.StatusText(res.StatusCode))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: zerolog will append a newline to the buffer
|
// NOTE: zerolog will append a newline to the buffer
|
||||||
event.Send()
|
event.Send()
|
||||||
return writer.Bytes()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f ACLLogFormatter) AppendACLLog(line []byte, info *maxmind.IPInfo, blocked bool) []byte {
|
func (f ConsoleFormatter) LogRequestZeroLog(logger *zerolog.Logger, req *http.Request, res *http.Response) {
|
||||||
writer := bytes.NewBuffer(line)
|
contentType := res.Header.Get("Content-Type")
|
||||||
logger := zerolog.New(writer)
|
|
||||||
|
var reqURI bytes.Buffer
|
||||||
|
appendRequestURI(&reqURI, req, f.cfg.Query.IterQuery(req.URL.Query()))
|
||||||
|
|
||||||
|
event := logger.Info().
|
||||||
|
Bytes("uri", reqURI.Bytes()).
|
||||||
|
Str("protocol", req.Proto).
|
||||||
|
Str("type", contentType).
|
||||||
|
Int64("size", res.ContentLength).
|
||||||
|
Str("useragent", req.UserAgent())
|
||||||
|
|
||||||
|
// NOTE: zerolog will append a newline to the buffer
|
||||||
|
event.Msgf("[%d] %s %s://%s from %s", res.StatusCode, req.Method, scheme(req), req.Host, clientIP(req))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f ACLLogFormatter) AppendACLLog(line *bytes.Buffer, info *maxmind.IPInfo, blocked bool) {
|
||||||
|
logger := zerolog.New(line)
|
||||||
|
f.LogACLZeroLog(&logger, info, blocked)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f ACLLogFormatter) LogACLZeroLog(logger *zerolog.Logger, info *maxmind.IPInfo, blocked bool) {
|
||||||
event := logger.Info().
|
event := logger.Info().
|
||||||
Str("time", mockable.TimeNow().Format(LogTimeFormat)).
|
Str("time", mockable.TimeNow().Format(LogTimeFormat)).
|
||||||
Str("ip", info.Str)
|
Str("ip", info.Str)
|
||||||
@@ -144,10 +160,32 @@ func (f ACLLogFormatter) AppendACLLog(line []byte, info *maxmind.IPInfo, blocked
|
|||||||
event.Str("action", "allow")
|
event.Str("action", "allow")
|
||||||
}
|
}
|
||||||
if info.City != nil {
|
if info.City != nil {
|
||||||
event.Str("iso_code", info.City.Country.IsoCode)
|
if isoCode := info.City.Country.IsoCode; isoCode != "" {
|
||||||
event.Str("time_zone", info.City.Location.TimeZone)
|
event.Str("iso_code", isoCode)
|
||||||
|
}
|
||||||
|
if timeZone := info.City.Location.TimeZone; timeZone != "" {
|
||||||
|
event.Str("time_zone", timeZone)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// NOTE: zerolog will append a newline to the buffer
|
// NOTE: zerolog will append a newline to the buffer
|
||||||
event.Send()
|
event.Send()
|
||||||
return writer.Bytes()
|
}
|
||||||
|
|
||||||
|
func (f ConsoleACLFormatter) LogACLZeroLog(logger *zerolog.Logger, info *maxmind.IPInfo, blocked bool) {
|
||||||
|
event := logger.Info()
|
||||||
|
if info.City != nil {
|
||||||
|
if isoCode := info.City.Country.IsoCode; isoCode != "" {
|
||||||
|
event.Str("iso_code", isoCode)
|
||||||
|
}
|
||||||
|
if timeZone := info.City.Location.TimeZone; timeZone != "" {
|
||||||
|
event.Str("time_zone", timeZone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action := "accepted"
|
||||||
|
if blocked {
|
||||||
|
action = "denied"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: zerolog will append a newline to the buffer
|
||||||
|
event.Msgf("request %s from %s", action, info.Str)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type MockFile struct {
|
|||||||
buffered bool
|
buffered bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ SupportRotate = (*MockFile)(nil)
|
var _ File = (*MockFile)(nil)
|
||||||
|
|
||||||
func NewMockFile(buffered bool) *MockFile {
|
func NewMockFile(buffered bool) *MockFile {
|
||||||
f, _ := afero.TempFile(afero.NewMemMapFs(), "", "")
|
f, _ := afero.TempFile(afero.NewMemMapFs(), "", "")
|
||||||
@@ -52,14 +52,9 @@ func (m *MockFile) NumLines() int {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockFile) Size() (int64, error) {
|
|
||||||
stat, _ := m.Stat()
|
|
||||||
return stat.Size(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockFile) MustSize() int64 {
|
func (m *MockFile) MustSize() int64 {
|
||||||
size, _ := m.Size()
|
stat, _ := m.Stat()
|
||||||
return size
|
return stat.Size()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockFile) Close() error {
|
func (m *MockFile) Close() error {
|
||||||
|
|||||||
@@ -15,14 +15,21 @@ type MultiAccessLogger struct {
|
|||||||
//
|
//
|
||||||
// If there is only one writer, it will return a single AccessLogger.
|
// If there is only one writer, it will return a single AccessLogger.
|
||||||
// Otherwise, it will return a MultiAccessLogger that writes to all the writers.
|
// Otherwise, it will return a MultiAccessLogger that writes to all the writers.
|
||||||
func NewMultiAccessLogger(parent task.Parent, cfg AnyConfig, writers []Writer) AccessLogger {
|
func NewMultiAccessLogger(parent task.Parent, cfg AnyConfig, writers []File) AccessLogger {
|
||||||
if len(writers) == 1 {
|
if len(writers) == 1 {
|
||||||
return NewAccessLoggerWithIO(parent, writers[0], cfg)
|
if writers[0] == stdout {
|
||||||
|
return NewConsoleLogger(cfg.ToConfig())
|
||||||
|
}
|
||||||
|
return NewFileAccessLogger(parent, writers[0], cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
accessLoggers := make([]AccessLogger, len(writers))
|
accessLoggers := make([]AccessLogger, len(writers))
|
||||||
for i, writer := range writers {
|
for i, writer := range writers {
|
||||||
accessLoggers[i] = NewAccessLoggerWithIO(parent, writer, cfg)
|
if writer == stdout {
|
||||||
|
accessLoggers[i] = NewConsoleLogger(cfg.ToConfig())
|
||||||
|
} else {
|
||||||
|
accessLoggers[i] = NewFileAccessLogger(parent, writer, cfg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &MultiAccessLogger{accessLoggers}
|
return &MultiAccessLogger{accessLoggers}
|
||||||
}
|
}
|
||||||
@@ -31,9 +38,9 @@ func (m *MultiAccessLogger) Config() *Config {
|
|||||||
return m.accessLoggers[0].Config()
|
return m.accessLoggers[0].Config()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MultiAccessLogger) Log(req *http.Request, res *http.Response) {
|
func (m *MultiAccessLogger) LogRequest(req *http.Request, res *http.Response) {
|
||||||
for _, accessLogger := range m.accessLoggers {
|
for _, accessLogger := range m.accessLoggers {
|
||||||
accessLogger.Log(req, res)
|
accessLogger.LogRequest(req, res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user