mirror of
https://github.com/yusing/godoxy.git
synced 2026-02-06 02:29:31 +01:00
Compare commits
79 Commits
feat/agent
...
v0.25.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0633cacb2a | ||
|
|
bf5b231e52 | ||
|
|
9cda7febb4 | ||
|
|
b3d4255868 | ||
|
|
9c2051840f | ||
|
|
1a3810db3a | ||
|
|
2335ef0fb1 | ||
|
|
fc73803bc1 | ||
|
|
59953fed30 | ||
|
|
57a2ca26db | ||
|
|
09ddb925a3 | ||
|
|
55e09c02b1 | ||
|
|
9adeb3e3dd | ||
|
|
0f087edfd6 | ||
|
|
c29798a48b | ||
|
|
c202e26559 | ||
|
|
568d24d746 | ||
|
|
cdd1353102 | ||
|
|
b4646b665f | ||
|
|
c191676565 | ||
|
|
9a96f3cc53 | ||
|
|
95a72930b5 | ||
|
|
71e5a507ba | ||
|
|
8f7ef5a015 | ||
|
|
a824e4c8c2 | ||
|
|
62fb690417 | ||
|
|
9f036a61f8 | ||
|
|
cdd60d99cd | ||
|
|
e718cd4c4a | ||
|
|
8ce821adb9 | ||
|
|
92598e05a2 | ||
|
|
1c0cd1ff03 | ||
|
|
630629a3fd | ||
|
|
a1f7375e7b | ||
|
|
dba6a4fedf | ||
|
|
6b752059da | ||
|
|
262d386a97 | ||
|
|
8df7eb2fe5 | ||
|
|
b0dc0e714d | ||
|
|
01b8554c0a | ||
|
|
5e32627363 | ||
|
|
f5047f4dfa | ||
|
|
92f8590edd | ||
|
|
17f87d6ece | ||
|
|
92bf8b196f | ||
|
|
077e0bc03b | ||
|
|
1b55573cc4 | ||
|
|
243a9dc388 | ||
|
|
cfe4587ec4 | ||
|
|
f01cfd8459 | ||
|
|
b1953d86c2 | ||
|
|
46f88964bf | ||
|
|
9d20fdb5c2 | ||
|
|
3cf108569b | ||
|
|
c55157193b | ||
|
|
c5886bd1e3 | ||
|
|
8c71d880cb | ||
|
|
2d0058aebc | ||
|
|
079f5f6ef2 | ||
|
|
7ed6c53f6b | ||
|
|
9d6e3fdc87 | ||
|
|
1e567bc950 | ||
|
|
edcde00dcc | ||
|
|
7d466625d6 | ||
|
|
8399a9ece7 | ||
|
|
966f0ab9c3 | ||
|
|
aaa3c9a8d8 | ||
|
|
bc44de3196 | ||
|
|
12b784d126 | ||
|
|
71f6636cc3 | ||
|
|
cc1fe30045 | ||
|
|
4ec352f1f6 | ||
|
|
df530245bd | ||
|
|
1a022bb3f4 | ||
|
|
2e57ca7743 | ||
|
|
69d04f1b76 | ||
|
|
74f97a6621 | ||
|
|
dc1b70d2d7 | ||
|
|
6fac5d2d3e |
37
.github/workflows/docker-image.yml
vendored
37
.github/workflows/docker-image.yml
vendored
@@ -45,11 +45,37 @@ jobs:
|
||||
attestations: write
|
||||
|
||||
steps:
|
||||
- name: Checkout (for tag resolution)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
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
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
@@ -80,14 +106,15 @@ jobs:
|
||||
file: ${{ env.DOCKERFILE }}
|
||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ inputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}
|
||||
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }}
|
||||
type=gha,scope=${{ github.workflow }}-${{ env.CACHE_VARIANT }}-${{ env.PLATFORM_PAIR }}
|
||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.CACHE_VARIANT }}-${{ env.PLATFORM_PAIR }}
|
||||
cache-to: |
|
||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max
|
||||
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }},mode=max
|
||||
type=gha,scope=${{ github.workflow }}-${{ env.CACHE_VARIANT }}-${{ env.PLATFORM_PAIR }},mode=max
|
||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.CACHE_VARIANT }}-${{ env.PLATFORM_PAIR }},mode=max
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
VERSION=${{ env.VERSION_FOR_BUILD }}
|
||||
MAKE_ARGS=${{ env.MAKE_ARGS }}
|
||||
BRANCH=${{ env.BRANCH_FOR_BUILD }}
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -7,3 +7,6 @@
|
||||
[submodule "goutils"]
|
||||
path = goutils
|
||||
url = https://github.com/yusing/goutils.git
|
||||
[submodule "internal/go-proxmox"]
|
||||
path = internal/go-proxmox
|
||||
url = https://github.com/yusing/go-proxmox
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Stage 1: deps
|
||||
FROM golang:1.25.5-alpine AS deps
|
||||
FROM golang:1.25.6-alpine AS deps
|
||||
HEALTHCHECK NONE
|
||||
|
||||
# package version does not matter
|
||||
@@ -14,6 +14,7 @@ WORKDIR /src
|
||||
COPY goutils/go.mod goutils/go.sum ./goutils/
|
||||
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/go-proxmox/go.mod internal/go-proxmox/go.sum ./internal/go-proxmox/
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# remove godoxy stuff from go.mod first
|
||||
@@ -43,6 +44,9 @@ ENV VERSION=${VERSION}
|
||||
ARG MAKE_ARGS
|
||||
ENV MAKE_ARGS=${MAKE_ARGS}
|
||||
|
||||
ARG BRANCH
|
||||
ENV BRANCH=${BRANCH}
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/root/go/pkg/mod \
|
||||
make ${MAKE_ARGS} docker=1 build
|
||||
|
||||
17
Makefile
17
Makefile
@@ -1,12 +1,20 @@
|
||||
shell := /bin/sh
|
||||
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 GOOS = linux
|
||||
|
||||
REPO_URL ?= https://github.com/yusing/godoxy
|
||||
|
||||
WEBUI_DIR ?= ../godoxy-webui
|
||||
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
|
||||
|
||||
ifeq ($(agent), 1)
|
||||
@@ -135,9 +143,6 @@ benchmark:
|
||||
dev-run: build
|
||||
cd dev-data && ${BIN_PATH}
|
||||
|
||||
mtrace:
|
||||
${BIN_PATH} debug-ls-mtrace > mtrace.json
|
||||
|
||||
rapid-crash:
|
||||
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
|
||||
sleep 3 &&\
|
||||
@@ -154,7 +159,7 @@ cloc:
|
||||
scc -w -i go --not-match '_test.go$$'
|
||||
|
||||
push-github:
|
||||
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
||||
git push origin $(BRANCH)
|
||||
|
||||
gen-swagger:
|
||||
# go install github.com/swaggo/swag/cmd/swag@latest
|
||||
@@ -175,4 +180,4 @@ gen-api-types: gen-swagger
|
||||
|
||||
.PHONY: update-wiki
|
||||
update-wiki:
|
||||
DOCS_DIR=${DOCS_DIR} bun --bun scripts/update-wiki/main.ts
|
||||
DOCS_DIR=${DOCS_DIR} REPO_URL=${REPO_URL} bun --bun scripts/update-wiki/main.ts
|
||||
|
||||
63
README.md
63
README.md
@@ -33,6 +33,10 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Setup](#setup)
|
||||
- [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)
|
||||
- [Screenshots](#screenshots)
|
||||
- [idlesleeper](#idlesleeper)
|
||||
@@ -46,8 +50,6 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
||||
|
||||
<https://demo.godoxy.dev>
|
||||
|
||||
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Simple**
|
||||
@@ -69,7 +71,11 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
||||
- Podman
|
||||
- **Idle-sleep**: stop and wake containers based on traffic _(see [screenshots](#idlesleeper))_
|
||||
- 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**
|
||||
- HTTP reserve proxy
|
||||
- TCP/UDP port forwarding
|
||||
@@ -82,7 +88,12 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
||||
- App Dashboard
|
||||
- Config Editor
|
||||
- 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**
|
||||
- Supports **linux/amd64** and **linux/arm64**
|
||||
- **Efficient and Performant**
|
||||
@@ -130,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`.
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
- [安裝](#安裝)
|
||||
- [手動安裝](#手動安裝)
|
||||
- [資料夾結構](#資料夾結構)
|
||||
- [Proxmox 整合](#proxmox-整合)
|
||||
- [自動路由綁定](#自動路由綁定)
|
||||
- [WebUI 管理](#webui-管理)
|
||||
- [API 端點](#api-端點)
|
||||
- [更新 / 卸載系統代理 (System Agent)](#更新--卸載系統代理-system-agent)
|
||||
- [截圖](#截圖)
|
||||
- [閒置休眠](#閒置休眠)
|
||||
@@ -45,8 +49,6 @@
|
||||
|
||||
<https://demo.godoxy.dev>
|
||||
|
||||
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
||||
|
||||
## 主要特點
|
||||
|
||||
- **簡單易用**
|
||||
@@ -69,6 +71,10 @@
|
||||
- **閒置休眠**:根據流量停止和喚醒容器 _(參見[截圖](#閒置休眠))_
|
||||
- Docker 容器
|
||||
- Proxmox LXC 容器
|
||||
- **Proxmox 整合**
|
||||
- **自動路由綁定**:透過比對主機名稱、IP 或別名自動將路由綁定至 Proxmox 節點或 LXC 容器
|
||||
- **LXC 生命週期控制**:可直接從 WebUI 啟動、停止、重新啟動容器
|
||||
- **即時日誌**:透過 WebSocket 串流節點和 LXC 容器的 journalctl 日誌
|
||||
- **流量管理**
|
||||
- HTTP 反向代理
|
||||
- TCP/UDP 連接埠轉送
|
||||
@@ -81,7 +87,12 @@
|
||||
- 應用程式一覽
|
||||
- 設定編輯器
|
||||
- 執行時間與系統指標
|
||||
- Docker 日誌檢視器
|
||||
- **Docker**
|
||||
- 容器生命週期管理 (啟動、停止、重新啟動)
|
||||
- 透過 WebSocket 即時串流容器日誌
|
||||
- **Proxmox**
|
||||
- LXC 容器生命週期管理 (啟動、停止、重新啟動)
|
||||
- 透過 WebSocket 即時串流節點和 LXC 容器 journalctl 日誌
|
||||
- **跨平台支援**
|
||||
- 支援 **linux/amd64** 與 **linux/arm64**
|
||||
- **高效能**
|
||||
@@ -146,6 +157,50 @@
|
||||
└── .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)
|
||||
|
||||
更新:
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
stdlog "log"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
@@ -18,7 +20,6 @@ import (
|
||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
httpServer "github.com/yusing/goutils/server"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
"github.com/yusing/goutils/task"
|
||||
"github.com/yusing/goutils/version"
|
||||
@@ -145,12 +146,19 @@ Tips:
|
||||
runtime := strutils.Title(string(env.Runtime))
|
||||
|
||||
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
|
||||
opts := httpServer.Options{
|
||||
Name: runtime,
|
||||
HTTPAddr: socketproxy.ListenAddr,
|
||||
Handler: socketproxy.NewHandler(),
|
||||
l, err := net.Listen("tcp", socketproxy.ListenAddr)
|
||||
if err != nil {
|
||||
gperr.LogFatal("failed to listen on port", err)
|
||||
}
|
||||
httpServer.StartServer(t, opts)
|
||||
errLog := log.Logger.With().Str("level", "error").Str("component", "socketproxy").Logger()
|
||||
srv := http.Server{
|
||||
Handler: socketproxy.NewHandler(),
|
||||
BaseContext: func(net.Listener) context.Context {
|
||||
return t.Context()
|
||||
},
|
||||
ErrorLog: stdlog.New(&errLog, "", 0),
|
||||
}
|
||||
srv.Serve(l)
|
||||
}
|
||||
|
||||
systeminfo.Poller.Start()
|
||||
|
||||
46
agent/go.mod
46
agent/go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/yusing/godoxy/agent
|
||||
|
||||
go 1.25.5
|
||||
go 1.25.6
|
||||
|
||||
replace (
|
||||
github.com/shirou/gopsutil/v4 => ../internal/gopsutil
|
||||
@@ -15,34 +15,30 @@ replace (
|
||||
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.14.2
|
||||
github.com/bytedance/sonic v1.15.0
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/pion/dtls/v3 v3.0.10
|
||||
github.com/pion/transport/v3 v3.1.1
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/yusing/godoxy v0.0.0-00010101000000-000000000000
|
||||
github.com/yusing/godoxy v0.25.0
|
||||
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
||||
github.com/yusing/goutils v0.7.0
|
||||
github.com/yusing/goutils/server v0.0.0-20260103043911-785deb23bd64
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.11.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/containerd/errdefs v1.0.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/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/cli v29.1.3+incompatible // indirect
|
||||
github.com/docker/cli v29.1.5+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
@@ -56,13 +52,12 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.1 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -76,28 +71,23 @@ require (
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // 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/puzpuzpuz/xsync/v4 v4.2.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.58.0 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/samber/slog-common v0.19.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.68.0 // indirect
|
||||
github.com/vincent-petithory/dataurl v1.0.0 // indirect
|
||||
github.com/yusing/ds v0.3.1 // indirect
|
||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||
github.com/yusing/ds v0.4.1 // indirect
|
||||
github.com/yusing/gointernals v0.1.16 // indirect
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260103043911-785deb23bd64 // indirect
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260103043911-785deb23bd64 // indirect
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260125040745-bcc4b498f878 // indirect
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260125040745-bcc4b498f878 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||
@@ -105,10 +95,10 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
146
agent/go.sum
146
agent/go.sum
@@ -10,10 +10,10 @@ github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
||||
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/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/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
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/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
@@ -37,8 +37,8 @@ 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/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/docker/cli v29.1.3+incompatible h1:+kz9uDWgs+mAaIZojWfFt4d53/jv0ZUOOoSh5ZnH36c=
|
||||
github.com/docker/cli v29.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao=
|
||||
github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
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-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -55,8 +55,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-acme/lego/v4 v4.30.1 h1:tmb6U0lvy8Mc3lQbqKwTat7oAhE8FUYNJ3D0gSg6pJU=
|
||||
github.com/go-acme/lego/v4 v4.30.1/go.mod h1:V7m/Ip+EeFkjOe028+zeH+SwWtESxw1LHelwMIfAjm4=
|
||||
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
|
||||
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -79,12 +79,11 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
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/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -100,8 +99,8 @@ github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
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/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -114,8 +113,8 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8
|
||||
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/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/luthermonson/go-proxmox v0.3.1 h1:h64s4/zIEQ06TBo0phFKcckV441YpvUPgLfRAptYsjY=
|
||||
github.com/luthermonson/go-proxmox v0.3.1/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
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/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@@ -125,8 +124,8 @@ 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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
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/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
||||
@@ -154,20 +153,20 @@ 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/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||
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.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pires/go-proxyproto v0.9.1 h1:wTPjpyk41pJm1Im9BqHtPLuhxfjxL+qNfSikx9ux0WY=
|
||||
github.com/pires/go-proxyproto v0.9.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
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/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
|
||||
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/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
|
||||
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
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/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
@@ -179,15 +178,14 @@ github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89
|
||||
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.0/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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
@@ -204,15 +202,14 @@ 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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusing/ds v0.3.1 h1:mCqTgTQD8RhiBpcysvii5kZ7ZBmqcknVsFubNALGLbY=
|
||||
github.com/yusing/ds v0.3.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
|
||||
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
||||
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
@@ -237,93 +234,30 @@ go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
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.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
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/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
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=
|
||||
|
||||
@@ -27,26 +27,26 @@ graph TD
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Purpose |
|
||||
| -------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| [`config.go`](agent/pkg/agent/config.go) | Core configuration, initialization, and API client logic. |
|
||||
| [`new_agent.go`](agent/pkg/agent/new_agent.go) | Agent creation and certificate generation logic. |
|
||||
| [`docker_compose.go`](agent/pkg/agent/docker_compose.go) | Generator for agent Docker Compose configurations. |
|
||||
| [`bare_metal.go`](agent/pkg/agent/bare_metal.go) | Generator for bare metal installation scripts. |
|
||||
| [`env.go`](agent/pkg/agent/env.go) | Environment configuration types and constants. |
|
||||
| [`common/`](agent/pkg/agent/common) | Shared constants and utilities for agents. |
|
||||
| File | Purpose |
|
||||
| ---------------------------------------- | --------------------------------------------------------- |
|
||||
| [`config.go`](config.go) | Core configuration, initialization, and API client logic. |
|
||||
| [`new_agent.go`](new_agent.go) | Agent creation and certificate generation logic. |
|
||||
| [`docker_compose.go`](docker_compose.go) | Generator for agent Docker Compose configurations. |
|
||||
| [`bare_metal.go`](bare_metal.go) | Generator for bare metal installation scripts. |
|
||||
| [`env.go`](env.go) | Environment configuration types and constants. |
|
||||
| `common/` | Shared constants and utilities for agents. |
|
||||
|
||||
## Core Types
|
||||
|
||||
### [`AgentConfig`](agent/pkg/agent/config.go:29)
|
||||
### [`AgentConfig`](config.go:29)
|
||||
|
||||
The primary struct used by the GoDoxy server to manage a connection to an agent. It stores the agent's address, metadata, and TLS configuration.
|
||||
|
||||
### [`AgentInfo`](agent/pkg/agent/config.go:45)
|
||||
### [`AgentInfo`](config.go:45)
|
||||
|
||||
Contains basic metadata about the agent, including its version, name, and container runtime (Docker or Podman).
|
||||
|
||||
### [`PEMPair`](agent/pkg/agent/new_agent.go:53)
|
||||
### [`PEMPair`](new_agent.go:53)
|
||||
|
||||
A utility struct for handling PEM-encoded certificate and key pairs, supporting encryption, decryption, and conversion to `tls.Certificate`.
|
||||
|
||||
@@ -54,7 +54,7 @@ A utility struct for handling PEM-encoded certificate and key pairs, supporting
|
||||
|
||||
### Certificate Generation
|
||||
|
||||
The [`NewAgent`](agent/pkg/agent/new_agent.go:147) function creates a complete certificate infrastructure for an agent:
|
||||
The [`NewAgent`](new_agent.go:147) function creates a complete certificate infrastructure for an agent:
|
||||
|
||||
- **CA Certificate**: Self-signed root certificate with 1000-year validity.
|
||||
- **Server Certificate**: For the agent's HTTPS server, signed by the CA.
|
||||
@@ -65,18 +65,18 @@ All certificates use ECDSA with P-256 curve and SHA-256 signatures.
|
||||
### Certificate Security
|
||||
|
||||
- Certificates are encrypted using AES-GCM with a provided encryption key.
|
||||
- The [`PEMPair`](agent/pkg/agent/new_agent.go:53) struct provides methods for encryption, decryption, and conversion to `tls.Certificate`.
|
||||
- The [`PEMPair`](new_agent.go:53) struct provides methods for encryption, decryption, and conversion to `tls.Certificate`.
|
||||
- Base64 encoding is used for certificate storage and transmission.
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Secure Communication
|
||||
|
||||
All communication between the GoDoxy server and agents is secured using mutual TLS (mTLS). The [`AgentConfig`](agent/pkg/agent/config.go:29) handles the loading of CA and client certificates to establish secure connections.
|
||||
All communication between the GoDoxy server and agents is secured using mutual TLS (mTLS). The [`AgentConfig`](config.go:29) handles the loading of CA and client certificates to establish secure connections.
|
||||
|
||||
### 2. Agent Discovery and Initialization
|
||||
|
||||
The [`Init`](agent/pkg/agent/config.go:231) and [`InitWithCerts`](agent/pkg/agent/config.go:110) methods allow the server to:
|
||||
The [`Init`](config.go:231) and [`InitWithCerts`](config.go:110) methods allow the server to:
|
||||
|
||||
- Fetch agent metadata (version, name, runtime).
|
||||
- Verify compatibility between server and agent versions.
|
||||
@@ -86,12 +86,12 @@ The [`Init`](agent/pkg/agent/config.go:231) and [`InitWithCerts`](agent/pkg/agen
|
||||
|
||||
The package provides interfaces and implementations for generating deployment artifacts:
|
||||
|
||||
- **Docker Compose**: Generates a `docker-compose.yml` for running the agent as a container via [`AgentComposeConfig.Generate()`](agent/pkg/agent/docker_compose.go:21).
|
||||
- **Bare Metal**: Generates a shell script to install and run the agent as a systemd service via [`AgentEnvConfig.Generate()`](agent/pkg/agent/bare_metal.go:27).
|
||||
- **Docker Compose**: Generates a `docker-compose.yml` for running the agent as a container via [`AgentComposeConfig.Generate()`](docker_compose.go:21).
|
||||
- **Bare Metal**: Generates a shell script to install and run the agent as a systemd service via [`AgentEnvConfig.Generate()`](bare_metal.go:27).
|
||||
|
||||
### 4. Fake Docker Host
|
||||
|
||||
The package supports a "fake" Docker host scheme (`agent://<addr>`) to identify containers managed by an agent, allowing the GoDoxy server to route requests appropriately. See [`IsDockerHostAgent`](agent/pkg/agent/config.go:90) and [`GetAgentAddrFromDockerHost`](agent/pkg/agent/config.go:94).
|
||||
The package supports a "fake" Docker host scheme (`agent://<addr>`) to identify containers managed by an agent, allowing the GoDoxy server to route requests appropriately. See [`IsDockerHostAgent`](config.go:90) and [`GetAgentAddrFromDockerHost`](config.go:94).
|
||||
|
||||
## Usage Example
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -147,9 +148,11 @@ func (s *TCPServer) handle(conn net.Conn) {
|
||||
func (s *TCPServer) redirect(conn net.Conn) (net.Conn, error) {
|
||||
// Read the stream header once as a handshake.
|
||||
var headerBuf [headerSize]byte
|
||||
_ = conn.SetReadDeadline(time.Now().Add(dialTimeout))
|
||||
if _, err := io.ReadFull(conn, headerBuf[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Time{})
|
||||
|
||||
header := ToHeader(&headerBuf)
|
||||
if !header.Validate() {
|
||||
|
||||
@@ -102,10 +102,13 @@ func (s *UDPServer) handleDTLSConnection(clientConn net.Conn) {
|
||||
|
||||
// Read the stream header once as a handshake.
|
||||
var headerBuf [headerSize]byte
|
||||
_ = clientConn.SetReadDeadline(time.Now().Add(dialTimeout))
|
||||
if _, err := io.ReadFull(clientConn, headerBuf[:]); err != nil {
|
||||
s.logger(clientConn).Err(err).Msg("failed to read stream header")
|
||||
return
|
||||
}
|
||||
_ = clientConn.SetReadDeadline(time.Time{})
|
||||
|
||||
header := ToHeader(&headerBuf)
|
||||
if !header.Validate() {
|
||||
s.logger(clientConn).Error().Bytes("header", headerBuf[:]).Msg("invalid stream header received")
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/env"
|
||||
"github.com/yusing/godoxy/agent/pkg/handler"
|
||||
"github.com/yusing/goutils/server"
|
||||
"github.com/yusing/goutils/task"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
CACert, ServerCert *tls.Certificate
|
||||
Port int
|
||||
}
|
||||
|
||||
func StartAgentServer(parent task.Parent, opt Options) {
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(opt.CACert.Leaf)
|
||||
|
||||
// Configure TLS
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*opt.ServerCert},
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
}
|
||||
|
||||
if env.AgentSkipClientCertCheck {
|
||||
tlsConfig.ClientAuth = tls.NoClientCert
|
||||
}
|
||||
|
||||
agentServer := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", opt.Port),
|
||||
Handler: handler.NewAgentHandler(),
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
|
||||
server.Start(parent.Subtask("agent-server", false), agentServer, server.WithLogger(&log.Logger))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.25.5-alpine AS builder
|
||||
FROM golang:1.25.6-alpine AS builder
|
||||
|
||||
HEALTHCHECK NONE
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module github.com/yusing/godoxy/cmd/bench_server
|
||||
|
||||
go 1.25.5
|
||||
go 1.25.6
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.25.5-alpine AS builder
|
||||
FROM golang:1.25.6-alpine AS builder
|
||||
|
||||
HEALTHCHECK NONE
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module github.com/yusing/godoxy/cmd/h2c_test_server
|
||||
|
||||
go 1.25.5
|
||||
go 1.25.6
|
||||
|
||||
require golang.org/x/net v0.48.0
|
||||
require golang.org/x/net v0.49.0
|
||||
|
||||
require golang.org/x/text v0.32.0 // indirect
|
||||
require golang.org/x/text v0.33.0 // indirect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
|
||||
28
cmd/main.go
28
cmd/main.go
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/api"
|
||||
@@ -10,7 +11,7 @@ import (
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/config"
|
||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||
"github.com/yusing/godoxy/internal/logging"
|
||||
"github.com/yusing/godoxy/internal/logging/memlogger"
|
||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||
@@ -32,6 +33,16 @@ func parallel(fns ...func()) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
done := make(chan struct{}, 1)
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-time.After(time.Second * 10):
|
||||
log.Fatal().Msgf("timeout waiting for initialization to complete, exiting...")
|
||||
}
|
||||
}()
|
||||
|
||||
initProfiling()
|
||||
|
||||
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
|
||||
@@ -39,7 +50,7 @@ func main() {
|
||||
log.Trace().Msg("trace enabled")
|
||||
parallel(
|
||||
dnsproviders.InitProviders,
|
||||
homepage.InitIconListCache,
|
||||
iconlist.InitCache,
|
||||
systeminfo.Poller.Start,
|
||||
middleware.LoadComposeFiles,
|
||||
)
|
||||
@@ -69,14 +80,25 @@ func main() {
|
||||
server.StartServer(task.RootTask("api_server", false), server.Options{
|
||||
Name: "api",
|
||||
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()
|
||||
|
||||
uptime.Poller.Start()
|
||||
config.WatchChanges()
|
||||
|
||||
close(done)
|
||||
|
||||
task.WaitExit(config.Value().TimeoutShutdown)
|
||||
}
|
||||
|
||||
|
||||
73
go.mod
73
go.mod
@@ -1,9 +1,10 @@
|
||||
module github.com/yusing/godoxy
|
||||
|
||||
go 1.25.5
|
||||
go 1.25.6
|
||||
|
||||
replace (
|
||||
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/yusing/godoxy/agent => ./agent
|
||||
github.com/yusing/godoxy/internal/dnsproviders => ./internal/dnsproviders
|
||||
@@ -18,18 +19,18 @@ require (
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
|
||||
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||
github.com/gin-gonic/gin v1.11.0 // api server
|
||||
github.com/go-acme/lego/v4 v4.30.1 // acme client
|
||||
github.com/go-acme/lego/v4 v4.31.0 // acme client
|
||||
github.com/go-playground/validator/v10 v10.30.1 // validator
|
||||
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
||||
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/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/puzpuzpuz/xsync/v4 v4.2.0 // lock free map for concurrent operations
|
||||
github.com/pires/go-proxyproto v0.9.1 // proxy protocol support
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 // lock free map for concurrent operations
|
||||
github.com/rs/zerolog v1.34.0 // logging
|
||||
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
||||
golang.org/x/crypto v0.46.0 // encrypting password with bcrypt
|
||||
golang.org/x/net v0.48.0 // HTTP header utilities
|
||||
golang.org/x/crypto v0.47.0 // encrypting password with bcrypt
|
||||
golang.org/x/net v0.49.0 // HTTP header utilities
|
||||
golang.org/x/oauth2 v0.34.0 // oauth2 authentication
|
||||
golang.org/x/sync v0.19.0 // errgroup and singleflight for concurrent operations
|
||||
golang.org/x/time v0.14.0 // time utilities
|
||||
@@ -37,34 +38,34 @@ require (
|
||||
|
||||
require (
|
||||
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
|
||||
github.com/bytedance/sonic v1.14.2 // fast json parsing
|
||||
github.com/docker/cli v29.1.3+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
||||
github.com/goccy/go-yaml v1.19.1 // yaml parsing for different config files
|
||||
github.com/bytedance/sonic v1.15.0 // fast json parsing
|
||||
github.com/docker/cli v29.1.5+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/golang-jwt/jwt/v5 v5.3.0 // jwt authentication
|
||||
github.com/luthermonson/go-proxmox v0.3.1 // proxmox API client
|
||||
github.com/luthermonson/go-proxmox v0.3.2 // proxmox API client
|
||||
github.com/moby/moby/api v1.52.0 // docker API
|
||||
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.58.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/spf13/afero v1.15.0 // afero for file system operations
|
||||
github.com/stretchr/testify v1.11.1 // testing framework
|
||||
github.com/valyala/fasthttp v1.68.0 // fast http for health check
|
||||
github.com/yusing/ds v0.3.1 // data structures and algorithms
|
||||
github.com/yusing/godoxy/agent v0.0.0-20260104140148-1c2515cb298d
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260104140148-1c2515cb298d
|
||||
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/godoxy/agent v0.0.0-20260125091326-9c2051840fd9
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260124133347-9a96f3cc539e
|
||||
github.com/yusing/gointernals v0.1.16
|
||||
github.com/yusing/goutils v0.7.0
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260103043911-785deb23bd64
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260103043911-785deb23bd64
|
||||
github.com/yusing/goutils/server v0.0.0-20260103043911-785deb23bd64
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260125040745-bcc4b498f878
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260125040745-bcc4b498f878
|
||||
github.com/yusing/goutils/server v0.0.0-20260125040745-bcc4b498f878
|
||||
)
|
||||
|
||||
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/compute/metadata v0.9.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||
@@ -92,7 +93,7 @@ require (
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
@@ -103,7 +104,7 @@ require (
|
||||
github.com/magefile/mage v1.15.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.69 // indirect
|
||||
github.com/miekg/dns v1.1.72 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
@@ -121,7 +122,7 @@ require (
|
||||
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/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
@@ -131,15 +132,15 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.uber.org/atomic v1.11.0
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/api v0.258.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
google.golang.org/api v0.262.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -148,7 +149,7 @@ require (
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.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/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
@@ -160,17 +161,17 @@ require (
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/go-resty/resty/v2 v2.17.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||
github.com/linode/linodego v1.63.0 // indirect
|
||||
github.com/linode/linodego v1.64.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.10 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
|
||||
120
go.sum
120
go.sum
@@ -1,12 +1,12 @@
|
||||
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
|
||||
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
|
||||
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
|
||||
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/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/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
@@ -51,10 +51,10 @@ github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
||||
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/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/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
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/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
@@ -76,8 +76,8 @@ 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/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/docker/cli v29.1.3+incompatible h1:+kz9uDWgs+mAaIZojWfFt4d53/jv0ZUOOoSh5ZnH36c=
|
||||
github.com/docker/cli v29.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao=
|
||||
github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
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-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -100,8 +100,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-acme/lego/v4 v4.30.1 h1:tmb6U0lvy8Mc3lQbqKwTat7oAhE8FUYNJ3D0gSg6pJU=
|
||||
github.com/go-acme/lego/v4 v4.30.1/go.mod h1:V7m/Ip+EeFkjOe028+zeH+SwWtESxw1LHelwMIfAjm4=
|
||||
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
|
||||
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -126,14 +126,14 @@ github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6x
|
||||
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
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/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
@@ -151,8 +151,8 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
@@ -177,8 +177,8 @@ github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||
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/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
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=
|
||||
@@ -191,14 +191,12 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/linode/linodego v1.63.0 h1:MdjizfXNJDVJU6ggoJmMO5O9h4KGPGivNX0fzrAnstk=
|
||||
github.com/linode/linodego v1.63.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
|
||||
github.com/linode/linodego v1.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY=
|
||||
github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
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/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/luthermonson/go-proxmox v0.3.1 h1:h64s4/zIEQ06TBo0phFKcckV441YpvUPgLfRAptYsjY=
|
||||
github.com/luthermonson/go-proxmox v0.3.1/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
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/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@@ -210,8 +208,8 @@ 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/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
|
||||
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
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/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
@@ -229,10 +227,10 @@ 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/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
||||
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 h1:l0tH15ACQADZAzC+LZ+mo2tIX4H6uZu0ulrVmG5Tqz0=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 h1:gzB4c6ztb38C/jYiqEaFC+mCGcWFHDji9e6jwymY9d4=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2/go.mod h1:l1qIPIq2uRV5WTSvkbhbl/ndbeOu7OCb3UZ+0+2ZSb8=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1 h1:+fx2mbWeR8XX/vidwpRMepJMtRIYQP44Iezm2oeObVM=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1 h1:GDhBiaIAm/QXLzHJ0ASDdY/6R/9w60+gk8lY5rgfxEQ=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1/go.mod h1:EHScJdbM0gg5Is7e3C0ceRYAFMMsfP4Vf8sBRoxoTgk=
|
||||
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/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
@@ -253,8 +251,8 @@ 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/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||
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.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pires/go-proxyproto v0.9.1 h1:wTPjpyk41pJm1Im9BqHtPLuhxfjxL+qNfSikx9ux0WY=
|
||||
github.com/pires/go-proxyproto v0.9.1/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/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -267,12 +265,12 @@ 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/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
|
||||
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/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
|
||||
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
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/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
@@ -286,8 +284,8 @@ github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
||||
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
@@ -300,7 +298,6 @@ github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
@@ -319,8 +316,8 @@ github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
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/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||
github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
|
||||
@@ -330,8 +327,8 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusing/ds v0.3.1 h1:mCqTgTQD8RhiBpcysvii5kZ7ZBmqcknVsFubNALGLbY=
|
||||
github.com/yusing/ds v0.3.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
|
||||
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
||||
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
@@ -366,15 +363,15 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -384,8 +381,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -405,7 +402,6 @@ golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220715151400-c0bba94af5f8/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -416,8 +412,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -436,8 +432,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -446,19 +442,19 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
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/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/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
|
||||
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
|
||||
google.golang.org/api v0.262.0 h1:4B+3u8He2GwyN8St3Jhnd3XRHlIvc//sBmgHSp78oNY=
|
||||
google.golang.org/api v0.262.0/go.mod h1:jNwmH8BgUBJ/VrUG6/lIl9YiildyLd09r9ZLHiQ6cGI=
|
||||
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/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/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/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=
|
||||
@@ -466,8 +462,8 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
|
||||
gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
||||
2
goutils
2
goutils
Submodule goutils updated: 78fda75d1e...272bc53439
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
@@ -27,9 +26,9 @@ type Config struct {
|
||||
Log *accesslog.ACLLoggerConfig `json:"log"`
|
||||
|
||||
Notify struct {
|
||||
To []string `json:"to"` // list of notification providers
|
||||
Interval time.Duration `json:"interval"` // interval between notifications
|
||||
IncludeAllowed *bool `json:"include_allowed"` // default: false
|
||||
To []string `json:"to,omitempty"` // list of notification providers
|
||||
Interval time.Duration `json:"interval,omitempty"` // interval between notifications
|
||||
IncludeAllowed *bool `json:"include_allowed,omitzero"` // default: false
|
||||
} `json:"notify"`
|
||||
|
||||
config
|
||||
@@ -75,8 +74,7 @@ type ipLog struct {
|
||||
allowed bool
|
||||
}
|
||||
|
||||
// could be nil
|
||||
var ActiveConfig atomic.Pointer[Config]
|
||||
type ContextKey struct{}
|
||||
|
||||
const cacheTTL = 1 * time.Minute
|
||||
|
||||
@@ -292,16 +290,16 @@ func (c *Config) IPAllowed(ip net.IP) bool {
|
||||
}
|
||||
|
||||
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) {
|
||||
c.logAndNotify(ipAndStr, false)
|
||||
c.cacheRecord(ipAndStr, false)
|
||||
return false
|
||||
}
|
||||
if c.Allow.Match(ipAndStr) {
|
||||
c.logAndNotify(ipAndStr, true)
|
||||
c.cacheRecord(ipAndStr, true)
|
||||
return true
|
||||
}
|
||||
|
||||
c.logAndNotify(ipAndStr, c.defaultAllow)
|
||||
c.cacheRecord(ipAndStr, c.defaultAllow)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
@@ -12,6 +13,7 @@ type MatcherFunc func(*maxmind.IPInfo) bool
|
||||
|
||||
type Matcher struct {
|
||||
match MatcherFunc
|
||||
raw string
|
||||
}
|
||||
|
||||
type Matchers []Matcher
|
||||
@@ -46,6 +48,7 @@ func (matcher *Matcher) Parse(s string) error {
|
||||
if len(parts) != 2 {
|
||||
return errSyntax
|
||||
}
|
||||
matcher.raw = s
|
||||
|
||||
switch parts[0] {
|
||||
case MatcherTypeIP:
|
||||
@@ -79,6 +82,18 @@ func (matchers Matchers) Match(ip *maxmind.IPInfo) bool {
|
||||
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 {
|
||||
return func(ip2 *maxmind.IPInfo) bool {
|
||||
return ip.Equal(ip2.IP)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
fileApi "github.com/yusing/godoxy/internal/api/v1/file"
|
||||
homepageApi "github.com/yusing/godoxy/internal/api/v1/homepage"
|
||||
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"
|
||||
"github.com/yusing/godoxy/internal/auth"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
@@ -38,7 +39,7 @@ import (
|
||||
|
||||
// @externalDocs.description GoDoxy Docs
|
||||
// @externalDocs.url https://docs.godoxy.dev
|
||||
func NewHandler() *gin.Engine {
|
||||
func NewHandler(requireAuth bool) *gin.Engine {
|
||||
if !common.IsDebug {
|
||||
gin.SetMode("release")
|
||||
}
|
||||
@@ -51,7 +52,7 @@ func NewHandler() *gin.Engine {
|
||||
|
||||
r.GET("/api/v1/version", apiV1.Version)
|
||||
|
||||
if auth.IsEnabled() {
|
||||
if auth.IsEnabled() && requireAuth {
|
||||
v1Auth := r.Group("/api/v1/auth")
|
||||
{
|
||||
v1Auth.HEAD("/check", authApi.Check)
|
||||
@@ -64,7 +65,7 @@ func NewHandler() *gin.Engine {
|
||||
}
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
if auth.IsEnabled() {
|
||||
if auth.IsEnabled() && requireAuth {
|
||||
v1.Use(AuthMiddleware())
|
||||
}
|
||||
if common.APISkipOriginCheck {
|
||||
@@ -140,6 +141,19 @@ func NewHandler() *gin.Engine {
|
||||
docker.POST("/start", dockerApi.Start)
|
||||
docker.POST("/stop", dockerApi.Stop)
|
||||
docker.POST("/restart", dockerApi.Restart)
|
||||
docker.GET("/stats/:id", dockerApi.Stats)
|
||||
}
|
||||
|
||||
proxmox := v1.Group("/proxmox")
|
||||
{
|
||||
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 |
|
||||
| `auth` | Authentication and session management |
|
||||
| `agent` | Remote agent creation and management |
|
||||
| `proxmox` | Proxmox API management and monitoring |
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -77,15 +78,16 @@ API listening address is configured with `GODOXY_API_ADDR` environment variable.
|
||||
|
||||
### Internal Dependencies
|
||||
|
||||
| Package | Purpose |
|
||||
| ----------------------- | --------------------------- |
|
||||
| `internal/route/routes` | Route storage and iteration |
|
||||
| `internal/docker` | Docker client management |
|
||||
| `internal/config` | Configuration access |
|
||||
| `internal/metrics` | System metrics collection |
|
||||
| `internal/homepage` | Homepage item generation |
|
||||
| `internal/agentpool` | Remote agent management |
|
||||
| `internal/auth` | Authentication services |
|
||||
| Package | Purpose |
|
||||
| ----------------------- | ------------------------------------- |
|
||||
| `internal/route/routes` | Route storage and iteration |
|
||||
| `internal/docker` | Docker client management |
|
||||
| `internal/config` | Configuration access |
|
||||
| `internal/metrics` | System metrics collection |
|
||||
| `internal/homepage` | Homepage item generation |
|
||||
| `internal/agentpool` | Remote agent management |
|
||||
| `internal/auth` | Authentication services |
|
||||
| `internal/proxmox` | Proxmox API management and monitoring |
|
||||
|
||||
### External Dependencies
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/api/pkg/stdcopy"
|
||||
@@ -22,6 +23,7 @@ type LogsQueryParams struct {
|
||||
Since string `form:"from"`
|
||||
Until string `form:"to"`
|
||||
Levels string `form:"levels"`
|
||||
Limit int `form:"limit,default=100" binding:"omitempty,min=1,max=1000"`
|
||||
} // @name LogsQueryParams
|
||||
|
||||
// @x-id "logs"
|
||||
@@ -34,9 +36,10 @@ type LogsQueryParams struct {
|
||||
// @Param id path string true "container id"
|
||||
// @Param stdout query bool false "show stdout"
|
||||
// @Param stderr query bool false "show stderr"
|
||||
// @Param from query string false "from timestamp"
|
||||
// @Param to query string false "to timestamp"
|
||||
// @Param from query string false "from timestamp"
|
||||
// @Param to query string false "to timestamp"
|
||||
// @Param levels query string false "levels"
|
||||
// @Param limit query int false "limit"
|
||||
// @Success 200
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
@@ -77,7 +80,7 @@ func Logs(c *gin.Context) {
|
||||
Until: queryParams.Until,
|
||||
Timestamps: true,
|
||||
Follow: true,
|
||||
Tail: "100",
|
||||
Tail: strconv.Itoa(queryParams.Limit),
|
||||
}
|
||||
if queryParams.Levels != "" {
|
||||
opts.Details = true
|
||||
|
||||
117
internal/api/v1/docker/stats.go
Normal file
117
internal/api/v1/docker/stats.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package dockerapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"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, client.ContainerStatsOptions{Stream: 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, client.ContainerStatsOptions{Stream: 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
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -180,6 +180,85 @@ definitions:
|
||||
total:
|
||||
type: integer
|
||||
type: object
|
||||
ContainerStatsResponse:
|
||||
properties:
|
||||
blkio_stats:
|
||||
allOf:
|
||||
- $ref: '#/definitions/container.BlkioStats'
|
||||
description: |-
|
||||
BlkioStats stores all IO service stats for data read and write.
|
||||
|
||||
This type is Linux-specific and holds many fields that are specific
|
||||
to cgroups v1.
|
||||
|
||||
On a cgroup v2 host, all fields other than "io_service_bytes_recursive"
|
||||
are omitted or "null".
|
||||
|
||||
This type is only populated on Linux and omitted for Windows containers.
|
||||
cpu_stats:
|
||||
allOf:
|
||||
- $ref: '#/definitions/container.CPUStats'
|
||||
description: CPUStats contains CPU related info of the container.
|
||||
id:
|
||||
description: ID is the ID of the container for which the stats were collected.
|
||||
type: string
|
||||
memory_stats:
|
||||
allOf:
|
||||
- $ref: '#/definitions/container.MemoryStats'
|
||||
description: |-
|
||||
MemoryStats aggregates all memory stats since container inception on Linux.
|
||||
Windows returns stats for commit and private working set only.
|
||||
name:
|
||||
description: Name is the name of the container for which the stats were collected.
|
||||
type: string
|
||||
networks:
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/container.NetworkStats'
|
||||
description: |-
|
||||
Networks contains Nntwork statistics for the container per interface.
|
||||
|
||||
This field is omitted if the container has no networking enabled.
|
||||
type: object
|
||||
num_procs:
|
||||
description: |-
|
||||
NumProcs is the number of processors on the system.
|
||||
|
||||
This field is Windows-specific and always zero for Linux containers.
|
||||
type: integer
|
||||
os_type:
|
||||
description: |-
|
||||
OSType is the OS of the container ("linux" or "windows") to allow
|
||||
platform-specific handling of stats.
|
||||
type: string
|
||||
pids_stats:
|
||||
allOf:
|
||||
- $ref: '#/definitions/container.PidsStats'
|
||||
description: |-
|
||||
PidsStats contains Linux-specific stats of a container's process-IDs (PIDs).
|
||||
|
||||
This field is Linux-specific and omitted for Windows containers.
|
||||
precpu_stats:
|
||||
allOf:
|
||||
- $ref: '#/definitions/container.CPUStats'
|
||||
description: PreCPUStats contains the CPUStats of the previous sample.
|
||||
preread:
|
||||
description: |-
|
||||
PreRead is the date and time at which this first sample was collected.
|
||||
This field is not propagated if the "one-shot" option is set. If the
|
||||
"one-shot" option is set, this field may be omitted, empty, or set
|
||||
to a default date (`0001-01-01T00:00:00Z`).
|
||||
type: string
|
||||
read:
|
||||
description: Read is the date and time at which this sample was collected.
|
||||
type: string
|
||||
storage_stats:
|
||||
allOf:
|
||||
- $ref: '#/definitions/container.StorageStats'
|
||||
description: |-
|
||||
StorageStats is the disk I/O stats for read/write on Windows.
|
||||
|
||||
This type is Windows-specific and omitted for Linux containers.
|
||||
type: object
|
||||
ContainerStopMethod:
|
||||
enum:
|
||||
- pause
|
||||
@@ -517,6 +596,33 @@ definitions:
|
||||
$ref: '#/definitions/HomepageItemConfig'
|
||||
type: object
|
||||
type: object
|
||||
IconFetchResult:
|
||||
properties:
|
||||
icon:
|
||||
items:
|
||||
format: int32
|
||||
type: integer
|
||||
type: array
|
||||
statusCode:
|
||||
type: integer
|
||||
type: object
|
||||
IconMetaSearch:
|
||||
properties:
|
||||
Dark:
|
||||
type: boolean
|
||||
Light:
|
||||
type: boolean
|
||||
PNG:
|
||||
type: boolean
|
||||
Ref:
|
||||
type: string
|
||||
SVG:
|
||||
type: boolean
|
||||
Source:
|
||||
$ref: '#/definitions/icons.Source'
|
||||
WebP:
|
||||
type: boolean
|
||||
type: object
|
||||
IdlewatcherConfig:
|
||||
properties:
|
||||
depends_on:
|
||||
@@ -528,13 +634,14 @@ definitions:
|
||||
idle_timeout:
|
||||
allOf:
|
||||
- $ref: '#/definitions/time.Duration'
|
||||
description: "0: no idle watcher.\nPositive: idle watcher with idle timeout.\nNegative:
|
||||
idle watcher as a dependency.\tIdleTimeout time.Duration `json:\"idle_timeout\"
|
||||
json_ext:\"duration\"`"
|
||||
description: |-
|
||||
0: no idle watcher.
|
||||
Positive: idle watcher with idle timeout.
|
||||
Negative: idle watcher as a dependency.
|
||||
no_loading_page:
|
||||
type: boolean
|
||||
proxmox:
|
||||
$ref: '#/definitions/ProxmoxConfig'
|
||||
$ref: '#/definitions/ProxmoxNodeConfig'
|
||||
start_endpoint:
|
||||
description: Optional path that must be hit to start container
|
||||
type: string
|
||||
@@ -826,12 +933,16 @@ definitions:
|
||||
- ProviderTypeDocker
|
||||
- ProviderTypeFile
|
||||
- ProviderTypeAgent
|
||||
ProxmoxConfig:
|
||||
ProxmoxNodeConfig:
|
||||
properties:
|
||||
node:
|
||||
type: string
|
||||
service:
|
||||
type: string
|
||||
vmid:
|
||||
type: integer
|
||||
vmname:
|
||||
type: string
|
||||
required:
|
||||
- node
|
||||
- vmid
|
||||
@@ -851,9 +962,6 @@ definitions:
|
||||
type: object
|
||||
RequestLoggerConfig:
|
||||
properties:
|
||||
buffer_size:
|
||||
description: 'Deprecated: buffer size is adjusted dynamically'
|
||||
type: integer
|
||||
fields:
|
||||
$ref: '#/definitions/accesslog.Fields'
|
||||
filters:
|
||||
@@ -946,6 +1054,10 @@ definitions:
|
||||
description: for backward compatibility
|
||||
type: string
|
||||
x-nullable: true
|
||||
proxmox:
|
||||
allOf:
|
||||
- $ref: '#/definitions/ProxmoxNodeConfig'
|
||||
x-nullable: true
|
||||
purl:
|
||||
type: string
|
||||
response_header_timeout:
|
||||
@@ -959,6 +1071,7 @@ definitions:
|
||||
items:
|
||||
$ref: '#/definitions/rules.Rule'
|
||||
type: array
|
||||
x-nullable: true
|
||||
scheme:
|
||||
enum:
|
||||
- http
|
||||
@@ -1049,16 +1162,10 @@ definitions:
|
||||
- napping
|
||||
- starting
|
||||
type: string
|
||||
display_name:
|
||||
type: string
|
||||
downtime:
|
||||
type: number
|
||||
idle:
|
||||
type: number
|
||||
is_docker:
|
||||
type: boolean
|
||||
is_excluded:
|
||||
type: boolean
|
||||
statuses:
|
||||
items:
|
||||
$ref: '#/definitions/RouteStatus'
|
||||
@@ -1262,6 +1369,100 @@ definitions:
|
||||
username:
|
||||
type: string
|
||||
type: object
|
||||
container.BlkioStatEntry:
|
||||
properties:
|
||||
major:
|
||||
type: integer
|
||||
minor:
|
||||
type: integer
|
||||
op:
|
||||
type: string
|
||||
value:
|
||||
type: integer
|
||||
type: object
|
||||
container.BlkioStats:
|
||||
properties:
|
||||
io_merged_recursive:
|
||||
items:
|
||||
$ref: '#/definitions/container.BlkioStatEntry'
|
||||
type: array
|
||||
io_queue_recursive:
|
||||
items:
|
||||
$ref: '#/definitions/container.BlkioStatEntry'
|
||||
type: array
|
||||
io_service_bytes_recursive:
|
||||
description: number of bytes transferred to and from the block device
|
||||
items:
|
||||
$ref: '#/definitions/container.BlkioStatEntry'
|
||||
type: array
|
||||
io_service_time_recursive:
|
||||
items:
|
||||
$ref: '#/definitions/container.BlkioStatEntry'
|
||||
type: array
|
||||
io_serviced_recursive:
|
||||
items:
|
||||
$ref: '#/definitions/container.BlkioStatEntry'
|
||||
type: array
|
||||
io_time_recursive:
|
||||
items:
|
||||
$ref: '#/definitions/container.BlkioStatEntry'
|
||||
type: array
|
||||
io_wait_time_recursive:
|
||||
items:
|
||||
$ref: '#/definitions/container.BlkioStatEntry'
|
||||
type: array
|
||||
sectors_recursive:
|
||||
items:
|
||||
$ref: '#/definitions/container.BlkioStatEntry'
|
||||
type: array
|
||||
type: object
|
||||
container.CPUStats:
|
||||
properties:
|
||||
cpu_usage:
|
||||
allOf:
|
||||
- $ref: '#/definitions/container.CPUUsage'
|
||||
description: CPU Usage. Linux and Windows.
|
||||
online_cpus:
|
||||
description: Online CPUs. Linux only.
|
||||
type: integer
|
||||
system_cpu_usage:
|
||||
description: System Usage. Linux only.
|
||||
type: integer
|
||||
throttling_data:
|
||||
allOf:
|
||||
- $ref: '#/definitions/container.ThrottlingData'
|
||||
description: Throttling Data. Linux only.
|
||||
type: object
|
||||
container.CPUUsage:
|
||||
properties:
|
||||
percpu_usage:
|
||||
description: |-
|
||||
Total CPU time consumed per core (Linux). Not used on Windows.
|
||||
Units: nanoseconds.
|
||||
items:
|
||||
type: integer
|
||||
type: array
|
||||
total_usage:
|
||||
description: |-
|
||||
Total CPU time consumed.
|
||||
Units: nanoseconds (Linux)
|
||||
Units: 100's of nanoseconds (Windows)
|
||||
type: integer
|
||||
usage_in_kernelmode:
|
||||
description: |-
|
||||
Time spent by tasks of the cgroup in kernel mode (Linux).
|
||||
Time spent by all container processes in kernel mode (Windows).
|
||||
Units: nanoseconds (Linux).
|
||||
Units: 100's of nanoseconds (Windows). Not populated for Hyper-V Containers.
|
||||
type: integer
|
||||
usage_in_usermode:
|
||||
description: |-
|
||||
Time spent by tasks of the cgroup in user mode (Linux).
|
||||
Time spent by all container processes in user mode (Windows).
|
||||
Units: nanoseconds (Linux).
|
||||
Units: 100's of nanoseconds (Windows). Not populated for Hyper-V Containers
|
||||
type: integer
|
||||
type: object
|
||||
container.ContainerState:
|
||||
enum:
|
||||
- created
|
||||
@@ -1299,6 +1500,85 @@ definitions:
|
||||
- StateRemoving
|
||||
- StateExited
|
||||
- StateDead
|
||||
container.MemoryStats:
|
||||
properties:
|
||||
commitbytes:
|
||||
description: committed bytes
|
||||
type: integer
|
||||
commitpeakbytes:
|
||||
description: peak committed bytes
|
||||
type: integer
|
||||
failcnt:
|
||||
description: number of times memory usage hits limits.
|
||||
type: integer
|
||||
limit:
|
||||
type: integer
|
||||
max_usage:
|
||||
description: maximum usage ever recorded.
|
||||
type: integer
|
||||
privateworkingset:
|
||||
description: private working set
|
||||
type: integer
|
||||
stats:
|
||||
additionalProperties:
|
||||
format: int64
|
||||
type: integer
|
||||
description: |-
|
||||
TODO(vishh): Export these as stronger types.
|
||||
all the stats exported via memory.stat.
|
||||
type: object
|
||||
usage:
|
||||
description: current res_counter usage for memory
|
||||
type: integer
|
||||
type: object
|
||||
container.NetworkStats:
|
||||
properties:
|
||||
endpoint_id:
|
||||
description: Endpoint ID. Not used on Linux.
|
||||
type: string
|
||||
instance_id:
|
||||
description: Instance ID. Not used on Linux.
|
||||
type: string
|
||||
rx_bytes:
|
||||
description: Bytes received. Windows and Linux.
|
||||
type: integer
|
||||
rx_dropped:
|
||||
description: Incoming packets dropped. Windows and Linux.
|
||||
type: integer
|
||||
rx_errors:
|
||||
description: |-
|
||||
Received errors. Not used on Windows. Note that we don't `omitempty` this
|
||||
field as it is expected in the >=v1.21 API stats structure.
|
||||
type: integer
|
||||
rx_packets:
|
||||
description: Packets received. Windows and Linux.
|
||||
type: integer
|
||||
tx_bytes:
|
||||
description: Bytes sent. Windows and Linux.
|
||||
type: integer
|
||||
tx_dropped:
|
||||
description: Outgoing packets dropped. Windows and Linux.
|
||||
type: integer
|
||||
tx_errors:
|
||||
description: |-
|
||||
Sent errors. Not used on Windows. Note that we don't `omitempty` this
|
||||
field as it is expected in the >=v1.21 API stats structure.
|
||||
type: integer
|
||||
tx_packets:
|
||||
description: Packets sent. Windows and Linux.
|
||||
type: integer
|
||||
type: object
|
||||
container.PidsStats:
|
||||
properties:
|
||||
current:
|
||||
description: Current is the number of pids in the cgroup
|
||||
type: integer
|
||||
limit:
|
||||
description: |-
|
||||
Limit is the hard limit on the number of pids in the cgroup.
|
||||
A "Limit" of 0 means that there is no limit.
|
||||
type: integer
|
||||
type: object
|
||||
container.PortSummary:
|
||||
properties:
|
||||
IP:
|
||||
@@ -1320,6 +1600,29 @@ definitions:
|
||||
Enum: ["tcp","udp","sctp"]
|
||||
type: string
|
||||
type: object
|
||||
container.StorageStats:
|
||||
properties:
|
||||
read_count_normalized:
|
||||
type: integer
|
||||
read_size_bytes:
|
||||
type: integer
|
||||
write_count_normalized:
|
||||
type: integer
|
||||
write_size_bytes:
|
||||
type: integer
|
||||
type: object
|
||||
container.ThrottlingData:
|
||||
properties:
|
||||
periods:
|
||||
description: Number of periods with throttling active
|
||||
type: integer
|
||||
throttled_periods:
|
||||
description: Number of periods when the container hits its throttling limit.
|
||||
type: integer
|
||||
throttled_time:
|
||||
description: Aggregate time the container was throttled for in nanoseconds.
|
||||
type: integer
|
||||
type: object
|
||||
disk.IOCountersStat:
|
||||
properties:
|
||||
iops:
|
||||
@@ -1428,34 +1731,7 @@ definitions:
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
homepage.FetchResult:
|
||||
properties:
|
||||
icon:
|
||||
items:
|
||||
format: int32
|
||||
type: integer
|
||||
type: array
|
||||
statusCode:
|
||||
type: integer
|
||||
type: object
|
||||
homepage.IconMetaSearch:
|
||||
properties:
|
||||
Dark:
|
||||
type: boolean
|
||||
Light:
|
||||
type: boolean
|
||||
PNG:
|
||||
type: boolean
|
||||
Ref:
|
||||
type: string
|
||||
SVG:
|
||||
type: boolean
|
||||
Source:
|
||||
$ref: '#/definitions/homepage.IconSource'
|
||||
WebP:
|
||||
type: boolean
|
||||
type: object
|
||||
homepage.IconSource:
|
||||
icons.Source:
|
||||
enum:
|
||||
- https://
|
||||
- '@target'
|
||||
@@ -1463,10 +1739,10 @@ definitions:
|
||||
- '@selfhst'
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- IconSourceAbsolute
|
||||
- IconSourceRelative
|
||||
- IconSourceWalkXCode
|
||||
- IconSourceSelfhSt
|
||||
- SourceAbsolute
|
||||
- SourceRelative
|
||||
- SourceWalkXCode
|
||||
- SourceSelfhSt
|
||||
mem.VirtualMemoryStat:
|
||||
properties:
|
||||
available:
|
||||
@@ -1508,6 +1784,37 @@ definitions:
|
||||
type: object
|
||||
netip.Addr:
|
||||
type: object
|
||||
proxmox.NodeStats:
|
||||
properties:
|
||||
cpu_model:
|
||||
type: string
|
||||
cpu_usage:
|
||||
type: string
|
||||
kernel_version:
|
||||
type: string
|
||||
load_avg_15m:
|
||||
type: string
|
||||
load_avg_1m:
|
||||
type: string
|
||||
load_avg_5m:
|
||||
type: string
|
||||
mem_pct:
|
||||
type: string
|
||||
mem_total:
|
||||
type: string
|
||||
mem_usage:
|
||||
type: string
|
||||
pve_version:
|
||||
type: string
|
||||
rootfs_pct:
|
||||
type: string
|
||||
rootfs_total:
|
||||
type: string
|
||||
rootfs_usage:
|
||||
type: string
|
||||
uptime:
|
||||
type: string
|
||||
type: object
|
||||
route.Route:
|
||||
properties:
|
||||
access_log:
|
||||
@@ -1581,6 +1888,10 @@ definitions:
|
||||
description: for backward compatibility
|
||||
type: string
|
||||
x-nullable: true
|
||||
proxmox:
|
||||
allOf:
|
||||
- $ref: '#/definitions/ProxmoxNodeConfig'
|
||||
x-nullable: true
|
||||
purl:
|
||||
type: string
|
||||
response_header_timeout:
|
||||
@@ -1594,6 +1905,7 @@ definitions:
|
||||
items:
|
||||
$ref: '#/definitions/rules.Rule'
|
||||
type: array
|
||||
x-nullable: true
|
||||
scheme:
|
||||
enum:
|
||||
- http
|
||||
@@ -2068,6 +2380,10 @@ paths:
|
||||
in: query
|
||||
name: levels
|
||||
type: string
|
||||
- description: limit
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -2168,6 +2484,43 @@ paths:
|
||||
tags:
|
||||
- docker
|
||||
x-id: start
|
||||
/docker/stats/{id}:
|
||||
get:
|
||||
description: Get container stats by container id
|
||||
parameters:
|
||||
- description: Container ID or route alias
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/ContainerStatsResponse'
|
||||
"400":
|
||||
description: 'Invalid request: id is required or route is not a docker container'
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"403":
|
||||
description: Forbidden
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"404":
|
||||
description: Container not found
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
summary: Get container stats
|
||||
tags:
|
||||
- docker
|
||||
- websocket
|
||||
x-id: stats
|
||||
/docker/stop:
|
||||
post:
|
||||
description: Stop container by container id
|
||||
@@ -2229,7 +2582,7 @@ paths:
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/homepage.FetchResult'
|
||||
$ref: '#/definitions/IconFetchResult'
|
||||
type: array
|
||||
"400":
|
||||
description: 'Bad Request: alias is empty or route is not HTTPRoute'
|
||||
@@ -2811,7 +3164,7 @@ paths:
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/homepage.IconMetaSearch'
|
||||
$ref: '#/definitions/IconMetaSearch'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
@@ -3039,6 +3392,334 @@ paths:
|
||||
- metrics
|
||||
- websocket
|
||||
x-id: uptime
|
||||
/proxmox/journalctl/{node}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get journalctl output for node or LXC container. If vmid is not
|
||||
provided, streams node journalctl.
|
||||
parameters:
|
||||
- description: Node name
|
||||
in: path
|
||||
name: node
|
||||
required: true
|
||||
type: string
|
||||
- description: Limit output lines (1-1000)
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Journalctl output
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: Invalid request
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"404":
|
||||
description: Node not found
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
summary: Get journalctl output
|
||||
tags:
|
||||
- proxmox
|
||||
- websocket
|
||||
x-id: journalctl
|
||||
/proxmox/journalctl/{node}/{vmid}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get journalctl output for node or LXC container. If vmid is not
|
||||
provided, streams node journalctl.
|
||||
parameters:
|
||||
- description: Node name
|
||||
in: path
|
||||
name: node
|
||||
required: true
|
||||
type: string
|
||||
- description: Container VMID (optional - if not provided, streams node journalctl)
|
||||
in: path
|
||||
name: vmid
|
||||
type: integer
|
||||
- description: Limit output lines (1-1000)
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Journalctl output
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: Invalid request
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"404":
|
||||
description: Node not found
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
summary: Get journalctl output
|
||||
tags:
|
||||
- proxmox
|
||||
- websocket
|
||||
x-id: journalctl
|
||||
/proxmox/journalctl/{node}/{vmid}/{service}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get journalctl output for node or LXC container. If vmid is not
|
||||
provided, streams node journalctl.
|
||||
parameters:
|
||||
- description: Node name
|
||||
in: path
|
||||
name: node
|
||||
required: true
|
||||
type: string
|
||||
- description: Container VMID (optional - if not provided, streams node journalctl)
|
||||
in: path
|
||||
name: vmid
|
||||
type: integer
|
||||
- description: Service name (e.g., 'pveproxy' for node, 'container@.service'
|
||||
format for LXC)
|
||||
in: path
|
||||
name: service
|
||||
type: string
|
||||
- description: Limit output lines (1-1000)
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Journalctl output
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: Invalid request
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"404":
|
||||
description: Node not found
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
summary: Get journalctl output
|
||||
tags:
|
||||
- proxmox
|
||||
- websocket
|
||||
x-id: journalctl
|
||||
/proxmox/lxc/:node/:vmid/restart:
|
||||
post:
|
||||
description: Restart LXC container by node and vmid
|
||||
parameters:
|
||||
- in: path
|
||||
name: node
|
||||
required: true
|
||||
type: string
|
||||
- in: path
|
||||
name: vmid
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/SuccessResponse'
|
||||
"400":
|
||||
description: Invalid request
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"404":
|
||||
description: Node not found
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
summary: Restart LXC container
|
||||
tags:
|
||||
- proxmox
|
||||
x-id: lxcRestart
|
||||
/proxmox/lxc/:node/:vmid/start:
|
||||
post:
|
||||
description: Start LXC container by node and vmid
|
||||
parameters:
|
||||
- in: path
|
||||
name: node
|
||||
required: true
|
||||
type: string
|
||||
- in: path
|
||||
name: vmid
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/SuccessResponse'
|
||||
"400":
|
||||
description: Invalid request
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"404":
|
||||
description: Node not found
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
summary: Start LXC container
|
||||
tags:
|
||||
- proxmox
|
||||
x-id: lxcStart
|
||||
/proxmox/lxc/:node/:vmid/stop:
|
||||
post:
|
||||
description: Stop LXC container by node and vmid
|
||||
parameters:
|
||||
- in: path
|
||||
name: node
|
||||
required: true
|
||||
type: string
|
||||
- in: path
|
||||
name: vmid
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/SuccessResponse'
|
||||
"400":
|
||||
description: Invalid request
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"404":
|
||||
description: Node not found
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
summary: Stop LXC container
|
||||
tags:
|
||||
- proxmox
|
||||
x-id: lxcStop
|
||||
/proxmox/stats/{node}:
|
||||
get:
|
||||
description: Get proxmox node stats in json
|
||||
parameters:
|
||||
- description: Node name
|
||||
in: path
|
||||
name: node
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Stats output
|
||||
schema:
|
||||
$ref: '#/definitions/proxmox.NodeStats'
|
||||
"400":
|
||||
description: Invalid request
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"404":
|
||||
description: Node not found
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
summary: Get proxmox node stats
|
||||
tags:
|
||||
- proxmox
|
||||
- websocket
|
||||
x-id: nodeStats
|
||||
/proxmox/stats/{node}/{vmid}:
|
||||
get:
|
||||
description: Get proxmox VM stats in format of "STATUS|CPU%%|MEM USAGE/LIMIT|MEM%%|NET
|
||||
I/O|BLOCK I/O"
|
||||
parameters:
|
||||
- in: path
|
||||
name: node
|
||||
required: true
|
||||
type: string
|
||||
- in: path
|
||||
name: vmid
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- text/plain
|
||||
responses:
|
||||
"200":
|
||||
description: Stats output
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: Invalid request
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"404":
|
||||
description: Node not found
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
summary: Get proxmox VM stats
|
||||
tags:
|
||||
- proxmox
|
||||
- websocket
|
||||
x-id: vmStats
|
||||
/reload:
|
||||
post:
|
||||
consumes:
|
||||
|
||||
@@ -5,7 +5,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
|
||||
@@ -13,9 +14,9 @@ import (
|
||||
)
|
||||
|
||||
type GetFavIconRequest struct {
|
||||
URL string `form:"url" binding:"required_without=Alias"`
|
||||
Alias string `form:"alias" binding:"required_without=URL"`
|
||||
Variant homepage.IconVariant `form:"variant" binding:"omitempty,oneof=light dark"`
|
||||
URL string `form:"url" binding:"required_without=Alias"`
|
||||
Alias string `form:"alias" binding:"required_without=URL"`
|
||||
Variant icons.Variant `form:"variant" binding:"omitempty,oneof=light dark"`
|
||||
} // @name GetFavIconRequest
|
||||
|
||||
// @x-id "favicon"
|
||||
@@ -27,7 +28,7 @@ type GetFavIconRequest struct {
|
||||
// @Produce image/svg+xml,image/x-icon,image/png,image/webp
|
||||
// @Param url query string false "URL of the route"
|
||||
// @Param alias query string false "Alias of the route"
|
||||
// @Success 200 {array} homepage.FetchResult
|
||||
// @Success 200 {array} iconfetch.Result
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Bad Request: alias is empty or route is not HTTPRoute"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden: unauthorized"
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Not Found: route or icon not found"
|
||||
@@ -42,18 +43,18 @@ func FavIcon(c *gin.Context) {
|
||||
|
||||
// try with url
|
||||
if request.URL != "" {
|
||||
var iconURL homepage.IconURL
|
||||
var iconURL icons.URL
|
||||
if err := iconURL.Parse(request.URL); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid url", err))
|
||||
return
|
||||
}
|
||||
icon := &iconURL
|
||||
if request.Variant != homepage.IconVariantNone {
|
||||
if request.Variant != icons.VariantNone {
|
||||
icon = icon.WithVariant(request.Variant)
|
||||
}
|
||||
fetchResult, err := homepage.FetchFavIconFromURL(c.Request.Context(), icon)
|
||||
fetchResult, err := iconfetch.FetchFavIconFromURL(c.Request.Context(), icon)
|
||||
if err != nil {
|
||||
homepage.GinFetchError(c, fetchResult.StatusCode, err)
|
||||
iconfetch.GinError(c, fetchResult.StatusCode, err)
|
||||
return
|
||||
}
|
||||
c.Data(fetchResult.StatusCode, fetchResult.ContentType(), fetchResult.Icon)
|
||||
@@ -63,40 +64,40 @@ func FavIcon(c *gin.Context) {
|
||||
// try with alias
|
||||
result, err := GetFavIconFromAlias(c.Request.Context(), request.Alias, request.Variant)
|
||||
if err != nil {
|
||||
homepage.GinFetchError(c, result.StatusCode, err)
|
||||
iconfetch.GinError(c, result.StatusCode, err)
|
||||
return
|
||||
}
|
||||
c.Data(result.StatusCode, result.ContentType(), result.Icon)
|
||||
}
|
||||
|
||||
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
|
||||
func GetFavIconFromAlias(ctx context.Context, alias string, variant homepage.IconVariant) (homepage.FetchResult, error) {
|
||||
func GetFavIconFromAlias(ctx context.Context, alias string, variant icons.Variant) (iconfetch.Result, error) {
|
||||
// try with route.Icon
|
||||
r, ok := routes.HTTP.Get(alias)
|
||||
if !ok {
|
||||
return homepage.FetchResultWithErrorf(http.StatusNotFound, "route not found")
|
||||
return iconfetch.FetchResultWithErrorf(http.StatusNotFound, "route not found")
|
||||
}
|
||||
|
||||
var (
|
||||
result homepage.FetchResult
|
||||
result iconfetch.Result
|
||||
err error
|
||||
)
|
||||
hp := r.HomepageItem()
|
||||
if hp.Icon != nil {
|
||||
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
||||
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL, variant)
|
||||
} else if variant != homepage.IconVariantNone {
|
||||
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(variant))
|
||||
if hp.Icon.Source == icons.SourceRelative {
|
||||
result, err = iconfetch.FindIcon(ctx, r, *hp.Icon.FullURL, variant)
|
||||
} else if variant != icons.VariantNone {
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(variant))
|
||||
if err != nil {
|
||||
// fallback to no variant
|
||||
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(homepage.IconVariantNone))
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(icons.VariantNone))
|
||||
}
|
||||
} else {
|
||||
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
}
|
||||
} else {
|
||||
// try extract from "link[rel=icon]"
|
||||
result, err = homepage.FindIcon(ctx, r, "/", variant)
|
||||
result, err = iconfetch.FindIcon(ctx, r, "/", variant)
|
||||
}
|
||||
if result.StatusCode == 0 {
|
||||
result.StatusCode = http.StatusOK
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ type ListIconsRequest struct {
|
||||
// @Produce json
|
||||
// @Param limit query int false "Limit"
|
||||
// @Param keyword query string false "Keyword"
|
||||
// @Success 200 {array} homepage.IconMetaSearch
|
||||
// @Success 200 {array} iconlist.IconMetaSearch
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Router /icons [get]
|
||||
@@ -32,6 +32,6 @@ func Icons(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
icons := homepage.SearchIcons(request.Keyword, request.Limit)
|
||||
icons := iconlist.SearchIcons(request.Keyword, request.Limit)
|
||||
c.JSON(http.StatusOK, icons)
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
77
internal/api/v1/proxmox/journalctl.go
Normal file
77
internal/api/v1/proxmox/journalctl.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"
|
||||
)
|
||||
|
||||
type JournalctlRequest struct {
|
||||
Node string `uri:"node" binding:"required"`
|
||||
VMID *int `uri:"vmid"` // optional - if not provided, streams node journalctl
|
||||
Service string `uri:"service"`
|
||||
Limit int `query:"limit" binding:"omitempty,min=1,max=1000"`
|
||||
}
|
||||
|
||||
// @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 node path string true "Node name"
|
||||
// @Param vmid path int false "Container VMID (optional - if not provided, streams node journalctl)"
|
||||
// @Param service path string false "Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC)"
|
||||
// @Param limit query int false "Limit output lines (1-1000)"
|
||||
// @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/{node} [get]
|
||||
// @Router /proxmox/journalctl/{node}/{vmid} [get]
|
||||
// @Router /proxmox/journalctl/{node}/{vmid}/{service} [get]
|
||||
func Journalctl(c *gin.Context) {
|
||||
var request JournalctlRequest
|
||||
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
|
||||
}
|
||||
|
||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||
return
|
||||
}
|
||||
defer manager.Close()
|
||||
|
||||
var reader io.ReadCloser
|
||||
if request.VMID == nil {
|
||||
reader, err = node.NodeJournalctl(c.Request.Context(), request.Service, request.Limit)
|
||||
} else {
|
||||
reader, err = node.LXCJournalctl(c.Request.Context(), *request.VMID, request.Service, request.Limit)
|
||||
}
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get journalctl output"))
|
||||
return
|
||||
}
|
||||
defer reader.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"))
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
statequery "github.com/yusing/godoxy/internal/config/query"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
@@ -33,17 +32,10 @@ func Route(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
route, ok := routes.Get(request.Which)
|
||||
route, ok := routes.GetIncludeExcluded(request.Which)
|
||||
if ok {
|
||||
c.JSON(http.StatusOK, route)
|
||||
return
|
||||
}
|
||||
|
||||
// also search for excluded routes
|
||||
route = statequery.SearchRoute(request.Which)
|
||||
if route != nil {
|
||||
c.JSON(http.StatusOK, route)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusNotFound, nil)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,11 @@ var (
|
||||
APIHTTPPort,
|
||||
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)
|
||||
APIJWTSecret = decodeJWTKey(env.GetEnvString("API_JWT_SECRET", ""))
|
||||
APIJWTTokenTTL = env.GetEnvDuation("API_JWT_TOKEN_TTL", 24*time.Hour)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
config "github.com/yusing/godoxy/internal/config/types"
|
||||
"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/events"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
@@ -59,6 +60,15 @@ func Load() error {
|
||||
|
||||
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)
|
||||
err := errors.Join(initErr, state.StartProviders())
|
||||
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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```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
|
||||
|
||||
```go
|
||||
@@ -213,14 +198,4 @@ func handleGetStats(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
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
|
||||
}
|
||||
|
||||
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/notif"
|
||||
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/types"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
@@ -74,7 +73,6 @@ func SetState(state config.State) {
|
||||
|
||||
cfg := state.Value()
|
||||
config.ActiveState.Store(state)
|
||||
acl.ActiveConfig.Store(cfg.ACL)
|
||||
entrypoint.ActiveConfig.Store(&cfg.Entrypoint)
|
||||
homepage.ActiveConfig.Store(&cfg.Homepage)
|
||||
if autocertProvider := state.AutoCertProvider(); autocertProvider != nil {
|
||||
@@ -113,14 +111,14 @@ func (state *state) Init(data []byte) error {
|
||||
g := gperr.NewGroup("config load error")
|
||||
g.Go(state.initMaxMind)
|
||||
g.Go(state.initProxmox)
|
||||
g.Go(state.loadRouteProviders)
|
||||
g.Go(state.initAutoCert)
|
||||
|
||||
errs := g.Wait()
|
||||
// these won't benefit from running on goroutines
|
||||
errs.Add(state.initNotification())
|
||||
errs.Add(state.initAccessLogger())
|
||||
errs.Add(state.initACL())
|
||||
errs.Add(state.initEntrypoint())
|
||||
errs.Add(state.loadRouteProviders())
|
||||
return errs.Error()
|
||||
}
|
||||
|
||||
@@ -192,12 +190,17 @@ func (state *state) FlushTmpLog() {
|
||||
state.tmpLogBuf.Reset()
|
||||
}
|
||||
|
||||
// this one is connection level access logger, different from entrypoint access logger
|
||||
func (state *state) initAccessLogger() error {
|
||||
// initACL initializes the ACL.
|
||||
func (state *state) initACL() error {
|
||||
if !state.ACL.Valid() {
|
||||
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 {
|
||||
@@ -319,15 +322,6 @@ func (state *state) storeProvider(p types.RouteProvider) {
|
||||
}
|
||||
|
||||
func (state *state) loadRouteProviders() error {
|
||||
// disable pool logging temporary since we will have pretty logging below
|
||||
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")
|
||||
results := gperr.NewGroup("loaded route providers")
|
||||
|
||||
@@ -36,7 +36,7 @@ type (
|
||||
Docker map[string]types.DockerProviderConfig `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys"`
|
||||
Agents []*agent.AgentConfig `json:"agents" yaml:"agents,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"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
module github.com/yusing/godoxy/internal/dnsproviders
|
||||
|
||||
go 1.25.5
|
||||
go 1.25.6
|
||||
|
||||
replace github.com/yusing/godoxy => ../..
|
||||
|
||||
require (
|
||||
github.com/go-acme/lego/v4 v4.30.1
|
||||
github.com/yusing/godoxy v0.23.0
|
||||
github.com/go-acme/lego/v4 v4.31.0
|
||||
github.com/yusing/godoxy v0.25.0
|
||||
)
|
||||
|
||||
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/compute/metadata v0.9.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||
@@ -24,8 +24,8 @@ require (
|
||||
github.com/benbjohnson/clock v1.3.5 // 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/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
@@ -41,14 +41,14 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/go-resty/resty/v2 v2.17.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/goccy/go-yaml v1.19.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||
github.com/gotify/server/v2 v2.8.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
@@ -57,22 +57,22 @@ require (
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/linode/linodego v1.63.0 // indirect
|
||||
github.com/linode/linodego v1.64.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/maxatome/go-testdeep v1.14.0 // indirect
|
||||
github.com/miekg/dns v1.1.69 // indirect
|
||||
github.com/miekg/dns v1.1.72 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
||||
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1 // indirect
|
||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||
github.com/ovh/go-ovh v1.9.0 // 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/pquerna/otp v1.5.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
|
||||
github.com/rs/zerolog v1.34.0 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
@@ -90,19 +90,19 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/api v0.258.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
google.golang.org/api v0.262.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
|
||||
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
|
||||
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
|
||||
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/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/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
@@ -39,10 +39,10 @@ github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVk
|
||||
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/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
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/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
@@ -62,8 +62,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/go-acme/lego/v4 v4.30.1 h1:tmb6U0lvy8Mc3lQbqKwTat7oAhE8FUYNJ3D0gSg6pJU=
|
||||
github.com/go-acme/lego/v4 v4.30.1/go.mod h1:V7m/Ip+EeFkjOe028+zeH+SwWtESxw1LHelwMIfAjm4=
|
||||
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
|
||||
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -83,10 +83,10 @@ github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy0
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
|
||||
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
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/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
@@ -103,8 +103,8 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
||||
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
|
||||
@@ -131,8 +131,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/linode/linodego v1.63.0 h1:MdjizfXNJDVJU6ggoJmMO5O9h4KGPGivNX0fzrAnstk=
|
||||
github.com/linode/linodego v1.63.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
|
||||
github.com/linode/linodego v1.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY=
|
||||
github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
@@ -142,18 +142,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/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
|
||||
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
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/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
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/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
||||
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 h1:l0tH15ACQADZAzC+LZ+mo2tIX4H6uZu0ulrVmG5Tqz0=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 h1:gzB4c6ztb38C/jYiqEaFC+mCGcWFHDji9e6jwymY9d4=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2/go.mod h1:l1qIPIq2uRV5WTSvkbhbl/ndbeOu7OCb3UZ+0+2ZSb8=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1 h1:+fx2mbWeR8XX/vidwpRMepJMtRIYQP44Iezm2oeObVM=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1 h1:GDhBiaIAm/QXLzHJ0ASDdY/6R/9w60+gk8lY5rgfxEQ=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1/go.mod h1:EHScJdbM0gg5Is7e3C0ceRYAFMMsfP4Vf8sBRoxoTgk=
|
||||
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/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
|
||||
@@ -166,8 +166,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/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
|
||||
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/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
@@ -223,12 +223,12 @@ 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=
|
||||
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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
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/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
@@ -237,26 +237,26 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
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/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
|
||||
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
|
||||
google.golang.org/api v0.262.0 h1:4B+3u8He2GwyN8St3Jhnd3XRHlIvc//sBmgHSp78oNY=
|
||||
google.golang.org/api v0.262.0/go.mod h1:jNwmH8BgUBJ/VrUG6/lIl9YiildyLd09r9ZLHiQ6cGI=
|
||||
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/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/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/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=
|
||||
@@ -264,8 +264,8 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
|
||||
gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
||||
@@ -210,23 +210,25 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
|
||||
return
|
||||
}
|
||||
if c.Network != "" {
|
||||
v, ok := helper.NetworkSettings.Networks[c.Network]
|
||||
if ok && v.IPAddress.IsValid() {
|
||||
v, hasNetwork := helper.NetworkSettings.Networks[c.Network]
|
||||
if hasNetwork && v.IPAddress.IsValid() {
|
||||
c.PrivateHostname = v.IPAddress.String()
|
||||
return
|
||||
}
|
||||
var hasComposeNetwork bool
|
||||
// try {project_name}_{network_name}
|
||||
if proj := DockerComposeProject(c); proj != "" {
|
||||
oldNetwork, newNetwork := c.Network, fmt.Sprintf("%s_%s", proj, c.Network)
|
||||
if newNetwork != oldNetwork {
|
||||
v, ok = helper.NetworkSettings.Networks[newNetwork]
|
||||
if ok && v.IPAddress.IsValid() {
|
||||
c.Network = newNetwork // update network to the new one
|
||||
c.PrivateHostname = v.IPAddress.String()
|
||||
return
|
||||
}
|
||||
newNetwork := fmt.Sprintf("%s_%s", proj, c.Network)
|
||||
v, hasComposeNetwork = helper.NetworkSettings.Networks[newNetwork]
|
||||
if hasComposeNetwork && v.IPAddress.IsValid() {
|
||||
c.Network = newNetwork // update network to the new one
|
||||
c.PrivateHostname = v.IPAddress.String()
|
||||
return
|
||||
}
|
||||
}
|
||||
if hasNetwork || hasComposeNetwork { // network is found, but no IP assigned yet
|
||||
return
|
||||
}
|
||||
nearest := gperr.DoYouMeanField(c.Network, helper.NetworkSettings.Networks)
|
||||
addError(c, fmt.Errorf("network %q not found, %w", c.Network, nearest))
|
||||
return
|
||||
|
||||
@@ -100,7 +100,7 @@ func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
rec := accesslog.GetResponseRecorder(w)
|
||||
w = rec
|
||||
defer func() {
|
||||
ep.accessLogger.Log(r, rec.Response())
|
||||
ep.accessLogger.LogRequest(r, rec.Response())
|
||||
accesslog.PutResponseRecorder(rec)
|
||||
}()
|
||||
}
|
||||
|
||||
1
internal/go-proxmox
Submodule
1
internal/go-proxmox
Submodule
Submodule internal/go-proxmox added at bcae3065be
@@ -26,6 +26,8 @@ type (
|
||||
config types.HealthCheckConfig
|
||||
url synk.Value[*url.URL]
|
||||
|
||||
onUpdateURL func(url *url.URL)
|
||||
|
||||
status synk.Value[types.HealthStatus]
|
||||
lastResult synk.Value[types.HealthCheckResult]
|
||||
|
||||
@@ -151,6 +153,9 @@ func (mon *monitor) UpdateURL(url *url.URL) {
|
||||
return
|
||||
}
|
||||
mon.url.Store(url)
|
||||
if mon.onUpdateURL != nil {
|
||||
mon.onUpdateURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
// URL implements HealthChecker.
|
||||
|
||||
@@ -97,7 +97,7 @@ func NewDockerHealthMonitor(config types.HealthCheckConfig, client *docker.Share
|
||||
isFirstFailure := true
|
||||
|
||||
var mon monitor
|
||||
mon.init(displayURL, config, func(u *url.URL) (result Result, err error) {
|
||||
mon.init(displayURL, config, func(_ *url.URL) (result Result, err error) {
|
||||
result, err = healthcheck.Docker(mon.Context(), state, config.Timeout)
|
||||
if err != nil {
|
||||
if isFirstFailure {
|
||||
@@ -110,13 +110,14 @@ func NewDockerHealthMonitor(config types.HealthCheckConfig, client *docker.Share
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
mon.onUpdateURL = fallback.UpdateURL
|
||||
return &mon
|
||||
}
|
||||
|
||||
func NewAgentProxiedMonitor(config types.HealthCheckConfig, agent *agentpool.Agent, targetUrl *url.URL) Monitor {
|
||||
var mon monitor
|
||||
mon.init(targetUrl, config, func(u *url.URL) (result Result, err error) {
|
||||
return CheckHealthAgentProxied(agent, config.Timeout, targetUrl)
|
||||
return CheckHealthAgentProxied(agent, config.Timeout, u)
|
||||
})
|
||||
return &mon
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -194,18 +194,6 @@ Widgets can display various types of information:
|
||||
- **Links**: Quick access links
|
||||
- **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
|
||||
|
||||
### Default Categories
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/ds/ordered"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
"github.com/yusing/godoxy/internal/homepage/widgets"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
@@ -22,13 +23,13 @@ type (
|
||||
} // @name HomepageCategory
|
||||
|
||||
ItemConfig struct {
|
||||
Show bool `json:"show"`
|
||||
Name string `json:"name"` // display name
|
||||
Icon *IconURL `json:"icon" swaggertype:"string"`
|
||||
Category string `json:"category" validate:"omitempty"`
|
||||
Description string `json:"description" aliases:"desc"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Favorite bool `json:"favorite"`
|
||||
Show bool `json:"show"`
|
||||
Name string `json:"name"` // display name
|
||||
Icon *icons.URL `json:"icon" swaggertype:"string"`
|
||||
Category string `json:"category" validate:"omitempty"`
|
||||
Description string `json:"description" aliases:"desc"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Favorite bool `json:"favorite"`
|
||||
|
||||
WidgetConfig *widgets.Config `json:"widget_config,omitempty" aliases:"widget" extensions:"x-nullable"`
|
||||
} // @name HomepageItemConfig
|
||||
|
||||
@@ -4,19 +4,24 @@ import (
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func TestOverrideItem(t *testing.T) {
|
||||
a := &Item{
|
||||
Alias: "foo",
|
||||
ItemConfig: ItemConfig{
|
||||
Show: false,
|
||||
Name: "Foo",
|
||||
Icon: &IconURL{
|
||||
FullURL: strPtr("/favicon.ico"),
|
||||
IconSource: IconSourceRelative,
|
||||
Icon: &icons.URL{
|
||||
FullURL: strPtr("/favicon.ico"),
|
||||
Source: icons.SourceRelative,
|
||||
},
|
||||
Category: "App",
|
||||
},
|
||||
@@ -25,9 +30,9 @@ func TestOverrideItem(t *testing.T) {
|
||||
Show: true,
|
||||
Name: "Bar",
|
||||
Category: "Test",
|
||||
Icon: &IconURL{
|
||||
FullURL: strPtr("@walkxcode/example.png"),
|
||||
IconSource: IconSourceWalkXCode,
|
||||
Icon: &icons.URL{
|
||||
FullURL: strPtr("@walkxcode/example.png"),
|
||||
Source: icons.SourceWalkXCode,
|
||||
},
|
||||
}
|
||||
overrides := GetOverrideConfig()
|
||||
|
||||
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` |
|
||||
@@ -1,4 +1,4 @@
|
||||
package homepage
|
||||
package iconfetch
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -1,4 +1,4 @@
|
||||
package homepage
|
||||
package iconfetch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/vincent-petithory/dataurl"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/cache"
|
||||
@@ -22,22 +24,22 @@ import (
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
)
|
||||
|
||||
type FetchResult struct {
|
||||
type Result struct {
|
||||
Icon []byte
|
||||
StatusCode int
|
||||
|
||||
contentType string
|
||||
} // @name IconFetchResult
|
||||
|
||||
func FetchResultWithErrorf(statusCode int, msgFmt string, args ...any) (Result, error) {
|
||||
return Result{StatusCode: statusCode}, fmt.Errorf(msgFmt, args...)
|
||||
}
|
||||
|
||||
func FetchResultWithErrorf(statusCode int, msgFmt string, args ...any) (FetchResult, error) {
|
||||
return FetchResult{StatusCode: statusCode}, fmt.Errorf(msgFmt, args...)
|
||||
func FetchResultOK(icon []byte, contentType string) (Result, error) {
|
||||
return Result{Icon: icon, contentType: contentType}, nil
|
||||
}
|
||||
|
||||
func FetchResultOK(icon []byte, contentType string) (FetchResult, error) {
|
||||
return FetchResult{Icon: icon, contentType: contentType}, nil
|
||||
}
|
||||
|
||||
func GinFetchError(c *gin.Context, statusCode int, err error) {
|
||||
func GinError(c *gin.Context, statusCode int, err error) {
|
||||
if statusCode == 0 {
|
||||
statusCode = http.StatusInternalServerError
|
||||
}
|
||||
@@ -50,7 +52,7 @@ func GinFetchError(c *gin.Context, statusCode int, err error) {
|
||||
|
||||
const faviconFetchTimeout = 3 * time.Second
|
||||
|
||||
func (res *FetchResult) ContentType() string {
|
||||
func (res *Result) ContentType() string {
|
||||
if res.contentType == "" {
|
||||
if bytes.HasPrefix(res.Icon, []byte("<svg")) || bytes.HasPrefix(res.Icon, []byte("<?xml")) {
|
||||
return "image/svg+xml"
|
||||
@@ -62,19 +64,19 @@ func (res *FetchResult) ContentType() string {
|
||||
|
||||
const maxRedirectDepth = 5
|
||||
|
||||
func FetchFavIconFromURL(ctx context.Context, iconURL *IconURL) (FetchResult, error) {
|
||||
switch iconURL.IconSource {
|
||||
case IconSourceAbsolute:
|
||||
func FetchFavIconFromURL(ctx context.Context, iconURL *icons.URL) (Result, error) {
|
||||
switch iconURL.Source {
|
||||
case icons.SourceAbsolute:
|
||||
return FetchIconAbsolute(ctx, iconURL.URL())
|
||||
case IconSourceRelative:
|
||||
case icons.SourceRelative:
|
||||
return FetchResultWithErrorf(http.StatusBadRequest, "unexpected relative icon")
|
||||
case IconSourceWalkXCode, IconSourceSelfhSt:
|
||||
case icons.SourceWalkXCode, icons.SourceSelfhSt:
|
||||
return fetchKnownIcon(ctx, iconURL)
|
||||
}
|
||||
return FetchResultWithErrorf(http.StatusBadRequest, "invalid icon source")
|
||||
}
|
||||
|
||||
var FetchIconAbsolute = cache.NewKeyFunc(func(ctx context.Context, url string) (FetchResult, error) {
|
||||
var FetchIconAbsolute = cache.NewKeyFunc(func(ctx context.Context, url string) (Result, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return FetchResultWithErrorf(http.StatusInternalServerError, "cannot create request: %w", err)
|
||||
@@ -103,7 +105,7 @@ var FetchIconAbsolute = cache.NewKeyFunc(func(ctx context.Context, url string) (
|
||||
return FetchResultWithErrorf(http.StatusNotFound, "empty icon")
|
||||
}
|
||||
|
||||
res := FetchResult{Icon: icon}
|
||||
res := Result{Icon: icon}
|
||||
if contentType := resp.Header.Get("Content-Type"); contentType != "" {
|
||||
res.contentType = contentType
|
||||
}
|
||||
@@ -122,22 +124,22 @@ func sanitizeName(name string) string {
|
||||
return strings.ToLower(nameSanitizer.Replace(name))
|
||||
}
|
||||
|
||||
func fetchKnownIcon(ctx context.Context, url *IconURL) (FetchResult, error) {
|
||||
func fetchKnownIcon(ctx context.Context, url *icons.URL) (Result, error) {
|
||||
// if icon isn't in the list, no need to fetch
|
||||
if !url.HasIcon() {
|
||||
return FetchResult{StatusCode: http.StatusNotFound}, errors.New("no such icon")
|
||||
return Result{StatusCode: http.StatusNotFound}, errors.New("no such icon")
|
||||
}
|
||||
|
||||
return FetchIconAbsolute(ctx, url.URL())
|
||||
}
|
||||
|
||||
func fetchIcon(ctx context.Context, filename string) (FetchResult, error) {
|
||||
func fetchIcon(ctx context.Context, filename string) (Result, error) {
|
||||
for _, fileType := range []string{"svg", "webp", "png"} {
|
||||
result, err := fetchKnownIcon(ctx, NewSelfhStIconURL(filename, fileType))
|
||||
result, err := fetchKnownIcon(ctx, icons.NewURL(icons.SourceSelfhSt, filename, fileType))
|
||||
if err == nil {
|
||||
return result, err
|
||||
}
|
||||
result, err = fetchKnownIcon(ctx, NewWalkXCodeIconURL(filename, fileType))
|
||||
result, err = fetchKnownIcon(ctx, icons.NewURL(icons.SourceWalkXCode, filename, fileType))
|
||||
if err == nil {
|
||||
return result, err
|
||||
}
|
||||
@@ -150,10 +152,10 @@ type contextValue struct {
|
||||
uri string
|
||||
}
|
||||
|
||||
func FindIcon(ctx context.Context, r route, uri string, variant IconVariant) (FetchResult, error) {
|
||||
func FindIcon(ctx context.Context, r route, uri string, variant icons.Variant) (Result, error) {
|
||||
for _, ref := range r.References() {
|
||||
ref = sanitizeName(ref)
|
||||
if variant != IconVariantNone {
|
||||
if variant != icons.VariantNone {
|
||||
ref += "-" + string(variant)
|
||||
}
|
||||
result, err := fetchIcon(ctx, ref)
|
||||
@@ -162,18 +164,21 @@ func FindIcon(ctx context.Context, r route, uri string, variant IconVariant) (Fe
|
||||
}
|
||||
}
|
||||
if r, ok := r.(httpRoute); ok {
|
||||
if mon := r.HealthMonitor(); mon != nil && !mon.Status().Good() {
|
||||
return FetchResultWithErrorf(http.StatusServiceUnavailable, "service unavailable")
|
||||
}
|
||||
// fallback to parse html
|
||||
return findIconSlowCached(context.WithValue(ctx, "route", contextValue{r: r, uri: uri}), r.Key())
|
||||
}
|
||||
return FetchResultWithErrorf(http.StatusNotFound, "no icon found")
|
||||
}
|
||||
|
||||
var findIconSlowCached = cache.NewKeyFunc(func(ctx context.Context, key string) (FetchResult, error) {
|
||||
var findIconSlowCached = cache.NewKeyFunc(func(ctx context.Context, key string) (Result, error) {
|
||||
v := ctx.Value("route").(contextValue)
|
||||
return findIconSlow(ctx, v.r, v.uri, nil)
|
||||
}).WithMaxEntries(200).Build() // no retries, no ttl
|
||||
}).WithMaxEntries(200).WithRetriesConstantBackoff(math.MaxInt, 15*time.Second).Build() // infinite retries, 15 seconds interval
|
||||
|
||||
func findIconSlow(ctx context.Context, r httpRoute, uri string, stack []string) (FetchResult, error) {
|
||||
func findIconSlow(ctx context.Context, r httpRoute, uri string, stack []string) (Result, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return FetchResultWithErrorf(http.StatusBadGateway, "request timeout")
|
||||
@@ -1,9 +1,10 @@
|
||||
package homepage
|
||||
package iconfetch
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
"github.com/yusing/goutils/pool"
|
||||
)
|
||||
|
||||
@@ -12,6 +13,7 @@ type route interface {
|
||||
ProviderName() string
|
||||
References() []string
|
||||
TargetURL() *nettypes.URL
|
||||
HealthMonitor() types.HealthMonitor
|
||||
}
|
||||
|
||||
type httpRoute interface {
|
||||
17
internal/homepage/icons/key.go
Normal file
17
internal/homepage/icons/key.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package icons
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Key string
|
||||
|
||||
func NewKey(source Source, reference string) Key {
|
||||
return Key(fmt.Sprintf("%s/%s", source, reference))
|
||||
}
|
||||
|
||||
func (k Key) SourceRef() (Source, string) {
|
||||
source, ref, _ := strings.Cut(string(k), "/")
|
||||
return Source(source), ref
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package homepage
|
||||
package iconlist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
"github.com/yusing/goutils/intern"
|
||||
@@ -21,60 +21,19 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
IconKey string
|
||||
IconMap map[IconKey]*IconMeta
|
||||
IconMap map[icons.Key]*icons.Meta
|
||||
IconList []string
|
||||
IconMeta struct {
|
||||
SVG bool `json:"SVG"`
|
||||
PNG bool `json:"PNG"`
|
||||
WebP bool `json:"WebP"`
|
||||
Light bool `json:"Light"`
|
||||
Dark bool `json:"Dark"`
|
||||
DisplayName string `json:"-"`
|
||||
Tag string `json:"-"`
|
||||
}
|
||||
IconMetaSearch struct {
|
||||
*IconMeta
|
||||
|
||||
Source IconSource `json:"Source"`
|
||||
Ref string `json:"Ref"`
|
||||
IconMetaSearch struct {
|
||||
*icons.Meta
|
||||
|
||||
Source icons.Source `json:"Source"`
|
||||
Ref string `json:"Ref"`
|
||||
|
||||
rank int
|
||||
}
|
||||
} // @name IconMetaSearch
|
||||
)
|
||||
|
||||
func (icon *IconMeta) Filenames(ref string) []string {
|
||||
filenames := make([]string, 0)
|
||||
if icon.SVG {
|
||||
filenames = append(filenames, ref+".svg")
|
||||
if icon.Light {
|
||||
filenames = append(filenames, ref+"-light.svg")
|
||||
}
|
||||
if icon.Dark {
|
||||
filenames = append(filenames, ref+"-dark.svg")
|
||||
}
|
||||
}
|
||||
if icon.PNG {
|
||||
filenames = append(filenames, ref+".png")
|
||||
if icon.Light {
|
||||
filenames = append(filenames, ref+"-light.png")
|
||||
}
|
||||
if icon.Dark {
|
||||
filenames = append(filenames, ref+"-dark.png")
|
||||
}
|
||||
}
|
||||
if icon.WebP {
|
||||
filenames = append(filenames, ref+".webp")
|
||||
if icon.Light {
|
||||
filenames = append(filenames, ref+"-light.webp")
|
||||
}
|
||||
if icon.Dark {
|
||||
filenames = append(filenames, ref+"-dark.webp")
|
||||
}
|
||||
}
|
||||
return filenames
|
||||
}
|
||||
|
||||
const updateInterval = 2 * time.Hour
|
||||
|
||||
var iconsCache synk.Value[IconMap]
|
||||
@@ -84,16 +43,17 @@ const (
|
||||
selfhstIcons = "https://raw.githubusercontent.com/selfhst/icons/refs/heads/main/index.json"
|
||||
)
|
||||
|
||||
func NewIconKey(source IconSource, reference string) IconKey {
|
||||
return IconKey(fmt.Sprintf("%s/%s", source, reference))
|
||||
type provider struct{}
|
||||
|
||||
func (p provider) HasIcon(u *icons.URL) bool {
|
||||
return HasIcon(u)
|
||||
}
|
||||
|
||||
func (k IconKey) SourceRef() (IconSource, string) {
|
||||
source, ref, _ := strings.Cut(string(k), "/")
|
||||
return IconSource(source), ref
|
||||
func init() {
|
||||
icons.SetProvider(provider{})
|
||||
}
|
||||
|
||||
func InitIconListCache() {
|
||||
func InitCache() {
|
||||
m := make(IconMap)
|
||||
err := serialization.LoadJSONIfExist(common.IconListCachePath, &m)
|
||||
if err != nil {
|
||||
@@ -196,10 +156,10 @@ func SearchIcons(keyword string, limit int) []*IconMetaSearch {
|
||||
|
||||
source, ref := k.SourceRef()
|
||||
ranked := &IconMetaSearch{
|
||||
Source: source,
|
||||
Ref: ref,
|
||||
IconMeta: icon,
|
||||
rank: rank,
|
||||
Source: source,
|
||||
Ref: ref,
|
||||
Meta: icon,
|
||||
rank: rank,
|
||||
}
|
||||
// Sorted insert based on rank (lower rank = better match)
|
||||
insertPos, _ := slices.BinarySearchFunc(results, ranked, sortByRank)
|
||||
@@ -213,7 +173,7 @@ func SearchIcons(keyword string, limit int) []*IconMetaSearch {
|
||||
return results[:min(len(results), limit)]
|
||||
}
|
||||
|
||||
func HasIcon(icon *IconURL) bool {
|
||||
func HasIcon(icon *icons.URL) bool {
|
||||
if icon.Extra == nil {
|
||||
return false
|
||||
}
|
||||
@@ -241,11 +201,11 @@ type HomepageMeta struct {
|
||||
Tag string
|
||||
}
|
||||
|
||||
func GetHomepageMeta(ref string) (HomepageMeta, bool) {
|
||||
meta, ok := ListAvailableIcons()[NewIconKey(IconSourceSelfhSt, ref)]
|
||||
func GetMetadata(ref string) (HomepageMeta, bool) {
|
||||
meta, ok := ListAvailableIcons()[icons.NewKey(icons.SourceSelfhSt, ref)]
|
||||
// these info is not available in walkxcode
|
||||
// if !ok {
|
||||
// meta, ok = iconsCache.Icons[NewIconKey(IconSourceWalkXCode, ref)]
|
||||
// meta, ok = iconsCache.Icons[icons.NewIconKey(icons.IconSourceWalkXCode, ref)]
|
||||
// }
|
||||
if !ok {
|
||||
return HomepageMeta{}, false
|
||||
@@ -317,14 +277,14 @@ func UpdateWalkxCodeIcons(m IconMap) error {
|
||||
}
|
||||
|
||||
for fileType, files := range data {
|
||||
var setExt func(icon *IconMeta)
|
||||
var setExt func(icon *icons.Meta)
|
||||
switch fileType {
|
||||
case "png":
|
||||
setExt = func(icon *IconMeta) { icon.PNG = true }
|
||||
setExt = func(icon *icons.Meta) { icon.PNG = true }
|
||||
case "svg":
|
||||
setExt = func(icon *IconMeta) { icon.SVG = true }
|
||||
setExt = func(icon *icons.Meta) { icon.SVG = true }
|
||||
case "webp":
|
||||
setExt = func(icon *IconMeta) { icon.WebP = true }
|
||||
setExt = func(icon *icons.Meta) { icon.WebP = true }
|
||||
}
|
||||
for _, f := range files {
|
||||
f = strings.TrimSuffix(f, "."+fileType)
|
||||
@@ -336,10 +296,10 @@ func UpdateWalkxCodeIcons(m IconMap) error {
|
||||
if isDark {
|
||||
f = strings.TrimSuffix(f, "-dark")
|
||||
}
|
||||
key := NewIconKey(IconSourceWalkXCode, f)
|
||||
key := icons.NewKey(icons.SourceWalkXCode, f)
|
||||
icon, ok := m[key]
|
||||
if !ok {
|
||||
icon = new(IconMeta)
|
||||
icon = new(icons.Meta)
|
||||
m[key] = icon
|
||||
}
|
||||
setExt(icon)
|
||||
@@ -401,7 +361,7 @@ func UpdateSelfhstIcons(m IconMap) error {
|
||||
tag, _, _ = strings.Cut(item.Tags, ",")
|
||||
tag = strings.TrimSpace(tag)
|
||||
}
|
||||
icon := &IconMeta{
|
||||
icon := &icons.Meta{
|
||||
DisplayName: item.Name,
|
||||
Tag: intern.Make(tag).Value(),
|
||||
SVG: item.SVG == "Yes",
|
||||
@@ -410,7 +370,7 @@ func UpdateSelfhstIcons(m IconMap) error {
|
||||
Light: item.Light == "Yes",
|
||||
Dark: item.Dark == "Yes",
|
||||
}
|
||||
key := NewIconKey(IconSourceSelfhSt, item.Reference)
|
||||
key := icons.NewKey(icons.SourceSelfhSt, item.Reference)
|
||||
m[key] = icon
|
||||
}
|
||||
return nil
|
||||
@@ -1,9 +1,10 @@
|
||||
package homepage_test
|
||||
package iconlist_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/godoxy/internal/homepage"
|
||||
. "github.com/yusing/godoxy/internal/homepage/icons"
|
||||
. "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||
)
|
||||
|
||||
const walkxcodeIcons = `{
|
||||
@@ -69,8 +70,8 @@ const selfhstIcons = `[
|
||||
]`
|
||||
|
||||
type testCases struct {
|
||||
Key IconKey
|
||||
IconMeta
|
||||
Key Key
|
||||
Meta
|
||||
}
|
||||
|
||||
func runTests(t *testing.T, iconsCache IconMap, test []testCases) {
|
||||
@@ -109,8 +110,8 @@ func TestListWalkxCodeIcons(t *testing.T) {
|
||||
}
|
||||
test := []testCases{
|
||||
{
|
||||
Key: NewIconKey(IconSourceWalkXCode, "app1"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceWalkXCode, "app1"),
|
||||
Meta: Meta{
|
||||
SVG: true,
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
@@ -118,15 +119,15 @@ func TestListWalkxCodeIcons(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: NewIconKey(IconSourceWalkXCode, "app2"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceWalkXCode, "app2"),
|
||||
Meta: Meta{
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: NewIconKey(IconSourceWalkXCode, "karakeep"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceWalkXCode, "karakeep"),
|
||||
Meta: Meta{
|
||||
SVG: true,
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
@@ -149,8 +150,8 @@ func TestListSelfhstIcons(t *testing.T) {
|
||||
}
|
||||
test := []testCases{
|
||||
{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "2fauth"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceSelfhSt, "2fauth"),
|
||||
Meta: Meta{
|
||||
SVG: true,
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
@@ -160,16 +161,16 @@ func TestListSelfhstIcons(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "dittofeed"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceSelfhSt, "dittofeed"),
|
||||
Meta: Meta{
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
DisplayName: "Dittofeed",
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "ars-technica"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceSelfhSt, "ars-technica"),
|
||||
Meta: Meta{
|
||||
SVG: true,
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
43
internal/homepage/icons/metadata.go
Normal file
43
internal/homepage/icons/metadata.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package icons
|
||||
|
||||
type Meta struct {
|
||||
SVG bool `json:"SVG"`
|
||||
PNG bool `json:"PNG"`
|
||||
WebP bool `json:"WebP"`
|
||||
Light bool `json:"Light"`
|
||||
Dark bool `json:"Dark"`
|
||||
DisplayName string `json:"-"`
|
||||
Tag string `json:"-"`
|
||||
}
|
||||
|
||||
func (icon *Meta) Filenames(ref string) []string {
|
||||
filenames := make([]string, 0)
|
||||
if icon.SVG {
|
||||
filenames = append(filenames, ref+".svg")
|
||||
if icon.Light {
|
||||
filenames = append(filenames, ref+"-light.svg")
|
||||
}
|
||||
if icon.Dark {
|
||||
filenames = append(filenames, ref+"-dark.svg")
|
||||
}
|
||||
}
|
||||
if icon.PNG {
|
||||
filenames = append(filenames, ref+".png")
|
||||
if icon.Light {
|
||||
filenames = append(filenames, ref+"-light.png")
|
||||
}
|
||||
if icon.Dark {
|
||||
filenames = append(filenames, ref+"-dark.png")
|
||||
}
|
||||
}
|
||||
if icon.WebP {
|
||||
filenames = append(filenames, ref+".webp")
|
||||
if icon.Light {
|
||||
filenames = append(filenames, ref+"-light.webp")
|
||||
}
|
||||
if icon.Dark {
|
||||
filenames = append(filenames, ref+"-dark.webp")
|
||||
}
|
||||
}
|
||||
return filenames
|
||||
}
|
||||
21
internal/homepage/icons/provider.go
Normal file
21
internal/homepage/icons/provider.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package icons
|
||||
|
||||
import "sync/atomic"
|
||||
|
||||
type Provider interface {
|
||||
HasIcon(u *URL) bool
|
||||
}
|
||||
|
||||
var provider atomic.Value
|
||||
|
||||
func SetProvider(p Provider) {
|
||||
provider.Store(p)
|
||||
}
|
||||
|
||||
func hasIcon(u *URL) bool {
|
||||
v := provider.Load()
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
return v.(Provider).HasIcon(u)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package homepage
|
||||
package icons
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -8,43 +8,43 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
IconURL struct {
|
||||
IconSource `json:"source"`
|
||||
URL struct {
|
||||
Source `json:"source"`
|
||||
|
||||
FullURL *string `json:"value,omitempty"` // only for absolute/relative icons
|
||||
Extra *IconExtra `json:"extra,omitempty"` // only for walkxcode/selfhst icons
|
||||
FullURL *string `json:"value,omitempty"` // only for absolute/relative icons
|
||||
Extra *Extra `json:"extra,omitempty"` // only for walkxcode/selfhst icons
|
||||
}
|
||||
|
||||
IconExtra struct {
|
||||
Key IconKey `json:"key"`
|
||||
Ref string `json:"ref"`
|
||||
FileType string `json:"file_type"`
|
||||
IsLight bool `json:"is_light"`
|
||||
IsDark bool `json:"is_dark"`
|
||||
Extra struct {
|
||||
Key Key `json:"key"`
|
||||
Ref string `json:"ref"`
|
||||
FileType string `json:"file_type"`
|
||||
IsLight bool `json:"is_light"`
|
||||
IsDark bool `json:"is_dark"`
|
||||
}
|
||||
|
||||
IconSource string
|
||||
IconVariant string
|
||||
Source string
|
||||
Variant string
|
||||
)
|
||||
|
||||
const (
|
||||
IconSourceAbsolute IconSource = "https://"
|
||||
IconSourceRelative IconSource = "@target"
|
||||
IconSourceWalkXCode IconSource = "@walkxcode"
|
||||
IconSourceSelfhSt IconSource = "@selfhst"
|
||||
SourceAbsolute Source = "https://"
|
||||
SourceRelative Source = "@target"
|
||||
SourceWalkXCode Source = "@walkxcode"
|
||||
SourceSelfhSt Source = "@selfhst"
|
||||
)
|
||||
|
||||
const (
|
||||
IconVariantNone IconVariant = ""
|
||||
IconVariantLight IconVariant = "light"
|
||||
IconVariantDark IconVariant = "dark"
|
||||
VariantNone Variant = ""
|
||||
VariantLight Variant = "light"
|
||||
VariantDark Variant = "dark"
|
||||
)
|
||||
|
||||
var ErrInvalidIconURL = gperr.New("invalid icon url")
|
||||
|
||||
func NewIconURL(source IconSource, refOrName, format string) *IconURL {
|
||||
func NewURL(source Source, refOrName, format string) *URL {
|
||||
switch source {
|
||||
case IconSourceWalkXCode, IconSourceSelfhSt:
|
||||
case SourceWalkXCode, SourceSelfhSt:
|
||||
default:
|
||||
panic("invalid icon source")
|
||||
}
|
||||
@@ -56,10 +56,10 @@ func NewIconURL(source IconSource, refOrName, format string) *IconURL {
|
||||
isDark = true
|
||||
refOrName = strings.TrimSuffix(refOrName, "-dark")
|
||||
}
|
||||
return &IconURL{
|
||||
IconSource: source,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(source, refOrName),
|
||||
return &URL{
|
||||
Source: source,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(source, refOrName),
|
||||
FileType: format,
|
||||
Ref: refOrName,
|
||||
IsLight: isLight,
|
||||
@@ -68,53 +68,42 @@ func NewIconURL(source IconSource, refOrName, format string) *IconURL {
|
||||
}
|
||||
}
|
||||
|
||||
func NewSelfhStIconURL(refOrName, format string) *IconURL {
|
||||
return NewIconURL(IconSourceSelfhSt, refOrName, format)
|
||||
func (u *URL) HasIcon() bool {
|
||||
return hasIcon(u)
|
||||
}
|
||||
|
||||
func NewWalkXCodeIconURL(name, format string) *IconURL {
|
||||
return NewIconURL(IconSourceWalkXCode, name, format)
|
||||
}
|
||||
|
||||
// HasIcon checks if the icon referenced by the IconURL exists in the cache based on its source.
|
||||
// Returns false if the icon does not exist for IconSourceSelfhSt or IconSourceWalkXCode,
|
||||
// otherwise returns true.
|
||||
func (u *IconURL) HasIcon() bool {
|
||||
return HasIcon(u)
|
||||
}
|
||||
|
||||
func (u *IconURL) WithVariant(variant IconVariant) *IconURL {
|
||||
switch u.IconSource {
|
||||
case IconSourceWalkXCode, IconSourceSelfhSt:
|
||||
func (u *URL) WithVariant(variant Variant) *URL {
|
||||
switch u.Source {
|
||||
case SourceWalkXCode, SourceSelfhSt:
|
||||
default:
|
||||
return u // no variant for absolute/relative icons
|
||||
}
|
||||
|
||||
var extra *IconExtra
|
||||
var extra *Extra
|
||||
if u.Extra != nil {
|
||||
extra = &IconExtra{
|
||||
extra = &Extra{
|
||||
Key: u.Extra.Key,
|
||||
Ref: u.Extra.Ref,
|
||||
FileType: u.Extra.FileType,
|
||||
IsLight: variant == IconVariantLight,
|
||||
IsDark: variant == IconVariantDark,
|
||||
IsLight: variant == VariantLight,
|
||||
IsDark: variant == VariantDark,
|
||||
}
|
||||
extra.Ref = strings.TrimSuffix(extra.Ref, "-light")
|
||||
extra.Ref = strings.TrimSuffix(extra.Ref, "-dark")
|
||||
}
|
||||
return &IconURL{
|
||||
IconSource: u.IconSource,
|
||||
FullURL: u.FullURL,
|
||||
Extra: extra,
|
||||
return &URL{
|
||||
Source: u.Source,
|
||||
FullURL: u.FullURL,
|
||||
Extra: extra,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse implements strutils.Parser.
|
||||
func (u *IconURL) Parse(v string) error {
|
||||
func (u *URL) Parse(v string) error {
|
||||
return u.parse(v, true)
|
||||
}
|
||||
|
||||
func (u *IconURL) parse(v string, checkExists bool) error {
|
||||
func (u *URL) parse(v string, checkExists bool) error {
|
||||
if v == "" {
|
||||
return ErrInvalidIconURL
|
||||
}
|
||||
@@ -126,19 +115,19 @@ func (u *IconURL) parse(v string, checkExists bool) error {
|
||||
switch beforeSlash {
|
||||
case "http:", "https:":
|
||||
u.FullURL = &v
|
||||
u.IconSource = IconSourceAbsolute
|
||||
u.Source = SourceAbsolute
|
||||
case "@target", "": // @target/favicon.ico, /favicon.ico
|
||||
url := v[slashIndex:]
|
||||
if url == "/" {
|
||||
return ErrInvalidIconURL.Withf("%s", "empty path")
|
||||
}
|
||||
u.FullURL = &url
|
||||
u.IconSource = IconSourceRelative
|
||||
u.Source = SourceRelative
|
||||
case "@selfhst", "@walkxcode": // selfh.st / walkxcode Icons, @selfhst/<reference>.<format>
|
||||
if beforeSlash == "@selfhst" {
|
||||
u.IconSource = IconSourceSelfhSt
|
||||
u.Source = SourceSelfhSt
|
||||
} else {
|
||||
u.IconSource = IconSourceWalkXCode
|
||||
u.Source = SourceWalkXCode
|
||||
}
|
||||
parts := strings.Split(v[slashIndex+1:], ".")
|
||||
if len(parts) != 2 {
|
||||
@@ -161,15 +150,15 @@ func (u *IconURL) parse(v string, checkExists bool) error {
|
||||
isDark = true
|
||||
reference = strings.TrimSuffix(reference, "-dark")
|
||||
}
|
||||
u.Extra = &IconExtra{
|
||||
Key: NewIconKey(u.IconSource, reference),
|
||||
u.Extra = &Extra{
|
||||
Key: NewKey(u.Source, reference),
|
||||
FileType: format,
|
||||
Ref: reference,
|
||||
IsLight: isLight,
|
||||
IsDark: isDark,
|
||||
}
|
||||
if checkExists && !u.HasIcon() {
|
||||
return ErrInvalidIconURL.Withf("no such icon %s.%s from %s", reference, format, u.IconSource)
|
||||
return ErrInvalidIconURL.Withf("no such icon %s.%s from %s", reference, format, u.Source)
|
||||
}
|
||||
default:
|
||||
return ErrInvalidIconURL.Subject(v)
|
||||
@@ -178,7 +167,7 @@ func (u *IconURL) parse(v string, checkExists bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *IconURL) URL() string {
|
||||
func (u *URL) URL() string {
|
||||
if u.FullURL != nil {
|
||||
return *u.FullURL
|
||||
}
|
||||
@@ -191,16 +180,16 @@ func (u *IconURL) URL() string {
|
||||
} else if u.Extra.IsDark {
|
||||
filename += "-dark"
|
||||
}
|
||||
switch u.IconSource {
|
||||
case IconSourceWalkXCode:
|
||||
switch u.Source {
|
||||
case SourceWalkXCode:
|
||||
return fmt.Sprintf("https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/%s/%s.%s", u.Extra.FileType, filename, u.Extra.FileType)
|
||||
case IconSourceSelfhSt:
|
||||
case SourceSelfhSt:
|
||||
return fmt.Sprintf("https://cdn.jsdelivr.net/gh/selfhst/icons/%s/%s.%s", u.Extra.FileType, filename, u.Extra.FileType)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (u *IconURL) String() string {
|
||||
func (u *URL) String() string {
|
||||
if u.FullURL != nil {
|
||||
return *u.FullURL
|
||||
}
|
||||
@@ -213,14 +202,14 @@ func (u *IconURL) String() string {
|
||||
} else if u.Extra.IsDark {
|
||||
suffix = "-dark"
|
||||
}
|
||||
return fmt.Sprintf("%s/%s%s.%s", u.IconSource, u.Extra.Ref, suffix, u.Extra.FileType)
|
||||
return fmt.Sprintf("%s/%s%s.%s", u.Source, u.Extra.Ref, suffix, u.Extra.FileType)
|
||||
}
|
||||
|
||||
func (u *IconURL) MarshalText() ([]byte, error) {
|
||||
func (u *URL) MarshalText() ([]byte, error) {
|
||||
return []byte(u.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||
func (u *IconURL) UnmarshalText(data []byte) error {
|
||||
func (u *URL) UnmarshalText(data []byte) error {
|
||||
return u.parse(string(data), false)
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package homepage_test
|
||||
package icons_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/godoxy/internal/homepage"
|
||||
. "github.com/yusing/godoxy/internal/homepage/icons"
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
@@ -15,31 +15,31 @@ func TestIconURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantValue *IconURL
|
||||
wantValue *URL
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "absolute",
|
||||
input: "http://example.com/icon.png",
|
||||
wantValue: &IconURL{
|
||||
FullURL: strPtr("http://example.com/icon.png"),
|
||||
IconSource: IconSourceAbsolute,
|
||||
wantValue: &URL{
|
||||
FullURL: strPtr("http://example.com/icon.png"),
|
||||
Source: SourceAbsolute,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "relative",
|
||||
input: "@target/icon.png",
|
||||
wantValue: &IconURL{
|
||||
FullURL: strPtr("/icon.png"),
|
||||
IconSource: IconSourceRelative,
|
||||
wantValue: &URL{
|
||||
FullURL: strPtr("/icon.png"),
|
||||
Source: SourceRelative,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "relative2",
|
||||
input: "/icon.png",
|
||||
wantValue: &IconURL{
|
||||
FullURL: strPtr("/icon.png"),
|
||||
IconSource: IconSourceRelative,
|
||||
wantValue: &URL{
|
||||
FullURL: strPtr("/icon.png"),
|
||||
Source: SourceRelative,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -55,10 +55,10 @@ func TestIconURL(t *testing.T) {
|
||||
{
|
||||
name: "walkxcode",
|
||||
input: "@walkxcode/adguard-home.png",
|
||||
wantValue: &IconURL{
|
||||
IconSource: IconSourceWalkXCode,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(IconSourceWalkXCode, "adguard-home"),
|
||||
wantValue: &URL{
|
||||
Source: SourceWalkXCode,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(SourceWalkXCode, "adguard-home"),
|
||||
FileType: "png",
|
||||
Ref: "adguard-home",
|
||||
},
|
||||
@@ -67,10 +67,10 @@ func TestIconURL(t *testing.T) {
|
||||
{
|
||||
name: "walkxcode_light",
|
||||
input: "@walkxcode/pfsense-light.png",
|
||||
wantValue: &IconURL{
|
||||
IconSource: IconSourceWalkXCode,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(IconSourceWalkXCode, "pfsense"),
|
||||
wantValue: &URL{
|
||||
Source: SourceWalkXCode,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(SourceWalkXCode, "pfsense"),
|
||||
FileType: "png",
|
||||
Ref: "pfsense",
|
||||
IsLight: true,
|
||||
@@ -85,10 +85,10 @@ func TestIconURL(t *testing.T) {
|
||||
{
|
||||
name: "selfh.st_valid",
|
||||
input: "@selfhst/adguard-home.webp",
|
||||
wantValue: &IconURL{
|
||||
IconSource: IconSourceSelfhSt,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "adguard-home"),
|
||||
wantValue: &URL{
|
||||
Source: SourceSelfhSt,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(SourceSelfhSt, "adguard-home"),
|
||||
FileType: "webp",
|
||||
Ref: "adguard-home",
|
||||
},
|
||||
@@ -97,10 +97,10 @@ func TestIconURL(t *testing.T) {
|
||||
{
|
||||
name: "selfh.st_light",
|
||||
input: "@selfhst/adguard-home-light.png",
|
||||
wantValue: &IconURL{
|
||||
IconSource: IconSourceSelfhSt,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "adguard-home"),
|
||||
wantValue: &URL{
|
||||
Source: SourceSelfhSt,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(SourceSelfhSt, "adguard-home"),
|
||||
FileType: "png",
|
||||
Ref: "adguard-home",
|
||||
IsLight: true,
|
||||
@@ -110,10 +110,10 @@ func TestIconURL(t *testing.T) {
|
||||
{
|
||||
name: "selfh.st_dark",
|
||||
input: "@selfhst/adguard-home-dark.svg",
|
||||
wantValue: &IconURL{
|
||||
IconSource: IconSourceSelfhSt,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "adguard-home"),
|
||||
wantValue: &URL{
|
||||
Source: SourceSelfhSt,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(SourceSelfhSt, "adguard-home"),
|
||||
FileType: "svg",
|
||||
Ref: "adguard-home",
|
||||
IsDark: true,
|
||||
@@ -143,7 +143,7 @@ func TestIconURL(t *testing.T) {
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
u := &IconURL{}
|
||||
u := &URL{}
|
||||
err := u.Parse(tc.input)
|
||||
if tc.wantErr {
|
||||
expect.ErrorIs(t, ErrInvalidIconURL, err)
|
||||
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]
|
||||
```
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch"
|
||||
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
@@ -99,18 +100,18 @@ func (w *Watcher) handleWakeEventsSSE(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) getFavIcon(ctx context.Context) (result homepage.FetchResult, err error) {
|
||||
func (w *Watcher) getFavIcon(ctx context.Context) (result iconfetch.Result, err error) {
|
||||
r := w.route
|
||||
hp := r.HomepageItem()
|
||||
if hp.Icon != nil {
|
||||
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
||||
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL, homepage.IconVariantNone)
|
||||
if hp.Icon.Source == icons.SourceRelative {
|
||||
result, err = iconfetch.FindIcon(ctx, r, *hp.Icon.FullURL, icons.VariantNone)
|
||||
} else {
|
||||
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
}
|
||||
} else {
|
||||
// try extract from "link[rel=icon]"
|
||||
result, err = homepage.FindIcon(ctx, r, "/", homepage.IconVariantNone)
|
||||
result, err = iconfetch.FindIcon(ctx, r, "/", icons.VariantNone)
|
||||
}
|
||||
if result.StatusCode == 0 {
|
||||
result.StatusCode = http.StatusOK
|
||||
|
||||
@@ -173,7 +173,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
depRoute, ok = routes.Get(dep)
|
||||
depRoute, ok = routes.GetIncludeExcluded(dep)
|
||||
if !ok {
|
||||
depErrors.Addf("dependency %q not found", dep)
|
||||
continue
|
||||
@@ -612,10 +612,6 @@ func (w *Watcher) watchUntilDestroy() (returnCause error) {
|
||||
if ready {
|
||||
// Container is now ready, notify waiting handlers
|
||||
w.healthTicker.Stop()
|
||||
select {
|
||||
case w.readyNotifyCh <- struct{}{}:
|
||||
default: // channel full, notification already pending
|
||||
}
|
||||
w.resetIdleTimer()
|
||||
}
|
||||
// If not ready yet, keep checking on next tick
|
||||
|
||||
@@ -13,10 +13,9 @@ type ReaderAtSeeker interface {
|
||||
|
||||
// BackScanner provides an interface to read a file backward line by line.
|
||||
type BackScanner struct {
|
||||
file ReaderAtSeeker
|
||||
size int64
|
||||
chunkSize int
|
||||
chunkBuf []byte
|
||||
file ReaderAtSeeker
|
||||
size int64
|
||||
chunkBuf []byte
|
||||
|
||||
offset int64
|
||||
chunk []byte
|
||||
@@ -27,16 +26,25 @@ type BackScanner struct {
|
||||
// NewBackScanner creates a new Scanner to read the file backward.
|
||||
// chunkSize determines the size of each read chunk from the end of the file.
|
||||
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 {
|
||||
return &BackScanner{
|
||||
file: file,
|
||||
size: fileSize,
|
||||
offset: fileSize,
|
||||
chunkSize: len(buf),
|
||||
chunkBuf: buf,
|
||||
file: file,
|
||||
size: fileSize,
|
||||
offset: fileSize,
|
||||
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
|
||||
if len(s.chunk) > 0 {
|
||||
s.line = s.chunk
|
||||
sizedPool.Put(s.chunk)
|
||||
s.chunk = nil
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
newOffset := max(0, s.offset-int64(s.chunkSize))
|
||||
newOffset := max(0, s.offset-int64(len(s.chunkBuf)))
|
||||
chunkSize := s.offset - newOffset
|
||||
chunk := s.chunkBuf[:chunkSize]
|
||||
|
||||
@@ -85,8 +94,19 @@ func (s *BackScanner) Scan() bool {
|
||||
}
|
||||
|
||||
// Prepend the chunk to the buffer
|
||||
clone := append([]byte{}, chunk[:n]...)
|
||||
s.chunk = append(clone, s.chunk...)
|
||||
if s.chunk == nil { // first 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
|
||||
|
||||
// Check for newline in the updated buffer
|
||||
@@ -111,12 +131,3 @@ func (s *BackScanner) Bytes() []byte {
|
||||
func (s *BackScanner) Err() error {
|
||||
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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
"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{
|
||||
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) {
|
||||
_, _ = 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"
|
||||
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) {
|
||||
file, err := afero.TempFile(afero.NewOsFs(), "", "accesslog")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
return func() []byte {
|
||||
// make a request to the server
|
||||
req, _ := http.NewRequest(http.MethodGet, srv.URL, nil)
|
||||
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.
|
||||
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) {
|
||||
file, err := afero.TempFile(afero.NewOsFs(), "", "accesslog")
|
||||
if err != nil {
|
||||
@@ -224,51 +178,58 @@ func BenchmarkBackScannerRealFile(b *testing.B) {
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
for range 10000 {
|
||||
_, err = file.Write(logEntry())
|
||||
if err != nil {
|
||||
b.Fatalf("failed to write to temp file: %v", err)
|
||||
}
|
||||
buf := bytes.NewBuffer(nil)
|
||||
for range 100000 {
|
||||
buf.Write(logEntry())
|
||||
}
|
||||
|
||||
stat, _ := file.Stat()
|
||||
scanner := NewBackScanner(file, stat.Size(), 256*kilobyte)
|
||||
b.ResetTimer()
|
||||
for scanner.Scan() {
|
||||
fSize := int64(buf.Len())
|
||||
_, err = file.Write(buf.Bytes())
|
||||
if err != nil {
|
||||
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
|
||||
BenchmarkBackScanner/2_KiB
|
||||
BenchmarkBackScanner/2_KiB-20 52 23254071 ns/op 67596663 B/op 26420 allocs/op
|
||||
BenchmarkBackScanner/4_KiB
|
||||
BenchmarkBackScanner/4_KiB-20 55 20961059 ns/op 62529378 B/op 13211 allocs/op
|
||||
BenchmarkBackScanner/8_KiB
|
||||
BenchmarkBackScanner/8_KiB-20 64 18242460 ns/op 62951141 B/op 6608 allocs/op
|
||||
BenchmarkBackScanner/16_KiB
|
||||
BenchmarkBackScanner/16_KiB-20 52 20162076 ns/op 62940256 B/op 3306 allocs/op
|
||||
BenchmarkBackScanner/32_KiB
|
||||
BenchmarkBackScanner/32_KiB-20 54 19247968 ns/op 67553645 B/op 1656 allocs/op
|
||||
BenchmarkBackScanner/64_KiB
|
||||
BenchmarkBackScanner/64_KiB-20 60 20909046 ns/op 64053342 B/op 827 allocs/op
|
||||
BenchmarkBackScanner/128_KiB
|
||||
BenchmarkBackScanner/128_KiB-20 68 17759890 ns/op 62201945 B/op 414 allocs/op
|
||||
BenchmarkBackScanner/256_KiB
|
||||
BenchmarkBackScanner/256_KiB-20 52 19531877 ns/op 61030487 B/op 208 allocs/op
|
||||
BenchmarkBackScanner/512_KiB
|
||||
BenchmarkBackScanner/512_KiB-20 54 19124656 ns/op 61030485 B/op 208 allocs/op
|
||||
BenchmarkBackScanner/1_MiB
|
||||
BenchmarkBackScanner/1_MiB-20 67 17078936 ns/op 61030495 B/op 208 allocs/op
|
||||
BenchmarkBackScanner/2_MiB
|
||||
BenchmarkBackScanner/2_MiB-20 66 18467421 ns/op 61030492 B/op 208 allocs/op
|
||||
BenchmarkBackScanner/4_MiB
|
||||
BenchmarkBackScanner/4_MiB-20 68 17214573 ns/op 61030486 B/op 208 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
|
||||
BenchmarkBackScannerRealFile
|
||||
BenchmarkBackScannerRealFile/2_KiB
|
||||
BenchmarkBackScannerRealFile/2_KiB-10 21 51796773 ns/op 619 B/op 1 allocs/op
|
||||
BenchmarkBackScannerRealFile/4_KiB
|
||||
BenchmarkBackScannerRealFile/4_KiB-10 36 32081281 ns/op 699 B/op 1 allocs/op
|
||||
BenchmarkBackScannerRealFile/8_KiB
|
||||
BenchmarkBackScannerRealFile/8_KiB-10 57 22155619 ns/op 847 B/op 1 allocs/op
|
||||
BenchmarkBackScannerRealFile/16_KiB
|
||||
BenchmarkBackScannerRealFile/16_KiB-10 62 21323125 ns/op 1449 B/op 1 allocs/op
|
||||
BenchmarkBackScannerRealFile/32_KiB
|
||||
BenchmarkBackScannerRealFile/32_KiB-10 63 17534883 ns/op 2729 B/op 1 allocs/op
|
||||
BenchmarkBackScannerRealFile/64_KiB
|
||||
BenchmarkBackScannerRealFile/64_KiB-10 73 17877029 ns/op 4617 B/op 1 allocs/op
|
||||
BenchmarkBackScannerRealFile/128_KiB
|
||||
BenchmarkBackScannerRealFile/128_KiB-10 75 17797267 ns/op 8866 B/op 1 allocs/op
|
||||
BenchmarkBackScannerRealFile/256_KiB
|
||||
BenchmarkBackScannerRealFile/256_KiB-10 67 16732108 ns/op 19691 B/op 1 allocs/op
|
||||
BenchmarkBackScannerRealFile/512_KiB
|
||||
BenchmarkBackScannerRealFile/512_KiB-10 70 17121683 ns/op 37577 B/op 1 allocs/op
|
||||
BenchmarkBackScannerRealFile/1_MiB
|
||||
BenchmarkBackScannerRealFile/1_MiB-10 51 19615791 ns/op 102930 B/op 1 allocs/op
|
||||
BenchmarkBackScannerRealFile/2_MiB
|
||||
BenchmarkBackScannerRealFile/2_MiB-10 26 41744928 ns/op 77595287 B/op 57 allocs/op
|
||||
BenchmarkBackScannerRealFile/4_MiB
|
||||
BenchmarkBackScannerRealFile/4_MiB-10 22 48081521 ns/op 79692224 B/op 49 allocs/op
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package accesslog
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
@@ -9,16 +10,15 @@ import (
|
||||
|
||||
type (
|
||||
ConfigBase struct {
|
||||
B int `json:"buffer_size"` // Deprecated: buffer size is adjusted dynamically
|
||||
Path string `json:"path"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Stdout bool `json:"stdout"`
|
||||
Retention *Retention `json:"retention" aliases:"keep"`
|
||||
RotateInterval time.Duration `json:"rotate_interval,omitempty" swaggertype:"primitive,integer"`
|
||||
}
|
||||
} // @name AccessLoggerConfigBase
|
||||
ACLLoggerConfig struct {
|
||||
ConfigBase
|
||||
LogAllowed bool `json:"log_allowed"`
|
||||
}
|
||||
} // @name ACLLoggerConfig
|
||||
RequestLoggerConfig struct {
|
||||
ConfigBase
|
||||
Format Format `json:"format" validate:"oneof=common combined json"`
|
||||
@@ -32,21 +32,21 @@ type (
|
||||
}
|
||||
AnyConfig interface {
|
||||
ToConfig() *Config
|
||||
Writers() ([]Writer, error)
|
||||
Writers() ([]File, error)
|
||||
}
|
||||
|
||||
Format string
|
||||
Filters struct {
|
||||
StatusCodes LogFilter[*StatusCodeRange] `json:"status_codes"`
|
||||
Method LogFilter[HTTPMethod] `json:"method"`
|
||||
Host LogFilter[Host] `json:"host"`
|
||||
Headers LogFilter[*HTTPHeader] `json:"headers"` // header exists or header == value
|
||||
CIDR LogFilter[*CIDR] `json:"cidr"`
|
||||
StatusCodes LogFilter[*StatusCodeRange] `json:"status_codes,omitzero"`
|
||||
Method LogFilter[HTTPMethod] `json:"method,omitzero"`
|
||||
Host LogFilter[Host] `json:"host,omitzero"`
|
||||
Headers LogFilter[*HTTPHeader] `json:"headers,omitzero"` // header exists or header == value
|
||||
CIDR LogFilter[*CIDR] `json:"cidr,omitzero"`
|
||||
}
|
||||
Fields struct {
|
||||
Headers FieldConfig `json:"headers" aliases:"header"`
|
||||
Query FieldConfig `json:"query" aliases:"queries"`
|
||||
Cookies FieldConfig `json:"cookies" aliases:"cookie"`
|
||||
Headers FieldConfig `json:"headers,omitzero" aliases:"header"`
|
||||
Query FieldConfig `json:"query,omitzero" aliases:"queries"`
|
||||
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.
|
||||
func (cfg *ConfigBase) Writers() ([]Writer, error) {
|
||||
writers := make([]Writer, 0, 2)
|
||||
func (cfg *ConfigBase) Writers() ([]File, error) {
|
||||
writers := make([]File, 0, 2)
|
||||
if cfg.Path != "" {
|
||||
io, err := NewFileIO(cfg.Path)
|
||||
f, err := OpenFile(cfg.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writers = append(writers, io)
|
||||
writers = append(writers, f)
|
||||
}
|
||||
if cfg.Stdout {
|
||||
writers = append(writers, NewStdout())
|
||||
writers = append(writers, stdout)
|
||||
}
|
||||
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 {
|
||||
return &RequestLoggerConfig{
|
||||
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 (
|
||||
AccessLogger interface {
|
||||
Log(req *http.Request, res *http.Response)
|
||||
LogError(req *http.Request, err error)
|
||||
LogACL(info *maxmind.IPInfo, blocked bool)
|
||||
|
||||
Config() *Config
|
||||
|
||||
Flush()
|
||||
Close() error
|
||||
File interface {
|
||||
io.WriteCloser
|
||||
supportRotate
|
||||
Name() string
|
||||
}
|
||||
|
||||
accessLogger struct {
|
||||
fileAccessLogger struct {
|
||||
task *task.Task
|
||||
cfg *Config
|
||||
|
||||
writer BufferedWriter
|
||||
supportRotate SupportRotate
|
||||
writeLock *sync.Mutex
|
||||
closed bool
|
||||
writer BufferedWriter
|
||||
file File
|
||||
writeLock *sync.Mutex
|
||||
closed bool
|
||||
|
||||
writeCount int64
|
||||
bufSize int
|
||||
@@ -48,32 +43,7 @@ type (
|
||||
logger zerolog.Logger
|
||||
|
||||
RequestFormatter
|
||||
ACLFormatter
|
||||
}
|
||||
|
||||
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
|
||||
ACLLogFormatter
|
||||
}
|
||||
)
|
||||
|
||||
@@ -96,112 +66,87 @@ const (
|
||||
var bytesPool = synk.GetUnsizedBytesPool()
|
||||
var sizedPool = synk.GetSizedBytesPool()
|
||||
|
||||
func NewAccessLogger(parent task.Parent, cfg AnyConfig) (AccessLogger, error) {
|
||||
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 {
|
||||
func NewFileAccessLogger(parent task.Parent, file File, anyCfg AnyConfig) AccessLogger {
|
||||
cfg := anyCfg.ToConfig()
|
||||
if cfg.RotateInterval == 0 {
|
||||
cfg.RotateInterval = defaultRotateInterval
|
||||
}
|
||||
|
||||
l := &accessLogger{
|
||||
task: parent.Subtask("accesslog."+writer.Name(), true),
|
||||
name := file.Name()
|
||||
l := &fileAccessLogger{
|
||||
task: parent.Subtask("accesslog."+name, true),
|
||||
cfg: cfg,
|
||||
bufSize: InitialBufferSize,
|
||||
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(writer, InitialBufferSize)
|
||||
} else {
|
||||
l.writer = NewUnbufferedWriter(writer)
|
||||
}
|
||||
|
||||
if supportRotate, ok := writer.(SupportRotate); ok {
|
||||
l.supportRotate = supportRotate
|
||||
}
|
||||
l.writer = ioutils.NewBufferedWriter(file, InitialBufferSize)
|
||||
l.file = file
|
||||
|
||||
if cfg.req != nil {
|
||||
fmt := CommonFormatter{cfg: &cfg.req.Fields}
|
||||
switch cfg.req.Format {
|
||||
case FormatCommon:
|
||||
l.RequestFormatter = &fmt
|
||||
l.RequestFormatter = CommonFormatter{cfg: &cfg.req.Fields}
|
||||
case FormatCombined:
|
||||
l.RequestFormatter = &CombinedFormatter{fmt}
|
||||
l.RequestFormatter = CombinedFormatter{CommonFormatter{cfg: &cfg.req.Fields}}
|
||||
case FormatJSON:
|
||||
l.RequestFormatter = &JSONFormatter{fmt}
|
||||
l.RequestFormatter = JSONFormatter{cfg: &cfg.req.Fields}
|
||||
default: // should not happen, validation has done by validate tags
|
||||
panic("invalid access log format")
|
||||
}
|
||||
} else {
|
||||
l.ACLFormatter = ACLLogFormatter{}
|
||||
}
|
||||
|
||||
go l.start()
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *accessLogger) Config() *Config {
|
||||
func (l *fileAccessLogger) Config() *Config {
|
||||
return l.cfg
|
||||
}
|
||||
|
||||
func (l *accessLogger) shouldLog(req *http.Request, res *http.Response) bool {
|
||||
if !l.cfg.req.Filters.StatusCodes.CheckKeep(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) {
|
||||
func (l *fileAccessLogger) LogRequest(req *http.Request, res *http.Response) {
|
||||
if !l.cfg.ShouldLogRequest(req, res) {
|
||||
return
|
||||
}
|
||||
|
||||
line := bytesPool.Get()
|
||||
line = l.AppendRequestLog(line, req, res)
|
||||
if line[len(line)-1] != '\n' {
|
||||
line = append(line, '\n')
|
||||
line := bytesPool.GetBuffer()
|
||||
defer bytesPool.PutBuffer(line)
|
||||
l.AppendRequestLog(line, req, res)
|
||||
// line is never empty
|
||||
if line.Bytes()[line.Len()-1] != '\n' {
|
||||
line.WriteByte('\n')
|
||||
}
|
||||
l.write(line)
|
||||
bytesPool.Put(line)
|
||||
l.write(line.Bytes())
|
||||
}
|
||||
|
||||
func (l *accessLogger) LogError(req *http.Request, err error) {
|
||||
l.Log(req, &http.Response{StatusCode: http.StatusInternalServerError, Status: err.Error()})
|
||||
var internalErrorResponse = &http.Response{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Status: http.StatusText(http.StatusInternalServerError),
|
||||
}
|
||||
|
||||
func (l *accessLogger) LogACL(info *maxmind.IPInfo, blocked bool) {
|
||||
line := bytesPool.Get()
|
||||
line = l.AppendACLLog(line, info, blocked)
|
||||
if line[len(line)-1] != '\n' {
|
||||
line = append(line, '\n')
|
||||
func (l *fileAccessLogger) LogError(req *http.Request, err error) {
|
||||
l.LogRequest(req, internalErrorResponse)
|
||||
}
|
||||
|
||||
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)
|
||||
bytesPool.Put(line)
|
||||
l.write(line.Bytes())
|
||||
}
|
||||
|
||||
func (l *accessLogger) ShouldRotate() bool {
|
||||
return l.supportRotate != nil && l.cfg.Retention.IsValid()
|
||||
func (l *fileAccessLogger) ShouldRotate() bool {
|
||||
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() {
|
||||
return false, nil
|
||||
}
|
||||
@@ -210,11 +155,11 @@ func (l *accessLogger) Rotate(result *RotateResult) (rotated bool, err error) {
|
||||
l.writeLock.Lock()
|
||||
defer l.writeLock.Unlock()
|
||||
|
||||
rotated, err = rotateLogFile(l.supportRotate, l.cfg.Retention, result)
|
||||
rotated, err = rotateLogFile(l.file, l.cfg.Retention, result)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *accessLogger) handleErr(err error) {
|
||||
func (l *fileAccessLogger) handleErr(err error) {
|
||||
if l.errRateLimiter.Allow() {
|
||||
gperr.LogError("failed to write access log", err, &l.logger)
|
||||
} else {
|
||||
@@ -223,7 +168,7 @@ func (l *accessLogger) handleErr(err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (l *accessLogger) start() {
|
||||
func (l *fileAccessLogger) start() {
|
||||
defer func() {
|
||||
l.Flush()
|
||||
l.Close()
|
||||
@@ -259,7 +204,7 @@ func (l *accessLogger) start() {
|
||||
}
|
||||
}
|
||||
|
||||
func (l *accessLogger) Close() error {
|
||||
func (l *fileAccessLogger) Close() error {
|
||||
l.writeLock.Lock()
|
||||
defer l.writeLock.Unlock()
|
||||
if l.closed {
|
||||
@@ -270,7 +215,7 @@ func (l *accessLogger) Close() error {
|
||||
return l.writer.Close()
|
||||
}
|
||||
|
||||
func (l *accessLogger) Flush() {
|
||||
func (l *fileAccessLogger) Flush() {
|
||||
l.writeLock.Lock()
|
||||
defer l.writeLock.Unlock()
|
||||
if l.closed {
|
||||
@@ -279,7 +224,7 @@ func (l *accessLogger) Flush() {
|
||||
l.writer.Flush()
|
||||
}
|
||||
|
||||
func (l *accessLogger) write(data []byte) {
|
||||
func (l *fileAccessLogger) write(data []byte) {
|
||||
l.writeLock.Lock()
|
||||
defer l.writeLock.Unlock()
|
||||
if l.closed {
|
||||
@@ -294,7 +239,7 @@ func (l *accessLogger) write(data []byte) {
|
||||
atomic.AddInt64(&l.writeCount, int64(n))
|
||||
}
|
||||
|
||||
func (l *accessLogger) adjustBuffer() {
|
||||
func (l *fileAccessLogger) adjustBuffer() {
|
||||
wps := int(atomic.SwapInt64(&l.writeCount, 0)) / int(bufferAdjustInterval.Seconds())
|
||||
origBufSize := l.bufSize
|
||||
newBufSize := origBufSize
|
||||
@@ -1,6 +1,7 @@
|
||||
package accesslog_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -53,13 +54,13 @@ var (
|
||||
)
|
||||
|
||||
func fmtLog(cfg *RequestLoggerConfig) (ts string, line string) {
|
||||
buf := make([]byte, 0, 1024)
|
||||
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||
|
||||
t := time.Now()
|
||||
logger := NewMockAccessLogger(testTask, cfg)
|
||||
mockable.MockTimeNow(t)
|
||||
buf = logger.(RequestFormatter).AppendRequestLog(buf, req, resp)
|
||||
return t.Format(LogTimeFormat), string(buf)
|
||||
logger.(RequestFormatter).AppendRequestLog(buf, req, resp)
|
||||
return t.Format(LogTimeFormat), buf.String()
|
||||
}
|
||||
|
||||
func TestAccessLoggerCommon(t *testing.T) {
|
||||
@@ -141,9 +142,6 @@ func TestAccessLoggerJSON(t *testing.T) {
|
||||
expect.Equal(t, entry.UserAgent, ua)
|
||||
expect.Equal(t, len(entry.Headers), 0)
|
||||
expect.Equal(t, len(entry.Cookies), 0)
|
||||
if status >= 400 {
|
||||
expect.Equal(t, entry.Error, http.StatusText(status))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAccessLoggerJSON(b *testing.B) {
|
||||
@@ -152,7 +150,7 @@ func BenchmarkAccessLoggerJSON(b *testing.B) {
|
||||
logger := NewMockAccessLogger(testTask, config)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
logger.Log(req, resp)
|
||||
logger.LogRequest(req, resp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +160,6 @@ func BenchmarkAccessLoggerCombined(b *testing.B) {
|
||||
logger := NewMockAccessLogger(testTask, config)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
logger.Log(req, resp)
|
||||
logger.LogRequest(req, resp)
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,11 @@ type (
|
||||
CommonFormatter struct {
|
||||
cfg *Fields
|
||||
}
|
||||
CombinedFormatter struct{ CommonFormatter }
|
||||
JSONFormatter struct{ CommonFormatter }
|
||||
ACLLogFormatter struct{}
|
||||
CombinedFormatter struct{ CommonFormatter }
|
||||
JSONFormatter struct{ cfg *Fields }
|
||||
ConsoleFormatter struct{ cfg *Fields }
|
||||
ACLLogFormatter struct{}
|
||||
ConsoleACLFormatter struct{}
|
||||
)
|
||||
|
||||
const LogTimeFormat = "02/Jan/2006:15:04:05 -0700"
|
||||
@@ -30,24 +32,26 @@ func scheme(req *http.Request) string {
|
||||
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()
|
||||
line = append(line, uri...)
|
||||
line.WriteString(uri)
|
||||
isFirst := true
|
||||
for k, v := range query {
|
||||
if isFirst {
|
||||
line = append(line, '?')
|
||||
line.WriteByte('?')
|
||||
isFirst = false
|
||||
} else {
|
||||
line = append(line, '&')
|
||||
line.WriteByte('&')
|
||||
}
|
||||
line = append(line, k...)
|
||||
line = append(line, '=')
|
||||
for _, v := range v {
|
||||
line = append(line, v...)
|
||||
for i, val := range v {
|
||||
if i > 0 {
|
||||
line.WriteByte('&')
|
||||
}
|
||||
line.WriteString(k)
|
||||
line.WriteByte('=')
|
||||
line.WriteString(val)
|
||||
}
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
func clientIP(req *http.Request) string {
|
||||
@@ -58,50 +62,51 @@ func clientIP(req *http.Request) string {
|
||||
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())
|
||||
|
||||
line = append(line, req.Host...)
|
||||
line = append(line, ' ')
|
||||
line.WriteString(req.Host)
|
||||
line.WriteByte(' ')
|
||||
|
||||
line = append(line, clientIP(req)...)
|
||||
line = append(line, " - - ["...)
|
||||
line.WriteString(clientIP(req))
|
||||
line.WriteString(" - - [")
|
||||
|
||||
line = mockable.TimeNow().AppendFormat(line, LogTimeFormat)
|
||||
line = append(line, `] "`...)
|
||||
line.WriteString(mockable.TimeNow().Format(LogTimeFormat))
|
||||
line.WriteString("] \"")
|
||||
|
||||
line = append(line, req.Method...)
|
||||
line = append(line, ' ')
|
||||
line = appendRequestURI(line, req, query)
|
||||
line = append(line, ' ')
|
||||
line = append(line, req.Proto...)
|
||||
line = append(line, '"')
|
||||
line = append(line, ' ')
|
||||
line.WriteString(req.Method)
|
||||
line.WriteByte(' ')
|
||||
appendRequestURI(line, req, query)
|
||||
line.WriteByte(' ')
|
||||
line.WriteString(req.Proto)
|
||||
line.WriteByte('"')
|
||||
line.WriteByte(' ')
|
||||
|
||||
line = strconv.AppendInt(line, int64(res.StatusCode), 10)
|
||||
line = append(line, ' ')
|
||||
line = strconv.AppendInt(line, res.ContentLength, 10)
|
||||
return line
|
||||
line.WriteString(strconv.FormatInt(int64(res.StatusCode), 10))
|
||||
line.WriteByte(' ')
|
||||
line.WriteString(strconv.FormatInt(res.ContentLength, 10))
|
||||
}
|
||||
|
||||
func (f *CombinedFormatter) AppendRequestLog(line []byte, req *http.Request, res *http.Response) []byte {
|
||||
line = f.CommonFormatter.AppendRequestLog(line, req, res)
|
||||
line = append(line, " \""...)
|
||||
line = append(line, req.Referer()...)
|
||||
line = append(line, "\" \""...)
|
||||
line = append(line, req.UserAgent()...)
|
||||
line = append(line, '"')
|
||||
return line
|
||||
func (f CombinedFormatter) AppendRequestLog(line *bytes.Buffer, req *http.Request, res *http.Response) {
|
||||
f.CommonFormatter.AppendRequestLog(line, req, res)
|
||||
line.WriteString(" \"")
|
||||
line.WriteString(req.Referer())
|
||||
line.WriteString("\" \"")
|
||||
line.WriteString(req.UserAgent())
|
||||
line.WriteByte('"')
|
||||
}
|
||||
|
||||
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())
|
||||
headers := f.cfg.Headers.ZerologHeaders(req.Header)
|
||||
cookies := f.cfg.Cookies.ZerologCookies(req.Cookies())
|
||||
contentType := res.Header.Get("Content-Type")
|
||||
|
||||
writer := bytes.NewBuffer(line)
|
||||
logger := zerolog.New(writer)
|
||||
event := logger.Info().
|
||||
Str("time", mockable.TimeNow().Format(LogTimeFormat)).
|
||||
Str("ip", clientIP(req)).
|
||||
@@ -119,22 +124,33 @@ func (f *JSONFormatter) AppendRequestLog(line []byte, req *http.Request, res *ht
|
||||
Object("headers", headers).
|
||||
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
|
||||
event.Send()
|
||||
return writer.Bytes()
|
||||
}
|
||||
|
||||
func (f ACLLogFormatter) AppendACLLog(line []byte, info *maxmind.IPInfo, blocked bool) []byte {
|
||||
writer := bytes.NewBuffer(line)
|
||||
logger := zerolog.New(writer)
|
||||
func (f ConsoleFormatter) LogRequestZeroLog(logger *zerolog.Logger, req *http.Request, res *http.Response) {
|
||||
contentType := res.Header.Get("Content-Type")
|
||||
|
||||
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().
|
||||
Str("time", mockable.TimeNow().Format(LogTimeFormat)).
|
||||
Str("ip", info.Str)
|
||||
@@ -144,10 +160,32 @@ func (f ACLLogFormatter) AppendACLLog(line []byte, info *maxmind.IPInfo, blocked
|
||||
event.Str("action", "allow")
|
||||
}
|
||||
if info.City != nil {
|
||||
event.Str("iso_code", info.City.Country.IsoCode)
|
||||
event.Str("time_zone", info.City.Location.TimeZone)
|
||||
if isoCode := info.City.Country.IsoCode; isoCode != "" {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
var _ SupportRotate = (*MockFile)(nil)
|
||||
var _ File = (*MockFile)(nil)
|
||||
|
||||
func NewMockFile(buffered bool) *MockFile {
|
||||
f, _ := afero.TempFile(afero.NewMemMapFs(), "", "")
|
||||
@@ -52,14 +52,9 @@ func (m *MockFile) NumLines() int {
|
||||
return count
|
||||
}
|
||||
|
||||
func (m *MockFile) Size() (int64, error) {
|
||||
stat, _ := m.Stat()
|
||||
return stat.Size(), nil
|
||||
}
|
||||
|
||||
func (m *MockFile) MustSize() int64 {
|
||||
size, _ := m.Size()
|
||||
return size
|
||||
stat, _ := m.Stat()
|
||||
return stat.Size()
|
||||
}
|
||||
|
||||
func (m *MockFile) Close() error {
|
||||
|
||||
@@ -15,14 +15,21 @@ type MultiAccessLogger struct {
|
||||
//
|
||||
// If there is only one writer, it will return a single AccessLogger.
|
||||
// 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 {
|
||||
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))
|
||||
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}
|
||||
}
|
||||
@@ -31,9 +38,9 @@ func (m *MultiAccessLogger) Config() *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 {
|
||||
accessLogger.Log(req, res)
|
||||
accessLogger.LogRequest(req, res)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestNewMultiAccessLogger(t *testing.T) {
|
||||
testTask := task.RootTask("test", false)
|
||||
cfg := DefaultRequestLoggerConfig()
|
||||
|
||||
writers := []Writer{
|
||||
writers := []File{
|
||||
NewMockFile(true),
|
||||
NewMockFile(true),
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func TestMultiAccessLoggerConfig(t *testing.T) {
|
||||
cfg := DefaultRequestLoggerConfig()
|
||||
cfg.Format = FormatCommon
|
||||
|
||||
writers := []Writer{
|
||||
writers := []File{
|
||||
NewMockFile(true),
|
||||
NewMockFile(true),
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func TestMultiAccessLoggerLog(t *testing.T) {
|
||||
|
||||
writer1 := NewMockFile(true)
|
||||
writer2 := NewMockFile(true)
|
||||
writers := []Writer{writer1, writer2}
|
||||
writers := []File{writer1, writer2}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
|
||||
@@ -68,7 +68,7 @@ func TestMultiAccessLoggerLog(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
}
|
||||
|
||||
logger.Log(req, resp)
|
||||
logger.LogRequest(req, resp)
|
||||
logger.Flush()
|
||||
|
||||
expect.Equal(t, writer1.NumLines(), 1)
|
||||
@@ -81,7 +81,7 @@ func TestMultiAccessLoggerLogError(t *testing.T) {
|
||||
|
||||
writer1 := NewMockFile(true)
|
||||
writer2 := NewMockFile(true)
|
||||
writers := []Writer{writer1, writer2}
|
||||
writers := []File{writer1, writer2}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
|
||||
@@ -107,7 +107,7 @@ func TestMultiAccessLoggerLogACL(t *testing.T) {
|
||||
|
||||
writer1 := NewMockFile(true)
|
||||
writer2 := NewMockFile(true)
|
||||
writers := []Writer{writer1, writer2}
|
||||
writers := []File{writer1, writer2}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
|
||||
@@ -129,7 +129,7 @@ func TestMultiAccessLoggerFlush(t *testing.T) {
|
||||
|
||||
writer1 := NewMockFile(true)
|
||||
writer2 := NewMockFile(true)
|
||||
writers := []Writer{writer1, writer2}
|
||||
writers := []File{writer1, writer2}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
|
||||
@@ -143,7 +143,7 @@ func TestMultiAccessLoggerFlush(t *testing.T) {
|
||||
StatusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
logger.Log(req, resp)
|
||||
logger.LogRequest(req, resp)
|
||||
logger.Flush()
|
||||
|
||||
expect.Equal(t, writer1.NumLines(), 1)
|
||||
@@ -156,7 +156,7 @@ func TestMultiAccessLoggerClose(t *testing.T) {
|
||||
|
||||
writer1 := NewMockFile(true)
|
||||
writer2 := NewMockFile(true)
|
||||
writers := []Writer{writer1, writer2}
|
||||
writers := []File{writer1, writer2}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
|
||||
@@ -170,7 +170,7 @@ func TestMultiAccessLoggerMultipleLogs(t *testing.T) {
|
||||
|
||||
writer1 := NewMockFile(true)
|
||||
writer2 := NewMockFile(true)
|
||||
writers := []Writer{writer1, writer2}
|
||||
writers := []File{writer1, writer2}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
|
||||
@@ -185,7 +185,7 @@ func TestMultiAccessLoggerMultipleLogs(t *testing.T) {
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
}
|
||||
logger.Log(req, resp)
|
||||
logger.LogRequest(req, resp)
|
||||
}
|
||||
|
||||
logger.Flush()
|
||||
@@ -199,7 +199,7 @@ func TestMultiAccessLoggerSingleWriter(t *testing.T) {
|
||||
cfg := DefaultRequestLoggerConfig()
|
||||
|
||||
writer := NewMockFile(true)
|
||||
writers := []Writer{writer}
|
||||
writers := []File{writer}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
expect.NotNil(t, logger)
|
||||
@@ -214,7 +214,7 @@ func TestMultiAccessLoggerSingleWriter(t *testing.T) {
|
||||
StatusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
logger.Log(req, resp)
|
||||
logger.LogRequest(req, resp)
|
||||
logger.Flush()
|
||||
|
||||
expect.Equal(t, writer.NumLines(), 1)
|
||||
@@ -226,7 +226,7 @@ func TestMultiAccessLoggerMixedOperations(t *testing.T) {
|
||||
|
||||
writer1 := NewMockFile(true)
|
||||
writer2 := NewMockFile(true)
|
||||
writers := []Writer{writer1, writer2}
|
||||
writers := []File{writer1, writer2}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
|
||||
@@ -241,7 +241,7 @@ func TestMultiAccessLoggerMixedOperations(t *testing.T) {
|
||||
StatusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
logger.Log(req, resp)
|
||||
logger.LogRequest(req, resp)
|
||||
logger.Flush()
|
||||
|
||||
info := &maxmind.IPInfo{
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
)
|
||||
|
||||
type Retention struct {
|
||||
Days uint64 `json:"days"`
|
||||
Last uint64 `json:"last"`
|
||||
KeepSize uint64 `json:"keep_size"`
|
||||
Days uint64 `json:"days,omitempty"`
|
||||
Last uint64 `json:"last,omitempty"`
|
||||
KeepSize uint64 `json:"keep_size,omitempty"`
|
||||
} // @name LogRetention
|
||||
|
||||
var (
|
||||
@@ -20,7 +20,7 @@ var (
|
||||
)
|
||||
|
||||
// see back_scanner_test.go#L210 for benchmarks
|
||||
var defaultChunkSize = 256 * kilobyte
|
||||
var defaultChunkSize = 32 * kilobyte
|
||||
|
||||
// Syntax:
|
||||
//
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
@@ -17,7 +18,7 @@ type supportRotate interface {
|
||||
io.ReaderAt
|
||||
io.WriterAt
|
||||
Truncate(size int64) error
|
||||
Size() (int64, error)
|
||||
Stat() (fs.FileInfo, error)
|
||||
}
|
||||
|
||||
type RotateResult struct {
|
||||
@@ -93,10 +94,11 @@ func rotateLogFileByPolicy(file supportRotate, config *Retention, result *Rotate
|
||||
return false, nil // should not happen
|
||||
}
|
||||
|
||||
fileSize, err := file.Size()
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
fileSize := stat.Size()
|
||||
|
||||
// nothing to rotate, return the nothing
|
||||
if fileSize == 0 {
|
||||
@@ -104,6 +106,7 @@ func rotateLogFileByPolicy(file supportRotate, config *Retention, result *Rotate
|
||||
}
|
||||
|
||||
s := NewBackScanner(file, fileSize, defaultChunkSize)
|
||||
defer s.Release()
|
||||
result.OriginalSize = fileSize
|
||||
|
||||
// Store the line positions and sizes we want to keep
|
||||
@@ -216,16 +219,17 @@ func fileContentMove(file supportRotate, srcPos, dstPos int64, size int) error {
|
||||
//
|
||||
// Invalid lines will not be detected and included in the result.
|
||||
func rotateLogFileBySize(file supportRotate, config *Retention, result *RotateResult) (rotated bool, err error) {
|
||||
filesize, err := file.Size()
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
fileSize := stat.Size()
|
||||
|
||||
result.OriginalSize = filesize
|
||||
result.OriginalSize = fileSize
|
||||
|
||||
keepSize := int64(config.KeepSize)
|
||||
if keepSize >= filesize {
|
||||
result.NumBytesKeep = filesize
|
||||
if keepSize >= fileSize {
|
||||
result.NumBytesKeep = fileSize
|
||||
return false, nil
|
||||
}
|
||||
result.NumBytesKeep = keepSize
|
||||
|
||||
@@ -57,13 +57,13 @@ func TestRotateKeepLast(t *testing.T) {
|
||||
t.Run(string(format)+" keep last", func(t *testing.T) {
|
||||
file := NewMockFile(true)
|
||||
mockable.MockTimeNow(testTime)
|
||||
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
logger := NewFileAccessLogger(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
Format: format,
|
||||
})
|
||||
expect.Nil(t, logger.Config().Retention)
|
||||
|
||||
for range 10 {
|
||||
logger.Log(req, resp)
|
||||
logger.LogRequest(req, resp)
|
||||
}
|
||||
logger.Flush()
|
||||
|
||||
@@ -87,14 +87,14 @@ func TestRotateKeepLast(t *testing.T) {
|
||||
|
||||
t.Run(string(format)+" keep days", func(t *testing.T) {
|
||||
file := NewMockFile(true)
|
||||
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
logger := NewFileAccessLogger(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
Format: format,
|
||||
})
|
||||
expect.Nil(t, logger.Config().Retention)
|
||||
nLines := 10
|
||||
for i := range nLines {
|
||||
mockable.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
|
||||
logger.Log(req, resp)
|
||||
logger.LogRequest(req, resp)
|
||||
}
|
||||
logger.Flush()
|
||||
expect.Equal(t, file.NumLines(), nLines)
|
||||
@@ -133,14 +133,14 @@ func TestRotateKeepFileSize(t *testing.T) {
|
||||
for _, format := range ReqLoggerFormats {
|
||||
t.Run(string(format)+" keep size no rotation", func(t *testing.T) {
|
||||
file := NewMockFile(true)
|
||||
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
logger := NewFileAccessLogger(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
Format: format,
|
||||
})
|
||||
expect.Nil(t, logger.Config().Retention)
|
||||
nLines := 10
|
||||
for i := range nLines {
|
||||
mockable.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
|
||||
logger.Log(req, resp)
|
||||
logger.LogRequest(req, resp)
|
||||
}
|
||||
logger.Flush()
|
||||
expect.Equal(t, file.NumLines(), nLines)
|
||||
@@ -165,14 +165,14 @@ func TestRotateKeepFileSize(t *testing.T) {
|
||||
|
||||
t.Run("keep size with rotation", func(t *testing.T) {
|
||||
file := NewMockFile(true)
|
||||
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
logger := NewFileAccessLogger(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
Format: FormatJSON,
|
||||
})
|
||||
expect.Nil(t, logger.Config().Retention)
|
||||
nLines := 100
|
||||
for i := range nLines {
|
||||
mockable.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
|
||||
logger.Log(req, resp)
|
||||
logger.LogRequest(req, resp)
|
||||
}
|
||||
logger.Flush()
|
||||
expect.Equal(t, file.NumLines(), nLines)
|
||||
@@ -199,14 +199,14 @@ func TestRotateSkipInvalidTime(t *testing.T) {
|
||||
for _, format := range ReqLoggerFormats {
|
||||
t.Run(string(format), func(t *testing.T) {
|
||||
file := NewMockFile(true)
|
||||
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
logger := NewFileAccessLogger(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
Format: format,
|
||||
})
|
||||
expect.Nil(t, logger.Config().Retention)
|
||||
nLines := 10
|
||||
for i := range nLines {
|
||||
mockable.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
|
||||
logger.Log(req, resp)
|
||||
logger.LogRequest(req, resp)
|
||||
logger.Flush()
|
||||
|
||||
n, err := file.Write([]byte("invalid time\n"))
|
||||
@@ -241,7 +241,7 @@ func BenchmarkRotate(b *testing.B) {
|
||||
for _, retention := range tests {
|
||||
b.Run(fmt.Sprintf("retention_%s", retention.String()), func(b *testing.B) {
|
||||
file := NewMockFile(true)
|
||||
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
logger := NewFileAccessLogger(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
ConfigBase: ConfigBase{
|
||||
Retention: retention,
|
||||
},
|
||||
@@ -249,7 +249,7 @@ func BenchmarkRotate(b *testing.B) {
|
||||
})
|
||||
for i := range 100 {
|
||||
mockable.MockTimeNow(testTime.AddDate(0, 0, -100+i+1))
|
||||
logger.Log(req, resp)
|
||||
logger.LogRequest(req, resp)
|
||||
}
|
||||
logger.Flush()
|
||||
content := file.Content()
|
||||
@@ -275,7 +275,7 @@ func BenchmarkRotateWithInvalidTime(b *testing.B) {
|
||||
for _, retention := range tests {
|
||||
b.Run(fmt.Sprintf("retention_%s", retention.String()), func(b *testing.B) {
|
||||
file := NewMockFile(true)
|
||||
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
logger := NewFileAccessLogger(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
ConfigBase: ConfigBase{
|
||||
Retention: retention,
|
||||
},
|
||||
@@ -283,7 +283,7 @@ func BenchmarkRotateWithInvalidTime(b *testing.B) {
|
||||
})
|
||||
for i := range 10000 {
|
||||
mockable.MockTimeNow(testTime.AddDate(0, 0, -10000+i+1))
|
||||
logger.Log(req, resp)
|
||||
logger.LogRequest(req, resp)
|
||||
if i%10 == 0 {
|
||||
_, _ = file.Write([]byte("invalid time\n"))
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"github.com/yusing/goutils/synk"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
f *os.File
|
||||
type sharedFileHandle struct {
|
||||
*os.File
|
||||
|
||||
// os.File.Name() may not equal to key of `openedFiles`.
|
||||
// Store it for later delete from `openedFiles`.
|
||||
@@ -22,18 +22,18 @@ type File struct {
|
||||
}
|
||||
|
||||
var (
|
||||
openedFiles = make(map[string]*File)
|
||||
openedFiles = make(map[string]*sharedFileHandle)
|
||||
openedFilesMu sync.Mutex
|
||||
)
|
||||
|
||||
// NewFileIO creates a new file writer with cleaned path.
|
||||
// OpenFile creates a new file writer with cleaned path.
|
||||
//
|
||||
// If the file is already opened, it will be returned.
|
||||
func NewFileIO(path string) (Writer, error) {
|
||||
func OpenFile(path string) (File, error) {
|
||||
openedFilesMu.Lock()
|
||||
defer openedFilesMu.Unlock()
|
||||
|
||||
var file *File
|
||||
var file *sharedFileHandle
|
||||
var err error
|
||||
|
||||
// make it absolute path, so that we can use it as key of `openedFiles` and shared lock
|
||||
@@ -53,65 +53,38 @@ func NewFileIO(path string) (Writer, error) {
|
||||
return nil, fmt.Errorf("access log open error: %w", err)
|
||||
}
|
||||
if _, err := f.Seek(0, io.SeekEnd); err != nil {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("access log seek error: %w", err)
|
||||
}
|
||||
file = &File{f: f, path: path, refCount: synk.NewRefCounter()}
|
||||
|
||||
file = &sharedFileHandle{File: f, path: path, refCount: synk.NewRefCounter()}
|
||||
openedFiles[path] = file
|
||||
|
||||
log.Debug().Str("path", path).Msg("file opened")
|
||||
|
||||
go file.closeOnZero()
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// Name returns the absolute path of the file.
|
||||
func (f *File) Name() string {
|
||||
func (f *sharedFileHandle) Name() string {
|
||||
return f.path
|
||||
}
|
||||
|
||||
func (f *File) ShouldBeBuffered() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *File) Write(p []byte) (n int, err error) {
|
||||
return f.f.Write(p)
|
||||
}
|
||||
|
||||
func (f *File) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
return f.f.ReadAt(p, off)
|
||||
}
|
||||
|
||||
func (f *File) WriteAt(p []byte, off int64) (n int, err error) {
|
||||
return f.f.WriteAt(p, off)
|
||||
}
|
||||
|
||||
func (f *File) Seek(offset int64, whence int) (int64, error) {
|
||||
return f.f.Seek(offset, whence)
|
||||
}
|
||||
|
||||
func (f *File) Size() (int64, error) {
|
||||
stat, err := f.f.Stat()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return stat.Size(), nil
|
||||
}
|
||||
|
||||
func (f *File) Truncate(size int64) error {
|
||||
return f.f.Truncate(size)
|
||||
}
|
||||
|
||||
func (f *File) Close() error {
|
||||
func (f *sharedFileHandle) Close() error {
|
||||
f.refCount.Sub()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *File) closeOnZero() {
|
||||
defer log.Debug().
|
||||
Str("path", f.path).
|
||||
Msg("access log closed")
|
||||
func (f *sharedFileHandle) closeOnZero() {
|
||||
defer log.Debug().Str("path", f.path).Msg("file closed")
|
||||
|
||||
<-f.refCount.Zero()
|
||||
|
||||
openedFilesMu.Lock()
|
||||
delete(openedFiles, f.path)
|
||||
openedFilesMu.Unlock()
|
||||
f.f.Close()
|
||||
err := f.File.Close()
|
||||
if err != nil {
|
||||
log.Error().Str("path", f.path).Err(err).Msg("failed to close file")
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yusing/goutils/task"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func TestConcurrentFileLoggersShareSameAccessLogIO(t *testing.T) {
|
||||
@@ -18,7 +19,7 @@ func TestConcurrentFileLoggersShareSameAccessLogIO(t *testing.T) {
|
||||
cfg.Path = "test.log"
|
||||
|
||||
loggerCount := runtime.GOMAXPROCS(0)
|
||||
accessLogIOs := make([]Writer, loggerCount)
|
||||
accessLogIOs := make([]File, loggerCount)
|
||||
|
||||
// make test log file
|
||||
file, err := os.Create(cfg.Path)
|
||||
@@ -28,16 +29,20 @@ func TestConcurrentFileLoggersShareSameAccessLogIO(t *testing.T) {
|
||||
assert.NoError(t, os.Remove(cfg.Path))
|
||||
})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var errs errgroup.Group
|
||||
for i := range loggerCount {
|
||||
wg.Go(func() {
|
||||
file, err := NewFileIO(cfg.Path)
|
||||
assert.NoError(t, err)
|
||||
errs.Go(func() error {
|
||||
file, err := OpenFile(cfg.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accessLogIOs[i] = file
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
err = errs.Wait()
|
||||
assert.NoError(t, err)
|
||||
|
||||
firstIO := accessLogIOs[0]
|
||||
for _, io := range accessLogIOs {
|
||||
@@ -58,7 +63,7 @@ func TestConcurrentAccessLoggerLogAndFlush(t *testing.T) {
|
||||
loggers := make([]AccessLogger, loggerCount)
|
||||
|
||||
for i := range loggerCount {
|
||||
loggers[i] = NewAccessLoggerWithIO(parent, file, cfg)
|
||||
loggers[i] = NewFileAccessLogger(parent, file, cfg)
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
@@ -87,7 +92,7 @@ func concurrentLog(logger AccessLogger, req *http.Request, resp *http.Response,
|
||||
var wg sync.WaitGroup
|
||||
for range n {
|
||||
wg.Go(func() {
|
||||
logger.Log(req, resp)
|
||||
logger.LogRequest(req, resp)
|
||||
if rand.IntN(2) == 0 {
|
||||
logger.Flush()
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package accesslog
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/godoxy/internal/logging"
|
||||
)
|
||||
|
||||
type Stdout struct {
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
func NewStdout() Writer {
|
||||
return &Stdout{logger: logging.NewLoggerWithFixedLevel(zerolog.InfoLevel, os.Stdout)}
|
||||
}
|
||||
|
||||
func (s Stdout) Name() string {
|
||||
return "stdout"
|
||||
}
|
||||
|
||||
func (s Stdout) ShouldBeBuffered() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s Stdout) Write(p []byte) (n int, err error) {
|
||||
return s.logger.Write(p)
|
||||
}
|
||||
|
||||
func (s Stdout) Close() error {
|
||||
return nil
|
||||
}
|
||||
55
internal/logging/accesslog/types.go
Normal file
55
internal/logging/accesslog/types.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package accesslog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
|
||||
"github.com/yusing/goutils/task"
|
||||
)
|
||||
|
||||
type (
|
||||
AccessLogger interface {
|
||||
LogRequest(req *http.Request, res *http.Response)
|
||||
LogError(req *http.Request, err error)
|
||||
LogACL(info *maxmind.IPInfo, blocked bool)
|
||||
|
||||
Config() *Config
|
||||
|
||||
Flush()
|
||||
Close() error
|
||||
}
|
||||
|
||||
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 *bytes.Buffer, req *http.Request, res *http.Response)
|
||||
}
|
||||
RequestFormatterZeroLog interface {
|
||||
// LogRequestZeroLog logs a request log to the logger
|
||||
LogRequestZeroLog(logger *zerolog.Logger, req *http.Request, res *http.Response)
|
||||
}
|
||||
ACLFormatter interface {
|
||||
// AppendACLLog appends a log line to line with or without a trailing newline
|
||||
AppendACLLog(line *bytes.Buffer, info *maxmind.IPInfo, blocked bool)
|
||||
// LogACLZeroLog logs an ACL log to the logger
|
||||
LogACLZeroLog(logger *zerolog.Logger, info *maxmind.IPInfo, blocked bool)
|
||||
}
|
||||
)
|
||||
|
||||
func NewAccessLogger(parent task.Parent, cfg AnyConfig) (AccessLogger, error) {
|
||||
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 NewFileAccessLogger(parent, NewMockFile(true), cfg)
|
||||
}
|
||||
@@ -92,7 +92,7 @@ func NewLogger(out ...io.Writer) zerolog.Logger {
|
||||
return zerolog.New(writer).Level(level).With().Timestamp().Logger()
|
||||
}
|
||||
|
||||
func NewLoggerWithFixedLevel(level zerolog.Level, out ...io.Writer) zerolog.Logger {
|
||||
func NewLoggerWithFixedLevel(lvl zerolog.Level, out ...io.Writer) zerolog.Logger {
|
||||
writer := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
|
||||
w.Out = multiLevelWriter(out...)
|
||||
w.TimeFormat = timeFmt
|
||||
@@ -103,5 +103,5 @@ func NewLoggerWithFixedLevel(level zerolog.Level, out ...io.Writer) zerolog.Logg
|
||||
return fmtMessage(msgI.(string))
|
||||
}
|
||||
})
|
||||
return zerolog.New(writer).Level(level).With().Str("level", level.String()).Timestamp().Logger()
|
||||
return zerolog.New(writer).Level(level).With().Str("level", lvl.String()).Timestamp().Logger()
|
||||
}
|
||||
|
||||
@@ -2,42 +2,31 @@ package memlogger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
)
|
||||
|
||||
type logEntryRange struct {
|
||||
Start, End int
|
||||
}
|
||||
|
||||
type memLogger struct {
|
||||
*bytes.Buffer
|
||||
sync.RWMutex
|
||||
buf *bytes.Buffer
|
||||
bufLock sync.RWMutex
|
||||
|
||||
notifyLock sync.RWMutex
|
||||
connChans *xsync.Map[chan *logEntryRange, struct{}]
|
||||
listeners *xsync.Map[chan []byte, struct{}]
|
||||
channelLock sync.RWMutex
|
||||
listeners *xsync.Map[chan []byte, struct{}]
|
||||
}
|
||||
|
||||
type MemLogger io.Writer
|
||||
|
||||
const (
|
||||
maxMemLogSize = 16 * 1024
|
||||
truncateSize = maxMemLogSize / 2
|
||||
initialWriteChunkSize = 4 * 1024
|
||||
writeTimeout = 10 * time.Second
|
||||
maxMemLogSize = 16 * 1024
|
||||
truncateSize = maxMemLogSize / 2
|
||||
listenerChanBufSize = 64
|
||||
)
|
||||
|
||||
var memLoggerInstance = &memLogger{
|
||||
Buffer: bytes.NewBuffer(make([]byte, maxMemLogSize)),
|
||||
connChans: xsync.NewMap[chan *logEntryRange, struct{}](),
|
||||
buf: bytes.NewBuffer(make([]byte, 0, maxMemLogSize)),
|
||||
listeners: xsync.NewMap[chan []byte, struct{}](),
|
||||
}
|
||||
|
||||
@@ -45,10 +34,6 @@ func GetMemLogger() MemLogger {
|
||||
return memLoggerInstance
|
||||
}
|
||||
|
||||
func HandlerFunc() gin.HandlerFunc {
|
||||
return memLoggerInstance.ServeHTTP
|
||||
}
|
||||
|
||||
func Events() (<-chan []byte, func()) {
|
||||
return memLoggerInstance.events()
|
||||
}
|
||||
@@ -56,136 +41,90 @@ func Events() (<-chan []byte, func()) {
|
||||
// Write implements io.Writer.
|
||||
func (m *memLogger) Write(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
if n == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
m.truncateIfNeeded(n)
|
||||
|
||||
pos, err := m.writeBuf(p)
|
||||
err = m.writeBuf(p)
|
||||
if err != nil {
|
||||
// not logging the error here, it will cause Run to be called again = infinite loop
|
||||
return n, err
|
||||
}
|
||||
|
||||
m.notifyWS(pos, n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (m *memLogger) ServeHTTP(c *gin.Context) {
|
||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create websocket manager"))
|
||||
return
|
||||
if m.listeners.Size() == 0 {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
logCh := make(chan *logEntryRange)
|
||||
m.connChans.Store(logCh, struct{}{})
|
||||
|
||||
defer func() {
|
||||
manager.Close()
|
||||
m.notifyLock.Lock()
|
||||
m.connChans.Delete(logCh)
|
||||
close(logCh)
|
||||
m.notifyLock.Unlock()
|
||||
}()
|
||||
|
||||
if err := m.wsInitial(manager); err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to send initial log"))
|
||||
return
|
||||
}
|
||||
|
||||
m.wsStreamLog(c.Request.Context(), manager, logCh)
|
||||
msg := slices.Clone(p)
|
||||
m.notifyWS(msg)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (m *memLogger) truncateIfNeeded(n int) {
|
||||
m.RLock()
|
||||
needTruncate := m.Len()+n > maxMemLogSize
|
||||
m.RUnlock()
|
||||
m.bufLock.RLock()
|
||||
needTruncate := m.buf.Len()+n > maxMemLogSize
|
||||
m.bufLock.RUnlock()
|
||||
|
||||
if needTruncate {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
needTruncate = m.Len()+n > maxMemLogSize
|
||||
if !needTruncate {
|
||||
return
|
||||
}
|
||||
|
||||
m.Truncate(truncateSize)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *memLogger) notifyWS(pos, n int) {
|
||||
if m.connChans.Size() == 0 && m.listeners.Size() == 0 {
|
||||
if !needTruncate {
|
||||
return
|
||||
}
|
||||
|
||||
timeout := time.NewTimer(3 * time.Second)
|
||||
defer timeout.Stop()
|
||||
m.bufLock.Lock()
|
||||
defer m.bufLock.Unlock()
|
||||
|
||||
m.notifyLock.RLock()
|
||||
defer m.notifyLock.RUnlock()
|
||||
|
||||
m.connChans.Range(func(ch chan *logEntryRange, _ struct{}) bool {
|
||||
select {
|
||||
case ch <- &logEntryRange{pos, pos + n}:
|
||||
return true
|
||||
case <-timeout.C:
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
if m.listeners.Size() > 0 {
|
||||
msg := m.Bytes()[pos : pos+n]
|
||||
m.listeners.Range(func(ch chan []byte, _ struct{}) bool {
|
||||
select {
|
||||
case <-timeout.C:
|
||||
return false
|
||||
case ch <- msg:
|
||||
return true
|
||||
}
|
||||
})
|
||||
discard := m.buf.Len() - truncateSize
|
||||
if discard > 0 {
|
||||
_ = m.buf.Next(discard)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *memLogger) writeBuf(b []byte) (pos int, err error) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
pos = m.Len()
|
||||
_, err = m.Buffer.Write(b)
|
||||
return pos, err
|
||||
func (m *memLogger) notifyWS(msg []byte) {
|
||||
if len(msg) == 0 || m.listeners.Size() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
m.channelLock.RLock()
|
||||
defer m.channelLock.RUnlock()
|
||||
|
||||
for ch := range m.listeners.Range {
|
||||
select {
|
||||
case ch <- msg:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *memLogger) writeBuf(b []byte) (err error) {
|
||||
m.bufLock.Lock()
|
||||
defer m.bufLock.Unlock()
|
||||
|
||||
_, err = m.buf.Write(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if m.buf.Len() > maxMemLogSize {
|
||||
discard := m.buf.Len() - maxMemLogSize
|
||||
if discard > 0 {
|
||||
_ = m.buf.Next(discard)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memLogger) events() (logs <-chan []byte, cancel func()) {
|
||||
ch := make(chan []byte)
|
||||
m.notifyLock.Lock()
|
||||
defer m.notifyLock.Unlock()
|
||||
ch := make(chan []byte, listenerChanBufSize)
|
||||
m.channelLock.Lock()
|
||||
defer m.channelLock.Unlock()
|
||||
m.listeners.Store(ch, struct{}{})
|
||||
|
||||
return ch, func() {
|
||||
m.notifyLock.Lock()
|
||||
defer m.notifyLock.Unlock()
|
||||
m.channelLock.Lock()
|
||||
defer m.channelLock.Unlock()
|
||||
m.listeners.Delete(ch)
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *memLogger) wsInitial(manager *websocket.Manager) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
return manager.WriteData(websocket.TextMessage, m.Bytes(), writeTimeout)
|
||||
}
|
||||
|
||||
func (m *memLogger) wsStreamLog(ctx context.Context, manager *websocket.Manager, ch <-chan *logEntryRange) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case logRange := <-ch:
|
||||
m.RLock()
|
||||
msg := m.Bytes()[logRange.Start:logRange.End]
|
||||
err := manager.WriteData(websocket.TextMessage, msg, writeTimeout)
|
||||
m.RUnlock()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,13 +55,10 @@ Individual route status at a point in time.
|
||||
```go
|
||||
type RouteAggregate struct {
|
||||
Alias string `json:"alias"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Uptime float32 `json:"uptime"`
|
||||
Downtime float32 `json:"downtime"`
|
||||
Idle float32 `json:"idle"`
|
||||
AvgLatency float32 `json:"avg_latency"`
|
||||
IsDocker bool `json:"is_docker"`
|
||||
IsExcluded bool `json:"is_excluded"`
|
||||
CurrentStatus types.HealthStatus `json:"current_status" swaggertype:"string" enums:"healthy,unhealthy,unknown,napping,starting"`
|
||||
Statuses []Status `json:"statuses"`
|
||||
}
|
||||
@@ -312,7 +309,7 @@ const ws = new WebSocket(
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
data.data.forEach((route) => {
|
||||
console.log(`${route.display_name}: ${route.uptime * 100}% uptime`);
|
||||
console.log(`${route.alias}: ${route.uptime * 100}% uptime`);
|
||||
});
|
||||
};
|
||||
```
|
||||
@@ -336,7 +333,7 @@ _, agg := uptime.aggregateStatuses(entries, url.Values{
|
||||
|
||||
for _, route := range agg {
|
||||
fmt.Printf("%s: %.1f%% uptime, %.1fms avg latency\n",
|
||||
route.DisplayName, route.Uptime*100, route.AvgLatency)
|
||||
route.Alias, route.Uptime*100, route.AvgLatency)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -365,13 +362,10 @@ for _, route := range agg {
|
||||
"data": [
|
||||
{
|
||||
"alias": "api-server",
|
||||
"display_name": "API Server",
|
||||
"uptime": 0.98,
|
||||
"downtime": 0.02,
|
||||
"idle": 0.0,
|
||||
"avg_latency": 45.5,
|
||||
"is_docker": true,
|
||||
"is_excluded": false,
|
||||
"current_status": "healthy",
|
||||
"statuses": [
|
||||
{ "status": "healthy", "latency": 45, "timestamp": 1704892800 }
|
||||
|
||||
@@ -27,13 +27,10 @@ type (
|
||||
RouteStatuses map[string][]Status // @name RouteStatuses
|
||||
RouteAggregate struct {
|
||||
Alias string `json:"alias"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Uptime float32 `json:"uptime"`
|
||||
Downtime float32 `json:"downtime"`
|
||||
Idle float32 `json:"idle"`
|
||||
AvgLatency float32 `json:"avg_latency"`
|
||||
IsDocker bool `json:"is_docker"`
|
||||
IsExcluded bool `json:"is_excluded"`
|
||||
CurrentStatus types.HealthStatus `json:"current_status" swaggertype:"string" enums:"healthy,unhealthy,unknown,napping,starting"`
|
||||
Statuses []Status `json:"statuses"`
|
||||
} // @name RouteUptimeAggregate
|
||||
@@ -129,18 +126,9 @@ func (rs RouteStatuses) aggregate(limit int, offset int) Aggregated {
|
||||
statuses := rs[alias]
|
||||
up, down, idle, latency := rs.calculateInfo(statuses)
|
||||
|
||||
displayName := alias
|
||||
r, ok := routes.Get(alias)
|
||||
if !ok {
|
||||
// also search for excluded routes
|
||||
r, ok = routes.Excluded.Get(alias)
|
||||
}
|
||||
if r != nil {
|
||||
displayName = r.DisplayName()
|
||||
}
|
||||
|
||||
status := types.StatusUnknown
|
||||
if r != nil {
|
||||
r, ok := routes.GetIncludeExcluded(alias)
|
||||
if ok {
|
||||
mon := r.HealthMonitor()
|
||||
if mon != nil {
|
||||
status = mon.Status()
|
||||
@@ -149,15 +137,12 @@ func (rs RouteStatuses) aggregate(limit int, offset int) Aggregated {
|
||||
|
||||
result[i] = RouteAggregate{
|
||||
Alias: alias,
|
||||
DisplayName: displayName,
|
||||
Uptime: up,
|
||||
Downtime: down,
|
||||
Idle: idle,
|
||||
AvgLatency: latency,
|
||||
CurrentStatus: status,
|
||||
Statuses: statuses,
|
||||
IsDocker: r != nil && r.IsDocker(),
|
||||
IsExcluded: r == nil || r.ShouldExclude(),
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -33,7 +33,7 @@ type (
|
||||
|
||||
task *task.Task
|
||||
|
||||
pool pool.Pool[types.LoadBalancerServer]
|
||||
pool *pool.Pool[types.LoadBalancerServer]
|
||||
poolMu sync.Mutex
|
||||
|
||||
sumWeight int
|
||||
|
||||
203
internal/net/gphttp/middleware/crowdsec.go
Normal file
203
internal/net/gphttp/middleware/crowdsec.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
ioutils "github.com/yusing/goutils/io"
|
||||
)
|
||||
|
||||
type (
|
||||
crowdsecMiddleware struct {
|
||||
CrowdsecMiddlewareOpts
|
||||
}
|
||||
|
||||
CrowdsecMiddlewareOpts struct {
|
||||
Route string `json:"route" validate:"required"` // route name (alias) or IP address
|
||||
Port int `json:"port"` // port number (optional if using route name)
|
||||
APIKey string `json:"api_key" validate:"required"` // API key for CrowdSec AppSec (mandatory)
|
||||
Endpoint string `json:"endpoint"` // default: "/"
|
||||
LogBlocked bool `json:"log_blocked"` // default: false
|
||||
Timeout time.Duration `json:"timeout"` // default: 5 seconds
|
||||
|
||||
httpClient *http.Client
|
||||
}
|
||||
)
|
||||
|
||||
var Crowdsec = NewMiddleware[crowdsecMiddleware]()
|
||||
|
||||
func (m *crowdsecMiddleware) setup() {
|
||||
m.CrowdsecMiddlewareOpts = CrowdsecMiddlewareOpts{
|
||||
Route: "",
|
||||
Port: 7422, // default port for CrowdSec AppSec
|
||||
APIKey: "",
|
||||
Endpoint: "/",
|
||||
LogBlocked: false,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *crowdsecMiddleware) finalize() error {
|
||||
if !strings.HasPrefix(m.Endpoint, "/") {
|
||||
return fmt.Errorf("endpoint must start with /")
|
||||
}
|
||||
if m.Timeout == 0 {
|
||||
m.Timeout = 5 * time.Second
|
||||
}
|
||||
m.httpClient = &http.Client{
|
||||
Timeout: m.Timeout,
|
||||
// do not follow redirects
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// before implements RequestModifier.
|
||||
func (m *crowdsecMiddleware) before(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
||||
// Build CrowdSec URL
|
||||
crowdsecURL, err := m.buildCrowdSecURL()
|
||||
if err != nil {
|
||||
Crowdsec.LogError(r).Err(err).Msg("failed to build CrowdSec URL")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return false
|
||||
}
|
||||
|
||||
// Determine HTTP method: GET for requests without body, POST for requests with body
|
||||
method := http.MethodGet
|
||||
var body io.Reader
|
||||
if r.Body != nil && r.Body != http.NoBody {
|
||||
method = http.MethodPost
|
||||
// Read the body
|
||||
bodyBytes, release, err := httputils.ReadAllRequestBody(r)
|
||||
if err != nil {
|
||||
Crowdsec.LogError(r).Err(err).Msg("failed to read request body")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return false
|
||||
}
|
||||
r.Body = ioutils.NewHookReadCloser(io.NopCloser(bytes.NewReader(bodyBytes)), func() {
|
||||
release(bodyBytes)
|
||||
})
|
||||
body = bytes.NewReader(bodyBytes)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), m.Timeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, crowdsecURL, body)
|
||||
if err != nil {
|
||||
Crowdsec.LogError(r).Err(err).Msg("failed to create CrowdSec request")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return false
|
||||
}
|
||||
|
||||
// Get remote IP
|
||||
remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
remoteIP = r.RemoteAddr
|
||||
}
|
||||
|
||||
// Get HTTP version in integer form (10, 11, 20, etc.)
|
||||
httpVersion := m.getHTTPVersion(r)
|
||||
|
||||
// Copy original headers
|
||||
req.Header = r.Header.Clone()
|
||||
|
||||
// Overwrite CrowdSec required headers to prevent spoofing
|
||||
req.Header.Set("X-Crowdsec-Appsec-Ip", remoteIP)
|
||||
req.Header.Set("X-Crowdsec-Appsec-Uri", r.URL.RequestURI())
|
||||
req.Header.Set("X-Crowdsec-Appsec-Host", r.Host)
|
||||
req.Header.Set("X-Crowdsec-Appsec-Verb", r.Method)
|
||||
req.Header.Set("X-Crowdsec-Appsec-Api-Key", m.APIKey)
|
||||
req.Header.Set("X-Crowdsec-Appsec-User-Agent", r.UserAgent())
|
||||
req.Header.Set("X-Crowdsec-Appsec-Http-Version", httpVersion)
|
||||
|
||||
// Make request to CrowdSec
|
||||
resp, err := m.httpClient.Do(req)
|
||||
if err != nil {
|
||||
Crowdsec.LogError(r).Err(err).Msg("failed to connect to CrowdSec server")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Handle response codes
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
// Request is allowed
|
||||
return true
|
||||
case http.StatusForbidden:
|
||||
// Request is blocked by CrowdSec
|
||||
if m.LogBlocked {
|
||||
Crowdsec.LogWarn(r).
|
||||
Str("ip", remoteIP).
|
||||
Msg("request blocked by CrowdSec")
|
||||
}
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return false
|
||||
case http.StatusInternalServerError:
|
||||
// CrowdSec server error
|
||||
bodyBytes, release, err := httputils.ReadAllBody(resp)
|
||||
if err == nil {
|
||||
defer release(bodyBytes)
|
||||
Crowdsec.LogError(r).
|
||||
Str("crowdsec_response", string(bodyBytes)).
|
||||
Msg("CrowdSec server error")
|
||||
}
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return false
|
||||
default:
|
||||
// Unexpected response code
|
||||
Crowdsec.LogWarn(r).
|
||||
Int("status_code", resp.StatusCode).
|
||||
Msg("unexpected response from CrowdSec server")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// buildCrowdSecURL constructs the CrowdSec server URL based on route or IP configuration
|
||||
func (m *crowdsecMiddleware) buildCrowdSecURL() (string, error) {
|
||||
// Try to get route first
|
||||
if m.Route != "" {
|
||||
if route, ok := routes.HTTP.Get(m.Route); ok {
|
||||
// Using route name
|
||||
targetURL := *route.TargetURL()
|
||||
targetURL.Path = m.Endpoint
|
||||
return targetURL.String(), nil
|
||||
}
|
||||
|
||||
// If not found in routes, assume it's an IP address
|
||||
if m.Port == 0 {
|
||||
return "", fmt.Errorf("port must be specified when using IP address")
|
||||
}
|
||||
return fmt.Sprintf("http://%s%s", net.JoinHostPort(m.Route, strconv.Itoa(m.Port)), m.Endpoint), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("route or IP address must be specified")
|
||||
}
|
||||
|
||||
func (m *crowdsecMiddleware) getHTTPVersion(r *http.Request) string {
|
||||
switch {
|
||||
case r.ProtoMajor == 1 && r.ProtoMinor == 0:
|
||||
return "10"
|
||||
case r.ProtoMajor == 1 && r.ProtoMinor == 1:
|
||||
return "11"
|
||||
case r.ProtoMajor == 2:
|
||||
return "20"
|
||||
case r.ProtoMajor == 3:
|
||||
return "30"
|
||||
default:
|
||||
return strconv.Itoa(r.ProtoMajor*10 + r.ProtoMinor)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ var allMiddlewares = map[string]*Middleware{
|
||||
|
||||
"oidc": OIDC,
|
||||
"forwardauth": ForwardAuth,
|
||||
"crowdsec": Crowdsec,
|
||||
|
||||
"request": ModifyRequest,
|
||||
"modifyrequest": ModifyRequest,
|
||||
|
||||
@@ -10,10 +10,15 @@ The proxmox package implements Proxmox API client management, node discovery, an
|
||||
|
||||
- Proxmox API client management
|
||||
- Node discovery and pool management
|
||||
- LXC container operations (start, stop, status)
|
||||
- IP address retrieval for containers
|
||||
- LXC container operations (start, stop, status, stats, command execution)
|
||||
- IP address retrieval for containers (online and offline)
|
||||
- Container stats streaming (like `docker stats`)
|
||||
- Container command execution via VNC websocket
|
||||
- Journalctl streaming for LXC containers
|
||||
- Reverse resource lookup by IP, hostname, or alias
|
||||
- Reverse node lookup by hostname, IP, or alias
|
||||
- TLS configuration options
|
||||
- Token-based authentication
|
||||
- Token and username/password authentication
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -29,12 +34,14 @@ graph TD
|
||||
G --> I[Start Container]
|
||||
G --> J[Stop Container]
|
||||
G --> K[Check Status]
|
||||
G --> L[Execute Command]
|
||||
G --> M[Stream Stats]
|
||||
|
||||
subgraph Node Pool
|
||||
F --> L[Nodes Map]
|
||||
L --> M[Node 1]
|
||||
L --> N[Node 2]
|
||||
L --> O[Node 3]
|
||||
F --> N[Nodes Map]
|
||||
N --> O[Node 1]
|
||||
N --> P[Node 2]
|
||||
N --> Q[Node 3]
|
||||
end
|
||||
```
|
||||
|
||||
@@ -45,8 +52,11 @@ graph TD
|
||||
```go
|
||||
type Config struct {
|
||||
URL string `json:"url" validate:"required,url"`
|
||||
TokenID string `json:"token_id" validate:"required"`
|
||||
Secret strutils.Redacted `json:"secret" validate:"required"`
|
||||
Username string `json:"username" validate:"required_without=TokenID Secret"`
|
||||
Password strutils.Redacted `json:"password" validate:"required_without=TokenID Secret"`
|
||||
Realm string `json:"realm" validate:"required_without=TokenID Secret"`
|
||||
TokenID string `json:"token_id" validate:"required_without=Username Password"`
|
||||
Secret strutils.Redacted `json:"secret" validate:"required_without=Username Password"`
|
||||
NoTLSVerify bool `json:"no_tls_verify"`
|
||||
|
||||
client *Client
|
||||
@@ -58,8 +68,16 @@ type Config struct {
|
||||
```go
|
||||
type Client struct {
|
||||
*proxmox.Client
|
||||
proxmox.Cluster
|
||||
*proxmox.Cluster
|
||||
Version *proxmox.Version
|
||||
// id -> resource; id: lxc/<vmid> or qemu/<vmid>
|
||||
resources map[string]*VMResource
|
||||
resourcesMu sync.RWMutex
|
||||
}
|
||||
|
||||
type VMResource struct {
|
||||
*proxmox.ClusterResource
|
||||
IPs []net.IP
|
||||
}
|
||||
```
|
||||
|
||||
@@ -69,12 +87,23 @@ type Client struct {
|
||||
type Node struct {
|
||||
name string
|
||||
id string
|
||||
client *proxmox.Client
|
||||
client *Client
|
||||
}
|
||||
|
||||
var Nodes = pool.New[*Node]("proxmox_nodes")
|
||||
```
|
||||
|
||||
### NodeConfig
|
||||
|
||||
```go
|
||||
type NodeConfig struct {
|
||||
Node string `json:"node" validate:"required"`
|
||||
VMID int `json:"vmid" validate:"required"`
|
||||
VMName string `json:"vmname,omitempty"`
|
||||
Service string `json:"service,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
## Public API
|
||||
|
||||
### Configuration
|
||||
@@ -87,11 +116,45 @@ func (c *Config) Init(ctx context.Context) gperr.Error
|
||||
func (c *Config) Client() *Client
|
||||
```
|
||||
|
||||
### Client Operations
|
||||
|
||||
```go
|
||||
// UpdateClusterInfo fetches cluster info and discovers nodes.
|
||||
func (c *Client) UpdateClusterInfo(ctx context.Context) error
|
||||
|
||||
// UpdateResources fetches VM resources and their IP addresses.
|
||||
func (c *Client) UpdateResources(ctx context.Context) error
|
||||
|
||||
// GetResource gets a resource by kind and id.
|
||||
func (c *Client) GetResource(kind string, id int) (*VMResource, error)
|
||||
|
||||
// ReverseLookupResource looks up a resource by IP, hostname, or alias.
|
||||
func (c *Client) ReverseLookupResource(ip net.IP, hostname string, alias string) (*VMResource, error)
|
||||
|
||||
// ReverseLookupNode looks up a node by hostname, IP, or alias.
|
||||
func (c *Client) ReverseLookupNode(hostname string, ip net.IP, alias string) string
|
||||
|
||||
// NumNodes returns the number of nodes in the cluster.
|
||||
func (c *Client) NumNodes() int
|
||||
```
|
||||
|
||||
### Node Operations
|
||||
|
||||
```go
|
||||
// AvailableNodeNames returns all available node names.
|
||||
// AvailableNodeNames returns all available node names as a comma-separated string.
|
||||
func AvailableNodeNames() string
|
||||
|
||||
// Node.Client returns the Proxmox client.
|
||||
func (n *Node) Client() *Client
|
||||
|
||||
// Node.Get performs a GET request on the node.
|
||||
func (n *Node) Get(ctx context.Context, path string, v any) error
|
||||
|
||||
// NodeCommand executes a command on the node and streams output.
|
||||
func (n *Node) NodeCommand(ctx context.Context, command string) (io.ReadCloser, error)
|
||||
|
||||
// NodeJournalctl streams journalctl output from the node.
|
||||
func (n *Node) NodeJournalctl(ctx context.Context, service string, limit int) (io.ReadCloser, error)
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -136,57 +199,83 @@ fmt.Printf("Available nodes: %s\n", names)
|
||||
|
||||
## LXC Operations
|
||||
|
||||
### Container Status
|
||||
|
||||
```go
|
||||
type LXCStatus string
|
||||
|
||||
const (
|
||||
LXCStatusRunning LXCStatus = "running"
|
||||
LXCStatusStopped LXCStatus = "stopped"
|
||||
LXCStatusSuspended LXCStatus = "suspended"
|
||||
)
|
||||
|
||||
// LXCStatus returns the current status of a container.
|
||||
func (node *Node) LXCStatus(ctx context.Context, vmid int) (LXCStatus, error)
|
||||
|
||||
// LXCIsRunning checks if a container is running.
|
||||
func (node *Node) LXCIsRunning(ctx context.Context, vmid int) (bool, error)
|
||||
|
||||
// LXCIsStopped checks if a container is stopped.
|
||||
func (node *Node) LXCIsStopped(ctx context.Context, vmid int) (bool, error)
|
||||
|
||||
// LXCName returns the name of a container.
|
||||
func (node *Node) LXCName(ctx context.Context, vmid int) (string, error)
|
||||
```
|
||||
|
||||
### Container Actions
|
||||
|
||||
```go
|
||||
type LXCAction string
|
||||
|
||||
const (
|
||||
LXCStart LXCAction = "start"
|
||||
LXCShutdown LXCAction = "shutdown"
|
||||
LXCSuspend LXCAction = "suspend"
|
||||
LXCResume LXCAction = "resume"
|
||||
LXCReboot LXCAction = "reboot"
|
||||
)
|
||||
|
||||
// LXCAction performs an action on a container with task tracking.
|
||||
func (node *Node) LXCAction(ctx context.Context, vmid int, action LXCAction) error
|
||||
|
||||
// LXCSetShutdownTimeout sets the shutdown timeout for a container.
|
||||
func (node *Node) LXCSetShutdownTimeout(ctx context.Context, vmid int, timeout time.Duration) error
|
||||
```
|
||||
|
||||
### Get Container IPs
|
||||
|
||||
```go
|
||||
func getContainerIPs(ctx context.Context, node *proxmox.Node, vmid int) ([]net.IP, error) {
|
||||
var ips []net.IP
|
||||
// LXCGetIPs returns IP addresses of a container.
|
||||
// First tries interfaces (online), then falls back to config (offline).
|
||||
func (node *Node) LXCGetIPs(ctx context.Context, vmid int) ([]net.IP, error)
|
||||
|
||||
err := node.Get(ctx, "/lxc/"+strconv.Itoa(vmid)+"/config", &config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// LXCGetIPsFromInterfaces returns IP addresses from network interfaces.
|
||||
// Returns empty if container is stopped.
|
||||
func (node *Node) LXCGetIPsFromInterfaces(ctx context.Context, vmid int) ([]net.IP, error)
|
||||
|
||||
// Parse IP addresses from config
|
||||
for _, ip := range config {
|
||||
if ipNet := net.ParseCIDR(ip); ipNet != nil {
|
||||
ips = append(ips, ipNet.IP)
|
||||
}
|
||||
}
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
// LXCGetIPsFromConfig returns IP addresses from container config.
|
||||
// Works for stopped/offline containers.
|
||||
func (node *Node) LXCGetIPsFromConfig(ctx context.Context, vmid int) ([]net.IP, error)
|
||||
```
|
||||
|
||||
### Check Container Status
|
||||
### Container Stats (like `docker stats`)
|
||||
|
||||
```go
|
||||
func (node *Node) LXCIsRunning(ctx context.Context, vmid int) (bool, error) {
|
||||
var status struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
err := node.Get(ctx, "/lxc/"+strconv.Itoa(vmid)+"/status/current", &status)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return status.Status == "running", nil
|
||||
}
|
||||
// LXCStats streams container statistics.
|
||||
// Format: "STATUS|CPU%%|MEM USAGE/LIMIT|MEM%%|NET I/O|BLOCK I/O"
|
||||
// Example: "running|31.1%|9.6GiB/20GiB|48.87%|4.7GiB/3.3GiB|25GiB/36GiB"
|
||||
func (node *Node) LXCStats(ctx context.Context, vmid int, stream bool) (io.ReadCloser, error)
|
||||
```
|
||||
|
||||
### Start Container
|
||||
### Container Command Execution
|
||||
|
||||
```go
|
||||
func (node *Node) LXCAction(ctx context.Context, vmid int, action string) error {
|
||||
return node.Post(ctx,
|
||||
"/lxc/"+strconv.Itoa(vmid)+"/status/"+action,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
// LXCCommand executes a command inside a container and streams output.
|
||||
func (node *Node) LXCCommand(ctx context.Context, vmid int, command string) (io.ReadCloser, error)
|
||||
|
||||
const LXCStart = "start"
|
||||
// LXCJournalctl streams journalctl output for a container service.
|
||||
func (node *Node) LXCJournalctl(ctx context.Context, vmid int, service string, limit int) (io.ReadCloser, error)
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
@@ -218,6 +307,13 @@ sequenceDiagram
|
||||
Node->>ProxmoxAPI: POST /lxc/{vmid}/status/start
|
||||
ProxmoxAPI-->>Node: Success
|
||||
Node-->>User: Done
|
||||
|
||||
User->>Node: LXCCommand(vmid, "df -h")
|
||||
Node->>ProxmoxAPI: WebSocket /nodes/{node}/termproxy
|
||||
ProxmoxAPI-->>Node: WebSocket connection
|
||||
Node->>ProxmoxAPI: Send: "pct exec {vmid} -- df -h"
|
||||
ProxmoxAPI-->>Node: Command output stream
|
||||
Node-->>User: Stream output
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -228,11 +324,38 @@ sequenceDiagram
|
||||
providers:
|
||||
proxmox:
|
||||
- url: https://proxmox.example.com:8006
|
||||
# Token-based authentication (optional)
|
||||
token_id: user@pam!token-name
|
||||
secret: your-api-token-secret
|
||||
|
||||
# Username/Password authentication (required for journalctl (service logs) streaming)
|
||||
# username: root
|
||||
# password: your-password
|
||||
# realm: pam
|
||||
|
||||
no_tls_verify: false
|
||||
```
|
||||
|
||||
### Authentication Options
|
||||
|
||||
```go
|
||||
// Token-based authentication (recommended)
|
||||
opts := []proxmox.Option{
|
||||
proxmox.WithAPIToken(c.TokenID, c.Secret.String()),
|
||||
proxmox.WithHTTPClient(&http.Client{Transport: tr}),
|
||||
}
|
||||
|
||||
// Username/Password authentication
|
||||
opts := []proxmox.Option{
|
||||
proxmox.WithCredentials(&proxmox.Credentials{
|
||||
Username: c.Username,
|
||||
Password: c.Password.String(),
|
||||
Realm: c.Realm,
|
||||
}),
|
||||
proxmox.WithHTTPClient(&http.Client{Transport: tr}),
|
||||
}
|
||||
```
|
||||
|
||||
### TLS Configuration
|
||||
|
||||
```go
|
||||
@@ -291,16 +414,16 @@ if r.Idlewatcher != nil && r.Idlewatcher.Proxmox != nil {
|
||||
|
||||
## Authentication
|
||||
|
||||
The package uses API tokens for authentication:
|
||||
The package supports two authentication methods:
|
||||
|
||||
```go
|
||||
opts := []proxmox.Option{
|
||||
proxmox.WithAPIToken(c.TokenID, c.Secret.String()),
|
||||
proxmox.WithHTTPClient(&http.Client{
|
||||
Transport: tr,
|
||||
}),
|
||||
}
|
||||
```
|
||||
1. **API Token** (recommended): Uses `token_id` and `secret`
|
||||
2. **Username/Password**: Uses `username`, `password`, and `realm`
|
||||
|
||||
Username/password authentication is required for:
|
||||
|
||||
- WebSocket connections (command execution, journalctl streaming)
|
||||
|
||||
Both methods support TLS verification options.
|
||||
|
||||
## Error Handling
|
||||
|
||||
@@ -312,11 +435,38 @@ if errors.Is(err, context.DeadlineExceeded) {
|
||||
|
||||
// Connection errors
|
||||
return gperr.New("failed to fetch proxmox cluster info").With(err)
|
||||
|
||||
// Resource not found
|
||||
return gperr.New("resource not found").With(ErrResourceNotFound)
|
||||
|
||||
// No session (for WebSocket operations)
|
||||
return gperr.New("no session").With(ErrNoSession)
|
||||
```
|
||||
|
||||
## Errors
|
||||
|
||||
```go
|
||||
var (
|
||||
ErrResourceNotFound = errors.New("resource not found")
|
||||
ErrNoResources = errors.New("no resources")
|
||||
ErrNoSession = fmt.Errorf("no session found, make sure username and password are set")
|
||||
)
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Cluster info fetched once on init
|
||||
- Nodes cached in pool
|
||||
- Per-operation API calls
|
||||
- 3-second timeout for initial connection
|
||||
- Resources updated in background loop (every 3 seconds by default)
|
||||
- Concurrent IP resolution for all containers (limited to GOMAXPROCS \* 2)
|
||||
- 5-second timeout for initial connection
|
||||
- Per-operation API calls with 3-second timeout
|
||||
- WebSocket connections properly closed to prevent goroutine leaks
|
||||
|
||||
## Constants
|
||||
|
||||
```go
|
||||
const ResourcePollInterval = 3 * time.Second
|
||||
```
|
||||
|
||||
The `ResourcePollInterval` constant controls how often resources are updated in the background loop.
|
||||
|
||||
@@ -2,20 +2,45 @@ package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/luthermonson/go-proxmox"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
*proxmox.Client
|
||||
proxmox.Cluster
|
||||
*proxmox.Cluster
|
||||
Version *proxmox.Version
|
||||
// id -> resource; id: lxc/<vmid> or qemu/<vmid>
|
||||
resources map[string]*VMResource
|
||||
resourcesMu sync.RWMutex
|
||||
}
|
||||
|
||||
type VMResource struct {
|
||||
*proxmox.ClusterResource
|
||||
IPs []net.IP
|
||||
}
|
||||
|
||||
var (
|
||||
ErrResourceNotFound = errors.New("resource not found")
|
||||
ErrNoResources = errors.New("no resources")
|
||||
)
|
||||
|
||||
func NewClient(baseUrl string, opts ...proxmox.Option) *Client {
|
||||
return &Client{Client: proxmox.NewClient(baseUrl, opts...)}
|
||||
return &Client{
|
||||
Client: proxmox.NewClient(baseUrl, opts...),
|
||||
resources: make(map[string]*VMResource),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) UpdateClusterInfo(ctx context.Context) (err error) {
|
||||
@@ -24,15 +49,139 @@ func (c *Client) UpdateClusterInfo(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
// requires (/, Sys.Audit)
|
||||
if err := c.Get(ctx, "/cluster/status", &c.Cluster); err != nil {
|
||||
cluster, err := c.Client.Cluster(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Cluster = cluster
|
||||
|
||||
for _, node := range c.Cluster.Nodes {
|
||||
Nodes.Add(&Node{name: node.Name, id: node.ID, client: c.Client})
|
||||
Nodes.Add(NewNode(c, node.Name, node.ID))
|
||||
}
|
||||
if cluster.Name == "" && len(c.Cluster.Nodes) == 1 {
|
||||
cluster.Name = c.Cluster.Nodes[0].Name
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) UpdateResources(ctx context.Context) error {
|
||||
resourcesSlice, err := c.Cluster.Resources(ctx, "vm")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vmResources := make([]*VMResource, len(resourcesSlice))
|
||||
for i, resource := range resourcesSlice {
|
||||
vmResources[i] = &VMResource{
|
||||
ClusterResource: resource,
|
||||
IPs: nil,
|
||||
}
|
||||
}
|
||||
var errs errgroup.Group
|
||||
errs.SetLimit(runtime.GOMAXPROCS(0) * 2)
|
||||
for i, resource := range resourcesSlice {
|
||||
vmResource := vmResources[i]
|
||||
errs.Go(func() error {
|
||||
node, ok := Nodes.Get(resource.Node)
|
||||
if !ok {
|
||||
return fmt.Errorf("node %s not found", resource.Node)
|
||||
}
|
||||
vmid, ok := strings.CutPrefix(resource.ID, "lxc/")
|
||||
if !ok {
|
||||
return nil // not a lxc resource
|
||||
}
|
||||
vmidInt, err := strconv.Atoi(vmid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid resource id %s: %w", resource.ID, err)
|
||||
}
|
||||
ips, err := node.LXCGetIPs(ctx, vmidInt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get ips for resource %s: %w", resource.ID, err)
|
||||
}
|
||||
vmResource.IPs = ips
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := errs.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.resourcesMu.Lock()
|
||||
clear(c.resources)
|
||||
for i, resource := range resourcesSlice {
|
||||
c.resources[resource.ID] = vmResources[i]
|
||||
}
|
||||
c.resourcesMu.Unlock()
|
||||
log.Debug().Str("cluster", c.Cluster.Name).Msgf("[proxmox] updated %d resources", len(c.resources))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetResource gets a resource by kind and id.
|
||||
// kind: lxc or qemu
|
||||
// id: <vmid>
|
||||
func (c *Client) GetResource(kind string, id int) (*VMResource, error) {
|
||||
c.resourcesMu.RLock()
|
||||
defer c.resourcesMu.RUnlock()
|
||||
resource, ok := c.resources[kind+"/"+strconv.Itoa(id)]
|
||||
if !ok {
|
||||
return nil, ErrResourceNotFound
|
||||
}
|
||||
return resource, nil
|
||||
}
|
||||
|
||||
// ReverseLookupResource looks up a resource by ip address, hostname, alias or all of them
|
||||
func (c *Client) ReverseLookupResource(ip net.IP, hostname string, alias string) (*VMResource, error) {
|
||||
c.resourcesMu.RLock()
|
||||
defer c.resourcesMu.RUnlock()
|
||||
|
||||
shouldCheckIP := ip != nil && !ip.IsLoopback() && !ip.IsUnspecified()
|
||||
shouldCheckHostname := hostname != ""
|
||||
shouldCheckAlias := alias != ""
|
||||
|
||||
if shouldCheckHostname {
|
||||
hostname, _, _ = strings.Cut(hostname, ".")
|
||||
}
|
||||
|
||||
for _, resource := range c.resources {
|
||||
if shouldCheckIP && slices.ContainsFunc(resource.IPs, func(a net.IP) bool { return a.Equal(ip) }) {
|
||||
return resource, nil
|
||||
}
|
||||
if shouldCheckHostname && resource.Name == hostname {
|
||||
return resource, nil
|
||||
}
|
||||
if shouldCheckAlias && resource.Name == alias {
|
||||
return resource, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrResourceNotFound
|
||||
}
|
||||
|
||||
// ReverseLookupNode looks up a node by name or IP address.
|
||||
// Returns the node name if found.
|
||||
func (c *Client) ReverseLookupNode(hostname string, ip net.IP, alias string) string {
|
||||
shouldCheckHostname := hostname != ""
|
||||
shouldCheckIP := ip != nil && !ip.IsLoopback() && !ip.IsUnspecified()
|
||||
shouldCheckAlias := alias != ""
|
||||
|
||||
if shouldCheckHostname {
|
||||
hostname, _, _ = strings.Cut(hostname, ".")
|
||||
}
|
||||
|
||||
for _, node := range c.Cluster.Nodes {
|
||||
if shouldCheckHostname && node.Name == hostname {
|
||||
return node.Name
|
||||
}
|
||||
if shouldCheckIP {
|
||||
nodeIP := net.ParseIP(node.IP)
|
||||
if nodeIP != nil && nodeIP.Equal(ip) {
|
||||
return node.Name
|
||||
}
|
||||
}
|
||||
if shouldCheckAlias && node.Name == alias {
|
||||
return node.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Key implements pool.Object
|
||||
func (c *Client) Key() string {
|
||||
return c.Cluster.ID
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/luthermonson/go-proxmox"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
@@ -17,14 +18,23 @@ import (
|
||||
type Config struct {
|
||||
URL string `json:"url" validate:"required,url"`
|
||||
|
||||
TokenID string `json:"token_id" validate:"required"`
|
||||
Secret strutils.Redacted `json:"secret" validate:"required"`
|
||||
Username string `json:"username" validate:"required_without=TokenID Secret"`
|
||||
Password strutils.Redacted `json:"password" validate:"required_without=TokenID Secret"`
|
||||
Realm string `json:"realm" validate:"required_without=TokenID Secret"`
|
||||
|
||||
TokenID string `json:"token_id" validate:"required_without=Username Password"`
|
||||
Secret strutils.Redacted `json:"secret" validate:"required_without=Username Password"`
|
||||
|
||||
NoTLSVerify bool `json:"no_tls_verify" yaml:"no_tls_verify,omitempty"`
|
||||
|
||||
client *Client
|
||||
}
|
||||
|
||||
const ResourcePollInterval = 3 * time.Second
|
||||
|
||||
// NodeStatsPollInterval controls how often node stats are streamed when streaming is enabled.
|
||||
const NodeStatsPollInterval = time.Second
|
||||
|
||||
func (c *Config) Client() *Client {
|
||||
if c.client == nil {
|
||||
panic("proxmox client accessed before init")
|
||||
@@ -49,21 +59,71 @@ func (c *Config) Init(ctx context.Context) gperr.Error {
|
||||
}
|
||||
|
||||
opts := []proxmox.Option{
|
||||
proxmox.WithAPIToken(c.TokenID, c.Secret.String()),
|
||||
proxmox.WithHTTPClient(&http.Client{
|
||||
Transport: tr,
|
||||
}),
|
||||
}
|
||||
useCredentials := false
|
||||
if c.Username != "" && c.Password != "" {
|
||||
opts = append(opts, proxmox.WithCredentials(&proxmox.Credentials{
|
||||
Username: c.Username,
|
||||
Password: c.Password.String(),
|
||||
Realm: c.Realm,
|
||||
}))
|
||||
useCredentials = true
|
||||
} else {
|
||||
opts = append(opts, proxmox.WithAPIToken(c.TokenID, c.Secret.String()))
|
||||
}
|
||||
c.client = NewClient(c.URL, opts...)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
initCtx, initCtxCancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer initCtxCancel()
|
||||
|
||||
if err := c.client.UpdateClusterInfo(ctx); err != nil {
|
||||
if useCredentials {
|
||||
err := c.client.CreateSession(initCtx)
|
||||
if err != nil {
|
||||
return gperr.New("failed to create session").With(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.client.UpdateClusterInfo(initCtx); err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return gperr.New("timeout fetching proxmox cluster info")
|
||||
}
|
||||
return gperr.New("failed to fetch proxmox cluster info").With(err)
|
||||
}
|
||||
|
||||
go c.updateResourcesLoop(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) updateResourcesLoop(ctx context.Context) {
|
||||
ticker := time.NewTicker(ResourcePollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
log.Trace().Str("cluster", c.client.Cluster.Name).Msg("[proxmox] starting resources update loop")
|
||||
|
||||
{
|
||||
reqCtx, reqCtxCancel := context.WithTimeout(ctx, ResourcePollInterval)
|
||||
err := c.client.UpdateResources(reqCtx)
|
||||
reqCtxCancel()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("cluster", c.client.Cluster.Name).Msg("[proxmox] failed to update resources")
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Trace().Str("cluster", c.client.Cluster.Name).Msg("[proxmox] stopping resources update loop")
|
||||
return
|
||||
case <-ticker.C:
|
||||
reqCtx, reqCtxCancel := context.WithTimeout(ctx, ResourcePollInterval)
|
||||
err := c.client.UpdateResources(reqCtx)
|
||||
reqCtxCancel()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("cluster", c.client.Cluster.Name).Msg("[proxmox] failed to update resources")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func (n *Node) LXCAction(ctx context.Context, vmid int, action LXCAction) error
|
||||
return err
|
||||
}
|
||||
|
||||
task := proxmox.NewTask(upid, n.client)
|
||||
task := proxmox.NewTask(upid, n.client.Client)
|
||||
checkTicker := time.NewTicker(proxmoxTaskCheckInterval)
|
||||
defer checkTicker.Stop()
|
||||
for {
|
||||
@@ -170,17 +170,17 @@ func getIPFromNet(s string) (res []net.IP) { // name:...,bridge:...,gw=..,ip=...
|
||||
}
|
||||
|
||||
// LXCGetIPs returns the ip addresses of the container
|
||||
// it first tries to get the ip addresses from the config
|
||||
// if that fails, it gets the ip addresses from the interfaces
|
||||
// it first tries to get the ip addresses from the interfaces
|
||||
// if that fails, it gets the ip addresses from the config (offline containers)
|
||||
func (n *Node) LXCGetIPs(ctx context.Context, vmid int) (res []net.IP, err error) {
|
||||
ips, err := n.LXCGetIPsFromConfig(ctx, vmid)
|
||||
ips, err := n.LXCGetIPsFromInterfaces(ctx, vmid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ips) > 0 {
|
||||
return ips, nil
|
||||
}
|
||||
ips, err = n.LXCGetIPsFromInterfaces(ctx, vmid)
|
||||
ips, err = n.LXCGetIPsFromConfig(ctx, vmid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
53
internal/proxmox/lxc_command.go
Normal file
53
internal/proxmox/lxc_command.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/luthermonson/go-proxmox"
|
||||
)
|
||||
|
||||
var ErrNoSession = fmt.Errorf("no session found, make sure username and password are set")
|
||||
|
||||
// closeTransportConnections forces close idle HTTP connections to prevent goroutine leaks.
|
||||
// This is needed because the go-proxmox library's TermWebSocket closer doesn't close
|
||||
// the underlying HTTP/2 connections, leaving goroutines stuck in writeLoop/readLoop.
|
||||
func closeTransportConnections(httpClient *http.Client) {
|
||||
if tr, ok := httpClient.Transport.(*http.Transport); ok {
|
||||
tr.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
// LXCCommand connects to the Proxmox VNC websocket and streams command output.
|
||||
// It returns an io.ReadCloser that streams the command output.
|
||||
func (n *Node) LXCCommand(ctx context.Context, vmid int, command string) (io.ReadCloser, error) {
|
||||
node := proxmox.NewNode(n.client.Client, n.name)
|
||||
lxc, err := node.Container(ctx, vmid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get container: %w", err)
|
||||
}
|
||||
|
||||
if lxc.Status != "running" {
|
||||
return io.NopCloser(bytes.NewReader(fmt.Appendf(nil, "container %d is not running, status: %s\n", vmid, lxc.Status))), nil
|
||||
}
|
||||
|
||||
return n.NodeCommand(ctx, fmt.Sprintf("pct exec %d -- %s", vmid, command))
|
||||
}
|
||||
|
||||
// LXCJournalctl streams journalctl output for the given service.
|
||||
//
|
||||
// If service is not empty, it will be used to filter the output by service.
|
||||
// If limit is greater than 0, it will be used to limit the number of lines of output.
|
||||
func (n *Node) LXCJournalctl(ctx context.Context, vmid int, service string, limit int) (io.ReadCloser, error) {
|
||||
command := "journalctl -f"
|
||||
if service != "" {
|
||||
command = fmt.Sprintf("journalctl -u %q -f", service)
|
||||
}
|
||||
if limit > 0 {
|
||||
command = fmt.Sprintf("%s -n %d", command, limit)
|
||||
}
|
||||
return n.LXCCommand(ctx, vmid, command)
|
||||
}
|
||||
171
internal/proxmox/lxc_stats.go
Normal file
171
internal/proxmox/lxc_stats.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// const statsScriptLocation = "/tmp/godoxy-stats.sh"
|
||||
|
||||
// const statsScript = `#!/bin/sh
|
||||
|
||||
// # LXCStats script, written by godoxy.
|
||||
// printf "%s|%s|%s|%s|%s\n" \
|
||||
// "$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1"%"}')" \
|
||||
// "$(free -b | awk 'NR==2{printf "%.0f\n%.0f", $3, $2}' | numfmt --to=iec-i --suffix=B | paste -sd/)" \
|
||||
// "$(free | awk 'NR==2{printf "%.2f%%", $3/$2*100}')" \
|
||||
// "$(awk 'NR>2{r+=$2;t+=$10}END{printf "%.0f\n%.0f", r, t}' /proc/net/dev | numfmt --to=iec-i --suffix=B | paste -sd/)" \
|
||||
// "$(awk '{r+=$6;w+=$10}END{printf "%.0f\n%.0f", r*512, w*512}' /proc/diskstats | numfmt --to=iec-i --suffix=B | paste -sd/)"`
|
||||
|
||||
// var statsScriptBase64 = base64.StdEncoding.EncodeToString([]byte(statsScript))
|
||||
|
||||
// var statsInitCommand = fmt.Sprintf("sh -c 'echo %s | base64 -d > %s && chmod +x %s'", statsScriptBase64, statsScriptLocation, statsScriptLocation)
|
||||
|
||||
// var statsStreamScript = fmt.Sprintf("watch -t -w -p -n1 '%s'", statsScriptLocation)
|
||||
// var statsNonStreamScript = statsScriptLocation
|
||||
|
||||
// lxcStatsScriptInit initializes the stats script for the given container.
|
||||
// func (n *Node) lxcStatsScriptInit(ctx context.Context, vmid int) error {
|
||||
// reader, err := n.LXCCommand(ctx, vmid, statsInitCommand)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("failed to execute stats init command: %w", err)
|
||||
// }
|
||||
// reader.Close()
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// LXCStats streams container stats, like docker stats.
|
||||
//
|
||||
// - format: "STATUS|CPU%%|MEM USAGE/LIMIT|MEM%%|NET I/O|BLOCK I/O"
|
||||
// - example: running|31.1%|9.6GiB/20GiB|48.87%|4.7GiB/3.3GiB|25GiB/36GiB
|
||||
func (n *Node) LXCStats(ctx context.Context, vmid int, stream bool) (io.ReadCloser, error) {
|
||||
if !stream {
|
||||
resource, err := n.client.GetResource("lxc", vmid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := writeLXCStatsLine(resource, &buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return io.NopCloser(&buf), nil
|
||||
}
|
||||
|
||||
// Validate the resource exists before returning a stream.
|
||||
_, err := n.client.GetResource("lxc", vmid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
|
||||
interval := ResourcePollInterval
|
||||
if interval <= 0 {
|
||||
interval = time.Second
|
||||
}
|
||||
|
||||
go func() {
|
||||
writeSample := func() error {
|
||||
resource, err := n.client.GetResource("lxc", vmid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = writeLXCStatsLine(resource, pw)
|
||||
return err
|
||||
}
|
||||
|
||||
// Match `watch` behavior: write immediately, then on each tick.
|
||||
if err := writeSample(); err != nil {
|
||||
_ = pw.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = pw.CloseWithError(ctx.Err())
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := writeSample(); err != nil {
|
||||
_ = pw.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
func writeLXCStatsLine(resource *VMResource, w io.Writer) error {
|
||||
cpu := fmt.Sprintf("%.1f%%", resource.CPU*100)
|
||||
|
||||
memUsage := formatIECBytes(resource.Mem)
|
||||
memLimit := formatIECBytes(resource.MaxMem)
|
||||
memPct := "0.00%"
|
||||
if resource.MaxMem > 0 {
|
||||
memPct = fmt.Sprintf("%.2f%%", float64(resource.Mem)/float64(resource.MaxMem)*100)
|
||||
}
|
||||
|
||||
netIO := formatIECBytes(resource.NetIn) + "/" + formatIECBytes(resource.NetOut)
|
||||
blockIO := formatIECBytes(resource.DiskRead) + "/" + formatIECBytes(resource.DiskWrite)
|
||||
|
||||
// Keep the format consistent with LXCStatsAlt / `statsScript` (newline terminated).
|
||||
_, err := fmt.Fprintf(w, "%s|%s|%s/%s|%s|%s|%s\n", resource.Status, cpu, memUsage, memLimit, memPct, netIO, blockIO)
|
||||
return err
|
||||
}
|
||||
|
||||
// formatIECBytes formats a byte count using IEC binary prefixes (KiB, MiB, GiB, ...),
|
||||
// similar to `numfmt --to=iec-i --suffix=B`.
|
||||
func formatIECBytes(b uint64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%dB", b)
|
||||
}
|
||||
|
||||
prefixes := []string{"B", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei"}
|
||||
val := float64(b)
|
||||
exp := 0
|
||||
for val >= unit && exp < len(prefixes)-1 {
|
||||
val /= unit
|
||||
exp++
|
||||
}
|
||||
|
||||
// One decimal, trimming trailing ".0" to keep output compact (e.g. "10GiB").
|
||||
s := fmt.Sprintf("%.1f", val)
|
||||
s = strings.TrimSuffix(s, ".0")
|
||||
if exp == 0 {
|
||||
return s + "B"
|
||||
}
|
||||
return s + prefixes[exp] + "B"
|
||||
}
|
||||
|
||||
// LXCStatsAlt streams container stats, like docker stats.
|
||||
//
|
||||
// - format: "CPU%%|MEM USAGE/LIMIT|MEM%%|NET I/O|BLOCK I/O"
|
||||
// - example: 31.1%|9.6GiB/20GiB|48.87%|4.7GiB/3.3GiB|25TiB/36TiB
|
||||
// func (n *Node) LXCStatsAlt(ctx context.Context, vmid int, stream bool) (io.ReadCloser, error) {
|
||||
// // Initialize the stats script if it hasn't been initialized yet.
|
||||
// initScriptErr, _ := n.statsScriptInitErrs.LoadOrCompute(vmid,
|
||||
// func() (newValue error, cancel bool) {
|
||||
// if err := n.lxcStatsScriptInit(ctx, vmid); err != nil {
|
||||
// cancel = errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
|
||||
// return err, cancel
|
||||
// }
|
||||
// return nil, false
|
||||
// })
|
||||
|
||||
// if initScriptErr != nil {
|
||||
// return nil, initScriptErr
|
||||
// }
|
||||
// if stream {
|
||||
// return n.LXCCommand(ctx, vmid, statsStreamScript)
|
||||
// }
|
||||
// return n.LXCCommand(ctx, vmid, statsNonStreamScript)
|
||||
// }
|
||||
@@ -6,18 +6,35 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/luthermonson/go-proxmox"
|
||||
"github.com/yusing/goutils/pool"
|
||||
)
|
||||
|
||||
type NodeConfig struct {
|
||||
Node string `json:"node" validate:"required"`
|
||||
VMID int `json:"vmid" validate:"required"`
|
||||
VMName string `json:"vmname,omitempty"`
|
||||
Service string `json:"service,omitempty"`
|
||||
} // @name ProxmoxNodeConfig
|
||||
|
||||
type Node struct {
|
||||
name string
|
||||
id string // likely node/<name>
|
||||
client *proxmox.Client
|
||||
client *Client
|
||||
|
||||
// statsScriptInitErrs *xsync.Map[int, error]
|
||||
}
|
||||
|
||||
var Nodes = pool.New[*Node]("proxmox_nodes")
|
||||
|
||||
func NewNode(client *Client, name, id string) *Node {
|
||||
return &Node{
|
||||
name: name,
|
||||
id: id,
|
||||
client: client,
|
||||
// statsScriptInitErrs: xsync.NewMap[int, error](xsync.WithGrowOnly()),
|
||||
}
|
||||
}
|
||||
|
||||
func AvailableNodeNames() string {
|
||||
if Nodes.Size() == 0 {
|
||||
return ""
|
||||
@@ -38,6 +55,10 @@ func (n *Node) Name() string {
|
||||
return n.name
|
||||
}
|
||||
|
||||
func (n *Node) Client() *Client {
|
||||
return n.client
|
||||
}
|
||||
|
||||
func (n *Node) String() string {
|
||||
return fmt.Sprintf("%s (%s)", n.name, n.id)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user