mirror of
https://github.com/yusing/godoxy.git
synced 2026-02-25 10:15:00 +01:00
Compare commits
98 Commits
feat/rules
...
compat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54450924dc | ||
|
|
e4e6f6b3e8 | ||
|
|
15b9635ee1 | ||
|
|
33d512f550 | ||
|
|
c7a70f630a | ||
|
|
12a8b9a1b4 | ||
|
|
4c62a4b365 | ||
|
|
5c30f4a859 | ||
|
|
c6e4e83fcd | ||
|
|
c54741aab6 | ||
|
|
fb6b692f55 | ||
|
|
1319f72805 | ||
|
|
52511f1618 | ||
|
|
ca2cbd3332 | ||
|
|
57f7b72923 | ||
|
|
54b3008cce | ||
|
|
d9abd65471 | ||
|
|
d5b9d2e6bc | ||
|
|
ec9cab05c7 | ||
|
|
6ff1346675 | ||
|
|
a75441aa8a | ||
|
|
32971b45c3 | ||
|
|
4bc7ce09c7 | ||
|
|
b5946b34b8 | ||
|
|
442e4a0972 | ||
|
|
d1c79acf87 | ||
|
|
df70f7f63c | ||
|
|
6707143aaf | ||
|
|
742a86d079 | ||
|
|
45f856b8d1 | ||
|
|
044c7bf8a0 | ||
|
|
e7b19c472b | ||
|
|
1ace5e641d | ||
|
|
83976646db | ||
|
|
8e1d75abd6 | ||
|
|
5c341d4745 | ||
|
|
9f245a62f2 | ||
|
|
ecc2547eb1 | ||
|
|
e2a48fcff9 | ||
|
|
6b09f54793 | ||
|
|
7b5ad3ab5e | ||
|
|
48790b4756 | ||
|
|
727a3e9452 | ||
|
|
f8f1b54aaf | ||
|
|
cb8f405e76 | ||
|
|
51704829c6 | ||
|
|
a1cc1d844d | ||
|
|
633deb85ca | ||
|
|
ee1c375fd9 | ||
|
|
2713f5282e | ||
|
|
f70cca0d00 | ||
|
|
3adefe4202 | ||
|
|
966c873013 | ||
|
|
b8c61c37dc | ||
|
|
22582cd32f | ||
|
|
a46573cab3 | ||
|
|
64e380cc40 | ||
|
|
2f68c7c386 | ||
|
|
e3001a70ed | ||
|
|
ea6543b3f9 | ||
|
|
1793dd629f | ||
|
|
edf8c6ea32 | ||
|
|
60cdffcf3c | ||
|
|
717ed04b38 | ||
|
|
80a6b21ff9 | ||
|
|
e48c3f57dd | ||
|
|
39f427555f | ||
|
|
280e455198 | ||
|
|
082991694b | ||
|
|
4ec9d818bf | ||
|
|
258d8921ad | ||
|
|
2b8d416625 | ||
|
|
b4e9613efe | ||
|
|
25605208e4 | ||
|
|
29036bed18 | ||
|
|
6d78f14cc7 | ||
|
|
4730753cb3 | ||
|
|
4fa2ef41aa | ||
|
|
732376328c | ||
|
|
110fe4b0aa | ||
|
|
45ba8d447a | ||
|
|
108417ca9d | ||
|
|
bd1ff9731d | ||
|
|
235af71343 | ||
|
|
059306c93b | ||
|
|
d938e24cf5 | ||
|
|
ab1881d02e | ||
|
|
90a4922b79 | ||
|
|
2022a0db82 | ||
|
|
4f8bb40d3d | ||
|
|
8ea296c99f | ||
|
|
ccefaf003d | ||
|
|
61236e0ace | ||
|
|
6a89ab77c8 | ||
|
|
9f1c279698 | ||
|
|
89cbcfee8c | ||
|
|
2da4bbedbf | ||
|
|
d73272b8e0 |
@@ -56,10 +56,6 @@ GODOXY_HTTP3_ENABLED=true
|
||||
# API listening address
|
||||
GODOXY_API_ADDR=127.0.0.1:8888
|
||||
|
||||
# Local API listening address (unauthenticated, optional)
|
||||
# Useful for local development, debugging or automation
|
||||
GODOXY_LOCAL_API_ADDR=
|
||||
|
||||
# Metrics
|
||||
GODOXY_METRICS_DISABLE_CPU=false
|
||||
GODOXY_METRICS_DISABLE_MEMORY=false
|
||||
|
||||
60
.github/workflows/cli-binary.yml
vendored
60
.github/workflows/cli-binary.yml
vendored
@@ -1,60 +0,0 @@
|
||||
name: GoDoxy CLI Binary
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "cmd/cli/**"
|
||||
- "internal/api/v1/docs/swagger.json"
|
||||
- "Makefile"
|
||||
- ".github/workflows/cli-binary.yml"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "cmd/cli/**"
|
||||
- "internal/api/v1/docs/swagger.json"
|
||||
- "Makefile"
|
||||
- ".github/workflows/cli-binary.yml"
|
||||
tags:
|
||||
- v*
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-latest
|
||||
platform: linux/amd64
|
||||
binary_name: godoxy-cli-linux-amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
platform: linux/arm64
|
||||
binary_name: godoxy-cli-linux-arm64
|
||||
name: Build ${{ matrix.platform }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: "recursive"
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Verify dependencies
|
||||
run: go mod verify
|
||||
|
||||
- name: Build CLI
|
||||
run: |
|
||||
make CLI_BIN_PATH=bin/${{ matrix.binary_name }} build-cli
|
||||
|
||||
- name: Check binary
|
||||
run: |
|
||||
file bin/${{ matrix.binary_name }}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.binary_name }}
|
||||
path: bin/${{ matrix.binary_name }}
|
||||
20
.github/workflows/docker-image-compat.yml
vendored
Normal file
20
.github/workflows/docker-image-compat.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Docker Image CI (compat)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- compat
|
||||
|
||||
jobs:
|
||||
build-compat:
|
||||
uses: ./.github/workflows/docker-image.yml
|
||||
with:
|
||||
image_name: ${{ github.repository_owner }}/godoxy
|
||||
tag: latest-compat
|
||||
target: main
|
||||
build-compat-agent:
|
||||
uses: ./.github/workflows/docker-image.yml
|
||||
with:
|
||||
image_name: ${{ github.repository_owner }}/godoxy-agent
|
||||
tag: latest-compat
|
||||
target: agent
|
||||
1
.github/workflows/docker-image-nightly.yml
vendored
1
.github/workflows/docker-image-nightly.yml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
- "**" # matches every branch
|
||||
- "!dependabot/*"
|
||||
- "!main" # excludes main
|
||||
- "!compat" # excludes compat branch
|
||||
|
||||
jobs:
|
||||
build-nightly:
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -40,7 +40,4 @@ CLAUDE.md
|
||||
!.trunk/configs
|
||||
|
||||
# minified files
|
||||
**/*-min.*
|
||||
|
||||
# generated CLI commands
|
||||
cmd/cli/generated_commands.go
|
||||
**/*-min.*
|
||||
6
.vscode/settings.example.json
vendored
6
.vscode/settings.example.json
vendored
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"yaml.schemas": {
|
||||
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/config.schema.json": [
|
||||
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/types/godoxy/config.schema.json": [
|
||||
"config.example.yml",
|
||||
"config.yml"
|
||||
],
|
||||
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/routes.schema.json": [
|
||||
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/types/godoxy/routes.schema.json": [
|
||||
"providers.example.yml"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
yusing@6uo.me.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
17
Makefile
17
Makefile
@@ -7,7 +7,7 @@ export GOOS = linux
|
||||
REPO_URL ?= https://github.com/yusing/godoxy
|
||||
|
||||
WEBUI_DIR ?= ../godoxy-webui
|
||||
DOCS_DIR ?= wiki
|
||||
DOCS_DIR ?= ${WEBUI_DIR}/wiki
|
||||
|
||||
ifneq ($(BRANCH), compat)
|
||||
GO_TAGS = sonic
|
||||
@@ -58,7 +58,6 @@ endif
|
||||
|
||||
BUILD_FLAGS += -tags '$(GO_TAGS)' -ldflags='$(LDFLAGS)'
|
||||
BIN_PATH := $(shell pwd)/bin/${NAME}
|
||||
CLI_BIN_PATH ?= $(shell pwd)/bin/godoxy-cli
|
||||
|
||||
export NAME
|
||||
export CGO_ENABLED
|
||||
@@ -179,20 +178,16 @@ gen-swagger:
|
||||
python3 scripts/fix-swagger-json.py
|
||||
# we don't need this
|
||||
rm internal/api/v1/docs/docs.go
|
||||
cp internal/api/v1/docs/swagger.json ${DOCS_DIR}/public/api.json
|
||||
|
||||
gen-swagger-markdown: gen-swagger
|
||||
# brew tap go-swagger/go-swagger && brew install go-swagger
|
||||
swagger generate markdown -f internal/api/v1/docs/swagger.yaml --skip-validation --output ${DOCS_DIR}/src/API.md
|
||||
|
||||
gen-api-types: gen-swagger
|
||||
# --disable-throw-on-error
|
||||
bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
|
||||
--responses -o ${WEBUI_DIR}/src/lib -n api.ts -p internal/api/v1/docs/swagger.json
|
||||
|
||||
gen-cli:
|
||||
cd cmd/cli && go run ./gen
|
||||
|
||||
build-cli: gen-cli
|
||||
mkdir -p $(shell dirname ${CLI_BIN_PATH})
|
||||
go build -C cmd/cli -o ${CLI_BIN_PATH} .
|
||||
|
||||
.PHONY: gen-cli build-cli update-wiki
|
||||
.PHONY: update-wiki
|
||||
update-wiki:
|
||||
DOCS_DIR=${DOCS_DIR} REPO_URL=${REPO_URL} bun --bun scripts/update-wiki/main.ts
|
||||
|
||||
20
README.md
20
README.md
@@ -36,6 +36,7 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
||||
- [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)
|
||||
@@ -166,8 +167,23 @@ routes:
|
||||
From the WebUI, you can:
|
||||
|
||||
- **LXC Lifecycle Control**: Start, stop, restart containers
|
||||
- **Node Logs**: Stream real-time journalctl or log files output from nodes
|
||||
- **LXC Logs**: Stream real-time journalctl or log files output from 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
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
- [Proxmox 整合](#proxmox-整合)
|
||||
- [自動路由綁定](#自動路由綁定)
|
||||
- [WebUI 管理](#webui-管理)
|
||||
- [API 端點](#api-端點)
|
||||
- [更新 / 卸載系統代理 (System Agent)](#更新--卸載系統代理-system-agent)
|
||||
- [截圖](#截圖)
|
||||
- [閒置休眠](#閒置休眠)
|
||||
@@ -182,8 +183,23 @@ routes:
|
||||
您可以從 WebUI:
|
||||
|
||||
- **LXC 生命週期控制**:啟動、停止、重新啟動容器
|
||||
- **節點日誌**:串流節點的即時 journalctl 或日誌檔案輸出
|
||||
- **LXC 日誌**:串流容器的即時 journalctl 或日誌檔案輸出
|
||||
- **節點日誌**:串流來自節點的即時 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)
|
||||
|
||||
|
||||
24
agent/go.mod
24
agent/go.mod
@@ -2,11 +2,6 @@ module github.com/yusing/godoxy/agent
|
||||
|
||||
go 1.26.0
|
||||
|
||||
exclude (
|
||||
github.com/moby/moby/api v1.53.0 // allow older daemon versions
|
||||
github.com/moby/moby/client v0.2.2 // allow older daemon versions
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/shirou/gopsutil/v4 => ../internal/gopsutil
|
||||
github.com/yusing/godoxy => ../
|
||||
@@ -19,8 +14,9 @@ replace (
|
||||
|
||||
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
|
||||
|
||||
exclude github.com/yusing/godoxy/internal/utils v0.0.0-20250927032450-e2aeef3a863f
|
||||
|
||||
require (
|
||||
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.1.2
|
||||
@@ -36,6 +32,7 @@ require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
@@ -44,6 +41,7 @@ require (
|
||||
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.2.1+incompatible // indirect
|
||||
github.com/docker/docker v28.5.2+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
|
||||
@@ -63,12 +61,13 @@ require (
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // 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
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/moby/api v1.52.0 // indirect
|
||||
github.com/moby/moby/client v0.2.1 // indirect
|
||||
github.com/moby/moby/api v1.53.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
@@ -76,6 +75,7 @@ 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/pkg/errors v0.9.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.4.0 // indirect
|
||||
@@ -91,12 +91,13 @@ require (
|
||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||
github.com/yusing/ds v0.4.1 // indirect
|
||||
github.com/yusing/gointernals v0.2.0 // indirect
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260218062549-0b0fa3a059ec // indirect
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260218062549-0b0fa3a059ec // indirect
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260215081811-494ab85a33ae // indirect
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260215081811-494ab85a33ae // 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.65.0 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
golang.org/x/arch v0.24.0 // indirect
|
||||
@@ -104,6 +105,7 @@ require (
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
49
agent/go.sum
49
agent/go.sum
@@ -1,3 +1,5 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||
@@ -24,6 +26,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
@@ -39,6 +43,8 @@ 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.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
|
||||
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -55,8 +61,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.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
|
||||
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
|
||||
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=
|
||||
@@ -95,6 +101,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/server/v2 v2.9.0 h1:2zRCl28wkq0oc6YNbyJS2n0dDOOVvOS3Oez5AG2ij54=
|
||||
github.com/gotify/server/v2 v2.9.0/go.mod h1:249wwlUqHTr0QsiKARGtFVqds0pNLIMjYLinHyMACdQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
||||
@@ -111,10 +119,10 @@ 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/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-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
|
||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/luthermonson/go-proxmox v0.4.0 h1:LKXpG9d64zTaQF79wV0kfOnnSwIcdG39m7sc4ga+XZs=
|
||||
github.com/luthermonson/go-proxmox v0.4.0/go.mod h1:U6dAkJ+iiwaeb1g/LMWpWuWN4nmvWeXhmoMuYJMumS4=
|
||||
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.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=
|
||||
@@ -128,15 +136,21 @@ 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=
|
||||
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
|
||||
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
||||
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
|
||||
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
@@ -155,6 +169,7 @@ github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k
|
||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
@@ -220,6 +235,10 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
@@ -228,6 +247,8 @@ go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4A
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
@@ -258,6 +279,12 @@ golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
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=
|
||||
@@ -268,5 +295,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# agent/pkg/agent
|
||||
# Agent Package
|
||||
|
||||
The `agent` package provides the client-side implementation for interacting with GoDoxy agents. It handles agent configuration, secure communication via TLS, and provides utilities for agent deployment and management.
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -15,7 +16,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||
@@ -366,7 +366,7 @@ func (cfg *AgentConfig) fetchJSON(ctx context.Context, endpoint string, out any)
|
||||
return resp.StatusCode, nil
|
||||
}
|
||||
|
||||
err = sonic.Unmarshal(data, out)
|
||||
err = json.Unmarshal(data, out)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# agent/pkg/agent/stream
|
||||
# Stream proxy protocol
|
||||
|
||||
This package implements a small header-based handshake that allows an authenticated client to request forwarding to a `(host, port)` destination. It supports both TCP-over-TLS and UDP-over-DTLS transports.
|
||||
|
||||
|
||||
@@ -21,10 +21,8 @@ const (
|
||||
|
||||
var version = [versionSize]byte{'0', '.', '1', '.', '0', 0, 0, 0}
|
||||
|
||||
var (
|
||||
ErrInvalidHeader = errors.New("invalid header")
|
||||
ErrCloseImmediately = errors.New("close immediately")
|
||||
)
|
||||
var ErrInvalidHeader = errors.New("invalid header")
|
||||
var ErrCloseImmediately = errors.New("close immediately")
|
||||
|
||||
type FlagType uint8
|
||||
|
||||
|
||||
@@ -45,10 +45,9 @@ func TestTLSALPNMux_HTTPAndStreamShareOnePort(t *testing.T) {
|
||||
defer func() { _ = tlsLn.Close() }()
|
||||
|
||||
// HTTP server
|
||||
httpSrv := &http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}),
|
||||
httpSrv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}),
|
||||
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){
|
||||
stream.StreamALPN: func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
|
||||
streamSrv.ServeConn(conn)
|
||||
|
||||
@@ -2,11 +2,11 @@ package agentproxy
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
route "github.com/yusing/godoxy/internal/route/types"
|
||||
)
|
||||
|
||||
@@ -53,7 +53,7 @@ func proxyConfigFromHeaders(h http.Header) (cfg Config, err error) {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
err = sonic.Unmarshal(cfgJSON, &cfg)
|
||||
err = json.Unmarshal(cfgJSON, &cfg)
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func (cfg *Config) SetAgentProxyConfigHeadersLegacy(h http.Header) {
|
||||
func (cfg *Config) SetAgentProxyConfigHeaders(h http.Header) {
|
||||
h.Set(HeaderXProxyHost, cfg.Host)
|
||||
h.Set(HeaderXProxyScheme, string(cfg.Scheme))
|
||||
cfgJSON, _ := sonic.Marshal(cfg.HTTPConfig)
|
||||
cfgJSON, _ := json.Marshal(cfg.HTTPConfig)
|
||||
cfgBase64 := base64.StdEncoding.EncodeToString(cfgJSON)
|
||||
h.Set(HeaderXProxyConfig, cfgBase64)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -8,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
healthcheck "github.com/yusing/godoxy/internal/health/check"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
)
|
||||
@@ -73,7 +73,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
sonic.ConfigDefault.NewEncoder(w).Encode(result)
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
func parseMsOrDefault(msStr string) time.Duration {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
@@ -51,7 +51,7 @@ func NewAgentHandler() http.Handler {
|
||||
Runtime: env.Runtime,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
sonic.ConfigDefault.NewEncoder(w).Encode(agentInfo)
|
||||
json.NewEncoder(w).Encode(agentInfo)
|
||||
})
|
||||
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
|
||||
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)
|
||||
|
||||
@@ -2,14 +2,13 @@ package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
|
||||
"math/rand/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
printables = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
random = make([]byte, 4096)
|
||||
)
|
||||
var printables = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
var random = make([]byte, 4096)
|
||||
|
||||
func init() {
|
||||
for i := range random {
|
||||
|
||||
730
cmd/cli/cli.go
730
cmd/cli/cli.go
@@ -1,730 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/yusing/goutils/env"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Addr string
|
||||
}
|
||||
|
||||
type stringSliceFlag struct {
|
||||
set bool
|
||||
v []string
|
||||
}
|
||||
|
||||
func (s *stringSliceFlag) String() string {
|
||||
return strings.Join(s.v, ",")
|
||||
}
|
||||
|
||||
func (s *stringSliceFlag) Set(value string) error {
|
||||
s.set = true
|
||||
if value == "" {
|
||||
s.v = nil
|
||||
return nil
|
||||
}
|
||||
s.v = strings.Split(value, ",")
|
||||
return nil
|
||||
}
|
||||
|
||||
func run(args []string) error {
|
||||
cfg, rest, err := parseGlobal(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rest) == 0 {
|
||||
printHelp()
|
||||
return nil
|
||||
}
|
||||
if rest[0] == "help" {
|
||||
printHelp()
|
||||
return nil
|
||||
}
|
||||
ep, matchedLen := findEndpoint(rest)
|
||||
if ep == nil {
|
||||
ep, matchedLen = findEndpointAlias(rest)
|
||||
}
|
||||
if ep == nil {
|
||||
return unknownCommandError(rest)
|
||||
}
|
||||
cmdArgs := rest[matchedLen:]
|
||||
return executeEndpoint(cfg.Addr, *ep, cmdArgs)
|
||||
}
|
||||
|
||||
func parseGlobal(args []string) (config, []string, error) {
|
||||
var cfg config
|
||||
fs := flag.NewFlagSet("godoxy", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
fs.StringVar(&cfg.Addr, "addr", "", "API address, e.g. 127.0.0.1:8888 or http://127.0.0.1:8888")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return cfg, nil, err
|
||||
}
|
||||
return cfg, fs.Args(), nil
|
||||
}
|
||||
|
||||
func resolveBaseURL(addrFlag string) (string, error) {
|
||||
if addrFlag != "" {
|
||||
return normalizeURL(addrFlag), nil
|
||||
}
|
||||
_, _, _, fullURL := env.GetAddrEnv("LOCAL_API_ADDR", "", "http")
|
||||
if fullURL == "" {
|
||||
return "", errors.New("missing LOCAL_API_ADDR (or GODOXY_LOCAL_API_ADDR). set env var or pass --addr")
|
||||
}
|
||||
return normalizeURL(fullURL), nil
|
||||
}
|
||||
|
||||
func normalizeURL(addr string) string {
|
||||
a := strings.TrimSpace(addr)
|
||||
if strings.Contains(a, "://") {
|
||||
return strings.TrimRight(a, "/")
|
||||
}
|
||||
return "http://" + strings.TrimRight(a, "/")
|
||||
}
|
||||
|
||||
func findEndpoint(args []string) (*Endpoint, int) {
|
||||
var best *Endpoint
|
||||
bestLen := -1
|
||||
for i := range generatedEndpoints {
|
||||
ep := &generatedEndpoints[i]
|
||||
if len(ep.CommandPath) > len(args) {
|
||||
continue
|
||||
}
|
||||
ok := true
|
||||
for j, tok := range ep.CommandPath {
|
||||
if args[j] != tok {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if ok && len(ep.CommandPath) > bestLen {
|
||||
best = ep
|
||||
bestLen = len(ep.CommandPath)
|
||||
}
|
||||
}
|
||||
return best, bestLen
|
||||
}
|
||||
|
||||
func executeEndpoint(addrFlag string, ep Endpoint, args []string) error {
|
||||
fs := flag.NewFlagSet(strings.Join(ep.CommandPath, "-"), flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
useWS := false
|
||||
if ep.IsWebSocket {
|
||||
fs.BoolVar(&useWS, "ws", false, "use websocket")
|
||||
}
|
||||
typedValues := make(map[string]any, len(ep.Params))
|
||||
isSet := make(map[string]bool, len(ep.Params))
|
||||
for _, p := range ep.Params {
|
||||
switch p.Type {
|
||||
case "integer":
|
||||
v := new(int)
|
||||
fs.IntVar(v, p.FlagName, 0, p.Description)
|
||||
typedValues[p.FlagName] = v
|
||||
case "number":
|
||||
v := new(float64)
|
||||
fs.Float64Var(v, p.FlagName, 0, p.Description)
|
||||
typedValues[p.FlagName] = v
|
||||
case "boolean":
|
||||
v := new(bool)
|
||||
fs.BoolVar(v, p.FlagName, false, p.Description)
|
||||
typedValues[p.FlagName] = v
|
||||
case "array":
|
||||
v := &stringSliceFlag{}
|
||||
fs.Var(v, p.FlagName, p.Description+" (comma-separated)")
|
||||
typedValues[p.FlagName] = v
|
||||
default:
|
||||
v := new(string)
|
||||
fs.StringVar(v, p.FlagName, "", p.Description)
|
||||
typedValues[p.FlagName] = v
|
||||
}
|
||||
}
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return fmt.Errorf("%w\n\n%s", err, formatEndpointHelp(ep))
|
||||
}
|
||||
if len(fs.Args()) > 0 {
|
||||
return fmt.Errorf("unexpected args: %s\n\n%s", strings.Join(fs.Args(), " "), formatEndpointHelp(ep))
|
||||
}
|
||||
fs.Visit(func(f *flag.Flag) {
|
||||
isSet[f.Name] = true
|
||||
})
|
||||
|
||||
for _, p := range ep.Params {
|
||||
if !p.Required {
|
||||
continue
|
||||
}
|
||||
if !isSet[p.FlagName] {
|
||||
return fmt.Errorf("missing required flag --%s\n\n%s", p.FlagName, formatEndpointHelp(ep))
|
||||
}
|
||||
}
|
||||
|
||||
baseURL, err := resolveBaseURL(addrFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqURL, body, err := buildRequest(ep, baseURL, typedValues, isSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if useWS {
|
||||
if !ep.IsWebSocket {
|
||||
return errors.New("--ws is only supported for websocket endpoints")
|
||||
}
|
||||
return execWebsocket(ep, reqURL)
|
||||
}
|
||||
return execHTTP(ep, reqURL, body)
|
||||
}
|
||||
|
||||
func buildRequest(ep Endpoint, baseURL string, typedValues map[string]any, isSet map[string]bool) (string, []byte, error) {
|
||||
path := ep.Path
|
||||
for _, p := range ep.Params {
|
||||
if p.In != "path" {
|
||||
continue
|
||||
}
|
||||
raw, err := paramValueString(p, typedValues[p.FlagName], isSet[p.FlagName])
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if raw == "" {
|
||||
continue
|
||||
}
|
||||
esc := url.PathEscape(raw)
|
||||
path = strings.ReplaceAll(path, "{"+p.Name+"}", esc)
|
||||
path = strings.ReplaceAll(path, ":"+p.Name, esc)
|
||||
}
|
||||
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("invalid base url: %w", err)
|
||||
}
|
||||
u.Path = strings.TrimRight(u.Path, "/") + path
|
||||
|
||||
q := u.Query()
|
||||
for _, p := range ep.Params {
|
||||
if p.In != "query" || !isSet[p.FlagName] {
|
||||
continue
|
||||
}
|
||||
val, err := paramQueryValues(p, typedValues[p.FlagName])
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
for _, v := range val {
|
||||
q.Add(p.Name, v)
|
||||
}
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
bodyMap := map[string]any{}
|
||||
rawBody := ""
|
||||
for _, p := range ep.Params {
|
||||
if p.In != "body" || !isSet[p.FlagName] {
|
||||
continue
|
||||
}
|
||||
if p.Name == "file" {
|
||||
s, err := paramValueString(p, typedValues[p.FlagName], true)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
rawBody = s
|
||||
continue
|
||||
}
|
||||
v, err := paramBodyValue(p, typedValues[p.FlagName])
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
bodyMap[p.Name] = v
|
||||
}
|
||||
|
||||
if rawBody != "" {
|
||||
return u.String(), []byte(rawBody), nil
|
||||
}
|
||||
if len(bodyMap) == 0 {
|
||||
return u.String(), nil, nil
|
||||
}
|
||||
data, err := json.Marshal(bodyMap)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("marshal body: %w", err)
|
||||
}
|
||||
return u.String(), data, nil
|
||||
}
|
||||
|
||||
func paramValueString(p Param, raw any, wasSet bool) (string, error) {
|
||||
if !wasSet {
|
||||
return "", nil
|
||||
}
|
||||
switch v := raw.(type) {
|
||||
case *string:
|
||||
return *v, nil
|
||||
case *int:
|
||||
return strconv.Itoa(*v), nil
|
||||
case *float64:
|
||||
return strconv.FormatFloat(*v, 'f', -1, 64), nil
|
||||
case *bool:
|
||||
if *v {
|
||||
return "true", nil
|
||||
}
|
||||
return "false", nil
|
||||
case *stringSliceFlag:
|
||||
return strings.Join(v.v, ","), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported flag value for %s", p.FlagName)
|
||||
}
|
||||
}
|
||||
|
||||
func paramQueryValues(p Param, raw any) ([]string, error) {
|
||||
switch v := raw.(type) {
|
||||
case *string:
|
||||
return []string{*v}, nil
|
||||
case *int:
|
||||
return []string{strconv.Itoa(*v)}, nil
|
||||
case *float64:
|
||||
return []string{strconv.FormatFloat(*v, 'f', -1, 64)}, nil
|
||||
case *bool:
|
||||
if *v {
|
||||
return []string{"true"}, nil
|
||||
}
|
||||
return []string{"false"}, nil
|
||||
case *stringSliceFlag:
|
||||
if len(v.v) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return v.v, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported query flag type for %s", p.FlagName)
|
||||
}
|
||||
}
|
||||
|
||||
func paramBodyValue(p Param, raw any) (any, error) {
|
||||
switch v := raw.(type) {
|
||||
case *string:
|
||||
if p.Type == "object" || p.Type == "array" {
|
||||
var decoded any
|
||||
if err := json.Unmarshal([]byte(*v), &decoded); err != nil {
|
||||
return nil, fmt.Errorf("invalid JSON for --%s: %w", p.FlagName, err)
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
return *v, nil
|
||||
case *int:
|
||||
return *v, nil
|
||||
case *float64:
|
||||
return *v, nil
|
||||
case *bool:
|
||||
return *v, nil
|
||||
case *stringSliceFlag:
|
||||
return v.v, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported body flag type for %s", p.FlagName)
|
||||
}
|
||||
}
|
||||
|
||||
func execHTTP(ep Endpoint, reqURL string, body []byte) error {
|
||||
var r io.Reader
|
||||
if body != nil {
|
||||
r = bytes.NewReader(body)
|
||||
}
|
||||
req, err := http.NewRequest(ep.Method, reqURL, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
payload, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
if len(payload) == 0 {
|
||||
return fmt.Errorf("%s %s failed: %s", ep.Method, ep.Path, resp.Status)
|
||||
}
|
||||
return fmt.Errorf("%s %s failed: %s: %s", ep.Method, ep.Path, resp.Status, strings.TrimSpace(string(payload)))
|
||||
}
|
||||
|
||||
printJSON(payload)
|
||||
return nil
|
||||
}
|
||||
|
||||
func execWebsocket(ep Endpoint, reqURL string) error {
|
||||
wsURL := strings.Replace(reqURL, "http://", "ws://", 1)
|
||||
wsURL = strings.Replace(wsURL, "https://", "wss://", 1)
|
||||
if strings.ToUpper(ep.Method) != http.MethodGet {
|
||||
return fmt.Errorf("--ws requires GET endpoint, got %s", ep.Method)
|
||||
}
|
||||
c, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
stopPing := make(chan struct{})
|
||||
defer close(stopPing)
|
||||
go func() {
|
||||
ticker := time.NewTicker(3 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-stopPing:
|
||||
return
|
||||
case <-ticker.C:
|
||||
_ = c.SetWriteDeadline(time.Now().Add(2 * time.Second))
|
||||
if err := c.WriteMessage(websocket.TextMessage, []byte("ping")); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
_, msg, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) || strings.Contains(err.Error(), "close") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if string(msg) == "pong" {
|
||||
continue
|
||||
}
|
||||
fmt.Println(string(msg))
|
||||
}
|
||||
}
|
||||
|
||||
func printJSON(payload []byte) {
|
||||
if len(payload) == 0 {
|
||||
fmt.Println("null")
|
||||
return
|
||||
}
|
||||
var v any
|
||||
if err := json.Unmarshal(payload, &v); err != nil {
|
||||
fmt.Println(strings.TrimSpace(string(payload)))
|
||||
return
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
_ = enc.Encode(v)
|
||||
}
|
||||
|
||||
func printHelp() {
|
||||
fmt.Println("godoxy [--addr ADDR] <command>")
|
||||
fmt.Println()
|
||||
fmt.Println("Examples:")
|
||||
fmt.Println(" godoxy version")
|
||||
fmt.Println(" godoxy route list")
|
||||
fmt.Println(" godoxy route route --which whoami")
|
||||
fmt.Println()
|
||||
printGroupedCommands()
|
||||
}
|
||||
|
||||
func printGroupedCommands() {
|
||||
grouped := map[string][]Endpoint{}
|
||||
groupOrder := make([]string, 0)
|
||||
seen := map[string]bool{}
|
||||
for _, ep := range generatedEndpoints {
|
||||
group := "root"
|
||||
if len(ep.CommandPath) > 1 {
|
||||
group = ep.CommandPath[0]
|
||||
}
|
||||
grouped[group] = append(grouped[group], ep)
|
||||
if !seen[group] {
|
||||
seen[group] = true
|
||||
groupOrder = append(groupOrder, group)
|
||||
}
|
||||
}
|
||||
sort.Strings(groupOrder)
|
||||
for _, group := range groupOrder {
|
||||
fmt.Printf("Commands (%s):\n", group)
|
||||
sort.Slice(grouped[group], func(i, j int) bool {
|
||||
li := strings.Join(grouped[group][i].CommandPath, " ")
|
||||
lj := strings.Join(grouped[group][j].CommandPath, " ")
|
||||
return li < lj
|
||||
})
|
||||
maxCmdWidth := 0
|
||||
for _, ep := range grouped[group] {
|
||||
cmd := strings.Join(ep.CommandPath, " ")
|
||||
if len(cmd) > maxCmdWidth {
|
||||
maxCmdWidth = len(cmd)
|
||||
}
|
||||
}
|
||||
for _, ep := range grouped[group] {
|
||||
cmd := strings.Join(ep.CommandPath, " ")
|
||||
fmt.Printf(" %-*s %s\n", maxCmdWidth, cmd, ep.Summary)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func unknownCommandError(rest []string) error {
|
||||
cmd := strings.Join(rest, " ")
|
||||
var b strings.Builder
|
||||
b.WriteString("unknown command: ")
|
||||
b.WriteString(cmd)
|
||||
if len(rest) > 0 && hasGroup(rest[0]) {
|
||||
if len(rest) > 1 {
|
||||
if hint := nearestForGroup(rest[0], rest[1]); hint != "" {
|
||||
b.WriteString("\nDo you mean ")
|
||||
b.WriteString(hint)
|
||||
b.WriteString("?")
|
||||
}
|
||||
}
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(formatGroupHelp(rest[0]))
|
||||
return errors.New(b.String())
|
||||
}
|
||||
if hint := nearestCommand(cmd); hint != "" {
|
||||
b.WriteString("\nDo you mean ")
|
||||
b.WriteString(hint)
|
||||
b.WriteString("?")
|
||||
}
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString("Run `godoxy help` for available commands.")
|
||||
return errors.New(b.String())
|
||||
}
|
||||
|
||||
func findEndpointAlias(args []string) (*Endpoint, int) {
|
||||
var best *Endpoint
|
||||
bestLen := -1
|
||||
for i := range generatedEndpoints {
|
||||
alias := aliasCommandPath(generatedEndpoints[i])
|
||||
if len(alias) == 0 || len(alias) > len(args) {
|
||||
continue
|
||||
}
|
||||
ok := true
|
||||
for j, tok := range alias {
|
||||
if args[j] != tok {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if ok && len(alias) > bestLen {
|
||||
best = &generatedEndpoints[i]
|
||||
bestLen = len(alias)
|
||||
}
|
||||
}
|
||||
return best, bestLen
|
||||
}
|
||||
|
||||
func aliasCommandPath(ep Endpoint) []string {
|
||||
rawPath := strings.TrimPrefix(ep.Path, "/api/v1/")
|
||||
rawPath = strings.Trim(rawPath, "/")
|
||||
if rawPath == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(rawPath, "/")
|
||||
if len(parts) == 1 {
|
||||
if isPathParam(parts[0]) {
|
||||
return nil
|
||||
}
|
||||
return []string{toKebabToken(parts[0])}
|
||||
}
|
||||
if isPathParam(parts[0]) || isPathParam(parts[1]) {
|
||||
return nil
|
||||
}
|
||||
return []string{toKebabToken(parts[0]), toKebabToken(parts[1])}
|
||||
}
|
||||
|
||||
func isPathParam(s string) bool {
|
||||
return strings.HasPrefix(s, "{") || strings.HasPrefix(s, ":")
|
||||
}
|
||||
|
||||
func toKebabToken(s string) string {
|
||||
s = strings.ReplaceAll(s, "_", "-")
|
||||
return strings.ToLower(strings.Trim(s, "-"))
|
||||
}
|
||||
|
||||
func hasGroup(group string) bool {
|
||||
for _, ep := range generatedEndpoints {
|
||||
if len(ep.CommandPath) > 1 && ep.CommandPath[0] == group {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func nearestCommand(input string) string {
|
||||
commands := make([]string, 0, len(generatedEndpoints))
|
||||
for _, ep := range generatedEndpoints {
|
||||
commands = append(commands, strings.Join(ep.CommandPath, " "))
|
||||
}
|
||||
return nearestByDistance(input, commands)
|
||||
}
|
||||
|
||||
func nearestForGroup(group, input string) string {
|
||||
choiceSet := map[string]struct{}{}
|
||||
for _, ep := range generatedEndpoints {
|
||||
if len(ep.CommandPath) < 2 || ep.CommandPath[0] != group {
|
||||
continue
|
||||
}
|
||||
choiceSet[ep.CommandPath[1]] = struct{}{}
|
||||
alias := aliasCommandPath(ep)
|
||||
if len(alias) == 2 && alias[0] == group {
|
||||
choiceSet[alias[1]] = struct{}{}
|
||||
}
|
||||
}
|
||||
choices := make([]string, 0, len(choiceSet))
|
||||
for choice := range choiceSet {
|
||||
choices = append(choices, choice)
|
||||
}
|
||||
if len(choices) == 0 {
|
||||
return ""
|
||||
}
|
||||
return group + " " + nearestByDistance(input, choices)
|
||||
}
|
||||
|
||||
func formatGroupHelp(group string) string {
|
||||
commands := make([]Endpoint, 0)
|
||||
for _, ep := range generatedEndpoints {
|
||||
if len(ep.CommandPath) > 1 && ep.CommandPath[0] == group {
|
||||
commands = append(commands, ep)
|
||||
}
|
||||
}
|
||||
sort.Slice(commands, func(i, j int) bool {
|
||||
return strings.Join(commands[i].CommandPath, " ") < strings.Join(commands[j].CommandPath, " ")
|
||||
})
|
||||
maxWidth := 0
|
||||
for _, ep := range commands {
|
||||
cmd := strings.Join(ep.CommandPath, " ")
|
||||
if len(cmd) > maxWidth {
|
||||
maxWidth = len(cmd)
|
||||
}
|
||||
}
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "Available subcommands for %s:\n", group)
|
||||
for _, ep := range commands {
|
||||
cmd := strings.Join(ep.CommandPath, " ")
|
||||
fmt.Fprintf(&b, " %-*s %s\n", maxWidth, cmd, ep.Summary)
|
||||
}
|
||||
return strings.TrimRight(b.String(), "\n")
|
||||
}
|
||||
|
||||
func formatEndpointHelp(ep Endpoint) string {
|
||||
cmd := "godoxy " + strings.Join(ep.CommandPath, " ")
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "Usage: %s [flags]\n", cmd)
|
||||
if ep.Summary != "" {
|
||||
fmt.Fprintf(&b, "Summary: %s\n", ep.Summary)
|
||||
}
|
||||
if alias := aliasCommandPath(ep); len(alias) > 0 && strings.Join(alias, " ") != strings.Join(ep.CommandPath, " ") {
|
||||
fmt.Fprintf(&b, "Alias: godoxy %s\n", strings.Join(alias, " "))
|
||||
}
|
||||
params := make([]Param, 0, len(ep.Params))
|
||||
params = append(params, ep.Params...)
|
||||
if ep.IsWebSocket {
|
||||
params = append(params, Param{
|
||||
FlagName: "ws",
|
||||
Type: "boolean",
|
||||
Description: "use websocket",
|
||||
Required: false,
|
||||
})
|
||||
}
|
||||
if len(params) == 0 {
|
||||
return strings.TrimRight(b.String(), "\n")
|
||||
}
|
||||
b.WriteString("Flags:\n")
|
||||
maxWidth := 0
|
||||
flagNames := make([]string, 0, len(params))
|
||||
for _, p := range params {
|
||||
name := "--" + p.FlagName
|
||||
if p.Required {
|
||||
name += " (required)"
|
||||
}
|
||||
flagNames = append(flagNames, name)
|
||||
if len(name) > maxWidth {
|
||||
maxWidth = len(name)
|
||||
}
|
||||
}
|
||||
for i, p := range params {
|
||||
desc := p.Description
|
||||
if desc == "" {
|
||||
desc = p.In + " " + p.Type
|
||||
}
|
||||
fmt.Fprintf(&b, " %-*s %s\n", maxWidth, flagNames[i], desc)
|
||||
}
|
||||
return strings.TrimRight(b.String(), "\n")
|
||||
}
|
||||
|
||||
func nearestByDistance(input string, choices []string) string {
|
||||
if len(choices) == 0 {
|
||||
return ""
|
||||
}
|
||||
nearest := choices[0]
|
||||
minDistance := levenshteinDistance(input, nearest)
|
||||
for _, choice := range choices[1:] {
|
||||
d := levenshteinDistance(input, choice)
|
||||
if d < minDistance {
|
||||
minDistance = d
|
||||
nearest = choice
|
||||
}
|
||||
}
|
||||
return nearest
|
||||
}
|
||||
|
||||
//nolint:intrange
|
||||
func levenshteinDistance(a, b string) int {
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
if len(a) == 0 {
|
||||
return len(b)
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return len(a)
|
||||
}
|
||||
|
||||
v0 := make([]int, len(b)+1)
|
||||
v1 := make([]int, len(b)+1)
|
||||
|
||||
for i := 0; i <= len(b); i++ {
|
||||
v0[i] = i
|
||||
}
|
||||
|
||||
for i := 0; i < len(a); i++ {
|
||||
v1[0] = i + 1
|
||||
|
||||
for j := 0; j < len(b); j++ {
|
||||
cost := 0
|
||||
if a[i] != b[j] {
|
||||
cost = 1
|
||||
}
|
||||
|
||||
v1[j+1] = min3(v1[j]+1, v0[j+1]+1, v0[j]+cost)
|
||||
}
|
||||
|
||||
for j := 0; j <= len(b); j++ {
|
||||
v0[j] = v1[j]
|
||||
}
|
||||
}
|
||||
|
||||
return v1[len(b)]
|
||||
}
|
||||
|
||||
func min3(a, b, c int) int {
|
||||
if a < b && a < c {
|
||||
return a
|
||||
}
|
||||
if b < a && b < c {
|
||||
return b
|
||||
}
|
||||
return c
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type swaggerSpec struct {
|
||||
BasePath string `json:"basePath"`
|
||||
Paths map[string]map[string]operation `json:"paths"`
|
||||
Definitions map[string]definition `json:"definitions"`
|
||||
}
|
||||
|
||||
type operation struct {
|
||||
OperationID string `json:"operationId"`
|
||||
Summary string `json:"summary"`
|
||||
Tags []string `json:"tags"`
|
||||
Parameters []parameter `json:"parameters"`
|
||||
}
|
||||
|
||||
type parameter struct {
|
||||
Name string `json:"name"`
|
||||
In string `json:"in"`
|
||||
Required bool `json:"required"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Schema *schemaRef `json:"schema"`
|
||||
}
|
||||
|
||||
type schemaRef struct {
|
||||
Ref string `json:"$ref"`
|
||||
}
|
||||
|
||||
type definition struct {
|
||||
Type string `json:"type"`
|
||||
Required []string `json:"required"`
|
||||
Properties map[string]definition `json:"properties"`
|
||||
Items *definition `json:"items"`
|
||||
}
|
||||
|
||||
type endpoint struct {
|
||||
CommandPath []string
|
||||
Method string
|
||||
Path string
|
||||
Summary string
|
||||
IsWebSocket bool
|
||||
Params []param
|
||||
}
|
||||
|
||||
type param struct {
|
||||
FlagName string
|
||||
Name string
|
||||
In string
|
||||
Type string
|
||||
Required bool
|
||||
Description string
|
||||
}
|
||||
|
||||
func main() {
|
||||
root := filepath.Join("..", "..")
|
||||
inPath := filepath.Join(root, "internal", "api", "v1", "docs", "swagger.json")
|
||||
outPath := "generated_commands.go"
|
||||
|
||||
raw, err := os.ReadFile(inPath)
|
||||
must(err)
|
||||
|
||||
var spec swaggerSpec
|
||||
must(json.Unmarshal(raw, &spec))
|
||||
|
||||
eps := buildEndpoints(spec)
|
||||
must(writeGenerated(outPath, eps))
|
||||
}
|
||||
|
||||
func buildEndpoints(spec swaggerSpec) []endpoint {
|
||||
byCommand := map[string]endpoint{}
|
||||
|
||||
pathKeys := make([]string, 0, len(spec.Paths))
|
||||
for p := range spec.Paths {
|
||||
pathKeys = append(pathKeys, p)
|
||||
}
|
||||
sort.Strings(pathKeys)
|
||||
|
||||
for _, p := range pathKeys {
|
||||
methodMap := spec.Paths[p]
|
||||
methods := make([]string, 0, len(methodMap))
|
||||
for m := range methodMap {
|
||||
methods = append(methods, strings.ToUpper(m))
|
||||
}
|
||||
sort.Strings(methods)
|
||||
|
||||
for _, method := range methods {
|
||||
op := methodMap[strings.ToLower(method)]
|
||||
if op.OperationID == "" {
|
||||
continue
|
||||
}
|
||||
ep := endpoint{
|
||||
CommandPath: commandPathFromOp(p, op.OperationID),
|
||||
Method: method,
|
||||
Path: ensureSlash(spec.BasePath) + normalizePath(p),
|
||||
Summary: op.Summary,
|
||||
IsWebSocket: hasTag(op.Tags, "websocket"),
|
||||
Params: collectParams(spec, op),
|
||||
}
|
||||
key := strings.Join(ep.CommandPath, " ")
|
||||
if existing, ok := byCommand[key]; ok {
|
||||
if betterEndpoint(ep, existing) {
|
||||
byCommand[key] = ep
|
||||
}
|
||||
continue
|
||||
}
|
||||
byCommand[key] = ep
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]endpoint, 0, len(byCommand))
|
||||
for _, ep := range byCommand {
|
||||
out = append(out, ep)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
ai := strings.Join(out[i].CommandPath, " ")
|
||||
aj := strings.Join(out[j].CommandPath, " ")
|
||||
return ai < aj
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func commandPathFromOp(path, opID string) []string {
|
||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
if len(parts) == 0 {
|
||||
return []string{toKebab(opID)}
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
return []string{toKebab(parts[0])}
|
||||
}
|
||||
group := toKebab(parts[0])
|
||||
name := toKebab(opID)
|
||||
if name == group {
|
||||
name = "get"
|
||||
}
|
||||
if group == "v1" {
|
||||
return []string{name}
|
||||
}
|
||||
return []string{group, name}
|
||||
}
|
||||
|
||||
func collectParams(spec swaggerSpec, op operation) []param {
|
||||
params := make([]param, 0)
|
||||
for _, p := range op.Parameters {
|
||||
switch p.In {
|
||||
case "body":
|
||||
if p.Schema != nil && p.Schema.Ref != "" {
|
||||
defName := strings.TrimPrefix(p.Schema.Ref, "#/definitions/")
|
||||
params = append(params, bodyParamsFromDef(spec.Definitions[defName])...)
|
||||
continue
|
||||
}
|
||||
params = append(params, param{
|
||||
FlagName: toKebab(p.Name),
|
||||
Name: p.Name,
|
||||
In: "body",
|
||||
Type: defaultType(p.Type),
|
||||
Required: p.Required,
|
||||
Description: p.Description,
|
||||
})
|
||||
default:
|
||||
params = append(params, param{
|
||||
FlagName: toKebab(p.Name),
|
||||
Name: p.Name,
|
||||
In: p.In,
|
||||
Type: defaultType(p.Type),
|
||||
Required: p.Required,
|
||||
Description: p.Description,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate by flag name, prefer required entries.
|
||||
byFlag := map[string]param{}
|
||||
for _, p := range params {
|
||||
if cur, ok := byFlag[p.FlagName]; ok {
|
||||
if !cur.Required && p.Required {
|
||||
byFlag[p.FlagName] = p
|
||||
}
|
||||
continue
|
||||
}
|
||||
byFlag[p.FlagName] = p
|
||||
}
|
||||
out := make([]param, 0, len(byFlag))
|
||||
for _, p := range byFlag {
|
||||
out = append(out, p)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].In != out[j].In {
|
||||
return out[i].In < out[j].In
|
||||
}
|
||||
return out[i].FlagName < out[j].FlagName
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func bodyParamsFromDef(def definition) []param {
|
||||
if def.Type != "object" {
|
||||
return nil
|
||||
}
|
||||
requiredSet := map[string]struct{}{}
|
||||
for _, name := range def.Required {
|
||||
requiredSet[name] = struct{}{}
|
||||
}
|
||||
keys := make([]string, 0, len(def.Properties))
|
||||
for k := range def.Properties {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
out := make([]param, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
prop := def.Properties[k]
|
||||
_, required := requiredSet[k]
|
||||
t := defaultType(prop.Type)
|
||||
if prop.Type == "array" {
|
||||
t = "array"
|
||||
}
|
||||
if prop.Type == "object" {
|
||||
t = "object"
|
||||
}
|
||||
out = append(out, param{
|
||||
FlagName: toKebab(k),
|
||||
Name: k,
|
||||
In: "body",
|
||||
Type: t,
|
||||
Required: required,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func betterEndpoint(a, b endpoint) bool {
|
||||
// Prefer GET, then fewer path params, then shorter path.
|
||||
if a.Method == "GET" && b.Method != "GET" {
|
||||
return true
|
||||
}
|
||||
if a.Method != "GET" && b.Method == "GET" {
|
||||
return false
|
||||
}
|
||||
ac := countPathParams(a.Path)
|
||||
bc := countPathParams(b.Path)
|
||||
if ac != bc {
|
||||
return ac < bc
|
||||
}
|
||||
return len(a.Path) < len(b.Path)
|
||||
}
|
||||
|
||||
func countPathParams(path string) int {
|
||||
count := 0
|
||||
for _, seg := range strings.Split(path, "/") {
|
||||
if strings.HasPrefix(seg, "{") || strings.HasPrefix(seg, ":") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func normalizePath(p string) string {
|
||||
parts := strings.Split(p, "/")
|
||||
for i, part := range parts {
|
||||
if strings.HasPrefix(part, "{") && strings.HasSuffix(part, "}") {
|
||||
name := strings.TrimSuffix(strings.TrimPrefix(part, "{"), "}")
|
||||
parts[i] = "{" + name + "}"
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "/")
|
||||
}
|
||||
|
||||
func hasTag(tags []string, want string) bool {
|
||||
for _, t := range tags {
|
||||
if strings.EqualFold(t, want) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func writeGenerated(outPath string, eps []endpoint) error {
|
||||
var b bytes.Buffer
|
||||
b.WriteString("// Code generated by cmd/cli/gen. DO NOT EDIT.\n")
|
||||
b.WriteString("package main\n\n")
|
||||
b.WriteString("var generatedEndpoints = []Endpoint{\n")
|
||||
for _, ep := range eps {
|
||||
b.WriteString("\t{\n")
|
||||
fmt.Fprintf(&b, "\t\tCommandPath: %#v,\n", ep.CommandPath)
|
||||
fmt.Fprintf(&b, "\t\tMethod: %q,\n", ep.Method)
|
||||
fmt.Fprintf(&b, "\t\tPath: %q,\n", ep.Path)
|
||||
fmt.Fprintf(&b, "\t\tSummary: %q,\n", ep.Summary)
|
||||
fmt.Fprintf(&b, "\t\tIsWebSocket: %t,\n", ep.IsWebSocket)
|
||||
b.WriteString("\t\tParams: []Param{\n")
|
||||
for _, p := range ep.Params {
|
||||
b.WriteString("\t\t\t{\n")
|
||||
fmt.Fprintf(&b, "\t\t\t\tFlagName: %q,\n", p.FlagName)
|
||||
fmt.Fprintf(&b, "\t\t\t\tName: %q,\n", p.Name)
|
||||
fmt.Fprintf(&b, "\t\t\t\tIn: %q,\n", p.In)
|
||||
fmt.Fprintf(&b, "\t\t\t\tType: %q,\n", p.Type)
|
||||
fmt.Fprintf(&b, "\t\t\t\tRequired: %t,\n", p.Required)
|
||||
fmt.Fprintf(&b, "\t\t\t\tDescription: %q,\n", p.Description)
|
||||
b.WriteString("\t\t\t},\n")
|
||||
}
|
||||
b.WriteString("\t\t},\n")
|
||||
b.WriteString("\t},\n")
|
||||
}
|
||||
b.WriteString("}\n")
|
||||
formatted, err := format.Source(b.Bytes())
|
||||
if err != nil {
|
||||
return fmt.Errorf("format generated source: %w", err)
|
||||
}
|
||||
return os.WriteFile(outPath, formatted, 0o644)
|
||||
}
|
||||
|
||||
func ensureSlash(s string) string {
|
||||
if strings.HasPrefix(s, "/") {
|
||||
return s
|
||||
}
|
||||
return "/" + s
|
||||
}
|
||||
|
||||
func defaultType(t string) string {
|
||||
switch t {
|
||||
case "integer", "number", "boolean", "array", "object", "string":
|
||||
return t
|
||||
default:
|
||||
return "string"
|
||||
}
|
||||
}
|
||||
|
||||
func toKebab(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
s = strings.ReplaceAll(s, "_", "-")
|
||||
s = strings.ReplaceAll(s, ".", "-")
|
||||
var out []rune
|
||||
for i, r := range s {
|
||||
if unicode.IsUpper(r) {
|
||||
if i > 0 && out[len(out)-1] != '-' {
|
||||
out = append(out, '-')
|
||||
}
|
||||
out = append(out, unicode.ToLower(r))
|
||||
continue
|
||||
}
|
||||
out = append(out, unicode.ToLower(r))
|
||||
}
|
||||
res := strings.Trim(string(out), "-")
|
||||
for strings.Contains(res, "--") {
|
||||
res = strings.ReplaceAll(res, "--", "-")
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
module github.com/yusing/godoxy/cli
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/yusing/goutils v0.7.0
|
||||
)
|
||||
|
||||
replace github.com/yusing/goutils => ../../goutils
|
||||
@@ -1,10 +0,0 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -1,13 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(os.Args[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package main
|
||||
|
||||
type Param struct {
|
||||
FlagName string
|
||||
Name string
|
||||
In string
|
||||
Type string
|
||||
Required bool
|
||||
Description string
|
||||
}
|
||||
|
||||
type Endpoint struct {
|
||||
CommandPath []string
|
||||
Method string
|
||||
Path string
|
||||
Summary string
|
||||
IsWebSocket bool
|
||||
Params []Param
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/api"
|
||||
apiV1 "github.com/yusing/godoxy/internal/api/v1"
|
||||
agentApi "github.com/yusing/godoxy/internal/api/v1/agent"
|
||||
@@ -129,31 +128,25 @@ func listenDebugServer() {
|
||||
mux.mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "image/svg+xml")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text x="50" y="50" text-anchor="middle" dominant-baseline="middle">🐙</text></svg>`)
|
||||
w.Write([]byte(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text x="50" y="50" text-anchor="middle" dominant-baseline="middle">🐙</text></svg>`))
|
||||
})
|
||||
|
||||
mux.HandleFunc("Auth block page", "GET", "/auth/block", AuthBlockPageHandler)
|
||||
mux.HandleFunc("Idlewatcher loading page", "GET", idlewatcherTypes.PathPrefix, idlewatcher.DebugHandler)
|
||||
apiHandler := newAPIHandler(mux)
|
||||
apiHandler := newApiHandler(mux)
|
||||
mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP)
|
||||
|
||||
mux.Finalize()
|
||||
|
||||
go func() {
|
||||
//nolint:gosec
|
||||
err := http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Expires", "0")
|
||||
mux.mux.ServeHTTP(w, r)
|
||||
}))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Error starting debug server")
|
||||
}
|
||||
}()
|
||||
go http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Expires", "0")
|
||||
mux.mux.ServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
func newAPIHandler(debugMux *debugMux) *gin.Engine {
|
||||
func newApiHandler(debugMux *debugMux) *gin.Engine {
|
||||
r := gin.New()
|
||||
r.Use(api.ErrorHandler())
|
||||
r.Use(api.ErrorLoggingMiddleware())
|
||||
|
||||
55
go.mod
55
go.mod
@@ -2,11 +2,6 @@ module github.com/yusing/godoxy
|
||||
|
||||
go 1.26.0
|
||||
|
||||
exclude (
|
||||
github.com/moby/moby/api v1.53.0 // allow older daemon versions
|
||||
github.com/moby/moby/client v0.2.2 // allow older daemon versions
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/coreos/go-oidc/v3 => ./internal/go-oidc
|
||||
github.com/luthermonson/go-proxmox => ./internal/go-proxmox
|
||||
@@ -19,13 +14,15 @@ replace (
|
||||
github.com/yusing/goutils/server => ./goutils/server
|
||||
)
|
||||
|
||||
exclude github.com/luthermonson/go-proxmox v0.3.0
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon; modify_html middleware
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // backoff for retrying operations
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
|
||||
github.com/docker/docker v28.5.2+incompatible // docker daemon
|
||||
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||
github.com/gin-gonic/gin v1.11.0 // api server
|
||||
github.com/go-acme/lego/v4 v4.32.0 // 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
|
||||
@@ -44,27 +41,25 @@ require (
|
||||
|
||||
require (
|
||||
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
|
||||
github.com/bytedance/sonic v1.15.0 // fast json parsing
|
||||
github.com/bytedance/sonic v1.15.0 // indirect; fast json parsing
|
||||
github.com/docker/cli v29.2.1+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.1 // jwt authentication
|
||||
github.com/luthermonson/go-proxmox v0.4.0 // 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/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/luthermonson/go-proxmox v0.3.2
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
github.com/quic-go/quic-go v0.59.0 // http3 support
|
||||
github.com/shirou/gopsutil/v4 v4.26.1 // 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.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-20260218101334-add7884a365e
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260218101334-add7884a365e
|
||||
github.com/yusing/godoxy/agent v0.0.0-20260216003355-b4a9f44f4ee9
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260216003355-b4a9f44f4ee9
|
||||
github.com/yusing/gointernals v0.2.0
|
||||
github.com/yusing/goutils v0.7.0
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260218062549-0b0fa3a059ec
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260218062549-0b0fa3a059ec
|
||||
github.com/yusing/goutils/server v0.0.0-20260218062549-0b0fa3a059ec
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260215081811-494ab85a33ae
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260215081811-494ab85a33ae
|
||||
github.com/yusing/goutils/server v0.0.0-20260215081811-494ab85a33ae
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -122,11 +117,11 @@ require (
|
||||
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/samber/slog-common v0.20.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.1 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
@@ -142,8 +137,8 @@ require (
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
google.golang.org/api v0.267.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||
google.golang.org/api v0.266.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||
google.golang.org/grpc v1.79.1 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||
@@ -151,15 +146,20 @@ require (
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require github.com/moby/moby/api v1.53.0
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/boombuler/barcode v1.1.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
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
@@ -169,28 +169,33 @@ require (
|
||||
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/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // 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.65.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pion/dtls/v3 v3.1.2 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/pquerna/otp v1.5.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.1 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // 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/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/vultr/govultr/v3 v3.27.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
golang.org/x/arch v0.24.0 // indirect
|
||||
)
|
||||
|
||||
60
go.sum
60
go.sum
@@ -23,6 +23,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourceg
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||
@@ -65,6 +67,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -78,6 +82,8 @@ 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.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
|
||||
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -100,8 +106,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.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
|
||||
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
|
||||
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=
|
||||
@@ -159,6 +165,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/server/v2 v2.9.0 h1:2zRCl28wkq0oc6YNbyJS2n0dDOOVvOS3Oez5AG2ij54=
|
||||
github.com/gotify/server/v2 v2.9.0/go.mod h1:249wwlUqHTr0QsiKARGtFVqds0pNLIMjYLinHyMACdQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
||||
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
@@ -195,8 +203,8 @@ github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7
|
||||
github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8=
|
||||
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-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
|
||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
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/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=
|
||||
@@ -214,23 +222,29 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
||||
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
|
||||
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
||||
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
|
||||
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
|
||||
github.com/nrdcg/goacmedns v0.2.0/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.108.2 h1:OWijzl3nHUApvTivl+3+78dbBwmyEHOnb+W9m6ixGbk=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 h1:9LsjN/zaIN7H8JE61NHpbWhxF0UGY96+kMlk3g8OvGU=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2/go.mod h1:32vZH06TuwZSn+IDMO1qcDvC2vHVlzUALCwXGWPA+dc=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1 h1:3oOIAQ9Fd2qTKTS/VlWmvKyBPKKhXBcCXjRZqOUypI4=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1 h1:2H75475moAv1hVVYlOk815KfqeiFCiQ7ovqn3OnN6FY=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1/go.mod h1:9HGOXiiQxcsG+4amgdr4xBIMq6IchdLW/nQDyZz07IE=
|
||||
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=
|
||||
@@ -255,6 +269,7 @@ github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG6
|
||||
github.com/pires/go-proxyproto v0.11.0/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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
|
||||
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
||||
@@ -312,8 +327,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
@@ -341,6 +356,10 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
@@ -349,6 +368,8 @@ go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4A
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
@@ -400,6 +421,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -447,14 +469,14 @@ golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0
|
||||
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.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
|
||||
google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
|
||||
google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk=
|
||||
google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
@@ -472,5 +494,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
|
||||
2
goutils
2
goutils
Submodule goutils updated: 3be815cb6e...494ab85a33
@@ -1,4 +1,4 @@
|
||||
# internal/acl
|
||||
# ACL (Access Control List)
|
||||
|
||||
Access control at the TCP connection level with IP/CIDR, timezone, and country-based filtering.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import "context"
|
||||
|
||||
type ContextKey struct{}
|
||||
|
||||
func SetCtx(ctx interface{ SetValue(key any, value any) }, acl ACL) {
|
||||
func SetCtx(ctx interface{ SetValue(any, any) }, acl ACL) {
|
||||
ctx.SetValue(ContextKey{}, acl)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# internal/agentpool
|
||||
# Agent Pool
|
||||
|
||||
Thread-safe pool for managing remote Docker agent connections.
|
||||
|
||||
|
||||
@@ -34,10 +34,7 @@ func newAgent(cfg *agent.AgentConfig) *Agent {
|
||||
if addr != agent.AgentHost+":443" {
|
||||
return nil, &net.AddrError{Err: "invalid address", Addr: addr}
|
||||
}
|
||||
dialer := &net.Dialer{
|
||||
Timeout: timeout,
|
||||
}
|
||||
return dialer.Dial("tcp", cfg.Addr)
|
||||
return net.DialTimeout("tcp", cfg.Addr, timeout)
|
||||
},
|
||||
TLSConfig: cfg.TLSConfig(),
|
||||
ReadTimeout: 5 * time.Second,
|
||||
|
||||
@@ -2,32 +2,32 @@ package agentpool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/valyala/fasthttp"
|
||||
agentPkg "github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/goutils/http/reverseproxy"
|
||||
)
|
||||
|
||||
func (agent *Agent) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, agentPkg.APIBaseURL+endpoint, body)
|
||||
func (cfg *Agent) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, agent.APIBaseURL+endpoint, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agent.httpClient.Do(req)
|
||||
return cfg.httpClient.Do(req)
|
||||
}
|
||||
|
||||
func (agent *Agent) Forward(req *http.Request, endpoint string) (*http.Response, error) {
|
||||
req.URL.Host = agentPkg.AgentHost
|
||||
func (cfg *Agent) Forward(req *http.Request, endpoint string) (*http.Response, error) {
|
||||
req.URL.Host = agent.AgentHost
|
||||
req.URL.Scheme = "https"
|
||||
req.URL.Path = agentPkg.APIEndpointBase + endpoint
|
||||
req.URL.Path = agent.APIEndpointBase + endpoint
|
||||
req.RequestURI = ""
|
||||
resp, err := agent.httpClient.Do(req)
|
||||
resp, err := cfg.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -40,20 +40,20 @@ type HealthCheckResponse struct {
|
||||
Latency time.Duration `json:"latency"`
|
||||
}
|
||||
|
||||
func (agent *Agent) DoHealthCheck(timeout time.Duration, query string) (ret HealthCheckResponse, err error) {
|
||||
func (cfg *Agent) DoHealthCheck(timeout time.Duration, query string) (ret HealthCheckResponse, err error) {
|
||||
req := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
req.SetRequestURI(agentPkg.APIBaseURL + agentPkg.EndpointHealth + "?" + query)
|
||||
req.SetRequestURI(agent.APIBaseURL + agent.EndpointHealth + "?" + query)
|
||||
req.Header.SetMethod(fasthttp.MethodGet)
|
||||
req.Header.Set("Accept-Encoding", "identity")
|
||||
req.SetConnectionClose()
|
||||
|
||||
start := time.Now()
|
||||
err = agent.fasthttpHcClient.DoTimeout(req, resp, timeout)
|
||||
err = cfg.fasthttpHcClient.DoTimeout(req, resp, timeout)
|
||||
ret.Latency = time.Since(start)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
@@ -63,7 +63,7 @@ func (agent *Agent) DoHealthCheck(timeout time.Duration, query string) (ret Heal
|
||||
ret.Detail = fmt.Sprintf("HTTP %d %s", status, resp.Body())
|
||||
return ret, nil
|
||||
} else {
|
||||
err = sonic.Unmarshal(resp.Body(), &ret)
|
||||
err = json.Unmarshal(resp.Body(), &ret)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
@@ -71,14 +71,14 @@ func (agent *Agent) DoHealthCheck(timeout time.Duration, query string) (ret Heal
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (agent *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
|
||||
transport := agent.Transport()
|
||||
func (cfg *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
|
||||
transport := cfg.Transport()
|
||||
dialer := websocket.Dialer{
|
||||
NetDialContext: transport.DialContext,
|
||||
NetDialTLSContext: transport.DialTLSContext,
|
||||
}
|
||||
return dialer.DialContext(ctx, agentPkg.APIBaseURL+endpoint, http.Header{
|
||||
"Host": {agentPkg.AgentHost},
|
||||
return dialer.DialContext(ctx, agent.APIBaseURL+endpoint, http.Header{
|
||||
"Host": {agent.AgentHost},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -86,9 +86,9 @@ func (agent *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.
|
||||
//
|
||||
// It will create a new request with the same context, method, and body, but with the agent host and scheme, and the endpoint
|
||||
// If the request has a query, it will be added to the proxy request's URL
|
||||
func (agent *Agent) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) {
|
||||
rp := reverseproxy.NewReverseProxy("agent", agentPkg.AgentURL, agent.Transport())
|
||||
req.URL.Host = agentPkg.AgentHost
|
||||
func (cfg *Agent) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) {
|
||||
rp := reverseproxy.NewReverseProxy("agent", agent.AgentURL, cfg.Transport())
|
||||
req.URL.Host = agent.AgentHost
|
||||
req.URL.Scheme = "https"
|
||||
req.URL.Path = endpoint
|
||||
req.RequestURI = ""
|
||||
|
||||
@@ -2,10 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog/log"
|
||||
apiV1 "github.com/yusing/godoxy/internal/api/v1"
|
||||
@@ -49,8 +47,6 @@ func NewHandler(requireAuth bool) *gin.Engine {
|
||||
r.Use(ErrorLoggingMiddleware())
|
||||
r.Use(NoCache())
|
||||
|
||||
log.Debug().Msg("gin codec json.API: " + reflect.TypeOf(json.API).Name())
|
||||
|
||||
r.GET("/api/v1/version", apiV1.Version)
|
||||
|
||||
if auth.IsEnabled() && requireAuth {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# internal/api/v1
|
||||
# API v1 Package
|
||||
|
||||
Implements the v1 REST API handlers for GoDoxy, exposing endpoints for managing routes, Docker containers, certificates, metrics, and system configuration.
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
@@ -43,22 +42,22 @@ func GetContainer(c *gin.Context) {
|
||||
|
||||
defer dockerClient.Close()
|
||||
|
||||
cont, err := dockerClient.ContainerInspect(c.Request.Context(), id, client.ContainerInspectOptions{})
|
||||
cont, err := dockerClient.ContainerInspect(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to inspect container"))
|
||||
return
|
||||
}
|
||||
|
||||
var state ContainerState
|
||||
if cont.Container.State != nil {
|
||||
state = cont.Container.State.Status
|
||||
if cont.State != nil {
|
||||
state = cont.State.Status
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &Container{
|
||||
Server: dockerCfg.URL,
|
||||
Name: cont.Container.Name,
|
||||
ID: cont.Container.ID,
|
||||
Image: cont.Container.Image,
|
||||
Name: cont.Name,
|
||||
ID: cont.ID,
|
||||
Image: cont.Image,
|
||||
State: state,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
|
||||
@@ -40,15 +39,15 @@ func Containers(c *gin.Context) {
|
||||
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, error) {
|
||||
errs := gperr.NewBuilder("failed to get containers")
|
||||
containers := make([]Container, 0)
|
||||
for name, dockerClient := range dockerClients {
|
||||
conts, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{All: true})
|
||||
for server, dockerClient := range dockerClients {
|
||||
conts, err := dockerClient.ContainerList(ctx, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
errs.AddSubject(err, name)
|
||||
errs.AddSubject(err, server)
|
||||
continue
|
||||
}
|
||||
for _, cont := range conts.Items {
|
||||
for _, cont := range conts {
|
||||
containers = append(containers, Container{
|
||||
Server: name,
|
||||
Server: server,
|
||||
Name: cont.Names[0],
|
||||
ID: cont.ID,
|
||||
Image: cont.Image,
|
||||
|
||||
@@ -4,9 +4,8 @@ import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
dockerSystem "github.com/docker/docker/api/types/system"
|
||||
"github.com/gin-gonic/gin"
|
||||
dockerSystem "github.com/moby/moby/api/types/system"
|
||||
"github.com/moby/moby/client"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
|
||||
@@ -65,13 +64,13 @@ func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerIn
|
||||
|
||||
i := 0
|
||||
for name, dockerClient := range dockerClients {
|
||||
info, err := dockerClient.Info(ctx, client.InfoOptions{})
|
||||
info, err := dockerClient.Info(ctx)
|
||||
if err != nil {
|
||||
errs.AddSubject(err, name)
|
||||
continue
|
||||
}
|
||||
info.Info.Name = name
|
||||
dockerInfos[i] = toDockerInfo(info.Info)
|
||||
info.Name = name
|
||||
dockerInfos[i] = toDockerInfo(info)
|
||||
i++
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/api/pkg/stdcopy"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
@@ -73,7 +73,7 @@ func Logs(c *gin.Context) {
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
opts := client.ContainerLogsOptions{
|
||||
opts := container.LogsOptions{
|
||||
ShowStdout: queryParams.Stdout,
|
||||
ShowStderr: queryParams.Stderr,
|
||||
Since: queryParams.Since,
|
||||
|
||||
@@ -4,23 +4,17 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type RestartRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
client.ContainerRestartOptions
|
||||
}
|
||||
|
||||
// @x-id "restart"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Restart container
|
||||
// @Description Restart container by container id
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param request body RestartRequest true "Request"
|
||||
// @Param request body StopRequest true "Request"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
@@ -28,7 +22,7 @@ type RestartRequest struct {
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/restart [post]
|
||||
func Restart(c *gin.Context) {
|
||||
var req RestartRequest
|
||||
var req StopRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
@@ -48,7 +42,7 @@ func Restart(c *gin.Context) {
|
||||
|
||||
defer client.Close()
|
||||
|
||||
_, err = client.ContainerRestart(c.Request.Context(), req.ID, req.ContainerRestartOptions)
|
||||
err = client.ContainerRestart(c.Request.Context(), req.ID, req.StopOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to restart container"))
|
||||
return
|
||||
|
||||
@@ -3,15 +3,15 @@ package dockerapi
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type StartRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
client.ContainerStartOptions
|
||||
container.StartOptions
|
||||
}
|
||||
|
||||
// @x-id "start"
|
||||
@@ -48,7 +48,7 @@ func Start(c *gin.Context) {
|
||||
|
||||
defer client.Close()
|
||||
|
||||
_, err = client.ContainerStart(c.Request.Context(), req.ID, req.ContainerStartOptions)
|
||||
err = client.ContainerStart(c.Request.Context(), req.ID, req.StartOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to start container"))
|
||||
return
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
@@ -68,7 +67,7 @@ func Stats(c *gin.Context) {
|
||||
defer dockerClient.Close()
|
||||
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
stats, err := dockerClient.ContainerStats(c.Request.Context(), id, client.ContainerStatsOptions{Stream: true})
|
||||
stats, err := dockerClient.ContainerStats(c.Request.Context(), id, true)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get container stats"))
|
||||
return
|
||||
@@ -102,7 +101,7 @@ func Stats(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
stats, err := dockerClient.ContainerStats(c.Request.Context(), id, client.ContainerStatsOptions{Stream: false})
|
||||
stats, err := dockerClient.ContainerStats(c.Request.Context(), id, false)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get container stats"))
|
||||
return
|
||||
|
||||
@@ -3,15 +3,15 @@ package dockerapi
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type StopRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
client.ContainerStopOptions
|
||||
container.StopOptions
|
||||
}
|
||||
|
||||
// @x-id "stop"
|
||||
@@ -48,7 +48,7 @@ func Stop(c *gin.Context) {
|
||||
|
||||
defer client.Close()
|
||||
|
||||
_, err = client.ContainerStop(c.Request.Context(), req.ID, req.ContainerStopOptions)
|
||||
err = client.ContainerStop(c.Request.Context(), req.ID, req.StopOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to stop container"))
|
||||
return
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/godoxy+yaml"
|
||||
],
|
||||
"tags": [
|
||||
@@ -2946,8 +2947,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-id": "list",
|
||||
"operationId": "list"
|
||||
"x-id": "routes",
|
||||
"operationId": "routes"
|
||||
}
|
||||
},
|
||||
"/route/playground": {
|
||||
@@ -4844,19 +4845,16 @@
|
||||
"properties": {
|
||||
"days": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"keep_size": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"last": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
}
|
||||
@@ -5093,6 +5091,11 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"isResponseRule": {
|
||||
"type": "boolean",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
@@ -6642,13 +6645,11 @@
|
||||
"x-omitempty": false
|
||||
},
|
||||
"fstype": {
|
||||
"description": "interned",
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"path": {
|
||||
"description": "interned",
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
@@ -6995,7 +6996,6 @@
|
||||
"x-omitempty": false
|
||||
},
|
||||
"name": {
|
||||
"description": "interned",
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
|
||||
@@ -767,13 +767,10 @@ definitions:
|
||||
LogRetention:
|
||||
properties:
|
||||
days:
|
||||
minimum: 0
|
||||
type: integer
|
||||
keep_size:
|
||||
minimum: 0
|
||||
type: integer
|
||||
last:
|
||||
minimum: 0
|
||||
type: integer
|
||||
type: object
|
||||
MetricsPeriod:
|
||||
@@ -891,6 +888,8 @@ definitions:
|
||||
properties:
|
||||
do:
|
||||
type: string
|
||||
isResponseRule:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
"on":
|
||||
@@ -1691,10 +1690,8 @@ definitions:
|
||||
free:
|
||||
type: integer
|
||||
fstype:
|
||||
description: interned
|
||||
type: string
|
||||
path:
|
||||
description: interned
|
||||
type: string
|
||||
total:
|
||||
type: number
|
||||
@@ -1889,7 +1886,6 @@ definitions:
|
||||
high:
|
||||
type: number
|
||||
name:
|
||||
description: interned
|
||||
type: string
|
||||
temperature:
|
||||
type: number
|
||||
@@ -2576,6 +2572,7 @@ paths:
|
||||
- FileTypeProvider
|
||||
- FileTypeMiddleware
|
||||
produces:
|
||||
- application/json
|
||||
- application/godoxy+yaml
|
||||
responses:
|
||||
"200":
|
||||
@@ -3936,7 +3933,7 @@ paths:
|
||||
tags:
|
||||
- route
|
||||
- websocket
|
||||
x-id: list
|
||||
x-id: routes
|
||||
/route/playground:
|
||||
post:
|
||||
consumes:
|
||||
|
||||
@@ -30,7 +30,7 @@ type GetFileContentRequest struct {
|
||||
// @Description Get file content
|
||||
// @Tags file
|
||||
// @Accept json
|
||||
// @Produce application/godoxy+yaml
|
||||
// @Produce json,application/godoxy+yaml
|
||||
// @Param query query GetFileContentRequest true "Request"
|
||||
// @Success 200 {string} application/godoxy+yaml "File content"
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
|
||||
@@ -4,12 +4,9 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/cenkalti/backoff/v5"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
@@ -37,11 +34,6 @@ type bytesFromPool struct {
|
||||
release func([]byte)
|
||||
}
|
||||
|
||||
type systemInfoData struct {
|
||||
agentName string
|
||||
systemInfo any
|
||||
}
|
||||
|
||||
// @x-id "all_system_info"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get system info
|
||||
@@ -79,19 +71,91 @@ func AllSystemInfo(c *gin.Context) {
|
||||
defer manager.Close()
|
||||
|
||||
query := c.Request.URL.Query()
|
||||
queryEncoded := query.Encode()
|
||||
queryEncoded := c.Request.URL.Query().Encode()
|
||||
|
||||
type SystemInfoData struct {
|
||||
AgentName string
|
||||
SystemInfo any
|
||||
}
|
||||
|
||||
// leave 5 extra slots for buffering in case new agents are added.
|
||||
dataCh := make(chan systemInfoData, 1+agentpool.Num()+5)
|
||||
dataCh := make(chan SystemInfoData, 1+agentpool.Num()+5)
|
||||
defer close(dataCh)
|
||||
|
||||
ticker := time.NewTicker(req.Interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
go streamSystemInfo(manager, dataCh)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-manager.Done():
|
||||
return
|
||||
case data := <-dataCh:
|
||||
err := marshalSystemInfo(manager, data.AgentName, data.SystemInfo)
|
||||
if err != nil {
|
||||
manager.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// processing function for one round.
|
||||
doRound := func() (bool, error) {
|
||||
var numErrs atomic.Int32
|
||||
|
||||
totalAgents := int32(1) // myself
|
||||
|
||||
var errs gperr.Group
|
||||
// get system info for me and all agents in parallel.
|
||||
errs.Go(func() error {
|
||||
data, err := systeminfo.Poller.GetRespData(req.Period, query)
|
||||
if err != nil {
|
||||
numErrs.Add(1)
|
||||
return gperr.PrependSubject(err, "Main server")
|
||||
}
|
||||
select {
|
||||
case <-manager.Done():
|
||||
return nil
|
||||
case dataCh <- SystemInfoData{
|
||||
AgentName: "GoDoxy",
|
||||
SystemInfo: data,
|
||||
}:
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
for _, a := range agentpool.Iter() {
|
||||
totalAgents++
|
||||
|
||||
errs.Go(func() error {
|
||||
data, err := getAgentSystemInfoWithRetry(manager.Context(), a, queryEncoded)
|
||||
if err != nil {
|
||||
numErrs.Add(1)
|
||||
return gperr.PrependSubject(err, "Agent "+a.Name)
|
||||
}
|
||||
select {
|
||||
case <-manager.Done():
|
||||
return nil
|
||||
case dataCh <- SystemInfoData{
|
||||
AgentName: a.Name,
|
||||
SystemInfo: data,
|
||||
}:
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
err := errs.Wait().Error()
|
||||
return numErrs.Load() == totalAgents, err
|
||||
}
|
||||
|
||||
// write system info immediately once.
|
||||
if hasSuccess, err := collectSystemInfoRound(manager, req, query, queryEncoded, dataCh); handleRoundResult(c, hasSuccess, err, false) {
|
||||
return
|
||||
if shouldContinue, err := doRound(); err != nil {
|
||||
if !shouldContinue {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// then continue on the ticker.
|
||||
@@ -100,95 +164,17 @@ func AllSystemInfo(c *gin.Context) {
|
||||
case <-manager.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if hasSuccess, err := collectSystemInfoRound(manager, req, query, queryEncoded, dataCh); handleRoundResult(c, hasSuccess, err, true) {
|
||||
return
|
||||
if shouldContinue, err := doRound(); err != nil {
|
||||
if !shouldContinue {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
|
||||
return
|
||||
}
|
||||
log.Warn().Err(err).Msg("failed to get some system info")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func streamSystemInfo(manager *websocket.Manager, dataCh <-chan systemInfoData) {
|
||||
for {
|
||||
select {
|
||||
case <-manager.Done():
|
||||
return
|
||||
case data := <-dataCh:
|
||||
err := marshalSystemInfo(manager, data.agentName, data.systemInfo)
|
||||
if err != nil {
|
||||
manager.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func queueSystemInfo(manager *websocket.Manager, dataCh chan<- systemInfoData, data systemInfoData) {
|
||||
select {
|
||||
case <-manager.Done():
|
||||
case dataCh <- data:
|
||||
}
|
||||
}
|
||||
|
||||
func collectSystemInfoRound(
|
||||
manager *websocket.Manager,
|
||||
req AllSystemInfoRequest,
|
||||
query url.Values,
|
||||
queryEncoded string,
|
||||
dataCh chan<- systemInfoData,
|
||||
) (hasSuccess bool, err error) {
|
||||
var numErrs atomic.Int32
|
||||
totalAgents := int32(1) // myself
|
||||
|
||||
var errs gperr.Group
|
||||
// get system info for me and all agents in parallel.
|
||||
errs.Go(func() error {
|
||||
data, err := systeminfo.Poller.GetRespData(req.Period, query)
|
||||
if err != nil {
|
||||
numErrs.Add(1)
|
||||
return gperr.PrependSubject(err, "Main server")
|
||||
}
|
||||
queueSystemInfo(manager, dataCh, systemInfoData{
|
||||
agentName: "GoDoxy",
|
||||
systemInfo: data,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
||||
for _, a := range agentpool.Iter() {
|
||||
totalAgents++
|
||||
|
||||
errs.Go(func() error {
|
||||
data, err := getAgentSystemInfoWithRetry(manager.Context(), a, queryEncoded)
|
||||
if err != nil {
|
||||
numErrs.Add(1)
|
||||
return gperr.PrependSubject(err, "Agent "+a.Name)
|
||||
}
|
||||
queueSystemInfo(manager, dataCh, systemInfoData{
|
||||
agentName: a.Name,
|
||||
systemInfo: data,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
err = errs.Wait().Error()
|
||||
return numErrs.Load() < totalAgents, err
|
||||
}
|
||||
|
||||
func handleRoundResult(c *gin.Context, hasSuccess bool, err error, logPartial bool) (stop bool) {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if !hasSuccess {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
|
||||
return true
|
||||
}
|
||||
if logPartial {
|
||||
log.Warn().Err(err).Msg("failed to get some system info")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getAgentSystemInfo(ctx context.Context, a *agentpool.Agent, query string) (bytesFromPool, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
@@ -210,26 +196,35 @@ func getAgentSystemInfo(ctx context.Context, a *agentpool.Agent, query string) (
|
||||
|
||||
func getAgentSystemInfoWithRetry(ctx context.Context, a *agentpool.Agent, query string) (bytesFromPool, error) {
|
||||
const maxRetries = 3
|
||||
const retryDelay = 5 * time.Second
|
||||
var attempt int
|
||||
data, err := backoff.Retry(ctx, func() (bytesFromPool, error) {
|
||||
attempt++
|
||||
var lastErr error
|
||||
|
||||
for attempt := range maxRetries {
|
||||
// Apply backoff delay for retries (not for first attempt)
|
||||
if attempt > 0 {
|
||||
delay := max((1<<attempt)*time.Second, 5*time.Second)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return bytesFromPool{}, ctx.Err()
|
||||
case <-time.After(delay):
|
||||
}
|
||||
}
|
||||
|
||||
data, err := getAgentSystemInfo(ctx, a, query)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
log.Err(err).Str("agent", a.Name).Int("attempt", attempt).Msg("Agent request attempt failed")
|
||||
return bytesFromPool{}, err
|
||||
},
|
||||
backoff.WithBackOff(backoff.NewConstantBackOff(retryDelay)),
|
||||
backoff.WithMaxTries(maxRetries),
|
||||
)
|
||||
if err != nil {
|
||||
return bytesFromPool{}, err
|
||||
lastErr = err
|
||||
|
||||
log.Debug().Str("agent", a.Name).Int("attempt", attempt+1).Str("error", err.Error()).Msg("Agent request attempt failed")
|
||||
|
||||
// Don't retry on context cancellation
|
||||
if ctx.Err() != nil {
|
||||
return bytesFromPool{}, ctx.Err()
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
|
||||
return bytesFromPool{}, lastErr
|
||||
}
|
||||
|
||||
func marshalSystemInfo(ws *websocket.Manager, agentName string, systemInfo any) error {
|
||||
@@ -241,7 +236,7 @@ func marshalSystemInfo(ws *websocket.Manager, agentName string, systemInfo any)
|
||||
defer bufFromPool.release(bufFromPool.RawMessage)
|
||||
}
|
||||
|
||||
err := sonic.ConfigDefault.NewEncoder(buf).Encode(map[string]any{
|
||||
err := json.NewEncoder(buf).Encode(map[string]any{
|
||||
agentName: systemInfo,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -16,10 +15,10 @@ import (
|
||||
// e.g. ws://localhost:8889/api/v1/proxmox/journalctl/pve/127?service=pveproxy&service=pvedaemon&limit=10
|
||||
|
||||
type JournalctlRequest struct {
|
||||
Node string `form:"node" uri:"node" binding:"required"` // Node name
|
||||
VMID *int `form:"vmid" uri:"vmid"` // Container VMID (optional - if not provided, streams node journalctl)
|
||||
Services []string `form:"service" uri:"service"` // Service names
|
||||
Limit *int `form:"limit" uri:"limit" default:"100" binding:"omitempty,min=1,max=1000"` // Limit output lines (1-1000)
|
||||
Node string `form:"node" uri:"node" binding:"required"` // Node name
|
||||
VMID *int `form:"vmid" uri:"vmid"` // Container VMID (optional - if not provided, streams node journalctl)
|
||||
Services []string `form:"service" uri:"service"` // Service names
|
||||
Limit *int `form:"limit" uri:"limit" default:"100" binding:"min=1,max=1000"` // Limit output lines (1-1000)
|
||||
} // @name ProxmoxJournalctlRequest
|
||||
|
||||
// @x-id "journalctl"
|
||||
@@ -41,11 +40,6 @@ type JournalctlRequest struct {
|
||||
// @Router /proxmox/journalctl/{node}/{vmid} [get]
|
||||
// @Router /proxmox/journalctl/{node}/{vmid}/{service} [get]
|
||||
func Journalctl(c *gin.Context) {
|
||||
if !httpheaders.IsWebsocket(c.Request.Header) {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("websocket required"))
|
||||
return
|
||||
}
|
||||
|
||||
var request JournalctlRequest
|
||||
uriErr := c.ShouldBindUri(&request)
|
||||
queryErr := c.ShouldBindQuery(&request)
|
||||
@@ -54,10 +48,6 @@ func Journalctl(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if request.Limit == nil {
|
||||
request.Limit = new(100)
|
||||
}
|
||||
|
||||
node, ok := proxmox.Nodes.Get(request.Node)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -35,11 +34,6 @@ type TailRequest struct {
|
||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||
// @Router /proxmox/tail [get]
|
||||
func Tail(c *gin.Context) {
|
||||
if !httpheaders.IsWebsocket(c.Request.Header) {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("websocket required"))
|
||||
return
|
||||
}
|
||||
|
||||
var request TailRequest
|
||||
if err := c.ShouldBindQuery(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
|
||||
@@ -64,6 +64,7 @@ type ParsedRule struct {
|
||||
On string `json:"on"`
|
||||
Do string `json:"do"`
|
||||
ValidationError error `json:"validationError,omitempty"` // we need the structured error, not the plain string
|
||||
IsResponseRule bool `json:"isResponseRule"`
|
||||
} // @name ParsedRule
|
||||
|
||||
type FinalRequest struct {
|
||||
@@ -297,6 +298,7 @@ func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, error) {
|
||||
On: onStr,
|
||||
Do: doStr,
|
||||
ValidationError: validationErr,
|
||||
IsResponseRule: rule.IsResponseRule(),
|
||||
})
|
||||
|
||||
// Only add valid rules to execution list
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
type RouteType route.Route // @name Route
|
||||
|
||||
// @x-id "list"
|
||||
// @x-id "routes"
|
||||
// @BasePath /api/v1
|
||||
// @Summary List routes
|
||||
// @Description List routes
|
||||
@@ -1,4 +1,4 @@
|
||||
# internal/auth
|
||||
# Authentication
|
||||
|
||||
Authentication providers supporting OIDC and username/password authentication with JWT-based sessions.
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
@@ -107,7 +107,7 @@ type UserPassAuthCallbackRequest struct {
|
||||
|
||||
func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var creds UserPassAuthCallbackRequest
|
||||
err := sonic.ConfigDefault.NewDecoder(r.Body).Decode(&creds)
|
||||
err := json.NewDecoder(r.Body).Decode(&creds)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -69,12 +70,12 @@ func cookieDomain(r *http.Request) string {
|
||||
}
|
||||
}
|
||||
|
||||
parts := strings.Split(reqHost, ".")
|
||||
parts := strutils.SplitRune(reqHost, '.')
|
||||
if len(parts) < 2 {
|
||||
return ""
|
||||
}
|
||||
parts[0] = ""
|
||||
return strings.Join(parts, ".")
|
||||
return strutils.JoinRune(parts, '.')
|
||||
}
|
||||
|
||||
func SetTokenCookie(w http.ResponseWriter, r *http.Request, name, value string, ttl time.Duration) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# internal/autocert
|
||||
# Autocert Package
|
||||
|
||||
Automated SSL certificate management using the ACME protocol (Let's Encrypt and compatible CAs).
|
||||
|
||||
|
||||
@@ -23,35 +23,33 @@ import (
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
)
|
||||
|
||||
type (
|
||||
ConfigExtra Config
|
||||
Config struct {
|
||||
Email string `json:"email,omitempty"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
CertPath string `json:"cert_path,omitempty"`
|
||||
KeyPath string `json:"key_path,omitempty"`
|
||||
Extra []ConfigExtra `json:"extra,omitempty"`
|
||||
ACMEKeyPath string `json:"acme_key_path,omitempty"` // shared by all extra providers with the same CA directory URL
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Options map[string]strutils.Redacted `json:"options,omitempty"`
|
||||
type ConfigExtra Config
|
||||
type Config struct {
|
||||
Email string `json:"email,omitempty"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
CertPath string `json:"cert_path,omitempty"`
|
||||
KeyPath string `json:"key_path,omitempty"`
|
||||
Extra []ConfigExtra `json:"extra,omitempty"`
|
||||
ACMEKeyPath string `json:"acme_key_path,omitempty"` // shared by all extra providers with the same CA directory URL
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Options map[string]strutils.Redacted `json:"options,omitempty"`
|
||||
|
||||
Resolvers []string `json:"resolvers,omitempty"`
|
||||
Resolvers []string `json:"resolvers,omitempty"`
|
||||
|
||||
// Custom ACME CA
|
||||
CADirURL string `json:"ca_dir_url,omitempty"`
|
||||
CACerts []string `json:"ca_certs,omitempty"`
|
||||
// Custom ACME CA
|
||||
CADirURL string `json:"ca_dir_url,omitempty"`
|
||||
CACerts []string `json:"ca_certs,omitempty"`
|
||||
|
||||
// EAB
|
||||
EABKid string `json:"eab_kid,omitempty" validate:"required_with=EABHmac"`
|
||||
EABHmac string `json:"eab_hmac,omitempty" validate:"required_with=EABKid"` // base64 encoded
|
||||
// EAB
|
||||
EABKid string `json:"eab_kid,omitempty" validate:"required_with=EABHmac"`
|
||||
EABHmac string `json:"eab_hmac,omitempty" validate:"required_with=EABKid"` // base64 encoded
|
||||
|
||||
HTTPClient *http.Client `json:"-"` // for tests only
|
||||
HTTPClient *http.Client `json:"-"` // for tests only
|
||||
|
||||
challengeProvider challenge.Provider
|
||||
challengeProvider challenge.Provider
|
||||
|
||||
idx int // 0: main, 1+: extra[i]
|
||||
}
|
||||
)
|
||||
idx int // 0: main, 1+: extra[i]
|
||||
}
|
||||
|
||||
var (
|
||||
ErrMissingField = gperr.New("missing field")
|
||||
|
||||
@@ -68,7 +68,7 @@ func TestMultipleCertificatesLifecycle(t *testing.T) {
|
||||
require.Equal(t, "custom", cfg.Extra[1].Provider)
|
||||
|
||||
/* track cert requests for all configs */
|
||||
os.MkdirAll("certs", 0o755)
|
||||
os.MkdirAll("certs", 0755)
|
||||
defer os.RemoveAll("certs")
|
||||
|
||||
err = provider.ObtainCertIfNotExistsAll()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# internal/config
|
||||
# Configuration Management
|
||||
|
||||
Centralized YAML configuration management with thread-safe state access and provider initialization.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# internal/config/query
|
||||
# Configuration Query
|
||||
|
||||
Read-only access to the active configuration state, including route providers and system statistics.
|
||||
|
||||
@@ -149,7 +149,7 @@ No metrics are currently exposed.
|
||||
## Performance Characteristics
|
||||
|
||||
- O(n) where n is number of providers for provider queries
|
||||
- O(n \* m) where m is routes per provider for route search
|
||||
- O(n * m) where m is routes per provider for route search
|
||||
- O(n) for statistics aggregation
|
||||
- No locking required (uses atomic load)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# internal/dnsproviders
|
||||
# DNS Providers
|
||||
|
||||
DNS provider integrations for Let's Encrypt certificate management via the lego library.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ go 1.26.0
|
||||
replace github.com/yusing/godoxy => ../..
|
||||
|
||||
require (
|
||||
github.com/go-acme/lego/v4 v4.32.0
|
||||
github.com/go-acme/lego/v4 v4.31.0
|
||||
github.com/yusing/godoxy v0.26.0
|
||||
)
|
||||
|
||||
@@ -23,12 +23,8 @@ require (
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||
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.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
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
@@ -53,7 +49,6 @@ require (
|
||||
github.com/gotify/server/v2 v2.9.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
@@ -65,8 +60,8 @@ require (
|
||||
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.108.2 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.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
|
||||
@@ -78,7 +73,6 @@ require (
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/vultr/govultr/v3 v3.27.0 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/yusing/gointernals v0.2.0 // indirect
|
||||
@@ -89,7 +83,6 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
golang.org/x/arch v0.24.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
@@ -98,8 +91,8 @@ require (
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
google.golang.org/api v0.267.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||
google.golang.org/api v0.266.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||
google.golang.org/grpc v1.79.1 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||
|
||||
@@ -37,18 +37,10 @@ github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||
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.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=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -62,8 +54,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.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
|
||||
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
|
||||
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=
|
||||
@@ -119,8 +111,6 @@ github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2
|
||||
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
||||
github.com/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/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -150,10 +140,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.108.2 h1:OWijzl3nHUApvTivl+3+78dbBwmyEHOnb+W9m6ixGbk=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 h1:9LsjN/zaIN7H8JE61NHpbWhxF0UGY96+kMlk3g8OvGU=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2/go.mod h1:32vZH06TuwZSn+IDMO1qcDvC2vHVlzUALCwXGWPA+dc=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1 h1:3oOIAQ9Fd2qTKTS/VlWmvKyBPKKhXBcCXjRZqOUypI4=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1 h1:2H75475moAv1hVVYlOk815KfqeiFCiQ7ovqn3OnN6FY=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1/go.mod h1:9HGOXiiQxcsG+4amgdr4xBIMq6IchdLW/nQDyZz07IE=
|
||||
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=
|
||||
@@ -188,11 +178,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8=
|
||||
github.com/vultr/govultr/v3 v3.27.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
@@ -221,8 +208,6 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/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.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
||||
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
@@ -249,14 +234,14 @@ golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
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.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
|
||||
google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
|
||||
google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk=
|
||||
google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# internal/docker
|
||||
# Docker Integration
|
||||
|
||||
Docker container discovery, connection management, and label-based route configuration.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/agentpool"
|
||||
@@ -198,7 +198,9 @@ func NewClient(cfg types.DockerProviderConfig, unique ...bool) (*SharedClient, e
|
||||
opt = append(opt, client.WithTLSClientConfig(cfg.TLS.CAFile, cfg.TLS.CertFile, cfg.TLS.KeyFile))
|
||||
}
|
||||
|
||||
client, err := client.New(opt...)
|
||||
opt = append(opt, client.WithAPIVersionNegotiation())
|
||||
|
||||
client, err := client.NewClientWithOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -11,9 +11,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/agentpool"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
@@ -99,18 +98,18 @@ func UpdatePorts(ctx context.Context, c *types.Container) error {
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
inspect, err := dockerClient.ContainerInspect(ctx, c.ContainerID, client.ContainerInspectOptions{})
|
||||
inspect, err := dockerClient.ContainerInspect(ctx, c.ContainerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for port := range inspect.Container.Config.ExposedPorts {
|
||||
proto, portStr := nat.SplitProtoPort(port.String())
|
||||
for port := range inspect.Config.ExposedPorts {
|
||||
proto, portStr := nat.SplitProtoPort(string(port))
|
||||
portInt, _ := nat.ParsePort(portStr)
|
||||
if portInt == 0 {
|
||||
continue
|
||||
}
|
||||
c.PublicPortMapping[portInt] = container.PortSummary{
|
||||
c.PublicPortMapping[portInt] = container.Port{
|
||||
PublicPort: uint16(portInt), //nolint:gosec
|
||||
PrivatePort: uint16(portInt), //nolint:gosec
|
||||
Type: proto,
|
||||
@@ -211,8 +210,8 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
|
||||
}
|
||||
if c.Network != "" {
|
||||
v, hasNetwork := helper.NetworkSettings.Networks[c.Network]
|
||||
if hasNetwork && v.IPAddress.IsValid() {
|
||||
c.PrivateHostname = v.IPAddress.String()
|
||||
if hasNetwork && v.IPAddress != "" {
|
||||
c.PrivateHostname = v.IPAddress
|
||||
return
|
||||
}
|
||||
var hasComposeNetwork bool
|
||||
@@ -220,9 +219,9 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
|
||||
if proj := DockerComposeProject(c); proj != "" {
|
||||
newNetwork := fmt.Sprintf("%s_%s", proj, c.Network)
|
||||
v, hasComposeNetwork = helper.NetworkSettings.Networks[newNetwork]
|
||||
if hasComposeNetwork && v.IPAddress.IsValid() {
|
||||
if hasComposeNetwork && v.IPAddress != "" {
|
||||
c.Network = newNetwork // update network to the new one
|
||||
c.PrivateHostname = v.IPAddress.String()
|
||||
c.PrivateHostname = v.IPAddress
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -235,9 +234,9 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
|
||||
}
|
||||
// fallback to first network if no network is specified
|
||||
for k, v := range helper.NetworkSettings.Networks {
|
||||
if v.IPAddress.IsValid() {
|
||||
if v.IPAddress != "" {
|
||||
c.Network = k // update network to the first network
|
||||
c.PrivateHostname = v.IPAddress.String()
|
||||
c.PrivateHostname = v.IPAddress
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package docker
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/yusing/ds/ordered"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
@@ -46,8 +46,8 @@ func (c containerHelper) getMounts() *ordered.Map[string, string] {
|
||||
}
|
||||
|
||||
func (c containerHelper) parseImage() *types.ContainerImage {
|
||||
colonSep := strings.Split(c.Image, ":")
|
||||
slashSep := strings.Split(colonSep[0], "/")
|
||||
colonSep := strutils.SplitRune(c.Image, ':')
|
||||
slashSep := strutils.SplitRune(colonSep[0], '/')
|
||||
_, sha256, _ := strings.Cut(c.ImageID, ":")
|
||||
im := &types.ContainerImage{
|
||||
SHA256: sha256,
|
||||
|
||||
@@ -3,7 +3,7 @@ package docker
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
)
|
||||
|
||||
var ErrInvalidLabel = errors.New("invalid label")
|
||||
@@ -30,7 +31,7 @@ func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, e
|
||||
ExpandWildcard(labels, aliases...)
|
||||
|
||||
for lbl, value := range labels {
|
||||
parts := strings.Split(lbl, ".")
|
||||
parts := strutils.SplitRune(lbl, '.')
|
||||
if parts[0] != NSProxy {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ package docker
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
)
|
||||
|
||||
var listOptions = client.ContainerListOptions{
|
||||
var listOptions = container.ListOptions{
|
||||
// created|restarting|running|removing|paused|exited|dead
|
||||
// Filters: filters.NewArgs(
|
||||
// filters.Arg("status", "created"),
|
||||
@@ -31,7 +31,7 @@ func ListContainers(ctx context.Context, dockerCfg types.DockerProviderConfig) (
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return containers.Items, nil
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func IsErrConnectionFailed(err error) bool {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# internal/entrypoint
|
||||
# Entrypoint
|
||||
|
||||
The entrypoint package provides the main HTTP entry point for GoDoxy, handling domain-based routing, middleware application, short link matching, access logging, and HTTP server lifecycle management.
|
||||
|
||||
|
||||
@@ -162,9 +162,10 @@ func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.Request
|
||||
}
|
||||
|
||||
func findRouteAnyDomain(routes HTTPRoutes, host string) types.HTTPRoute {
|
||||
before, _, ok := strings.Cut(host, ".")
|
||||
if ok {
|
||||
target := before
|
||||
//nolint:modernize
|
||||
idx := strings.IndexByte(host, '.')
|
||||
if idx != -1 {
|
||||
target := host[:idx]
|
||||
if r, ok := routes.Get(target); ok {
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
type ContextKey struct{}
|
||||
|
||||
func SetCtx(ctx interface{ SetValue(key any, value any) }, ep Entrypoint) {
|
||||
func SetCtx(ctx interface{ SetValue(any, any) }, ep Entrypoint) {
|
||||
ctx.SetValue(ContextKey{}, ep)
|
||||
}
|
||||
|
||||
|
||||
Submodule internal/go-oidc updated: 15707eda85...bcfa54222d
Submodule internal/go-proxmox updated: d13c6c346d...7a07c21f07
Submodule internal/gopsutil updated: edfb472ac5...9532b08add
@@ -1,4 +1,4 @@
|
||||
# internal/health/check
|
||||
# Health Check Package
|
||||
|
||||
Low-level health check implementations for different protocols and services in GoDoxy.
|
||||
|
||||
|
||||
@@ -2,13 +2,12 @@ package healthcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
@@ -46,7 +45,7 @@ func Docker(ctx context.Context, state *DockerHealthcheckState, timeout time.Dur
|
||||
defer cancel()
|
||||
|
||||
// the actual inspect response is intercepted and returned as RequestInterceptedError
|
||||
_, err := state.client.ContainerInspect(ctx, state.containerID, client.ContainerInspectOptions{})
|
||||
_, err := state.client.ContainerInspect(ctx, state.containerID)
|
||||
|
||||
var interceptedErr *httputils.RequestInterceptedError
|
||||
if !httputils.AsRequestInterceptedError(err, &interceptedErr) {
|
||||
@@ -108,7 +107,7 @@ func interceptDockerInspectResponse(resp *http.Response) (intercepted bool, err
|
||||
}
|
||||
|
||||
var state container.State
|
||||
err = sonic.Unmarshal(body, &state)
|
||||
err = json.Unmarshal(body, &state)
|
||||
release(body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
||||
@@ -24,13 +24,12 @@ var h2cClient = &http.Client{
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var pinger = &fasthttp.Client{
|
||||
MaxConnDuration: 0,
|
||||
DisableHeaderNamesNormalizing: true,
|
||||
DisablePathNormalizing: true,
|
||||
TLSConfig: &tls.Config{
|
||||
InsecureSkipVerify: true, //nolint:gosec
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
MaxConnsPerHost: 1000,
|
||||
NoDefaultUserAgentHeader: true,
|
||||
@@ -52,7 +51,7 @@ func HTTP(url *url.URL, method, path string, timeout time.Duration) (types.Healt
|
||||
respErr := pinger.DoTimeout(req, resp, timeout)
|
||||
lat := time.Since(start)
|
||||
|
||||
return processHealthResponse(lat, respErr, resp.StatusCode), nil
|
||||
return processHealthResponse(lat, respErr, resp.StatusCode)
|
||||
}
|
||||
|
||||
func H2C(ctx context.Context, url *url.URL, method, path string, timeout time.Duration) (types.HealthCheckResult, error) {
|
||||
@@ -88,7 +87,7 @@ func H2C(ctx context.Context, url *url.URL, method, path string, timeout time.Du
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
return processHealthResponse(lat, err, func() int { return resp.StatusCode }), nil
|
||||
return processHealthResponse(lat, err, func() int { return resp.StatusCode })
|
||||
}
|
||||
|
||||
var userAgent = "GoDoxy/" + version.Get().String()
|
||||
@@ -101,20 +100,20 @@ func setCommonHeaders(setHeader func(key, value string)) {
|
||||
setHeader("Pragma", "no-cache")
|
||||
}
|
||||
|
||||
func processHealthResponse(lat time.Duration, err error, getStatusCode func() int) types.HealthCheckResult {
|
||||
func processHealthResponse(lat time.Duration, err error, getStatusCode func() int) (types.HealthCheckResult, error) {
|
||||
if err != nil {
|
||||
var tlsErr *tls.CertificateVerificationError
|
||||
if ok := errors.As(err, &tlsErr); !ok {
|
||||
return types.HealthCheckResult{
|
||||
Latency: lat,
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
return types.HealthCheckResult{
|
||||
Latency: lat,
|
||||
Healthy: true,
|
||||
Detail: tlsErr.Error(),
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
statusCode := getStatusCode()
|
||||
@@ -122,11 +121,11 @@ func processHealthResponse(lat time.Duration, err error, getStatusCode func() in
|
||||
return types.HealthCheckResult{
|
||||
Latency: lat,
|
||||
Detail: http.StatusText(statusCode),
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
return types.HealthCheckResult{
|
||||
Latency: lat,
|
||||
Healthy: true,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# internal/health/monitor
|
||||
# Health Monitor Package
|
||||
|
||||
Route health monitoring with configurable check intervals, retry policies, and notification integration.
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -200,7 +199,7 @@ func (mon *monitor) Detail() string {
|
||||
|
||||
// Name implements HealthMonitor.
|
||||
func (mon *monitor) Name() string {
|
||||
parts := strings.Split(mon.service, "/")
|
||||
parts := strutils.SplitRune(mon.service, '/')
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
|
||||
@@ -245,11 +244,9 @@ func (mon *monitor) checkUpdateHealth() error {
|
||||
// change of status
|
||||
if result.Healthy != (lastStatus == types.StatusHealthy) {
|
||||
if result.Healthy {
|
||||
mon.notifyServiceUp(&logger, &result)
|
||||
mon.numConsecFailures.Store(0)
|
||||
if mon.downNotificationSent.Load() { // Only notify if the service down has been notified
|
||||
mon.notifyServiceUp(&logger, &result)
|
||||
mon.downNotificationSent.Store(false) // Reset notification state when service comes back up
|
||||
}
|
||||
mon.downNotificationSent.Store(false) // Reset notification state when service comes back up
|
||||
} else if mon.config.Retries < 0 {
|
||||
// immediate notification when retries < 0
|
||||
mon.notifyServiceDown(&logger, &result)
|
||||
|
||||
@@ -171,12 +171,12 @@ func TestNotification_ServiceRecoversBeforeThreshold(t *testing.T) {
|
||||
err = mon.checkUpdateHealth()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should have no notifications because threshold was never met.
|
||||
// Recovery notification is only sent after a down notification was sent.
|
||||
// Should have 1 up notification, but no down notification
|
||||
// because threshold was never met
|
||||
up, down, last := tracker.getStats()
|
||||
require.Equal(t, 0, down)
|
||||
require.Equal(t, 0, up)
|
||||
require.Empty(t, last)
|
||||
require.Equal(t, 1, up)
|
||||
require.Equal(t, "up", last)
|
||||
}
|
||||
|
||||
func TestNotification_ConsecutiveFailureReset(t *testing.T) {
|
||||
@@ -210,11 +210,10 @@ func TestNotification_ConsecutiveFailureReset(t *testing.T) {
|
||||
err = mon.checkUpdateHealth()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should have no notifications, consecutive failures should reset.
|
||||
// Recovery notification is only sent after a down notification was sent.
|
||||
// Should have 1 up notification, consecutive failures should reset
|
||||
up, down, _ := tracker.getStats()
|
||||
require.Equal(t, 0, down)
|
||||
require.Equal(t, 0, up)
|
||||
require.Equal(t, 1, up)
|
||||
|
||||
// Go down again - consecutive counter should start from 0
|
||||
mon.checkHealth = func(u *url.URL) (types.HealthCheckResult, error) {
|
||||
@@ -228,7 +227,7 @@ func TestNotification_ConsecutiveFailureReset(t *testing.T) {
|
||||
// Should still have no down notifications (need 2 consecutive)
|
||||
up, down, _ = tracker.getStats()
|
||||
require.Equal(t, 0, down)
|
||||
require.Equal(t, 0, up)
|
||||
require.Equal(t, 1, up)
|
||||
|
||||
// Second consecutive failure - should trigger notification
|
||||
err = mon.checkUpdateHealth()
|
||||
@@ -237,7 +236,7 @@ func TestNotification_ConsecutiveFailureReset(t *testing.T) {
|
||||
// Now should have down notification
|
||||
up, down, last := tracker.getStats()
|
||||
require.Equal(t, 1, down)
|
||||
require.Equal(t, 0, up)
|
||||
require.Equal(t, 1, up)
|
||||
require.Equal(t, "down", last)
|
||||
}
|
||||
|
||||
@@ -280,11 +279,11 @@ func TestNotification_ContextCancellation(t *testing.T) {
|
||||
require.Equal(t, 0, up)
|
||||
}
|
||||
|
||||
func TestImmediateUpNotificationAfterDownNotification(t *testing.T) {
|
||||
func TestImmediateUpNotification(t *testing.T) {
|
||||
config := types.HealthCheckConfig{
|
||||
Interval: 100 * time.Millisecond,
|
||||
Timeout: 50 * time.Millisecond,
|
||||
Retries: 2,
|
||||
Retries: 2, // NotifyAfter should not affect up notifications
|
||||
}
|
||||
|
||||
mon, tracker := createTestMonitor(config, func(u *url.URL) (types.HealthCheckResult, error) {
|
||||
@@ -293,7 +292,6 @@ func TestImmediateUpNotificationAfterDownNotification(t *testing.T) {
|
||||
|
||||
// Start unhealthy
|
||||
mon.status.Store(types.StatusUnhealthy)
|
||||
mon.downNotificationSent.Store(true)
|
||||
|
||||
// Set to healthy
|
||||
mon.checkHealth = func(u *url.URL) (types.HealthCheckResult, error) {
|
||||
@@ -304,7 +302,7 @@ func TestImmediateUpNotificationAfterDownNotification(t *testing.T) {
|
||||
err := mon.checkUpdateHealth()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Up notification should happen immediately once a prior down notification exists.
|
||||
// Up notification should happen immediately regardless of NotifyAfter setting
|
||||
require.Equal(t, types.StatusHealthy, mon.Status())
|
||||
|
||||
// Should have exactly 1 up notification immediately
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# internal/homepage
|
||||
# Homepage
|
||||
|
||||
The homepage package provides the GoDoxy WebUI dashboard with support for categories, favorites, widgets, dynamic item configuration, and icon management.
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ import (
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func TestOverrideItem(t *testing.T) {
|
||||
a := &Item{
|
||||
Alias: "foo",
|
||||
@@ -16,7 +20,7 @@ func TestOverrideItem(t *testing.T) {
|
||||
Show: false,
|
||||
Name: "Foo",
|
||||
Icon: &icons.URL{
|
||||
FullURL: new("/favicon.ico"),
|
||||
FullURL: strPtr("/favicon.ico"),
|
||||
Source: icons.SourceRelative,
|
||||
},
|
||||
Category: "App",
|
||||
@@ -27,7 +31,7 @@ func TestOverrideItem(t *testing.T) {
|
||||
Name: "Bar",
|
||||
Category: "Test",
|
||||
Icon: &icons.URL{
|
||||
FullURL: new("@walkxcode/example.png"),
|
||||
FullURL: strPtr("@walkxcode/example.png"),
|
||||
Source: icons.SourceWalkXCode,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# internal/homepage/icons
|
||||
# Icons Package
|
||||
|
||||
Icon URL parsing, fetching, and listing for the homepage dashboard.
|
||||
|
||||
|
||||
@@ -3,12 +3,10 @@ package iconfetch
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
@@ -19,6 +17,7 @@ import (
|
||||
"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"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
@@ -77,42 +76,26 @@ func FetchFavIconFromURL(ctx context.Context, iconURL *icons.URL) (Result, error
|
||||
return FetchResultWithErrorf(http.StatusBadRequest, "invalid icon source")
|
||||
}
|
||||
|
||||
var fetchIconClient = http.Client{
|
||||
Timeout: faviconFetchTimeout,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true, //nolint:gosec
|
||||
},
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
resp, err := fetchIconClient.Do(req)
|
||||
if err != nil {
|
||||
resp, err := gphttp.Do(req)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
} else {
|
||||
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
|
||||
return FetchResultWithErrorf(http.StatusBadGateway, "request timeout")
|
||||
}
|
||||
return FetchResultWithErrorf(http.StatusBadGateway, "connection error: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return FetchResultWithErrorf(resp.StatusCode, "upstream error: http %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
ct, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if ct != "" && !strings.HasPrefix(ct, "image/") {
|
||||
return FetchResultWithErrorf(http.StatusNotFound, "not an image")
|
||||
}
|
||||
|
||||
icon, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return FetchResultWithErrorf(http.StatusInternalServerError, "failed to read response body: %w", err)
|
||||
@@ -122,15 +105,10 @@ var FetchIconAbsolute = cache.NewKeyFunc(func(ctx context.Context, url string) (
|
||||
return FetchResultWithErrorf(http.StatusNotFound, "empty icon")
|
||||
}
|
||||
|
||||
if ct == "" {
|
||||
ct = http.DetectContentType(icon)
|
||||
if !strings.HasPrefix(ct, "image/") {
|
||||
return FetchResultWithErrorf(http.StatusNotFound, "not an image")
|
||||
}
|
||||
}
|
||||
|
||||
res := Result{Icon: icon}
|
||||
res.contentType = ct
|
||||
if contentType := resp.Header.Get("Content-Type"); contentType != "" {
|
||||
res.contentType = contentType
|
||||
}
|
||||
// else leave it empty
|
||||
return res, nil
|
||||
}).WithMaxEntries(200).WithRetriesExponentialBackoff(3).WithTTL(4 * time.Hour).Build()
|
||||
|
||||
@@ -2,12 +2,12 @@ package iconlist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
@@ -55,27 +55,26 @@ func init() {
|
||||
|
||||
func InitCache() {
|
||||
m := make(IconMap)
|
||||
err := serialization.LoadFileIfExist(common.IconListCachePath, &m, sonic.Unmarshal)
|
||||
switch {
|
||||
case err != nil:
|
||||
err := serialization.LoadFileIfExist(common.IconListCachePath, &m, json.Unmarshal)
|
||||
if err != nil {
|
||||
// backward compatible
|
||||
oldFormat := struct {
|
||||
Icons IconMap
|
||||
LastUpdate time.Time
|
||||
}{}
|
||||
err = serialization.LoadFileIfExist(common.IconListCachePath, &oldFormat, sonic.Unmarshal)
|
||||
err = serialization.LoadFileIfExist(common.IconListCachePath, &oldFormat, json.Unmarshal)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to load icons")
|
||||
} else {
|
||||
m = oldFormat.Icons
|
||||
// store it to disk immediately
|
||||
_ = serialization.SaveFile(common.IconListCachePath, &m, 0o644, sonic.Marshal)
|
||||
_ = serialization.SaveFile(common.IconListCachePath, &m, 0o644, json.Marshal)
|
||||
}
|
||||
case len(m) > 0:
|
||||
} else if len(m) > 0 {
|
||||
log.Info().
|
||||
Int("icons", len(m)).
|
||||
Msg("icons loaded")
|
||||
default:
|
||||
} else {
|
||||
if err := updateIcons(m); err != nil {
|
||||
log.Error().Err(err).Msg("failed to update icons")
|
||||
}
|
||||
@@ -85,7 +84,7 @@ func InitCache() {
|
||||
|
||||
task.OnProgramExit("save_icons_cache", func() {
|
||||
icons := iconsCache.Load()
|
||||
_ = serialization.SaveFile(common.IconListCachePath, &icons, 0o644, sonic.Marshal)
|
||||
_ = serialization.SaveFile(common.IconListCachePath, &icons, 0o644, json.Marshal)
|
||||
})
|
||||
|
||||
go backgroundUpdateIcons()
|
||||
@@ -106,7 +105,7 @@ func backgroundUpdateIcons() {
|
||||
// swap old cache with new cache
|
||||
iconsCache.Store(newCache)
|
||||
// save it to disk
|
||||
err := serialization.SaveFile(common.IconListCachePath, &newCache, 0o644, sonic.Marshal)
|
||||
err := serialization.SaveFile(common.IconListCachePath, &newCache, 0o644, json.Marshal)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed to save icons")
|
||||
}
|
||||
@@ -143,46 +142,33 @@ func SearchIcons(keyword string, limit int) []*IconMetaSearch {
|
||||
return a.rank - b.rank
|
||||
}
|
||||
|
||||
dashedKeyword := strings.ReplaceAll(keyword, " ", "-")
|
||||
whitespacedKeyword := strings.ReplaceAll(keyword, "-", " ")
|
||||
|
||||
var rank int
|
||||
icons := ListAvailableIcons()
|
||||
for k, icon := range icons {
|
||||
source, ref := k.SourceRef()
|
||||
|
||||
var rank int
|
||||
switch {
|
||||
case strings.EqualFold(ref, dashedKeyword):
|
||||
// exact match: best rank, use source as tiebreaker (lower index = higher priority)
|
||||
if strutils.ContainsFold(string(k), keyword) || strutils.ContainsFold(icon.DisplayName, keyword) {
|
||||
rank = 0
|
||||
case strutils.HasPrefixFold(ref, dashedKeyword):
|
||||
// prefix match: rank by how much extra the name has (shorter = better)
|
||||
rank = 100 + len(ref) - len(dashedKeyword)
|
||||
case strutils.ContainsFold(ref, dashedKeyword) || strutils.ContainsFold(icon.DisplayName, whitespacedKeyword):
|
||||
// contains match
|
||||
rank = 500 + len(ref) - len(dashedKeyword)
|
||||
default:
|
||||
rank = fuzzy.RankMatchFold(keyword, ref)
|
||||
} else {
|
||||
rank = fuzzy.RankMatchFold(keyword, string(k))
|
||||
if rank == -1 || rank > 3 {
|
||||
continue
|
||||
}
|
||||
rank += 1000
|
||||
}
|
||||
|
||||
source, ref := k.SourceRef()
|
||||
ranked := &IconMetaSearch{
|
||||
Source: source,
|
||||
Ref: ref,
|
||||
Meta: icon,
|
||||
rank: rank,
|
||||
}
|
||||
results = append(results, ranked)
|
||||
// Sorted insert based on rank (lower rank = better match)
|
||||
insertPos, _ := slices.BinarySearchFunc(results, ranked, sortByRank)
|
||||
results = slices.Insert(results, insertPos, ranked)
|
||||
if len(results) == searchLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
slices.SortStableFunc(results, sortByRank)
|
||||
|
||||
// Extract results and limit to the requested count
|
||||
return results[:min(len(results), limit)]
|
||||
}
|
||||
@@ -263,8 +249,6 @@ func httpGetImpl(url string) ([]byte, func([]byte), error) {
|
||||
}
|
||||
|
||||
/*
|
||||
UpdateWalkxCodeIcons updates the icon map with the icons from walkxcode.
|
||||
|
||||
format:
|
||||
|
||||
{
|
||||
@@ -286,7 +270,7 @@ func UpdateWalkxCodeIcons(m IconMap) error {
|
||||
}
|
||||
|
||||
data := make(map[string][]string)
|
||||
err = sonic.Unmarshal(body, &data)
|
||||
err = json.Unmarshal(body, &data)
|
||||
release(body)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -365,7 +349,7 @@ func UpdateSelfhstIcons(m IconMap) error {
|
||||
}
|
||||
|
||||
data := make([]SelfhStIcon, 0)
|
||||
err = sonic.Unmarshal(body, &data) //nolint:musttag
|
||||
err = json.Unmarshal(body, &data) //nolint:musttag
|
||||
release(body)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -106,21 +106,21 @@ func (u *URL) Parse(v string) error {
|
||||
|
||||
func (u *URL) parse(v string, checkExists bool) error {
|
||||
if v == "" {
|
||||
return gperr.PrependSubject(ErrInvalidIconURL, "empty url")
|
||||
return ErrInvalidIconURL
|
||||
}
|
||||
slashIndex := strings.Index(v, "/")
|
||||
if slashIndex == -1 {
|
||||
return gperr.PrependSubject(ErrInvalidIconURL, v)
|
||||
return ErrInvalidIconURL
|
||||
}
|
||||
beforeSlash := v[:slashIndex]
|
||||
switch beforeSlash {
|
||||
case "http:", "https:":
|
||||
u.FullURL = &v
|
||||
u.Source = SourceAbsolute
|
||||
case "@target", "": // @target/favicon.ico, /favicon.ico
|
||||
case "@target", "": // @target/favicon.ico, /favicon.ico
|
||||
url := v[slashIndex:]
|
||||
if url == "/" {
|
||||
return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("%s", "empty path")
|
||||
return fmt.Errorf("%w: empty path", ErrInvalidIconURL)
|
||||
}
|
||||
u.FullURL = &url
|
||||
u.Source = SourceRelative
|
||||
@@ -132,16 +132,16 @@ func (u *URL) parse(v string, checkExists bool) error {
|
||||
}
|
||||
parts := strings.Split(v[slashIndex+1:], ".")
|
||||
if len(parts) != 2 {
|
||||
return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("expect %s/<reference>.<format>, e.g. %s/adguard-home.webp", beforeSlash, beforeSlash)
|
||||
return fmt.Errorf("%w: expect %s/<reference>.<format>, e.g. %s/adguard-home.webp", ErrInvalidIconURL, beforeSlash, beforeSlash)
|
||||
}
|
||||
reference, format := parts[0], strings.ToLower(parts[1])
|
||||
if reference == "" || format == "" {
|
||||
return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("empty reference or format")
|
||||
return ErrInvalidIconURL
|
||||
}
|
||||
switch format {
|
||||
case "svg", "png", "webp":
|
||||
default:
|
||||
return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("invalid image format, expect svg/png/webp")
|
||||
return fmt.Errorf("%w: invalid image format, expect svg/png/webp", ErrInvalidIconURL)
|
||||
}
|
||||
isLight, isDark := false, false
|
||||
if strings.HasSuffix(reference, "-light") {
|
||||
@@ -159,7 +159,7 @@ func (u *URL) parse(v string, checkExists bool) error {
|
||||
IsDark: isDark,
|
||||
}
|
||||
if checkExists && !u.HasIcon() {
|
||||
return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("no such icon from %s", u.Source)
|
||||
return fmt.Errorf("%w: no such icon %s.%s from %s", ErrInvalidIconURL, reference, format, u.Source)
|
||||
}
|
||||
default:
|
||||
return gperr.PrependSubject(ErrInvalidIconURL, v)
|
||||
|
||||
@@ -7,6 +7,10 @@ import (
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func TestIconURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -18,7 +22,7 @@ func TestIconURL(t *testing.T) {
|
||||
name: "absolute",
|
||||
input: "http://example.com/icon.png",
|
||||
wantValue: &URL{
|
||||
FullURL: new("http://example.com/icon.png"),
|
||||
FullURL: strPtr("http://example.com/icon.png"),
|
||||
Source: SourceAbsolute,
|
||||
},
|
||||
},
|
||||
@@ -26,7 +30,7 @@ func TestIconURL(t *testing.T) {
|
||||
name: "relative",
|
||||
input: "@target/icon.png",
|
||||
wantValue: &URL{
|
||||
FullURL: new("/icon.png"),
|
||||
FullURL: strPtr("/icon.png"),
|
||||
Source: SourceRelative,
|
||||
},
|
||||
},
|
||||
@@ -34,7 +38,7 @@ func TestIconURL(t *testing.T) {
|
||||
name: "relative2",
|
||||
input: "/icon.png",
|
||||
wantValue: &URL{
|
||||
FullURL: new("/icon.png"),
|
||||
FullURL: strPtr("/icon.png"),
|
||||
Source: SourceRelative,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# internal/homepage/integrations/qbittorrent
|
||||
# qBittorrent Integration Package
|
||||
|
||||
This package provides a qBittorrent widget for the GoDoxy homepage dashboard, enabling real-time monitoring of torrent status and transfer statistics.
|
||||
|
||||
|
||||
@@ -2,37 +2,25 @@ package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/yusing/godoxy/internal/homepage/widgets"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
URL string
|
||||
Username string
|
||||
Password strutils.Redacted
|
||||
Password string
|
||||
}
|
||||
|
||||
func (c *Client) Initialize(ctx context.Context, url string, cfg map[string]any) error {
|
||||
c.URL = url
|
||||
|
||||
username, ok := cfg["username"].(string)
|
||||
if !ok {
|
||||
return errors.New("username is not a string")
|
||||
}
|
||||
c.Username = username
|
||||
|
||||
password, ok := cfg["password"].(string)
|
||||
if !ok {
|
||||
return errors.New("password is not a string")
|
||||
}
|
||||
c.Password = strutils.Redacted(password)
|
||||
c.Username = cfg["username"].(string)
|
||||
c.Password = cfg["password"].(string)
|
||||
|
||||
_, err := c.Version(ctx)
|
||||
if err != nil {
|
||||
@@ -49,7 +37,7 @@ func (c *Client) doRequest(ctx context.Context, method, endpoint string, query u
|
||||
}
|
||||
|
||||
if c.Username != "" && c.Password != "" {
|
||||
req.SetBasicAuth(c.Username, c.Password.String())
|
||||
req.SetBasicAuth(c.Username, c.Password)
|
||||
}
|
||||
|
||||
resp, err := widgets.HTTPClient.Do(req)
|
||||
@@ -71,7 +59,7 @@ func jsonRequest[T any](ctx context.Context, client *Client, endpoint string, qu
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
err = sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&result)
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
const endpointLogs = "/api/v2/log/main"
|
||||
@@ -45,7 +44,7 @@ func (l *LogEntry) Level() string {
|
||||
}
|
||||
|
||||
func (l *LogEntry) MarshalJSON() ([]byte, error) {
|
||||
return sonic.Marshal(map[string]any{
|
||||
return json.Marshal(map[string]any{
|
||||
"id": l.ID,
|
||||
"timestamp": l.Timestamp,
|
||||
"level": l.Level(),
|
||||
|
||||
13
internal/homepage/types/README.md
Normal file
13
internal/homepage/types/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Types Package
|
||||
|
||||
Configuration types for the homepage package.
|
||||
|
||||
## Config
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
UseDefaultCategories bool `json:"use_default_categories"`
|
||||
}
|
||||
|
||||
var ActiveConfig atomic.Pointer[Config]
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
# internal/homepage/widgets
|
||||
# Homepage Widgets Package
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# internal/idlewatcher
|
||||
# Idlewatcher
|
||||
|
||||
Manages container lifecycle based on idle timeout, automatically stopping/pausing containers and waking them on request.
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"iter"
|
||||
"strconv"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ type watcherDebug struct {
|
||||
|
||||
func (w watcherDebug) MarshalJSON() ([]byte, error) {
|
||||
state := w.state.Load()
|
||||
return sonic.Marshal(map[string]any{
|
||||
return json.Marshal(map[string]any{
|
||||
"name": w.Name(),
|
||||
"state": map[string]string{
|
||||
"status": string(state.status),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
gevents "github.com/yusing/goutils/events"
|
||||
"github.com/yusing/goutils/events"
|
||||
)
|
||||
|
||||
type WakeEvent struct {
|
||||
@@ -26,7 +26,7 @@ const (
|
||||
)
|
||||
|
||||
func writeSSE(w io.Writer, v any) error {
|
||||
data, err := sonic.Marshal(v)
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -58,12 +58,12 @@ func (w *Watcher) sendEvent(eventType WakeEventType, message string, err error)
|
||||
|
||||
w.l.Debug().Str("event", string(eventType)).Str("message", message).Err(err).Msg("sending event")
|
||||
|
||||
level := gevents.LevelInfo
|
||||
level := events.LevelInfo
|
||||
if eventType == WakeEventError {
|
||||
level = gevents.LevelError
|
||||
level = events.LevelError
|
||||
}
|
||||
|
||||
w.events.Add(gevents.NewEvent(
|
||||
w.events.Add(events.NewEvent(
|
||||
level,
|
||||
w.cfg.ContainerName(),
|
||||
string(eventType),
|
||||
|
||||
@@ -62,6 +62,6 @@ func DebugHandler(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
default:
|
||||
_ = w.writeLoadingPage(rw)
|
||||
w.writeLoadingPage(rw)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user