Compare commits

...

93 Commits

Author SHA1 Message Date
yusing
fd74bfedf0 fix(agent): improve url handling to not break urls with encoded characters 2026-01-01 18:25:27 +08:00
yusing
a47170da39 feat(metrics): add IsExcluded field to RouteUptimeAggregate for enhanced status tracking
- updated swagger
2026-01-01 13:20:17 +08:00
yusing
89a4ca767d fix(homepage): improve alphabetical sorting by normalizing item names (#181)
- Updated the sorting function to use Title case for item names to ensure consistent alphabetical ordering.
2026-01-01 12:31:36 +08:00
yusing
3dbbde164b fix(route): enhance host parsing with port suffix support
- Added logic to strip the trailing :port from the host when searching for routes.
- Updated findRouteByDomains function to ensure consistent host formatting.
- Added related tests
2025-12-30 22:46:38 +08:00
yusing
e75eede332 chore(goutils): update subproject commit reference to 51a75d68 2025-12-30 22:01:01 +08:00
yusing
e4658a8f09 fix(route): update health monitor initialization to use implementation instance 2025-12-30 21:59:43 +08:00
yusing
e25ccdbd24 chore: upgrade dependencies 2025-12-30 21:56:54 +08:00
yusing
5087800fd7 fix(tests/metrics): correct syntax error 2025-12-30 21:53:10 +08:00
yusing
d7f33b7390 chore(.gitignore): add dev-data directory to ignore list 2025-12-30 21:52:04 +08:00
yusing
1978329314 feat(route): add CommandRoute for routing requests to specified routes
- Introduced CommandRoute to handle routing requests to other defined routes.
- Added validation to ensure a single argument is provided for the route.
- Implemented command handler to serve the specified route or return a 404 error if not found.
2025-12-30 21:49:47 +08:00
yusing
dba8441e8a refactor(routes): add excluded routes to health check and route list
- Updated route iteration to include all routes, including excluded ones.
- Renamed existing functions for clarity.
- Adjusted health info retrieval to reflect changes in route iteration.
- Improved route management by adding health monitoring capabilities for excluded routes.
2025-12-30 12:39:58 +08:00
yusing
44fc678496 refactor(docker): simplify docker host parsing 2025-12-29 10:38:43 +08:00
yusing
0b410311da fix(oidc): add trailing slash to OIDCAuthBasePath to work with paths like /authorize 2025-12-29 10:22:38 +08:00
yusing
dc39f0cb6e chore(swagger): add installation instruction for swaggo in Makefile 2025-12-23 17:18:59 +08:00
yusing
e232b9d122 chore(swagger): update swagger regarding new docker config structure 2025-12-23 17:18:13 +08:00
yusing
41f8d3cfc0 refactor(docker): update TLS config validation to require both CertFile and KeyFile exists or both empty 2025-12-23 12:25:26 +08:00
Yuzerion
5ab0392cd3 feat: docker over tls (#178) 2025-12-23 12:01:11 +08:00
yusing
09702266a9 feat(debug): implement debug server for development environment
- Added `listenDebugServer` function to handle debug requests.
- Introduced table based debug page with different functionalities.
- Updated Makefile to use `scc` for code analysis instead of `cloc`.
2025-12-22 16:57:47 +08:00
yusing
14f3ed95ea feat(auth): modernize block page styling 2025-12-22 15:48:55 +08:00
yusing
eb3aa21e37 fix(healthcheck): fix fileserver health check by removing zero port check 2025-12-22 12:04:09 +08:00
yusing
a6e86ea420 fix(auth): correct logic in AuthOrProceed when auth is disabled 2025-12-22 12:00:14 +08:00
yusing
dd96e09a7a refactor(docker): streamline label loading in loadDeleteIdlewatcherLabels function 2025-12-22 11:54:37 +08:00
yusing
4d08efbd4f chore(deps): upgrade dependencies 2025-12-22 11:54:13 +08:00
yusing
f67480d085 feat(oidc): make rate limit customizable; per oidc instance rate limit
- add env variables OIDC_RATE_LIMIT and OIDC_RATE_LIMIT_PERIOD
- default rate limit changed to 10 rps from 1 rps
- rate limit is no longer applied globally
2025-12-22 10:43:41 +08:00
yusing
736985b79d fix(auth): enforce HTML acceptance in OIDC login handler 2025-12-22 10:35:43 +08:00
yusing
1fb1ee0279 refactor(auth): enhance error handling in OIDC login and callback handlers with user-friendly pages 2025-12-22 10:35:07 +08:00
yusing
4b2a6023bb refactor(auth): update WriteBlockPage function to include action text and URL 2025-12-22 10:27:48 +08:00
yusing
5852053ef9 fix(config): remove duplicated reload error 2025-12-21 11:23:42 +08:00
yusing
c687795cd8 refactor(docker): remove unnecessary http client in NewClient method 2025-12-21 11:23:21 +08:00
yusing
93af695e95 refactor(list_icons): interning app category names to save memory 2025-12-20 20:43:21 +08:00
yusing
58325e60b4 refactor(docker): remove deprecated client.WithAPIVersionNegotiation() 2025-12-20 19:51:43 +08:00
yusing
b134b92704 feat(fileserver): implement spa support; add spa and index fields to config 2025-12-20 19:24:39 +08:00
yusing
376ac61279 fix(healthcheck): nil panic on health check 2025-12-20 11:07:42 +08:00
yusing
dca701e044 fix(healthcheck): nil panic on agents 2025-12-20 10:03:43 +08:00
yusing
4bb3af3671 feat(workflow): add cherry-pick workflow for tagging into compat branch 2025-12-18 23:24:48 +08:00
yusing
95efc127cf fix(idlewatcher): incorrect "dependency has positive idle timeout" error 2025-12-18 23:22:42 +08:00
yusing
6e55c4624b refactor(http): consolidate User-Agent header in health monitor 2025-12-18 00:25:47 +08:00
yusing
e9374364dd feat(reverse_proxy): add scheme mismatch handling for retry logic in reverse proxy 2025-12-18 00:24:46 +08:00
yusing
216679eb8d fix(docker): nil panic reading container names 2025-12-17 23:17:11 +08:00
yusing
505a3d3972 refactor(http): enhance health check error logic by treating all 5xx as unhealthy 2025-12-17 17:43:59 +08:00
yusing
27512b4d04 chore: upgrade dependencies 2025-12-17 17:43:53 +08:00
yusing
88d7255c7a fix(idlewatcher): directly serve the request on ready instead of redirecting 2025-12-17 11:46:52 +08:00
yusing
ea67095967 refactor(docker): streamline idlewatcher label handling
- Introduced a map for idlewatcher labels to simplify the loading of configuration values.
- Simplify logic to check for the presence of an idle timeout and handle dependencies.
2025-12-17 10:33:37 +08:00
yusing
86a46d191d feat(idlewatcher): add option to disable loading page 2025-12-17 10:33:33 +08:00
yusing
b7250b29e0 fix(idlewatcher): improve container readiness handling in wakeFromHTTP
- Updated the wakeFromHTTP method to send a 100 Continue response to prevent client wait-header timeout.
- Implemented logic for non-HTML requests to wait for the container to become ready, returning an error message if it times out, or redirecting if successful.
- Adjusted the waitForReady method to return true upon receiving a ready notification.
2025-12-17 10:30:06 +08:00
yusing
e44ecc0ccc fix(access_log): fix slice out-of-bound panic on log rotation 2025-12-16 17:20:59 +08:00
yusing
6f9f995100 fix(config): nil panic introduced in ff934a4bb2911f5fa3c23d8fe6fea252d881fdc3; remove duplicated log 2025-12-16 15:04:21 +08:00
yusing
496aec6bb6 refactor: simplify and optimize deserialization 2025-12-16 14:48:33 +08:00
yusing
4afed02fc2 refactor(pool): simplify and optimize SizedPool; remove sync pool 2025-12-16 14:08:38 +08:00
yusing
f7eb4b132a refactor(config): remove unused ActiveConfig 2025-12-16 11:57:09 +08:00
yusing
ff934a4bb2 fix(config): fix default values not applied 2025-12-16 11:55:47 +08:00
yusing
db0cbc6577 refactor(config): remove unnecessary indirection 2025-12-16 11:22:17 +08:00
yusing
de3f92246f feat(rules): add protocol matching functionality
- Introduced a new checker for HTTP protocols (http, https, h3) in the routing rules.
- Added corresponding test cases to validate protocol matching behavior in requests.
2025-12-16 10:22:00 +08:00
yusing
c143593284 fix(icons): add handling for dark icons for walkxcode 2025-12-15 15:42:59 +08:00
yusing
31bf889d4a refactor(favicon): enhance FindIcon function to support icon variants
- Updated FindIcon to accept an additional variant parameter for improved icon fetching.
- Adjusted FavIcon and GetFavIconFromAlias functions to utilize the new variant handling logic.
2025-12-15 14:54:05 +08:00
yusing
baa7e72ad6 refactor(icon): improve handling in WithVariant 2025-12-15 14:42:31 +08:00
yusing
f43e07fe60 fix(favicon): enhance variant handling in GetFavIconFromAlias function
- Added fallback logic to handle cases where the requested icon variant is unavailable.
- If variant not provided, do not call WithVariant.
2025-12-15 14:37:33 +08:00
yusing
d319ee99ad fix(favicon): correct icon cache key in FindIcon method 2025-12-15 14:31:16 +08:00
yusing
ab58559afc refactor(icon): add variant handling for absolute/relative icons in WithVariant method 2025-12-15 14:30:31 +08:00
yusing
a6bdbb5603 chore: update api swagger 2025-12-15 12:28:14 +08:00
yusing
a0c589c546 feat(favicon): add variant support for favicons
- Introduced a new Variant field in GetFavIconRequest to specify icon variants (light/dark).
- Updated GetFavIconFromAlias function to handle the variant when fetching favicons.
- Added WithVariant method in IconURL to manage icon variants effectively.
2025-12-15 12:28:03 +08:00
yusing
76b8252755 fix(socket-proxy): update golang version. fix Dockerfile 2025-12-10 17:56:04 +08:00
yusing
d547872a41 fix(ci): correct socket-proxy github workflow 2025-12-10 17:39:39 +08:00
yusing
8d4618cedf chore(deps): go mod tidy 2025-12-10 17:37:46 +08:00
yusing
2ba758939b chore(deps): upgrade dependencies 2025-12-10 17:27:00 +08:00
yusing
fdd37b777a fix(http): 'runtime error: comparing uncomparable type httputils.UnwrittenBody' 2025-12-10 17:23:13 +08:00
yusing
bc19a54976 chore(deps): upgrade dependencies in submodules 2025-12-08 14:17:14 +08:00
yusing
12d999809f fix(http): correct Unwrap method and enhance error handling in Hijack method
- Updated the Hijack method in LazyResponseModifier and ResponseModifier to return a wrapped error for unsupported hijacking.
- Added a nil check in LazyResponseModifier's Unwrap method to ensure safe access to the underlying ResponseWriter.
2025-12-08 14:06:58 +08:00
yusing
6771293336 fix(middleware): enhance response modification handling in ServeHTTP
- Replaced ResponseModifier with new LazyResponseModifier.
- Added logic to skip modification for non-HTML content.
2025-12-08 13:45:53 +08:00
yusing
d240c9dfee fix(io): limit buffer size to 16KB to avoid high memory usage and improve context propagation 2025-12-08 10:46:00 +08:00
yusing
c7eda38933 refactor(route): simplify context handling in RouteContext
- Removed unnecessary requestInternal struct and directly accessed the context field of http.Request.
- Simplified the initialization of ctxFieldOffset.
2025-12-05 18:26:34 +08:00
yusing
09caa888ad refactor(config): update config structures to use strutils.Redacted for sensitive fields
- Modified Config structs in various packages to replace string fields with strutils.Redacted to prevent logging sensitive information.
- Updated serialization methods to accommodate new data types.
- Adjusted API token handling in Proxmox configuration.
2025-12-05 18:26:16 +08:00
yusing
e41a487371 chore: remove go.work 2025-12-05 17:51:22 +08:00
yusing
7c08a8da2e Revert "ci: Add workflow to automatically merge main into compat on tag push"
This reverts commit 9930f3fa2e.
2025-12-05 16:29:45 +08:00
yusing
82df824490 chore: go mod tidy 2025-12-05 16:20:29 +08:00
yusing
2f341001c1 chore(Makefile): add socket-proxy to docker build test and update build command syntax 2025-12-05 16:10:14 +08:00
yusing
25ee8041da refactor(http,rules): move SharedData and ResponseModifier to httputils
- implemented dependency injection for rule auth handler
2025-12-05 16:06:36 +08:00
yusing
8687a57b6c fix(Dockerfile): exclude goutils in mod caching stage 2025-12-05 01:29:30 +08:00
yusing
3f4ed31e46 fix(middleware): skip modify response for websocket and event-stream requests in ServeHTTP 2025-12-05 01:18:27 +08:00
yusing
9930f3fa2e ci: Add workflow to automatically merge main into compat on tag push 2025-12-05 01:11:50 +08:00
yusing
2157545e17 fix(route): nil panic when used as an idlewatcher dependency 2025-12-05 01:10:48 +08:00
yusing
f721395ff0 refactor(healthcheck): agent health check 2025-12-05 00:45:24 +08:00
yusing
0dc7c59af1 refactor(deps): upgrade go to 1.25.5; isolate dependencies for reverseproxy, websocket and server modules 2025-12-05 00:36:16 +08:00
yusing
e3fe126a5c chore(example): introduce health check configuration defaults in example config 2025-12-04 18:08:26 +08:00
yusing
aa2575696d fix(http): handle 0 content length properly in some cases 2025-12-04 17:33:01 +08:00
yusing
c1f9c2c957 fix(middleware): skip modification for HEAD requests in ModifyHTML middleware 2025-12-04 17:27:26 +08:00
yusing
c098fef615 fix(http): enhance Content-Length handling in ResponseModifier
- Introduced origContentLength and bodyModified fields to track original content length and body modification status.
- Updated ContentLength and ContentLengthStr methods to return accurate content length based on body modification state.
- Adjusted Write and FlushRelease methods to ensure proper handling of Content-Length header.
- Modified middleware to use the new ContentLengthStr method.
2025-12-04 17:26:15 +08:00
yusing
9cdc985fb0 fix(tests): correct test expectations for middleware bypass and rules 2025-12-04 16:18:14 +08:00
yusing
2034738422 refactor(labels): refine wildcard expansion logic and tests
- Added multiple test cases for the ExpandWildcard function to cover various scenarios including basic wildcards, no wildcards, empty labels, and YAML configurations.
- Improved handling of nested maps and invalid YAML inputs.
- Ensured that explicit labels and reference aliases are correctly processed and expanded.
2025-12-04 16:16:43 +08:00
yusing
55a42b81de refactor(healthcheck): streamline health check configuration and defaults
- Moved health check constants from common package alongside type definition.
- Updated health check configuration to use struct directly instead of pointers.
- Introduced global default health check config
2025-12-04 15:19:10 +08:00
yusing
48627753d6 refactor(routes): simplify route exclusion check and health check defaults 2025-12-04 15:12:35 +08:00
yusing
09b514393d chore(deps): upgrade go version and dependencies 2025-12-04 12:18:12 +08:00
yusing
3b2ae5dbd6 refactor: move some utility functions to goutils and update references 2025-12-04 12:17:33 +08:00
141 changed files with 2683 additions and 1822 deletions

View File

@@ -6,13 +6,12 @@ on:
- main
paths:
- "socket-proxy/**"
- "socket-proxy.Dockerfile"
- ".github/workflows/docker-image-socket-proxy.yml"
tags-ignore:
- '**'
- "**"
workflow_dispatch:
permissions:
contents: read
jobs:
build:
uses: ./.github/workflows/docker-image.yml

View File

@@ -0,0 +1,39 @@
name: Cherry-pick into Compat
on:
push:
tags:
- v*
paths:
- ".github/workflows/merge-main-into-compat.yml"
jobs:
cherry-pick:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure git user
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Cherry-pick commits from last tag
run: |
git fetch origin compat
git checkout compat
CURRENT_TAG=${{ github.ref_name }}
PREV_TAG=$(git describe --tags --abbrev=0 $CURRENT_TAG^ 2>/dev/null || echo "")
if [ -z "$PREV_TAG" ]; then
echo "No previous tag found. Cherry-picking all commits up to $CURRENT_TAG"
git rev-list --reverse --no-merges $CURRENT_TAG | xargs -r git cherry-pick
else
echo "Cherry-picking commits from $PREV_TAG to $CURRENT_TAG"
git rev-list --reverse --no-merges $PREV_TAG..$CURRENT_TAG | xargs -r git cherry-pick
fi
- name: Push compat
run: |
git push origin compat

1
.gitignore vendored
View File

@@ -40,3 +40,4 @@ tsconfig.tsbuildinfo
!agent.compose.yml
!agent/pkg/**
dev-data/

View File

@@ -1,5 +1,5 @@
# Stage 1: deps
FROM golang:1.25.4-alpine AS deps
FROM golang:1.25.5-alpine AS deps
HEALTHCHECK NONE
# package version does not matter
@@ -19,7 +19,9 @@ COPY go.mod go.sum ./
# remove godoxy stuff from go.mod first
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/root/go/pkg/mod \
sed -i '/^module github\.com\/yusing\/godoxy/!{/github\.com\/yusing\/godoxy/d}' go.mod && go mod download -x
sed -i '/^module github\.com\/yusing\/godoxy/!{/github\.com\/yusing\/godoxy/d}' go.mod && \
sed -i '/^module github\.com\/yusing\/goutils/!{/github\.com\/yusing\/goutils/d}' go.mod && \
go mod download -x
# Stage 2: builder
FROM deps AS builder

View File

@@ -35,7 +35,7 @@ else ifeq ($(debug), 1)
CGO_ENABLED = 1
GODOXY_DEBUG = 1
GO_TAGS += debug
BUILD_FLAGS += -asan # FIXME: -gcflags=all='-N -l'
# FIXME: BUILD_FLAGS += -asan -gcflags=all='-N -l'
else ifeq ($(pprof), 1)
CGO_ENABLED = 0
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
@@ -80,6 +80,7 @@ test:
docker-build-test:
docker build -t godoxy .
docker build --build-arg=MAKE_ARGS=agent=1 -t godoxy-agent .
docker build --build-arg=MAKE_ARGS=socket-proxy=1 -t godoxy-socket-proxy .
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2)
files := $(shell find . -name go.mod -type f -or -name Dockerfile -type f)
@@ -110,7 +111,7 @@ mod-tidy:
build:
mkdir -p $(shell dirname ${BIN_PATH})
cd ${PWD} && go build ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
go build -C ${PWD} ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
${POST_BUILD}
run:
@@ -141,12 +142,13 @@ ci-test:
act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)"
cloc:
cloc --include-lang=Go --not-match-f '_test.go$$' .
scc -w -i go --not-match '_test.go$'
push-github:
git push origin $(shell git rev-parse --abbrev-ref HEAD)
gen-swagger:
# go install github.com/swaggo/swag/cmd/swag@latest
swag init --parseDependency --parseInternal --parseFuncBody -g handler.go -d internal/api -o internal/api/v1/docs
python3 scripts/fix-swagger-json.py
# we don't need this

View File

@@ -1,14 +1,16 @@
module github.com/yusing/godoxy/agent
go 1.25.4
go 1.25.5
replace github.com/yusing/godoxy => ..
replace github.com/yusing/godoxy/socketproxy => ../socket-proxy
replace github.com/shirou/gopsutil/v4 => ../internal/gopsutil
replace github.com/yusing/goutils => ../goutils
replace (
github.com/shirou/gopsutil/v4 => ../internal/gopsutil
github.com/yusing/godoxy => ../
github.com/yusing/godoxy/socketproxy => ../socket-proxy
github.com/yusing/goutils => ../goutils
github.com/yusing/goutils/http/reverseproxy => ../goutils/http/reverseproxy
github.com/yusing/goutils/http/websocket => ../goutils/http/websocket
github.com/yusing/goutils/server => ../goutils/server
)
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
@@ -20,9 +22,11 @@ require (
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1
github.com/valyala/fasthttp v1.68.0
github.com/yusing/godoxy v0.20.2
github.com/yusing/godoxy v0.20.10
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
github.com/yusing/goutils v0.7.0
github.com/yusing/goutils/http/reverseproxy v0.0.0-20251217162119-cb0f79b51ce2
github.com/yusing/goutils/server v0.0.0-20251217162119-cb0f79b51ce2
)
require (
@@ -30,57 +34,70 @@ require (
github.com/PuerkitoBio/goquery v1.11.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/diskfs/go-diskfs v1.7.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v29.0.1+incompatible // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/docker/cli v29.1.3+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
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-acme/lego/v4 v4.30.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/goccy/go-yaml v1.19.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gotify/server/v2 v2.7.3 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/luthermonson/go-proxmox v0.2.4 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.69 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/moby/api v1.52.0 // indirect
github.com/moby/moby/client v0.1.0 // indirect
github.com/moby/moby/client v0.2.1 // 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
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.56.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.58.0 // indirect
github.com/samber/lo v1.52.0 // indirect
github.com/samber/slog-common v0.19.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.10 // indirect
github.com/shirou/gopsutil/v4 v4.25.11 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/spf13/afero v1.15.0 // 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
@@ -89,18 +106,22 @@ require (
github.com/vincent-petithory/dataurl v1.0.0 // indirect
github.com/yusing/ds v0.3.1 // indirect
github.com/yusing/gointernals v0.1.16 // indirect
github.com/yusing/goutils/http/websocket v0.0.0-20251217162119-cb0f79b51ce2 // 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.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -2,6 +2,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=
github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
@@ -16,14 +18,16 @@ github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2N
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/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/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
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/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
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=
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=
@@ -35,26 +39,28 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v29.0.1+incompatible h1:EnvMEAR9Ro5xQEKbMitlabj5vCDY0vwcDyY/Lsow7FQ=
github.com/docker/cli v29.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.1.3+incompatible h1:+kz9uDWgs+mAaIZojWfFt4d53/jv0ZUOOoSh5ZnH36c=
github.com/docker/cli v29.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/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=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/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.28.1 h1:zt301JYF51UIEkpSXsdeGq9hRePeFzQCq070OdAmP0Q=
github.com/go-acme/lego/v4 v4.28.1/go.mod h1:bzjilr03IgbaOwlH396hq5W56Bi0/uoRwW/JM8hP7m4=
github.com/go-acme/lego/v4 v4.30.1 h1:tmb6U0lvy8Mc3lQbqKwTat7oAhE8FUYNJ3D0gSg6pJU=
github.com/go-acme/lego/v4 v4.30.1/go.mod h1:V7m/Ip+EeFkjOe028+zeH+SwWtESxw1LHelwMIfAjm4=
github.com/go-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=
@@ -71,14 +77,16 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
@@ -94,12 +102,16 @@ 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.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
github.com/gotify/server/v2 v2.7.3/go.mod h1:VAtE1RIc/2j886PYs9WPQbMjqbFsoyQ0G8IdFtnAxU0=
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=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -112,8 +124,8 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/luthermonson/go-proxmox v0.2.3 h1:NAjUJ5Jd1ynIK6UHMGd/VLGgNZWpGXhfL+DBmAVSEaA=
github.com/luthermonson/go-proxmox v0.2.3/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
github.com/luthermonson/go-proxmox v0.2.4 h1:XQ6YNUTVvHS7N4EJxWpuqWLW2s1VPtsIblxLV/rGHLw=
github.com/luthermonson/go-proxmox v0.2.4/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=
@@ -123,14 +135,14 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
github.com/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.1.0 h1:nt+hn6O9cyJQqq5UWnFGqsZRTS/JirUqzPjEl0Bdc/8=
github.com/moby/moby/client v0.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
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/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=
@@ -144,9 +156,13 @@ github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/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=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -154,10 +170,10 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
@@ -192,6 +208,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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
@@ -209,22 +227,22 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
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.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -233,15 +251,15 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -251,10 +269,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -262,14 +280,16 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -281,8 +301,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -301,8 +321,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -311,11 +331,11 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -1,6 +1,7 @@
package handler
import (
"context"
"fmt"
"net/http"
"net/url"
@@ -12,8 +13,6 @@ import (
"github.com/yusing/godoxy/internal/watcher/health/monitor"
)
var defaultHealthConfig = types.DefaultHealthConfig()
func CheckHealth(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
scheme := query.Get("scheme")
@@ -49,7 +48,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
Scheme: scheme,
Host: host,
Path: path,
}, defaultHealthConfig).CheckHealth()
}, healthCheckConfigFromRequest(r)).CheckHealth()
case "tcp", "udp":
host := query.Get("host")
if host == "" {
@@ -68,7 +67,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
result, err = monitor.NewRawHealthMonitor(&url.URL{
Scheme: scheme,
Host: host,
}, defaultHealthConfig).CheckHealth()
}, healthCheckConfigFromRequest(r)).CheckHealth()
}
if err != nil {
@@ -80,3 +79,13 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
sonic.ConfigDefault.NewEncoder(w).Encode(result)
}
func healthCheckConfigFromRequest(r *http.Request) types.HealthCheckConfig {
// we only need timeout and base context because it's one shot request
return types.HealthCheckConfig{
Timeout: types.HealthCheckTimeoutDefault,
BaseContext: func() context.Context {
return r.Context()
},
}
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"net/http/httputil"
"strings"
"time"
"github.com/yusing/godoxy/agent/pkg/agent"
@@ -43,10 +44,22 @@ func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
return
}
r.URL.Scheme = ""
r.URL.Host = ""
r.URL.Path = r.URL.Path[agent.HTTPProxyURLPrefixLen:] // strip the {API_BASE}/proxy/http prefix
r.RequestURI = r.URL.String()
// Strip the {API_BASE}/proxy/http prefix while preserving URL escaping.
//
// NOTE: `r.URL.Path` is decoded. If we rewrite it without keeping `RawPath`
// in sync, Go may re-escape the path (e.g. turning "%5B" into "%255B"),
// which breaks urls with percent-encoded characters, like Next.js static chunk URLs.
prefix := agent.APIEndpointBase + agent.EndpointProxyHTTP
r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix)
if r.URL.RawPath != "" {
if after, ok := strings.CutPrefix(r.URL.RawPath, prefix); ok {
r.URL.RawPath = after
} else {
// RawPath is no longer a valid encoding for Path; force Go to re-derive it.
r.URL.RawPath = ""
}
}
r.RequestURI = ""
rp := &httputil.ReverseProxy{
Director: func(r *http.Request) {

257
cmd/debug_page.go Normal file
View File

@@ -0,0 +1,257 @@
//go:build !production
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/api"
apiV1 "github.com/yusing/godoxy/internal/api/v1"
agentApi "github.com/yusing/godoxy/internal/api/v1/agent"
authApi "github.com/yusing/godoxy/internal/api/v1/auth"
certApi "github.com/yusing/godoxy/internal/api/v1/cert"
dockerApi "github.com/yusing/godoxy/internal/api/v1/docker"
fileApi "github.com/yusing/godoxy/internal/api/v1/file"
homepageApi "github.com/yusing/godoxy/internal/api/v1/homepage"
metricsApi "github.com/yusing/godoxy/internal/api/v1/metrics"
routeApi "github.com/yusing/godoxy/internal/api/v1/route"
"github.com/yusing/godoxy/internal/auth"
"github.com/yusing/godoxy/internal/idlewatcher"
idlewatcherTypes "github.com/yusing/godoxy/internal/idlewatcher/types"
)
type debugMux struct {
endpoints []debugEndpoint
mux http.ServeMux
}
type debugEndpoint struct {
name string
method string
path string
}
func newDebugMux() *debugMux {
return &debugMux{
endpoints: make([]debugEndpoint, 0),
mux: *http.NewServeMux(),
}
}
func (mux *debugMux) registerEndpoint(name, method, path string) {
mux.endpoints = append(mux.endpoints, debugEndpoint{name: name, method: method, path: path})
}
func (mux *debugMux) HandleFunc(name, method, path string, handler http.HandlerFunc) {
mux.registerEndpoint(name, method, path)
mux.mux.HandleFunc(method+" "+path, handler)
}
func (mux *debugMux) Finalize() {
mux.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, `
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji;
font-size: 16px;
line-height: 1.5;
color: #f8f9fa;
background-color: #121212;
margin: 0;
padding: 0;
}
table {
border-collapse: collapse;
width: 100%;
margin-top: 20px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #333;
}
th {
background-color: #1e1e1e;
font-weight: 600;
color: #f8f9fa;
}
td {
color: #e9ecef;
}
.link {
color: #007bff;
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
.method {
color: #6c757d;
font-family: monospace;
}
.path {
color: #6c757d;
font-family: monospace;
}
</style>
</head>
<body>
<table>
<thead>
<tr>
<th>Name</th>
<th>Method</th>
<th>Path</th>
</tr>
</thead>
<tbody>`)
for _, endpoint := range mux.endpoints {
fmt.Fprintf(w, "<tr><td><a class='link' href=%q>%s</a></td><td class='method'>%s</td><td class='path'>%s</td></tr>", endpoint.path, endpoint.name, endpoint.method, endpoint.path)
}
fmt.Fprintln(w, `
</tbody>
</table>
</body>
</html>`)
})
}
func listenDebugServer() {
mux := newDebugMux()
mux.mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/svg+xml")
w.WriteHeader(http.StatusOK)
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)
mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP)
mux.Finalize()
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 {
r := gin.New()
r.Use(api.ErrorHandler())
r.Use(api.ErrorLoggingMiddleware())
r.Use(api.NoCache())
registerGinRoute := func(router gin.IRouter, method, name string, path string, handler gin.HandlerFunc) {
if group, ok := router.(*gin.RouterGroup); ok {
debugMux.registerEndpoint(name, method, group.BasePath()+path)
} else {
debugMux.registerEndpoint(name, method, path)
}
router.Handle(method, path, handler)
}
registerGinRoute(r, "GET", "App version", "/api/v1/version", apiV1.Version)
v1 := r.Group("/api/v1")
if auth.IsEnabled() {
v1Auth := v1.Group("/auth")
{
registerGinRoute(v1Auth, "HEAD", "Auth check", "/check", authApi.Check)
registerGinRoute(v1Auth, "POST", "Auth login", "/login", authApi.Login)
registerGinRoute(v1Auth, "GET", "Auth callback", "/callback", authApi.Callback)
registerGinRoute(v1Auth, "POST", "Auth callback", "/callback", authApi.Callback)
registerGinRoute(v1Auth, "POST", "Auth logout", "/logout", authApi.Logout)
registerGinRoute(v1Auth, "GET", "Auth logout", "/logout", authApi.Logout)
}
}
{
// enable cache for favicon
registerGinRoute(v1, "GET", "Route favicon", "/favicon", apiV1.FavIcon)
registerGinRoute(v1, "GET", "Route health", "/health", apiV1.Health)
registerGinRoute(v1, "GET", "List icons", "/icons", apiV1.Icons)
registerGinRoute(v1, "POST", "Config reload", "/reload", apiV1.Reload)
registerGinRoute(v1, "GET", "Route stats", "/stats", apiV1.Stats)
route := v1.Group("/route")
{
registerGinRoute(route, "GET", "List routes", "/list", routeApi.Routes)
registerGinRoute(route, "GET", "Get route", "/:which", routeApi.Route)
registerGinRoute(route, "GET", "List providers", "/providers", routeApi.Providers)
registerGinRoute(route, "GET", "List routes by provider", "/by_provider", routeApi.ByProvider)
registerGinRoute(route, "POST", "Playground", "/playground", routeApi.Playground)
}
file := v1.Group("/file")
{
registerGinRoute(file, "GET", "List files", "/list", fileApi.List)
registerGinRoute(file, "GET", "Get file", "/content", fileApi.Get)
registerGinRoute(file, "PUT", "Set file", "/content", fileApi.Set)
registerGinRoute(file, "POST", "Set file", "/content", fileApi.Set)
registerGinRoute(file, "POST", "Validate file", "/validate", fileApi.Validate)
}
homepage := v1.Group("/homepage")
{
registerGinRoute(homepage, "GET", "List categories", "/categories", homepageApi.Categories)
registerGinRoute(homepage, "GET", "List items", "/items", homepageApi.Items)
registerGinRoute(homepage, "POST", "Set item", "/set/item", homepageApi.SetItem)
registerGinRoute(homepage, "POST", "Set items batch", "/set/items_batch", homepageApi.SetItemsBatch)
registerGinRoute(homepage, "POST", "Set item visible", "/set/item_visible", homepageApi.SetItemVisible)
registerGinRoute(homepage, "POST", "Set item favorite", "/set/item_favorite", homepageApi.SetItemFavorite)
registerGinRoute(homepage, "POST", "Set item sort order", "/set/item_sort_order", homepageApi.SetItemSortOrder)
registerGinRoute(homepage, "POST", "Set item all sort order", "/set/item_all_sort_order", homepageApi.SetItemAllSortOrder)
registerGinRoute(homepage, "POST", "Set item fav sort order", "/set/item_fav_sort_order", homepageApi.SetItemFavSortOrder)
registerGinRoute(homepage, "POST", "Set category order", "/set/category_order", homepageApi.SetCategoryOrder)
registerGinRoute(homepage, "POST", "Item click", "/item_click", homepageApi.ItemClick)
}
cert := v1.Group("/cert")
{
registerGinRoute(cert, "GET", "Get cert info", "/info", certApi.Info)
registerGinRoute(cert, "GET", "Renew cert", "/renew", certApi.Renew)
}
agent := v1.Group("/agent")
{
registerGinRoute(agent, "GET", "List agents", "/list", agentApi.List)
registerGinRoute(agent, "POST", "Create agent", "/create", agentApi.Create)
registerGinRoute(agent, "POST", "Verify agent", "/verify", agentApi.Verify)
}
metrics := v1.Group("/metrics")
{
registerGinRoute(metrics, "GET", "Get system info", "/system_info", metricsApi.SystemInfo)
registerGinRoute(metrics, "GET", "Get all system info", "/all_system_info", metricsApi.AllSystemInfo)
registerGinRoute(metrics, "GET", "Get uptime", "/uptime", metricsApi.Uptime)
}
docker := v1.Group("/docker")
{
registerGinRoute(docker, "GET", "Get container", "/container/:id", dockerApi.GetContainer)
registerGinRoute(docker, "GET", "List containers", "/containers", dockerApi.Containers)
registerGinRoute(docker, "GET", "Get docker info", "/info", dockerApi.Info)
registerGinRoute(docker, "GET", "Get docker logs", "/logs/:id", dockerApi.Logs)
registerGinRoute(docker, "POST", "Start docker container", "/start", dockerApi.Start)
registerGinRoute(docker, "POST", "Stop docker container", "/stop", dockerApi.Stop)
registerGinRoute(docker, "POST", "Restart docker container", "/restart", dockerApi.Restart)
}
}
return r
}
func AuthBlockPageHandler(w http.ResponseWriter, r *http.Request) {
auth.WriteBlockPage(w, http.StatusForbidden, "Forbidden", "Login", "/login")
}

7
cmd/debug_page_prod.go Normal file
View File

@@ -0,0 +1,7 @@
//go:build production
package main
func listenDebugServer() {
// no-op
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/yusing/godoxy/internal/metrics/systeminfo"
"github.com/yusing/godoxy/internal/metrics/uptime"
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/route/rules"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/server"
"github.com/yusing/goutils/task"
@@ -58,9 +59,12 @@ func main() {
}
config.StartProxyServers()
if err := auth.Initialize(); err != nil {
log.Fatal().Err(err).Msg("failed to initialize authentication")
}
rules.InitAuthHandler(auth.AuthOrProceed)
// API Handler needs to start after auth is initialized.
server.StartServer(task.RootTask("api_server", false), server.Options{
Name: "api",
@@ -68,6 +72,8 @@ func main() {
Handler: api.NewHandler(),
})
listenDebugServer()
uptime.Poller.Start()
config.WatchChanges()

View File

@@ -88,6 +88,12 @@ entrypoint:
# - name: default
# do: proxy http://other-proxy:8080
defaults:
healthcheck:
interval: 5s
timeout: 15s
retries: 3
providers:
# include files are standalone yaml files under `config/` directory
#

107
go.mod
View File

@@ -1,24 +1,25 @@
module github.com/yusing/godoxy
go 1.25.4
go 1.25.5
replace github.com/yusing/godoxy/agent => ./agent
replace github.com/yusing/godoxy/internal/dnsproviders => ./internal/dnsproviders
replace github.com/coreos/go-oidc/v3 => ./internal/go-oidc
replace github.com/shirou/gopsutil/v4 => ./internal/gopsutil
replace github.com/yusing/goutils => ./goutils
replace (
github.com/coreos/go-oidc/v3 => ./internal/go-oidc
github.com/shirou/gopsutil/v4 => ./internal/gopsutil
github.com/yusing/godoxy/agent => ./agent
github.com/yusing/godoxy/internal/dnsproviders => ./internal/dnsproviders
github.com/yusing/goutils => ./goutils
github.com/yusing/goutils/http/reverseproxy => ./goutils/http/reverseproxy
github.com/yusing/goutils/http/websocket => ./goutils/http/websocket
github.com/yusing/goutils/server => ./goutils/server
)
require (
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon
github.com/coreos/go-oidc/v3 v3.16.0 // oidc authentication
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
github.com/fsnotify/fsnotify v1.9.0 // file watcher
github.com/gin-gonic/gin v1.11.0 // api server
github.com/go-acme/lego/v4 v4.28.1 // acme client
github.com/go-playground/validator/v10 v10.28.0 // validator
github.com/go-acme/lego/v4 v4.30.1 // acme client
github.com/go-playground/validator/v10 v10.30.1 // validator
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
github.com/gotify/server/v2 v2.7.3 // reference the Message struct for json response
@@ -27,37 +28,40 @@ require (
github.com/puzpuzpuz/xsync/v4 v4.2.0 // lock free map for concurrent operations
github.com/rs/zerolog v1.34.0 // logging
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
golang.org/x/crypto v0.44.0 // encrypting password with bcrypt
golang.org/x/net v0.47.0 // HTTP header utilities
golang.org/x/oauth2 v0.33.0 // oauth2 authentication
golang.org/x/sync v0.18.0
golang.org/x/crypto v0.46.0 // encrypting password with bcrypt
golang.org/x/net v0.48.0 // HTTP header utilities
golang.org/x/oauth2 v0.34.0 // oauth2 authentication
golang.org/x/sync v0.19.0
golang.org/x/time v0.14.0 // time utilities
)
require (
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
github.com/bytedance/sonic v1.14.2 // fast json parsing
github.com/docker/cli v29.0.1+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
github.com/goccy/go-yaml v1.18.0 // yaml parsing for different config files
github.com/docker/cli v29.1.3+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
github.com/goccy/go-yaml v1.19.1 // yaml parsing for different config files
github.com/golang-jwt/jwt/v5 v5.3.0 // jwt authentication
github.com/luthermonson/go-proxmox v0.2.3 // proxmox API client
github.com/luthermonson/go-proxmox v0.2.4 // proxmox API client
github.com/moby/moby/api v1.52.0 // docker API
github.com/moby/moby/client v0.1.0 // docker client
github.com/moby/moby/client v0.2.1 // docker client
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
github.com/quic-go/quic-go v0.56.0 // http3 support
github.com/shirou/gopsutil/v4 v4.25.10 // system information
github.com/quic-go/quic-go v0.58.0 // http3 support
github.com/shirou/gopsutil/v4 v4.25.11 // system information
github.com/spf13/afero v1.15.0 // afero for file system operations
github.com/stretchr/testify v1.11.1 // testing framework
github.com/valyala/fasthttp v1.68.0 // fast http for health check
github.com/yusing/ds v0.3.1 // data structures and algorithms
github.com/yusing/godoxy/agent v0.0.0-20251114142829-a291a49a0e42
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20251114142829-a291a49a0e42
github.com/yusing/godoxy/agent v0.0.0-20251230135310-5087800fd763
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20251230043958-dba8441e8a5d
github.com/yusing/gointernals v0.1.16
github.com/yusing/goutils v0.7.0
github.com/yusing/goutils/http/reverseproxy v0.0.0-20251217162119-cb0f79b51ce2
github.com/yusing/goutils/http/websocket v0.0.0-20251217162119-cb0f79b51ce2
github.com/yusing/goutils/server v0.0.0-20251217162119-cb0f79b51ce2
)
require (
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth v0.18.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
@@ -79,7 +83,7 @@ require (
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@@ -89,7 +93,7 @@ require (
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
@@ -99,7 +103,7 @@ require (
github.com/magefile/mage v1.15.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/miekg/dns v1.1.69 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -112,29 +116,29 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/samber/lo v1.52.0 // indirect
github.com/samber/slog-common v0.19.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/sony/gobreaker v1.0.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.uber.org/atomic v1.11.0
go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/api v0.256.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/api v0.258.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
@@ -145,6 +149,7 @@ require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bytedance/sonic/loader v0.4.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
@@ -152,15 +157,15 @@ require (
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/go-resty/resty/v2 v2.16.5 // indirect
github.com/go-resty/resty/v2 v2.17.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/linode/linodego v1.61.0 // indirect
github.com/linode/linodego v1.63.0 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/stretchr/objx v0.5.3 // indirect
@@ -170,9 +175,7 @@ require (
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.24.0 // indirect
github.com/vultr/govultr/v3 v3.26.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/arch v0.23.0 // indirect
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
)

168
go.sum
View File

@@ -1,5 +1,5 @@
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
@@ -54,6 +54,8 @@ github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2N
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/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/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@@ -71,8 +73,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v29.0.1+incompatible h1:EnvMEAR9Ro5xQEKbMitlabj5vCDY0vwcDyY/Lsow7FQ=
github.com/docker/cli v29.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.1.3+incompatible h1:+kz9uDWgs+mAaIZojWfFt4d53/jv0ZUOOoSh5ZnH36c=
github.com/docker/cli v29.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/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=
@@ -87,14 +89,14 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/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.28.1 h1:zt301JYF51UIEkpSXsdeGq9hRePeFzQCq070OdAmP0Q=
github.com/go-acme/lego/v4 v4.28.1/go.mod h1:bzjilr03IgbaOwlH396hq5W56Bi0/uoRwW/JM8hP7m4=
github.com/go-acme/lego/v4 v4.30.1 h1:tmb6U0lvy8Mc3lQbqKwTat7oAhE8FUYNJ3D0gSg6pJU=
github.com/go-acme/lego/v4 v4.30.1/go.mod h1:V7m/Ip+EeFkjOe028+zeH+SwWtESxw1LHelwMIfAjm4=
github.com/go-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=
@@ -113,18 +115,18 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
@@ -132,12 +134,11 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
@@ -145,8 +146,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotify/server/v2 v2.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
@@ -169,8 +170,8 @@ github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -181,14 +182,14 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/linode/linodego v1.61.0 h1:9g20NWl+/SbhDFj6X5EOZXtM2hBm1Mx8I9h8+F3l1LM=
github.com/linode/linodego v1.61.0/go.mod h1:64o30geLNwR0NeYh5HM/WrVCBXcSqkKnRK3x9xoRuJI=
github.com/linode/linodego v1.63.0 h1:MdjizfXNJDVJU6ggoJmMO5O9h4KGPGivNX0fzrAnstk=
github.com/linode/linodego v1.63.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/luthermonson/go-proxmox v0.2.3 h1:NAjUJ5Jd1ynIK6UHMGd/VLGgNZWpGXhfL+DBmAVSEaA=
github.com/luthermonson/go-proxmox v0.2.3/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
github.com/luthermonson/go-proxmox v0.2.4 h1:XQ6YNUTVvHS7N4EJxWpuqWLW2s1VPtsIblxLV/rGHLw=
github.com/luthermonson/go-proxmox v0.2.4/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=
@@ -200,16 +201,16 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
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.1.0 h1:nt+hn6O9cyJQqq5UWnFGqsZRTS/JirUqzPjEl0Bdc/8=
github.com/moby/moby/client v0.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
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/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=
@@ -217,10 +218,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
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/oci-go-sdk/common/v1065 v1065.104.1 h1:kR8dHXC53heV4fttilXXkDkGmkdC5bvQ2XgbVBoD+ns=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1 h1:559XLHTF3F1A0J03PCIk6LlR0G9CmhHEDCm/TSk5BWQ=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1/go.mod h1:1FfSn6xcdK+wrNxUhtAAhLjYrwk9z8X3p3CNHwTc3zQ=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 h1:l0tH15ACQADZAzC+LZ+mo2tIX4H6uZu0ulrVmG5Tqz0=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 h1:gzB4c6ztb38C/jYiqEaFC+mCGcWFHDji9e6jwymY9d4=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2/go.mod h1:l1qIPIq2uRV5WTSvkbhbl/ndbeOu7OCb3UZ+0+2ZSb8=
github.com/nrdcg/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=
@@ -249,10 +250,10 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
@@ -264,8 +265,8 @@ github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
@@ -303,8 +304,8 @@ github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFn
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
github.com/vultr/govultr/v3 v3.24.0 h1:fTTTj0VBve+Miy+wGhlb90M2NMDfpGFi6Frlj3HVy6M=
github.com/vultr/govultr/v3 v3.24.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
@@ -320,22 +321,22 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
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.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
@@ -346,15 +347,15 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -364,10 +365,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -375,8 +376,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -396,8 +397,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -416,8 +417,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -426,24 +427,23 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba h1:Ze6qXW0j37YCqZdCD2LkzVSxgEWez0cO4NUyd44DiDY=
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:4FLPzLA8eGAktPOTemJGDgDYRpLYwrNu4u2JtWINhnI=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

Submodule goutils updated: cfb8d74b17...51a75d684b

View File

@@ -14,7 +14,6 @@ import (
"github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/maxmind"
"github.com/yusing/godoxy/internal/notif"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/task"
@@ -82,7 +81,7 @@ var ActiveConfig atomic.Pointer[Config]
const cacheTTL = 1 * time.Minute
func (c *checkCache) Expired() bool {
return c.created.Add(cacheTTL).Before(utils.TimeNow())
return c.created.Add(cacheTTL).Before(time.Now())
}
// TODO: add stats
@@ -180,7 +179,7 @@ func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool) {
c.ipCache.Store(info.Str, &checkCache{
IPInfo: info,
allow: allow,
created: utils.TimeNow(),
created: time.Now(),
})
}

View File

@@ -29,13 +29,13 @@ func GetContainer(c *gin.Context) {
return
}
dockerHost, ok := docker.GetDockerHostByContainerID(id)
dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
if !ok {
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
return
}
dockerClient, err := docker.NewClient(dockerHost)
dockerClient, err := docker.NewClient(dockerCfg)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
return
@@ -55,7 +55,7 @@ func GetContainer(c *gin.Context) {
}
c.JSON(http.StatusOK, &Container{
Server: dockerHost,
Server: dockerCfg.URL,
Name: cont.Container.Name,
ID: cont.Container.ID,
Image: cont.Container.Image,

View File

@@ -57,13 +57,13 @@ func Logs(c *gin.Context) {
}
// TODO: implement levels
dockerHost, ok := docker.GetDockerHostByContainerID(id)
dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
if !ok {
c.JSON(http.StatusNotFound, apitypes.Error(fmt.Sprintf("container %s not found", id)))
return
}
dockerClient, err := docker.NewClient(dockerHost)
dockerClient, err := docker.NewClient(dockerCfg)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to get docker client"))
return
@@ -105,7 +105,7 @@ func Logs(c *gin.Context) {
return
}
log.Err(err).
Str("server", dockerHost).
Str("server", dockerCfg.URL).
Str("container", id).
Msg("failed to de-multiplex logs")
}

View File

@@ -34,13 +34,13 @@ func Restart(c *gin.Context) {
return
}
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
dockerCfg, ok := docker.GetDockerCfgByContainerID(req.ID)
if !ok {
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
return
}
client, err := docker.NewClient(dockerHost)
client, err := docker.NewClient(dockerCfg)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
return

View File

@@ -34,13 +34,13 @@ func Start(c *gin.Context) {
return
}
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
dockerCfg, ok := docker.GetDockerCfgByContainerID(req.ID)
if !ok {
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
return
}
client, err := docker.NewClient(dockerHost)
client, err := docker.NewClient(dockerCfg)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
return

View File

@@ -34,13 +34,13 @@ func Stop(c *gin.Context) {
return
}
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
dockerCfg, ok := docker.GetDockerCfgByContainerID(req.ID)
if !ok {
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
return
}
client, err := docker.NewClient(dockerHost)
client, err := docker.NewClient(dockerCfg)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
return

View File

@@ -2458,8 +2458,8 @@
"x-nullable": false,
"x-omitempty": false
},
"docker_host": {
"type": "string",
"docker_cfg": {
"$ref": "#/definitions/DockerProviderConfig",
"x-nullable": false,
"x-omitempty": false
},
@@ -2715,7 +2715,7 @@
"required": [
"container_id",
"container_name",
"docker_host"
"docker_cfg"
],
"properties": {
"container_id": {
@@ -2728,7 +2728,24 @@
"x-nullable": false,
"x-omitempty": false
},
"docker_host": {
"docker_cfg": {
"$ref": "#/definitions/DockerProviderConfig",
"x-nullable": false,
"x-omitempty": false
}
},
"x-nullable": false,
"x-omitempty": false
},
"DockerProviderConfig": {
"type": "object",
"properties": {
"tls": {
"$ref": "#/definitions/DockerTLSConfig",
"x-nullable": false,
"x-omitempty": false
},
"url": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
@@ -2737,6 +2754,27 @@
"x-nullable": false,
"x-omitempty": false
},
"DockerTLSConfig": {
"type": "object",
"required": [
"ca_file"
],
"properties": {
"ca_file": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"cert_file": {
"type": "string"
},
"key_file": {
"type": "string"
}
},
"x-nullable": false,
"x-omitempty": false
},
"ErrorResponse": {
"type": "object",
"properties": {
@@ -2881,7 +2919,7 @@
"x-omitempty": false
},
"retries": {
"description": "<0: immediate, >=0: threshold",
"description": "<0: immediate, 0: default, >0: threshold",
"type": "integer",
"x-nullable": false,
"x-omitempty": false
@@ -3430,6 +3468,11 @@
"x-nullable": false,
"x-omitempty": false
},
"no_loading_page": {
"type": "boolean",
"x-nullable": false,
"x-omitempty": false
},
"proxmox": {
"$ref": "#/definitions/ProxmoxConfig",
"x-nullable": false,
@@ -4234,6 +4277,12 @@
],
"x-nullable": true
},
"index": {
"description": "Index file to serve for single-page app mode",
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"load_balance": {
"allOf": [
{
@@ -4315,6 +4364,12 @@
"x-nullable": false,
"x-omitempty": false
},
"spa": {
"description": "Single-page app mode: serves index for non-existent paths",
"type": "boolean",
"x-nullable": false,
"x-omitempty": false
},
"ssl_certificate": {
"description": "Path to client certificate",
"type": "string",
@@ -4500,6 +4555,11 @@
"x-nullable": false,
"x-omitempty": false
},
"is_excluded": {
"type": "boolean",
"x-nullable": false,
"x-omitempty": false
},
"statuses": {
"type": "array",
"items": {
@@ -5354,6 +5414,12 @@
],
"x-nullable": true
},
"index": {
"description": "Index file to serve for single-page app mode",
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"load_balance": {
"allOf": [
{
@@ -5435,6 +5501,12 @@
"x-nullable": false,
"x-omitempty": false
},
"spa": {
"description": "Single-page app mode: serves index for non-existent paths",
"type": "boolean",
"x-nullable": false,
"x-omitempty": false
},
"ssl_certificate": {
"description": "Path to client certificate",
"type": "string",

View File

@@ -57,8 +57,8 @@ definitions:
type: string
container_name:
type: string
docker_host:
type: string
docker_cfg:
$ref: '#/definitions/DockerProviderConfig'
errors:
type: string
idlewatcher_config:
@@ -192,12 +192,30 @@ definitions:
type: string
container_name:
type: string
docker_host:
type: string
docker_cfg:
$ref: '#/definitions/DockerProviderConfig'
required:
- container_id
- container_name
- docker_host
- docker_cfg
type: object
DockerProviderConfig:
properties:
tls:
$ref: '#/definitions/DockerTLSConfig'
url:
type: string
type: object
DockerTLSConfig:
properties:
ca_file:
type: string
cert_file:
type: string
key_file:
type: string
required:
- ca_file
type: object
ErrorResponse:
properties:
@@ -269,7 +287,7 @@ definitions:
path:
type: string
retries:
description: '<0: immediate, >=0: threshold'
description: '<0: immediate, 0: default, >0: threshold'
type: integer
timeout:
type: integer
@@ -517,6 +535,8 @@ definitions:
description: "0: no idle watcher.\nPositive: idle watcher with idle timeout.\nNegative:
idle watcher as a dependency.\tIdleTimeout time.Duration `json:\"idle_timeout\"
json_ext:\"duration\"`"
no_loading_page:
type: boolean
proxmox:
$ref: '#/definitions/ProxmoxConfig'
start_endpoint:
@@ -897,6 +917,9 @@ definitions:
allOf:
- $ref: '#/definitions/IdlewatcherConfig'
x-nullable: true
index:
description: Index file to serve for single-page app mode
type: string
load_balance:
allOf:
- $ref: '#/definitions/LoadBalancerConfig'
@@ -944,6 +967,9 @@ definitions:
- udp
- fileserver
type: string
spa:
description: 'Single-page app mode: serves index for non-existent paths'
type: boolean
ssl_certificate:
description: Path to client certificate
type: string
@@ -1030,6 +1056,8 @@ definitions:
type: number
is_docker:
type: boolean
is_excluded:
type: boolean
statuses:
items:
$ref: '#/definitions/RouteStatus'
@@ -1504,6 +1532,9 @@ definitions:
allOf:
- $ref: '#/definitions/IdlewatcherConfig'
x-nullable: true
index:
description: Index file to serve for single-page app mode
type: string
load_balance:
allOf:
- $ref: '#/definitions/LoadBalancerConfig'
@@ -1551,6 +1582,9 @@ definitions:
- udp
- fileserver
type: string
spa:
description: 'Single-page app mode: serves index for non-existent paths'
type: boolean
ssl_certificate:
description: Path to client certificate
type: string

View File

@@ -13,8 +13,9 @@ import (
)
type GetFavIconRequest struct {
URL string `form:"url" binding:"required_without=Alias"`
Alias string `form:"alias" binding:"required_without=URL"`
URL string `form:"url" binding:"required_without=Alias"`
Alias string `form:"alias" binding:"required_without=URL"`
Variant homepage.IconVariant `form:"variant" binding:"omitempty,oneof=light dark"`
} // @name GetFavIconRequest
// @x-id "favicon"
@@ -46,7 +47,11 @@ func FavIcon(c *gin.Context) {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid url", err))
return
}
fetchResult, err := homepage.FetchFavIconFromURL(c.Request.Context(), &iconURL)
icon := &iconURL
if request.Variant != homepage.IconVariantNone {
icon = icon.WithVariant(request.Variant)
}
fetchResult, err := homepage.FetchFavIconFromURL(c.Request.Context(), icon)
if err != nil {
homepage.GinFetchError(c, fetchResult.StatusCode, err)
return
@@ -56,7 +61,7 @@ func FavIcon(c *gin.Context) {
}
// try with alias
result, err := GetFavIconFromAlias(c.Request.Context(), request.Alias)
result, err := GetFavIconFromAlias(c.Request.Context(), request.Alias, request.Variant)
if err != nil {
homepage.GinFetchError(c, result.StatusCode, err)
return
@@ -65,7 +70,7 @@ func FavIcon(c *gin.Context) {
}
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
func GetFavIconFromAlias(ctx context.Context, alias string) (homepage.FetchResult, error) {
func GetFavIconFromAlias(ctx context.Context, alias string, variant homepage.IconVariant) (homepage.FetchResult, error) {
// try with route.Icon
r, ok := routes.HTTP.Get(alias)
if !ok {
@@ -79,13 +84,19 @@ func GetFavIconFromAlias(ctx context.Context, alias string) (homepage.FetchResul
hp := r.HomepageItem()
if hp.Icon != nil {
if hp.Icon.IconSource == homepage.IconSourceRelative {
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL)
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL, variant)
} else if variant != homepage.IconVariantNone {
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(variant))
if err != nil {
// fallback to no variant
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(homepage.IconVariantNone))
}
} else {
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon)
}
} else {
// try extract from "link[rel=icon]"
result, err = homepage.FindIcon(ctx, r, "/")
result, err = homepage.FindIcon(ctx, r, "/", variant)
}
if result.StatusCode == 0 {
result.StatusCode = http.StatusOK

View File

@@ -6,8 +6,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/utils"
apitypes "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/fs"
)
type ListFilesResponse struct {
@@ -35,7 +35,7 @@ func List(c *gin.Context) {
}
// config/
files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
files, err := fs.ListFiles(common.ConfigBasePath, 0, true)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to list files"))
return
@@ -48,7 +48,7 @@ func List(c *gin.Context) {
}
// config/middlewares/
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
mids, err := fs.ListFiles(common.MiddlewareComposeBasePath, 0, true)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to list files"))
return

View File

@@ -12,6 +12,7 @@ import (
"github.com/yusing/godoxy/internal/route/rules"
apitypes "github.com/yusing/goutils/apitypes"
gperr "github.com/yusing/goutils/errs"
httputils "github.com/yusing/goutils/http"
)
type RawRule struct {
@@ -348,7 +349,7 @@ func checkMatchedRules(rulesList rules.Rules, w http.ResponseWriter, r *http.Req
var matched []string
// Create a ResponseModifier to properly check rules
rm := rules.NewResponseModifier(w)
rm := httputils.NewResponseModifier(w)
for _, rule := range rulesList {
// Check if rule matches

View File

@@ -34,12 +34,12 @@ func Routes(c *gin.Context) {
provider := c.Query("provider")
if provider == "" {
c.JSON(http.StatusOK, slices.Collect(routes.Iter))
c.JSON(http.StatusOK, slices.Collect(routes.IterAll))
return
}
rts := make([]types.Route, 0, routes.NumRoutes())
for r := range routes.Iter {
rts := make([]types.Route, 0, routes.NumAllRoutes())
for r := range routes.IterAll {
if r.ProviderName() == provider {
rts = append(rts, r)
}
@@ -51,14 +51,14 @@ func RoutesWS(c *gin.Context) {
provider := c.Query("provider")
if provider == "" {
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
return slices.Collect(routes.Iter), nil
return slices.Collect(routes.IterAll), nil
})
return
}
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
rts := make([]types.Route, 0, routes.NumRoutes())
for r := range routes.Iter {
rts := make([]types.Route, 0, routes.NumAllRoutes())
for r := range routes.IterAll {
if r.ProviderName() == provider {
rts = append(rts, r)
}

View File

@@ -65,14 +65,12 @@ func AuthCheckHandler(w http.ResponseWriter, r *http.Request) {
func AuthOrProceed(w http.ResponseWriter, r *http.Request) (proceed bool) {
if defaultAuth == nil {
w.WriteHeader(http.StatusServiceUnavailable)
return false
return true
}
err := defaultAuth.CheckToken(r)
if err != nil {
defaultAuth.LoginHandler(w, r)
return false
} else {
return true
}
return true
}

View File

@@ -12,11 +12,12 @@ var blockPageHTML string
var blockPageTemplate = template.Must(template.New("block_page").Parse(blockPageHTML))
func WriteBlockPage(w http.ResponseWriter, status int, error string, logoutURL string) {
func WriteBlockPage(w http.ResponseWriter, status int, errorMessage, actionText, actionURL string) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
blockPageTemplate.Execute(w, map[string]string{
"StatusText": http.StatusText(status),
"Error": error,
"LogoutURL": logoutURL,
"Error": errorMessage,
"ActionURL": actionURL,
"ActionText": actionText,
})
}

View File

@@ -1,14 +1,231 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Access Denied</title>
</head>
<body>
<h1>{{.StatusText}}</h1>
<p>{{.Error}}</p>
<a href="{{.LogoutURL}}">Logout</a>
</body>
<meta name="color-scheme" content="dark" />
<style>
:root {
color-scheme: dark;
--bg0: #070a12;
--bg1: #0b1020;
--card: rgba(255, 255, 255, 0.055);
--card2: rgba(255, 255, 255, 0.05);
--text: rgba(255, 255, 255, 0.92);
--muted: rgba(255, 255, 255, 0.68);
--border: rgba(255, 255, 255, 0.12);
--borderSoft: rgba(255, 255, 255, 0.08);
--borderStrong: rgba(255, 255, 255, 0.14);
--borderHover: rgba(255, 255, 255, 0.22);
--shadow: 0 22px 60px rgba(0, 0, 0, 0.55);
--shadowCard: 0 22px 60px rgba(0, 0, 0, 0.58);
--shadowButton: 0 12px 28px rgba(0, 0, 0, 0.35);
--insetHighlight: inset 0 1px 0 rgba(255, 255, 255, 0.04);
--ring: rgba(120, 160, 210, 0.42);
--accent0: #7aa3c8;
--accent1: #9a8bc7;
--btn: rgba(255, 255, 255, 0.06);
--btnHover: rgba(255, 255, 255, 0.08);
}
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
}
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto,
Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji;
color: var(--text);
background-color: var(--bg1);
background-image: none;
}
.wrap {
min-height: 100%;
display: grid;
place-items: center;
padding: 28px 16px;
}
.card {
width: min(720px, 100%);
background: var(--card);
border: 1px solid var(--border);
border-radius: 16px;
box-shadow: var(--shadowCard), var(--insetHighlight);
overflow: hidden;
}
.topbar {
display: flex;
align-items: center;
gap: 12px;
padding: 18px 18px 12px;
border-bottom: 1px solid var(--borderSoft);
background: var(--card2);
}
.badge {
width: 38px;
height: 38px;
border-radius: 12px;
display: grid;
place-items: center;
border: 1px solid var(--borderStrong);
background: var(--card2);
}
.badge svg {
opacity: 0.95;
}
.badge .bang {
font-size: 22px;
line-height: 1;
font-weight: 700;
color: rgba(255, 255, 255, 0.9);
transform: translateY(-1px);
}
h1 {
margin: 0;
font-size: 18px;
line-height: 1.25;
letter-spacing: 0.2px;
}
.sub {
margin: 2px 0 0;
font-size: 13px;
color: var(--muted);
}
.content {
padding: 18px;
}
.error {
margin: 0;
padding: 14px 14px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.25);
color: rgba(255, 255, 255, 0.8);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
Liberation Mono, Courier New, monospace;
font-size: 13px;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
text-transform: capitalize;
}
.actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
margin-top: 14px;
}
a.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 12px;
border-radius: 10px;
font-size: 14px;
text-decoration: none;
color: rgba(255, 255, 255, 0.92);
border: 1px solid var(--borderStrong);
background: var(--btn);
transition: transform 120ms ease, border-color 120ms ease,
background 120ms ease, box-shadow 120ms ease;
box-shadow: var(--shadowButton);
}
a.button:hover {
transform: translateY(-1px);
border-color: var(--borderHover);
background: var(--btnHover);
}
a.button:focus-visible {
outline: 0;
box-shadow: 0 0 0 3px var(--ring), var(--shadowButton);
}
.hint {
color: var(--muted);
font-size: 12px;
line-height: 1.4;
}
.hint kbd {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
Liberation Mono, Courier New, monospace;
font-size: 11px;
padding: 2px 4px;
border-radius: 6px;
border: 1px solid var(--borderStrong);
background: var(--btn);
color: rgba(255, 255, 255, 0.86);
}
kbd {
font-weight: 500;
}
.kbd-container {
display: inline-flex;
gap: 2px;
align-items: center;
}
</style>
</head>
<body>
<div class="wrap">
<main class="card" role="main" aria-labelledby="title">
<header class="topbar">
<div class="badge" aria-hidden="true">
<span class="bang">!</span>
</div>
<div>
<h1 id="title">{{.StatusText}}</h1>
<p class="sub">
You dont have permission to access this resource.
</p>
</div>
</header>
<section class="content">
<pre class="error">{{.Error}}</pre>
<div class="actions">
<a class="button" href="{{.ActionURL}}">
<span>{{.ActionText}}</span>
<span aria-hidden="true"></span>
</a>
<div class="hint">
If you just signed in, try refreshing the page.
<span aria-hidden="true"> </span>
<div class="kbd-container">
<kbd>Ctrl</kbd>
<span>+</span>
<kbd>R</kbd>
</div>
</div>
</div>
</section>
</main>
</div>
</body>
</html>

View File

@@ -32,6 +32,8 @@ type (
allowedUsers []string
allowedGroups []string
rateLimit *rate.Limiter
onUnknownPathHandler http.HandlerFunc
}
@@ -66,9 +68,9 @@ func (auth *OIDCProvider) getAppScopedCookieName(baseName string) string {
const (
OIDCAuthInitPath = "/"
OIDCAuthBasePath = "/auth"
OIDCPostAuthPath = OIDCAuthBasePath + "/callback"
OIDCLogoutPath = OIDCAuthBasePath + "/logout"
OIDCAuthBasePath = "/auth/"
OIDCPostAuthPath = OIDCAuthBasePath + "callback"
OIDCLogoutPath = OIDCAuthBasePath + "logout"
)
var (
@@ -123,6 +125,7 @@ func NewOIDCProvider(issuerURL, clientID, clientSecret string, allowedUsers, all
endSessionURL: endSessionURL,
allowedUsers: allowedUsers,
allowedGroups: allowedGroups,
rateLimit: rate.NewLimiter(rate.Every(common.OIDCRateLimitPeriod), common.OIDCRateLimit),
}, nil
}
@@ -165,6 +168,7 @@ func NewOIDCProviderWithCustomClient(baseProvider *OIDCProvider, clientID, clien
endSessionURL: baseProvider.endSessionURL,
allowedUsers: baseProvider.allowedUsers,
allowedGroups: baseProvider.allowedGroups,
rateLimit: baseProvider.rateLimit,
}, nil
}
@@ -228,9 +232,12 @@ func (auth *OIDCProvider) HandleAuth(w http.ResponseWriter, r *http.Request) {
}
}
var rateLimit = rate.NewLimiter(rate.Every(time.Second), 1)
func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
if !httputils.GetAccept(r.Header).AcceptHTML() {
http.Error(w, "authentication is required", http.StatusForbidden)
return
}
// check for session token
sessionToken, err := r.Cookie(auth.getAppScopedCookieName(CookieOauthSessionToken))
if err == nil { // session token exists
@@ -250,8 +257,8 @@ func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
return
}
if !rateLimit.Allow() {
http.Error(w, "auth rate limit exceeded", http.StatusTooManyRequests)
if !auth.rateLimit.Allow() {
WriteBlockPage(w, http.StatusTooManyRequests, "auth rate limit exceeded", "Try again", OIDCAuthInitPath)
return
}
@@ -318,34 +325,39 @@ func (auth *OIDCProvider) PostAuthCallbackHandler(w http.ResponseWriter, r *http
// verify state
state, err := r.Cookie(auth.getAppScopedCookieName(CookieOauthState))
if err != nil {
http.Error(w, "missing state cookie", http.StatusBadRequest)
auth.clearCookie(w, r)
WriteBlockPage(w, http.StatusBadRequest, "missing state cookie", "Back to Login", OIDCAuthInitPath)
return
}
if r.URL.Query().Get("state") != state.Value {
http.Error(w, "invalid oauth state", http.StatusBadRequest)
auth.clearCookie(w, r)
WriteBlockPage(w, http.StatusBadRequest, "invalid oauth state", "Back to Login", OIDCAuthInitPath)
return
}
code := r.URL.Query().Get("code")
oauth2Token, err := auth.oauthConfig.Exchange(r.Context(), code, optRedirectPostAuth(r))
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
httputils.LogError(r).Msg(fmt.Sprintf("failed to exchange token: %v", err))
auth.clearCookie(w, r)
WriteBlockPage(w, http.StatusInternalServerError, "failed to exchange token", "Try again", OIDCAuthInitPath)
httputils.LogError(r).Msgf("failed to exchange token: %v", err)
return
}
idTokenJWT, idToken, err := auth.getIDToken(r.Context(), oauth2Token)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
httputils.LogError(r).Msg(fmt.Sprintf("failed to get ID token: %v", err))
auth.clearCookie(w, r)
WriteBlockPage(w, http.StatusInternalServerError, "failed to get ID token", "Try again", OIDCAuthInitPath)
httputils.LogError(r).Msgf("failed to get ID token: %v", err)
return
}
if oauth2Token.RefreshToken != "" {
claims, err := parseClaims(idToken)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
httputils.LogError(r).Msg(fmt.Sprintf("failed to parse claims: %v", err))
auth.clearCookie(w, r)
WriteBlockPage(w, http.StatusInternalServerError, "failed to parse claims", "Try again", OIDCAuthInitPath)
httputils.LogError(r).Msgf("failed to parse claims: %v", err)
return
}
session := newSession(claims.Username, claims.Groups)

View File

@@ -15,18 +15,18 @@ import (
"github.com/go-acme/lego/v4/lego"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
)
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"`
ACMEKeyPath string `json:"acme_key_path,omitempty"`
Provider string `json:"provider,omitempty"`
Options map[string]any `json:"options,omitempty"`
Email string `json:"email,omitempty"`
Domains []string `json:"domains,omitempty"`
CertPath string `json:"cert_path,omitempty"`
KeyPath string `json:"key_path,omitempty"`
ACMEKeyPath string `json:"acme_key_path,omitempty"`
Provider string `json:"provider,omitempty"`
Options map[string]strutils.Redacted `json:"options,omitempty"`
Resolvers []string `json:"resolvers,omitempty"`
@@ -96,7 +96,7 @@ func (cfg *Config) Validate() gperr.Error {
if cfg.Provider != ProviderCustom {
b.Add(ErrUnknownProvider.
Subject(cfg.Provider).
With(gperr.DoYouMean(utils.NearestField(cfg.Provider, Providers))))
With(gperr.DoYouMeanField(cfg.Provider, Providers)))
}
} else {
provider, err := providerConstructor(cfg.Options)

View File

@@ -4,9 +4,10 @@ import (
"github.com/go-acme/lego/v4/challenge"
"github.com/yusing/godoxy/internal/serialization"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
)
type Generator func(map[string]any) (challenge.Provider, gperr.Error)
type Generator func(map[string]strutils.Redacted) (challenge.Provider, gperr.Error)
var Providers = make(map[string]Generator)
@@ -14,10 +15,10 @@ func DNSProvider[CT any, PT challenge.Provider](
defaultCfg func() *CT,
newProvider func(*CT) (PT, error),
) Generator {
return func(opt map[string]any) (challenge.Provider, gperr.Error) {
return func(opt map[string]strutils.Redacted) (challenge.Provider, gperr.Error) {
cfg := defaultCfg()
if len(opt) > 0 {
err := serialization.MapUnmarshalValidate(opt, &cfg)
err := serialization.MapUnmarshalValidate(serialization.ToSerializedObject(opt), &cfg)
if err != nil {
return nil, err
}

View File

@@ -1,9 +1,5 @@
package common
import (
"time"
)
// file, folder structure
const (
@@ -38,10 +34,6 @@ var RequiredDirectories = []string{
const DockerHostFromEnv = "$DOCKER_HOST"
const (
HealthCheckIntervalDefault = 5 * time.Second
HealthCheckTimeoutDefault = 5 * time.Second
HealthCheckDownNotifyDelayDefault = 15 * time.Second
WakeTimeoutDefault = "3m"
StopTimeoutDefault = "3m"
StopMethodDefault = "stop"

View File

@@ -39,12 +39,14 @@ var (
DebugDisableAuth = env.GetEnvBool("DEBUG_DISABLE_AUTH", false)
// OIDC Configuration.
OIDCIssuerURL = env.GetEnvString("OIDC_ISSUER_URL", "")
OIDCClientID = env.GetEnvString("OIDC_CLIENT_ID", "")
OIDCClientSecret = env.GetEnvString("OIDC_CLIENT_SECRET", "")
OIDCScopes = env.GetEnvCommaSep("OIDC_SCOPES", "openid, profile, email, groups")
OIDCAllowedUsers = env.GetEnvCommaSep("OIDC_ALLOWED_USERS", "")
OIDCAllowedGroups = env.GetEnvCommaSep("OIDC_ALLOWED_GROUPS", "")
OIDCIssuerURL = env.GetEnvString("OIDC_ISSUER_URL", "")
OIDCClientID = env.GetEnvString("OIDC_CLIENT_ID", "")
OIDCClientSecret = env.GetEnvString("OIDC_CLIENT_SECRET", "")
OIDCScopes = env.GetEnvCommaSep("OIDC_SCOPES", "openid, profile, email, groups")
OIDCAllowedUsers = env.GetEnvCommaSep("OIDC_ALLOWED_USERS", "")
OIDCAllowedGroups = env.GetEnvCommaSep("OIDC_ALLOWED_GROUPS", "")
OIDCRateLimit = env.GetEnvInt("OIDC_RATE_LIMIT", 10)
OIDCRateLimitPeriod = env.GetEnvDuation("OIDC_RATE_LIMIT_PERIOD", time.Second)
// metrics configuration
MetricsDisableCPU = env.GetEnvBool("METRICS_DISABLE_CPU", false)

View File

@@ -57,6 +57,8 @@ func Load() error {
panic(errors.New("config already loaded"))
}
state := NewState()
config.WorkingState.Store(state)
cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName)
initErr := state.InitFromFile(common.ConfigPath)
@@ -82,11 +84,13 @@ func Reload() gperr.Error {
defer reloadMu.Unlock()
newState := NewState()
config.WorkingState.Store(newState)
err := newState.InitFromFile(common.ConfigPath)
if err != nil {
newState.Task().FinishAndWait(err)
logNotifyError("reload", err)
return gperr.New(ansi.Warning("using last config")).With(err)
config.WorkingState.Store(GetState())
return gperr.Wrap(err, ansi.Warning("using last config"))
}
// flush temporary log
@@ -98,7 +102,6 @@ func Reload() gperr.Error {
SetState(newState)
if err := newState.StartProviders(); err != nil {
gperr.LogWarn("start providers error", err)
logNotifyError("start providers", err)
return nil // continue
}
@@ -113,7 +116,7 @@ func WatchChanges() {
configEventFlushInterval,
OnConfigChange,
func(err gperr.Error) {
gperr.LogError("config reload error", err)
logNotifyError("reload", err)
},
)
eventQueue.Start(cfgWatcher.Events(t.Context()))

View File

@@ -70,7 +70,6 @@ func SetState(state config.State) {
defer stateMu.Unlock()
cfg := state.Value()
config.ActiveConfig.Store(cfg)
config.ActiveState.Store(state)
acl.ActiveConfig.Store(cfg.ACL)
entrypoint.ActiveConfig.Store(&cfg.Entrypoint)
@@ -87,13 +86,13 @@ func HasState() bool {
}
func Value() *config.Config {
return config.ActiveConfig.Load()
return config.ActiveState.Load().Value()
}
func (state *state) InitFromFile(filename string) error {
data, err := os.ReadFile(common.ConfigPath)
if err != nil {
state.Config = *config.DefaultConfig()
state.Config = config.DefaultConfig()
return err
}
return state.Init(data)
@@ -319,9 +318,9 @@ func (state *state) loadRouteProviders() error {
})
}
for name, dockerHost := range providers.Docker {
for name, dockerCfg := range providers.Docker {
providersProducer.Go(func() {
providersCh <- route.NewDockerProvider(name, dockerHost)
providersCh <- route.NewDockerProvider(name, dockerCfg)
})
}

View File

@@ -2,7 +2,6 @@ package config
import (
"regexp"
"sync/atomic"
"github.com/go-playground/validator/v10"
"github.com/yusing/godoxy/agent/pkg/agent"
@@ -14,6 +13,7 @@ import (
"github.com/yusing/godoxy/internal/notif"
"github.com/yusing/godoxy/internal/proxmox"
"github.com/yusing/godoxy/internal/serialization"
"github.com/yusing/godoxy/internal/types"
gperr "github.com/yusing/goutils/errs"
)
@@ -25,32 +25,29 @@ type (
Providers Providers `json:"providers"`
MatchDomains []string `json:"match_domains" validate:"domain_name"`
Homepage homepage.Config `json:"homepage"`
Defaults Defaults `json:"defaults"`
TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"`
}
Defaults struct {
HealthCheck types.HealthCheckConfig `json:"healthcheck"`
}
Providers struct {
Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"`
Docker map[string]string `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys,dive,unix_addr|url"`
Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
Notification []*notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"`
Proxmox []proxmox.Config `json:"proxmox" yaml:"proxmox,omitempty"`
MaxMind *maxmind.Config `json:"maxmind" yaml:"maxmind,omitempty"`
Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"`
Docker map[string]types.DockerProviderConfig `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys"`
Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
Notification []*notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"`
Proxmox []proxmox.Config `json:"proxmox" yaml:"proxmox,omitempty"`
MaxMind *maxmind.Config `json:"maxmind" yaml:"maxmind,omitempty"`
}
)
// nil-safe
var ActiveConfig atomic.Pointer[Config]
func init() {
ActiveConfig.Store(DefaultConfig())
}
func Validate(data []byte) gperr.Error {
var model Config
return serialization.UnmarshalValidateYAML(data, &model)
}
func DefaultConfig() *Config {
return &Config{
func DefaultConfig() Config {
return Config{
TimeoutShutdown: 3,
Homepage: homepage.Config{
UseDefaultCategories: true,
@@ -61,7 +58,6 @@ func DefaultConfig() *Config {
var matchDomainsRegex = regexp.MustCompile(`^[^\.]?([\w\d\-_]\.?)+[^\.]?$`)
func init() {
serialization.RegisterDefaultValueFactory(DefaultConfig)
serialization.MustRegisterValidation("domain_name", func(fl validator.FieldLevel) bool {
domains := fl.Field().Interface().([]string)
for _, domain := range domains {
@@ -72,7 +68,7 @@ func init() {
return true
})
serialization.MustRegisterValidation("non_empty_docker_keys", func(fl validator.FieldLevel) bool {
m := fl.Field().Interface().(map[string]string)
m := fl.Field().Interface().(map[string]types.DockerProviderConfig)
for k := range m {
if k == "" {
return false

View File

@@ -33,7 +33,10 @@ type State interface {
FlushTmpLog()
}
// could be nil
// could be nil before first call on Load
var ActiveState synk.Value[State]
// working state while loading config, same as ActiveState after successful load
var WorkingState synk.Value[State]
var ErrConfigChanged = errors.New("config changed")

View File

@@ -1,16 +1,16 @@
module github.com/yusing/godoxy/internal/dnsproviders
go 1.25.4
go 1.25.5
replace github.com/yusing/godoxy => ../..
require (
github.com/go-acme/lego/v4 v4.28.1
github.com/yusing/godoxy v0.20.8
github.com/go-acme/lego/v4 v4.30.1
github.com/yusing/godoxy v0.21.3
)
require (
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth v0.18.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
@@ -26,76 +26,76 @@ require (
github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/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/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/go-resty/resty/v2 v2.16.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/go-resty/resty/v2 v2.17.1 // indirect
github.com/goccy/go-yaml v1.19.1 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
github.com/gotify/server/v2 v2.7.3 // 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/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/linode/linodego v1.61.0 // indirect
github.com/linode/linodego v1.63.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/maxatome/go-testdeep v1.14.0 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/miekg/dns v1.1.69 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/nrdcg/goacmedns v0.2.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/ovh/go-ovh v1.9.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/puzpuzpuz/xsync/v4 v4.2.0 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
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.24.0 // indirect
github.com/vultr/govultr/v3 v3.26.1 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusing/gointernals v0.1.16 // indirect
github.com/yusing/goutils v0.7.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/api v0.256.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/api v0.258.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

View File

@@ -1,5 +1,5 @@
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
@@ -42,6 +42,8 @@ github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2N
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/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=
@@ -53,10 +55,10 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-acme/lego/v4 v4.28.1 h1:zt301JYF51UIEkpSXsdeGq9hRePeFzQCq070OdAmP0Q=
github.com/go-acme/lego/v4 v4.28.1/go.mod h1:bzjilr03IgbaOwlH396hq5W56Bi0/uoRwW/JM8hP7m4=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-acme/lego/v4 v4.30.1 h1:tmb6U0lvy8Mc3lQbqKwTat7oAhE8FUYNJ3D0gSg6pJU=
github.com/go-acme/lego/v4 v4.30.1/go.mod h1:V7m/Ip+EeFkjOe028+zeH+SwWtESxw1LHelwMIfAjm4=
github.com/go-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=
@@ -72,12 +74,12 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
@@ -85,19 +87,19 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
github.com/gotify/server/v2 v2.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
github.com/gotify/server/v2 v2.7.3/go.mod h1:VAtE1RIc/2j886PYs9WPQbMjqbFsoyQ0G8IdFtnAxU0=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
@@ -120,8 +122,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/linode/linodego v1.61.0 h1:9g20NWl+/SbhDFj6X5EOZXtM2hBm1Mx8I9h8+F3l1LM=
github.com/linode/linodego v1.61.0/go.mod h1:64o30geLNwR0NeYh5HM/WrVCBXcSqkKnRK3x9xoRuJI=
github.com/linode/linodego v1.63.0 h1:MdjizfXNJDVJU6ggoJmMO5O9h4KGPGivNX0fzrAnstk=
github.com/linode/linodego v1.63.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -131,16 +133,16 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1 h1:kR8dHXC53heV4fttilXXkDkGmkdC5bvQ2XgbVBoD+ns=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1 h1:559XLHTF3F1A0J03PCIk6LlR0G9CmhHEDCm/TSk5BWQ=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1/go.mod h1:1FfSn6xcdK+wrNxUhtAAhLjYrwk9z8X3p3CNHwTc3zQ=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 h1:l0tH15ACQADZAzC+LZ+mo2tIX4H6uZu0ulrVmG5Tqz0=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 h1:gzB4c6ztb38C/jYiqEaFC+mCGcWFHDji9e6jwymY9d4=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2/go.mod h1:l1qIPIq2uRV5WTSvkbhbl/ndbeOu7OCb3UZ+0+2ZSb8=
github.com/nrdcg/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=
@@ -158,8 +160,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -178,8 +180,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
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.24.0 h1:fTTTj0VBve+Miy+wGhlb90M2NMDfpGFi6Frlj3HVy6M=
github.com/vultr/govultr/v3 v3.24.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
@@ -190,61 +192,60 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
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.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
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.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba h1:Ze6qXW0j37YCqZdCD2LkzVSxgEWez0cO4NUyd44DiDY=
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:4FLPzLA8eGAktPOTemJGDgDYRpLYwrNu4u2JtWINhnI=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -2,7 +2,6 @@ package docker
import (
"context"
"errors"
"fmt"
"maps"
"net"
@@ -17,7 +16,7 @@ import (
"github.com/moby/moby/client"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/types"
httputils "github.com/yusing/goutils/http"
"github.com/yusing/goutils/task"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
@@ -28,6 +27,8 @@ type (
SharedClient struct {
*client.Client
cfg types.DockerProviderConfig
refCount atomic.Int32
closedOn atomic.Int64
@@ -110,19 +111,17 @@ func Clients() map[string]*SharedClient {
return clients
}
var versionArg = client.WithAPIVersionNegotiation()
// NewClient creates a new Docker client connection to the specified host.
//
// Returns existing client if available.
//
// Parameters:
// - host: the host to connect to (either a URL or common.DockerHostFromEnv).
// - host: the host to connect to (either a URL or client.DefaultDockerHost).
//
// Returns:
// - Client: the Docker client connection.
// - error: an error if the connection failed.
func NewClient(host string, unique ...bool) (*SharedClient, error) {
func NewClient(cfg types.DockerProviderConfig, unique ...bool) (*SharedClient, error) {
initClientCleanerOnce.Do(initClientCleaner)
u := false
@@ -130,6 +129,8 @@ func NewClient(host string, unique ...bool) (*SharedClient, error) {
u = unique[0]
}
host := cfg.URL
if !u {
clientMapMu.Lock()
defer clientMapMu.Unlock()
@@ -154,45 +155,30 @@ func NewClient(host string, unique ...bool) (*SharedClient, error) {
opt = []client.Opt{
client.WithHost(agent.DockerHost),
client.WithHTTPClient(cfg.NewHTTPClient()),
versionArg,
}
addr = "tcp://" + cfg.Addr
dial = cfg.DialContext
} else {
switch host {
case "":
return nil, errors.New("empty docker host")
case common.DockerHostFromEnv:
helper, err := connhelper.GetConnectionHelper(host)
if err != nil {
log.Panic().Err(err).Msg("failed to get connection helper")
}
if helper != nil {
opt = []client.Opt{
client.WithHostFromEnv(),
versionArg,
client.WithHost(helper.Host),
client.WithDialContext(helper.Dialer),
}
default:
helper, err := connhelper.GetConnectionHelper(host)
if err != nil {
log.Panic().Err(err).Msg("failed to get connection helper")
}
if helper != nil {
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
opt = []client.Opt{
client.WithHTTPClient(httpClient),
client.WithHost(helper.Host),
versionArg,
client.WithDialContext(helper.Dialer),
}
} else {
opt = []client.Opt{
client.WithHost(host),
versionArg,
}
} else {
opt = []client.Opt{
client.WithHost(host),
}
}
}
if cfg.TLS != nil {
opt = append(opt, client.WithTLSClientConfig(cfg.TLS.CAFile, cfg.TLS.CertFile, cfg.TLS.KeyFile))
}
client, err := client.New(opt...)
if err != nil {
return nil, err
@@ -200,6 +186,7 @@ func NewClient(host string, unique ...bool) (*SharedClient, error) {
c := &SharedClient{
Client: client,
cfg: cfg,
addr: addr,
key: host,
dial: dial,
@@ -236,7 +223,7 @@ func (c *SharedClient) InterceptHTTPClient(intercept httputils.InterceptFunc) {
func (c *SharedClient) CloneUnique() *SharedClient {
// there will be no error here
// since we are using the same host from a valid client.
c, _ = NewClient(c.key, true)
c, _ = NewClient(c.cfg, true)
return c
}

View File

@@ -17,7 +17,6 @@ import (
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/serialization"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
)
@@ -30,7 +29,7 @@ var (
ErrNoNetwork = errors.New("no network found")
)
func FromDocker(c *container.Summary, dockerHost string) (res *types.Container) {
func FromDocker(c *container.Summary, dockerCfg types.DockerProviderConfig) (res *types.Container) {
actualLabels := maps.Clone(c.Labels)
_, isExplicit := c.Labels[LabelAliases]
@@ -48,7 +47,7 @@ func FromDocker(c *container.Summary, dockerHost string) (res *types.Container)
isExcluded, _ := strconv.ParseBool(helper.getDeleteLabel(LabelExclude))
res = &types.Container{
DockerHost: dockerHost,
DockerCfg: dockerCfg,
Image: helper.parseImage(),
ContainerName: helper.getName(),
ContainerID: c.ID,
@@ -70,11 +69,11 @@ func FromDocker(c *container.Summary, dockerHost string) (res *types.Container)
State: c.State,
}
if agent.IsDockerHostAgent(dockerHost) {
if agent.IsDockerHostAgent(dockerCfg.URL) {
var ok bool
res.Agent, ok = agent.GetAgent(dockerHost)
res.Agent, ok = agent.GetAgent(dockerCfg.URL)
if !ok {
addError(res, fmt.Errorf("agent %q not found", dockerHost))
addError(res, fmt.Errorf("agent %q not found", dockerCfg.URL))
}
}
@@ -93,7 +92,7 @@ func IsBlacklisted(c *types.Container) bool {
}
func UpdatePorts(c *types.Container) error {
dockerClient, err := NewClient(c.DockerHost)
dockerClient, err := NewClient(c.DockerCfg)
if err != nil {
return err
}
@@ -164,14 +163,14 @@ func isDatabase(c *types.Container) bool {
}
func isLocal(c *types.Container) bool {
if strings.HasPrefix(c.DockerHost, "unix://") {
if strings.HasPrefix(c.DockerCfg.URL, "unix://") {
return true
}
// treat it as local if the docker host is the same as the environment variable
if c.DockerHost == EnvDockerHost {
if c.DockerCfg.URL == EnvDockerHost {
return true
}
url, err := url.Parse(c.DockerHost)
url, err := url.Parse(c.DockerCfg.URL)
if err != nil {
return false
}
@@ -191,7 +190,7 @@ func setPublicHostname(c *types.Container) {
c.PublicHostname = "127.0.0.1"
return
}
url, err := url.Parse(c.DockerHost)
url, err := url.Parse(c.DockerCfg.URL)
if err != nil {
c.PublicHostname = "127.0.0.1"
return
@@ -224,7 +223,7 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
}
}
}
nearest := gperr.DoYouMean(utils.NearestField(c.Network, helper.NetworkSettings.Networks))
nearest := gperr.DoYouMeanField(c.Network, helper.NetworkSettings.Networks)
addError(c, fmt.Errorf("network %q not found, %w", c.Network, nearest))
return
}
@@ -239,25 +238,27 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
}
func loadDeleteIdlewatcherLabels(c *types.Container, helper containerHelper) {
cfg := map[string]any{
"idle_timeout": helper.getDeleteLabel(LabelIdleTimeout),
"wake_timeout": helper.getDeleteLabel(LabelWakeTimeout),
"stop_method": helper.getDeleteLabel(LabelStopMethod),
"stop_timeout": helper.getDeleteLabel(LabelStopTimeout),
"stop_signal": helper.getDeleteLabel(LabelStopSignal),
"start_endpoint": helper.getDeleteLabel(LabelStartEndpoint),
"depends_on": Dependencies(c),
hasIdleTimeout := false
cfg := make(map[string]any, len(idlewatcherLabels))
for lbl, key := range idlewatcherLabels {
value := helper.getDeleteLabel(lbl)
if value == "" {
continue
}
cfg[key] = value
switch lbl {
case LabelIdleTimeout:
hasIdleTimeout = true
case LabelDependsOn:
cfg[key] = Dependencies(c)
}
}
// ensure it's deleted from labels
helper.getDeleteLabel(LabelDependsOn)
// set only if idlewatcher is enabled
idleTimeout := cfg["idle_timeout"]
if idleTimeout != "" {
if hasIdleTimeout {
idwCfg := new(types.IdlewatcherConfig)
idwCfg.Docker = &types.DockerConfig{
DockerHost: c.DockerHost,
DockerCfg: c.DockerCfg,
ContainerID: c.ContainerID,
ContainerName: c.ContainerName,
}

View File

@@ -31,6 +31,9 @@ func (c containerHelper) getAliases() []string {
}
func (c containerHelper) getName() string {
if len(c.Names) == 0 { // Why did it happen? Every container must have a name.
return ""
}
return strings.TrimPrefix(c.Names[0], "/")
}

View File

@@ -4,6 +4,7 @@ import (
"testing"
"github.com/moby/moby/api/types/container"
"github.com/yusing/godoxy/internal/types"
expect "github.com/yusing/goutils/testing"
)
@@ -36,7 +37,7 @@ func TestContainerExplicit(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := FromDocker(&container.Summary{Names: []string{"test"}, State: "test", Labels: tt.labels}, "")
c := FromDocker(&container.Summary{Names: []string{"test"}, State: "test", Labels: tt.labels}, types.DockerProviderConfig{})
expect.Equal(t, c.IsExplicit, tt.isExplicit)
})
}
@@ -73,7 +74,7 @@ func TestContainerHostNetworkMode(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := FromDocker(tt.container, "")
c := FromDocker(tt.container, types.DockerProviderConfig{})
expect.Equal(t, c.IsHostNetworkMode, tt.isHostNetworkMode)
})
}

View File

@@ -2,18 +2,19 @@ package docker
import (
"github.com/puzpuzpuz/xsync/v4"
"github.com/yusing/godoxy/internal/types"
)
var idDockerHostMap = xsync.NewMap[string, string](xsync.WithPresize(100))
var idDockerCfgMap = xsync.NewMap[string, types.DockerProviderConfig](xsync.WithPresize(100))
func GetDockerHostByContainerID(id string) (string, bool) {
return idDockerHostMap.Load(id)
func GetDockerCfgByContainerID(id string) (types.DockerProviderConfig, bool) {
return idDockerCfgMap.Load(id)
}
func SetDockerHostByContainerID(id, host string) {
idDockerHostMap.Store(id, host)
func SetDockerCfgByContainerID(id string, cfg types.DockerProviderConfig) {
idDockerCfgMap.Store(id, cfg)
}
func DeleteDockerHostByContainerID(id string) {
idDockerHostMap.Delete(id)
func DeleteDockerCfgByContainerID(id string) {
idDockerCfgMap.Delete(id)
}

View File

@@ -2,6 +2,7 @@ package docker
import (
"fmt"
"strconv"
"strings"
"github.com/goccy/go-yaml"
@@ -12,6 +13,16 @@ import (
var ErrInvalidLabel = gperr.New("invalid label")
const nsProxyDot = NSProxy + "."
var refPrefixes = func() []string {
prefixes := make([]string, 100)
for i := range prefixes {
prefixes[i] = nsProxyDot + "#" + strconv.Itoa(i+1) + "."
}
return prefixes
}()
func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, gperr.Error) {
nestedMap := make(types.LabelMap)
errs := gperr.NewBuilder("labels error")
@@ -57,57 +68,83 @@ func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, g
}
func ExpandWildcard(labels map[string]string, aliases ...string) {
// collect all explicit aliases first
aliasSet := make(map[string]int, len(labels))
// wildcardLabels holds mapping suffix -> value derived from wildcard label definitions
wildcardLabels := make(map[string]string)
aliasSet := make(map[string]int, len(aliases))
for i, alias := range aliases {
aliasSet[alias] = i
}
// iterate over a copy of the keys to safely mutate the map while ranging
wildcardLabels := make(map[string]string)
// First pass: collect wildcards and discover aliases
for lbl, value := range labels {
parts := strings.SplitN(lbl, ".", 3)
if len(parts) < 2 || parts[0] != NSProxy {
if !strings.HasPrefix(lbl, nsProxyDot) {
continue
}
alias := parts[1]
if alias == WildcardAlias { // "*"
// remove wildcard label from original map it should not remain afterwards
// lbl is "proxy.X..." where X is alias or wildcard
rest := lbl[len(nsProxyDot):] // "X..." or "X.suffix"
dotIdx := strings.IndexByte(rest, '.')
var alias, suffix string
if dotIdx == -1 {
alias = rest
} else {
alias = rest[:dotIdx]
suffix = rest[dotIdx+1:]
}
if alias == WildcardAlias {
delete(labels, lbl)
// value looks like YAML (multiline)
if strings.Count(value, "\n") > 1 {
if suffix == "" || strings.Count(value, "\n") > 1 {
expandYamlWildcard(value, wildcardLabels)
continue
} else {
wildcardLabels[suffix] = value
}
// normal wildcard label with suffix store directly
wildcardLabels[parts[2]] = value
continue
}
// explicit alias label remember the alias (but not reference aliases like #1, #2)
if _, ok := aliasSet[alias]; !ok && !strings.HasPrefix(alias, "#") {
if suffix == "" || alias[0] == '#' {
continue
}
if _, known := aliasSet[alias]; !known {
aliasSet[alias] = len(aliasSet)
}
}
if len(aliasSet) == 0 || len(wildcardLabels) == 0 {
return // nothing to expand
return
}
// expand collected wildcard labels for every alias
for suffix, v := range wildcardLabels {
for alias, i := range aliasSet {
// use numeric index instead of the alias name
alias = fmt.Sprintf("#%d", i+1)
// Second pass: convert explicit labels to #N format
for lbl, value := range labels {
if !strings.HasPrefix(lbl, nsProxyDot) {
continue
}
rest := lbl[len(nsProxyDot):]
dotIdx := strings.IndexByte(rest, '.')
if dotIdx == -1 {
continue
}
alias := rest[:dotIdx]
if alias[0] == '#' {
continue
}
suffix := rest[dotIdx+1:]
key := fmt.Sprintf("%s.%s.%s", NSProxy, alias, suffix)
if suffix == "" { // this should not happen (root wildcard handled earlier) but keep safe
key = fmt.Sprintf("%s.%s", NSProxy, alias)
}
labels[key] = v
idx, known := aliasSet[alias]
if !known {
continue
}
delete(labels, lbl)
if _, overridden := wildcardLabels[suffix]; !overridden {
labels[refPrefixes[idx]+suffix] = value
}
}
// Expand wildcards for all aliases
for suffix, value := range wildcardLabels {
for _, idx := range aliasSet {
labels[refPrefixes[idx]+suffix] = value
}
}
}
@@ -139,12 +176,46 @@ func flattenMap(prefix string, src map[string]any, dest map[string]string) {
case map[string]any:
flattenMap(key, vv, dest)
case map[any]any:
// convert to map[string]any by stringifying keys
tmp := make(map[string]any, len(vv))
for kk, vvv := range vv {
tmp[fmt.Sprintf("%v", kk)] = vvv
}
flattenMap(key, tmp, dest)
flattenMapAny(key, vv, dest)
case string:
dest[key] = vv
case int:
dest[key] = strconv.Itoa(vv)
case bool:
dest[key] = strconv.FormatBool(vv)
case float64:
dest[key] = strconv.FormatFloat(vv, 'f', -1, 64)
default:
dest[key] = fmt.Sprint(v)
}
}
}
func flattenMapAny(prefix string, src map[any]any, dest map[string]string) {
for k, v := range src {
var key string
switch kk := k.(type) {
case string:
key = kk
default:
key = fmt.Sprint(k)
}
if prefix != "" {
key = prefix + "." + key
}
switch vv := v.(type) {
case map[string]any:
flattenMap(key, vv, dest)
case map[any]any:
flattenMapAny(key, vv, dest)
case string:
dest[key] = vv
case int:
dest[key] = strconv.Itoa(vv)
case bool:
dest[key] = strconv.FormatBool(vv)
case float64:
dest[key] = strconv.FormatFloat(vv, 'f', -1, 64)
default:
dest[key] = fmt.Sprint(v)
}

View File

@@ -8,87 +8,248 @@ import (
)
func TestExpandWildcard(t *testing.T) {
labels := map[string]string{
"proxy.a.host": "localhost",
"proxy.b.port": "4444",
"proxy.b.scheme": "http",
"proxy.*.port": "5555",
"proxy.*.healthcheck.disable": "true",
}
t.Run("basic", func(t *testing.T) {
labels := map[string]string{
"proxy.a.host": "localhost",
"proxy.b.port": "4444",
"proxy.b.scheme": "http",
"proxy.*.port": "5555",
"proxy.*.healthcheck.disable": "true",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "5555",
"proxy.#1.healthcheck.disable": "true",
"proxy.#2.port": "5555",
"proxy.#2.scheme": "http",
"proxy.#2.healthcheck.disable": "true",
}, labels)
})
docker.ExpandWildcard(labels)
t.Run("no wildcards", func(t *testing.T) {
labels := map[string]string{
"proxy.a.host": "localhost",
"proxy.b.port": "4444",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.a.host": "localhost",
"proxy.b.port": "4444",
}, labels)
})
require.Equal(t, map[string]string{
"proxy.a.host": "localhost",
"proxy.a.port": "5555",
"proxy.a.healthcheck.disable": "true",
"proxy.b.scheme": "http",
"proxy.b.port": "5555",
"proxy.b.healthcheck.disable": "true",
}, labels)
}
t.Run("no aliases", func(t *testing.T) {
labels := map[string]string{
"proxy.*.port": "5555",
}
docker.ExpandWildcard(labels)
require.Equal(t, map[string]string{}, labels)
})
func TestExpandWildcardWithFQDNAliases(t *testing.T) {
labels := map[string]string{
"proxy.c.host": "localhost",
"proxy.*.port": "5555",
}
docker.ExpandWildcard(labels, "a.example.com", "b.example.com")
require.Equal(t, map[string]string{
"proxy.#1.port": "5555",
"proxy.#2.port": "5555",
"proxy.c.host": "localhost",
"proxy.c.port": "5555",
}, labels)
t.Run("empty labels", func(t *testing.T) {
labels := map[string]string{}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{}, labels)
})
t.Run("only wildcards no explicit labels", func(t *testing.T) {
labels := map[string]string{
"proxy.*.port": "5555",
"proxy.*.scheme": "https",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.port": "5555",
"proxy.#1.scheme": "https",
"proxy.#2.port": "5555",
"proxy.#2.scheme": "https",
}, labels)
})
t.Run("non-proxy labels unchanged", func(t *testing.T) {
labels := map[string]string{
"other.label": "value",
"proxy.*.port": "5555",
"proxy.a.scheme": "http",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"other.label": "value",
"proxy.#1.port": "5555",
"proxy.#1.scheme": "http",
}, labels)
})
t.Run("single alias multiple labels", func(t *testing.T) {
labels := map[string]string{
"proxy.a.host": "localhost",
"proxy.a.port": "8080",
"proxy.a.scheme": "https",
"proxy.*.port": "5555",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "5555",
"proxy.#1.scheme": "https",
}, labels)
})
t.Run("wildcard partial override", func(t *testing.T) {
labels := map[string]string{
"proxy.a.host": "localhost",
"proxy.a.port": "8080",
"proxy.a.healthcheck.path": "/health",
"proxy.*.port": "5555",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "5555",
"proxy.#1.healthcheck.path": "/health",
}, labels)
})
t.Run("nested suffix distinction", func(t *testing.T) {
labels := map[string]string{
"proxy.a.healthcheck.path": "/health",
"proxy.a.healthcheck.interval": "10s",
"proxy.*.healthcheck.disable": "true",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"proxy.#1.healthcheck.path": "/health",
"proxy.#1.healthcheck.interval": "10s",
"proxy.#1.healthcheck.disable": "true",
}, labels)
})
t.Run("discovered alias from explicit label", func(t *testing.T) {
labels := map[string]string{
"proxy.c.host": "localhost",
"proxy.*.port": "5555",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.port": "5555",
"proxy.#2.port": "5555",
"proxy.#3.host": "localhost",
"proxy.#3.port": "5555",
}, labels)
})
t.Run("ref alias not converted", func(t *testing.T) {
labels := map[string]string{
"proxy.#1.host": "localhost",
"proxy.#2.port": "8080",
"proxy.*.scheme": "https",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.scheme": "https",
"proxy.#2.port": "8080",
"proxy.#2.scheme": "https",
}, labels)
})
t.Run("mixed ref and named aliases", func(t *testing.T) {
labels := map[string]string{
"proxy.#1.host": "host1",
"proxy.a.host": "host2",
"proxy.*.port": "5555",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.host": "host2",
"proxy.#1.port": "5555",
"proxy.#2.port": "5555",
}, labels)
})
}
func TestExpandWildcardYAML(t *testing.T) {
yaml := `
t.Run("basic yaml wildcard", func(t *testing.T) {
yaml := `
host: localhost
port: 5555
healthcheck:
disable: true`
labels := map[string]string{
"proxy.*": yaml[1:],
"proxy.a.port": "4444",
"proxy.a.healthcheck.disable": "false",
"proxy.a.healthcheck.path": "/health",
"proxy.b.port": "6666",
}
docker.ExpandWildcard(labels)
require.Equal(t, map[string]string{
"proxy.a.host": "localhost", // set by wildcard
"proxy.a.port": "5555", // overridden by wildcard
"proxy.a.healthcheck.disable": "true", // overridden by wildcard
"proxy.a.healthcheck.path": "/health", // own label
"proxy.b.host": "localhost", // set by wildcard
"proxy.b.port": "5555", // overridden by wildcard
"proxy.b.healthcheck.disable": "true", // overridden by wildcard
}, labels)
}
disable: true`[1:]
labels := map[string]string{
"proxy.*": yaml,
"proxy.a.port": "4444",
"proxy.a.healthcheck.disable": "false",
"proxy.a.healthcheck.path": "/health",
"proxy.b.port": "6666",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "5555",
"proxy.#1.healthcheck.disable": "true",
"proxy.#1.healthcheck.path": "/health",
"proxy.#2.host": "localhost",
"proxy.#2.port": "5555",
"proxy.#2.healthcheck.disable": "true",
}, labels)
})
func TestWildcardWithRefAliases(t *testing.T) {
labels := map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "5555",
"proxy.*.middlewares.request.hide_headers": "X-Header1,X-Header2",
}
docker.ExpandWildcard(labels, "a.example.com", "b.example.com")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "5555",
"proxy.#1.middlewares.request.hide_headers": "X-Header1,X-Header2",
"proxy.#2.middlewares.request.hide_headers": "X-Header1,X-Header2",
}, labels)
t.Run("yaml with nested maps", func(t *testing.T) {
yaml := `
middlewares:
request:
hide_headers: X-Secret
add_headers:
X-Custom: value`[1:]
labels := map[string]string{
"proxy.*": yaml,
"proxy.a.middlewares.request.set_headers": "X-Override: yes",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"proxy.#1.middlewares.request.hide_headers": "X-Secret",
"proxy.#1.middlewares.request.add_headers.X-Custom": "value",
"proxy.#1.middlewares.request.set_headers": "X-Override: yes",
}, labels)
})
t.Run("yaml only no explicit labels", func(t *testing.T) {
yaml := `
host: localhost
port: 8080`[1:]
labels := map[string]string{
"proxy.*": yaml,
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "8080",
"proxy.#2.host": "localhost",
"proxy.#2.port": "8080",
}, labels)
})
t.Run("invalid yaml ignored", func(t *testing.T) {
labels := map[string]string{
"proxy.*": "invalid: yaml: content:\n\t\tbad",
"proxy.a.port": "8080",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"proxy.a.port": "8080",
}, labels)
})
}
func BenchmarkParseLabels(b *testing.B) {
m := map[string]string{
"proxy.a.host": "localhost",
"proxy.b.port": "4444",
"proxy.*.scheme": "http",
"proxy.*.middlewares.request.hide_headers": "X-Header1,X-Header2",
}
for b.Loop() {
_, _ = docker.ParseLabels(map[string]string{
"proxy.a.host": "localhost",
"proxy.b.port": "4444",
"proxy.*.scheme": "http",
"proxy.*.middlewares.request.hide_headers": "X-Header1,X-Header2",
})
_, _ = docker.ParseLabels(m, "a", "b")
}
}

View File

@@ -14,5 +14,18 @@ const (
LabelStopSignal = NSProxy + ".stop_signal"
LabelStartEndpoint = NSProxy + ".start_endpoint"
LabelDependsOn = NSProxy + ".depends_on"
LabelNoLoadingPage = NSProxy + ".no_loading_page" // No loading page when using idlewatcher
LabelNetwork = NSProxy + ".network"
)
// key: label, value: key in IdlewatcherConfig
var idlewatcherLabels = map[string]string{
LabelIdleTimeout: "idle_timeout",
LabelWakeTimeout: "wake_timeout",
LabelStopMethod: "stop_method",
LabelStopTimeout: "stop_timeout",
LabelStopSignal: "stop_signal",
LabelStartEndpoint: "start_endpoint",
LabelDependsOn: "depends_on",
LabelNoLoadingPage: "no_loading_page",
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/yusing/godoxy/internal/types"
)
var listOptions = client.ContainerListOptions{
@@ -19,8 +20,8 @@ var listOptions = client.ContainerListOptions{
All: true,
}
func ListContainers(ctx context.Context, clientHost string) ([]container.Summary, error) {
dockerClient, err := NewClient(clientHost)
func ListContainers(ctx context.Context, dockerCfg types.DockerProviderConfig) ([]container.Summary, error) {
dockerClient, err := NewClient(dockerCfg)
if err != nil {
return nil, err
}

View File

@@ -146,11 +146,18 @@ func findRouteAnyDomain(host string) types.HTTPRoute {
if r, ok := routes.HTTP.Get(host); ok {
return r
}
// try striping the trailing :port from the host
if before, _, ok := strings.Cut(host, ":"); ok {
if r, ok := routes.HTTP.Get(before); ok {
return r
}
}
return nil
}
func findRouteByDomains(domains []string) func(host string) types.HTTPRoute {
return func(host string) types.HTTPRoute {
host, _, _ = strings.Cut(host, ":") // strip the trailing :port
for _, domain := range domains {
if target, ok := strings.CutSuffix(host, domain); ok {
if r, ok := routes.HTTP.Get(target); ok {

View File

@@ -82,7 +82,7 @@ func BenchmarkEntrypointReal(b *testing.B) {
Scheme: routeTypes.SchemeHTTP,
Host: host,
Port: route.Port{Proxy: portInt},
HealthCheck: &types.HealthCheckConfig{Disable: true},
HealthCheck: types.HealthCheckConfig{Disable: true},
}
err = r.Validate()
@@ -125,7 +125,7 @@ func BenchmarkEntrypoint(b *testing.B) {
Port: route.Port{
Proxy: 8080,
},
HealthCheck: &types.HealthCheckConfig{
HealthCheck: types.HealthCheckConfig{
Disable: true,
},
}

View File

@@ -128,3 +128,47 @@ func TestFindRouteByDomainsExactMatch(t *testing.T) {
run(t, tests, testsNoMatch)
}
func TestFindRouteWithPort(t *testing.T) {
t.Run("AnyDomain", func(t *testing.T) {
addRoute("app1")
addRoute("app2.com")
tests := []string{
"app1:8080",
"app1.domain.com:8080",
"app2.com:8080",
}
testsNoMatch := []string{
"app11",
"app2.co",
"app2.co:8080",
}
run(t, tests, testsNoMatch)
})
t.Run("ByDomains", func(t *testing.T) {
ep.SetFindRouteDomains([]string{
".domain.com",
})
addRoute("app1")
addRoute("app2")
addRoute("app3.domain.com")
tests := []string{
"app1.domain.com:8080",
"app2:8080", // exact match fallback
"app3.domain.com:8080",
}
testsNoMatch := []string{
"app11",
"app1.domain.co",
"app1.domain.co:8080",
"app2.co",
"app2.co:8080",
"app3.domain.co",
"app3.domain.co:8080",
}
run(t, tests, testsNoMatch)
})
}

View File

@@ -145,23 +145,32 @@ func fetchIcon(ctx context.Context, filename string) (FetchResult, error) {
return FetchResultWithErrorf(http.StatusNotFound, "no icon found")
}
func FindIcon(ctx context.Context, r route, uri string) (FetchResult, error) {
type contextValue struct {
r httpRoute
uri string
}
func FindIcon(ctx context.Context, r route, uri string, variant IconVariant) (FetchResult, error) {
for _, ref := range r.References() {
result, err := fetchIcon(ctx, sanitizeName(ref))
ref = sanitizeName(ref)
if variant != IconVariantNone {
ref += "-" + string(variant)
}
result, err := fetchIcon(ctx, ref)
if err == nil {
return result, err
}
}
if r, ok := r.(httpRoute); ok {
// fallback to parse html
return findIconSlowCached(context.WithValue(ctx, "route", r), uri)
return findIconSlowCached(context.WithValue(ctx, "route", contextValue{r: r, uri: uri}), r.Key())
}
return FetchResultWithErrorf(http.StatusNotFound, "no icon found")
}
var findIconSlowCached = cache.NewKeyFunc(func(ctx context.Context, key string) (FetchResult, error) {
r := ctx.Value("route").(httpRoute)
return findIconSlow(ctx, r, key, nil)
v := ctx.Value("route").(contextValue)
return findIconSlow(ctx, v.r, v.uri, nil)
}).WithMaxEntries(200).Build() // no retries, no ttl
func findIconSlow(ctx context.Context, r httpRoute, uri string, stack []string) (FetchResult, error) {

View File

@@ -7,6 +7,7 @@ import (
"github.com/yusing/ds/ordered"
"github.com/yusing/godoxy/internal/homepage/widgets"
"github.com/yusing/godoxy/internal/serialization"
strutils "github.com/yusing/goutils/strings"
)
type (
@@ -146,13 +147,13 @@ func (c *Category) sortByClicks() {
return 1
}
// fallback to alphabetical
return strings.Compare(a.Name, b.Name)
return strings.Compare(strutils.Title(a.Name), strutils.Title(b.Name))
})
}
func (c *Category) sortByAlphabetical() {
slices.SortStableFunc(c.Items, func(a, b *Item) int {
return strings.Compare(a.Name, b.Name)
return strings.Compare(strutils.Title(a.Name), strutils.Title(b.Name))
})
}

View File

@@ -23,7 +23,8 @@ type (
IsDark bool `json:"is_dark"`
}
IconSource string
IconSource string
IconVariant string
)
const (
@@ -33,6 +34,12 @@ const (
IconSourceSelfhSt IconSource = "@selfhst"
)
const (
IconVariantNone IconVariant = ""
IconVariantLight IconVariant = "light"
IconVariantDark IconVariant = "dark"
)
var ErrInvalidIconURL = gperr.New("invalid icon url")
func NewIconURL(source IconSource, refOrName, format string) *IconURL {
@@ -76,6 +83,32 @@ func (u *IconURL) HasIcon() bool {
return HasIcon(u)
}
func (u *IconURL) WithVariant(variant IconVariant) *IconURL {
switch u.IconSource {
case IconSourceWalkXCode, IconSourceSelfhSt:
default:
return u // no variant for absolute/relative icons
}
var extra *IconExtra
if u.Extra != nil {
extra = &IconExtra{
Key: u.Extra.Key,
Ref: u.Extra.Ref,
FileType: u.Extra.FileType,
IsLight: variant == IconVariantLight,
IsDark: variant == IconVariantDark,
}
extra.Ref = strings.TrimSuffix(extra.Ref, "-light")
extra.Ref = strings.TrimSuffix(extra.Ref, "-dark")
}
return &IconURL{
IconSource: u.IconSource,
FullURL: u.FullURL,
Extra: extra,
}
}
// Parse implements strutils.Parser.
func (u *IconURL) Parse(v string) error {
return u.parse(v, true)

View File

@@ -14,6 +14,7 @@ import (
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/serialization"
httputils "github.com/yusing/goutils/http"
"github.com/yusing/goutils/intern"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/synk"
"github.com/yusing/goutils/task"
@@ -219,8 +220,7 @@ func HasIcon(icon *IconURL) bool {
if common.IsTest {
return true
}
key := NewIconKey(icon.IconSource, icon.Extra.Ref)
meta, ok := ListAvailableIcons()[key]
meta, ok := ListAvailableIcons()[icon.Extra.Key]
if !ok {
return false
}
@@ -332,6 +332,10 @@ func UpdateWalkxCodeIcons(m IconMap) error {
if isLight {
f = strings.TrimSuffix(f, "-light")
}
isDark := strings.HasSuffix(f, "-dark")
if isDark {
f = strings.TrimSuffix(f, "-dark")
}
key := NewIconKey(IconSourceWalkXCode, f)
icon, ok := m[key]
if !ok {
@@ -342,6 +346,9 @@ func UpdateWalkxCodeIcons(m IconMap) error {
if isLight {
icon.Light = true
}
if isDark {
icon.Dark = true
}
}
}
return nil
@@ -396,7 +403,7 @@ func UpdateSelfhstIcons(m IconMap) error {
}
icon := &IconMeta{
DisplayName: item.Name,
Tag: tag,
Tag: intern.Make(tag).Value(),
SVG: item.SVG == "Yes",
PNG: item.PNG == "Yes",
WebP: item.WebP == "Yes",

View File

@@ -10,16 +10,22 @@ const walkxcodeIcons = `{
"png": [
"app1.png",
"app1-light.png",
"app2.png"
"app2.png",
"karakeep.png",
"karakeep-dark.png"
],
"svg": [
"app1.svg",
"app1-light.svg"
"app1-light.svg",
"karakeep.svg",
"karakeep-dark.svg"
],
"webp": [
"app1.webp",
"app1-light.webp",
"app2.webp"
"app2.webp",
"karakeep.webp",
"karakeep-dark.webp"
]
}`
@@ -98,8 +104,8 @@ func TestListWalkxCodeIcons(t *testing.T) {
if err := UpdateWalkxCodeIcons(m); err != nil {
t.Fatal(err)
}
if len(m) != 2 {
t.Fatalf("expect 2 icons, got %d", len(m))
if len(m) != 3 {
t.Fatalf("expect 3 icons, got %d", len(m))
}
test := []testCases{
{
@@ -118,6 +124,15 @@ func TestListWalkxCodeIcons(t *testing.T) {
WebP: true,
},
},
{
Key: NewIconKey(IconSourceWalkXCode, "karakeep"),
IconMeta: IconMeta{
SVG: true,
PNG: true,
WebP: true,
Dark: true,
},
},
}
runTests(t, m, test)
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
nettypes "github.com/yusing/godoxy/internal/net/types"
"github.com/yusing/godoxy/internal/utils/pool"
"github.com/yusing/goutils/pool"
)
type route interface {

View File

@@ -104,13 +104,13 @@ func (w *Watcher) getFavIcon(ctx context.Context) (result homepage.FetchResult,
hp := r.HomepageItem()
if hp.Icon != nil {
if hp.Icon.IconSource == homepage.IconSourceRelative {
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL)
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL, homepage.IconVariantNone)
} else {
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon)
}
} else {
// try extract from "link[rel=icon]"
result, err = homepage.FindIcon(ctx, r, "/")
result, err = homepage.FindIcon(ctx, r, "/", homepage.IconVariantNone)
}
if result.StatusCode == 0 {
result.StatusCode = http.StatusOK
@@ -175,9 +175,15 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
}
}
if !acceptHTML {
serveStaticContent(rw, http.StatusOK, "text/plain", []byte("Container woken"))
return false
if !acceptHTML || w.cfg.NoLoadingPage {
// send a continue response to prevent client wait-header timeout
rw.WriteHeader(http.StatusContinue)
ready := w.waitForReady(r.Context())
if !ready {
serveStaticContent(rw, http.StatusInternalServerError, "text/plain", []byte("Timeout waiting for container to become ready"))
return false
}
return true
}
// Send a loading response to the client

View File

@@ -0,0 +1,73 @@
//go:build !production
package idlewatcher
import (
"math/rand/v2"
"net/http"
"time"
"github.com/puzpuzpuz/xsync/v4"
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
"github.com/yusing/godoxy/internal/types"
)
func DebugHandler(rw http.ResponseWriter, r *http.Request) {
w := &Watcher{
eventChs: xsync.NewMap[chan *WakeEvent, struct{}](),
cfg: &types.IdlewatcherConfig{
IdlewatcherProviderConfig: types.IdlewatcherProviderConfig{
Docker: &types.DockerConfig{
ContainerName: "test",
},
},
},
}
switch r.URL.Path {
case idlewatcher.LoadingPageCSSPath:
serveStaticContent(rw, http.StatusOK, "text/css", cssBytes)
case idlewatcher.LoadingPageJSPath:
serveStaticContent(rw, http.StatusOK, "application/javascript", jsBytes)
case idlewatcher.WakeEventsPath:
go w.handleWakeEventsSSE(rw, r)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
events := []WakeEventType{
WakeEventStarting,
WakeEventWakingDep,
WakeEventDepReady,
WakeEventContainerWoke,
WakeEventWaitingReady,
WakeEventError,
WakeEventReady,
}
messages := []string{
"Starting",
"Waking dependency",
"Dependency ready",
"Container woke",
"Waiting for ready",
"Error",
"Ready",
}
for {
select {
case <-r.Context().Done():
return
case <-ticker.C:
idx := rand.IntN(len(events))
for ch := range w.eventChs.Range {
ch <- &WakeEvent{
Type: string(events[idx]),
Message: messages[idx],
Timestamp: time.Now(),
}
}
}
}
default:
w.writeLoadingPage(rw)
}
}

View File

@@ -20,14 +20,14 @@ type DockerProvider struct {
var startOptions = client.ContainerStartOptions{}
func NewDockerProvider(dockerHost, containerID string) (idlewatcher.Provider, error) {
client, err := docker.NewClient(dockerHost)
func NewDockerProvider(dockerCfg types.DockerProviderConfig, containerID string) (idlewatcher.Provider, error) {
client, err := docker.NewClient(dockerCfg)
if err != nil {
return nil, err
}
return &DockerProvider{
client: client,
watcher: watcher.NewDockerWatcher(dockerHost),
watcher: watcher.NewDockerWatcher(dockerCfg),
containerID: containerID,
}, nil
}

View File

@@ -75,7 +75,7 @@ func (w *Watcher) waitForReady(ctx context.Context) bool {
// Wait for ready notification or context cancellation
select {
case <-w.readyNotifyCh:
return w.ready() // double-check in case of race condition
return true
case <-ctx.Done():
return false
}

View File

@@ -2,7 +2,8 @@ package idlewatcher
const (
FavIconPath = "/favicon.ico"
LoadingPageCSSPath = "/$godoxy/style.css"
LoadingPageJSPath = "/$godoxy/loading.js"
WakeEventsPath = "/$godoxy/wake-events"
PathPrefix = "/$godoxy/"
LoadingPageCSSPath = PathPrefix + "style.css"
LoadingPageJSPath = PathPrefix + "loading.js"
WakeEventsPath = PathPrefix + "wake-events"
)

View File

@@ -200,7 +200,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
depCfg = new(types.IdlewatcherConfig)
depCfg.IdlewatcherConfigBase = cfg.IdlewatcherConfigBase
depCfg.IdleTimeout = neverTick // disable auto sleep for dependencies
} else if depCfg.IdleTimeout > 0 {
} else if depCfg.IdleTimeout > 0 && depCfg.IdleTimeout != neverTick {
depErrors.Addf("dependency %q has positive idle timeout %s", dep, depCfg.IdleTimeout)
continue
}
@@ -209,7 +209,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
depCont := depRoute.ContainerInfo()
if depCont != nil {
depCfg.Docker = &types.DockerConfig{
DockerHost: depCont.DockerHost,
DockerCfg: depCont.DockerCfg,
ContainerID: depCont.ContainerID,
ContainerName: depCont.ContainerName,
}
@@ -256,7 +256,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
var kind string
switch {
case cfg.Docker != nil:
p, err = provider.NewDockerProvider(cfg.Docker.DockerHost, cfg.Docker.ContainerID)
p, err = provider.NewDockerProvider(cfg.Docker.DockerCfg, cfg.Docker.ContainerID)
kind = "docker"
default:
p, err = provider.NewProxmoxProvider(cfg.Proxmox.Node, cfg.Proxmox.VMID)
@@ -332,6 +332,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
w.idleTicker.Stop()
w.healthTicker.Stop()
w.setReady()
close(w.readyNotifyCh)
w.task.Finish(cause)
}()

View File

@@ -94,6 +94,7 @@ const (
)
var bytesPool = synk.GetUnsizedBytesPool()
var sizedPool = synk.GetSizedBytesPool()
func NewAccessLogger(parent task.Parent, cfg AnyConfig) (AccessLogger, error) {
writers, err := cfg.Writers()

View File

@@ -9,7 +9,7 @@ import (
"time"
. "github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/utils"
"github.com/yusing/goutils/mockable"
"github.com/yusing/goutils/task"
expect "github.com/yusing/goutils/testing"
)
@@ -57,7 +57,7 @@ func fmtLog(cfg *RequestLoggerConfig) (ts string, line string) {
t := time.Now()
logger := NewMockAccessLogger(testTask, cfg)
utils.MockTimeNow(t)
mockable.MockTimeNow(t)
buf = logger.(RequestFormatter).AppendRequestLog(buf, req, resp)
return t.Format(LogTimeFormat), string(buf)
}

View File

@@ -9,7 +9,7 @@ import (
"github.com/rs/zerolog"
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
"github.com/yusing/godoxy/internal/utils"
"github.com/yusing/goutils/mockable"
)
type (
@@ -67,7 +67,7 @@ func (f *CommonFormatter) AppendRequestLog(line []byte, req *http.Request, res *
line = append(line, clientIP(req)...)
line = append(line, " - - ["...)
line = utils.TimeNow().AppendFormat(line, LogTimeFormat)
line = mockable.TimeNow().AppendFormat(line, LogTimeFormat)
line = append(line, `] "`...)
line = append(line, req.Method...)
@@ -103,7 +103,7 @@ func (f *JSONFormatter) AppendRequestLog(line []byte, req *http.Request, res *ht
writer := bytes.NewBuffer(line)
logger := zerolog.New(writer)
event := logger.Info().
Str("time", utils.TimeNow().Format(LogTimeFormat)).
Str("time", mockable.TimeNow().Format(LogTimeFormat)).
Str("ip", clientIP(req)).
Str("method", req.Method).
Str("scheme", scheme(req)).
@@ -136,7 +136,7 @@ func (f ACLLogFormatter) AppendACLLog(line []byte, info *maxmind.IPInfo, blocked
writer := bytes.NewBuffer(line)
logger := zerolog.New(writer)
event := logger.Info().
Str("time", utils.TimeNow().Format(LogTimeFormat)).
Str("time", mockable.TimeNow().Format(LogTimeFormat)).
Str("ip", info.Str)
if blocked {
event.Str("action", "block")

View File

@@ -3,13 +3,12 @@ package accesslog
import (
"bytes"
"errors"
"fmt"
"io"
"slices"
"time"
"github.com/rs/zerolog"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/mockable"
strutils "github.com/yusing/goutils/strings"
)
@@ -81,14 +80,14 @@ func rotateLogFile(file supportRotate, config *Retention, result *RotateResult)
func rotateLogFileByPolicy(file supportRotate, config *Retention, result *RotateResult) (rotated bool, err error) {
var shouldStop func() bool
t := utils.TimeNow()
t := mockable.TimeNow()
switch {
case config.Last > 0:
shouldStop = func() bool { return result.NumLinesKeep-result.NumLinesInvalid == int(config.Last) }
// not needed to parse time for last N lines
case config.Days > 0:
cutoff := utils.TimeNow().AddDate(0, 0, -int(config.Days)+1)
cutoff := mockable.TimeNow().AddDate(0, 0, -int(config.Days)+1)
shouldStop = func() bool { return t.Before(cutoff) }
default:
return false, nil // should not happen
@@ -164,31 +163,14 @@ func rotateLogFileByPolicy(file supportRotate, config *Retention, result *Rotate
// Read each line and write it to the beginning of the file
writePos := int64(0)
buf := bytesPool.Get()
defer func() {
bytesPool.Put(buf)
}()
// in reverse order to keep the order of the lines (from old to new)
for i := len(linesToKeep) - 1; i >= 0; i-- {
line := linesToKeep[i]
n := line.Size
if cap(buf) < int(n) {
buf = slices.Grow(buf, int(n)-cap(buf))
}
buf = buf[:n]
// Read the line from its original position
if _, err := file.ReadAt(buf, line.Pos); err != nil {
if err := fileContentMove(file, line.Pos, writePos, int(n)); err != nil {
return false, err
}
// Write it to the new position
if _, err := file.WriteAt(buf, writePos); err != nil {
return false, err
} else if n < line.Size {
return false, gperr.Errorf("%w, writing %d bytes, only %d written", io.ErrShortWrite, line.Size, n)
}
writePos += n
}
@@ -199,6 +181,34 @@ func rotateLogFileByPolicy(file supportRotate, config *Retention, result *Rotate
return true, nil
}
// fileContentMove moves the content of the file from the source position to the destination position.
//
// this is only used for moving from the back to the front of the file.
func fileContentMove(file supportRotate, srcPos, dstPos int64, size int) error {
buf := sizedPool.GetSized(size)
defer sizedPool.Put(buf)
// Read the line from its original position
nRead, err := file.ReadAt(buf, srcPos)
if err != nil {
return err
}
if nRead != size {
return fmt.Errorf("%w, reading %d bytes, only %d read", io.ErrShortBuffer, size, nRead)
}
// Write it to the new position
nWritten, err := file.WriteAt(buf, dstPos)
if err != nil {
return err
}
if nWritten != size {
return fmt.Errorf("%w, writing %d bytes, only %d written", io.ErrShortWrite, size, nWritten)
}
return nil
}
// rotateLogFileBySize rotates the log file by size.
// It returns the result of the rotation and an error if any.
//

View File

@@ -7,7 +7,7 @@ import (
"time"
. "github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/utils"
"github.com/yusing/goutils/mockable"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/task"
expect "github.com/yusing/goutils/testing"
@@ -56,7 +56,7 @@ func TestRotateKeepLast(t *testing.T) {
for _, format := range ReqLoggerFormats {
t.Run(string(format)+" keep last", func(t *testing.T) {
file := NewMockFile(true)
utils.MockTimeNow(testTime)
mockable.MockTimeNow(testTime)
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
Format: format,
})
@@ -93,7 +93,7 @@ func TestRotateKeepLast(t *testing.T) {
expect.Nil(t, logger.Config().Retention)
nLines := 10
for i := range nLines {
utils.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
logger.Log(req, resp)
}
logger.Flush()
@@ -105,7 +105,7 @@ func TestRotateKeepLast(t *testing.T) {
expect.Equal(t, retention.KeepSize, 0)
logger.Config().Retention = retention
utils.MockTimeNow(testTime)
mockable.MockTimeNow(testTime)
var result RotateResult
rotated, err := logger.(AccessLogRotater).Rotate(&result)
expect.NoError(t, err)
@@ -139,7 +139,7 @@ func TestRotateKeepFileSize(t *testing.T) {
expect.Nil(t, logger.Config().Retention)
nLines := 10
for i := range nLines {
utils.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
logger.Log(req, resp)
}
logger.Flush()
@@ -151,7 +151,7 @@ func TestRotateKeepFileSize(t *testing.T) {
expect.Equal(t, retention.Last, 0)
logger.Config().Retention = retention
utils.MockTimeNow(testTime)
mockable.MockTimeNow(testTime)
var result RotateResult
rotated, err := logger.(AccessLogRotater).Rotate(&result)
expect.NoError(t, err)
@@ -171,7 +171,7 @@ func TestRotateKeepFileSize(t *testing.T) {
expect.Nil(t, logger.Config().Retention)
nLines := 100
for i := range nLines {
utils.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
logger.Log(req, resp)
}
logger.Flush()
@@ -183,7 +183,7 @@ func TestRotateKeepFileSize(t *testing.T) {
expect.Equal(t, retention.Last, 0)
logger.Config().Retention = retention
utils.MockTimeNow(testTime)
mockable.MockTimeNow(testTime)
var result RotateResult
rotated, err := logger.(AccessLogRotater).Rotate(&result)
expect.NoError(t, err)
@@ -205,7 +205,7 @@ func TestRotateSkipInvalidTime(t *testing.T) {
expect.Nil(t, logger.Config().Retention)
nLines := 10
for i := range nLines {
utils.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
logger.Log(req, resp)
logger.Flush()
@@ -248,7 +248,7 @@ func BenchmarkRotate(b *testing.B) {
Format: FormatJSON,
})
for i := range 100 {
utils.MockTimeNow(testTime.AddDate(0, 0, -100+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -100+i+1))
logger.Log(req, resp)
}
logger.Flush()
@@ -282,7 +282,7 @@ func BenchmarkRotateWithInvalidTime(b *testing.B) {
Format: FormatJSON,
})
for i := range 10000 {
utils.MockTimeNow(testTime.AddDate(0, 0, -10000+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -10000+i+1))
logger.Log(req, resp)
if i%10 == 0 {
_, _ = file.Write([]byte("invalid time\n"))

View File

@@ -178,7 +178,7 @@ func (cfg *MaxMind) doReq(method string) (*http.Response, error) {
if err != nil {
return nil, err
}
req.SetBasicAuth(cfg.AccountID, cfg.LicenseKey)
req.SetBasicAuth(cfg.AccountID, cfg.LicenseKey.String())
resp, err := doReq(req)
if err != nil {
return nil, err

View File

@@ -4,14 +4,15 @@ import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
)
type (
DatabaseType string
Config struct {
AccountID string `json:"account_id" validate:"required"`
LicenseKey string `json:"license_key" validate:"required"`
Database DatabaseType `json:"database" validate:"omitempty,oneof=geolite geoip2"`
AccountID string `json:"account_id" validate:"required"`
LicenseKey strutils.Redacted `json:"license_key" validate:"required"`
Database DatabaseType `json:"database" validate:"omitempty,oneof=geolite geoip2"`
}
)

View File

@@ -11,7 +11,7 @@ import (
"github.com/shirou/gopsutil/v4/mem"
"github.com/shirou/gopsutil/v4/net"
"github.com/shirou/gopsutil/v4/sensors"
"github.com/yusing/goutils/num"
"github.com/yusing/goutils/intern"
expect "github.com/yusing/goutils/testing"
)
@@ -22,32 +22,26 @@ var (
Timestamp: 123456,
CPUAverage: &cpuAvg,
Memory: mem.VirtualMemoryStat{
Total: 16000000000,
Available: 8000000000,
Used: 8000000000,
UsedPercent: 50.0,
Available: 8000000000,
Used: 8000000000,
},
Disks: map[string]disk.UsageStat{
"sda": {
Path: "/",
Fstype: "ext4",
Total: 500000000000,
Free: 250000000000,
Used: 250000000000,
UsedPercent: 50.0,
Path: intern.Make("/"),
Fstype: intern.Make("ext4"),
Free: 250000000000,
Used: 250000000000,
},
"nvme0n1": {
Path: "/",
Fstype: "zfs",
Total: 500000000000,
Free: 250000000000,
Used: 250000000000,
UsedPercent: 50.0,
Path: intern.Make("/"),
Fstype: intern.Make("zfs"),
Free: 250000000000,
Used: 250000000000,
},
},
DisksIO: map[string]*disk.IOCountersStat{
"media": {
Name: "media",
Name: intern.Make("media"),
ReadBytes: 1000000,
WriteBytes: 2000000,
IOCountersStatExtra: disk.IOCountersStatExtra{
@@ -57,7 +51,7 @@ var (
},
},
"nvme0n1": {
Name: "nvme0n1",
Name: intern.Make("nvme0n1"),
ReadBytes: 1000000,
WriteBytes: 2000000,
IOCountersStatExtra: disk.IOCountersStatExtra{
@@ -75,16 +69,12 @@ var (
},
Sensors: []sensors.TemperatureStat{
{
SensorKey: "cpu_temp",
Temperature: num.NewPercentage(30.0),
High: num.NewPercentage(40.0),
Critical: num.NewPercentage(50.0),
SensorKey: intern.Make("cpu_temp"),
Temperature: 30.0,
},
{
SensorKey: "gpu_temp",
Temperature: num.NewPercentage(40.0),
High: num.NewPercentage(50.0),
Critical: num.NewPercentage(60.0),
SensorKey: intern.Make("gpu_temp"),
Temperature: 40.0,
},
},
}
@@ -141,10 +131,10 @@ func TestSerialize(t *testing.T) {
expect.NoError(t, err)
var v []map[string]any
expect.NoError(t, json.Unmarshal(s, &v))
expect.Equal(t, len(v), len(result.Entries))
expect.Equal(t, len(v), len(result))
for i, m := range v {
for k, v := range m {
vv := reflect.ValueOf(result.Entries[i][k])
vv := reflect.ValueOf(result[i][k])
expect.Equal(t, reflect.ValueOf(v).Interface(), vv.Interface())
}
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/bytedance/sonic"
"github.com/lithammer/fuzzysearch/fuzzy"
statequery "github.com/yusing/godoxy/internal/config/query"
"github.com/yusing/godoxy/internal/metrics/period"
metricsutils "github.com/yusing/godoxy/internal/metrics/utils"
"github.com/yusing/godoxy/internal/route/routes"
@@ -34,6 +33,7 @@ type (
Idle float32 `json:"idle"`
AvgLatency float32 `json:"avg_latency"`
IsDocker bool `json:"is_docker"`
IsExcluded bool `json:"is_excluded"`
CurrentStatus types.HealthStatus `json:"current_status" swaggertype:"string" enums:"healthy,unhealthy,unknown,napping,starting"`
Statuses []Status `json:"statuses"`
} // @name RouteUptimeAggregate
@@ -133,7 +133,7 @@ func (rs RouteStatuses) aggregate(limit int, offset int) Aggregated {
r, ok := routes.Get(alias)
if !ok {
// also search for excluded routes
r = statequery.SearchRoute(alias)
r, ok = routes.Excluded.Get(alias)
}
if r != nil {
displayName = r.DisplayName()
@@ -157,6 +157,7 @@ func (rs RouteStatuses) aggregate(limit int, offset int) Aggregated {
CurrentStatus: status,
Statuses: statuses,
IsDocker: r != nil && r.IsDocker(),
IsExcluded: r == nil || r.ShouldExclude(),
}
}
return result

View File

@@ -10,8 +10,8 @@ import (
"github.com/rs/zerolog/log"
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/utils/pool"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/pool"
"github.com/yusing/goutils/task"
"golang.org/x/sync/errgroup"
)

View File

@@ -7,6 +7,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/auth"
"github.com/yusing/godoxy/internal/route/rules"
httputils "github.com/yusing/goutils/http"
)
type Bypass []rules.RuleOn
@@ -50,7 +51,7 @@ func (c *checkBypass) before(w http.ResponseWriter, r *http.Request) (proceedNex
}
func (c *checkBypass) modifyResponse(resp *http.Response) error {
if c.modRes == nil || (!c.isEnforced(resp.Request) && c.bypass.ShouldBypass(rules.ResponseAsRW(resp), resp.Request)) {
if c.modRes == nil || (!c.isEnforced(resp.Request) && c.bypass.ShouldBypass(httputils.ResponseAsRW(resp), resp.Request)) {
return nil
}
log.Debug().Str("middleware", c.name).Str("url", resp.Request.Host+resp.Request.URL.Path).Msg("modifying response")

View File

@@ -201,6 +201,7 @@ func TestBypassResponse(t *testing.T) {
StatusCode: test.statusCode,
Body: io.NopCloser(strings.NewReader("test")),
Header: make(http.Header),
Request: httptest.NewRequest("GET", "http://example.com", nil),
}
mErr := mr.ModifyResponse(resp)
expect.NoError(t, mErr)

View File

@@ -8,7 +8,6 @@ import (
_ "embed"
"github.com/yusing/godoxy/internal/jsonstore"
"github.com/yusing/godoxy/internal/utils"
)
type CaptchaSession struct {
@@ -22,7 +21,7 @@ var CaptchaSessions = jsonstore.Store[*CaptchaSession]("captcha_sessions")
func newCaptchaSession(p Provider) *CaptchaSession {
buf := make([]byte, 32)
_, _ = rand.Read(buf)
now := utils.TimeNow()
now := time.Now()
return &CaptchaSession{
ID: hex.EncodeToString(buf),
Expiry: now.Add(p.SessionExpiry()),
@@ -30,5 +29,5 @@ func newCaptchaSession(p Provider) *CaptchaSession {
}
func (s *CaptchaSession) expired() bool {
return utils.TimeNow().After(s.Expiry)
return time.Now().After(s.Expiry)
}

View File

@@ -9,10 +9,10 @@ import (
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/utils"
"github.com/yusing/godoxy/internal/watcher"
"github.com/yusing/godoxy/internal/watcher/events"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/fs"
"github.com/yusing/goutils/task"
)
@@ -46,7 +46,7 @@ func GetErrorPageByStatus(statusCode int) (content []byte, ok bool) {
}
func loadContent() {
files, err := utils.ListFiles(errPagesBasePath, 0)
files, err := fs.ListFiles(errPagesBasePath, 0)
if err != nil {
log.Err(err).Msg("failed to list error page resources")
return

View File

@@ -9,9 +9,10 @@ import (
"github.com/bytedance/sonic"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/route/rules"
"github.com/yusing/godoxy/internal/serialization"
gperr "github.com/yusing/goutils/errs"
httputils "github.com/yusing/goutils/http"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/reverseproxy"
)
@@ -190,11 +191,22 @@ func (m *Middleware) ServeHTTP(next http.HandlerFunc, w http.ResponseWriter, r *
}
}
if exec, ok := m.impl.(ResponseModifier); ok {
rm := rules.NewResponseModifier(w)
defer rm.FlushRelease()
next(rm, r)
if httpheaders.IsWebsocket(r.Header) || r.Header.Get("Accept") == "text/event-stream" {
next(w, r)
return
}
if exec, ok := m.impl.(ResponseModifier); ok {
lrm := httputils.NewLazyResponseModifier(w, needsBuffering)
defer lrm.FlushRelease()
next(lrm, r)
// Skip modification if response wasn't buffered (non-HTML content)
if !lrm.IsBuffered() {
return
}
rm := lrm.ResponseModifier()
currentBody := rm.BodyReader()
currentResp := &http.Response{
StatusCode: rm.StatusCode(),
@@ -210,8 +222,8 @@ func (m *Middleware) ServeHTTP(next http.HandlerFunc, w http.ResponseWriter, r *
// override the response status code
rm.WriteHeader(currentResp.StatusCode)
// overriding the response header is not necessary
// modifyResponse is supposed to write to Header directly instead of assigning a new header map)
// overriding the response header
maps.Copy(rm.Header(), currentResp.Header)
// override the content length and body if changed
if currentResp.Body != currentBody {
@@ -222,6 +234,12 @@ func (m *Middleware) ServeHTTP(next http.HandlerFunc, w http.ResponseWriter, r *
}
}
// needsBuffering determines if a response should be buffered for modification.
// Only HTML responses need buffering; streaming content (video, audio, etc.) should pass through.
func needsBuffering(header http.Header) bool {
return httputils.GetContentType(header).IsHTML()
}
func (m *Middleware) LogWarn(req *http.Request) *zerolog.Event {
return log.Warn().Str("middleware", m.name).
Str("host", req.Host).

View File

@@ -7,8 +7,8 @@ import (
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
fsutils "github.com/yusing/goutils/fs"
strutils "github.com/yusing/goutils/strings"
)
@@ -52,7 +52,7 @@ func Get(name string) (*Middleware, Error) {
if !ok {
return nil, ErrUnknownMiddleware.
Subject(name).
With(gperr.DoYouMean(utils.NearestField(name, allMiddlewares)))
With(gperr.DoYouMeanField(name, allMiddlewares))
}
return middleware, nil
}
@@ -63,7 +63,7 @@ func All() map[string]*Middleware {
func LoadComposeFiles() {
errs := gperr.NewBuilder("middleware compile errors")
middlewareDefs, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0)
middlewareDefs, err := fsutils.ListFiles(common.MiddlewareComposeBasePath, 0)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return

View File

@@ -40,6 +40,10 @@ func (eofReader) Close() error { return nil }
// modifyResponse implements ResponseModifier.
func (m *modifyHTML) modifyResponse(resp *http.Response) error {
// Skip HEAD requests - no body to modify
if resp.Request.Method == http.MethodHead {
return nil
}
// including text/html and application/xhtml+xml
if !httputils.GetContentType(resp.Header).IsHTML() {
return nil

View File

@@ -117,7 +117,7 @@ func (amw *oidcMiddleware) before(w http.ResponseWriter, r *http.Request) (proce
case errors.Is(err, auth.ErrMissingOAuthToken):
amw.auth.HandleAuth(w, r)
default:
auth.WriteBlockPage(w, http.StatusForbidden, err.Error(), auth.OIDCLogoutPath)
auth.WriteBlockPage(w, http.StatusForbidden, err.Error(), "Logout", auth.OIDCLogoutPath)
}
return false
}

View File

@@ -4,11 +4,9 @@ import (
urlPkg "net/url"
"github.com/bytedance/sonic"
"github.com/yusing/godoxy/internal/utils"
)
type URL struct {
_ utils.NoCopy
urlPkg.URL
}

View File

@@ -11,13 +11,14 @@ import (
"github.com/luthermonson/go-proxmox"
"github.com/yusing/godoxy/internal/net/gphttp"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
)
type Config struct {
URL string `json:"url" validate:"required,url"`
TokenID string `json:"token_id" validate:"required"`
Secret string `json:"secret" validate:"required"`
TokenID string `json:"token_id" validate:"required"`
Secret strutils.Redacted `json:"secret" validate:"required"`
NoTLSVerify bool `json:"no_tls_verify" yaml:"no_tls_verify,omitempty"`
@@ -48,7 +49,7 @@ func (c *Config) Init() gperr.Error {
}
opts := []proxmox.Option{
proxmox.WithAPIToken(c.TokenID, c.Secret),
proxmox.WithAPIToken(c.TokenID, c.Secret.String()),
proxmox.WithHTTPClient(&http.Client{
Transport: tr,
}),

View File

@@ -7,7 +7,7 @@ import (
"github.com/bytedance/sonic"
"github.com/luthermonson/go-proxmox"
"github.com/yusing/godoxy/internal/utils/pool"
"github.com/yusing/goutils/pool"
)
type Node struct {

View File

@@ -2,6 +2,7 @@ package route
import (
"net/http"
"os"
"path"
"path/filepath"
@@ -9,6 +10,7 @@ import (
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/watcher/health/monitor"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/task"
@@ -24,8 +26,27 @@ type (
}
)
func handler(root string) http.Handler {
return http.FileServer(http.Dir(root))
var _ types.FileServerRoute = (*FileServer)(nil)
func handler(root string, spa bool, index string) http.Handler {
if !spa {
return http.FileServer(http.Dir(root))
}
indexPath := filepath.Join(root, index)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
urlPath := path.Clean(r.URL.Path)
if urlPath == "/" {
http.ServeFile(w, r, indexPath)
return
}
fullPath := filepath.Join(root, filepath.FromSlash(urlPath))
stat, err := os.Stat(fullPath)
if err == nil && !stat.IsDir() {
http.ServeFile(w, r, fullPath)
return
}
http.ServeFile(w, r, indexPath)
})
}
func NewFileServer(base *Route) (*FileServer, gperr.Error) {
@@ -36,7 +57,12 @@ func NewFileServer(base *Route) (*FileServer, gperr.Error) {
return nil, gperr.New("`root` must be an absolute path")
}
s.handler = handler(s.Root)
if s.Index == "" {
s.Index = "/index.html"
} else if s.Index[0] != '/' {
s.Index = "/" + s.Index
}
s.handler = handler(s.Root, s.SPA, s.Index)
if len(s.Middlewares) > 0 {
mid, err := middleware.BuildMiddlewareFromMap(s.Alias, s.Middlewares)
@@ -91,16 +117,12 @@ func (s *FileServer) Start(parent task.Parent) gperr.Error {
}
if s.UseHealthCheck() {
s.HealthMon = monitor.NewFileServerHealthMonitor(s.HealthCheck, s.Root)
s.HealthMon = monitor.NewMonitor(s)
if err := s.HealthMon.Start(s.task); err != nil {
return err
}
}
if s.ShouldExclude() {
return nil
}
routes.HTTP.Add(s)
s.task.OnFinished("remove_route_from_http", func() {
routes.HTTP.Del(s)
@@ -108,6 +130,10 @@ func (s *FileServer) Start(parent task.Parent) gperr.Error {
return nil
}
func (s *FileServer) RootPath() string {
return s.Root
}
// ServeHTTP implements http.Handler.
func (s *FileServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
s.handler.ServeHTTP(w, req)

View File

@@ -18,8 +18,9 @@ import (
)
type DockerProvider struct {
name, dockerHost string
l zerolog.Logger
name string
dockerCfg types.DockerProviderConfig
l zerolog.Logger
}
const (
@@ -29,10 +30,10 @@ const (
var ErrAliasRefIndexOutOfRange = gperr.New("index out of range")
func DockerProviderImpl(name, dockerHost string) ProviderImpl {
func DockerProviderImpl(name string, dockerCfg types.DockerProviderConfig) ProviderImpl {
return &DockerProvider{
name,
dockerHost,
dockerCfg,
log.With().Str("type", "docker").Str("name", name).Logger(),
}
}
@@ -54,14 +55,14 @@ func (p *DockerProvider) Logger() *zerolog.Logger {
}
func (p *DockerProvider) NewWatcher() watcher.Watcher {
return watcher.NewDockerWatcher(p.dockerHost)
return watcher.NewDockerWatcher(p.dockerCfg)
}
func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
containers, err := docker.ListContainers(ctx, p.dockerHost)
containers, err := docker.ListContainers(ctx, p.dockerCfg)
if err != nil {
return nil, gperr.Wrap(err)
}
@@ -70,7 +71,7 @@ func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) {
routes := make(route.Routes)
for _, c := range containers {
container := docker.FromDocker(&c, p.dockerHost)
container := docker.FromDocker(&c, p.dockerCfg)
if container.Errors != nil {
errs.Add(container.Errors)

View File

@@ -6,6 +6,7 @@ import (
"github.com/goccy/go-yaml"
"github.com/moby/moby/api/types/container"
"github.com/yusing/godoxy/internal/docker"
"github.com/yusing/godoxy/internal/types"
expect "github.com/yusing/goutils/testing"
_ "embed"
@@ -28,7 +29,7 @@ func TestParseDockerLabels(t *testing.T) {
Ports: []container.PortSummary{
{Type: "tcp", PrivatePort: 1234, PublicPort: 1234},
},
}, "/var/run/docker.sock"),
}, types.DockerProviderConfig{URL: "unix:///var/run/docker.sock"}),
)
expect.NoError(t, err)
expect.True(t, routes.Contains("app"))

View File

@@ -11,6 +11,7 @@ import (
D "github.com/yusing/godoxy/internal/docker"
"github.com/yusing/godoxy/internal/route"
routeTypes "github.com/yusing/godoxy/internal/route/types"
"github.com/yusing/godoxy/internal/types"
expect "github.com/yusing/goutils/testing"
)
@@ -31,7 +32,7 @@ func makeRoutes(cont *container.Summary, dockerHostIP ...string) route.Routes {
}
cont.ID = "test"
p.name = "test"
routes := expect.Must(p.routesFromContainerLabels(D.FromDocker(cont, host)))
routes := expect.Must(p.routesFromContainerLabels(D.FromDocker(cont, types.DockerProviderConfig{URL: host})))
for _, r := range routes {
r.Finalize()
}
@@ -39,7 +40,7 @@ func makeRoutes(cont *container.Summary, dockerHostIP ...string) route.Routes {
}
func TestExplicitOnly(t *testing.T) {
p := NewDockerProvider("a!", "")
p := NewDockerProvider("a!", types.DockerProviderConfig{})
expect.True(t, p.IsExplicitOnly())
}
@@ -199,7 +200,7 @@ func TestApplyLabelWithRefIndexError(t *testing.T) {
"proxy.*.port": "4444",
"proxy.#4.scheme": "https",
},
}, "")
}, types.DockerProviderConfig{})
var p DockerProvider
_, err := p.routesFromContainerLabels(c)
expect.ErrorIs(t, ErrAliasRefIndexOutOfRange, err)
@@ -211,7 +212,7 @@ func TestApplyLabelWithRefIndexError(t *testing.T) {
D.LabelAliases: "a,b",
"proxy.#0.host": "localhost",
},
}, "")
}, types.DockerProviderConfig{})
_, err = p.routesFromContainerLabels(c)
expect.ErrorIs(t, ErrAliasRefIndexOutOfRange, err)
}

View File

@@ -8,17 +8,14 @@ import (
"sync"
"time"
"github.com/moby/moby/client"
"github.com/rs/zerolog"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/docker"
"github.com/yusing/godoxy/internal/route"
provider "github.com/yusing/godoxy/internal/route/provider/types"
"github.com/yusing/godoxy/internal/types"
W "github.com/yusing/godoxy/internal/watcher"
"github.com/yusing/godoxy/internal/watcher/events"
"github.com/yusing/goutils/env"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/task"
)
@@ -69,13 +66,9 @@ func NewFileProvider(filename string) (p *Provider, err error) {
return p, err
}
func NewDockerProvider(name string, dockerHost string) *Provider {
if dockerHost == common.DockerHostFromEnv {
dockerHost = env.GetEnvString("DOCKER_HOST", client.DefaultDockerHost)
}
func NewDockerProvider(name string, dockerCfg types.DockerProviderConfig) *Provider {
p := newProvider(provider.ProviderTypeDocker)
p.ProviderImpl = DockerProviderImpl(name, dockerHost)
p.ProviderImpl = DockerProviderImpl(name, dockerCfg)
p.watcher = p.NewWatcher()
return p
}
@@ -84,7 +77,9 @@ func NewAgentProvider(cfg *agent.AgentConfig) *Provider {
p := newProvider(provider.ProviderTypeAgent)
agent := &AgentProvider{
AgentConfig: cfg,
docker: DockerProviderImpl(cfg.Name, cfg.FakeDockerHost()),
docker: DockerProviderImpl(cfg.Name, types.DockerProviderConfig{
URL: cfg.FakeDockerHost(),
}),
}
p.ProviderImpl = agent
p.watcher = p.NewWatcher()
@@ -224,10 +219,13 @@ func (p *Provider) startRoute(parent task.Parent, r *route.Route) gperr.Error {
p.lockDeleteRoute(r.Alias)
return err.Subject(r.Alias)
}
p.lockAddRoute(r)
r.Task().OnCancel("remove_route_from_provider", func() {
p.lockDeleteRoute(r.Alias)
})
if !r.ShouldExclude() {
r.Task().OnCancel("remove_route_from_provider", func() {
p.lockDeleteRoute(r.Alias)
})
}
return nil
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
nettypes "github.com/yusing/godoxy/internal/net/types"
"github.com/yusing/godoxy/internal/route/routes"
route "github.com/yusing/godoxy/internal/route/types"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/watcher/health/monitor"
gperr "github.com/yusing/goutils/errs"
@@ -60,6 +61,28 @@ func NewReverseProxyRoute(base *Route) (*ReveseProxyRoute, gperr.Error) {
service := base.Name()
rp := reverseproxy.NewReverseProxy(service, &proxyURL.URL, trans)
scheme := base.Scheme
retried := false
retryLock := sync.Mutex{}
rp.OnSchemeMisMatch = func() (retry bool) { // switch scheme and retry
retryLock.Lock()
defer retryLock.Unlock()
if retried {
return false
}
retried = true
if scheme == route.SchemeHTTP {
rp.TargetURL.Scheme = "https"
} else {
rp.TargetURL.Scheme = "http"
}
rp.Info().Msgf("scheme mismatch detected, retrying with %s", rp.TargetURL.Scheme)
return true
}
if len(base.Middlewares) > 0 {
err := middleware.PatchReverseProxy(rp, base.Middlewares)
if err != nil {
@@ -137,10 +160,6 @@ func (r *ReveseProxyRoute) Start(parent task.Parent) gperr.Error {
}
}
if r.ShouldExclude() {
return nil
}
if r.UseLoadBalance() {
r.addToLoadBalancer(parent)
} else {

View File

@@ -15,6 +15,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/agent/pkg/agent"
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/docker"
"github.com/yusing/godoxy/internal/homepage"
homepagecfg "github.com/yusing/godoxy/internal/homepage/types"
@@ -23,12 +24,14 @@ import (
"github.com/yusing/godoxy/internal/proxmox"
"github.com/yusing/godoxy/internal/serialization"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/watcher/health/monitor"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/task"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/godoxy/internal/route/rules"
rulepresets "github.com/yusing/godoxy/internal/route/rules/presets"
route "github.com/yusing/godoxy/internal/route/types"
@@ -43,13 +46,16 @@ type (
Scheme route.Scheme `json:"scheme,omitempty" swaggertype:"string" enums:"http,https,tcp,udp,fileserver"`
Host string `json:"host,omitempty"`
Port route.Port `json:"port"`
Root string `json:"root,omitempty"`
Root string `json:"root,omitempty"`
SPA bool `json:"spa,omitempty"` // Single-page app mode: serves index for non-existent paths
Index string `json:"index,omitempty"` // Index file to serve for single-page app mode
route.HTTPConfig
PathPatterns []string `json:"path_patterns,omitempty" extensions:"x-nullable"`
Rules rules.Rules `json:"rules,omitempty" extension:"x-nullable"`
RuleFile string `json:"rule_file,omitempty" extensions:"x-nullable"`
HealthCheck *types.HealthCheckConfig `json:"healthcheck,omitempty" extensions:"x-nullable"` // null on load-balancer routes
HealthCheck types.HealthCheckConfig `json:"healthcheck,omitempty" extensions:"x-nullable"` // null on load-balancer routes
LoadBalance *types.LoadBalancerConfig `json:"load_balance,omitempty" extensions:"x-nullable"`
Middlewares map[string]types.LabelMap `json:"middlewares,omitempty" extensions:"x-nullable"`
Homepage *homepage.ItemConfig `json:"homepage"`
@@ -378,25 +384,39 @@ func (r *Route) start(parent task.Parent) gperr.Error {
defer close(r.started)
// skip checking for excluded routes
if !r.ShouldExclude() {
excluded := r.ShouldExclude()
if !excluded {
if err := checkExists(r); err != nil {
return err
}
}
if cont := r.ContainerInfo(); cont != nil {
docker.SetDockerHostByContainerID(cont.ContainerID, cont.DockerHost)
docker.SetDockerCfgByContainerID(cont.ContainerID, cont.DockerCfg)
}
if err := r.impl.Start(parent); err != nil {
return err
if !excluded {
if err := r.impl.Start(parent); err != nil {
return err
}
} else {
r.task = parent.Subtask("excluded."+r.Name(), true)
routes.Excluded.Add(r.impl)
r.task.OnCancel("remove_route_from_excluded", func() {
routes.Excluded.Del(r.impl)
})
if r.UseHealthCheck() {
r.HealthMon = monitor.NewMonitor(r.impl)
err := r.HealthMon.Start(r.task)
return err
}
}
return nil
}
func (r *Route) Finish(reason any) {
if cont := r.ContainerInfo(); cont != nil {
docker.DeleteDockerHostByContainerID(cont.ContainerID)
docker.DeleteDockerCfgByContainerID(cont.ContainerID)
}
r.FinishAndWait(reason)
}
@@ -494,7 +514,7 @@ func (r *Route) IdlewatcherConfig() *types.IdlewatcherConfig {
return r.Idlewatcher
}
func (r *Route) HealthCheckConfig() *types.HealthCheckConfig {
func (r *Route) HealthCheckConfig() types.HealthCheckConfig {
return r.HealthCheck
}
@@ -577,30 +597,10 @@ func (r *Route) IsZeroPort() bool {
}
func (r *Route) ShouldExclude() bool {
if r.valErr.Get() != nil {
if r.ExcludedReason != ExcludedReasonNone {
return true
}
if r.Excluded {
return true
}
if r.Container != nil {
switch {
case r.Container.IsExcluded:
return true
case r.IsZeroPort() && !r.UseIdleWatcher():
return true
case !r.Container.IsExplicit && docker.IsBlacklisted(r.Container):
return true
case strings.HasPrefix(r.Container.ContainerName, "buildx_"):
return true
}
} else if r.IsZeroPort() && r.Scheme != route.SchemeFileServer {
return true
}
if strings.HasSuffix(r.Alias, "-old") {
return true
}
return false
return r.findExcludedReason() != ExcludedReasonNone
}
type ExcludedReason uint8
@@ -788,17 +788,7 @@ func (r *Route) Finalize() {
}
r.Port.Listening, r.Port.Proxy = lp, pp
if r.HealthCheck == nil {
r.HealthCheck = types.DefaultHealthConfig()
}
if r.HealthCheck.Interval == 0 {
r.HealthCheck.Interval = common.HealthCheckIntervalDefault
}
if r.HealthCheck.Timeout == 0 {
r.HealthCheck.Timeout = common.HealthCheckTimeoutDefault
}
r.HealthCheck.ApplyDefaults(config.WorkingState.Load().Value().Defaults.HealthCheck)
}
func (r *Route) FinalizeHomepageConfig() {
@@ -811,7 +801,6 @@ func (r *Route) FinalizeHomepageConfig() {
if r.Homepage == nil {
r.Homepage = &homepage.ItemConfig{
Show: true,
Name: r.Alias,
}
}

View File

@@ -2,6 +2,7 @@ package route
import (
"testing"
"time"
"github.com/yusing/godoxy/internal/common"
route "github.com/yusing/godoxy/internal/route/types"
@@ -40,7 +41,7 @@ func TestRouteValidate(t *testing.T) {
Scheme: route.SchemeHTTP,
Host: "example.com",
Port: route.Port{Proxy: 80},
HealthCheck: &types.HealthCheckConfig{
HealthCheck: types.HealthCheckConfig{
Disable: true,
},
LoadBalance: &types.LoadBalancerConfig{
@@ -179,3 +180,14 @@ func TestRouteAgent(t *testing.T) {
expect.NoError(t, err, "Validate should not return error for valid route with agent")
expect.NotNil(t, r.GetAgent(), "GetAgent should return agent")
}
func TestRouteApplyingHealthCheckDefaults(t *testing.T) {
hc := types.HealthCheckConfig{}
hc.ApplyDefaults(types.HealthCheckConfig{
Interval: 15 * time.Second,
Timeout: 10 * time.Second,
})
expect.Equal(t, hc.Interval, 15*time.Second)
expect.Equal(t, hc.Timeout, 10*time.Second)
}

View File

@@ -2,10 +2,6 @@ package routes
import (
"context"
"crypto/tls"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"reflect"
@@ -34,7 +30,8 @@ func (r *RouteContext) Value(key any) any {
func WithRouteContext(r *http.Request, route types.HTTPRoute) *http.Request {
// we don't want to copy the request object every fucking requests
// return r.WithContext(context.WithValue(r.Context(), routeContextKey, route))
(*requestInternal)(unsafe.Pointer(r)).ctx = &RouteContext{
ctxFieldPtr := (*context.Context)(unsafe.Pointer(uintptr(unsafe.Pointer(r)) + ctxFieldOffset))
*ctxFieldPtr = &RouteContext{
Context: r.Context(),
Route: route,
}
@@ -107,43 +104,12 @@ func TryGetUpstreamURL(r *http.Request) string {
return ""
}
type requestInternal struct {
Method string
URL *url.URL
Proto string
ProtoMajor int
ProtoMinor int
Header http.Header
Body io.ReadCloser
GetBody func() (io.ReadCloser, error)
ContentLength int64
TransferEncoding []string
Close bool
Host string
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
Trailer http.Header
RemoteAddr string
RequestURI string
TLS *tls.ConnectionState
Cancel <-chan struct{}
Response *http.Response
Pattern string
ctx context.Context
}
var ctxFieldOffset uintptr
func init() {
// make sure ctx has the same offset as http.Request
f, ok := reflect.TypeFor[requestInternal]().FieldByName("ctx")
f, ok := reflect.TypeFor[http.Request]().FieldByName("ctx")
if !ok {
panic("ctx field not found")
}
f2, ok := reflect.TypeFor[http.Request]().FieldByName("ctx")
if !ok {
panic("ctx field not found")
}
if f.Offset != f2.Offset {
panic(fmt.Sprintf("ctx has different offset than http.Request: %d != %d", f.Offset, f2.Offset))
}
ctxFieldOffset = f.Offset
}

View File

@@ -17,17 +17,23 @@ type HealthInfoWithoutDetail struct {
Latency time.Duration `json:"latency" swaggertype:"number"` // latency in microseconds
} // @name HealthInfoWithoutDetail
// GetHealthInfo returns a map of route name to health info.
//
// The health info is for all routes, including excluded routes.
func GetHealthInfo() map[string]HealthInfo {
healthMap := make(map[string]HealthInfo, NumRoutes())
for r := range Iter {
healthMap := make(map[string]HealthInfo, NumAllRoutes())
for r := range IterAll {
healthMap[r.Name()] = getHealthInfo(r)
}
return healthMap
}
// GetHealthInfoWithoutDetail returns a map of route name to health info without detail.
//
// The health info is for all routes, including excluded routes.
func GetHealthInfoWithoutDetail() map[string]HealthInfoWithoutDetail {
healthMap := make(map[string]HealthInfoWithoutDetail, NumRoutes())
for r := range Iter {
healthMap := make(map[string]HealthInfoWithoutDetail, NumAllRoutes())
for r := range IterAll {
healthMap[r.Name()] = getHealthInfoWithoutDetail(r)
}
return healthMap
@@ -67,9 +73,12 @@ func getHealthInfoWithoutDetail(r types.Route) HealthInfoWithoutDetail {
}
}
// ByProvider returns a map of provider name to routes.
//
// The routes are all routes, including excluded routes.
func ByProvider() map[string][]types.Route {
rts := make(map[string][]types.Route)
for r := range Iter {
for r := range IterAll {
rts[r.ProviderName()] = append(rts[r.ProviderName()], r)
}
return rts

View File

@@ -2,15 +2,17 @@ package routes
import (
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/utils/pool"
"github.com/yusing/goutils/pool"
)
var (
HTTP = pool.New[types.HTTPRoute]("http_routes")
Stream = pool.New[types.StreamRoute]("stream_routes")
Excluded = pool.New[types.Route]("excluded_routes")
)
func Iter(yield func(r types.Route) bool) {
func IterActive(yield func(r types.Route) bool) {
for _, r := range HTTP.Iter {
if !yield(r) {
break
@@ -23,26 +25,36 @@ func Iter(yield func(r types.Route) bool) {
}
}
func IterKV(yield func(alias string, r types.Route) bool) {
for k, r := range HTTP.Iter {
if !yield(k, r) {
func IterAll(yield func(r types.Route) bool) {
for _, r := range HTTP.Iter {
if !yield(r) {
break
}
}
for k, r := range Stream.Iter {
if !yield(k, r) {
for _, r := range Stream.Iter {
if !yield(r) {
break
}
}
for _, r := range Excluded.Iter {
if !yield(r) {
break
}
}
}
func NumRoutes() int {
func NumActiveRoutes() int {
return HTTP.Size() + Stream.Size()
}
func NumAllRoutes() int {
return HTTP.Size() + Stream.Size() + Excluded.Size()
}
func Clear() {
HTTP.Clear()
Stream.Clear()
Excluded.Clear()
}
func GetHTTPRouteOrExact(alias, host string) (types.HTTPRoute, bool) {
@@ -54,6 +66,9 @@ func GetHTTPRouteOrExact(alias, host string) (types.HTTPRoute, bool) {
return HTTP.Get(host)
}
// Get returns the route with the given alias.
//
// It does not return excluded routes.
func Get(alias string) (types.Route, bool) {
if r, ok := HTTP.Get(alias); ok {
return r, true

Some files were not shown because too many files have changed in this diff Show More