mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-31 14:18:17 +01:00
Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52511f1618 | ||
|
|
ca2cbd3332 | ||
|
|
57f7b72923 | ||
|
|
54b3008cce | ||
|
|
d9abd65471 | ||
|
|
d5b9d2e6bc | ||
|
|
ec9cab05c7 | ||
|
|
6ff1346675 | ||
|
|
a75441aa8a | ||
|
|
32971b45c3 | ||
|
|
4bc7ce09c7 | ||
|
|
b5946b34b8 | ||
|
|
442e4a0972 | ||
|
|
d1c79acf87 | ||
|
|
df70f7f63c | ||
|
|
6707143aaf | ||
|
|
742a86d079 | ||
|
|
45f856b8d1 | ||
|
|
044c7bf8a0 | ||
|
|
e7b19c472b | ||
|
|
1ace5e641d | ||
|
|
83976646db | ||
|
|
8e1d75abd6 | ||
|
|
5c341d4745 | ||
|
|
9f245a62f2 | ||
|
|
ecc2547eb1 | ||
|
|
e2a48fcff9 | ||
|
|
6b09f54793 | ||
|
|
7b5ad3ab5e | ||
|
|
48790b4756 | ||
|
|
727a3e9452 | ||
|
|
f8f1b54aaf | ||
|
|
cb8f405e76 | ||
|
|
51704829c6 | ||
|
|
a1cc1d844d | ||
|
|
633deb85ca | ||
|
|
ee1c375fd9 | ||
|
|
2713f5282e | ||
|
|
f70cca0d00 | ||
|
|
3adefe4202 | ||
|
|
966c873013 | ||
|
|
b8c61c37dc | ||
|
|
22582cd32f | ||
|
|
a46573cab3 | ||
|
|
64e380cc40 | ||
|
|
2f68c7c386 | ||
|
|
e3001a70ed | ||
|
|
ea6543b3f9 | ||
|
|
1793dd629f | ||
|
|
edf8c6ea32 | ||
|
|
60cdffcf3c | ||
|
|
717ed04b38 | ||
|
|
80a6b21ff9 | ||
|
|
e48c3f57dd | ||
|
|
39f427555f | ||
|
|
280e455198 | ||
|
|
082991694b | ||
|
|
4ec9d818bf | ||
|
|
258d8921ad | ||
|
|
2b8d416625 | ||
|
|
b4e9613efe | ||
|
|
25605208e4 | ||
|
|
29036bed18 | ||
|
|
6d78f14cc7 | ||
|
|
4730753cb3 | ||
|
|
4fa2ef41aa | ||
|
|
732376328c | ||
|
|
110fe4b0aa | ||
|
|
45ba8d447a | ||
|
|
108417ca9d | ||
|
|
bd1ff9731d | ||
|
|
235af71343 | ||
|
|
059306c93b | ||
|
|
d938e24cf5 | ||
|
|
ab1881d02e | ||
|
|
90a4922b79 | ||
|
|
2022a0db82 | ||
|
|
4f8bb40d3d | ||
|
|
8ea296c99f | ||
|
|
ccefaf003d | ||
|
|
61236e0ace | ||
|
|
6a89ab77c8 | ||
|
|
9f1c279698 | ||
|
|
89cbcfee8c | ||
|
|
2da4bbedbf | ||
|
|
d73272b8e0 |
20
.github/workflows/docker-image-compat.yml
vendored
Normal file
20
.github/workflows/docker-image-compat.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Docker Image CI (compat)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- compat
|
||||
|
||||
jobs:
|
||||
build-compat:
|
||||
uses: ./.github/workflows/docker-image.yml
|
||||
with:
|
||||
image_name: ${{ github.repository_owner }}/godoxy
|
||||
tag: latest-compat
|
||||
target: main
|
||||
build-compat-agent:
|
||||
uses: ./.github/workflows/docker-image.yml
|
||||
with:
|
||||
image_name: ${{ github.repository_owner }}/godoxy-agent
|
||||
tag: latest-compat
|
||||
target: agent
|
||||
1
.github/workflows/docker-image-nightly.yml
vendored
1
.github/workflows/docker-image-nightly.yml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
- "**" # matches every branch
|
||||
- "!dependabot/*"
|
||||
- "!main" # excludes main
|
||||
- "!compat" # excludes compat branch
|
||||
|
||||
jobs:
|
||||
build-nightly:
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -40,4 +40,8 @@ tsconfig.tsbuildinfo
|
||||
|
||||
!agent.compose.yml
|
||||
!agent/pkg/**
|
||||
dev-data/
|
||||
dev-data/
|
||||
|
||||
RELEASE_NOTES.md
|
||||
CLAUDE.md
|
||||
.kilocode/**
|
||||
21
agent/go.mod
21
agent/go.mod
@@ -14,15 +14,16 @@ replace (
|
||||
|
||||
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
|
||||
|
||||
exclude github.com/yusing/godoxy/internal/utils v0.0.0-20250927032450-e2aeef3a863f
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.15.0
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/pion/dtls/v3 v3.0.10
|
||||
github.com/pion/transport/v3 v3.1.1
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/yusing/godoxy v0.25.0
|
||||
github.com/yusing/godoxy v0.25.2
|
||||
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
||||
github.com/yusing/goutils v0.7.0
|
||||
)
|
||||
@@ -31,6 +32,7 @@ require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
@@ -38,7 +40,8 @@ require (
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/cli v29.1.5+incompatible // indirect
|
||||
github.com/docker/cli v29.2.0+incompatible // indirect
|
||||
github.com/docker/docker v28.5.2+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
@@ -62,8 +65,9 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/moby/api v1.52.0 // indirect
|
||||
github.com/moby/moby/client v0.2.1 // indirect
|
||||
github.com/moby/moby/api v1.53.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
@@ -71,6 +75,7 @@ require (
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
|
||||
@@ -86,12 +91,13 @@ require (
|
||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||
github.com/yusing/ds v0.4.1 // indirect
|
||||
github.com/yusing/gointernals v0.1.16 // indirect
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260125040745-bcc4b498f878 // indirect
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260125040745-bcc4b498f878 // indirect
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468 // indirect
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
@@ -99,6 +105,7 @@ require (
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
49
agent/go.sum
49
agent/go.sum
@@ -1,3 +1,5 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||
@@ -24,6 +26,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
@@ -37,8 +41,10 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao=
|
||||
github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
|
||||
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -82,8 +88,8 @@ github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -95,6 +101,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
|
||||
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
||||
@@ -128,15 +136,21 @@ github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
||||
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
|
||||
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
||||
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
|
||||
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
@@ -153,8 +167,9 @@ github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkY
|
||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||
github.com/pires/go-proxyproto v0.9.1 h1:wTPjpyk41pJm1Im9BqHtPLuhxfjxL+qNfSikx9ux0WY=
|
||||
github.com/pires/go-proxyproto v0.9.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
|
||||
github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
@@ -220,6 +235,10 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGN
|
||||
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/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
@@ -228,6 +247,8 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
@@ -258,6 +279,12 @@ golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -268,5 +295,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
|
||||
@@ -150,7 +150,7 @@ func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte)
|
||||
// test stream server connection
|
||||
const fakeAddress = "localhost:8080" // it won't be used, just for testing
|
||||
// test TCP stream support
|
||||
err := agentstream.TCPHealthCheck(cfg.Addr, cfg.caCert, cfg.clientCert)
|
||||
err := agentstream.TCPHealthCheck(ctx, cfg.Addr, cfg.caCert, cfg.clientCert)
|
||||
if err != nil {
|
||||
streamUnsupportedErrs.Addf("failed to connect to stream server via TCP: %w", err)
|
||||
} else {
|
||||
@@ -158,7 +158,7 @@ func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte)
|
||||
}
|
||||
|
||||
// test UDP stream support
|
||||
err = agentstream.UDPHealthCheck(cfg.Addr, cfg.caCert, cfg.clientCert)
|
||||
err = agentstream.UDPHealthCheck(ctx, cfg.Addr, cfg.caCert, cfg.clientCert)
|
||||
if err != nil {
|
||||
streamUnsupportedErrs.Addf("failed to connect to stream server via UDP: %w", err)
|
||||
} else {
|
||||
@@ -313,8 +313,18 @@ func (cfg *AgentConfig) do(ctx context.Context, method, endpoint string, body io
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timeout := 5 * time.Second
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
remaining := time.Until(deadline)
|
||||
if remaining > 0 {
|
||||
timeout = remaining
|
||||
}
|
||||
}
|
||||
|
||||
client := http.Client{
|
||||
Transport: cfg.Transport(),
|
||||
Timeout: timeout,
|
||||
}
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
@@ -34,13 +35,13 @@ func NewTCPClient(serverAddr, targetAddress string, caCert *x509.Certificate, cl
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newTCPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
||||
return newTCPClientWIthHeader(context.Background(), serverAddr, header, caCert, clientCert)
|
||||
}
|
||||
|
||||
func TCPHealthCheck(serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
||||
func TCPHealthCheck(ctx context.Context, serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
||||
header := NewStreamHealthCheckHeader()
|
||||
|
||||
conn, err := newTCPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
||||
conn, err := newTCPClientWIthHeader(ctx, serverAddr, header, caCert, clientCert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -49,7 +50,7 @@ func TCPHealthCheck(serverAddr string, caCert *x509.Certificate, clientCert *tls
|
||||
return nil
|
||||
}
|
||||
|
||||
func newTCPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||
func newTCPClientWIthHeader(ctx context.Context, serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||
// Setup TLS configuration
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(caCert)
|
||||
@@ -62,17 +63,43 @@ func newTCPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCe
|
||||
ServerName: common.CertsDNSName,
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: dialTimeout,
|
||||
}
|
||||
tlsDialer := &tls.Dialer{
|
||||
NetDialer: dialer,
|
||||
Config: tlsConfig,
|
||||
}
|
||||
|
||||
// Establish TLS connection
|
||||
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: dialTimeout}, "tcp", serverAddr, tlsConfig)
|
||||
conn, err := tlsDialer.DialContext(ctx, "tcp", serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deadline, hasDeadline := ctx.Deadline()
|
||||
if hasDeadline {
|
||||
err := conn.SetWriteDeadline(deadline)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Send the stream header once as a handshake.
|
||||
if _, err := conn.Write(header.Bytes()); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hasDeadline {
|
||||
// reset write deadline
|
||||
err = conn.SetWriteDeadline(time.Time{})
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &TCPClient{
|
||||
conn: conn,
|
||||
}, nil
|
||||
|
||||
@@ -12,7 +12,7 @@ func TestTCPHealthCheck(t *testing.T) {
|
||||
|
||||
srv := startTCPServer(t, certs)
|
||||
|
||||
err := stream.TCPHealthCheck(srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
||||
err := stream.TCPHealthCheck(t.Context(), srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
||||
require.NoError(t, err, "health check")
|
||||
}
|
||||
|
||||
@@ -21,6 +21,6 @@ func TestUDPHealthCheck(t *testing.T) {
|
||||
|
||||
srv := startUDPServer(t, certs)
|
||||
|
||||
err := stream.UDPHealthCheck(srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
||||
err := stream.UDPHealthCheck(t.Context(), srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
||||
require.NoError(t, err, "health check")
|
||||
}
|
||||
|
||||
@@ -102,7 +102,6 @@ func TestUDPServer_RejectInvalidClient(t *testing.T) {
|
||||
|
||||
srv := startUDPServer(t, certs)
|
||||
|
||||
|
||||
// Try to connect with a client cert from a different CA
|
||||
_, err = stream.NewUDPClient(srv.Addr.String(), dstAddr, certs.CaCert, invalidClientCert)
|
||||
require.Error(t, err, "expected error when connecting with client cert from different CA")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
@@ -35,10 +36,10 @@ func NewUDPClient(serverAddr, targetAddress string, caCert *x509.Certificate, cl
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newUDPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
||||
return newUDPClientWIthHeader(context.Background(), serverAddr, header, caCert, clientCert)
|
||||
}
|
||||
|
||||
func newUDPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||
func newUDPClientWIthHeader(ctx context.Context, serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||
// Setup DTLS configuration
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(caCert)
|
||||
@@ -62,21 +63,40 @@ func newUDPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCe
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deadline, hasDeadline := ctx.Deadline()
|
||||
if hasDeadline {
|
||||
err := conn.SetWriteDeadline(deadline)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Send the stream header once as a handshake.
|
||||
if _, err := conn.Write(header.Bytes()); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hasDeadline {
|
||||
// reset write deadline
|
||||
err = conn.SetWriteDeadline(time.Time{})
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &UDPClient{
|
||||
conn: conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func UDPHealthCheck(serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
||||
func UDPHealthCheck(ctx context.Context, serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
||||
header := NewStreamHealthCheckHeader()
|
||||
|
||||
conn, err := newUDPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
||||
conn, err := newUDPClientWIthHeader(ctx, serverAddr, header, caCert, clientCert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ package agentproxy
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
route "github.com/yusing/godoxy/internal/route/types"
|
||||
)
|
||||
|
||||
@@ -53,7 +53,7 @@ func proxyConfigFromHeaders(h http.Header) (cfg Config, err error) {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
err = sonic.Unmarshal(cfgJSON, &cfg)
|
||||
err = json.Unmarshal(cfgJSON, &cfg)
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func (cfg *Config) SetAgentProxyConfigHeadersLegacy(h http.Header) {
|
||||
func (cfg *Config) SetAgentProxyConfigHeaders(h http.Header) {
|
||||
h.Set(HeaderXProxyHost, cfg.Host)
|
||||
h.Set(HeaderXProxyScheme, string(cfg.Scheme))
|
||||
cfgJSON, _ := sonic.Marshal(cfg.HTTPConfig)
|
||||
cfgJSON, _ := json.Marshal(cfg.HTTPConfig)
|
||||
cfgBase64 := base64.StdEncoding.EncodeToString(cfgJSON)
|
||||
h.Set(HeaderXProxyConfig, cfgBase64)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -8,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
healthcheck "github.com/yusing/godoxy/internal/health/check"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
)
|
||||
@@ -73,7 +73,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
sonic.ConfigDefault.NewEncoder(w).Encode(result)
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
func parseMsOrDefault(msStr string) time.Duration {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
@@ -51,7 +51,7 @@ func NewAgentHandler() http.Handler {
|
||||
Runtime: env.Runtime,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
sonic.ConfigDefault.NewEncoder(w).Encode(agentInfo)
|
||||
json.NewEncoder(w).Encode(agentInfo)
|
||||
})
|
||||
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
|
||||
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)
|
||||
|
||||
@@ -38,7 +38,7 @@ func main() {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-time.After(time.Second * 10):
|
||||
case <-time.After(common.InitTimeout):
|
||||
log.Fatal().Msgf("timeout waiting for initialization to complete, exiting...")
|
||||
}
|
||||
}()
|
||||
|
||||
48
go.mod
48
go.mod
@@ -14,9 +14,12 @@ replace (
|
||||
github.com/yusing/goutils/server => ./goutils/server
|
||||
)
|
||||
|
||||
exclude github.com/luthermonson/go-proxmox v0.3.0
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon; modify_html middleware
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
|
||||
github.com/docker/docker v28.5.2+incompatible // docker daemon
|
||||
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||
github.com/gin-gonic/gin v1.11.0 // api server
|
||||
github.com/go-acme/lego/v4 v4.31.0 // acme client
|
||||
@@ -25,7 +28,7 @@ require (
|
||||
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
||||
github.com/gotify/server/v2 v2.8.0 // reference the Message struct for json response
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
||||
github.com/pires/go-proxyproto v0.9.1 // proxy protocol support
|
||||
github.com/pires/go-proxyproto v0.9.2 // proxy protocol support
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 // lock free map for concurrent operations
|
||||
github.com/rs/zerolog v1.34.0 // logging
|
||||
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
||||
@@ -38,27 +41,25 @@ require (
|
||||
|
||||
require (
|
||||
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
|
||||
github.com/bytedance/sonic v1.15.0 // fast json parsing
|
||||
github.com/docker/cli v29.1.5+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
||||
github.com/bytedance/sonic v1.15.0 // indirect; fast json parsing
|
||||
github.com/docker/cli v29.2.0+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
||||
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // jwt authentication
|
||||
github.com/luthermonson/go-proxmox v0.3.2 // proxmox API client
|
||||
github.com/moby/moby/api v1.52.0 // docker API
|
||||
github.com/moby/moby/client v0.2.1 // docker client
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/luthermonson/go-proxmox v0.3.2
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
github.com/quic-go/quic-go v0.59.0 // http3 support
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 // system information
|
||||
github.com/spf13/afero v1.15.0 // afero for file system operations
|
||||
github.com/stretchr/testify v1.11.1 // testing framework
|
||||
github.com/valyala/fasthttp v1.69.0 // fast http for health check
|
||||
github.com/yusing/ds v0.4.1 // data structures and algorithms
|
||||
github.com/yusing/godoxy/agent v0.0.0-20260125091326-9c2051840fd9
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260124133347-9a96f3cc539e
|
||||
github.com/yusing/godoxy/agent v0.0.0-20260129101716-0f13004ad6ba
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260129101716-0f13004ad6ba
|
||||
github.com/yusing/gointernals v0.1.16
|
||||
github.com/yusing/goutils v0.7.0
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260125040745-bcc4b498f878
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260125040745-bcc4b498f878
|
||||
github.com/yusing/goutils/server v0.0.0-20260125040745-bcc4b498f878
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468
|
||||
github.com/yusing/goutils/server v0.0.0-20260129081554-24e52ede7468
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -116,11 +117,11 @@ require (
|
||||
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/samber/slog-common v0.19.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
@@ -136,8 +137,8 @@ require (
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
google.golang.org/api v0.262.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect
|
||||
google.golang.org/api v0.263.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||
@@ -145,7 +146,10 @@ require (
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require github.com/moby/moby/api v1.53.0
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
@@ -155,6 +159,7 @@ require (
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
@@ -164,28 +169,33 @@ require (
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||
github.com/linode/linodego v1.64.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.10 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/pquerna/otp v1.5.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/vultr/govultr/v3 v3.26.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
)
|
||||
|
||||
68
go.sum
68
go.sum
@@ -23,6 +23,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourceg
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||
@@ -65,6 +67,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -76,8 +80,10 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao=
|
||||
github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
|
||||
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -137,8 +143,8 @@ github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@@ -159,6 +165,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
|
||||
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
||||
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
@@ -214,23 +222,29 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
||||
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
|
||||
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
||||
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
|
||||
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
|
||||
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
||||
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
||||
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1 h1:+fx2mbWeR8XX/vidwpRMepJMtRIYQP44Iezm2oeObVM=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1 h1:GDhBiaIAm/QXLzHJ0ASDdY/6R/9w60+gk8lY5rgfxEQ=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1/go.mod h1:EHScJdbM0gg5Is7e3C0ceRYAFMMsfP4Vf8sBRoxoTgk=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 h1:eMzyN+jGJbxG4ut278uwIsUo9XacXc711lFjhKnaUso=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 h1:t34IpOa+8NfmjkU8bdWtYrLrmr346/FGhu8FlpJDQok=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0/go.mod h1:p95/OxVsdx71I2Qrck1GtIS87sRxcTRKXzUi5nWm9NY=
|
||||
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
||||
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
@@ -251,10 +265,11 @@ github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||
github.com/pires/go-proxyproto v0.9.1 h1:wTPjpyk41pJm1Im9BqHtPLuhxfjxL+qNfSikx9ux0WY=
|
||||
github.com/pires/go-proxyproto v0.9.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
|
||||
github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
|
||||
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
||||
@@ -312,8 +327,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
@@ -341,6 +356,10 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGN
|
||||
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/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
@@ -349,6 +368,8 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
@@ -400,6 +421,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -447,14 +469,14 @@ golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.262.0 h1:4B+3u8He2GwyN8St3Jhnd3XRHlIvc//sBmgHSp78oNY=
|
||||
google.golang.org/api v0.262.0/go.mod h1:jNwmH8BgUBJ/VrUG6/lIl9YiildyLd09r9ZLHiQ6cGI=
|
||||
google.golang.org/api v0.263.0 h1:UFs7qn8gInIdtk1ZA6eXRXp5JDAnS4x9VRsRVCeKdbk=
|
||||
google.golang.org/api v0.263.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/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-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
@@ -472,5 +494,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
|
||||
2
goutils
2
goutils
Submodule goutils updated: 272bc53439...e5fba76994
@@ -106,7 +106,7 @@ func (c *Config) Validate() gperr.Error {
|
||||
c.allowLocal = true
|
||||
}
|
||||
|
||||
if c.Notify.Interval < 0 {
|
||||
if c.Notify.Interval <= 0 {
|
||||
c.Notify.Interval = defaultNotifyInterval
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ func newAgent(cfg *agent.AgentConfig) *Agent {
|
||||
AgentConfig: cfg,
|
||||
httpClient: &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
fasthttpHcClient: &fasthttp.Client{
|
||||
DialTimeout: func(addr string, timeout time.Duration) (net.Conn, error) {
|
||||
|
||||
@@ -2,12 +2,12 @@ package agentpool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/valyala/fasthttp"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
@@ -63,7 +63,7 @@ func (cfg *Agent) DoHealthCheck(timeout time.Duration, query string) (ret Health
|
||||
ret.Detail = fmt.Sprintf("HTTP %d %s", status, resp.Body())
|
||||
return ret, nil
|
||||
} else {
|
||||
err = sonic.Unmarshal(resp.Body(), &ret)
|
||||
err = json.Unmarshal(resp.Body(), &ret)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/codec/json"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog/log"
|
||||
apiV1 "github.com/yusing/godoxy/internal/api/v1"
|
||||
@@ -48,8 +46,6 @@ func NewHandler(requireAuth bool) *gin.Engine {
|
||||
r.Use(ErrorLoggingMiddleware())
|
||||
r.Use(NoCache())
|
||||
|
||||
log.Debug().Msg("gin codec json.API: " + reflect.TypeOf(json.API).Name())
|
||||
|
||||
r.GET("/api/v1/version", apiV1.Version)
|
||||
|
||||
if auth.IsEnabled() && requireAuth {
|
||||
@@ -86,6 +82,8 @@ func NewHandler(requireAuth bool) *gin.Engine {
|
||||
route.GET("/providers", routeApi.Providers)
|
||||
route.GET("/by_provider", routeApi.ByProvider)
|
||||
route.POST("/playground", routeApi.Playground)
|
||||
route.GET("/validate", routeApi.Validate) // websocket
|
||||
route.POST("/validate", routeApi.Validate)
|
||||
}
|
||||
|
||||
file := v1.Group("/file")
|
||||
@@ -146,6 +144,8 @@ func NewHandler(requireAuth bool) *gin.Engine {
|
||||
|
||||
proxmox := v1.Group("/proxmox")
|
||||
{
|
||||
proxmox.GET("/tail", proxmoxApi.Tail)
|
||||
proxmox.GET("/journalctl", proxmoxApi.Journalctl)
|
||||
proxmox.GET("/journalctl/:node", proxmoxApi.Journalctl)
|
||||
proxmox.GET("/journalctl/:node/:vmid", proxmoxApi.Journalctl)
|
||||
proxmox.GET("/journalctl/:node/:vmid/:service", proxmoxApi.Journalctl)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
@@ -43,22 +42,22 @@ func GetContainer(c *gin.Context) {
|
||||
|
||||
defer dockerClient.Close()
|
||||
|
||||
cont, err := dockerClient.ContainerInspect(c.Request.Context(), id, client.ContainerInspectOptions{})
|
||||
cont, err := dockerClient.ContainerInspect(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to inspect container"))
|
||||
return
|
||||
}
|
||||
|
||||
var state ContainerState
|
||||
if cont.Container.State != nil {
|
||||
state = cont.Container.State.Status
|
||||
if cont.State != nil {
|
||||
state = cont.State.Status
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &Container{
|
||||
Server: dockerCfg.URL,
|
||||
Name: cont.Container.Name,
|
||||
ID: cont.Container.ID,
|
||||
Image: cont.Container.Image,
|
||||
Name: cont.Name,
|
||||
ID: cont.ID,
|
||||
Image: cont.Image,
|
||||
State: state,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
|
||||
_ "github.com/yusing/goutils/apitypes"
|
||||
@@ -40,12 +39,12 @@ func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Containe
|
||||
errs := gperr.NewBuilder("failed to get containers")
|
||||
containers := make([]Container, 0)
|
||||
for server, dockerClient := range dockerClients {
|
||||
conts, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{All: true})
|
||||
conts, err := dockerClient.ContainerList(ctx, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
errs.Add(err)
|
||||
continue
|
||||
}
|
||||
for _, cont := range conts.Items {
|
||||
for _, cont := range conts {
|
||||
containers = append(containers, Container{
|
||||
Server: server,
|
||||
Name: cont.Names[0],
|
||||
|
||||
@@ -4,9 +4,8 @@ import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
dockerSystem "github.com/docker/docker/api/types/system"
|
||||
"github.com/gin-gonic/gin"
|
||||
dockerSystem "github.com/moby/moby/api/types/system"
|
||||
"github.com/moby/moby/client"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
|
||||
@@ -65,13 +64,13 @@ func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerIn
|
||||
|
||||
i := 0
|
||||
for name, dockerClient := range dockerClients {
|
||||
info, err := dockerClient.Info(ctx, client.InfoOptions{})
|
||||
info, err := dockerClient.Info(ctx)
|
||||
if err != nil {
|
||||
errs.Add(err)
|
||||
continue
|
||||
}
|
||||
info.Info.Name = name
|
||||
dockerInfos[i] = toDockerInfo(info.Info)
|
||||
info.Name = name
|
||||
dockerInfos[i] = toDockerInfo(info)
|
||||
i++
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/api/pkg/stdcopy"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
@@ -23,7 +23,7 @@ type LogsQueryParams struct {
|
||||
Since string `form:"from"`
|
||||
Until string `form:"to"`
|
||||
Levels string `form:"levels"`
|
||||
Limit int `form:"limit,default=100" binding:"omitempty,min=1,max=1000"`
|
||||
Limit int `form:"limit,default=100" binding:"min=1,max=1000"`
|
||||
} // @name LogsQueryParams
|
||||
|
||||
// @x-id "logs"
|
||||
@@ -73,7 +73,7 @@ func Logs(c *gin.Context) {
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
opts := client.ContainerLogsOptions{
|
||||
opts := container.LogsOptions{
|
||||
ShowStdout: queryParams.Stdout,
|
||||
ShowStderr: queryParams.Stderr,
|
||||
Since: queryParams.Since,
|
||||
|
||||
@@ -4,23 +4,17 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type RestartRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
client.ContainerRestartOptions
|
||||
}
|
||||
|
||||
// @x-id "restart"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Restart container
|
||||
// @Description Restart container by container id
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param request body RestartRequest true "Request"
|
||||
// @Param request body StopRequest true "Request"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
@@ -28,7 +22,7 @@ type RestartRequest struct {
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/restart [post]
|
||||
func Restart(c *gin.Context) {
|
||||
var req RestartRequest
|
||||
var req StopRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
@@ -48,7 +42,7 @@ func Restart(c *gin.Context) {
|
||||
|
||||
defer client.Close()
|
||||
|
||||
_, err = client.ContainerRestart(c.Request.Context(), req.ID, req.ContainerRestartOptions)
|
||||
err = client.ContainerRestart(c.Request.Context(), req.ID, req.StopOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to restart container"))
|
||||
return
|
||||
|
||||
@@ -3,15 +3,15 @@ package dockerapi
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type StartRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
client.ContainerStartOptions
|
||||
container.StartOptions
|
||||
}
|
||||
|
||||
// @x-id "start"
|
||||
@@ -48,7 +48,7 @@ func Start(c *gin.Context) {
|
||||
|
||||
defer client.Close()
|
||||
|
||||
_, err = client.ContainerStart(c.Request.Context(), req.ID, req.ContainerStartOptions)
|
||||
err = client.ContainerStart(c.Request.Context(), req.ID, req.StartOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to start container"))
|
||||
return
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
@@ -68,7 +67,7 @@ func Stats(c *gin.Context) {
|
||||
defer dockerClient.Close()
|
||||
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
stats, err := dockerClient.ContainerStats(c.Request.Context(), id, client.ContainerStatsOptions{Stream: true})
|
||||
stats, err := dockerClient.ContainerStats(c.Request.Context(), id, true)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get container stats"))
|
||||
return
|
||||
@@ -102,7 +101,7 @@ func Stats(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
stats, err := dockerClient.ContainerStats(c.Request.Context(), id, client.ContainerStatsOptions{Stream: false})
|
||||
stats, err := dockerClient.ContainerStats(c.Request.Context(), id, false)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get container stats"))
|
||||
return
|
||||
|
||||
@@ -3,15 +3,15 @@ package dockerapi
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type StopRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
client.ContainerStopOptions
|
||||
container.StopOptions
|
||||
}
|
||||
|
||||
// @x-id "stop"
|
||||
@@ -48,7 +48,7 @@ func Stop(c *gin.Context) {
|
||||
|
||||
defer client.Close()
|
||||
|
||||
_, err = client.ContainerStop(c.Request.Context(), req.ID, req.ContainerStopOptions)
|
||||
err = client.ContainerStop(c.Request.Context(), req.ID, req.StopOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to stop container"))
|
||||
return
|
||||
|
||||
@@ -1087,7 +1087,7 @@
|
||||
"post": {
|
||||
"description": "Validate file",
|
||||
"consumes": [
|
||||
"text/plain"
|
||||
"application/yaml"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -2077,6 +2077,90 @@
|
||||
"operationId": "uptime"
|
||||
}
|
||||
},
|
||||
"/proxmox/journalctl": {
|
||||
"get": {
|
||||
"description": "Get journalctl output for node or LXC container. If vmid is not provided, streams node journalctl.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"proxmox",
|
||||
"websocket"
|
||||
],
|
||||
"summary": "Get journalctl output",
|
||||
"parameters": [
|
||||
{
|
||||
"maximum": 1000,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"description": "Limit output lines (1-1000)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Node name",
|
||||
"name": "node",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"description": "Service names",
|
||||
"name": "service",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Container VMID (optional - if not provided, streams node journalctl)",
|
||||
"name": "vmid",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Journalctl output",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Node not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-id": "journalctl",
|
||||
"operationId": "journalctl"
|
||||
}
|
||||
},
|
||||
"/proxmox/journalctl/{node}": {
|
||||
"get": {
|
||||
"description": "Get journalctl output for node or LXC container. If vmid is not provided, streams node journalctl.",
|
||||
@@ -2092,18 +2176,44 @@
|
||||
],
|
||||
"summary": "Get journalctl output",
|
||||
"parameters": [
|
||||
{
|
||||
"maximum": 1000,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"description": "Limit output lines (1-1000)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Node name",
|
||||
"name": "node",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"description": "Service names",
|
||||
"name": "service",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Container VMID (optional - if not provided, streams node journalctl)",
|
||||
"name": "vmid",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Node name",
|
||||
"name": "node",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Limit output lines (1-1000)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -2157,6 +2267,38 @@
|
||||
],
|
||||
"summary": "Get journalctl output",
|
||||
"parameters": [
|
||||
{
|
||||
"maximum": 1000,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"description": "Limit output lines (1-1000)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Node name",
|
||||
"name": "node",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"description": "Service names",
|
||||
"name": "service",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Container VMID (optional - if not provided, streams node journalctl)",
|
||||
"name": "vmid",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Node name",
|
||||
@@ -2169,12 +2311,6 @@
|
||||
"description": "Container VMID (optional - if not provided, streams node journalctl)",
|
||||
"name": "vmid",
|
||||
"in": "path"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Limit output lines (1-1000)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -2228,6 +2364,38 @@
|
||||
],
|
||||
"summary": "Get journalctl output",
|
||||
"parameters": [
|
||||
{
|
||||
"maximum": 1000,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"description": "Limit output lines (1-1000)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Node name",
|
||||
"name": "node",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"description": "Service names",
|
||||
"name": "service",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Container VMID (optional - if not provided, streams node journalctl)",
|
||||
"name": "vmid",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Node name",
|
||||
@@ -2236,22 +2404,20 @@
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Container VMID (optional - if not provided, streams node journalctl)",
|
||||
"name": "vmid",
|
||||
"in": "path"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"description": "Service names",
|
||||
"name": "service",
|
||||
"in": "path"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Limit output lines (1-1000)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
"description": "Container VMID (optional - if not provided, streams node journalctl)",
|
||||
"name": "vmid",
|
||||
"in": "path"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -2569,6 +2735,91 @@
|
||||
"operationId": "vmStats"
|
||||
}
|
||||
},
|
||||
"/proxmox/tail": {
|
||||
"get": {
|
||||
"description": "Get tail output for node or LXC container. If vmid is not provided, streams node tail.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"proxmox",
|
||||
"websocket"
|
||||
],
|
||||
"summary": "Get tail output",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"description": "File paths",
|
||||
"name": "file",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"maximum": 1000,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"description": "Limit output lines (1-1000)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Node name",
|
||||
"name": "node",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Container VMID (optional - if not provided, streams node journalctl)",
|
||||
"name": "vmid",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Tail output",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Node not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-id": "tail",
|
||||
"operationId": "tail"
|
||||
}
|
||||
},
|
||||
"/reload": {
|
||||
"post": {
|
||||
"description": "Reload config",
|
||||
@@ -2775,6 +3026,122 @@
|
||||
"operationId": "providers"
|
||||
}
|
||||
},
|
||||
"/route/validate": {
|
||||
"get": {
|
||||
"description": "Validate route,",
|
||||
"consumes": [
|
||||
"application/yaml"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"route",
|
||||
"websocket"
|
||||
],
|
||||
"summary": "Validate route",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Route",
|
||||
"name": "route",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Route"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Route validated",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/SuccessResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
},
|
||||
"417": {
|
||||
"description": "Validation failed",
|
||||
"schema": {}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-id": "validate",
|
||||
"operationId": "validate"
|
||||
},
|
||||
"post": {
|
||||
"description": "Validate route,",
|
||||
"consumes": [
|
||||
"application/yaml"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"route",
|
||||
"websocket"
|
||||
],
|
||||
"summary": "Validate route",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Route",
|
||||
"name": "route",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Route"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Route validated",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/SuccessResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
},
|
||||
"417": {
|
||||
"description": "Validation failed",
|
||||
"schema": {}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-id": "validate",
|
||||
"operationId": "validate"
|
||||
}
|
||||
},
|
||||
"/route/{which}": {
|
||||
"get": {
|
||||
"description": "List route",
|
||||
@@ -3389,33 +3756,6 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"DockerConfig": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"container_id",
|
||||
"container_name",
|
||||
"docker_cfg"
|
||||
],
|
||||
"properties": {
|
||||
"container_id": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"container_name": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"docker_cfg": {
|
||||
"$ref": "#/definitions/DockerProviderConfig",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
}
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"DockerProviderConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -4164,7 +4504,7 @@
|
||||
"x-omitempty": false
|
||||
},
|
||||
"docker": {
|
||||
"$ref": "#/definitions/DockerConfig",
|
||||
"$ref": "#/definitions/IdlewatcherDockerConfig",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
@@ -4184,7 +4524,7 @@
|
||||
"x-omitempty": false
|
||||
},
|
||||
"proxmox": {
|
||||
"$ref": "#/definitions/ProxmoxNodeConfig",
|
||||
"$ref": "#/definitions/IdlewatcherProxmoxNodeConfig",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
@@ -4218,6 +4558,54 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"IdlewatcherDockerConfig": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"container_id",
|
||||
"container_name",
|
||||
"docker_cfg"
|
||||
],
|
||||
"properties": {
|
||||
"container_id": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"container_name": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"docker_cfg": {
|
||||
"$ref": "#/definitions/DockerProviderConfig",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
}
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"IdlewatcherProxmoxNodeConfig": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node",
|
||||
"vmid"
|
||||
],
|
||||
"properties": {
|
||||
"node": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"vmid": {
|
||||
"type": "integer",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
}
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"ListFilesResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -4805,26 +5193,38 @@
|
||||
},
|
||||
"ProxmoxNodeConfig": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node",
|
||||
"vmid"
|
||||
],
|
||||
"properties": {
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"node": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"service": {
|
||||
"type": "string"
|
||||
"services": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"vmid": {
|
||||
"description": "unset: auto discover; explicit 0: node-level route; >0: lxc/qemu resource route",
|
||||
"type": "integer",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"vmname": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
}
|
||||
},
|
||||
"x-nullable": false,
|
||||
@@ -6461,229 +6861,6 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"route.Route": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_log": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RequestLoggerConfig"
|
||||
}
|
||||
],
|
||||
"x-nullable": true
|
||||
},
|
||||
"agent": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"alias": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"bind": {
|
||||
"description": "for TCP and UDP routes, bind address to listen on",
|
||||
"type": "string",
|
||||
"x-nullable": true
|
||||
},
|
||||
"container": {
|
||||
"description": "Docker only",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Container"
|
||||
}
|
||||
],
|
||||
"x-nullable": true
|
||||
},
|
||||
"disable_compression": {
|
||||
"type": "boolean",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"excluded": {
|
||||
"type": "boolean",
|
||||
"x-nullable": true
|
||||
},
|
||||
"excluded_reason": {
|
||||
"type": "string",
|
||||
"x-nullable": true
|
||||
},
|
||||
"health": {
|
||||
"description": "for swagger",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/HealthJSON"
|
||||
}
|
||||
],
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"healthcheck": {
|
||||
"description": "null on load-balancer routes",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/HealthCheckConfig"
|
||||
}
|
||||
],
|
||||
"x-nullable": true
|
||||
},
|
||||
"homepage": {
|
||||
"$ref": "#/definitions/HomepageItemConfig",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"idlewatcher": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/IdlewatcherConfig"
|
||||
}
|
||||
],
|
||||
"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": [
|
||||
{
|
||||
"$ref": "#/definitions/LoadBalancerConfig"
|
||||
}
|
||||
],
|
||||
"x-nullable": true
|
||||
},
|
||||
"lurl": {
|
||||
"description": "private fields",
|
||||
"type": "string",
|
||||
"x-nullable": true
|
||||
},
|
||||
"middlewares": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/types.LabelMap"
|
||||
},
|
||||
"x-nullable": true
|
||||
},
|
||||
"no_tls_verify": {
|
||||
"type": "boolean",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"path_patterns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-nullable": true
|
||||
},
|
||||
"port": {
|
||||
"$ref": "#/definitions/Port",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"provider": {
|
||||
"description": "for backward compatibility",
|
||||
"type": "string",
|
||||
"x-nullable": true
|
||||
},
|
||||
"proxmox": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ProxmoxNodeConfig"
|
||||
}
|
||||
],
|
||||
"x-nullable": true
|
||||
},
|
||||
"purl": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"response_header_timeout": {
|
||||
"type": "integer",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"root": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"rule_file": {
|
||||
"type": "string",
|
||||
"x-nullable": true
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/rules.Rule"
|
||||
},
|
||||
"x-nullable": true
|
||||
},
|
||||
"scheme": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"http",
|
||||
"https",
|
||||
"h2c",
|
||||
"tcp",
|
||||
"udp",
|
||||
"fileserver"
|
||||
],
|
||||
"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",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"ssl_certificate_key": {
|
||||
"description": "Path to client certificate key",
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"ssl_protocols": {
|
||||
"description": "Allowed TLS protocols",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"ssl_server_name": {
|
||||
"description": "SSL/TLS proxy options (nginx-like)",
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"ssl_trusted_certificate": {
|
||||
"description": "Path to trusted CA certificates",
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
}
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"routeApi.RawRule": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -6711,7 +6888,7 @@
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/route.Route"
|
||||
"$ref": "#/definitions/Route"
|
||||
}
|
||||
},
|
||||
"x-nullable": false,
|
||||
|
||||
@@ -269,19 +269,6 @@ definitions:
|
||||
- ContainerStopMethodPause
|
||||
- ContainerStopMethodStop
|
||||
- ContainerStopMethodKill
|
||||
DockerConfig:
|
||||
properties:
|
||||
container_id:
|
||||
type: string
|
||||
container_name:
|
||||
type: string
|
||||
docker_cfg:
|
||||
$ref: '#/definitions/DockerProviderConfig'
|
||||
required:
|
||||
- container_id
|
||||
- container_name
|
||||
- docker_cfg
|
||||
type: object
|
||||
DockerProviderConfig:
|
||||
properties:
|
||||
tls:
|
||||
@@ -630,7 +617,7 @@ definitions:
|
||||
type: string
|
||||
type: array
|
||||
docker:
|
||||
$ref: '#/definitions/DockerConfig'
|
||||
$ref: '#/definitions/IdlewatcherDockerConfig'
|
||||
idle_timeout:
|
||||
allOf:
|
||||
- $ref: '#/definitions/time.Duration'
|
||||
@@ -641,7 +628,7 @@ definitions:
|
||||
no_loading_page:
|
||||
type: boolean
|
||||
proxmox:
|
||||
$ref: '#/definitions/ProxmoxNodeConfig'
|
||||
$ref: '#/definitions/IdlewatcherProxmoxNodeConfig'
|
||||
start_endpoint:
|
||||
description: Optional path that must be hit to start container
|
||||
type: string
|
||||
@@ -654,6 +641,29 @@ definitions:
|
||||
wake_timeout:
|
||||
$ref: '#/definitions/time.Duration'
|
||||
type: object
|
||||
IdlewatcherDockerConfig:
|
||||
properties:
|
||||
container_id:
|
||||
type: string
|
||||
container_name:
|
||||
type: string
|
||||
docker_cfg:
|
||||
$ref: '#/definitions/DockerProviderConfig'
|
||||
required:
|
||||
- container_id
|
||||
- container_name
|
||||
- docker_cfg
|
||||
type: object
|
||||
IdlewatcherProxmoxNodeConfig:
|
||||
properties:
|
||||
node:
|
||||
type: string
|
||||
vmid:
|
||||
type: integer
|
||||
required:
|
||||
- node
|
||||
- vmid
|
||||
type: object
|
||||
ListFilesResponse:
|
||||
properties:
|
||||
config:
|
||||
@@ -935,17 +945,22 @@ definitions:
|
||||
- ProviderTypeAgent
|
||||
ProxmoxNodeConfig:
|
||||
properties:
|
||||
files:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
node:
|
||||
type: string
|
||||
service:
|
||||
type: string
|
||||
services:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
vmid:
|
||||
description: 'unset: auto discover; explicit 0: node-level route; >0: lxc/qemu
|
||||
resource route'
|
||||
type: integer
|
||||
vmname:
|
||||
type: string
|
||||
required:
|
||||
- node
|
||||
- vmid
|
||||
type: object
|
||||
ProxyStats:
|
||||
properties:
|
||||
@@ -1815,127 +1830,6 @@ definitions:
|
||||
uptime:
|
||||
type: string
|
||||
type: object
|
||||
route.Route:
|
||||
properties:
|
||||
access_log:
|
||||
allOf:
|
||||
- $ref: '#/definitions/RequestLoggerConfig'
|
||||
x-nullable: true
|
||||
agent:
|
||||
type: string
|
||||
alias:
|
||||
type: string
|
||||
bind:
|
||||
description: for TCP and UDP routes, bind address to listen on
|
||||
type: string
|
||||
x-nullable: true
|
||||
container:
|
||||
allOf:
|
||||
- $ref: '#/definitions/Container'
|
||||
description: Docker only
|
||||
x-nullable: true
|
||||
disable_compression:
|
||||
type: boolean
|
||||
excluded:
|
||||
type: boolean
|
||||
x-nullable: true
|
||||
excluded_reason:
|
||||
type: string
|
||||
x-nullable: true
|
||||
health:
|
||||
allOf:
|
||||
- $ref: '#/definitions/HealthJSON'
|
||||
description: for swagger
|
||||
healthcheck:
|
||||
allOf:
|
||||
- $ref: '#/definitions/HealthCheckConfig'
|
||||
description: null on load-balancer routes
|
||||
x-nullable: true
|
||||
homepage:
|
||||
$ref: '#/definitions/HomepageItemConfig'
|
||||
host:
|
||||
type: string
|
||||
idlewatcher:
|
||||
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'
|
||||
x-nullable: true
|
||||
lurl:
|
||||
description: private fields
|
||||
type: string
|
||||
x-nullable: true
|
||||
middlewares:
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/types.LabelMap'
|
||||
type: object
|
||||
x-nullable: true
|
||||
no_tls_verify:
|
||||
type: boolean
|
||||
path_patterns:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-nullable: true
|
||||
port:
|
||||
$ref: '#/definitions/Port'
|
||||
provider:
|
||||
description: for backward compatibility
|
||||
type: string
|
||||
x-nullable: true
|
||||
proxmox:
|
||||
allOf:
|
||||
- $ref: '#/definitions/ProxmoxNodeConfig'
|
||||
x-nullable: true
|
||||
purl:
|
||||
type: string
|
||||
response_header_timeout:
|
||||
type: integer
|
||||
root:
|
||||
type: string
|
||||
rule_file:
|
||||
type: string
|
||||
x-nullable: true
|
||||
rules:
|
||||
items:
|
||||
$ref: '#/definitions/rules.Rule'
|
||||
type: array
|
||||
x-nullable: true
|
||||
scheme:
|
||||
enum:
|
||||
- http
|
||||
- https
|
||||
- h2c
|
||||
- tcp
|
||||
- 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
|
||||
ssl_certificate_key:
|
||||
description: Path to client certificate key
|
||||
type: string
|
||||
ssl_protocols:
|
||||
description: Allowed TLS protocols
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
ssl_server_name:
|
||||
description: SSL/TLS proxy options (nginx-like)
|
||||
type: string
|
||||
ssl_trusted_certificate:
|
||||
description: Path to trusted CA certificates
|
||||
type: string
|
||||
type: object
|
||||
routeApi.RawRule:
|
||||
properties:
|
||||
do:
|
||||
@@ -1948,7 +1842,7 @@ definitions:
|
||||
routeApi.RoutesByProvider:
|
||||
additionalProperties:
|
||||
items:
|
||||
$ref: '#/definitions/route.Route'
|
||||
$ref: '#/definitions/Route'
|
||||
type: array
|
||||
type: object
|
||||
rules.Rule:
|
||||
@@ -2726,7 +2620,7 @@ paths:
|
||||
/file/validate:
|
||||
post:
|
||||
consumes:
|
||||
- text/plain
|
||||
- application/yaml
|
||||
description: Validate file
|
||||
parameters:
|
||||
- description: Type
|
||||
@@ -3392,6 +3286,64 @@ paths:
|
||||
- metrics
|
||||
- websocket
|
||||
x-id: uptime
|
||||
/proxmox/journalctl:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get journalctl output for node or LXC container. If vmid is not
|
||||
provided, streams node journalctl.
|
||||
parameters:
|
||||
- default: 100
|
||||
description: Limit output lines (1-1000)
|
||||
in: query
|
||||
maximum: 1000
|
||||
minimum: 1
|
||||
name: limit
|
||||
type: integer
|
||||
- description: Node name
|
||||
in: query
|
||||
name: node
|
||||
required: true
|
||||
type: string
|
||||
- collectionFormat: csv
|
||||
description: Service names
|
||||
in: query
|
||||
items:
|
||||
type: string
|
||||
name: service
|
||||
type: array
|
||||
- description: Container VMID (optional - if not provided, streams node journalctl)
|
||||
in: query
|
||||
name: vmid
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Journalctl output
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: Invalid request
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"404":
|
||||
description: Node not found
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
summary: Get journalctl output
|
||||
tags:
|
||||
- proxmox
|
||||
- websocket
|
||||
x-id: journalctl
|
||||
/proxmox/journalctl/{node}:
|
||||
get:
|
||||
consumes:
|
||||
@@ -3399,15 +3351,34 @@ paths:
|
||||
description: Get journalctl output for node or LXC container. If vmid is not
|
||||
provided, streams node journalctl.
|
||||
parameters:
|
||||
- default: 100
|
||||
description: Limit output lines (1-1000)
|
||||
in: query
|
||||
maximum: 1000
|
||||
minimum: 1
|
||||
name: limit
|
||||
type: integer
|
||||
- description: Node name
|
||||
in: query
|
||||
name: node
|
||||
required: true
|
||||
type: string
|
||||
- collectionFormat: csv
|
||||
description: Service names
|
||||
in: query
|
||||
items:
|
||||
type: string
|
||||
name: service
|
||||
type: array
|
||||
- description: Container VMID (optional - if not provided, streams node journalctl)
|
||||
in: query
|
||||
name: vmid
|
||||
type: integer
|
||||
- description: Node name
|
||||
in: path
|
||||
name: node
|
||||
required: true
|
||||
type: string
|
||||
- description: Limit output lines (1-1000)
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -3443,6 +3414,29 @@ paths:
|
||||
description: Get journalctl output for node or LXC container. If vmid is not
|
||||
provided, streams node journalctl.
|
||||
parameters:
|
||||
- default: 100
|
||||
description: Limit output lines (1-1000)
|
||||
in: query
|
||||
maximum: 1000
|
||||
minimum: 1
|
||||
name: limit
|
||||
type: integer
|
||||
- description: Node name
|
||||
in: query
|
||||
name: node
|
||||
required: true
|
||||
type: string
|
||||
- collectionFormat: csv
|
||||
description: Service names
|
||||
in: query
|
||||
items:
|
||||
type: string
|
||||
name: service
|
||||
type: array
|
||||
- description: Container VMID (optional - if not provided, streams node journalctl)
|
||||
in: query
|
||||
name: vmid
|
||||
type: integer
|
||||
- description: Node name
|
||||
in: path
|
||||
name: node
|
||||
@@ -3452,10 +3446,6 @@ paths:
|
||||
in: path
|
||||
name: vmid
|
||||
type: integer
|
||||
- description: Limit output lines (1-1000)
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -3491,24 +3481,45 @@ paths:
|
||||
description: Get journalctl output for node or LXC container. If vmid is not
|
||||
provided, streams node journalctl.
|
||||
parameters:
|
||||
- default: 100
|
||||
description: Limit output lines (1-1000)
|
||||
in: query
|
||||
maximum: 1000
|
||||
minimum: 1
|
||||
name: limit
|
||||
type: integer
|
||||
- description: Node name
|
||||
in: query
|
||||
name: node
|
||||
required: true
|
||||
type: string
|
||||
- collectionFormat: csv
|
||||
description: Service names
|
||||
in: query
|
||||
items:
|
||||
type: string
|
||||
name: service
|
||||
type: array
|
||||
- description: Container VMID (optional - if not provided, streams node journalctl)
|
||||
in: query
|
||||
name: vmid
|
||||
type: integer
|
||||
- description: Node name
|
||||
in: path
|
||||
name: node
|
||||
required: true
|
||||
type: string
|
||||
- collectionFormat: csv
|
||||
description: Service names
|
||||
in: path
|
||||
items:
|
||||
type: string
|
||||
name: service
|
||||
type: array
|
||||
- description: Container VMID (optional - if not provided, streams node journalctl)
|
||||
in: path
|
||||
name: vmid
|
||||
type: integer
|
||||
- description: Service name (e.g., 'pveproxy' for node, 'container@.service'
|
||||
format for LXC)
|
||||
in: path
|
||||
name: service
|
||||
type: string
|
||||
- description: Limit output lines (1-1000)
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -3720,6 +3731,65 @@ paths:
|
||||
- proxmox
|
||||
- websocket
|
||||
x-id: vmStats
|
||||
/proxmox/tail:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get tail output for node or LXC container. If vmid is not provided,
|
||||
streams node tail.
|
||||
parameters:
|
||||
- collectionFormat: csv
|
||||
description: File paths
|
||||
in: query
|
||||
items:
|
||||
type: string
|
||||
name: file
|
||||
required: true
|
||||
type: array
|
||||
- default: 100
|
||||
description: Limit output lines (1-1000)
|
||||
in: query
|
||||
maximum: 1000
|
||||
minimum: 1
|
||||
name: limit
|
||||
type: integer
|
||||
- description: Node name
|
||||
in: query
|
||||
name: node
|
||||
required: true
|
||||
type: string
|
||||
- description: Container VMID (optional - if not provided, streams node journalctl)
|
||||
in: query
|
||||
name: vmid
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Tail output
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: Invalid request
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"403":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"404":
|
||||
description: Node not found
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
summary: Get tail output
|
||||
tags:
|
||||
- proxmox
|
||||
- websocket
|
||||
x-id: tail
|
||||
/reload:
|
||||
post:
|
||||
consumes:
|
||||
@@ -3888,6 +3958,83 @@ paths:
|
||||
- route
|
||||
- websocket
|
||||
x-id: providers
|
||||
/route/validate:
|
||||
get:
|
||||
consumes:
|
||||
- application/yaml
|
||||
description: Validate route,
|
||||
parameters:
|
||||
- description: Route
|
||||
in: body
|
||||
name: route
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/Route'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Route validated
|
||||
schema:
|
||||
$ref: '#/definitions/SuccessResponse'
|
||||
"400":
|
||||
description: Bad request
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"403":
|
||||
description: Forbidden
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"417":
|
||||
description: Validation failed
|
||||
schema: {}
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
summary: Validate route
|
||||
tags:
|
||||
- route
|
||||
- websocket
|
||||
x-id: validate
|
||||
post:
|
||||
consumes:
|
||||
- application/yaml
|
||||
description: Validate route,
|
||||
parameters:
|
||||
- description: Route
|
||||
in: body
|
||||
name: route
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/Route'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Route validated
|
||||
schema:
|
||||
$ref: '#/definitions/SuccessResponse'
|
||||
"400":
|
||||
description: Bad request
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"403":
|
||||
description: Forbidden
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"417":
|
||||
description: Validation failed
|
||||
schema: {}
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
summary: Validate route
|
||||
tags:
|
||||
- route
|
||||
- websocket
|
||||
x-id: validate
|
||||
/stats:
|
||||
get:
|
||||
consumes:
|
||||
|
||||
@@ -20,7 +20,7 @@ type ValidateFileRequest struct {
|
||||
// @Summary Validate file
|
||||
// @Description Validate file
|
||||
// @Tags file
|
||||
// @Accept text/plain
|
||||
// @Accept application/yaml
|
||||
// @Produce json
|
||||
// @Param type query FileType true "Type"
|
||||
// @Param file body string true "File content"
|
||||
@@ -29,7 +29,7 @@ type ValidateFileRequest struct {
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden"
|
||||
// @Failure 417 {object} any "Validation failed"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||
// @Router /file/validate [post]
|
||||
// @Router /file/validate [post]
|
||||
func Validate(c *gin.Context) {
|
||||
var request ValidateFileRequest
|
||||
if err := c.ShouldBindQuery(&request); err != nil {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
@@ -237,7 +236,7 @@ func marshalSystemInfo(ws *websocket.Manager, agentName string, systemInfo any)
|
||||
defer bufFromPool.release(bufFromPool.RawMessage)
|
||||
}
|
||||
|
||||
err := sonic.ConfigDefault.NewEncoder(buf).Encode(map[string]any{
|
||||
err := json.NewEncoder(buf).Encode(map[string]any{
|
||||
agentName: systemInfo,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -3,4 +3,4 @@ package proxmoxapi
|
||||
type ActionRequest struct {
|
||||
Node string `uri:"node" binding:"required"`
|
||||
VMID int `uri:"vmid" binding:"required"`
|
||||
}
|
||||
} // @name ProxmoxVMActionRequest
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package proxmoxapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
@@ -10,36 +11,40 @@ import (
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
)
|
||||
|
||||
// e.g. ws://localhost:8889/api/v1/proxmox/journalctl?node=pve&vmid=127&service=pveproxy&service=pvedaemon&limit=10
|
||||
// e.g. ws://localhost:8889/api/v1/proxmox/journalctl/pve/127?service=pveproxy&service=pvedaemon&limit=10
|
||||
|
||||
type JournalctlRequest struct {
|
||||
Node string `uri:"node" binding:"required"`
|
||||
VMID *int `uri:"vmid"` // optional - if not provided, streams node journalctl
|
||||
Service string `uri:"service"`
|
||||
Limit int `query:"limit" binding:"omitempty,min=1,max=1000"`
|
||||
}
|
||||
Node string `form:"node" uri:"node" binding:"required"` // Node name
|
||||
VMID *int `form:"vmid" uri:"vmid"` // Container VMID (optional - if not provided, streams node journalctl)
|
||||
Services []string `form:"service" uri:"service"` // Service names
|
||||
Limit *int `form:"limit" uri:"limit" default:"100" binding:"min=1,max=1000"` // Limit output lines (1-1000)
|
||||
} // @name ProxmoxJournalctlRequest
|
||||
|
||||
// @x-id "journalctl"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get journalctl output
|
||||
// @Description Get journalctl output for node or LXC container. If vmid is not provided, streams node journalctl.
|
||||
// @Tags proxmox,websocket
|
||||
// @Accept json
|
||||
// @Accept json
|
||||
// @Produce application/json
|
||||
// @Param node path string true "Node name"
|
||||
// @Param vmid path int false "Container VMID (optional - if not provided, streams node journalctl)"
|
||||
// @Param service path string false "Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC)"
|
||||
// @Param limit query int false "Limit output lines (1-1000)"
|
||||
// @Param query query JournalctlRequest true "Request"
|
||||
// @Param path path JournalctlRequest true "Request"
|
||||
// @Success 200 string plain "Journalctl output"
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||
// @Router /proxmox/journalctl [get]
|
||||
// @Router /proxmox/journalctl/{node} [get]
|
||||
// @Router /proxmox/journalctl/{node}/{vmid} [get]
|
||||
// @Router /proxmox/journalctl/{node}/{vmid}/{service} [get]
|
||||
func Journalctl(c *gin.Context) {
|
||||
var request JournalctlRequest
|
||||
if err := c.ShouldBindUri(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
uriErr := c.ShouldBindUri(&request)
|
||||
queryErr := c.ShouldBindQuery(&request)
|
||||
if uriErr != nil && queryErr != nil { // allow both uri and query parameters to be set
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", errors.Join(uriErr, queryErr)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -49,18 +54,14 @@ func Journalctl(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||
return
|
||||
}
|
||||
defer manager.Close()
|
||||
c.Status(http.StatusContinue)
|
||||
|
||||
var reader io.ReadCloser
|
||||
var err error
|
||||
if request.VMID == nil {
|
||||
reader, err = node.NodeJournalctl(c.Request.Context(), request.Service, request.Limit)
|
||||
reader, err = node.NodeJournalctl(c.Request.Context(), request.Services, *request.Limit)
|
||||
} else {
|
||||
reader, err = node.LXCJournalctl(c.Request.Context(), *request.VMID, request.Service, request.Limit)
|
||||
reader, err = node.LXCJournalctl(c.Request.Context(), *request.VMID, request.Services, *request.Limit)
|
||||
}
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get journalctl output"))
|
||||
@@ -68,6 +69,13 @@ func Journalctl(c *gin.Context) {
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||
return
|
||||
}
|
||||
defer manager.Close()
|
||||
|
||||
writer := manager.NewWriter(websocket.TextMessage)
|
||||
_, err = io.Copy(writer, reader)
|
||||
if err != nil {
|
||||
|
||||
77
internal/api/v1/proxmox/tail.go
Normal file
77
internal/api/v1/proxmox/tail.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package proxmoxapi
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/proxmox"
|
||||
"github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
)
|
||||
|
||||
// e.g. ws://localhost:8889/api/v1/proxmox/tail?node=pve&vmid=127&file=/var/log/immich/web.log&file=/var/log/immich/ml.log&limit=10
|
||||
|
||||
type TailRequest struct {
|
||||
Node string `form:"node" binding:"required"` // Node name
|
||||
VMID *int `form:"vmid"` // Container VMID (optional - if not provided, streams node journalctl)
|
||||
Files []string `form:"file" binding:"required,dive,filepath"` // File paths
|
||||
Limit int `form:"limit" default:"100" binding:"min=1,max=1000"` // Limit output lines (1-1000)
|
||||
} // @name ProxmoxTailRequest
|
||||
|
||||
// @x-id "tail"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get tail output
|
||||
// @Description Get tail output for node or LXC container. If vmid is not provided, streams node tail.
|
||||
// @Tags proxmox,websocket
|
||||
// @Accept json
|
||||
// @Produce application/json
|
||||
// @Param query query TailRequest true "Request"
|
||||
// @Success 200 string plain "Tail output"
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Node not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||
// @Router /proxmox/tail [get]
|
||||
func Tail(c *gin.Context) {
|
||||
var request TailRequest
|
||||
if err := c.ShouldBindQuery(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
node, ok := proxmox.Nodes.Get(request.Node)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusContinue)
|
||||
|
||||
var reader io.ReadCloser
|
||||
var err error
|
||||
if request.VMID == nil {
|
||||
reader, err = node.NodeTail(c.Request.Context(), request.Files, request.Limit)
|
||||
} else {
|
||||
reader, err = node.LXCTail(c.Request.Context(), *request.VMID, request.Files, request.Limit)
|
||||
}
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get journalctl output"))
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||
return
|
||||
}
|
||||
defer manager.Close()
|
||||
|
||||
writer := manager.NewWriter(websocket.TextMessage)
|
||||
_, err = io.Copy(writer, reader)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to copy journalctl output"))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -37,5 +37,5 @@ func Route(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, route)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusNotFound, nil)
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("route not found"))
|
||||
}
|
||||
|
||||
69
internal/api/v1/route/validate.go
Normal file
69
internal/api/v1/route/validate.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package routeApi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/yusing/godoxy/internal/route"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
)
|
||||
|
||||
type _ = route.Route
|
||||
|
||||
// @x-id "validate"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Validate route
|
||||
// @Description Validate route,
|
||||
// @Tags route,websocket
|
||||
// @Accept application/yaml
|
||||
// @Produce json
|
||||
// @Param route body route.Route true "Route"
|
||||
// @Success 200 {object} apitypes.SuccessResponse "Route validated"
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Bad request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden"
|
||||
// @Failure 417 {object} any "Validation failed"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||
// @Router /route/validate [get]
|
||||
// @Router /route/validate [post]
|
||||
func Validate(c *gin.Context) {
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
ValidateWS(c)
|
||||
return
|
||||
}
|
||||
var request route.Route
|
||||
if err := c.ShouldBindWith(&request, serialization.GinYAMLBinding{}); err != nil {
|
||||
c.JSON(http.StatusExpectationFailed, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, apitypes.Success("route validated"))
|
||||
}
|
||||
|
||||
func ValidateWS(c *gin.Context) {
|
||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||
return
|
||||
}
|
||||
defer manager.Close()
|
||||
|
||||
const writeTimeout = 5 * time.Second
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-manager.Done():
|
||||
return
|
||||
case msg := <-manager.ReadCh():
|
||||
var request route.Route
|
||||
if err := serialization.UnmarshalValidate(msg, &request, yaml.Unmarshal); err != nil {
|
||||
manager.WriteJSON(gin.H{"error": err}, writeTimeout)
|
||||
continue
|
||||
}
|
||||
manager.WriteJSON(gin.H{"message": "route validated"}, writeTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
@@ -109,7 +109,7 @@ type UserPassAuthCallbackRequest struct {
|
||||
|
||||
func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var creds UserPassAuthCallbackRequest
|
||||
err := sonic.ConfigDefault.NewDecoder(r.Body).Decode(&creds)
|
||||
err := json.NewDecoder(r.Body).Decode(&creds)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/internal/autocert"
|
||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||
@@ -25,9 +26,9 @@ func TestEABConfigRequired(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
yaml := fmt.Appendf(nil, "eab_kid: %s\neab_hmac: %s", test.cfg.EABKid, test.cfg.EABHmac)
|
||||
yamlCfg := fmt.Appendf(nil, "eab_kid: %s\neab_hmac: %s", test.cfg.EABKid, test.cfg.EABHmac)
|
||||
cfg := autocert.Config{}
|
||||
err := serialization.UnmarshalValidateYAML(yaml, &cfg)
|
||||
err := serialization.UnmarshalValidate(yamlCfg, &cfg, yaml.Unmarshal)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, test.wantErr)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/internal/autocert"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
@@ -41,7 +42,7 @@ func TestMultipleCertificatesLifecycle(t *testing.T) {
|
||||
cfg.HTTPClient = acmeServer.httpClient()
|
||||
|
||||
/* unmarshal yaml config with multiple certs */
|
||||
err := error(serialization.UnmarshalValidateYAML(yamlConfig, &cfg))
|
||||
err := error(serialization.UnmarshalValidate(yamlConfig, &cfg, yaml.Unmarshal))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"main.example.com"}, cfg.Domains)
|
||||
require.Len(t, cfg.Extra, 2)
|
||||
|
||||
@@ -3,6 +3,7 @@ package autocert_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/internal/autocert"
|
||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||
@@ -42,7 +43,7 @@ extra:
|
||||
`
|
||||
|
||||
var cfg autocert.Config
|
||||
err := error(serialization.UnmarshalValidateYAML([]byte(cfgYAML), &cfg))
|
||||
err := error(serialization.UnmarshalValidate([]byte(cfgYAML), &cfg, yaml.Unmarshal))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test: extra[0] inherits all fields from main except CertPath and KeyPath.
|
||||
|
||||
@@ -13,6 +13,8 @@ var (
|
||||
IsDebug = env.GetEnvBool("DEBUG", IsTest)
|
||||
IsTrace = env.GetEnvBool("TRACE", false) && IsDebug
|
||||
|
||||
InitTimeout = env.GetEnvDuation("INIT_TIMEOUT", 1*time.Minute)
|
||||
|
||||
ShortLinkPrefix = env.GetEnvString("SHORTLINK_PREFIX", "go")
|
||||
|
||||
ProxyHTTPAddr,
|
||||
|
||||
@@ -103,7 +103,7 @@ func (state *state) InitFromFile(filename string) error {
|
||||
}
|
||||
|
||||
func (state *state) Init(data []byte) error {
|
||||
err := serialization.UnmarshalValidateYAML(data, &state.Config)
|
||||
err := serialization.UnmarshalValidate(data, &state.Config, yaml.Unmarshal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -317,67 +317,50 @@ func (state *state) initProxmox() error {
|
||||
return errs.Wait().Error()
|
||||
}
|
||||
|
||||
func (state *state) storeProvider(p types.RouteProvider) {
|
||||
state.providers.Store(p.String(), p)
|
||||
}
|
||||
|
||||
func (state *state) loadRouteProviders() error {
|
||||
providers := &state.Providers
|
||||
providers := state.Providers
|
||||
errs := gperr.NewGroup("route provider errors")
|
||||
results := gperr.NewGroup("loaded route providers")
|
||||
|
||||
agentpool.RemoveAll()
|
||||
|
||||
numProviders := len(providers.Agents) + len(providers.Files) + len(providers.Docker)
|
||||
providersCh := make(chan types.RouteProvider, numProviders)
|
||||
|
||||
// start providers concurrently
|
||||
var providersConsumer sync.WaitGroup
|
||||
providersConsumer.Go(func() {
|
||||
for p := range providersCh {
|
||||
if actual, loaded := state.providers.LoadOrStore(p.String(), p); loaded {
|
||||
errs.Add(gperr.Errorf("provider %s already exists, first: %s, second: %s", p.String(), actual.GetType(), p.GetType()))
|
||||
continue
|
||||
}
|
||||
state.storeProvider(p)
|
||||
registerProvider := func(p types.RouteProvider) {
|
||||
if actual, loaded := state.providers.LoadOrStore(p.String(), p); loaded {
|
||||
errs.Addf("provider %s already exists, first: %s, second: %s", p.String(), actual.GetType(), p.GetType())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var providersProducer sync.WaitGroup
|
||||
agentErrs := gperr.NewGroup("agent init errors")
|
||||
for _, a := range providers.Agents {
|
||||
providersProducer.Go(func() {
|
||||
agentErrs.Go(func() error {
|
||||
if err := a.Init(state.task.Context()); err != nil {
|
||||
errs.Add(gperr.PrependSubject(a.String(), err))
|
||||
return
|
||||
return gperr.PrependSubject(a.String(), err)
|
||||
}
|
||||
agentpool.Add(a)
|
||||
p := route.NewAgentProvider(a)
|
||||
providersCh <- p
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := agentErrs.Wait().Error(); err != nil {
|
||||
errs.Add(err)
|
||||
}
|
||||
|
||||
for _, a := range providers.Agents {
|
||||
registerProvider(route.NewAgentProvider(a))
|
||||
}
|
||||
|
||||
for _, filename := range providers.Files {
|
||||
providersProducer.Go(func() {
|
||||
p, err := route.NewFileProvider(filename)
|
||||
if err != nil {
|
||||
errs.Add(gperr.PrependSubject(filename, err))
|
||||
} else {
|
||||
providersCh <- p
|
||||
}
|
||||
})
|
||||
p, err := route.NewFileProvider(filename)
|
||||
if err != nil {
|
||||
errs.Add(gperr.PrependSubject(filename, err))
|
||||
return err
|
||||
}
|
||||
registerProvider(p)
|
||||
}
|
||||
|
||||
for name, dockerCfg := range providers.Docker {
|
||||
providersProducer.Go(func() {
|
||||
providersCh <- route.NewDockerProvider(name, dockerCfg)
|
||||
})
|
||||
registerProvider(route.NewDockerProvider(name, dockerCfg))
|
||||
}
|
||||
|
||||
providersProducer.Wait()
|
||||
|
||||
close(providersCh)
|
||||
providersConsumer.Wait()
|
||||
|
||||
lenLongestName := 0
|
||||
for k := range state.providers.Range {
|
||||
if len(k) > lenLongestName {
|
||||
@@ -386,18 +369,26 @@ func (state *state) loadRouteProviders() error {
|
||||
}
|
||||
|
||||
// load routes concurrently
|
||||
var providersLoader sync.WaitGroup
|
||||
loadErrs := gperr.NewGroup("route load errors")
|
||||
|
||||
results := gperr.NewBuilder("loaded route providers")
|
||||
resultsMu := sync.Mutex{}
|
||||
for _, p := range state.providers.Range {
|
||||
providersLoader.Go(func() {
|
||||
loadErrs.Go(func() error {
|
||||
if err := p.LoadRoutes(); err != nil {
|
||||
errs.Add(err.Subject(p.String()))
|
||||
return err.Subject(p.String())
|
||||
}
|
||||
resultsMu.Lock()
|
||||
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes())
|
||||
resultsMu.Unlock()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
providersLoader.Wait()
|
||||
if err := loadErrs.Wait().Error(); err != nil {
|
||||
errs.Add(err)
|
||||
}
|
||||
|
||||
state.tmpLog.Info().Msg(results.Wait().String())
|
||||
state.tmpLog.Info().Msg(results.String())
|
||||
state.printRoutesByProvider(lenLongestName)
|
||||
state.printState()
|
||||
return errs.Wait().Error()
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"regexp"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/acl"
|
||||
"github.com/yusing/godoxy/internal/autocert"
|
||||
@@ -43,7 +44,7 @@ type (
|
||||
|
||||
func Validate(data []byte) gperr.Error {
|
||||
var model Config
|
||||
return serialization.UnmarshalValidateYAML(data, &model)
|
||||
return serialization.UnmarshalValidate(data, &model, yaml.Unmarshal)
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
|
||||
@@ -6,7 +6,7 @@ replace github.com/yusing/godoxy => ../..
|
||||
|
||||
require (
|
||||
github.com/go-acme/lego/v4 v4.31.0
|
||||
github.com/yusing/godoxy v0.25.0
|
||||
github.com/yusing/godoxy v0.25.2
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -23,12 +23,8 @@ require (
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
@@ -44,7 +40,7 @@ require (
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // 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
|
||||
@@ -53,7 +49,6 @@ require (
|
||||
github.com/gotify/server/v2 v2.8.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
@@ -65,8 +60,8 @@ require (
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
||||
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 // indirect
|
||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
@@ -78,7 +73,6 @@ require (
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/vultr/govultr/v3 v3.26.1 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/yusing/gointernals v0.1.16 // indirect
|
||||
@@ -89,7 +83,6 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/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.47.0 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
@@ -98,8 +91,8 @@ require (
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
google.golang.org/api v0.262.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect
|
||||
google.golang.org/api v0.263.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||
|
||||
@@ -37,18 +37,10 @@ github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -90,8 +82,8 @@ github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@@ -119,8 +111,6 @@ github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2
|
||||
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -150,10 +140,10 @@ github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0
|
||||
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
||||
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
||||
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1 h1:+fx2mbWeR8XX/vidwpRMepJMtRIYQP44Iezm2oeObVM=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.106.1/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1 h1:GDhBiaIAm/QXLzHJ0ASDdY/6R/9w60+gk8lY5rgfxEQ=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.106.1/go.mod h1:EHScJdbM0gg5Is7e3C0ceRYAFMMsfP4Vf8sBRoxoTgk=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 h1:eMzyN+jGJbxG4ut278uwIsUo9XacXc711lFjhKnaUso=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 h1:t34IpOa+8NfmjkU8bdWtYrLrmr346/FGhu8FlpJDQok=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0/go.mod h1:p95/OxVsdx71I2Qrck1GtIS87sRxcTRKXzUi5nWm9NY=
|
||||
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
||||
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
||||
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
|
||||
@@ -188,11 +178,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/vultr/govultr/v3 v3.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=
|
||||
@@ -221,8 +208,6 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
|
||||
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
@@ -249,14 +234,14 @@ golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
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.262.0 h1:4B+3u8He2GwyN8St3Jhnd3XRHlIvc//sBmgHSp78oNY=
|
||||
google.golang.org/api v0.262.0/go.mod h1:jNwmH8BgUBJ/VrUG6/lIl9YiildyLd09r9ZLHiQ6cGI=
|
||||
google.golang.org/api v0.263.0 h1:UFs7qn8gInIdtk1ZA6eXRXp5JDAnS4x9VRsRVCeKdbk=
|
||||
google.golang.org/api v0.263.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/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-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/agentpool"
|
||||
@@ -152,7 +152,7 @@ func NewClient(cfg types.DockerProviderConfig, unique ...bool) (*SharedClient, e
|
||||
if agent.IsDockerHostAgent(host) {
|
||||
a, ok := agentpool.Get(host)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("agent %q not found", host))
|
||||
return nil, fmt.Errorf("agent %q not found", host)
|
||||
}
|
||||
opt = []client.Opt{
|
||||
client.WithHost(agent.DockerHost),
|
||||
@@ -198,7 +198,9 @@ func NewClient(cfg types.DockerProviderConfig, unique ...bool) (*SharedClient, e
|
||||
opt = append(opt, client.WithTLSClientConfig(cfg.TLS.CAFile, cfg.TLS.CertFile, cfg.TLS.KeyFile))
|
||||
}
|
||||
|
||||
client, err := client.New(opt...)
|
||||
opt = append(opt, client.WithAPIVersionNegotiation())
|
||||
|
||||
client, err := client.NewClientWithOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -11,9 +11,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/agentpool"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
@@ -99,18 +98,18 @@ func UpdatePorts(ctx context.Context, c *types.Container) error {
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
inspect, err := dockerClient.ContainerInspect(ctx, c.ContainerID, client.ContainerInspectOptions{})
|
||||
inspect, err := dockerClient.ContainerInspect(ctx, c.ContainerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for port := range inspect.Container.Config.ExposedPorts {
|
||||
proto, portStr := nat.SplitProtoPort(port.String())
|
||||
for port := range inspect.Config.ExposedPorts {
|
||||
proto, portStr := nat.SplitProtoPort(string(port))
|
||||
portInt, _ := nat.ParsePort(portStr)
|
||||
if portInt == 0 {
|
||||
continue
|
||||
}
|
||||
c.PublicPortMapping[portInt] = container.PortSummary{
|
||||
c.PublicPortMapping[portInt] = container.Port{
|
||||
PublicPort: uint16(portInt), //nolint:gosec
|
||||
PrivatePort: uint16(portInt), //nolint:gosec
|
||||
Type: proto,
|
||||
@@ -211,8 +210,8 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
|
||||
}
|
||||
if c.Network != "" {
|
||||
v, hasNetwork := helper.NetworkSettings.Networks[c.Network]
|
||||
if hasNetwork && v.IPAddress.IsValid() {
|
||||
c.PrivateHostname = v.IPAddress.String()
|
||||
if hasNetwork && v.IPAddress != "" {
|
||||
c.PrivateHostname = v.IPAddress
|
||||
return
|
||||
}
|
||||
var hasComposeNetwork bool
|
||||
@@ -220,9 +219,9 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
|
||||
if proj := DockerComposeProject(c); proj != "" {
|
||||
newNetwork := fmt.Sprintf("%s_%s", proj, c.Network)
|
||||
v, hasComposeNetwork = helper.NetworkSettings.Networks[newNetwork]
|
||||
if hasComposeNetwork && v.IPAddress.IsValid() {
|
||||
if hasComposeNetwork && v.IPAddress != "" {
|
||||
c.Network = newNetwork // update network to the new one
|
||||
c.PrivateHostname = v.IPAddress.String()
|
||||
c.PrivateHostname = v.IPAddress
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -235,9 +234,9 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
|
||||
}
|
||||
// fallback to first network if no network is specified
|
||||
for k, v := range helper.NetworkSettings.Networks {
|
||||
if v.IPAddress.IsValid() {
|
||||
if v.IPAddress != "" {
|
||||
c.Network = k // update network to the first network
|
||||
c.PrivateHostname = v.IPAddress.String()
|
||||
c.PrivateHostname = v.IPAddress
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package docker
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/yusing/ds/ordered"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
|
||||
@@ -3,7 +3,7 @@ package docker
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
@@ -3,12 +3,12 @@ package docker
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
)
|
||||
|
||||
var listOptions = client.ContainerListOptions{
|
||||
var listOptions = container.ListOptions{
|
||||
// created|restarting|running|removing|paused|exited|dead
|
||||
// Filters: filters.NewArgs(
|
||||
// filters.Arg("status", "created"),
|
||||
@@ -31,7 +31,7 @@ func ListContainers(ctx context.Context, dockerCfg types.DockerProviderConfig) (
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return containers.Items, nil
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func IsErrConnectionFailed(err error) bool {
|
||||
|
||||
Submodule internal/go-proxmox updated: bcae3065be...7a07c21f07
@@ -2,13 +2,12 @@ package healthcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
@@ -44,7 +43,7 @@ func Docker(ctx context.Context, state *DockerHealthcheckState, timeout time.Dur
|
||||
defer cancel()
|
||||
|
||||
// the actual inspect response is intercepted and returned as RequestInterceptedError
|
||||
_, err := state.client.ContainerInspect(ctx, state.containerId, client.ContainerInspectOptions{})
|
||||
_, err := state.client.ContainerInspect(ctx, state.containerId)
|
||||
|
||||
var interceptedErr *httputils.RequestInterceptedError
|
||||
if !httputils.AsRequestInterceptedError(err, &interceptedErr) {
|
||||
@@ -106,7 +105,7 @@ func interceptDockerInspectResponse(resp *http.Response) (intercepted bool, err
|
||||
}
|
||||
|
||||
var state container.State
|
||||
err = sonic.Unmarshal(body, &state)
|
||||
err = json.Unmarshal(body, &state)
|
||||
release(body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
||||
@@ -76,8 +76,11 @@ func H2C(ctx context.Context, url *url.URL, method, path string, timeout time.Du
|
||||
|
||||
setCommonHeaders(req.Header.Set)
|
||||
|
||||
client := *h2cClient
|
||||
client.Timeout = timeout
|
||||
|
||||
start := time.Now()
|
||||
resp, err := h2cClient.Do(req)
|
||||
resp, err := client.Do(req)
|
||||
lat := time.Since(start)
|
||||
|
||||
if resp != nil {
|
||||
|
||||
@@ -2,12 +2,12 @@ package iconlist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
@@ -55,20 +55,20 @@ func init() {
|
||||
|
||||
func InitCache() {
|
||||
m := make(IconMap)
|
||||
err := serialization.LoadJSONIfExist(common.IconListCachePath, &m)
|
||||
err := serialization.LoadFileIfExist(common.IconListCachePath, &m, json.Unmarshal)
|
||||
if err != nil {
|
||||
// backward compatible
|
||||
oldFormat := struct {
|
||||
Icons IconMap
|
||||
LastUpdate time.Time
|
||||
}{}
|
||||
err = serialization.LoadJSONIfExist(common.IconListCachePath, &oldFormat)
|
||||
err = serialization.LoadFileIfExist(common.IconListCachePath, &oldFormat, json.Unmarshal)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to load icons")
|
||||
} else {
|
||||
m = oldFormat.Icons
|
||||
// store it to disk immediately
|
||||
_ = serialization.SaveJSON(common.IconListCachePath, &m, 0o644)
|
||||
_ = serialization.SaveFile(common.IconListCachePath, &m, 0o644, json.Marshal)
|
||||
}
|
||||
} else if len(m) > 0 {
|
||||
log.Info().
|
||||
@@ -84,7 +84,7 @@ func InitCache() {
|
||||
|
||||
task.OnProgramExit("save_icons_cache", func() {
|
||||
icons := iconsCache.Load()
|
||||
_ = serialization.SaveJSON(common.IconListCachePath, &icons, 0o644)
|
||||
_ = serialization.SaveFile(common.IconListCachePath, &icons, 0o644, json.Marshal)
|
||||
})
|
||||
|
||||
go backgroundUpdateIcons()
|
||||
@@ -105,7 +105,7 @@ func backgroundUpdateIcons() {
|
||||
// swap old cache with new cache
|
||||
iconsCache.Store(newCache)
|
||||
// save it to disk
|
||||
err := serialization.SaveJSON(common.IconListCachePath, &newCache, 0o644)
|
||||
err := serialization.SaveFile(common.IconListCachePath, &newCache, 0o644, json.Marshal)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed to save icons")
|
||||
}
|
||||
@@ -270,7 +270,7 @@ func UpdateWalkxCodeIcons(m IconMap) error {
|
||||
}
|
||||
|
||||
data := make(map[string][]string)
|
||||
err = sonic.Unmarshal(body, &data)
|
||||
err = json.Unmarshal(body, &data)
|
||||
release(body)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -349,7 +349,7 @@ func UpdateSelfhstIcons(m IconMap) error {
|
||||
}
|
||||
|
||||
data := make([]SelfhStIcon, 0)
|
||||
err = sonic.Unmarshal(body, &data) //nolint:musttag
|
||||
err = json.Unmarshal(body, &data) //nolint:musttag
|
||||
release(body)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -2,11 +2,11 @@ package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/yusing/godoxy/internal/homepage/widgets"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
)
|
||||
@@ -59,7 +59,7 @@ func jsonRequest[T any](ctx context.Context, client *Client, endpoint string, qu
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
err = sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&result)
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
const endpointLogs = "/api/v2/log/main"
|
||||
@@ -45,7 +44,7 @@ func (l *LogEntry) Level() string {
|
||||
}
|
||||
|
||||
func (l *LogEntry) MarshalJSON() ([]byte, error) {
|
||||
return sonic.Marshal(map[string]any{
|
||||
return json.Marshal(map[string]any{
|
||||
"id": l.ID,
|
||||
"timestamp": l.Timestamp,
|
||||
"level": l.Level(),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"iter"
|
||||
"strconv"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ type watcherDebug struct {
|
||||
|
||||
func (w watcherDebug) MarshalJSON() ([]byte, error) {
|
||||
state := w.state.Load()
|
||||
return sonic.Marshal(map[string]any{
|
||||
return json.Marshal(map[string]any{
|
||||
"name": w.Name(),
|
||||
"state": map[string]string{
|
||||
"status": string(state.status),
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
type WakeEvent struct {
|
||||
@@ -40,7 +39,7 @@ func (w *Watcher) newWakeEvent(eventType WakeEventType, message string, err erro
|
||||
}
|
||||
|
||||
func (e *WakeEvent) WriteSSE(w io.Writer) error {
|
||||
data, err := sonic.Marshal(e)
|
||||
data, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@ package provider
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
@@ -18,7 +17,7 @@ type DockerProvider struct {
|
||||
containerID string
|
||||
}
|
||||
|
||||
var startOptions = client.ContainerStartOptions{}
|
||||
var startOptions = container.StartOptions{}
|
||||
|
||||
func NewDockerProvider(dockerCfg types.DockerProviderConfig, containerID string) (idlewatcher.Provider, error) {
|
||||
client, err := docker.NewClient(dockerCfg)
|
||||
@@ -33,41 +32,34 @@ func NewDockerProvider(dockerCfg types.DockerProviderConfig, containerID string)
|
||||
}
|
||||
|
||||
func (p *DockerProvider) ContainerPause(ctx context.Context) error {
|
||||
_, err := p.client.ContainerPause(ctx, p.containerID, client.ContainerPauseOptions{})
|
||||
return err
|
||||
return p.client.ContainerPause(ctx, p.containerID)
|
||||
}
|
||||
|
||||
func (p *DockerProvider) ContainerUnpause(ctx context.Context) error {
|
||||
_, err := p.client.ContainerUnpause(ctx, p.containerID, client.ContainerUnpauseOptions{})
|
||||
return err
|
||||
return p.client.ContainerUnpause(ctx, p.containerID)
|
||||
}
|
||||
|
||||
func (p *DockerProvider) ContainerStart(ctx context.Context) error {
|
||||
_, err := p.client.ContainerStart(ctx, p.containerID, startOptions)
|
||||
return err
|
||||
return p.client.ContainerStart(ctx, p.containerID, startOptions)
|
||||
}
|
||||
|
||||
func (p *DockerProvider) ContainerStop(ctx context.Context, signal types.ContainerSignal, timeout int) error {
|
||||
_, err := p.client.ContainerStop(ctx, p.containerID, client.ContainerStopOptions{
|
||||
return p.client.ContainerStop(ctx, p.containerID, container.StopOptions{
|
||||
Signal: string(signal),
|
||||
Timeout: &timeout,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *DockerProvider) ContainerKill(ctx context.Context, signal types.ContainerSignal) error {
|
||||
_, err := p.client.ContainerKill(ctx, p.containerID, client.ContainerKillOptions{
|
||||
Signal: string(signal),
|
||||
})
|
||||
return err
|
||||
return p.client.ContainerKill(ctx, p.containerID, string(signal))
|
||||
}
|
||||
|
||||
func (p *DockerProvider) ContainerStatus(ctx context.Context) (idlewatcher.ContainerStatus, error) {
|
||||
status, err := p.client.ContainerInspect(ctx, p.containerID, client.ContainerInspectOptions{})
|
||||
status, err := p.client.ContainerInspect(ctx, p.containerID)
|
||||
if err != nil {
|
||||
return idlewatcher.ContainerStatusError, err
|
||||
}
|
||||
switch status.Container.State.Status {
|
||||
switch status.State.Status {
|
||||
case container.StateRunning:
|
||||
return idlewatcher.ContainerStatusRunning, nil
|
||||
case container.StateExited, container.StateDead, container.StateRestarting:
|
||||
@@ -75,12 +67,12 @@ func (p *DockerProvider) ContainerStatus(ctx context.Context) (idlewatcher.Conta
|
||||
case container.StatePaused:
|
||||
return idlewatcher.ContainerStatusPaused, nil
|
||||
}
|
||||
return idlewatcher.ContainerStatusError, idlewatcher.ErrUnexpectedContainerStatus.Subject(string(status.Container.State.Status))
|
||||
return idlewatcher.ContainerStatusError, idlewatcher.ErrUnexpectedContainerStatus.Subject(status.State.Status)
|
||||
}
|
||||
|
||||
func (p *DockerProvider) Watch(ctx context.Context) (eventCh <-chan watcher.Event, errCh <-chan gperr.Error) {
|
||||
return p.watcher.EventsWithOptions(ctx, watcher.DockerListOptions{
|
||||
Filters: watcher.NewDockerFilters(
|
||||
Filters: watcher.NewDockerFilter(
|
||||
watcher.DockerFilterContainer,
|
||||
watcher.DockerFilterContainerNameID(p.containerID),
|
||||
watcher.DockerFilterStart,
|
||||
|
||||
@@ -26,6 +26,10 @@ const proxmoxStateCheckInterval = 1 * time.Second
|
||||
var ErrNodeNotFound = gperr.New("node not found in pool")
|
||||
|
||||
func NewProxmoxProvider(ctx context.Context, nodeName string, vmid int) (idlewatcher.Provider, error) {
|
||||
if nodeName == "" || vmid == 0 {
|
||||
return nil, gperr.New("node name and vmid are required")
|
||||
}
|
||||
|
||||
node, ok := proxmox.Nodes.Get(nodeName)
|
||||
if !ok {
|
||||
return nil, ErrNodeNotFound.Subject(nodeName).
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package idlewatcher
|
||||
@@ -261,7 +261,7 @@ func loadNS[T store](ns namespace) T {
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if err := sonic.ConfigDefault.NewDecoder(file).Decode(&store); err != nil {
|
||||
if err := json.NewDecoder(file).Decode(&store); err != nil {
|
||||
log.Err(err).Msg("failed to decode store")
|
||||
}
|
||||
|
||||
@@ -316,7 +316,7 @@ type MapStore[VT any] struct {
|
||||
|
||||
```go
|
||||
func (s MapStore[VT]) MarshalJSON() ([]byte, error) {
|
||||
return sonic.Marshal(xsync.ToPlainMap(s.Map))
|
||||
return json.Marshal(xsync.ToPlainMap(s.Map))
|
||||
}
|
||||
```
|
||||
|
||||
@@ -325,7 +325,7 @@ func (s MapStore[VT]) MarshalJSON() ([]byte, error) {
|
||||
```go
|
||||
func (s *MapStore[VT]) UnmarshalJSON(data []byte) error {
|
||||
tmp := make(map[string]VT)
|
||||
if err := sonic.Unmarshal(data, &tmp); err != nil {
|
||||
if err := json.Unmarshal(data, &tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
s.Map = xsync.NewMap[string, VT](xsync.WithPresize(len(tmp)))
|
||||
@@ -349,7 +349,7 @@ The jsonstore package integrates with:
|
||||
Errors are logged but don't prevent store usage:
|
||||
|
||||
```go
|
||||
if err := sonic.Unmarshal(data, &tmp); err != nil {
|
||||
if err := json.Unmarshal(data, &tmp); err != nil {
|
||||
log.Err(err).
|
||||
Str("path", path).
|
||||
Msg("failed to load store")
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
@@ -66,7 +65,7 @@ func loadNS[T store](ns namespace) T {
|
||||
}
|
||||
} else {
|
||||
defer file.Close()
|
||||
if err := sonic.ConfigDefault.NewDecoder(file).Decode(&store); err != nil {
|
||||
if err := json.NewDecoder(file).Decode(&store); err != nil {
|
||||
log.Err(err).
|
||||
Str("path", path).
|
||||
Msg("failed to load store")
|
||||
@@ -83,7 +82,8 @@ func loadNS[T store](ns namespace) T {
|
||||
func save() error {
|
||||
errs := gperr.NewBuilder("failed to save data stores")
|
||||
for ns, store := range stores {
|
||||
if err := serialization.SaveJSON(filepath.Join(storesPath, string(ns)+".json"), &store, 0o644); err != nil {
|
||||
path := filepath.Join(storesPath, string(ns)+".json")
|
||||
if err := serialization.SaveFile(path, &store, 0o644, json.Marshal); err != nil {
|
||||
errs.Add(err)
|
||||
}
|
||||
}
|
||||
@@ -113,12 +113,12 @@ func (s *MapStore[VT]) Initialize() {
|
||||
}
|
||||
|
||||
func (s MapStore[VT]) MarshalJSON() ([]byte, error) {
|
||||
return sonic.Marshal(xsync.ToPlainMap(s.Map))
|
||||
return json.Marshal(xsync.ToPlainMap(s.Map))
|
||||
}
|
||||
|
||||
func (s *MapStore[VT]) UnmarshalJSON(data []byte) error {
|
||||
tmp := make(map[string]VT)
|
||||
if err := sonic.Unmarshal(data, &tmp); err != nil {
|
||||
if err := json.Unmarshal(data, &tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
s.Map = xsync.NewMap[string, VT](xsync.WithPresize(len(tmp)))
|
||||
@@ -134,10 +134,10 @@ func (obj *ObjectStore[Ptr]) Initialize() {
|
||||
}
|
||||
|
||||
func (obj ObjectStore[Ptr]) MarshalJSON() ([]byte, error) {
|
||||
return sonic.Marshal(obj.ptr)
|
||||
return json.Marshal(obj.ptr)
|
||||
}
|
||||
|
||||
func (obj *ObjectStore[Ptr]) UnmarshalJSON(data []byte) error {
|
||||
obj.Initialize()
|
||||
return sonic.Unmarshal(data, obj.ptr)
|
||||
return json.Unmarshal(data, obj.ptr)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
|
||||
"github.com/rs/zerolog/diode"
|
||||
zerologlog "github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -68,7 +69,13 @@ func fmtMessage(msg string) string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func multiLevelWriter(out ...io.Writer) io.Writer {
|
||||
func diodeMultiWriter(out ...io.Writer) io.Writer {
|
||||
return diode.NewWriter(multiWriter(out...), 1024, 0, func(missed int) {
|
||||
zerologlog.Warn().Int("missed", missed).Msg("missed log messages")
|
||||
})
|
||||
}
|
||||
|
||||
func multiWriter(out ...io.Writer) io.Writer {
|
||||
if len(out) == 0 {
|
||||
return os.Stdout
|
||||
}
|
||||
@@ -80,7 +87,7 @@ func multiLevelWriter(out ...io.Writer) io.Writer {
|
||||
|
||||
func NewLogger(out ...io.Writer) zerolog.Logger {
|
||||
writer := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
|
||||
w.Out = multiLevelWriter(out...)
|
||||
w.Out = diodeMultiWriter(out...)
|
||||
w.TimeFormat = timeFmt
|
||||
w.FormatMessage = func(msgI any) string { // pad spaces for each line
|
||||
if msgI == nil {
|
||||
@@ -94,7 +101,7 @@ func NewLogger(out ...io.Writer) zerolog.Logger {
|
||||
|
||||
func NewLoggerWithFixedLevel(lvl zerolog.Level, out ...io.Writer) zerolog.Logger {
|
||||
writer := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
|
||||
w.Out = multiLevelWriter(out...)
|
||||
w.Out = diodeMultiWriter(out...)
|
||||
w.TimeFormat = timeFmt
|
||||
w.FormatMessage = func(msgI any) string { // pad spaces for each line
|
||||
if msgI == nil {
|
||||
|
||||
@@ -3,8 +3,6 @@ package period
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
type Entries[T any] struct {
|
||||
@@ -75,7 +73,7 @@ type entriesJSON[T any] struct {
|
||||
}
|
||||
|
||||
func (e *Entries[T]) MarshalJSON() ([]byte, error) {
|
||||
return sonic.Marshal(entriesJSON[T]{
|
||||
return json.Marshal(entriesJSON[T]{
|
||||
Entries: e.Get(),
|
||||
Interval: e.interval,
|
||||
})
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/rs/zerolog/log"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
"github.com/yusing/goutils/synk"
|
||||
@@ -98,7 +97,7 @@ func (p *Poller[T, AggregateT]) save() error {
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
err = sonic.ConfigDefault.NewEncoder(f).Encode(p.period)
|
||||
err = json.NewEncoder(f).Encode(p.period)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
@@ -82,12 +81,12 @@ var (
|
||||
|
||||
func TestSystemInfo(t *testing.T) {
|
||||
// Test marshaling
|
||||
data, err := sonic.Marshal(testInfo)
|
||||
data, err := json.Marshal(testInfo)
|
||||
expect.NoError(t, err)
|
||||
|
||||
// Test unmarshaling back
|
||||
var decoded SystemInfo
|
||||
err = sonic.Unmarshal(data, &decoded)
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
expect.NoError(t, err)
|
||||
|
||||
// Compare original and decoded
|
||||
@@ -127,7 +126,7 @@ func TestSerialize(t *testing.T) {
|
||||
for _, query := range allQueries {
|
||||
t.Run(string(query), func(t *testing.T) {
|
||||
_, result := aggregate(entries, url.Values{"aggregate": []string{string(query)}})
|
||||
s, err := sonic.Marshal(result)
|
||||
s, err := json.Marshal(result)
|
||||
expect.NoError(t, err)
|
||||
var v []map[string]any
|
||||
expect.NoError(t, json.Unmarshal(s, &v))
|
||||
|
||||
@@ -2,11 +2,11 @@ package uptime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
"github.com/yusing/godoxy/internal/metrics/period"
|
||||
metricsutils "github.com/yusing/godoxy/internal/metrics/utils"
|
||||
@@ -47,7 +47,7 @@ func getStatuses(ctx context.Context, _ StatusByAlias) (StatusByAlias, error) {
|
||||
}
|
||||
|
||||
func (s *Status) MarshalJSON() ([]byte, error) {
|
||||
return sonic.Marshal(map[string]any{
|
||||
return json.Marshal(map[string]any{
|
||||
"status": s.Status.String(),
|
||||
"latency": s.Latency,
|
||||
"timestamp": s.Timestamp,
|
||||
@@ -149,5 +149,5 @@ func (rs RouteStatuses) aggregate(limit int, offset int) Aggregated {
|
||||
}
|
||||
|
||||
func (result Aggregated) MarshalJSON() ([]byte, error) {
|
||||
return sonic.Marshal([]RouteAggregate(result))
|
||||
return json.Marshal([]RouteAggregate(result))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package captcha
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -11,7 +12,6 @@ import (
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
)
|
||||
|
||||
@@ -70,7 +70,7 @@ func (p *HcaptchaProvider) Verify(r *http.Request) error {
|
||||
Success bool `json:"success"`
|
||||
Error []string `json:"error-codes"`
|
||||
}
|
||||
if err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&respData); err != nil {
|
||||
if err := json.NewDecoder(resp.Body).Decode(&respData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"maps"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
@@ -152,7 +152,7 @@ func (m *Middleware) MarshalJSON() ([]byte, error) {
|
||||
commonOptions
|
||||
any
|
||||
}
|
||||
return sonic.MarshalIndent(map[string]any{
|
||||
return json.MarshalIndent(map[string]any{
|
||||
"name": m.name,
|
||||
"options": allOptions{
|
||||
commonOptions: m.commonOptions,
|
||||
|
||||
@@ -3,12 +3,12 @@ package middleware
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
@@ -24,7 +24,7 @@ func init() {
|
||||
return
|
||||
}
|
||||
tmp := map[string]string{}
|
||||
err := sonic.Unmarshal(testHeadersRaw, &tmp)
|
||||
err := json.Unmarshal(testHeadersRaw, &tmp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package nettypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
urlPkg "net/url"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
type URL struct {
|
||||
@@ -47,7 +46,7 @@ func (u *URL) MarshalJSON() (text []byte, err error) {
|
||||
if u == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return sonic.Marshal(u.URL.String())
|
||||
return json.Marshal(u.URL.String())
|
||||
}
|
||||
|
||||
func (u *URL) Equals(other *URL) bool {
|
||||
|
||||
@@ -2,9 +2,9 @@ package notif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
)
|
||||
|
||||
@@ -69,7 +69,7 @@ func (f FieldsBody) Format(format LogFormat) ([]byte, error) {
|
||||
}
|
||||
return msg.Bytes(), nil
|
||||
case LogFormatRawJSON:
|
||||
return sonic.Marshal(f)
|
||||
return json.Marshal(f)
|
||||
}
|
||||
return f.Format(LogFormatMarkdown)
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func (l ListBody) Format(format LogFormat) ([]byte, error) {
|
||||
}
|
||||
return msg.Bytes(), nil
|
||||
case LogFormatRawJSON:
|
||||
return sonic.Marshal(l)
|
||||
return json.Marshal(l)
|
||||
}
|
||||
return l.Format(LogFormatMarkdown)
|
||||
}
|
||||
@@ -97,7 +97,7 @@ func (m MessageBody) Format(format LogFormat) ([]byte, error) {
|
||||
case LogFormatPlain, LogFormatMarkdown:
|
||||
return []byte(m), nil
|
||||
case LogFormatRawJSON:
|
||||
return sonic.Marshal(m)
|
||||
return json.Marshal(m)
|
||||
}
|
||||
return []byte(m), nil
|
||||
}
|
||||
@@ -105,7 +105,7 @@ func (m MessageBody) Format(format LogFormat) ([]byte, error) {
|
||||
func (m MessageBodyBytes) Format(format LogFormat) ([]byte, error) {
|
||||
switch format {
|
||||
case LogFormatRawJSON:
|
||||
return sonic.Marshal(string(m))
|
||||
return json.Marshal(string(m))
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
@@ -113,7 +113,7 @@ func (m MessageBodyBytes) Format(format LogFormat) ([]byte, error) {
|
||||
func (e errorBody) Format(format LogFormat) ([]byte, error) {
|
||||
switch format {
|
||||
case LogFormatRawJSON:
|
||||
return sonic.Marshal(e.Error)
|
||||
return json.Marshal(e.Error)
|
||||
case LogFormatPlain:
|
||||
return gperr.Plain(e.Error), nil
|
||||
case LogFormatMarkdown:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package notif
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gotify/server/v2/model"
|
||||
"github.com/rs/zerolog"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
@@ -65,7 +65,7 @@ func (client *GotifyClient) MarshalMessage(logMsg *LogMessage) ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
data, err := sonic.Marshal(msg)
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func (client *GotifyClient) MarshalMessage(logMsg *LogMessage) ([]byte, error) {
|
||||
// fmtError implements Provider.
|
||||
func (client *GotifyClient) fmtError(respBody io.Reader) error {
|
||||
var errm model.Error
|
||||
err := sonic.ConfigDefault.NewDecoder(respBody).Decode(&errm)
|
||||
err := json.NewDecoder(respBody).Decode(&errm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode err response: %w", err)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ package notif
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
)
|
||||
|
||||
@@ -99,7 +99,7 @@ func (webhook *Webhook) fmtError(respBody io.Reader) error {
|
||||
}
|
||||
|
||||
func (webhook *Webhook) MarshalMessage(logMsg *LogMessage) ([]byte, error) {
|
||||
title, err := sonic.Marshal(logMsg.Title)
|
||||
title, err := json.Marshal(logMsg.Title)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -118,7 +118,7 @@ func (webhook *Webhook) MarshalMessage(logMsg *LogMessage) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
if webhook.MIMEType == MimeTypeJSON {
|
||||
message, err = sonic.Marshal(string(message))
|
||||
message, err = json.Marshal(string(message))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -147,5 +147,5 @@ func validateJSONPayload(payload string) bool {
|
||||
"$color", "",
|
||||
)
|
||||
payload = replacer.Replace(payload)
|
||||
return sonic.Valid([]byte(payload))
|
||||
return json.Valid([]byte(payload))
|
||||
}
|
||||
|
||||
@@ -52,11 +52,11 @@ graph TD
|
||||
```go
|
||||
type Config struct {
|
||||
URL string `json:"url" validate:"required,url"`
|
||||
Username string `json:"username" validate:"required_without=TokenID Secret"`
|
||||
Password strutils.Redacted `json:"password" validate:"required_without=TokenID Secret"`
|
||||
Realm string `json:"realm" validate:"required_without=TokenID Secret"`
|
||||
TokenID string `json:"token_id" validate:"required_without=Username Password"`
|
||||
Secret strutils.Redacted `json:"secret" validate:"required_without=Username Password"`
|
||||
Username string `json:"username" validate:"required_without_all=TokenID Secret"`
|
||||
Password strutils.Redacted `json:"password" validate:"required_without_all=TokenID Secret"`
|
||||
Realm string `json:"realm"`
|
||||
TokenID string `json:"token_id" validate:"required_without_all=Username Password"`
|
||||
Secret strutils.Redacted `json:"secret" validate:"required_without_all=Username Password"`
|
||||
NoTLSVerify bool `json:"no_tls_verify"`
|
||||
|
||||
client *Client
|
||||
@@ -70,6 +70,7 @@ type Client struct {
|
||||
*proxmox.Client
|
||||
*proxmox.Cluster
|
||||
Version *proxmox.Version
|
||||
BaseURL *url.URL
|
||||
// id -> resource; id: lxc/<vmid> or qemu/<vmid>
|
||||
resources map[string]*VMResource
|
||||
resourcesMu sync.RWMutex
|
||||
@@ -79,6 +80,9 @@ type VMResource struct {
|
||||
*proxmox.ClusterResource
|
||||
IPs []net.IP
|
||||
}
|
||||
|
||||
// NewClient creates a new Proxmox client.
|
||||
func NewClient(baseUrl string, opts ...proxmox.Option) *Client
|
||||
```
|
||||
|
||||
### Node
|
||||
@@ -97,10 +101,11 @@ var Nodes = pool.New[*Node]("proxmox_nodes")
|
||||
|
||||
```go
|
||||
type NodeConfig struct {
|
||||
Node string `json:"node" validate:"required"`
|
||||
VMID int `json:"vmid" validate:"required"`
|
||||
VMName string `json:"vmname,omitempty"`
|
||||
Service string `json:"service,omitempty"`
|
||||
Node string `json:"node" validate:"required"`
|
||||
VMID *int `json:"vmid"` // nil: auto discover; 0: node-level route; >0: lxc/qemu resource route
|
||||
VMName string `json:"vmname,omitempty"`
|
||||
Services []string `json:"services,omitempty" aliases:"service"`
|
||||
Files []string `json:"files,omitempty" aliases:"file"`
|
||||
}
|
||||
```
|
||||
|
||||
@@ -119,6 +124,9 @@ func (c *Config) Client() *Client
|
||||
### Client Operations
|
||||
|
||||
```go
|
||||
// NewClient creates a new Proxmox client.
|
||||
func NewClient(baseUrl string, opts ...proxmox.Option) *Client
|
||||
|
||||
// UpdateClusterInfo fetches cluster info and discovers nodes.
|
||||
func (c *Client) UpdateClusterInfo(ctx context.Context) error
|
||||
|
||||
@@ -136,6 +144,15 @@ func (c *Client) ReverseLookupNode(hostname string, ip net.IP, alias string) str
|
||||
|
||||
// NumNodes returns the number of nodes in the cluster.
|
||||
func (c *Client) NumNodes() int
|
||||
|
||||
// Key returns the cluster ID.
|
||||
func (c *Client) Key() string
|
||||
|
||||
// Name returns the cluster name.
|
||||
func (c *Client) Name() string
|
||||
|
||||
// MarshalJSON returns the cluster info as JSON.
|
||||
func (c *Client) MarshalJSON() ([]byte, error)
|
||||
```
|
||||
|
||||
### Node Operations
|
||||
@@ -144,17 +161,29 @@ func (c *Client) NumNodes() int
|
||||
// AvailableNodeNames returns all available node names as a comma-separated string.
|
||||
func AvailableNodeNames() string
|
||||
|
||||
// NewNode creates a new node.
|
||||
func NewNode(client *Client, name, id string) *Node
|
||||
|
||||
// Node.Client returns the Proxmox client.
|
||||
func (n *Node) Client() *Client
|
||||
|
||||
// Node.Get performs a GET request on the node.
|
||||
func (n *Node) Get(ctx context.Context, path string, v any) error
|
||||
|
||||
// Node.Key returns the node name.
|
||||
func (n *Node) Key() string
|
||||
|
||||
// Node.Name returns the node name.
|
||||
func (n *Node) Name() string
|
||||
|
||||
// NodeCommand executes a command on the node and streams output.
|
||||
func (n *Node) NodeCommand(ctx context.Context, command string) (io.ReadCloser, error)
|
||||
|
||||
// NodeJournalctl streams journalctl output from the node.
|
||||
func (n *Node) NodeJournalctl(ctx context.Context, service string, limit int) (io.ReadCloser, error)
|
||||
func (n *Node) NodeJournalctl(ctx context.Context, services []string, limit int) (io.ReadCloser, error)
|
||||
|
||||
// NodeTail streams tail output for the given file.
|
||||
func (n *Node) NodeTail(ctx context.Context, files []string, limit int) (io.ReadCloser, error)
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -275,7 +304,35 @@ func (node *Node) LXCStats(ctx context.Context, vmid int, stream bool) (io.ReadC
|
||||
func (node *Node) LXCCommand(ctx context.Context, vmid int, command string) (io.ReadCloser, error)
|
||||
|
||||
// LXCJournalctl streams journalctl output for a container service.
|
||||
func (node *Node) LXCJournalctl(ctx context.Context, vmid int, service string, limit int) (io.ReadCloser, error)
|
||||
// On non-systemd systems, it falls back to tailing /var/log/messages.
|
||||
func (node *Node) LXCJournalctl(ctx context.Context, vmid int, services []string, limit int) (io.ReadCloser, error)
|
||||
|
||||
// LXCTail streams tail output for the given file.
|
||||
func (node *Node) LXCTail(ctx context.Context, vmid int, files []string, limit int) (io.ReadCloser, error)
|
||||
```
|
||||
|
||||
## Node Stats
|
||||
|
||||
```go
|
||||
type NodeStats struct {
|
||||
KernelVersion string `json:"kernel_version"`
|
||||
PVEVersion string `json:"pve_version"`
|
||||
CPUUsage string `json:"cpu_usage"`
|
||||
CPUModel string `json:"cpu_model"`
|
||||
MemUsage string `json:"mem_usage"`
|
||||
MemTotal string `json:"mem_total"`
|
||||
MemPct string `json:"mem_pct"`
|
||||
RootFSUsage string `json:"rootfs_usage"`
|
||||
RootFSTotal string `json:"rootfs_total"`
|
||||
RootFSPct string `json:"rootfs_pct"`
|
||||
Uptime string `json:"uptime"`
|
||||
LoadAvg1m string `json:"load_avg_1m"`
|
||||
LoadAvg5m string `json:"load_avg_5m"`
|
||||
LoadAvg15m string `json:"load_avg_15m"`
|
||||
}
|
||||
|
||||
// NodeStats streams node statistics like docker stats.
|
||||
func (n *Node) NodeStats(ctx context.Context, stream bool) (io.ReadCloser, error)
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
@@ -453,6 +510,12 @@ var (
|
||||
)
|
||||
```
|
||||
|
||||
| Error | Description |
|
||||
| --------------------- | --------------------------------------------------------------------- |
|
||||
| `ErrResourceNotFound` | Resource not found in cluster |
|
||||
| `ErrNoResources` | No resources available |
|
||||
| `ErrNoSession` | No session for WebSocket operations (requires username/password auth) |
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Cluster info fetched once on init
|
||||
@@ -463,10 +526,26 @@ var (
|
||||
- Per-operation API calls with 3-second timeout
|
||||
- WebSocket connections properly closed to prevent goroutine leaks
|
||||
|
||||
## Command Validation
|
||||
|
||||
Commands executed via WebSocket are validated to prevent command injection. Invalid characters include:
|
||||
|
||||
```
|
||||
& | $ ; ' " ` $( ${ < >
|
||||
```
|
||||
|
||||
Services and files passed to `journalctl` and `tail` commands are automatically validated.
|
||||
|
||||
## Constants
|
||||
|
||||
```go
|
||||
const ResourcePollInterval = 3 * time.Second
|
||||
const SessionRefreshInterval = 1 * time.Minute
|
||||
const NodeStatsPollInterval = time.Second
|
||||
```
|
||||
|
||||
The `ResourcePollInterval` constant controls how often resources are updated in the background loop.
|
||||
| Constant | Default | Description |
|
||||
| ------------------------ | ------- | ---------------------------------- |
|
||||
| `ResourcePollInterval` | 3s | How often VM resources are updated |
|
||||
| `SessionRefreshInterval` | 1m | How often sessions are refreshed |
|
||||
| `NodeStatsPollInterval` | 1s | How often node stats are streamed |
|
||||
|
||||
@@ -2,18 +2,18 @@ package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/luthermonson/go-proxmox"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ type Client struct {
|
||||
*proxmox.Client
|
||||
*proxmox.Cluster
|
||||
Version *proxmox.Version
|
||||
BaseURL *url.URL
|
||||
// id -> resource; id: lxc/<vmid> or qemu/<vmid>
|
||||
resources map[string]*VMResource
|
||||
resourcesMu sync.RWMutex
|
||||
@@ -44,6 +45,11 @@ func NewClient(baseUrl string, opts ...proxmox.Option) *Client {
|
||||
}
|
||||
|
||||
func (c *Client) UpdateClusterInfo(ctx context.Context) (err error) {
|
||||
baseURL, err := url.Parse(c.Client.GetBaseURL())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.BaseURL = baseURL
|
||||
c.Version, err = c.Client.Version(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -65,6 +71,9 @@ func (c *Client) UpdateClusterInfo(ctx context.Context) (err error) {
|
||||
}
|
||||
|
||||
func (c *Client) UpdateResources(ctx context.Context) error {
|
||||
if c.Cluster == nil {
|
||||
return errors.New("cluster not initialized, call UpdateClusterInfo first")
|
||||
}
|
||||
resourcesSlice, err := c.Cluster.Resources(ctx, "vm")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -110,7 +119,6 @@ func (c *Client) UpdateResources(ctx context.Context) error {
|
||||
c.resources[resource.ID] = vmResources[i]
|
||||
}
|
||||
c.resourcesMu.Unlock()
|
||||
log.Debug().Str("cluster", c.Cluster.Name).Msgf("[proxmox] updated %d resources", len(c.resources))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -193,7 +201,7 @@ func (c *Client) Name() string {
|
||||
}
|
||||
|
||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
return sonic.Marshal(map[string]any{
|
||||
return json.Marshal(map[string]any{
|
||||
"version": c.Version,
|
||||
"cluster": map[string]any{
|
||||
"name": c.Cluster.Name,
|
||||
|
||||
60
internal/proxmox/command_common.go
Normal file
60
internal/proxmox/command_common.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// checkValidInput checks if the input contains invalid characters.
|
||||
//
|
||||
// The characters are: & | $ ; ' " ` $( ${ < >
|
||||
// These characters are used in the command line to escape the input or to expand variables.
|
||||
// We need to check if the input contains these characters and return an error if it does.
|
||||
// This is to prevent command injection.
|
||||
func checkValidInput(input string) error {
|
||||
if strings.ContainsAny(input, "&|$;'\"`<>") {
|
||||
return fmt.Errorf("input contains invalid characters: %q", input)
|
||||
}
|
||||
if strings.Contains(input, "$(") {
|
||||
return fmt.Errorf("input contains $(: %q", input)
|
||||
}
|
||||
if strings.Contains(input, "${") {
|
||||
return fmt.Errorf("input contains ${: %q", input)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatTail(files []string, limit int) (string, error) {
|
||||
for _, file := range files {
|
||||
if err := checkValidInput(file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
var command strings.Builder
|
||||
command.WriteString("tail -f -q ")
|
||||
for _, file := range files {
|
||||
fmt.Fprintf(&command, " %q ", file)
|
||||
}
|
||||
if limit > 0 {
|
||||
fmt.Fprintf(&command, " -n %d", limit)
|
||||
}
|
||||
// try --retry first, if it fails, try the command again
|
||||
return fmt.Sprintf("sh -c '%s --retry 2>/dev/null || %s'", command.String(), command.String()), nil
|
||||
}
|
||||
|
||||
func formatJournalctl(services []string, limit int) (string, error) {
|
||||
for _, service := range services {
|
||||
if err := checkValidInput(service); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
var command strings.Builder
|
||||
command.WriteString("journalctl -f")
|
||||
for _, service := range services {
|
||||
fmt.Fprintf(&command, " -u %q ", service)
|
||||
}
|
||||
if limit > 0 {
|
||||
fmt.Fprintf(&command, " -n %d", limit)
|
||||
}
|
||||
return command.String(), nil
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"math"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -18,12 +19,12 @@ import (
|
||||
type Config struct {
|
||||
URL string `json:"url" validate:"required,url"`
|
||||
|
||||
Username string `json:"username" validate:"required_without=TokenID Secret"`
|
||||
Password strutils.Redacted `json:"password" validate:"required_without=TokenID Secret"`
|
||||
Realm string `json:"realm" validate:"required_without=TokenID Secret"`
|
||||
Username string `json:"username" validate:"required_without_all=TokenID Secret"`
|
||||
Password strutils.Redacted `json:"password" validate:"required_without_all=TokenID Secret"`
|
||||
Realm string `json:"realm"` // default is "pam"
|
||||
|
||||
TokenID string `json:"token_id" validate:"required_without=Username Password"`
|
||||
Secret strutils.Redacted `json:"secret" validate:"required_without=Username Password"`
|
||||
TokenID string `json:"token_id" validate:"required_without_all=Username Password"`
|
||||
Secret strutils.Redacted `json:"secret" validate:"required_without_all=Username Password"`
|
||||
|
||||
NoTLSVerify bool `json:"no_tls_verify" yaml:"no_tls_verify,omitempty"`
|
||||
|
||||
@@ -31,6 +32,7 @@ type Config struct {
|
||||
}
|
||||
|
||||
const ResourcePollInterval = 3 * time.Second
|
||||
const SessionRefreshInterval = 1 * time.Minute
|
||||
|
||||
// NodeStatsPollInterval controls how often node stats are streamed when streaming is enabled.
|
||||
const NodeStatsPollInterval = time.Second
|
||||
@@ -65,6 +67,9 @@ func (c *Config) Init(ctx context.Context) gperr.Error {
|
||||
}
|
||||
useCredentials := false
|
||||
if c.Username != "" && c.Password != "" {
|
||||
if c.Realm == "" {
|
||||
c.Realm = "pam"
|
||||
}
|
||||
opts = append(opts, proxmox.WithCredentials(&proxmox.Credentials{
|
||||
Username: c.Username,
|
||||
Password: c.Password.String(),
|
||||
@@ -93,16 +98,6 @@ func (c *Config) Init(ctx context.Context) gperr.Error {
|
||||
return gperr.New("failed to fetch proxmox cluster info").With(err)
|
||||
}
|
||||
|
||||
go c.updateResourcesLoop(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) updateResourcesLoop(ctx context.Context) {
|
||||
ticker := time.NewTicker(ResourcePollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
log.Trace().Str("cluster", c.client.Cluster.Name).Msg("[proxmox] starting resources update loop")
|
||||
|
||||
{
|
||||
reqCtx, reqCtxCancel := context.WithTimeout(ctx, ResourcePollInterval)
|
||||
err := c.client.UpdateResources(reqCtx)
|
||||
@@ -112,6 +107,17 @@ func (c *Config) updateResourcesLoop(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
go c.updateResourcesLoop(ctx)
|
||||
go c.refreshSessionLoop(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) updateResourcesLoop(ctx context.Context) {
|
||||
ticker := time.NewTicker(ResourcePollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
log.Trace().Str("cluster", c.client.Cluster.Name).Msg("[proxmox] starting resources update loop")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -127,3 +133,33 @@ func (c *Config) updateResourcesLoop(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) refreshSessionLoop(ctx context.Context) {
|
||||
ticker := time.NewTicker(SessionRefreshInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
log.Trace().Str("cluster", c.client.Cluster.Name).Msg("[proxmox] starting session refresh loop")
|
||||
|
||||
numRetries := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Trace().Str("cluster", c.client.Cluster.Name).Msg("[proxmox] stopping session refresh loop")
|
||||
return
|
||||
case <-ticker.C:
|
||||
reqCtx, reqCtxCancel := context.WithTimeout(ctx, SessionRefreshInterval)
|
||||
err := c.client.RefreshSession(reqCtx)
|
||||
reqCtxCancel()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("cluster", c.client.Cluster.Name).Msg("[proxmox] failed to refresh session")
|
||||
// exponential backoff
|
||||
numRetries++
|
||||
backoff := time.Duration(min(math.Pow(2, float64(numRetries)), 10)) * time.Second
|
||||
ticker.Reset(backoff)
|
||||
} else {
|
||||
ticker.Reset(SessionRefreshInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,15 +39,30 @@ func (n *Node) LXCCommand(ctx context.Context, vmid int, command string) (io.Rea
|
||||
|
||||
// LXCJournalctl streams journalctl output for the given service.
|
||||
//
|
||||
// If service is not empty, it will be used to filter the output by service.
|
||||
// On non systemd systems, it will tail /var/log/messages as fallback.
|
||||
//
|
||||
// If services are not empty, it will be used to filter the output by service.
|
||||
// If limit is greater than 0, it will be used to limit the number of lines of output.
|
||||
func (n *Node) LXCJournalctl(ctx context.Context, vmid int, service string, limit int) (io.ReadCloser, error) {
|
||||
command := "journalctl -f"
|
||||
if service != "" {
|
||||
command = fmt.Sprintf("journalctl -u %q -f", service)
|
||||
func (n *Node) LXCJournalctl(ctx context.Context, vmid int, services []string, limit int) (io.ReadCloser, error) {
|
||||
command, err := formatJournalctl(services, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if limit > 0 {
|
||||
command = fmt.Sprintf("%s -n %d", command, limit)
|
||||
if len(services) == 0 {
|
||||
// add /var/log/messages fallback for non systemd systems
|
||||
// in tail command, try --retry first, if it fails, try the command again
|
||||
command = fmt.Sprintf("sh -c '%s 2>/dev/null || tail -f -q --retry /var/log/messages 2>/dev/null || tail -f -q /var/log/messages'", command)
|
||||
}
|
||||
return n.LXCCommand(ctx, vmid, command)
|
||||
}
|
||||
|
||||
// LXCTail streams tail output for the given file.
|
||||
//
|
||||
// If limit is greater than 0, it will be used to limit the number of lines of output.
|
||||
func (n *Node) LXCTail(ctx context.Context, vmid int, files []string, limit int) (io.ReadCloser, error) {
|
||||
command, err := formatTail(files, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return n.LXCCommand(ctx, vmid, command)
|
||||
}
|
||||
|
||||
@@ -2,18 +2,20 @@ package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
"github.com/yusing/goutils/pool"
|
||||
)
|
||||
|
||||
type NodeConfig struct {
|
||||
Node string `json:"node" validate:"required"`
|
||||
VMID int `json:"vmid" validate:"required"`
|
||||
VMName string `json:"vmname,omitempty"`
|
||||
Service string `json:"service,omitempty"`
|
||||
Node string `json:"node"`
|
||||
VMID *int `json:"vmid"` // unset: auto discover; explicit 0: node-level route; >0: lxc/qemu resource route
|
||||
VMName string `json:"vmname,omitempty"`
|
||||
Services []string `json:"services,omitempty" aliases:"service"`
|
||||
Files []string `json:"files,omitempty" aliases:"file"`
|
||||
} // @name ProxmoxNodeConfig
|
||||
|
||||
type Node struct {
|
||||
@@ -24,6 +26,22 @@ type Node struct {
|
||||
// statsScriptInitErrs *xsync.Map[int, error]
|
||||
}
|
||||
|
||||
// Validate implements the serialization.CustomValidator interface.
|
||||
func (n *NodeConfig) Validate() gperr.Error {
|
||||
var errs gperr.Builder
|
||||
for i, service := range n.Services {
|
||||
if err := checkValidInput(service); err != nil {
|
||||
errs.AddSubjectf(err, "services[%d]", i)
|
||||
}
|
||||
}
|
||||
for i, file := range n.Files {
|
||||
if err := checkValidInput(file); err != nil {
|
||||
errs.AddSubjectf(err, "files[%d]", i)
|
||||
}
|
||||
}
|
||||
return errs.Error()
|
||||
}
|
||||
|
||||
var Nodes = pool.New[*Node]("proxmox_nodes")
|
||||
|
||||
func NewNode(client *Client, name, id string) *Node {
|
||||
@@ -64,7 +82,7 @@ func (n *Node) String() string {
|
||||
}
|
||||
|
||||
func (n *Node) MarshalJSON() ([]byte, error) {
|
||||
return sonic.Marshal(map[string]any{
|
||||
return json.Marshal(map[string]any{
|
||||
"name": n.name,
|
||||
"id": n.id,
|
||||
})
|
||||
|
||||
@@ -50,6 +50,7 @@ func (n *Node) NodeCommand(ctx context.Context, command string) (io.ReadCloser,
|
||||
// Send command
|
||||
cmd := []byte(command + "\n")
|
||||
if err := handleSend(cmd); err != nil {
|
||||
closeFn()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -70,6 +71,7 @@ func (n *Node) NodeCommand(ctx context.Context, command string) (io.ReadCloser,
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = pw.CloseWithError(ctx.Err())
|
||||
return
|
||||
case msg := <-recv:
|
||||
// skip the header message like
|
||||
@@ -106,7 +108,6 @@ func (n *Node) NodeCommand(ctx context.Context, command string) (io.ReadCloser,
|
||||
case err := <-errs:
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
_ = pw.Close()
|
||||
return
|
||||
}
|
||||
_ = pw.CloseWithError(err)
|
||||
@@ -119,13 +120,25 @@ func (n *Node) NodeCommand(ctx context.Context, command string) (io.ReadCloser,
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
func (n *Node) NodeJournalctl(ctx context.Context, service string, limit int) (io.ReadCloser, error) {
|
||||
command := "journalctl -f"
|
||||
if service != "" {
|
||||
command = fmt.Sprintf("journalctl -u %q -f", service)
|
||||
}
|
||||
if limit > 0 {
|
||||
command = fmt.Sprintf("%s -n %d", command, limit)
|
||||
// NodeJournalctl streams journalctl output for the given service.
|
||||
//
|
||||
// If services are not empty, it will be used to filter the output by services.
|
||||
// If limit is greater than 0, it will be used to limit the number of lines of output.
|
||||
func (n *Node) NodeJournalctl(ctx context.Context, services []string, limit int) (io.ReadCloser, error) {
|
||||
command, err := formatJournalctl(services, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return n.NodeCommand(ctx, command)
|
||||
}
|
||||
|
||||
// NodeTail streams tail output for the given file.
|
||||
//
|
||||
// If limit is greater than 0, it will be used to limit the number of lines of output.
|
||||
func (n *Node) NodeTail(ctx context.Context, files []string, limit int) (io.ReadCloser, error) {
|
||||
command, err := formatTail(files, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return n.NodeCommand(ctx, command)
|
||||
}
|
||||
|
||||
56
internal/proxmox/validation_test.go
Normal file
56
internal/proxmox/validation_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
)
|
||||
|
||||
func TestValidateCommandArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
yamlCfg string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid_services",
|
||||
yamlCfg: `services: ["foo", "bar"]`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid_services",
|
||||
yamlCfg: `services: ["foo", "bar & baz"]`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_services_with_$(",
|
||||
yamlCfg: `services: ["foo", "bar & $(echo 'hello')"]`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid_files",
|
||||
yamlCfg: `files: ["foo", "bar"]`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid_files",
|
||||
yamlCfg: `files: ["foo", "bar & baz"]`,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var cfg NodeConfig
|
||||
err := serialization.UnmarshalValidate([]byte(tt.yamlCfg), &cfg, yaml.Unmarshal)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "input contains invalid characters")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ package provider
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"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"
|
||||
@@ -26,7 +26,7 @@ func TestParseDockerLabels(t *testing.T) {
|
||||
Names: []string{"container"},
|
||||
Labels: labels,
|
||||
State: "running",
|
||||
Ports: []container.PortSummary{
|
||||
Ports: []container.Port{
|
||||
{Type: "tcp", PrivatePort: 1234, PublicPort: 1234},
|
||||
},
|
||||
}, types.DockerProviderConfig{URL: "unix:///var/run/docker.sock"}),
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
D "github.com/yusing/godoxy/internal/docker"
|
||||
"github.com/yusing/godoxy/internal/route"
|
||||
routeTypes "github.com/yusing/godoxy/internal/route/types"
|
||||
@@ -276,7 +275,7 @@ func TestPrivateIPLocalhost(t *testing.T) {
|
||||
NetworkSettings: &container.NetworkSettingsSummary{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"network": {
|
||||
IPAddress: netip.MustParseAddr(testDockerIP),
|
||||
IPAddress: testDockerIP,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -294,7 +293,7 @@ func TestPrivateIPRemote(t *testing.T) {
|
||||
NetworkSettings: &container.NetworkSettingsSummary{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"network": {
|
||||
IPAddress: netip.MustParseAddr(testDockerIP),
|
||||
IPAddress: testDockerIP,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -316,11 +315,11 @@ func TestStreamDefaultValues(t *testing.T) {
|
||||
NetworkSettings: &container.NetworkSettingsSummary{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"network": {
|
||||
IPAddress: netip.MustParseAddr(privIP),
|
||||
IPAddress: privIP,
|
||||
},
|
||||
},
|
||||
},
|
||||
Ports: []container.PortSummary{
|
||||
Ports: []container.Port{
|
||||
{Type: "udp", PrivatePort: privPort, PublicPort: pubPort},
|
||||
},
|
||||
}
|
||||
@@ -373,7 +372,7 @@ func TestImplicitExcludeDatabase(t *testing.T) {
|
||||
t.Run("exposed port detection", func(t *testing.T) {
|
||||
r, ok := makeRoutes(&container.Summary{
|
||||
Names: dummyNames,
|
||||
Ports: []container.PortSummary{
|
||||
Ports: []container.Port{
|
||||
{Type: "tcp", PrivatePort: 5432, PublicPort: 5432},
|
||||
},
|
||||
})["a"]
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
@@ -43,7 +44,7 @@ func removeXPrefix(m map[string]any) gperr.Error {
|
||||
}
|
||||
|
||||
func validate(data []byte) (routes route.Routes, err gperr.Error) {
|
||||
err = serialization.UnmarshalValidateYAMLIntercept(data, &routes, removeXPrefix)
|
||||
err = serialization.UnmarshalValidate(data, &routes, yaml.Unmarshal, removeXPrefix)
|
||||
return routes, err
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ type (
|
||||
Idlewatcher *types.IdlewatcherConfig `json:"idlewatcher,omitempty" extensions:"x-nullable"`
|
||||
|
||||
Metadata `deserialize:"-"`
|
||||
}
|
||||
} // @name Route
|
||||
|
||||
Metadata struct {
|
||||
/* Docker only */
|
||||
@@ -185,87 +185,68 @@ func (r *Route) validate() gperr.Error {
|
||||
if r.Proxmox != nil && r.Idlewatcher != nil {
|
||||
r.Idlewatcher.Proxmox = &types.ProxmoxConfig{
|
||||
Node: r.Proxmox.Node,
|
||||
VMID: r.Proxmox.VMID,
|
||||
}
|
||||
if r.Proxmox.VMID != nil {
|
||||
r.Idlewatcher.Proxmox.VMID = *r.Proxmox.VMID
|
||||
}
|
||||
}
|
||||
|
||||
if r.Proxmox == nil && r.Idlewatcher != nil && r.Idlewatcher.Proxmox != nil {
|
||||
r.Proxmox = &proxmox.NodeConfig{
|
||||
Node: r.Idlewatcher.Proxmox.Node,
|
||||
VMID: r.Idlewatcher.Proxmox.VMID,
|
||||
VMID: &r.Idlewatcher.Proxmox.VMID,
|
||||
}
|
||||
}
|
||||
|
||||
if (r.Proxmox == nil || r.Proxmox.Node == "" || r.Proxmox.VMID == nil) && r.Container == nil {
|
||||
wasNotNil := r.Proxmox != nil
|
||||
proxmoxProviders := config.WorkingState.Load().Value().Providers.Proxmox
|
||||
if len(proxmoxProviders) > 0 {
|
||||
// it's fine if ip is nil
|
||||
hostname := r.Host
|
||||
ip := net.ParseIP(hostname)
|
||||
for _, p := range proxmoxProviders {
|
||||
// First check if hostname, IP, or alias matches a node (node-level route)
|
||||
if nodeName := p.Client().ReverseLookupNode(hostname, ip, r.Alias); nodeName != "" {
|
||||
zero := 0
|
||||
if r.Proxmox == nil {
|
||||
r.Proxmox = &proxmox.NodeConfig{}
|
||||
}
|
||||
r.Proxmox.Node = nodeName
|
||||
r.Proxmox.VMID = &zero
|
||||
r.Proxmox.VMName = ""
|
||||
log.Info().
|
||||
Str("node", nodeName).
|
||||
Msgf("found proxmox node for route %q", r.Alias)
|
||||
break
|
||||
}
|
||||
|
||||
// Then check if hostname, IP, or alias matches a VM resource
|
||||
resource, _ := p.Client().ReverseLookupResource(ip, hostname, r.Alias)
|
||||
if resource != nil {
|
||||
vmid := int(resource.VMID)
|
||||
if r.Proxmox == nil {
|
||||
r.Proxmox = &proxmox.NodeConfig{}
|
||||
}
|
||||
r.Proxmox.Node = resource.Node
|
||||
r.Proxmox.VMID = &vmid
|
||||
r.Proxmox.VMName = resource.Name
|
||||
log.Info().
|
||||
Str("node", resource.Node).
|
||||
Int("vmid", int(resource.VMID)).
|
||||
Str("vmname", resource.Name).
|
||||
Msgf("found proxmox resource for route %q", r.Alias)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if wasNotNil && (r.Proxmox.Node == "" || r.Proxmox.VMID == nil) {
|
||||
log.Warn().Msgf("no proxmox node / resource found for route %q", r.Alias)
|
||||
}
|
||||
}
|
||||
|
||||
if r.Proxmox != nil {
|
||||
nodeName := r.Proxmox.Node
|
||||
vmid := r.Proxmox.VMID
|
||||
if nodeName == "" {
|
||||
return gperr.Errorf("node (proxmox node name) is required")
|
||||
}
|
||||
|
||||
node, ok := proxmox.Nodes.Get(nodeName)
|
||||
if !ok {
|
||||
return gperr.Errorf("proxmox node %s not found in pool", nodeName)
|
||||
}
|
||||
|
||||
// Node-level route (VMID = 0) - no container control needed
|
||||
if vmid > 0 {
|
||||
res, err := node.Client().GetResource("lxc", vmid)
|
||||
if err != nil {
|
||||
return gperr.Wrap(err) // ErrResourceNotFound
|
||||
}
|
||||
|
||||
r.Proxmox.VMName = res.Name
|
||||
|
||||
if r.Host == DefaultHost {
|
||||
containerName := r.Idlewatcher.ContainerName()
|
||||
// get ip addresses of the vmid
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
ips := res.IPs
|
||||
if len(ips) == 0 {
|
||||
return gperr.Multiline().
|
||||
Addf("no ip addresses found for %s", containerName).
|
||||
Adds("make sure you have set static ip address for container instead of dhcp").
|
||||
Subject(containerName)
|
||||
}
|
||||
|
||||
l := log.With().Str("container", containerName).Logger()
|
||||
|
||||
l.Info().Msg("checking if container is running")
|
||||
running, err := node.LXCIsRunning(ctx, vmid)
|
||||
if err != nil {
|
||||
return gperr.New("failed to check container state").With(err)
|
||||
}
|
||||
|
||||
if !running {
|
||||
l.Info().Msg("starting container")
|
||||
if err := node.LXCAction(ctx, vmid, proxmox.LXCStart); err != nil {
|
||||
return gperr.New("failed to start container").With(err)
|
||||
}
|
||||
}
|
||||
|
||||
l.Info().Msgf("finding reachable ip addresses")
|
||||
errs := gperr.NewBuilder("failed to find reachable ip addresses")
|
||||
for _, ip := range ips {
|
||||
if err := netutils.PingTCP(ctx, ip, r.Port.Proxy); err != nil {
|
||||
errs.Add(gperr.Unwrap(err).Subjectf("%s:%d", ip, r.Port.Proxy))
|
||||
} else {
|
||||
r.Host = ip.String()
|
||||
l.Info().Msgf("using ip %s", r.Host)
|
||||
break
|
||||
}
|
||||
}
|
||||
if r.Host == DefaultHost {
|
||||
return gperr.Multiline().
|
||||
Addf("no reachable ip addresses found, tried %d IPs", len(ips)).
|
||||
With(errs.Error()).
|
||||
Subject(containerName)
|
||||
}
|
||||
}
|
||||
}
|
||||
r.validateProxmox()
|
||||
}
|
||||
|
||||
if r.Container != nil && r.Container.IdlewatcherConfig != nil {
|
||||
@@ -336,45 +317,6 @@ func (r *Route) validate() gperr.Error {
|
||||
}
|
||||
}
|
||||
|
||||
if r.Proxmox == nil && r.Container == nil {
|
||||
proxmoxProviders := config.WorkingState.Load().Value().Providers.Proxmox
|
||||
if len(proxmoxProviders) > 0 {
|
||||
// it's fine if ip is nil
|
||||
hostname := r.ProxyURL.Hostname()
|
||||
ip := net.ParseIP(hostname)
|
||||
for _, p := range config.WorkingState.Load().Value().Providers.Proxmox {
|
||||
// First check if hostname, IP, or alias matches a node (node-level route)
|
||||
if nodeName := p.Client().ReverseLookupNode(hostname, ip, r.Alias); nodeName != "" {
|
||||
r.Proxmox = &proxmox.NodeConfig{
|
||||
Node: nodeName,
|
||||
VMID: 0, // node-level route, no specific VM
|
||||
VMName: "",
|
||||
}
|
||||
log.Info().
|
||||
Str("node", nodeName).
|
||||
Msgf("found proxmox node for route %q", r.Alias)
|
||||
break
|
||||
}
|
||||
|
||||
// Then check if hostname, IP, or alias matches a VM resource
|
||||
resource, _ := p.Client().ReverseLookupResource(ip, hostname, r.Alias)
|
||||
if resource != nil {
|
||||
r.Proxmox = &proxmox.NodeConfig{
|
||||
Node: resource.Node,
|
||||
VMID: int(resource.VMID),
|
||||
VMName: resource.Name,
|
||||
}
|
||||
log.Info().
|
||||
Str("node", resource.Node).
|
||||
Int("vmid", int(resource.VMID)).
|
||||
Str("vmname", resource.Name).
|
||||
Msgf("found proxmox resource for route %q", r.Alias)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !r.UseHealthCheck() && (r.UseLoadBalance() || r.UseIdleWatcher()) {
|
||||
errs.Adds("cannot disable healthcheck when loadbalancer or idle watcher is enabled")
|
||||
}
|
||||
@@ -454,6 +396,90 @@ func (r *Route) validateRules() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Route) validateProxmox() {
|
||||
l := log.With().Str("route", r.Alias).Logger()
|
||||
|
||||
nodeName := r.Proxmox.Node
|
||||
vmid := r.Proxmox.VMID
|
||||
if nodeName == "" || vmid == nil {
|
||||
l.Error().Msg("node (proxmox node name) is required")
|
||||
return
|
||||
}
|
||||
|
||||
node, ok := proxmox.Nodes.Get(nodeName)
|
||||
if !ok {
|
||||
l.Error().Msgf("proxmox node %s not found in pool", nodeName)
|
||||
return
|
||||
}
|
||||
|
||||
// Node-level route (VMID = 0)
|
||||
if *vmid == 0 {
|
||||
r.Scheme = route.SchemeHTTPS
|
||||
if r.Host == DefaultHost {
|
||||
r.Host = node.Client().BaseURL.Hostname()
|
||||
}
|
||||
port, _ := strconv.Atoi(node.Client().BaseURL.Port())
|
||||
if port == 0 {
|
||||
port = 8006
|
||||
}
|
||||
r.Port.Proxy = port
|
||||
} else {
|
||||
res, err := node.Client().GetResource("lxc", *vmid)
|
||||
if err != nil { // ErrResourceNotFound
|
||||
l.Err(err).Msgf("failed to get resource %d", *vmid)
|
||||
return
|
||||
}
|
||||
|
||||
r.Proxmox.VMName = res.Name
|
||||
|
||||
if r.Host == DefaultHost {
|
||||
containerName := res.Name
|
||||
// get ip addresses of the vmid
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
ips := res.IPs
|
||||
if len(ips) == 0 {
|
||||
l.Warn().Msgf("no ip addresses found for %s, make sure you have set static ip address for container instead of dhcp", containerName)
|
||||
return
|
||||
}
|
||||
|
||||
l = l.With().Str("container", containerName).Logger()
|
||||
|
||||
l.Info().Msgf("checking if container is running")
|
||||
running, err := node.LXCIsRunning(ctx, *vmid)
|
||||
if err != nil {
|
||||
l.Err(err).Msgf("failed to check container state")
|
||||
return
|
||||
}
|
||||
|
||||
if !running {
|
||||
l.Info().Msgf("starting container")
|
||||
if err := node.LXCAction(ctx, *vmid, proxmox.LXCStart); err != nil {
|
||||
l.Err(err).Msgf("failed to start container")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
l.Info().Msgf("finding reachable ip addresses")
|
||||
errs := gperr.NewBuilder("failed to find reachable ip addresses")
|
||||
for _, ip := range ips {
|
||||
if err := netutils.PingTCP(ctx, ip, r.Port.Proxy); err != nil {
|
||||
errs.Add(gperr.Unwrap(err).Subjectf("%s:%d", ip, r.Port.Proxy))
|
||||
} else {
|
||||
r.Host = ip.String()
|
||||
l.Info().Msgf("using ip %s", r.Host)
|
||||
break
|
||||
}
|
||||
}
|
||||
if r.Host == DefaultHost {
|
||||
l.Warn().Err(errs.Error()).Msgf("no reachable ip addresses found, tried %d IPs", len(ips))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Route) Impl() types.Route {
|
||||
return r.impl
|
||||
}
|
||||
@@ -556,11 +582,11 @@ func (r *Route) References() []string {
|
||||
}
|
||||
|
||||
if r.Proxmox != nil {
|
||||
if r.Proxmox.Service != "" && r.Proxmox.Service != aliasRef {
|
||||
if len(r.Proxmox.Services) > 0 && r.Proxmox.Services[0] != aliasRef {
|
||||
if r.Proxmox.VMName != aliasRef {
|
||||
return []string{r.Proxmox.VMName, aliasRef, r.Proxmox.Service}
|
||||
return []string{r.Proxmox.VMName, aliasRef, r.Proxmox.Services[0]}
|
||||
}
|
||||
return []string{r.Proxmox.Service, aliasRef}
|
||||
return []string{r.Proxmox.Services[0], aliasRef}
|
||||
} else {
|
||||
if r.Proxmox.VMName != aliasRef {
|
||||
return []string{r.Proxmox.VMName, aliasRef}
|
||||
|
||||
@@ -79,6 +79,10 @@ var commands = map[string]struct {
|
||||
},
|
||||
build: func(args any) CommandHandler {
|
||||
return NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||
if authHandler == nil {
|
||||
http.Error(w, "Auth handler not initialized", http.StatusInternalServerError)
|
||||
return errTerminated
|
||||
}
|
||||
if !authHandler(w, r) {
|
||||
return errTerminated
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ func (rules Rules) BuildHandler(up http.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
|
||||
func appendRuleError(rm *httputils.ResponseModifier, rule *Rule, err error) {
|
||||
rm.AppendError("rule: %s, error: %w", rule.Name, err)
|
||||
// rm.AppendError("rule: %s, error: %w", rule.Name, err)
|
||||
}
|
||||
|
||||
func isTerminatingHandler(handler CommandHandler) bool {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
)
|
||||
|
||||
@@ -57,7 +57,7 @@ func (s Scheme) MarshalJSON() ([]byte, error) {
|
||||
|
||||
func (s *Scheme) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := sonic.Unmarshal(data, &v); err != nil {
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Parse(v)
|
||||
|
||||
@@ -11,7 +11,7 @@ This package provides robust YAML/JSON serialization with:
|
||||
- Case-insensitive field matching using FNV-1a hashing
|
||||
- Environment variable substitution (`${VAR}` syntax)
|
||||
- Field-level validation with go-playground/validator tags
|
||||
- Custom type conversion with alias support
|
||||
- Custom type conversion with pluggable format handlers
|
||||
|
||||
### Primary Consumers
|
||||
|
||||
@@ -55,21 +55,27 @@ type CustomValidator interface {
|
||||
### Deserialization Functions
|
||||
|
||||
```go
|
||||
// YAML with full validation
|
||||
func UnmarshalValidateYAML[T any](data []byte, target *T) gperr.Error
|
||||
// Generic unmarshal with pluggable format handler
|
||||
func UnmarshalValidate[T any](data []byte, target *T, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) gperr.Error
|
||||
|
||||
// YAML with interceptor for preprocessing
|
||||
func UnmarshalValidateYAMLIntercept[T any](
|
||||
data []byte,
|
||||
target *T,
|
||||
intercept func(m map[string]any) gperr.Error,
|
||||
) gperr.Error
|
||||
// Read from io.Reader with format decoder
|
||||
func UnmarshalValidateReader[T any](reader io.Reader, target *T, newDecoder newDecoderFunc, interceptFns ...interceptFunc) gperr.Error
|
||||
|
||||
// Direct map deserialization
|
||||
func MapUnmarshalValidate(src SerializedObject, dst any) gperr.Error
|
||||
|
||||
// To xsync.Map
|
||||
func UnmarshalValidateYAMLXSync[V any](data []byte) (*xsync.Map[string, V], gperr.Error)
|
||||
// To xsync.Map with pluggable format handler
|
||||
func UnmarshalValidateXSync[V any](data []byte, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) (*xsync.Map[string, V], gperr.Error)
|
||||
```
|
||||
|
||||
### File I/O Functions
|
||||
|
||||
```go
|
||||
// Write marshaled data to file
|
||||
func SaveFile[T any](path string, src *T, perm os.FileMode, marshaler marshalFunc) error
|
||||
|
||||
// Read and unmarshal file if it exists
|
||||
func LoadFileIfExist[T any](path string, dst *T, unmarshaler unmarshalFunc) error
|
||||
```
|
||||
|
||||
### Conversion Functions
|
||||
@@ -115,19 +121,19 @@ func ToSerializedObject[VT any](m map[string]VT) SerializedObject
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Caller
|
||||
participant U as UnmarshalValidateYAML
|
||||
participant U as UnmarshalValidate
|
||||
participant E as Env Substitution
|
||||
participant Y as YAML Parser
|
||||
participant F as Format Parser
|
||||
participant M as MapUnmarshalValidate
|
||||
participant T as Type Info Cache
|
||||
participant CV as Convert
|
||||
participant V as Validator
|
||||
|
||||
C->>U: YAML bytes + target struct
|
||||
C->>U: Data bytes + target struct + format handler
|
||||
U->>E: Substitute ${ENV} vars
|
||||
E-->>U: Substituted bytes
|
||||
U->>Y: Parse YAML
|
||||
Y-->>U: map[string]any
|
||||
U->>F: Parse with format handler (YAML/JSON)
|
||||
F-->>U: map[string]any
|
||||
U->>M: Map + target
|
||||
M->>T: Get type info
|
||||
loop For each field in map
|
||||
@@ -147,9 +153,9 @@ sequenceDiagram
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Input Processing
|
||||
YAML[YAML Bytes] --> EnvSub[Env Substitution]
|
||||
EnvSub --> YAMLParse[YAML Parse]
|
||||
YAMLParse --> Map[map<string,any>]
|
||||
Bytes[Data Bytes] --> EnvSub[Env Substitution]
|
||||
EnvSub --> FormatParse[Format Parse]
|
||||
FormatParse --> Map[map<string,any>]
|
||||
end
|
||||
|
||||
subgraph Type Inspection
|
||||
@@ -221,6 +227,7 @@ autocert:
|
||||
### Internal Dependencies
|
||||
|
||||
- `github.com/yusing/goutils/errs` - Error handling
|
||||
- `github.com/yusing/gointernals` - Reflection utilities
|
||||
|
||||
## Observability
|
||||
|
||||
@@ -251,11 +258,11 @@ ErrUnsupportedConversion.Subjectf("string to int")
|
||||
| Validation failure | Structured error | Fix field value |
|
||||
| Type mismatch | Error | Check field type |
|
||||
| Missing env var | Error | Set environment variable |
|
||||
| Invalid YAML | Error | Fix YAML syntax |
|
||||
| Invalid format | Error | Fix YAML/JSON syntax |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Struct Deserialization
|
||||
### YAML Deserialization
|
||||
|
||||
```go
|
||||
type ServerConfig struct {
|
||||
@@ -273,7 +280,16 @@ tls_enabled: true
|
||||
`)
|
||||
|
||||
var config ServerConfig
|
||||
if err := serialization.UnmarshalValidateYAML(yamlData, &config); err != nil {
|
||||
if err := serialization.UnmarshalValidate(yamlData, &config, yaml.Unmarshal); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
### JSON Deserialization
|
||||
|
||||
```go
|
||||
var config ServerConfig
|
||||
if err := serialization.UnmarshalValidate(jsonData, &config, json.Unmarshal); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
@@ -293,7 +309,7 @@ func (c *Config) Validate() gperr.Error {
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Type with Parse Method
|
||||
### Custom Type with Parser Interface
|
||||
|
||||
```go
|
||||
type Duration struct {
|
||||
@@ -307,6 +323,31 @@ func (d *Duration) Parse(v string) error {
|
||||
}
|
||||
```
|
||||
|
||||
### Reading from File
|
||||
|
||||
```go
|
||||
var config ServerConfig
|
||||
if err := serialization.LoadFileIfExist("config.yml", &config, yaml.Unmarshal); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Save back to file
|
||||
if err := serialization.SaveFile("config.yml", &config, 0644, yaml.Marshal); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
### Reading from io.Reader
|
||||
|
||||
```go
|
||||
var config ServerConfig
|
||||
file, _ := os.Open("config.yml")
|
||||
defer file.Close()
|
||||
if err := serialization.UnmarshalValidateReader(file, &config, yaml.NewDecoder); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Notes
|
||||
|
||||
- `serialization_test.go` - Core functionality tests
|
||||
@@ -319,3 +360,4 @@ func (d *Duration) Parse(v string) error {
|
||||
- String conversions
|
||||
- Environment substitution
|
||||
- Custom validators
|
||||
- Multiple format handlers (YAML/JSON)
|
||||
|
||||
37
internal/serialization/gin_binding.go
Normal file
37
internal/serialization/gin_binding.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package serialization
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
type (
|
||||
GinJSONBinding struct{}
|
||||
GinYAMLBinding struct{}
|
||||
)
|
||||
|
||||
func (b GinJSONBinding) Name() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
func (b GinJSONBinding) Bind(req *http.Request, obj any) error {
|
||||
m := make(map[string]any)
|
||||
if err := json.NewDecoder(NewSubstituteEnvReader(req.Body)).Decode(&m); err != nil {
|
||||
return err
|
||||
}
|
||||
return MapUnmarshalValidate(m, obj)
|
||||
}
|
||||
|
||||
func (b GinYAMLBinding) Name() string {
|
||||
return "yaml"
|
||||
}
|
||||
|
||||
func (b GinYAMLBinding) Bind(req *http.Request, obj any) error {
|
||||
m := make(map[string]any)
|
||||
if err := yaml.NewDecoder(NewSubstituteEnvReader(req.Body)).Decode(&m); err != nil {
|
||||
return err
|
||||
}
|
||||
return MapUnmarshalValidate(m, obj)
|
||||
}
|
||||
50
internal/serialization/gin_binding_test.go
Normal file
50
internal/serialization/gin_binding_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package serialization_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
)
|
||||
|
||||
type TestStruct struct {
|
||||
Value string `json:"value"`
|
||||
Value2 int `json:"value2"`
|
||||
}
|
||||
|
||||
func (t *TestStruct) Validate() gperr.Error {
|
||||
if t.Value == "" {
|
||||
return gperr.New("value is required")
|
||||
}
|
||||
if t.Value2 != 0 && (t.Value2 < 5 || t.Value2 > 10) {
|
||||
return gperr.New("value2 must be between 5 and 10")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestGinBinding(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid1", `{"value": "test", "value2": 7}`, false},
|
||||
{"valid2", `{"value": "test"}`, false},
|
||||
{"invalid1", `{"value2": 7}`, true},
|
||||
{"invalid2", `{"value": "test", "value2": 3}`, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var dst TestStruct
|
||||
body := bytes.NewBufferString(tt.input)
|
||||
req := httptest.NewRequest("POST", "/", body)
|
||||
err := serialization.GinJSONBinding{}.Bind(req, &dst)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("%s: Bind() error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
146
internal/serialization/reader.go
Normal file
146
internal/serialization/reader.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package serialization
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
type SubstituteEnvReader struct {
|
||||
reader io.Reader
|
||||
buf []byte // buffered data with substitutions applied
|
||||
err error // sticky error
|
||||
}
|
||||
|
||||
func NewSubstituteEnvReader(reader io.Reader) *SubstituteEnvReader {
|
||||
return &SubstituteEnvReader{reader: reader}
|
||||
}
|
||||
|
||||
const peekSize = 4096
|
||||
const maxVarNameLength = 256
|
||||
|
||||
func (r *SubstituteEnvReader) Read(p []byte) (n int, err error) {
|
||||
// Return buffered data first
|
||||
if len(r.buf) > 0 {
|
||||
n = copy(p, r.buf)
|
||||
r.buf = r.buf[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Return sticky error if we have one
|
||||
if r.err != nil {
|
||||
return 0, r.err
|
||||
}
|
||||
|
||||
var buf [2 * peekSize]byte
|
||||
|
||||
// Read a chunk from the underlying reader
|
||||
chunk, more := buf[:peekSize], buf[peekSize:]
|
||||
nRead, readErr := r.reader.Read(chunk)
|
||||
if nRead == 0 {
|
||||
if readErr != nil {
|
||||
return 0, readErr
|
||||
}
|
||||
return 0, io.EOF
|
||||
}
|
||||
chunk = chunk[:nRead]
|
||||
|
||||
// Check if there's a potential incomplete pattern at the end
|
||||
// Pattern: ${VAR_NAME}
|
||||
// We need to check if chunk ends with a partial pattern like "$", "${", "${VAR", etc.
|
||||
incompleteStart := findIncompletePatternStart(chunk)
|
||||
|
||||
if incompleteStart >= 0 && readErr == nil {
|
||||
// There might be an incomplete pattern, read more to complete it
|
||||
incomplete := chunk[incompleteStart:]
|
||||
chunk = chunk[:incompleteStart]
|
||||
|
||||
// Keep reading until we complete the pattern or hit EOF/error
|
||||
for {
|
||||
// Limit how much we buffer to prevent memory exhaustion
|
||||
if len(incomplete) > maxVarNameLength+3 { // ${} + var name
|
||||
// Pattern too long to be valid, give up and process as-is
|
||||
chunk = append(chunk, incomplete...)
|
||||
break
|
||||
}
|
||||
nMore, moreErr := r.reader.Read(more)
|
||||
if nMore > 0 {
|
||||
incomplete = append(incomplete, more[:nMore]...)
|
||||
// Check if pattern is now complete
|
||||
if idx := bytes.IndexByte(incomplete, '}'); idx >= 0 {
|
||||
// Pattern complete, append the rest back to chunk
|
||||
chunk = append(chunk, incomplete...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if moreErr != nil {
|
||||
// No more data, append whatever we have
|
||||
chunk = append(chunk, incomplete...)
|
||||
readErr = moreErr
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
substituted, subErr := substituteEnv(chunk)
|
||||
if subErr != nil {
|
||||
r.err = subErr
|
||||
return 0, subErr
|
||||
}
|
||||
|
||||
n = copy(p, substituted)
|
||||
if n < len(substituted) {
|
||||
// Buffer the rest
|
||||
r.buf = substituted[n:]
|
||||
}
|
||||
|
||||
// Store sticky error for next read
|
||||
if readErr != nil && readErr != io.EOF {
|
||||
r.err = readErr
|
||||
} else {
|
||||
if readErr == io.EOF && n > 0 {
|
||||
return n, nil
|
||||
}
|
||||
if readErr == io.EOF {
|
||||
return n, io.EOF
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// findIncompletePatternStart returns the index where an incomplete ${...} pattern starts,
|
||||
// or -1 if there's no incomplete pattern at the end.
|
||||
func findIncompletePatternStart(data []byte) int {
|
||||
// Look for '$' near the end that might be start of ${VAR}
|
||||
// Maximum var name we reasonably expect + "${}" = ~256 chars
|
||||
searchStart := max(0, len(data)-maxVarNameLength)
|
||||
|
||||
for i := len(data) - 1; i >= searchStart; i-- {
|
||||
if data[i] == '$' {
|
||||
// Check if this is a complete pattern or incomplete
|
||||
if i+1 >= len(data) {
|
||||
// Just "$" at end
|
||||
return i
|
||||
}
|
||||
if data[i+1] == '{' {
|
||||
// Check if there's anything after "${"
|
||||
if i+2 >= len(data) {
|
||||
// Just "${" at end
|
||||
return i
|
||||
}
|
||||
// Check if pattern is complete by looking for '}'
|
||||
for j := i + 2; j < len(data); j++ {
|
||||
if data[j] == '}' {
|
||||
// This pattern is complete, continue searching for another
|
||||
break
|
||||
}
|
||||
if j == len(data)-1 {
|
||||
// Reached end without finding '}', incomplete pattern
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
286
internal/serialization/reader_bench_test.go
Normal file
286
internal/serialization/reader_bench_test.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package serialization
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// setupEnv sets up environment variables for benchmarks
|
||||
func setupEnv(b *testing.B) {
|
||||
b.Helper()
|
||||
os.Setenv("BENCH_VAR", "benchmark_value")
|
||||
os.Setenv("BENCH_VAR_2", "second_value")
|
||||
os.Setenv("BENCH_VAR_3", "third_value")
|
||||
}
|
||||
|
||||
// cleanupEnv cleans up environment variables after benchmarks
|
||||
func cleanupEnv(b *testing.B) {
|
||||
b.Helper()
|
||||
os.Unsetenv("BENCH_VAR")
|
||||
os.Unsetenv("BENCH_VAR_2")
|
||||
os.Unsetenv("BENCH_VAR_3")
|
||||
}
|
||||
|
||||
// BenchmarkSubstituteEnvReader_NoSubstitution benchmarks reading without any env substitutions
|
||||
func BenchmarkSubstituteEnvReader_NoSubstitution(b *testing.B) {
|
||||
r := strings.NewReader(`key: value
|
||||
name: test
|
||||
data: some content here
|
||||
`)
|
||||
|
||||
for b.Loop() {
|
||||
reader := NewSubstituteEnvReader(r)
|
||||
_, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
r.Seek(0, io.SeekStart)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSubstituteEnvReader_SingleSubstitution benchmarks reading with a single env substitution
|
||||
func BenchmarkSubstituteEnvReader_SingleSubstitution(b *testing.B) {
|
||||
setupEnv(b)
|
||||
defer cleanupEnv(b)
|
||||
|
||||
r := strings.NewReader(`key: ${BENCH_VAR}
|
||||
`)
|
||||
|
||||
for b.Loop() {
|
||||
reader := NewSubstituteEnvReader(r)
|
||||
_, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
r.Seek(0, io.SeekStart)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSubstituteEnvReader_MultipleSubstitutions benchmarks reading with multiple env substitutions
|
||||
func BenchmarkSubstituteEnvReader_MultipleSubstitutions(b *testing.B) {
|
||||
setupEnv(b)
|
||||
defer cleanupEnv(b)
|
||||
|
||||
r := strings.NewReader(`key1: ${BENCH_VAR}
|
||||
key2: ${BENCH_VAR_2}
|
||||
key3: ${BENCH_VAR_3}
|
||||
`)
|
||||
|
||||
for b.Loop() {
|
||||
reader := NewSubstituteEnvReader(r)
|
||||
_, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
r.Seek(0, io.SeekStart)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSubstituteEnvReader_LargeInput_NoSubstitution benchmarks large input without substitutions
|
||||
func BenchmarkSubstituteEnvReader_LargeInput_NoSubstitution(b *testing.B) {
|
||||
r := strings.NewReader(strings.Repeat("x", 100000))
|
||||
|
||||
for b.Loop() {
|
||||
reader := NewSubstituteEnvReader(r)
|
||||
_, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
r.Seek(0, io.SeekStart)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSubstituteEnvReader_LargeInput_WithSubstitutions benchmarks large input with scattered substitutions
|
||||
func BenchmarkSubstituteEnvReader_LargeInput_WithSubstitutions(b *testing.B) {
|
||||
setupEnv(b)
|
||||
defer cleanupEnv(b)
|
||||
|
||||
var builder bytes.Buffer
|
||||
for range 100 {
|
||||
builder.WriteString(strings.Repeat("x", 1000))
|
||||
builder.WriteString("${BENCH_VAR}")
|
||||
}
|
||||
r := bytes.NewReader(builder.Bytes())
|
||||
|
||||
for b.Loop() {
|
||||
reader := NewSubstituteEnvReader(r)
|
||||
_, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
r.Seek(0, io.SeekStart)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSubstituteEnvReader_SmallBuffer benchmarks reading with a small buffer size
|
||||
func BenchmarkSubstituteEnvReader_SmallBuffer(b *testing.B) {
|
||||
setupEnv(b)
|
||||
defer cleanupEnv(b)
|
||||
|
||||
r := strings.NewReader(`key: ${BENCH_VAR} and some more content here`)
|
||||
buf := make([]byte, 16)
|
||||
|
||||
for b.Loop() {
|
||||
reader := NewSubstituteEnvReader(r)
|
||||
for {
|
||||
_, err := reader.Read(buf)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
r.Seek(0, io.SeekStart)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSubstituteEnvReader_YAMLConfig benchmarks a realistic YAML config scenario
|
||||
func BenchmarkSubstituteEnvReader_YAMLConfig(b *testing.B) {
|
||||
setupEnv(b)
|
||||
defer cleanupEnv(b)
|
||||
|
||||
r := strings.NewReader(`database:
|
||||
host: ${BENCH_VAR}
|
||||
port: ${BENCH_VAR_2}
|
||||
username: ${BENCH_VAR_3}
|
||||
password: ${BENCH_VAR}
|
||||
cache:
|
||||
enabled: true
|
||||
ttl: ${BENCH_VAR_2}
|
||||
server:
|
||||
host: ${BENCH_VAR}
|
||||
port: 8080
|
||||
`)
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
reader := NewSubstituteEnvReader(r)
|
||||
_, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
r.Seek(0, io.SeekStart)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSubstituteEnvReader_BoundaryPattern benchmarks patterns at buffer boundaries (4096 bytes)
|
||||
func BenchmarkSubstituteEnvReader_BoundaryPattern(b *testing.B) {
|
||||
setupEnv(b)
|
||||
defer cleanupEnv(b)
|
||||
|
||||
// Pattern exactly at 4090 bytes, with ${VAR} crossing the 4096 boundary
|
||||
prefix := strings.Repeat("x", 4090)
|
||||
r := strings.NewReader(prefix + "${BENCH_VAR}")
|
||||
|
||||
for b.Loop() {
|
||||
reader := NewSubstituteEnvReader(r)
|
||||
_, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
r.Seek(0, io.SeekStart)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSubstituteEnvReader_MultipleBoundaries benchmarks multiple patterns crossing boundaries
|
||||
func BenchmarkSubstituteEnvReader_MultipleBoundaries(b *testing.B) {
|
||||
setupEnv(b)
|
||||
defer cleanupEnv(b)
|
||||
|
||||
var builder bytes.Buffer
|
||||
for range 10 {
|
||||
builder.WriteString(strings.Repeat("x", 4000))
|
||||
builder.WriteString("${BENCH_VAR}")
|
||||
}
|
||||
r := bytes.NewReader(builder.Bytes())
|
||||
|
||||
for b.Loop() {
|
||||
reader := NewSubstituteEnvReader(r)
|
||||
_, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
r.Seek(0, io.SeekStart)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSubstituteEnvReader_SpecialChars benchmarks substitution with special characters
|
||||
func BenchmarkSubstituteEnvReader_SpecialChars(b *testing.B) {
|
||||
os.Setenv("SPECIAL_BENCH_VAR", `value with "quotes" and \backslash\`)
|
||||
defer os.Unsetenv("SPECIAL_BENCH_VAR")
|
||||
|
||||
r := strings.NewReader(`key: ${SPECIAL_BENCH_VAR}
|
||||
`)
|
||||
|
||||
for b.Loop() {
|
||||
reader := NewSubstituteEnvReader(r)
|
||||
_, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
r.Seek(0, io.SeekStart)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSubstituteEnvReader_EmptyValue benchmarks substitution with empty value
|
||||
func BenchmarkSubstituteEnvReader_EmptyValue(b *testing.B) {
|
||||
os.Setenv("EMPTY_BENCH_VAR", "")
|
||||
defer os.Unsetenv("EMPTY_BENCH_VAR")
|
||||
|
||||
r := strings.NewReader(`key: ${EMPTY_BENCH_VAR}
|
||||
`)
|
||||
|
||||
for b.Loop() {
|
||||
reader := NewSubstituteEnvReader(r)
|
||||
_, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
r.Seek(0, io.SeekStart)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSubstituteEnvReader_DollarWithoutBrace benchmarks $ without following {
|
||||
func BenchmarkSubstituteEnvReader_DollarWithoutBrace(b *testing.B) {
|
||||
os.Setenv("BENCH_VAR", "benchmark_value")
|
||||
defer os.Unsetenv("BENCH_VAR")
|
||||
|
||||
r := strings.NewReader(`price: $100 and $200 for ${BENCH_VAR}`)
|
||||
|
||||
for b.Loop() {
|
||||
reader := NewSubstituteEnvReader(r)
|
||||
_, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
r.Seek(0, io.SeekStart)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkFindIncompletePatternStart benchmarks the findIncompletePatternStart function
|
||||
func BenchmarkFindIncompletePatternStart(b *testing.B) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{"no pattern", strings.Repeat("hello world ", 100)},
|
||||
{"complete pattern", strings.Repeat("hello ${VAR} world ", 50)},
|
||||
{"dollar at end", strings.Repeat("hello ", 100) + "$"},
|
||||
{"incomplete at end", strings.Repeat("hello ", 100) + "${VAR"},
|
||||
{"large input no pattern", strings.Repeat("x", 5000)},
|
||||
{"large input with pattern", strings.Repeat("x", 4000) + "${VAR}"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
data := []byte(tc.input)
|
||||
for b.Loop() {
|
||||
findIncompletePatternStart(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
217
internal/serialization/reader_test.go
Normal file
217
internal/serialization/reader_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package serialization
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSubstituteEnvReader_Basic(t *testing.T) {
|
||||
os.Setenv("TEST_VAR", "hello")
|
||||
defer os.Unsetenv("TEST_VAR")
|
||||
|
||||
input := []byte(`key: ${TEST_VAR}`)
|
||||
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||
|
||||
output, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `key: "hello"`, string(output))
|
||||
}
|
||||
|
||||
func TestSubstituteEnvReader_Multiple(t *testing.T) {
|
||||
os.Setenv("VAR1", "first")
|
||||
os.Setenv("VAR2", "second")
|
||||
defer os.Unsetenv("VAR1")
|
||||
defer os.Unsetenv("VAR2")
|
||||
|
||||
input := []byte(`a: ${VAR1}, b: ${VAR2}`)
|
||||
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||
|
||||
output, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `a: "first", b: "second"`, string(output))
|
||||
}
|
||||
|
||||
func TestSubstituteEnvReader_NoSubstitution(t *testing.T) {
|
||||
input := []byte(`key: value`)
|
||||
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||
|
||||
output, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `key: value`, string(output))
|
||||
}
|
||||
|
||||
func TestSubstituteEnvReader_UnsetEnvError(t *testing.T) {
|
||||
os.Unsetenv("UNSET_VAR_FOR_TEST")
|
||||
|
||||
input := []byte(`key: ${UNSET_VAR_FOR_TEST}`)
|
||||
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||
|
||||
_, err := io.ReadAll(reader)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "UNSET_VAR_FOR_TEST is not set")
|
||||
}
|
||||
|
||||
func TestSubstituteEnvReader_SmallBuffer(t *testing.T) {
|
||||
os.Setenv("SMALL_BUF_VAR", "value")
|
||||
defer os.Unsetenv("SMALL_BUF_VAR")
|
||||
|
||||
input := []byte(`key: ${SMALL_BUF_VAR}`)
|
||||
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||
|
||||
var result []byte
|
||||
buf := make([]byte, 3)
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
if n > 0 {
|
||||
result = append(result, buf[:n]...)
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Equal(t, `key: "value"`, string(result))
|
||||
}
|
||||
|
||||
func TestSubstituteEnvReader_SpecialChars(t *testing.T) {
|
||||
os.Setenv("SPECIAL_VAR", `hello "world" \n`)
|
||||
defer os.Unsetenv("SPECIAL_VAR")
|
||||
|
||||
input := []byte(`key: ${SPECIAL_VAR}`)
|
||||
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||
|
||||
output, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `key: "hello \"world\" \\n"`, string(output))
|
||||
}
|
||||
|
||||
func TestSubstituteEnvReader_EmptyValue(t *testing.T) {
|
||||
os.Setenv("EMPTY_VAR", "")
|
||||
defer os.Unsetenv("EMPTY_VAR")
|
||||
|
||||
input := []byte(`key: ${EMPTY_VAR}`)
|
||||
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||
|
||||
output, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `key: ""`, string(output))
|
||||
}
|
||||
|
||||
func TestSubstituteEnvReader_LargeInput(t *testing.T) {
|
||||
os.Setenv("LARGE_VAR", "replaced")
|
||||
defer os.Unsetenv("LARGE_VAR")
|
||||
|
||||
prefix := strings.Repeat("x", 5000)
|
||||
suffix := strings.Repeat("y", 5000)
|
||||
input := []byte(prefix + "${LARGE_VAR}" + suffix)
|
||||
|
||||
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||
|
||||
output, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
expected := prefix + `"replaced"` + suffix
|
||||
require.Equal(t, expected, string(output))
|
||||
}
|
||||
|
||||
func TestSubstituteEnvReader_PatternAtBoundary(t *testing.T) {
|
||||
os.Setenv("BOUNDARY_VAR", "boundary_value")
|
||||
defer os.Unsetenv("BOUNDARY_VAR")
|
||||
|
||||
prefix := strings.Repeat("a", 4090)
|
||||
input := []byte(prefix + "${BOUNDARY_VAR}")
|
||||
|
||||
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||
|
||||
output, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
expected := prefix + `"boundary_value"`
|
||||
require.Equal(t, expected, string(output))
|
||||
}
|
||||
|
||||
func TestSubstituteEnvReader_MultiplePatternsBoundary(t *testing.T) {
|
||||
os.Setenv("VAR_A", "aaa")
|
||||
os.Setenv("VAR_B", "bbb")
|
||||
defer os.Unsetenv("VAR_A")
|
||||
defer os.Unsetenv("VAR_B")
|
||||
|
||||
prefix := strings.Repeat("x", 4090)
|
||||
input := []byte(prefix + "${VAR_A} middle ${VAR_B}")
|
||||
|
||||
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||
|
||||
output, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
expected := prefix + `"aaa" middle "bbb"`
|
||||
require.Equal(t, expected, string(output))
|
||||
}
|
||||
|
||||
func TestSubstituteEnvReader_YAMLConfig(t *testing.T) {
|
||||
os.Setenv("DB_HOST", "localhost")
|
||||
os.Setenv("DB_PORT", "5432")
|
||||
os.Setenv("DB_PASSWORD", "secret123")
|
||||
defer os.Unsetenv("DB_HOST")
|
||||
defer os.Unsetenv("DB_PORT")
|
||||
defer os.Unsetenv("DB_PASSWORD")
|
||||
|
||||
input := []byte(`database:
|
||||
host: ${DB_HOST}
|
||||
port: ${DB_PORT}
|
||||
password: ${DB_PASSWORD}
|
||||
`)
|
||||
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||
|
||||
output, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
expected := `database:
|
||||
host: "localhost"
|
||||
port: "5432"
|
||||
password: "secret123"
|
||||
`
|
||||
require.Equal(t, expected, string(output))
|
||||
}
|
||||
|
||||
func TestSubstituteEnvReader_DollarWithoutBrace(t *testing.T) {
|
||||
input := []byte(`key: $NOT_A_PATTERN`)
|
||||
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||
|
||||
output, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `key: $NOT_A_PATTERN`, string(output))
|
||||
}
|
||||
|
||||
func TestSubstituteEnvReader_EmptyInput(t *testing.T) {
|
||||
input := []byte(``)
|
||||
reader := NewSubstituteEnvReader(bytes.NewReader(input))
|
||||
|
||||
output, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ``, string(output))
|
||||
}
|
||||
|
||||
func TestFindIncompletePatternStart(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected int
|
||||
}{
|
||||
{"no pattern", "hello world", -1},
|
||||
{"complete pattern", "hello ${VAR} world", -1},
|
||||
{"dollar at end", "hello $", 6},
|
||||
{"dollar brace at end", "hello ${", 6},
|
||||
{"incomplete var at end", "hello ${VAR", 6},
|
||||
{"complete then incomplete", "hello ${VAR} ${INCOMPLETE", 13},
|
||||
{"multiple complete", "${A} ${B} ${C}", -1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := findIncompletePatternStart([]byte(tt.input))
|
||||
require.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package serialization
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
@@ -85,6 +86,10 @@ func initPtr(dst reflect.Value) {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate performs struct validation using go-playground/validator tags.
|
||||
//
|
||||
// It collects all validation errors and returns them as a single error.
|
||||
// Field names in errors are prefixed with their namespace (e.g., "User.Email").
|
||||
func ValidateWithFieldTags(s any) gperr.Error {
|
||||
var errs gperr.Builder
|
||||
err := validate.Struct(s)
|
||||
@@ -257,10 +262,11 @@ func initTypeKeyFieldIndexesMap(t reflect.Type) typeInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// MapUnmarshalValidate takes a SerializedObject and a target value, and assigns the values in the SerializedObject to the target value.
|
||||
// MapUnmarshalValidate ignores case differences between the field names in the SerializedObject and the target.
|
||||
// MapUnmarshalValidate takes a SerializedObject and a target value,
|
||||
// and assigns the values in the SerializedObject to the target value.
|
||||
//
|
||||
// It ignores case differences between the field names in the SerializedObject and the target.
|
||||
//
|
||||
// The target value must be a struct or a map[string]any.
|
||||
// If the target value is a struct , and implements the MapUnmarshaller interface,
|
||||
// the UnmarshalMap method will be called.
|
||||
//
|
||||
@@ -309,7 +315,7 @@ func mapUnmarshalValidate(src SerializedObject, dstV reflect.Value, checkValidat
|
||||
info := getTypeInfo(dstT)
|
||||
for k, v := range src {
|
||||
if field, ok := info.getField(dstV, k); ok {
|
||||
err := Convert(reflect.ValueOf(v), field, !info.hasValidateTag)
|
||||
err := Convert(reflect.ValueOf(v), field, checkValidateTag)
|
||||
if err != nil {
|
||||
errs.Add(err.Subject(k))
|
||||
}
|
||||
@@ -455,6 +461,13 @@ func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.
|
||||
return ErrUnsupportedConversion.Subjectf("%s to %s", srcT, dstT)
|
||||
}
|
||||
|
||||
// ConvertSlice converts a source slice to a destination slice.
|
||||
//
|
||||
// - Elements are converted one by one using the Convert function.
|
||||
// - Validation is performed on each element if checkValidateTag is true.
|
||||
// - The destination slice is initialized with the source length.
|
||||
// - On error, the destination slice is truncated to the number of
|
||||
// successfully converted elements.
|
||||
func ConvertSlice(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.Error {
|
||||
if dst.Kind() == reflect.Pointer {
|
||||
if dst.IsNil() && !dst.CanSet() {
|
||||
@@ -507,6 +520,12 @@ func ConvertSlice(src reflect.Value, dst reflect.Value, checkValidateTag bool) g
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConvertString converts a string value to the destination reflect.Value.
|
||||
// - It handles various types including numeric types, booleans, time.Duration,
|
||||
// slices (comma-separated or YAML), maps, and structs (YAML).
|
||||
// - If the destination implements the Parser interface, it is used for conversion.
|
||||
// - Returns true if conversion was handled (even with error), false if
|
||||
// conversion is unsupported.
|
||||
func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gperr.Error) {
|
||||
convertible = true
|
||||
dstT := dst.Type()
|
||||
@@ -618,48 +637,80 @@ func substituteEnv(data []byte) ([]byte, gperr.Error) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func UnmarshalValidateYAML[T any](data []byte, target *T) gperr.Error {
|
||||
type (
|
||||
marshalFunc func(src any) ([]byte, error)
|
||||
unmarshalFunc func(data []byte, target any) error
|
||||
newDecoderFunc func(r io.Reader) interface {
|
||||
Decode(v any) error
|
||||
}
|
||||
interceptFunc func(m map[string]any) gperr.Error
|
||||
)
|
||||
|
||||
// UnmarshalValidate unmarshals data into a map, applies optional intercept
|
||||
// functions, and validates the result against the target struct using field tags.
|
||||
// - Environment variables in the data are substituted using ${VAR} syntax.
|
||||
// - The unmarshaler function converts data to a map[string]any.
|
||||
// - Intercept functions can modify or validate the map before unmarshaling.
|
||||
func UnmarshalValidate[T any](data []byte, target *T, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) gperr.Error {
|
||||
data, err := substituteEnv(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m := make(map[string]any)
|
||||
if err := yaml.Unmarshal(data, &m); err != nil {
|
||||
if err := unmarshaler(data, &m); err != nil {
|
||||
return gperr.Wrap(err)
|
||||
}
|
||||
for _, intercept := range interceptFns {
|
||||
if err := intercept(m); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return MapUnmarshalValidate(m, target)
|
||||
}
|
||||
|
||||
func UnmarshalValidateYAMLIntercept[T any](data []byte, target *T, intercept func(m map[string]any) gperr.Error) gperr.Error {
|
||||
// UnmarshalValidateReader reads from an io.Reader, unmarshals to a map,
|
||||
// - Applies optional intercept functions, and validates against the target struct.
|
||||
// - Environment variables are substituted during reading using ${VAR} syntax.
|
||||
// - The newDecoder function creates a decoder for the reader (e.g.,
|
||||
// json.NewDecoder).
|
||||
func UnmarshalValidateReader[T any](reader io.Reader, target *T, newDecoder newDecoderFunc, interceptFns ...interceptFunc) gperr.Error {
|
||||
m := make(map[string]any)
|
||||
if err := newDecoder(NewSubstituteEnvReader(reader)).Decode(&m); err != nil {
|
||||
return gperr.Wrap(err)
|
||||
}
|
||||
for _, intercept := range interceptFns {
|
||||
if err := intercept(m); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return MapUnmarshalValidate(m, target)
|
||||
}
|
||||
|
||||
// UnmarshalValidateXSync unmarshals data into an xsync.Map[string, V].
|
||||
// - Environment variables in the data are substituted using ${VAR} syntax.
|
||||
// - The unmarshaler function converts data to a map[string]any.
|
||||
// - Intercept functions can modify or validate the map before unmarshaling.
|
||||
// - Returns a thread-safe concurrent map with the unmarshaled values.
|
||||
func UnmarshalValidateXSync[V any](data []byte, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) (*xsync.Map[string, V], gperr.Error) {
|
||||
data, err := substituteEnv(data)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := make(map[string]any)
|
||||
if err := yaml.Unmarshal(data, &m); err != nil {
|
||||
return gperr.Wrap(err)
|
||||
if err := unmarshaler(data, &m); err != nil {
|
||||
return nil, gperr.Wrap(err)
|
||||
}
|
||||
if err := intercept(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return MapUnmarshalValidate(m, target)
|
||||
}
|
||||
|
||||
func UnmarshalValidateYAMLXSync[V any](data []byte) (_ *xsync.Map[string, V], err gperr.Error) {
|
||||
data, err = substituteEnv(data)
|
||||
if err != nil {
|
||||
return
|
||||
for _, intercept := range interceptFns {
|
||||
if err := intercept(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
m := make(map[string]any)
|
||||
if err = gperr.Wrap(yaml.Unmarshal(data, &m)); err != nil {
|
||||
return
|
||||
}
|
||||
m2 := make(map[string]V, len(m))
|
||||
if err = MapUnmarshalValidate(m, m2); err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
ret := xsync.NewMap[string, V](xsync.WithPresize(len(m)))
|
||||
for k, v := range m2 {
|
||||
@@ -668,26 +719,27 @@ func UnmarshalValidateYAMLXSync[V any](data []byte) (_ *xsync.Map[string, V], er
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func loadSerialized[T any](path string, dst *T, deserialize func(data []byte, dst any) error) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return deserialize(data, dst)
|
||||
}
|
||||
|
||||
func SaveJSON[T any](path string, src *T, perm os.FileMode) error {
|
||||
data, err := sonic.Marshal(src)
|
||||
// SaveFile marshals a value to bytes and writes it to a file.
|
||||
// - The marshaler function converts the value to bytes.
|
||||
// - The file is written with the specified permissions.
|
||||
func SaveFile[T any](path string, src *T, perm os.FileMode, marshaler marshalFunc) error {
|
||||
data, err := marshaler(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, perm)
|
||||
}
|
||||
|
||||
func LoadJSONIfExist[T any](path string, dst *T) error {
|
||||
err := loadSerialized(path, dst, sonic.Unmarshal)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
// LoadFileIfExist reads a file and unmarshals its contents to a value.
|
||||
// - The unmarshaler function converts the bytes to a value.
|
||||
// - If the file does not exist, nil is returned and dst remains unchanged.
|
||||
func LoadFileIfExist[T any](path string, dst *T, unmarshaler unmarshalFunc) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return err
|
||||
return unmarshaler(data, dst)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/stretchr/testify/require"
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
@@ -303,6 +304,6 @@ autocert:
|
||||
} `yaml:"options"`
|
||||
} `yaml:"autocert"`
|
||||
}
|
||||
require.NoError(t, UnmarshalValidateYAML(data, &cfg))
|
||||
require.NoError(t, UnmarshalValidate(data, &cfg, yaml.Unmarshal))
|
||||
require.Equal(t, "test", cfg.Autocert.Options.AuthToken)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user