mirror of
https://github.com/yusing/godoxy.git
synced 2026-02-16 13:57:46 +01:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fac3d67a51 | ||
|
|
cb642d7b32 | ||
|
|
9285977495 | ||
|
|
e00cd8a35b | ||
|
|
8ac459c038 | ||
|
|
1bcaf0dab5 | ||
|
|
a291a49a0e | ||
|
|
28fdf3d2f4 | ||
|
|
84b17baf46 | ||
|
|
06ddb178f8 | ||
|
|
61fa7d2665 | ||
|
|
615521ee1c | ||
|
|
bbe308e821 | ||
|
|
c156173757 | ||
|
|
b1aae1cacf | ||
|
|
f46552b477 | ||
|
|
efe1350ffd | ||
|
|
219eedf3c5 | ||
|
|
f6dcc8f118 | ||
|
|
4d6541c851 | ||
|
|
c9db350cbc | ||
|
|
56374d595a | ||
|
|
d81521f293 | ||
|
|
e9ac3cd1a9 | ||
|
|
d33ff2192a | ||
|
|
910ef639a4 | ||
|
|
3cbd70f73a | ||
|
|
83d70d3bb2 | ||
|
|
bbb1b8497f | ||
|
|
d57d76dc65 | ||
|
|
ef893974ea | ||
|
|
b90f2409ab | ||
|
|
36e9b0d416 | ||
|
|
306cb7a20e | ||
|
|
e3915210aa | ||
|
|
e8fb202ea9 | ||
|
|
082b2f5da2 | ||
|
|
e670acb4b8 | ||
|
|
77e486f4fe | ||
|
|
3ccaba3163 | ||
|
|
705923960c | ||
|
|
ca737c8979 | ||
|
|
b6b5d4dbd7 | ||
|
|
b2919fbaf6 | ||
|
|
722c40d103 | ||
|
|
860d9c71b6 |
@@ -1,5 +1,5 @@
|
||||
# Stage 1: deps
|
||||
FROM golang:1.25.3-alpine AS deps
|
||||
FROM golang:1.25.4-alpine AS deps
|
||||
HEALTHCHECK NONE
|
||||
|
||||
# package version does not matter
|
||||
|
||||
11
Jenkinsfile
vendored
Normal file
11
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
node {
|
||||
stage('SCM') {
|
||||
checkout scm
|
||||
}
|
||||
stage('SonarQube Analysis') {
|
||||
def scannerHome = tool 'SonarScanner';
|
||||
withSonarQubeEnv() {
|
||||
sh "${scannerHome}/bin/sonar-scanner"
|
||||
}
|
||||
}
|
||||
}
|
||||
43
agent/go.mod
43
agent/go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/yusing/godoxy/agent
|
||||
|
||||
go 1.25.3
|
||||
go 1.25.4
|
||||
|
||||
replace github.com/yusing/godoxy => ..
|
||||
|
||||
@@ -26,9 +26,8 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.11.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
@@ -39,13 +38,12 @@ 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 v28.5.1+incompatible // indirect
|
||||
github.com/docker/docker v28.5.1+incompatible // indirect
|
||||
github.com/docker/cli v29.0.1+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.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
@@ -57,7 +55,6 @@ require (
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gotify/server/v2 v2.7.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
@@ -67,25 +64,25 @@ 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/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/moby/api v1.52.0 // indirect
|
||||
github.com/moby/moby/client v0.1.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.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/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||
github.com/quic-go/quic-go v0.56.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.8.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.9 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.10 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
@@ -96,20 +93,14 @@ require (
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.44.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
)
|
||||
|
||||
109
agent/go.sum
109
agent/go.sum
@@ -1,9 +1,7 @@
|
||||
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.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
@@ -24,8 +22,6 @@ 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.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
|
||||
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
@@ -39,28 +35,26 @@ 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 v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY=
|
||||
github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
||||
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/cli v29.0.1+incompatible h1:EnvMEAR9Ro5xQEKbMitlabj5vCDY0vwcDyY/Lsow7FQ=
|
||||
github.com/docker/cli v29.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-acme/lego/v4 v4.27.0 h1:cIhWd7Uj4BNFLEF3IpwuMkukVVRs5qjlp4KdUGa75yU=
|
||||
github.com/go-acme/lego/v4 v4.27.0/go.mod h1:9FfNZHZmg6hf5CWOp4Lzo4gU8aBEvqZvrwdkBboa+4g=
|
||||
github.com/go-acme/lego/v4 v4.28.1 h1:zt301JYF51UIEkpSXsdeGq9hRePeFzQCq070OdAmP0Q=
|
||||
github.com/go-acme/lego/v4 v4.28.1/go.mod h1:bzjilr03IgbaOwlH396hq5W56Bi0/uoRwW/JM8hP7m4=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -100,8 +94,6 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/server/v2 v2.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
|
||||
github.com/gotify/server/v2 v2.7.3/go.mod h1:VAtE1RIc/2j886PYs9WPQbMjqbFsoyQ0G8IdFtnAxU0=
|
||||
github.com/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=
|
||||
@@ -135,19 +127,15 @@ github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
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/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/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
||||
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
github.com/moby/moby/client v0.1.0 h1:nt+hn6O9cyJQqq5UWnFGqsZRTS/JirUqzPjEl0Bdc/8=
|
||||
github.com/moby/moby/client v0.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
||||
github.com/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=
|
||||
@@ -158,7 +146,6 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/errors v0.9.1 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=
|
||||
@@ -169,8 +156,8 @@ github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wv
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||
github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
|
||||
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
@@ -180,8 +167,8 @@ github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
||||
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||
github.com/samber/slog-zerolog/v2 v2.8.0 h1:K3+PJieRyi2rX/eaJZ95EdmpY/pzdeDd3jRnIQZG6kU=
|
||||
github.com/samber/slog-zerolog/v2 v2.8.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
@@ -197,10 +184,10 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
||||
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/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
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/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
@@ -226,10 +213,6 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/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.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
@@ -238,29 +221,27 @@ go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
||||
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -270,10 +251,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -281,14 +262,13 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -301,8 +281,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -321,8 +301,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -331,16 +311,9 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -351,3 +324,5 @@ 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=
|
||||
|
||||
@@ -60,11 +60,8 @@ func (cfg *AgentConfig) DoHealthCheck(timeout time.Duration, query string) (ret
|
||||
}
|
||||
|
||||
if status := resp.StatusCode(); status != http.StatusOK {
|
||||
// clone body since fasthttp response will be released
|
||||
body := resp.Body()
|
||||
cloneBody := make([]byte, len(body))
|
||||
copy(cloneBody, body)
|
||||
return ret, fmt.Errorf("HTTP %d %s", status, cloneBody)
|
||||
ret.Detail = fmt.Sprintf("HTTP %d %s", status, resp.Body())
|
||||
return ret, nil
|
||||
} else {
|
||||
err = sonic.Unmarshal(resp.Body(), &ret)
|
||||
if err != nil {
|
||||
|
||||
@@ -22,6 +22,8 @@ services:
|
||||
- ${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}:2375
|
||||
frontend:
|
||||
image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}
|
||||
# lite variant
|
||||
# image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}-lite
|
||||
container_name: godoxy-frontend
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
@@ -74,10 +76,9 @@ services:
|
||||
- ./error_pages:/app/error_pages:ro
|
||||
- ./data:/app/data
|
||||
|
||||
# To use autocert, certs will be stored in "./certs".
|
||||
# You can also use a docker volume to store it
|
||||
# This path stores certs obtained from autocert and agent TLS client certs
|
||||
- ./certs:/app/certs
|
||||
|
||||
# remove "./certs:/app/certs" and uncomment below to use existing certificate
|
||||
# mount existing certificate
|
||||
# - /path/to/certs/cert.crt:/app/certs/cert.crt
|
||||
# - /path/to/certs/priv.key:/app/certs/priv.key
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
# autocert:
|
||||
# provider: local
|
||||
# cert_path: /path/to/cert.crt # default: /app/certs/cert.crt
|
||||
# key_path: /path/to/priv.key # default: /app/certs/priv.key
|
||||
|
||||
# 2. cloudflare
|
||||
# autocert:
|
||||
|
||||
100
go.mod
100
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/yusing/godoxy
|
||||
|
||||
go 1.25.3
|
||||
go 1.25.4
|
||||
|
||||
replace github.com/yusing/godoxy/agent => ./agent
|
||||
|
||||
@@ -13,12 +13,11 @@ replace github.com/shirou/gopsutil/v4 => ./internal/gopsutil
|
||||
replace github.com/yusing/goutils => ./goutils
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // parsing HTML for extract fav icon
|
||||
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon
|
||||
github.com/coreos/go-oidc/v3 v3.16.0 // oidc authentication
|
||||
github.com/docker/docker v28.5.1+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.27.0 // acme client
|
||||
github.com/go-acme/lego/v4 v4.28.1 // acme client
|
||||
github.com/go-playground/validator/v10 v10.28.0 // validator
|
||||
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
||||
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
||||
@@ -28,26 +27,32 @@ require (
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 // lock free map for concurrent operations
|
||||
github.com/rs/zerolog v1.34.0 // logging
|
||||
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
||||
golang.org/x/crypto v0.43.0 // encrypting password with bcrypt
|
||||
golang.org/x/net v0.46.0 // HTTP header utilities
|
||||
golang.org/x/oauth2 v0.32.0 // oauth2 authentication
|
||||
golang.org/x/sync v0.17.0
|
||||
golang.org/x/crypto v0.44.0 // encrypting password with bcrypt
|
||||
golang.org/x/net v0.47.0 // HTTP header utilities
|
||||
golang.org/x/oauth2 v0.33.0 // oauth2 authentication
|
||||
golang.org/x/sync v0.18.0
|
||||
golang.org/x/time v0.14.0 // time utilities
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/docker/cli v28.5.1+incompatible
|
||||
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
|
||||
github.com/bytedance/sonic v1.14.2 // fast json parsing
|
||||
github.com/docker/cli v29.0.1+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
||||
github.com/goccy/go-yaml v1.18.0 // yaml parsing for different config files
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/luthermonson/go-proxmox v0.2.3
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
github.com/quic-go/quic-go v0.55.0 // indirect; http3 support
|
||||
github.com/samber/slog-zerolog/v2 v2.8.0 // indirect
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/yusing/ds v0.3.1
|
||||
github.com/yusing/godoxy/agent v0.0.0-20251028124446-1797a222cd18
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20251028124446-1797a222cd18
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // jwt authentication
|
||||
github.com/luthermonson/go-proxmox v0.2.3 // proxmox API client
|
||||
github.com/moby/moby/api v1.52.0 // docker API
|
||||
github.com/moby/moby/client v0.1.0 // docker client
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
|
||||
github.com/quic-go/quic-go v0.56.0 // http3 support
|
||||
github.com/shirou/gopsutil/v4 v4.25.10 // system information
|
||||
github.com/spf13/afero v1.15.0 // afero for file system operations
|
||||
github.com/stretchr/testify v1.11.1 // testing framework
|
||||
github.com/valyala/fasthttp v1.68.0 // fast http for health check
|
||||
github.com/yusing/ds v0.3.1 // data structures and algorithms
|
||||
github.com/yusing/godoxy/agent v0.0.0-20251114142829-a291a49a0e42
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20251114142829-a291a49a0e42
|
||||
github.com/yusing/gointernals v0.1.16
|
||||
github.com/yusing/goutils v0.7.0
|
||||
)
|
||||
|
||||
@@ -55,13 +60,13 @@ require (
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
@@ -72,9 +77,9 @@ require (
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/docker/go-connections v0.6.0
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
@@ -83,7 +88,7 @@ require (
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
@@ -106,11 +111,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.5.1 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/samber/slog-common v0.19.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
@@ -122,12 +127,12 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.uber.org/atomic v1.11.0
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/api v0.253.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
google.golang.org/api v0.256.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
@@ -135,23 +140,14 @@ require (
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.14.2
|
||||
github.com/shirou/gopsutil/v4 v4.25.9
|
||||
github.com/valyala/fasthttp v1.68.0
|
||||
github.com/yusing/gointernals v0.1.16
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/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/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
@@ -161,26 +157,22 @@ require (
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/linode/linodego v1.60.0 // indirect
|
||||
github.com/linode/linodego v1.61.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.14 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/vultr/govultr/v3 v3.24.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
)
|
||||
|
||||
149
go.sum
149
go.sum
@@ -5,10 +5,10 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3R
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
||||
@@ -23,16 +23,14 @@ 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.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
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.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 h1:h/33OxYLqBk0BYmEbSUy7MlvgQR/m1w1/7OJFKoPL1I=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0/go.mod h1:rvh3imDA6EaQi+oM/GQHkQAOHbXPKJ7EWJvfjuw141Q=
|
||||
github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=
|
||||
@@ -62,8 +60,6 @@ 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=
|
||||
@@ -75,16 +71,14 @@ 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 v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY=
|
||||
github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
||||
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/cli v29.0.1+incompatible h1:EnvMEAR9Ro5xQEKbMitlabj5vCDY0vwcDyY/Lsow7FQ=
|
||||
github.com/docker/cli v29.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=
|
||||
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
@@ -93,14 +87,14 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-acme/lego/v4 v4.27.0 h1:cIhWd7Uj4BNFLEF3IpwuMkukVVRs5qjlp4KdUGa75yU=
|
||||
github.com/go-acme/lego/v4 v4.27.0/go.mod h1:9FfNZHZmg6hf5CWOp4Lzo4gU8aBEvqZvrwdkBboa+4g=
|
||||
github.com/go-acme/lego/v4 v4.28.1 h1:zt301JYF51UIEkpSXsdeGq9hRePeFzQCq070OdAmP0Q=
|
||||
github.com/go-acme/lego/v4 v4.28.1/go.mod h1:bzjilr03IgbaOwlH396hq5W56Bi0/uoRwW/JM8hP7m4=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -149,16 +143,14 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/server/v2 v2.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
|
||||
github.com/gotify/server/v2 v2.7.3/go.mod h1:VAtE1RIc/2j886PYs9WPQbMjqbFsoyQ0G8IdFtnAxU0=
|
||||
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=
|
||||
@@ -189,8 +181,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/linode/linodego v1.60.0 h1:SgsebJFRCi+lSmYy+C40wmKZeJllGGm+W12Qw4+yVdI=
|
||||
github.com/linode/linodego v1.60.0/go.mod h1:1+Bt0oTz5rBnDOJbGhccxn7LYVytXTIIfAy7QYmijDs=
|
||||
github.com/linode/linodego v1.61.0 h1:9g20NWl+/SbhDFj6X5EOZXtM2hBm1Mx8I9h8+F3l1LM=
|
||||
github.com/linode/linodego v1.61.0/go.mod h1:64o30geLNwR0NeYh5HM/WrVCBXcSqkKnRK3x9xoRuJI=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
@@ -214,25 +206,21 @@ 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/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/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
||||
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
github.com/moby/moby/client v0.1.0 h1:nt+hn6O9cyJQqq5UWnFGqsZRTS/JirUqzPjEl0Bdc/8=
|
||||
github.com/moby/moby/client v0.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
||||
github.com/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/oci-go-sdk/common/v1065 v1065.102.1 h1:45giryNXrlUHzK/Cd4DDBOhaK0EklXrhjTgv00Zo5po=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.1/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1 h1:2EthQw4pEN2rbbSLWlF9itV+Ws2xmAmIcfKYsrwCbVA=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1/go.mod h1:xOLJ0zNGmF4M4LqdQclLONwdzjJewNl/7WQiZgrvYR8=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1 h1:kR8dHXC53heV4fttilXXkDkGmkdC5bvQ2XgbVBoD+ns=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1 h1:559XLHTF3F1A0J03PCIk6LlR0G9CmhHEDCm/TSk5BWQ=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1/go.mod h1:1FfSn6xcdK+wrNxUhtAAhLjYrwk9z8X3p3CNHwTc3zQ=
|
||||
github.com/nrdcg/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,7 +239,6 @@ github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaAS
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 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=
|
||||
@@ -264,8 +251,8 @@ github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wv
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||
github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
|
||||
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
@@ -275,8 +262,8 @@ github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
||||
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||
github.com/samber/slog-zerolog/v2 v2.8.0 h1:K3+PJieRyi2rX/eaJZ95EdmpY/pzdeDd3jRnIQZG6kU=
|
||||
github.com/samber/slog-zerolog/v2 v2.8.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
||||
@@ -300,16 +287,16 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
||||
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/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
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/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.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
|
||||
github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
@@ -337,10 +324,6 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/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.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
@@ -349,31 +332,29 @@ go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
||||
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/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.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -383,10 +364,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -394,8 +375,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -415,8 +396,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -435,8 +416,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -445,20 +426,20 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.253.0 h1:apU86Eq9Q2eQco3NsUYFpVTfy7DwemojL7LmbAj7g/I=
|
||||
google.golang.org/api v0.253.0/go.mod h1:PX09ad0r/4du83vZVAaGg7OaeyGnaUmT/CYPNvtLCbw=
|
||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
|
||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090/go.mod h1:zwJI9HzbJJlw2KXy0wX+lmT2JuZoaKK9JC4ppqmxxjk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
|
||||
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
|
||||
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba h1:Ze6qXW0j37YCqZdCD2LkzVSxgEWez0cO4NUyd44DiDY=
|
||||
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:4FLPzLA8eGAktPOTemJGDgDYRpLYwrNu4u2JtWINhnI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
@@ -476,3 +457,5 @@ 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: 3e2b05fb0c...cfb8d74b17
@@ -55,7 +55,7 @@ type config struct {
|
||||
|
||||
logAllowed bool
|
||||
// will be nil if Log is nil
|
||||
logger *accesslog.AccessLogger
|
||||
logger accesslog.AccessLogger
|
||||
|
||||
// will never tick if Notify.To is empty
|
||||
notifyTicker *time.Ticker
|
||||
|
||||
@@ -4,6 +4,7 @@ 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"
|
||||
)
|
||||
@@ -34,30 +35,30 @@ func GetContainer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
dockerClient, err := docker.NewClient(dockerHost)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
}
|
||||
|
||||
defer client.Close()
|
||||
defer dockerClient.Close()
|
||||
|
||||
cont, err := client.ContainerInspect(c.Request.Context(), id)
|
||||
cont, err := dockerClient.ContainerInspect(c.Request.Context(), id, client.ContainerInspectOptions{})
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to inspect container"))
|
||||
return
|
||||
}
|
||||
|
||||
var state ContainerState
|
||||
if cont.State != nil {
|
||||
state = cont.State.Status
|
||||
if cont.Container.State != nil {
|
||||
state = cont.Container.State.Status
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &Container{
|
||||
Server: dockerHost,
|
||||
Name: cont.Name,
|
||||
ID: cont.ID,
|
||||
Image: cont.Image,
|
||||
Name: cont.Container.Name,
|
||||
ID: cont.Container.ID,
|
||||
Image: cont.Container.Image,
|
||||
State: state,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ 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"
|
||||
@@ -39,12 +40,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, container.ListOptions{All: true})
|
||||
conts, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{All: true})
|
||||
if err != nil {
|
||||
errs.Add(err)
|
||||
continue
|
||||
}
|
||||
for _, cont := range conts {
|
||||
for _, cont := range conts.Items {
|
||||
containers = append(containers, Container{
|
||||
Server: server,
|
||||
Name: cont.Names[0],
|
||||
|
||||
@@ -4,8 +4,9 @@ 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"
|
||||
|
||||
@@ -64,13 +65,13 @@ func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerIn
|
||||
|
||||
i := 0
|
||||
for name, dockerClient := range dockerClients {
|
||||
info, err := dockerClient.Info(ctx)
|
||||
info, err := dockerClient.Info(ctx, client.InfoOptions{})
|
||||
if err != nil {
|
||||
errs.Add(err)
|
||||
continue
|
||||
}
|
||||
info.Name = name
|
||||
dockerInfos[i] = toDockerInfo(info)
|
||||
info.Info.Name = name
|
||||
dockerInfos[i] = toDockerInfo(info.Info)
|
||||
i++
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"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"
|
||||
@@ -70,7 +70,7 @@ func Logs(c *gin.Context) {
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
opts := container.LogsOptions{
|
||||
opts := client.ContainerLogsOptions{
|
||||
ShowStdout: queryParams.Stdout,
|
||||
ShowStderr: queryParams.Stderr,
|
||||
Since: queryParams.Since,
|
||||
|
||||
@@ -4,17 +4,23 @@ 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 StopRequest true "Request"
|
||||
// @Param request body RestartRequest true "Request"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
@@ -22,7 +28,7 @@ import (
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/restart [post]
|
||||
func Restart(c *gin.Context) {
|
||||
var req StopRequest
|
||||
var req RestartRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
@@ -42,7 +48,7 @@ func Restart(c *gin.Context) {
|
||||
|
||||
defer client.Close()
|
||||
|
||||
err = client.ContainerRestart(c.Request.Context(), req.ID, req.StopOptions)
|
||||
_, err = client.ContainerRestart(c.Request.Context(), req.ID, req.ContainerRestartOptions)
|
||||
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"`
|
||||
container.StartOptions
|
||||
client.ContainerStartOptions
|
||||
}
|
||||
|
||||
// @x-id "start"
|
||||
@@ -48,7 +48,7 @@ func Start(c *gin.Context) {
|
||||
|
||||
defer client.Close()
|
||||
|
||||
err = client.ContainerStart(c.Request.Context(), req.ID, req.StartOptions)
|
||||
_, err = client.ContainerStart(c.Request.Context(), req.ID, req.ContainerStartOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to start 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 StopRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
container.StopOptions
|
||||
client.ContainerStopOptions
|
||||
}
|
||||
|
||||
// @x-id "stop"
|
||||
@@ -48,7 +48,7 @@ func Stop(c *gin.Context) {
|
||||
|
||||
defer client.Close()
|
||||
|
||||
err = client.ContainerStop(c.Request.Context(), req.ID, req.StopOptions)
|
||||
_, err = client.ContainerStop(c.Request.Context(), req.ID, req.ContainerStopOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to stop container"))
|
||||
return
|
||||
|
||||
@@ -618,7 +618,7 @@
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dockerapi.StopRequest"
|
||||
"$ref": "#/definitions/dockerapi.RestartRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -3515,6 +3515,16 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"sticky": {
|
||||
"type": "boolean",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"sticky_max_age": {
|
||||
"$ref": "#/definitions/time.Duration",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"weight": {
|
||||
"type": "integer",
|
||||
"x-nullable": false,
|
||||
@@ -4198,9 +4208,13 @@
|
||||
"x-omitempty": false
|
||||
},
|
||||
"healthcheck": {
|
||||
"$ref": "#/definitions/HealthCheckConfig",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
"description": "null on load-balancer routes",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/HealthCheckConfig"
|
||||
}
|
||||
],
|
||||
"x-nullable": true
|
||||
},
|
||||
"homepage": {
|
||||
"$ref": "#/definitions/HomepageItemConfig",
|
||||
@@ -4916,12 +4930,16 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"container.Port": {
|
||||
"container.PortSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"IP": {
|
||||
"description": "Host IP address that the container's port is mapped to",
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/netip.Addr"
|
||||
}
|
||||
],
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
@@ -4938,7 +4956,7 @@
|
||||
"x-omitempty": false
|
||||
},
|
||||
"Type": {
|
||||
"description": "type\nRequired: true",
|
||||
"description": "type\nRequired: true\nEnum: [\"tcp\",\"udp\",\"sctp\"]",
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
@@ -5033,6 +5051,29 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"dockerapi.RestartRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"signal": {
|
||||
"description": "Signal (optional) is the signal to send to the container to (gracefully)\nstop it before forcibly terminating the container with SIGKILL after the\ntimeout expires. If no value is set, the default (SIGTERM) is used.",
|
||||
"type": "string"
|
||||
},
|
||||
"timeout": {
|
||||
"description": "Timeout (optional) is the timeout (in seconds) to wait for the container\nto stop gracefully before forcibly terminating it with SIGKILL.\n\n- Use nil to use the default timeout (10 seconds).\n- Use '-1' to wait indefinitely.\n- Use '0' to not wait for the container to exit gracefully, and\n immediately proceeds to forcibly terminating the container.\n- Other positive values are used as timeout (in seconds).",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"dockerapi.StartRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -5066,7 +5107,7 @@
|
||||
"x-omitempty": false
|
||||
},
|
||||
"signal": {
|
||||
"description": "Signal (optional) is the signal to send to the container to (gracefully)\nstop it before forcibly terminating the container with SIGKILL after the\ntimeout expires. If not value is set, the default (SIGTERM) is used.",
|
||||
"description": "Signal (optional) is the signal to send to the container to (gracefully)\nstop it before forcibly terminating the container with SIGKILL after the\ntimeout expires. If no value is set, the default (SIGTERM) is used.",
|
||||
"type": "string"
|
||||
},
|
||||
"timeout": {
|
||||
@@ -5219,6 +5260,20 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"netip.Addr": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "ipv4"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "ipv6"
|
||||
}
|
||||
],
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"route.Route": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -5273,9 +5328,13 @@
|
||||
"x-omitempty": false
|
||||
},
|
||||
"healthcheck": {
|
||||
"$ref": "#/definitions/HealthCheckConfig",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
"description": "null on load-balancer routes",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/HealthCheckConfig"
|
||||
}
|
||||
],
|
||||
"x-nullable": true
|
||||
},
|
||||
"homepage": {
|
||||
"$ref": "#/definitions/HomepageItemConfig",
|
||||
@@ -5530,7 +5589,7 @@
|
||||
"types.PortMapping": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/container.Port"
|
||||
"$ref": "#/definitions/container.PortSummary"
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
|
||||
@@ -555,6 +555,10 @@ definitions:
|
||||
options:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
sticky:
|
||||
type: boolean
|
||||
sticky_max_age:
|
||||
$ref: '#/definitions/time.Duration'
|
||||
weight:
|
||||
type: integer
|
||||
type: object
|
||||
@@ -881,7 +885,10 @@ definitions:
|
||||
- $ref: '#/definitions/HealthJSON'
|
||||
description: for swagger
|
||||
healthcheck:
|
||||
$ref: '#/definitions/HealthCheckConfig'
|
||||
allOf:
|
||||
- $ref: '#/definitions/HealthCheckConfig'
|
||||
description: null on load-balancer routes
|
||||
x-nullable: true
|
||||
homepage:
|
||||
$ref: '#/definitions/HomepageItemConfig'
|
||||
host:
|
||||
@@ -1248,11 +1255,12 @@ definitions:
|
||||
- StateRemoving
|
||||
- StateExited
|
||||
- StateDead
|
||||
container.Port:
|
||||
container.PortSummary:
|
||||
properties:
|
||||
IP:
|
||||
allOf:
|
||||
- $ref: '#/definitions/netip.Addr'
|
||||
description: Host IP address that the container's port is mapped to
|
||||
type: string
|
||||
PrivatePort:
|
||||
description: |-
|
||||
Port on the container
|
||||
@@ -1265,6 +1273,7 @@ definitions:
|
||||
description: |-
|
||||
type
|
||||
Required: true
|
||||
Enum: ["tcp","udp","sctp"]
|
||||
type: string
|
||||
type: object
|
||||
disk.IOCountersStat:
|
||||
@@ -1316,6 +1325,30 @@ definitions:
|
||||
used_percent:
|
||||
type: number
|
||||
type: object
|
||||
dockerapi.RestartRequest:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
signal:
|
||||
description: |-
|
||||
Signal (optional) is the signal to send to the container to (gracefully)
|
||||
stop it before forcibly terminating the container with SIGKILL after the
|
||||
timeout expires. If no value is set, the default (SIGTERM) is used.
|
||||
type: string
|
||||
timeout:
|
||||
description: |-
|
||||
Timeout (optional) is the timeout (in seconds) to wait for the container
|
||||
to stop gracefully before forcibly terminating it with SIGKILL.
|
||||
|
||||
- Use nil to use the default timeout (10 seconds).
|
||||
- Use '-1' to wait indefinitely.
|
||||
- Use '0' to not wait for the container to exit gracefully, and
|
||||
immediately proceeds to forcibly terminating the container.
|
||||
- Other positive values are used as timeout (in seconds).
|
||||
type: integer
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
dockerapi.StartRequest:
|
||||
properties:
|
||||
checkpointDir:
|
||||
@@ -1335,7 +1368,7 @@ definitions:
|
||||
description: |-
|
||||
Signal (optional) is the signal to send to the container to (gracefully)
|
||||
stop it before forcibly terminating the container with SIGKILL after the
|
||||
timeout expires. If not value is set, the default (SIGTERM) is used.
|
||||
timeout expires. If no value is set, the default (SIGTERM) is used.
|
||||
type: string
|
||||
timeout:
|
||||
description: |-
|
||||
@@ -1429,6 +1462,8 @@ definitions:
|
||||
description: godoxy
|
||||
type: number
|
||||
type: object
|
||||
netip.Addr:
|
||||
type: object
|
||||
route.Route:
|
||||
properties:
|
||||
access_log:
|
||||
@@ -1457,7 +1492,10 @@ definitions:
|
||||
- $ref: '#/definitions/HealthJSON'
|
||||
description: for swagger
|
||||
healthcheck:
|
||||
$ref: '#/definitions/HealthCheckConfig'
|
||||
allOf:
|
||||
- $ref: '#/definitions/HealthCheckConfig'
|
||||
description: null on load-balancer routes
|
||||
x-nullable: true
|
||||
homepage:
|
||||
$ref: '#/definitions/HomepageItemConfig'
|
||||
host:
|
||||
@@ -1592,7 +1630,7 @@ definitions:
|
||||
type: object
|
||||
types.PortMapping:
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/container.Port'
|
||||
$ref: '#/definitions/container.PortSummary'
|
||||
type: object
|
||||
widgets.Config:
|
||||
properties:
|
||||
@@ -2008,7 +2046,7 @@ paths:
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dockerapi.StopRequest'
|
||||
$ref: '#/definitions/dockerapi.RestartRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
||||
@@ -51,6 +51,10 @@ func ProceedNext(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func AuthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if defaultAuth == nil {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
err := defaultAuth.CheckToken(r)
|
||||
if err != nil {
|
||||
defaultAuth.LoginHandler(w, r)
|
||||
@@ -60,6 +64,10 @@ func AuthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func AuthOrProceed(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
||||
if defaultAuth == nil {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
return false
|
||||
}
|
||||
err := defaultAuth.CheckToken(r)
|
||||
if err != nil {
|
||||
defaultAuth.LoginHandler(w, r)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
@@ -30,6 +31,8 @@ type (
|
||||
endSessionURL *url.URL
|
||||
allowedUsers []string
|
||||
allowedGroups []string
|
||||
|
||||
onUnknownPathHandler http.HandlerFunc
|
||||
}
|
||||
|
||||
IDTokenClaims struct {
|
||||
@@ -63,8 +66,9 @@ func (auth *OIDCProvider) getAppScopedCookieName(baseName string) string {
|
||||
|
||||
const (
|
||||
OIDCAuthInitPath = "/"
|
||||
OIDCPostAuthPath = "/auth/callback"
|
||||
OIDCLogoutPath = "/auth/logout"
|
||||
OIDCAuthBasePath = "/auth"
|
||||
OIDCPostAuthPath = OIDCAuthBasePath + "/callback"
|
||||
OIDCLogoutPath = OIDCAuthBasePath + "/logout"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -176,6 +180,10 @@ func (auth *OIDCProvider) SetScopes(scopes []string) {
|
||||
auth.oauthConfig.Scopes = scopes
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) SetOnUnknownPathHandler(handler http.HandlerFunc) {
|
||||
auth.onUnknownPathHandler = handler
|
||||
}
|
||||
|
||||
// optRedirectPostAuth returns an oauth2 option that sets the "redirect_uri"
|
||||
// parameter of the authorization URL to the post auth path of the current
|
||||
// request host.
|
||||
@@ -199,7 +207,7 @@ func (auth *OIDCProvider) HandleAuth(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "" {
|
||||
r.URL.Path = OIDCAuthInitPath
|
||||
}
|
||||
if r.TLS == nil && r.Header.Get("X-Forwarded-Proto") != "https" {
|
||||
if r.TLS == nil && strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") {
|
||||
r.URL.Scheme = "https"
|
||||
http.Redirect(w, r, r.URL.String(), http.StatusFound)
|
||||
return
|
||||
@@ -212,6 +220,10 @@ func (auth *OIDCProvider) HandleAuth(w http.ResponseWriter, r *http.Request) {
|
||||
case OIDCLogoutPath:
|
||||
auth.LogoutHandler(w, r)
|
||||
default:
|
||||
if auth.onUnknownPathHandler != nil {
|
||||
auth.onUnknownPathHandler(w, r)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, OIDCAuthInitPath, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,5 +409,6 @@ func (state *state) printRoutesByProvider(lenLongestName int) {
|
||||
|
||||
func (state *state) printState() {
|
||||
state.tmpLog.Info().Msg("active config:")
|
||||
yaml.NewEncoder(state.tmpLog).Encode(state.Config)
|
||||
yamlRepr, _ := yaml.Marshal(state.Config)
|
||||
state.tmpLog.Info().Msgf("%s", yamlRepr) // prevent copying when casting to string
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
module github.com/yusing/godoxy/internal/dnsproviders
|
||||
|
||||
go 1.25.3
|
||||
go 1.25.4
|
||||
|
||||
replace github.com/yusing/godoxy => ../..
|
||||
|
||||
require (
|
||||
github.com/go-acme/lego/v4 v4.27.0
|
||||
github.com/yusing/godoxy v0.20.2
|
||||
github.com/go-acme/lego/v4 v4.28.1
|
||||
github.com/yusing/godoxy v0.20.8
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
@@ -29,7 +29,7 @@ require (
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
@@ -44,7 +44,7 @@ require (
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/gotify/server/v2 v2.7.3 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
@@ -52,15 +52,15 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/linode/linodego v1.60.0 // indirect
|
||||
github.com/linode/linodego v1.61.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/maxatome/go-testdeep v1.14.0 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1 // indirect
|
||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
@@ -83,17 +83,17 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/api v0.253.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.44.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.33.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
google.golang.org/api v0.256.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
||||
@@ -5,10 +5,10 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3R
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
||||
@@ -25,8 +25,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
|
||||
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.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 h1:h/33OxYLqBk0BYmEbSUy7MlvgQR/m1w1/7OJFKoPL1I=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0/go.mod h1:rvh3imDA6EaQi+oM/GQHkQAOHbXPKJ7EWJvfjuw141Q=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
@@ -53,10 +53,10 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/go-acme/lego/v4 v4.27.0 h1:cIhWd7Uj4BNFLEF3IpwuMkukVVRs5qjlp4KdUGa75yU=
|
||||
github.com/go-acme/lego/v4 v4.27.0/go.mod h1:9FfNZHZmg6hf5CWOp4Lzo4gU8aBEvqZvrwdkBboa+4g=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/go-acme/lego/v4 v4.28.1 h1:zt301JYF51UIEkpSXsdeGq9hRePeFzQCq070OdAmP0Q=
|
||||
github.com/go-acme/lego/v4 v4.28.1/go.mod h1:bzjilr03IgbaOwlH396hq5W56Bi0/uoRwW/JM8hP7m4=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -94,8 +94,8 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/gotify/server/v2 v2.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
|
||||
@@ -120,8 +120,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/linode/linodego v1.60.0 h1:SgsebJFRCi+lSmYy+C40wmKZeJllGGm+W12Qw4+yVdI=
|
||||
github.com/linode/linodego v1.60.0/go.mod h1:1+Bt0oTz5rBnDOJbGhccxn7LYVytXTIIfAy7QYmijDs=
|
||||
github.com/linode/linodego v1.61.0 h1:9g20NWl+/SbhDFj6X5EOZXtM2hBm1Mx8I9h8+F3l1LM=
|
||||
github.com/linode/linodego v1.61.0/go.mod h1:64o30geLNwR0NeYh5HM/WrVCBXcSqkKnRK3x9xoRuJI=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
@@ -137,10 +137,10 @@ 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/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
|
||||
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.1 h1:45giryNXrlUHzK/Cd4DDBOhaK0EklXrhjTgv00Zo5po=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.1/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1 h1:2EthQw4pEN2rbbSLWlF9itV+Ws2xmAmIcfKYsrwCbVA=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1/go.mod h1:xOLJ0zNGmF4M4LqdQclLONwdzjJewNl/7WQiZgrvYR8=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1 h1:kR8dHXC53heV4fttilXXkDkGmkdC5bvQ2XgbVBoD+ns=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1 h1:559XLHTF3F1A0J03PCIk6LlR0G9CmhHEDCm/TSk5BWQ=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1/go.mod h1:1FfSn6xcdK+wrNxUhtAAhLjYrwk9z8X3p3CNHwTc3zQ=
|
||||
github.com/nrdcg/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=
|
||||
@@ -206,41 +206,41 @@ 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.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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.253.0 h1:apU86Eq9Q2eQco3NsUYFpVTfy7DwemojL7LmbAj7g/I=
|
||||
google.golang.org/api v0.253.0/go.mod h1:PX09ad0r/4du83vZVAaGg7OaeyGnaUmT/CYPNvtLCbw=
|
||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
|
||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090/go.mod h1:zwJI9HzbJJlw2KXy0wX+lmT2JuZoaKK9JC4ppqmxxjk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
|
||||
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
|
||||
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba h1:Ze6qXW0j37YCqZdCD2LkzVSxgEWez0cO4NUyd44DiDY=
|
||||
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:4FLPzLA8eGAktPOTemJGDgDYRpLYwrNu4u2JtWINhnI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
@@ -110,6 +110,8 @@ func Clients() map[string]*SharedClient {
|
||||
return clients
|
||||
}
|
||||
|
||||
var versionArg = client.WithAPIVersionNegotiation()
|
||||
|
||||
// NewClient creates a new Docker client connection to the specified host.
|
||||
//
|
||||
// Returns existing client if available.
|
||||
@@ -152,7 +154,7 @@ func NewClient(host string, unique ...bool) (*SharedClient, error) {
|
||||
opt = []client.Opt{
|
||||
client.WithHost(agent.DockerHost),
|
||||
client.WithHTTPClient(cfg.NewHTTPClient()),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
versionArg,
|
||||
}
|
||||
addr = "tcp://" + cfg.Addr
|
||||
dial = cfg.DialContext
|
||||
@@ -163,7 +165,7 @@ func NewClient(host string, unique ...bool) (*SharedClient, error) {
|
||||
case common.DockerHostFromEnv:
|
||||
opt = []client.Opt{
|
||||
client.WithHostFromEnv(),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
versionArg,
|
||||
}
|
||||
default:
|
||||
helper, err := connhelper.GetConnectionHelper(host)
|
||||
@@ -179,19 +181,19 @@ func NewClient(host string, unique ...bool) (*SharedClient, error) {
|
||||
opt = []client.Opt{
|
||||
client.WithHTTPClient(httpClient),
|
||||
client.WithHost(helper.Host),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
versionArg,
|
||||
client.WithDialContext(helper.Dialer),
|
||||
}
|
||||
} else {
|
||||
opt = []client.Opt{
|
||||
client.WithHost(host),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
versionArg,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client, err := client.NewClientWithOpts(opt...)
|
||||
client, err := client.New(opt...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ 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/serialization"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
@@ -92,24 +93,24 @@ func IsBlacklisted(c *types.Container) bool {
|
||||
}
|
||||
|
||||
func UpdatePorts(c *types.Container) error {
|
||||
client, err := NewClient(c.DockerHost)
|
||||
dockerClient, err := NewClient(c.DockerHost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
defer dockerClient.Close()
|
||||
|
||||
inspect, err := client.ContainerInspect(context.Background(), c.ContainerID)
|
||||
inspect, err := dockerClient.ContainerInspect(context.Background(), c.ContainerID, client.ContainerInspectOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for port := range inspect.Config.ExposedPorts {
|
||||
proto, portStr := nat.SplitProtoPort(string(port))
|
||||
for port := range inspect.Container.Config.ExposedPorts {
|
||||
proto, portStr := nat.SplitProtoPort(port.String())
|
||||
portInt, _ := nat.ParsePort(portStr)
|
||||
if portInt == 0 {
|
||||
continue
|
||||
}
|
||||
c.PublicPortMapping[portInt] = container.Port{
|
||||
c.PublicPortMapping[portInt] = container.PortSummary{
|
||||
PublicPort: uint16(portInt), //nolint:gosec
|
||||
PrivatePort: uint16(portInt), //nolint:gosec
|
||||
Type: proto,
|
||||
@@ -207,8 +208,8 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
|
||||
}
|
||||
if c.Network != "" {
|
||||
v, ok := helper.NetworkSettings.Networks[c.Network]
|
||||
if ok {
|
||||
c.PrivateHostname = v.IPAddress
|
||||
if ok && v.IPAddress.IsValid() {
|
||||
c.PrivateHostname = v.IPAddress.String()
|
||||
return
|
||||
}
|
||||
// try {project_name}_{network_name}
|
||||
@@ -216,9 +217,9 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
|
||||
oldNetwork, newNetwork := c.Network, fmt.Sprintf("%s_%s", proj, c.Network)
|
||||
if newNetwork != oldNetwork {
|
||||
v, ok = helper.NetworkSettings.Networks[newNetwork]
|
||||
if ok {
|
||||
if ok && v.IPAddress.IsValid() {
|
||||
c.Network = newNetwork // update network to the new one
|
||||
c.PrivateHostname = v.IPAddress
|
||||
c.PrivateHostname = v.IPAddress.String()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -229,9 +230,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 != "" {
|
||||
if v.IPAddress.IsValid() {
|
||||
c.Network = k // update network to the first network
|
||||
c.PrivateHostname = v.IPAddress
|
||||
c.PrivateHostname = v.IPAddress.String()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package docker
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/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/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
|
||||
@@ -87,8 +87,8 @@ func ExpandWildcard(labels map[string]string, aliases ...string) {
|
||||
wildcardLabels[parts[2]] = value
|
||||
continue
|
||||
}
|
||||
// explicit alias label – remember the alias
|
||||
if _, ok := aliasSet[alias]; !ok {
|
||||
// explicit alias label – remember the alias (but not reference aliases like #1, #2)
|
||||
if _, ok := aliasSet[alias]; !ok && !strings.HasPrefix(alias, "#") {
|
||||
aliasSet[alias] = len(aliasSet)
|
||||
}
|
||||
}
|
||||
@@ -100,10 +100,9 @@ func ExpandWildcard(labels map[string]string, aliases ...string) {
|
||||
// expand collected wildcard labels for every alias
|
||||
for suffix, v := range wildcardLabels {
|
||||
for alias, i := range aliasSet {
|
||||
// for FQDN aliases, use numeric index instead of the alias name
|
||||
if strings.Contains(alias, ".") {
|
||||
alias = fmt.Sprintf("#%d", i+1)
|
||||
}
|
||||
// use numeric index instead of the alias name
|
||||
alias = fmt.Sprintf("#%d", i+1)
|
||||
|
||||
key := fmt.Sprintf("%s.%s.%s", NSProxy, alias, suffix)
|
||||
if suffix == "" { // this should not happen (root wildcard handled earlier) but keep safe
|
||||
key = fmt.Sprintf("%s.%s", NSProxy, alias)
|
||||
|
||||
@@ -67,6 +67,21 @@ healthcheck:
|
||||
}, labels)
|
||||
}
|
||||
|
||||
func TestWildcardWithRefAliases(t *testing.T) {
|
||||
labels := map[string]string{
|
||||
"proxy.#1.host": "localhost",
|
||||
"proxy.#1.port": "5555",
|
||||
"proxy.*.middlewares.request.hide_headers": "X-Header1,X-Header2",
|
||||
}
|
||||
docker.ExpandWildcard(labels, "a.example.com", "b.example.com")
|
||||
require.Equal(t, map[string]string{
|
||||
"proxy.#1.host": "localhost",
|
||||
"proxy.#1.port": "5555",
|
||||
"proxy.#1.middlewares.request.hide_headers": "X-Header1,X-Header2",
|
||||
"proxy.#2.middlewares.request.hide_headers": "X-Header1,X-Header2",
|
||||
}, labels)
|
||||
}
|
||||
|
||||
func BenchmarkParseLabels(b *testing.B) {
|
||||
for b.Loop() {
|
||||
_, _ = docker.ParseLabels(map[string]string{
|
||||
|
||||
@@ -3,11 +3,11 @@ package docker
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
var listOptions = container.ListOptions{
|
||||
var listOptions = client.ContainerListOptions{
|
||||
// created|restarting|running|removing|paused|exited|dead
|
||||
// Filters: filters.NewArgs(
|
||||
// filters.Arg("status", "created"),
|
||||
@@ -30,7 +30,7 @@ func ListContainers(ctx context.Context, clientHost string) ([]container.Summary
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return containers, nil
|
||||
return containers.Items, nil
|
||||
}
|
||||
|
||||
func IsErrConnectionFailed(err error) bool {
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
type Entrypoint struct {
|
||||
middleware *middleware.Middleware
|
||||
notFoundHandler http.Handler
|
||||
accessLogger *accesslog.AccessLogger
|
||||
accessLogger accesslog.AccessLogger
|
||||
findRouteFunc func(host string) types.HTTPRoute
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
func (w *Watcher) canceled(reqCtx context.Context) bool {
|
||||
select {
|
||||
case <-reqCtx.Done():
|
||||
w.l.Debug().AnErr("cause", context.Cause(reqCtx)).Msg("wake canceled")
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) waitStarted(reqCtx context.Context) bool {
|
||||
select {
|
||||
case <-reqCtx.Done():
|
||||
return false
|
||||
case <-w.route.Started():
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func (w *Watcher) newDepError(action string, dep *dependency, err error) error {
|
||||
if dErr, ok := err.(*depError); ok { //nolint:errorlint
|
||||
return dErr
|
||||
}
|
||||
return w.newWatcherError(&depError{action: action, dep: dep, err: convertError(err)})
|
||||
return &depError{action: action, dep: dep, err: convertError(err)}
|
||||
}
|
||||
|
||||
func convertError(err error) error {
|
||||
|
||||
76
internal/idlewatcher/events.go
Normal file
76
internal/idlewatcher/events.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
type WakeEvent struct {
|
||||
Type string `json:"type"`
|
||||
Message string `json:"message"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type WakeEventType string
|
||||
|
||||
const (
|
||||
WakeEventStarting WakeEventType = "starting"
|
||||
WakeEventWakingDep WakeEventType = "waking_dep"
|
||||
WakeEventDepReady WakeEventType = "dep_ready"
|
||||
WakeEventContainerWoke WakeEventType = "container_woke"
|
||||
WakeEventWaitingReady WakeEventType = "waiting_ready"
|
||||
WakeEventReady WakeEventType = "ready"
|
||||
WakeEventError WakeEventType = "error"
|
||||
)
|
||||
|
||||
func (w *Watcher) newWakeEvent(eventType WakeEventType, message string, err error) *WakeEvent {
|
||||
event := &WakeEvent{
|
||||
Type: string(eventType),
|
||||
Message: message,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
if err != nil {
|
||||
event.Error = err.Error()
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
func (e *WakeEvent) WriteSSE(w io.Writer) error {
|
||||
data, err := sonic.Marshal(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintf(w, "data: %s\n\n", data)
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *Watcher) clearEventHistory() {
|
||||
w.eventHistoryMu.Lock()
|
||||
w.eventHistory = w.eventHistory[:0]
|
||||
w.eventHistoryMu.Unlock()
|
||||
}
|
||||
|
||||
func (w *Watcher) sendEvent(eventType WakeEventType, message string, err error) {
|
||||
// NOTE: events will be cleared on stop/pause
|
||||
event := w.newWakeEvent(eventType, message, err)
|
||||
|
||||
w.l.Debug().Str("event", string(eventType)).Str("message", message).Err(err).Msg("sending event")
|
||||
|
||||
// Store event in history
|
||||
w.eventHistoryMu.Lock()
|
||||
w.eventHistory = append(w.eventHistory, *event)
|
||||
w.eventHistoryMu.Unlock()
|
||||
|
||||
// Broadcast to current subscribers
|
||||
for ch := range w.eventChs.Range {
|
||||
select {
|
||||
case ch <- event:
|
||||
default:
|
||||
// channel full, drop event
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,15 @@ package idlewatcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
|
||||
_ "unsafe"
|
||||
)
|
||||
@@ -43,16 +45,58 @@ func (w *Watcher) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func isFaviconPath(path string) bool {
|
||||
return path == "/favicon.ico"
|
||||
}
|
||||
func (w *Watcher) handleWakeEventsSSE(rw http.ResponseWriter, r *http.Request) {
|
||||
// Create a dedicated channel for this SSE connection and register it
|
||||
eventCh := make(chan *WakeEvent, 10)
|
||||
w.eventChs.Store(eventCh, struct{}{})
|
||||
// Clean up when done
|
||||
defer func() {
|
||||
w.eventChs.Delete(eventCh)
|
||||
close(eventCh)
|
||||
}()
|
||||
|
||||
func (w *Watcher) redirectToStartEndpoint(rw http.ResponseWriter, r *http.Request) {
|
||||
uri := "/"
|
||||
if w.cfg.StartEndpoint != "" {
|
||||
uri = w.cfg.StartEndpoint
|
||||
// Set SSE headers
|
||||
rw.Header().Set("Content-Type", "text/event-stream")
|
||||
rw.Header().Set("Cache-Control", "no-cache")
|
||||
rw.Header().Set("Connection", "keep-alive")
|
||||
rw.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
rw.Header().Set("Access-Control-Allow-Headers", "Cache-Control")
|
||||
|
||||
controller := http.NewResponseController(rw)
|
||||
ctx := r.Context()
|
||||
|
||||
// Send historical events first
|
||||
w.eventHistoryMu.RLock()
|
||||
historicalEvents := make([]WakeEvent, len(w.eventHistory))
|
||||
copy(historicalEvents, w.eventHistory)
|
||||
w.eventHistoryMu.RUnlock()
|
||||
|
||||
for _, event := range historicalEvents {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
err := errors.Join(event.WriteSSE(rw), controller.Flush())
|
||||
if err != nil {
|
||||
gperr.LogError("Failed to write SSE event", err, &w.l)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for new events and send them to client
|
||||
for {
|
||||
select {
|
||||
case event := <-eventCh:
|
||||
err := errors.Join(event.WriteSSE(rw), controller.Flush())
|
||||
if err != nil {
|
||||
gperr.LogError("Failed to write SSE event", err, &w.l)
|
||||
return
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Redirect(rw, r, uri, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (w *Watcher) getFavIcon(ctx context.Context) (result homepage.FetchResult, err error) {
|
||||
@@ -74,26 +118,43 @@ func (w *Watcher) getFavIcon(ctx context.Context) (result homepage.FetchResult,
|
||||
return result, err
|
||||
}
|
||||
|
||||
func serveStaticContent(rw http.ResponseWriter, status int, contentType string, content []byte) {
|
||||
rw.Header().Set("Content-Type", contentType)
|
||||
rw.Header().Set("Content-Length", strconv.Itoa(len(content)))
|
||||
rw.WriteHeader(status)
|
||||
rw.Write(content)
|
||||
}
|
||||
|
||||
func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldNext bool) {
|
||||
w.resetIdleTimer()
|
||||
|
||||
// pass through if container is already ready
|
||||
if w.ready() {
|
||||
return true
|
||||
}
|
||||
|
||||
// handle favicon request
|
||||
if isFaviconPath(r.URL.Path) {
|
||||
// handle static files
|
||||
switch r.URL.Path {
|
||||
case idlewatcher.FavIconPath:
|
||||
result, err := w.getFavIcon(r.Context())
|
||||
if err != nil {
|
||||
rw.WriteHeader(result.StatusCode)
|
||||
fmt.Fprint(rw, err)
|
||||
return false
|
||||
}
|
||||
rw.Header().Set("Content-Type", result.ContentType())
|
||||
rw.WriteHeader(result.StatusCode)
|
||||
rw.Write(result.Icon)
|
||||
serveStaticContent(rw, result.StatusCode, result.ContentType(), result.Icon)
|
||||
return false
|
||||
case idlewatcher.LoadingPageCSSPath:
|
||||
serveStaticContent(rw, http.StatusOK, "text/css", cssBytes)
|
||||
return false
|
||||
case idlewatcher.LoadingPageJSPath:
|
||||
serveStaticContent(rw, http.StatusOK, "application/javascript", jsBytes)
|
||||
return false
|
||||
case idlewatcher.WakeEventsPath:
|
||||
w.handleWakeEventsSSE(rw, r)
|
||||
return false
|
||||
}
|
||||
|
||||
// Allow request to proceed if the container is already ready.
|
||||
// This check occurs after serving static files because a container can become ready quickly;
|
||||
// otherwise, requests for assets may get a 404, leaving the user stuck on the loading screen.
|
||||
if w.ready() {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if start endpoint is configured and request path matches
|
||||
@@ -105,54 +166,26 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
|
||||
accept := httputils.GetAccept(r.Header)
|
||||
acceptHTML := (r.Method == http.MethodGet && accept.AcceptHTML() || r.RequestURI == "/" && accept.IsEmpty())
|
||||
|
||||
isCheckRedirect := r.Header.Get(httpheaders.HeaderGoDoxyCheckRedirect) != ""
|
||||
if !isCheckRedirect && acceptHTML {
|
||||
// Send a loading response to the client
|
||||
body := w.makeLoadingPageBody()
|
||||
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
rw.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
||||
rw.Header().Set("Cache-Control", "no-cache")
|
||||
rw.Header().Add("Cache-Control", "no-store")
|
||||
rw.Header().Add("Cache-Control", "must-revalidate")
|
||||
rw.Header().Add("Connection", "close")
|
||||
if _, err := rw.Write(body); err != nil {
|
||||
err := w.Wake(r.Context())
|
||||
if err != nil {
|
||||
gperr.LogError("Failed to wake container", err, &w.l)
|
||||
if !acceptHTML {
|
||||
http.Error(rw, "Failed to wake container", http.StatusInternalServerError)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if !acceptHTML {
|
||||
serveStaticContent(rw, http.StatusOK, "text/plain", []byte("Container woken"))
|
||||
return false
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
if w.canceled(ctx) {
|
||||
w.redirectToStartEndpoint(rw, r)
|
||||
return false
|
||||
}
|
||||
|
||||
w.l.Trace().Msg("signal received")
|
||||
err := w.Wake(ctx)
|
||||
if err != nil {
|
||||
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
|
||||
httputils.LogError(r).Msg(fmt.Sprintf("failed to wake: %v", err))
|
||||
return false
|
||||
}
|
||||
|
||||
// Wait for route to be started
|
||||
if !w.waitStarted(ctx) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Wait for container to become ready
|
||||
if !w.waitForReady(ctx) {
|
||||
if w.canceled(ctx) {
|
||||
w.redirectToStartEndpoint(rw, r)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if isCheckRedirect {
|
||||
w.l.Debug().Stringer("url", w.hc.URL()).Msg("container is ready, redirecting")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return false
|
||||
}
|
||||
w.l.Debug().Stringer("url", w.hc.URL()).Msg("container is ready, passing through")
|
||||
return true
|
||||
// Send a loading response to the client
|
||||
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
rw.Header().Set("Cache-Control", "no-cache")
|
||||
rw.Header().Add("Cache-Control", "no-store")
|
||||
rw.Header().Add("Cache-Control", "must-revalidate")
|
||||
rw.Header().Add("Connection", "close")
|
||||
_ = w.writeLoadingPage(rw)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -154,9 +154,12 @@ func (w *Watcher) checkUpdateState() (ready bool, err error) {
|
||||
// log every 3 seconds
|
||||
const everyN = int(3 * time.Second / idleWakerCheckInterval)
|
||||
if newHealthTries%everyN == 0 {
|
||||
url := w.hc.URL()
|
||||
w.l.Debug().
|
||||
Int("health_tries", newHealthTries).
|
||||
Dur("elapsed", time.Since(state.startedAt)).
|
||||
Str("url", url.String()).
|
||||
Str("detail", res.Detail).
|
||||
Msg("health check failed, still starting")
|
||||
}
|
||||
|
||||
|
||||
115
internal/idlewatcher/html/loading.js
Normal file
115
internal/idlewatcher/html/loading.js
Normal file
@@ -0,0 +1,115 @@
|
||||
let ready = false;
|
||||
|
||||
window.onload = async function () {
|
||||
const consoleEl = document.getElementById("console");
|
||||
const loadingDotsEl = document.getElementById("loading-dots");
|
||||
|
||||
if (!consoleEl || !loadingDotsEl) {
|
||||
console.error("Required DOM elements not found");
|
||||
return;
|
||||
}
|
||||
|
||||
function formatTimestamp(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString("en-US", {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
fractionalSecondDigits: 3,
|
||||
});
|
||||
}
|
||||
|
||||
function addConsoleLine(type, message, timestamp) {
|
||||
const line = document.createElement("div");
|
||||
line.className = `console-line ${type}`;
|
||||
|
||||
const timestampEl = document.createElement("span");
|
||||
timestampEl.className = "console-timestamp";
|
||||
timestampEl.textContent = formatTimestamp(timestamp);
|
||||
|
||||
const messageEl = document.createElement("span");
|
||||
messageEl.className = "console-message";
|
||||
messageEl.textContent = message;
|
||||
|
||||
line.appendChild(timestampEl);
|
||||
line.appendChild(messageEl);
|
||||
|
||||
consoleEl.appendChild(line);
|
||||
consoleEl.scrollTop = consoleEl.scrollHeight;
|
||||
}
|
||||
|
||||
if (typeof wakeEventsPath === "undefined" || !wakeEventsPath) {
|
||||
addConsoleLine(
|
||||
"error",
|
||||
"Configuration error: wakeEventsPath not defined",
|
||||
new Date().toISOString()
|
||||
);
|
||||
loadingDotsEl.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof EventSource === "undefined") {
|
||||
addConsoleLine(
|
||||
"error",
|
||||
"Browser does not support Server-Sent Events",
|
||||
new Date().toISOString()
|
||||
);
|
||||
loadingDotsEl.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to SSE endpoint
|
||||
const eventSource = new EventSource(wakeEventsPath);
|
||||
|
||||
eventSource.onmessage = function (event) {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(event.data);
|
||||
} catch (error) {
|
||||
addConsoleLine(
|
||||
"error",
|
||||
"Invalid event data: " + event.data,
|
||||
new Date().toISOString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === "ready") {
|
||||
ready = true;
|
||||
// Container is ready, hide loading dots and refresh
|
||||
loadingDotsEl.style.display = "none";
|
||||
addConsoleLine(
|
||||
data.type,
|
||||
"Container is ready, refreshing...",
|
||||
data.timestamp
|
||||
);
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 200);
|
||||
} else if (data.type === "error") {
|
||||
// Show error message and hide loading dots
|
||||
const errorMessage = data.error || data.message;
|
||||
addConsoleLine(data.type, errorMessage, data.timestamp);
|
||||
loadingDotsEl.style.display = "none";
|
||||
eventSource.close();
|
||||
} else {
|
||||
// Show other message types
|
||||
addConsoleLine(data.type, data.message, data.timestamp);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = function (event) {
|
||||
if (ready) {
|
||||
// event will be closed by the server
|
||||
return;
|
||||
}
|
||||
addConsoleLine(
|
||||
"error",
|
||||
"Connection lost. Please refresh the page.",
|
||||
new Date().toISOString()
|
||||
);
|
||||
loadingDotsEl.style.display = "none";
|
||||
eventSource.close();
|
||||
};
|
||||
};
|
||||
@@ -1,138 +1,32 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{.Title}}</title>
|
||||
<style>
|
||||
/* size variables */
|
||||
:root {
|
||||
--dot-size: 12px;
|
||||
--logo-size: 100px;
|
||||
}
|
||||
/* Global Styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
font-family:
|
||||
"Inter",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
"Open Sans",
|
||||
"Helvetica Neue",
|
||||
sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: #f8f9fa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
gap: 32px;
|
||||
background: linear-gradient(135deg, #121212 0%, #1e1e1e 100%);
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
border-radius: 16px;
|
||||
background-color: rgba(30, 30, 30, 0.6);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(8px);
|
||||
max-width: 90%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Spinner Styles */
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.dot {
|
||||
width: var(--dot-size);
|
||||
height: var(--dot-size);
|
||||
background-color: #66d9ef;
|
||||
border-radius: 50%;
|
||||
animation: bounce 1.3s infinite ease-in-out;
|
||||
}
|
||||
.dot:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
.dot:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Message Styles */
|
||||
.message {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: #f8f9fa;
|
||||
max-width: 500px;
|
||||
letter-spacing: 0.3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Logo */
|
||||
.logo {
|
||||
width: var(--logo-size);
|
||||
height: var(--logo-size);
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="{{.LoadingPageCSSPath}}" />
|
||||
<link rel="icon" href="{{.FavIconPath}}" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
const wakeEventsPath = "{{.WakeEventsPath}}";
|
||||
</script>
|
||||
<script src="{{.LoadingPageJSPath}}" defer></script>
|
||||
<div class="container">
|
||||
<!-- icon handled by waker_http -->
|
||||
<img class="logo" src="/favicon.ico" />
|
||||
<img class="logo" src="{{.FavIconPath}}" />
|
||||
<div id="loading-dots" class="loading-dots">
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
<div id="message" class="message">{{.Message}}</div>
|
||||
<div id="console" class="console"></div>
|
||||
</div>
|
||||
<script>
|
||||
window.onload = async function () {
|
||||
let resp = await fetch(window.location.href, {
|
||||
headers: {
|
||||
"{{.CheckRedirectHeader}}": "1",
|
||||
},
|
||||
});
|
||||
if (resp.ok) {
|
||||
window.location.href = resp.url;
|
||||
} else {
|
||||
document.getElementById("message").innerText = await resp.text();
|
||||
document.getElementById("loading-dots").remove();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
194
internal/idlewatcher/html/style.css
Normal file
194
internal/idlewatcher/html/style.css
Normal file
@@ -0,0 +1,194 @@
|
||||
/* size variables */
|
||||
:root {
|
||||
--dot-size: 12px;
|
||||
--logo-size: 100px;
|
||||
}
|
||||
/* Global Styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: #f8f9fa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #121212 0%, #1e1e1e 100%);
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
border-radius: 16px;
|
||||
background-color: rgba(30, 30, 30, 0.9);
|
||||
box-shadow: 0 16px 64px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(12px);
|
||||
max-width: 90%;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 600px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
min-width: auto;
|
||||
max-width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Spinner Styles */
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.dot {
|
||||
width: var(--dot-size);
|
||||
height: var(--dot-size);
|
||||
background-color: #66d9ef;
|
||||
border-radius: 50%;
|
||||
animation: bounce 1.3s infinite ease-in-out;
|
||||
}
|
||||
.dot:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
.dot:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Message Styles */
|
||||
.message {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
color: #f8f9fa;
|
||||
max-width: 100%;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* Console Styles */
|
||||
.console {
|
||||
font-family: "Fira Code", "Consolas", "Monaco", "Courier New", monospace;
|
||||
font-size: 14px;
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
color: #e0e0e0;
|
||||
overflow-y: auto;
|
||||
max-height: 300px;
|
||||
min-height: 200px;
|
||||
width: 100%;
|
||||
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.console-line {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 4px;
|
||||
padding: 2px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.console-line:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.console-timestamp {
|
||||
color: #66d9ef;
|
||||
margin-right: 12px;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.console-message {
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.console-line.starting .console-message {
|
||||
color: #f9f871;
|
||||
}
|
||||
|
||||
.console-line.waking_dep .console-message {
|
||||
color: #66d9ef;
|
||||
}
|
||||
|
||||
.console-line.dep_ready .console-message {
|
||||
color: #a6e22e;
|
||||
}
|
||||
|
||||
.console-line.container_woke .console-message {
|
||||
color: #a6e22e;
|
||||
}
|
||||
|
||||
.console-line.waiting_ready .console-message {
|
||||
color: #fd971f;
|
||||
}
|
||||
|
||||
.console-line.ready .console-message {
|
||||
color: #a6e22e;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.console-line.error .console-message {
|
||||
color: #f92672;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Loading dots in console */
|
||||
.console-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.console-loading-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #66d9ef;
|
||||
border-radius: 50%;
|
||||
animation: bounce 1.3s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.console-loading-dot:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.console-loading-dot:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
/* Logo */
|
||||
.logo {
|
||||
width: var(--logo-size);
|
||||
height: var(--logo-size);
|
||||
}
|
||||
@@ -1,35 +1,49 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"text/template"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
|
||||
)
|
||||
|
||||
type templateData struct {
|
||||
CheckRedirectHeader string
|
||||
Title string
|
||||
Message string
|
||||
Title string
|
||||
Message string
|
||||
|
||||
FavIconPath string
|
||||
LoadingPageCSSPath string
|
||||
LoadingPageJSPath string
|
||||
WakeEventsPath string
|
||||
}
|
||||
|
||||
//go:embed html/loading_page.html
|
||||
var loadingPage []byte
|
||||
var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage)))
|
||||
|
||||
func (w *Watcher) makeLoadingPageBody() []byte {
|
||||
//go:embed html/style.css
|
||||
var cssBytes []byte
|
||||
|
||||
//go:embed html/loading.js
|
||||
var jsBytes []byte
|
||||
|
||||
func (w *Watcher) writeLoadingPage(rw http.ResponseWriter) error {
|
||||
msg := w.cfg.ContainerName() + " is starting..."
|
||||
|
||||
data := new(templateData)
|
||||
data.CheckRedirectHeader = httpheaders.HeaderGoDoxyCheckRedirect
|
||||
data.Title = w.cfg.ContainerName()
|
||||
data.Message = msg
|
||||
data.FavIconPath = idlewatcher.FavIconPath
|
||||
data.LoadingPageCSSPath = idlewatcher.LoadingPageCSSPath
|
||||
data.LoadingPageJSPath = idlewatcher.LoadingPageJSPath
|
||||
data.WakeEventsPath = idlewatcher.WakeEventsPath
|
||||
|
||||
buf := bytes.NewBuffer(make([]byte, len(loadingPage)+len(data.Title)+len(data.Message)+len(httpheaders.HeaderGoDoxyCheckRedirect)))
|
||||
err := loadingPageTmpl.Execute(buf, data)
|
||||
if err != nil { // should never happen in production
|
||||
panic(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
rw.Header().Set("Cache-Control", "no-cache")
|
||||
rw.Header().Add("Cache-Control", "no-store")
|
||||
rw.Header().Add("Cache-Control", "must-revalidate")
|
||||
rw.Header().Add("Connection", "close")
|
||||
err := loadingPageTmpl.Execute(rw, data)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ package provider
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
@@ -17,7 +18,7 @@ type DockerProvider struct {
|
||||
containerID string
|
||||
}
|
||||
|
||||
var startOptions = container.StartOptions{}
|
||||
var startOptions = client.ContainerStartOptions{}
|
||||
|
||||
func NewDockerProvider(dockerHost, containerID string) (idlewatcher.Provider, error) {
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
@@ -32,47 +33,54 @@ func NewDockerProvider(dockerHost, containerID string) (idlewatcher.Provider, er
|
||||
}
|
||||
|
||||
func (p *DockerProvider) ContainerPause(ctx context.Context) error {
|
||||
return p.client.ContainerPause(ctx, p.containerID)
|
||||
_, err := p.client.ContainerPause(ctx, p.containerID, client.ContainerPauseOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *DockerProvider) ContainerUnpause(ctx context.Context) error {
|
||||
return p.client.ContainerUnpause(ctx, p.containerID)
|
||||
_, err := p.client.ContainerUnpause(ctx, p.containerID, client.ContainerUnpauseOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *DockerProvider) ContainerStart(ctx context.Context) error {
|
||||
return p.client.ContainerStart(ctx, p.containerID, startOptions)
|
||||
_, err := p.client.ContainerStart(ctx, p.containerID, startOptions)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *DockerProvider) ContainerStop(ctx context.Context, signal types.ContainerSignal, timeout int) error {
|
||||
return p.client.ContainerStop(ctx, p.containerID, container.StopOptions{
|
||||
_, err := p.client.ContainerStop(ctx, p.containerID, client.ContainerStopOptions{
|
||||
Signal: string(signal),
|
||||
Timeout: &timeout,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *DockerProvider) ContainerKill(ctx context.Context, signal types.ContainerSignal) error {
|
||||
return p.client.ContainerKill(ctx, p.containerID, string(signal))
|
||||
_, err := p.client.ContainerKill(ctx, p.containerID, client.ContainerKillOptions{
|
||||
Signal: string(signal),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *DockerProvider) ContainerStatus(ctx context.Context) (idlewatcher.ContainerStatus, error) {
|
||||
status, err := p.client.ContainerInspect(ctx, p.containerID)
|
||||
status, err := p.client.ContainerInspect(ctx, p.containerID, client.ContainerInspectOptions{})
|
||||
if err != nil {
|
||||
return idlewatcher.ContainerStatusError, err
|
||||
}
|
||||
switch status.State.Status {
|
||||
case "running":
|
||||
switch status.Container.State.Status {
|
||||
case container.StateRunning:
|
||||
return idlewatcher.ContainerStatusRunning, nil
|
||||
case "exited", "dead", "restarting":
|
||||
case container.StateExited, container.StateDead, container.StateRestarting:
|
||||
return idlewatcher.ContainerStatusStopped, nil
|
||||
case "paused":
|
||||
case container.StatePaused:
|
||||
return idlewatcher.ContainerStatusPaused, nil
|
||||
}
|
||||
return idlewatcher.ContainerStatusError, idlewatcher.ErrUnexpectedContainerStatus.Subject(status.State.Status)
|
||||
return idlewatcher.ContainerStatusError, idlewatcher.ErrUnexpectedContainerStatus.Subject(string(status.Container.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.NewDockerFilter(
|
||||
Filters: watcher.NewDockerFilters(
|
||||
watcher.DockerFilterContainer,
|
||||
watcher.DockerFilterContainerNameID(p.containerID),
|
||||
watcher.DockerFilterStart,
|
||||
|
||||
@@ -24,6 +24,8 @@ func (w *Watcher) setReady() {
|
||||
status: idlewatcher.ContainerStatusRunning,
|
||||
ready: true,
|
||||
})
|
||||
// Send ready event via SSE
|
||||
w.sendEvent(WakeEventReady, w.cfg.ContainerName()+" is ready!", nil)
|
||||
// Notify waiting handlers that container is ready
|
||||
select {
|
||||
case w.readyNotifyCh <- struct{}{}:
|
||||
@@ -42,6 +44,7 @@ func (w *Watcher) setStarting() {
|
||||
}
|
||||
|
||||
func (w *Watcher) setNapping(status idlewatcher.ContainerStatus) {
|
||||
w.clearEventHistory() // Clear events on stop/pause
|
||||
w.state.Store(&containerState{
|
||||
status: status,
|
||||
ready: false,
|
||||
@@ -51,6 +54,7 @@ func (w *Watcher) setNapping(status idlewatcher.ContainerStatus) {
|
||||
}
|
||||
|
||||
func (w *Watcher) setError(err error) {
|
||||
w.sendEvent(WakeEventError, "Container error", err)
|
||||
w.state.Store(&containerState{
|
||||
status: idlewatcher.ContainerStatusError,
|
||||
ready: false,
|
||||
@@ -76,3 +80,12 @@ func (w *Watcher) waitForReady(ctx context.Context) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) waitStarted(reqCtx context.Context) bool {
|
||||
select {
|
||||
case <-reqCtx.Done():
|
||||
return false
|
||||
case <-w.route.Started():
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
8
internal/idlewatcher/types/paths.go
Normal file
8
internal/idlewatcher/types/paths.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package idlewatcher
|
||||
|
||||
const (
|
||||
FavIconPath = "/favicon.ico"
|
||||
LoadingPageCSSPath = "/$godoxy/style.css"
|
||||
LoadingPageJSPath = "/$godoxy/loading.js"
|
||||
WakeEventsPath = "/$godoxy/wake-events"
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
@@ -11,5 +12,5 @@ type Waker interface {
|
||||
types.HealthMonitor
|
||||
http.Handler
|
||||
nettypes.Stream
|
||||
Wake() error
|
||||
Wake(ctx context.Context) error
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/ds/ordered"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"github.com/yusing/godoxy/internal/watcher/health/monitor"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
"github.com/yusing/goutils/http/reverseproxy"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
"github.com/yusing/goutils/synk"
|
||||
"github.com/yusing/goutils/task"
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -53,7 +55,7 @@ type (
|
||||
|
||||
cfg *types.IdlewatcherConfig
|
||||
|
||||
provider idlewatcher.Provider
|
||||
provider synk.Value[idlewatcher.Provider]
|
||||
|
||||
state synk.Value[*containerState]
|
||||
lastReset synk.Value[time.Time]
|
||||
@@ -63,6 +65,12 @@ type (
|
||||
readyNotifyCh chan struct{} // notifies when container becomes ready
|
||||
task *task.Task
|
||||
|
||||
// SSE event broadcasting, HTTP routes only
|
||||
eventChs *xsync.Map[chan *WakeEvent, struct{}]
|
||||
eventHistory []WakeEvent // Global event history buffer
|
||||
eventHistoryMu sync.RWMutex // Mutex for event history
|
||||
|
||||
// FIXME: missing dependencies
|
||||
dependsOn []*dependency
|
||||
}
|
||||
|
||||
@@ -77,6 +85,8 @@ type (
|
||||
|
||||
const ContextKey = "idlewatcher.watcher"
|
||||
|
||||
var _ idlewatcher.Waker = (*Watcher)(nil)
|
||||
|
||||
var (
|
||||
watcherMap = make(map[string]*Watcher)
|
||||
watcherMapMu sync.RWMutex
|
||||
@@ -115,11 +125,16 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
|
||||
}
|
||||
cfg = w.cfg
|
||||
w.resetIdleTimer()
|
||||
// Update health monitor URL with current route info on reload
|
||||
if targetURL := r.TargetURL(); targetURL != nil {
|
||||
w.hc.UpdateURL(&targetURL.URL)
|
||||
}
|
||||
} else {
|
||||
w = &Watcher{
|
||||
idleTicker: time.NewTicker(cfg.IdleTimeout),
|
||||
healthTicker: time.NewTicker(idleWakerCheckInterval),
|
||||
readyNotifyCh: make(chan struct{}, 1), // buffered to avoid blocking
|
||||
eventChs: xsync.NewMap[chan *WakeEvent, struct{}](),
|
||||
cfg: cfg,
|
||||
routeHelper: routeHelper{
|
||||
hc: monitor.NewMonitor(r),
|
||||
@@ -223,8 +238,8 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
|
||||
})
|
||||
}
|
||||
|
||||
if w.provider != nil { // it's a reload, close the old provider
|
||||
w.provider.Close()
|
||||
if pOld := w.provider.Load(); pOld != nil { // it's a reload, close the old provider
|
||||
pOld.Close()
|
||||
}
|
||||
|
||||
if depErrors.HasError() {
|
||||
@@ -247,19 +262,24 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
|
||||
p, err = provider.NewProxmoxProvider(cfg.Proxmox.Node, cfg.Proxmox.VMID)
|
||||
kind = "proxmox"
|
||||
}
|
||||
targetURL := r.TargetURL()
|
||||
if targetURL == nil {
|
||||
return nil, errors.New("target URL is not set")
|
||||
}
|
||||
w.l = log.With().
|
||||
Str("kind", kind).
|
||||
Str("container", cfg.ContainerName()).
|
||||
Str("url", targetURL.String()).
|
||||
Logger()
|
||||
|
||||
if cfg.IdleTimeout != neverTick {
|
||||
w.l = w.l.With().Stringer("idle_timeout", cfg.IdleTimeout).Logger()
|
||||
w.l = w.l.With().Str("idle_timeout", strutils.FormatDuration(cfg.IdleTimeout)).Logger()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w.provider = p
|
||||
w.provider.Store(p)
|
||||
|
||||
switch r := r.(type) {
|
||||
case types.ReverseProxyRoute:
|
||||
@@ -267,22 +287,22 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
|
||||
case types.StreamRoute:
|
||||
w.stream = r.Stream()
|
||||
default:
|
||||
w.provider.Close()
|
||||
p.Close()
|
||||
return nil, w.newWatcherError(gperr.Errorf("unexpected route type: %T", r))
|
||||
}
|
||||
w.route = r
|
||||
|
||||
ctx, cancel := context.WithTimeout(parent.Context(), reqTimeout)
|
||||
defer cancel()
|
||||
status, err := w.provider.ContainerStatus(ctx)
|
||||
status, err := p.ContainerStatus(ctx)
|
||||
if err != nil {
|
||||
w.provider.Close()
|
||||
p.Close()
|
||||
return nil, w.newWatcherError(err)
|
||||
}
|
||||
w.state.Store(&containerState{status: status})
|
||||
|
||||
// when more providers are added, we need to add a new case here.
|
||||
switch p := w.provider.(type) { //nolint:gocritic
|
||||
switch p := p.(type) { //nolint:gocritic
|
||||
case *provider.ProxmoxProvider:
|
||||
shutdownTimeout := max(time.Second, cfg.StopTimeout-idleWakerCheckTimeout)
|
||||
err = p.LXCSetShutdownTimeout(ctx, cfg.Proxmox.VMID, shutdownTimeout)
|
||||
@@ -302,16 +322,17 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
|
||||
delete(watcherMap, key)
|
||||
watcherMapMu.Unlock()
|
||||
|
||||
if errors.Is(cause, causeContainerDestroy) || errors.Is(cause, task.ErrProgramExiting) || errors.Is(cause, config.ErrConfigChanged) {
|
||||
if errors.Is(cause, causeReload) {
|
||||
// no log
|
||||
} else if errors.Is(cause, causeContainerDestroy) || errors.Is(cause, task.ErrProgramExiting) || errors.Is(cause, config.ErrConfigChanged) {
|
||||
w.l.Info().Msg("idlewatcher stopped")
|
||||
} else if !errors.Is(cause, causeReload) {
|
||||
} else {
|
||||
gperr.LogError("idlewatcher stopped unexpectedly", cause, &w.l)
|
||||
}
|
||||
|
||||
w.idleTicker.Stop()
|
||||
w.healthTicker.Stop()
|
||||
close(w.readyNotifyCh)
|
||||
w.provider.Close()
|
||||
w.task.Finish(cause)
|
||||
}()
|
||||
}
|
||||
@@ -353,13 +374,27 @@ func (w *Watcher) Key() string {
|
||||
func (w *Watcher) Wake(ctx context.Context) error {
|
||||
// wake dependencies first.
|
||||
if err := w.wakeDependencies(ctx); err != nil {
|
||||
w.sendEvent(WakeEventError, "Failed to wake dependencies", err)
|
||||
return w.newWatcherError(err)
|
||||
}
|
||||
|
||||
if w.wakeInProgress() {
|
||||
w.l.Debug().Msg("already starting, ignoring duplicate start event")
|
||||
return nil
|
||||
}
|
||||
|
||||
// wake itself.
|
||||
// use container name instead of Key() here as the container id will change on restart (docker).
|
||||
_, err, _ := singleFlight.Do(w.cfg.ContainerName(), func() (any, error) {
|
||||
return nil, w.wakeIfStopped(ctx)
|
||||
containerName := w.cfg.ContainerName()
|
||||
_, err, _ := singleFlight.Do(containerName, func() (any, error) {
|
||||
err := w.wakeIfStopped(ctx)
|
||||
if err != nil {
|
||||
w.sendEvent(WakeEventError, "Failed to start "+containerName, err)
|
||||
} else {
|
||||
w.sendEvent(WakeEventContainerWoke, containerName+" started successfully", nil)
|
||||
w.sendEvent(WakeEventWaitingReady, "Waiting for "+containerName+" to be ready...", nil)
|
||||
}
|
||||
return nil, err
|
||||
})
|
||||
if err != nil {
|
||||
return w.newWatcherError(err)
|
||||
@@ -368,6 +403,14 @@ func (w *Watcher) Wake(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Watcher) wakeInProgress() bool {
|
||||
state := w.state.Load()
|
||||
if state == nil {
|
||||
return false
|
||||
}
|
||||
return !state.startedAt.IsZero()
|
||||
}
|
||||
|
||||
func (w *Watcher) wakeDependencies(ctx context.Context) error {
|
||||
if len(w.dependsOn) == 0 {
|
||||
return nil
|
||||
@@ -375,10 +418,16 @@ func (w *Watcher) wakeDependencies(ctx context.Context) error {
|
||||
|
||||
errs := errgroup.Group{}
|
||||
for _, dep := range w.dependsOn {
|
||||
if dep.wakeInProgress() {
|
||||
w.l.Debug().Str("dep", dep.cfg.ContainerName()).Msg("dependency already starting, ignoring duplicate start event")
|
||||
continue
|
||||
}
|
||||
errs.Go(func() error {
|
||||
w.sendEvent(WakeEventWakingDep, "Waking dependency: "+dep.cfg.ContainerName(), nil)
|
||||
if err := dep.Wake(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
w.sendEvent(WakeEventDepReady, "Dependency woke: "+dep.cfg.ContainerName(), nil)
|
||||
if dep.waitHealthy {
|
||||
// initial health check before starting the ticker
|
||||
if h, err := dep.hc.CheckHealth(); err != nil {
|
||||
@@ -417,13 +466,17 @@ func (w *Watcher) wakeIfStopped(ctx context.Context) error {
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, w.cfg.WakeTimeout)
|
||||
defer cancel()
|
||||
p := w.provider.Load()
|
||||
if p == nil {
|
||||
return gperr.Errorf("provider not set")
|
||||
}
|
||||
switch state.status {
|
||||
case idlewatcher.ContainerStatusStopped:
|
||||
w.l.Info().Msg("starting container")
|
||||
return w.provider.ContainerStart(ctx)
|
||||
w.sendEvent(WakeEventStarting, w.cfg.ContainerName()+" is starting...", nil)
|
||||
return p.ContainerStart(ctx)
|
||||
case idlewatcher.ContainerStatusPaused:
|
||||
w.l.Info().Msg("unpausing container")
|
||||
return w.provider.ContainerUnpause(ctx)
|
||||
w.sendEvent(WakeEventStarting, w.cfg.ContainerName()+" is unpausing...", nil)
|
||||
return p.ContainerUnpause(ctx)
|
||||
default:
|
||||
return gperr.Errorf("unexpected container status: %s", state.status)
|
||||
}
|
||||
@@ -458,13 +511,17 @@ func (w *Watcher) stopByMethod() error {
|
||||
|
||||
// stop itself first.
|
||||
var err error
|
||||
p := w.provider.Load()
|
||||
if p == nil {
|
||||
return gperr.New("provider not set")
|
||||
}
|
||||
switch cfg.StopMethod {
|
||||
case types.ContainerStopMethodPause:
|
||||
err = w.provider.ContainerPause(ctx)
|
||||
err = p.ContainerPause(ctx)
|
||||
case types.ContainerStopMethodStop:
|
||||
err = w.provider.ContainerStop(ctx, cfg.StopSignal, int(math.Ceil(cfg.StopTimeout.Seconds())))
|
||||
err = p.ContainerStop(ctx, cfg.StopSignal, int(math.Ceil(cfg.StopTimeout.Seconds())))
|
||||
case types.ContainerStopMethodKill:
|
||||
err = w.provider.ContainerKill(ctx, cfg.StopSignal)
|
||||
err = p.ContainerKill(ctx, cfg.StopSignal)
|
||||
default:
|
||||
err = w.newWatcherError(gperr.Errorf("unexpected stop method: %q", cfg.StopMethod))
|
||||
}
|
||||
@@ -506,7 +563,12 @@ func (w *Watcher) expires() time.Time {
|
||||
// it exits only if the context is canceled, the container is destroyed,
|
||||
// errors occurred on docker client, or route provider died (mainly caused by config reload).
|
||||
func (w *Watcher) watchUntilDestroy() (returnCause error) {
|
||||
eventCh, errCh := w.provider.Watch(w.Task().Context())
|
||||
p := w.provider.Load()
|
||||
if p == nil {
|
||||
return gperr.Errorf("provider not set")
|
||||
}
|
||||
defer p.Close()
|
||||
eventCh, errCh := p.Watch(w.Task().Context())
|
||||
|
||||
for {
|
||||
select {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
|
||||
@@ -19,15 +20,24 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
AccessLogger struct {
|
||||
AccessLogger interface {
|
||||
Log(req *http.Request, res *http.Response)
|
||||
LogError(req *http.Request, err error)
|
||||
LogACL(info *maxmind.IPInfo, blocked bool)
|
||||
|
||||
Config() *Config
|
||||
|
||||
Flush()
|
||||
Close() error
|
||||
}
|
||||
|
||||
accessLogger struct {
|
||||
task *task.Task
|
||||
cfg *Config
|
||||
|
||||
rawWriter io.Writer
|
||||
closer io.Closer
|
||||
supportRotate supportRotate
|
||||
writer *ioutils.BufferedWriter
|
||||
writeLock sync.Mutex
|
||||
writer BufferedWriter
|
||||
supportRotate SupportRotate
|
||||
writeLock *sync.Mutex
|
||||
closed bool
|
||||
|
||||
writeCount int64
|
||||
@@ -41,8 +51,9 @@ type (
|
||||
ACLFormatter
|
||||
}
|
||||
|
||||
WriterWithName interface {
|
||||
Writer interface {
|
||||
io.WriteCloser
|
||||
ShouldBeBuffered() bool
|
||||
Name() string // file name or path
|
||||
}
|
||||
|
||||
@@ -52,6 +63,10 @@ type (
|
||||
Name() string
|
||||
}
|
||||
|
||||
AccessLogRotater interface {
|
||||
Rotate(result *RotateResult) (rotated bool, err error)
|
||||
}
|
||||
|
||||
RequestFormatter interface {
|
||||
// AppendRequestLog appends a log line to line with or without a trailing newline
|
||||
AppendRequestLog(line []byte, req *http.Request, res *http.Response) []byte
|
||||
@@ -62,6 +77,8 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
var writerLocks = xsync.NewMap[string, *sync.Mutex]()
|
||||
|
||||
const (
|
||||
InitialBufferSize = 4 * kilobyte
|
||||
MaxBufferSize = 8 * megabyte
|
||||
@@ -78,41 +95,43 @@ const (
|
||||
|
||||
var bytesPool = synk.GetUnsizedBytesPool()
|
||||
|
||||
func NewAccessLogger(parent task.Parent, cfg AnyConfig) (*AccessLogger, error) {
|
||||
io, err := cfg.IO()
|
||||
func NewAccessLogger(parent task.Parent, cfg AnyConfig) (AccessLogger, error) {
|
||||
writers, err := cfg.Writers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewAccessLoggerWithIO(parent, io, cfg), nil
|
||||
|
||||
return NewMultiAccessLogger(parent, cfg, writers), nil
|
||||
}
|
||||
|
||||
func NewMockAccessLogger(parent task.Parent, cfg *RequestLoggerConfig) *AccessLogger {
|
||||
return NewAccessLoggerWithIO(parent, NewMockFile(), cfg)
|
||||
func NewMockAccessLogger(parent task.Parent, cfg *RequestLoggerConfig) AccessLogger {
|
||||
return NewAccessLoggerWithIO(parent, NewMockFile(true), cfg)
|
||||
}
|
||||
|
||||
func NewAccessLoggerWithIO(parent task.Parent, writer WriterWithName, anyCfg AnyConfig) *AccessLogger {
|
||||
func NewAccessLoggerWithIO(parent task.Parent, writer Writer, anyCfg AnyConfig) AccessLogger {
|
||||
cfg := anyCfg.ToConfig()
|
||||
if cfg.RotateInterval == 0 {
|
||||
cfg.RotateInterval = defaultRotateInterval
|
||||
}
|
||||
|
||||
l := &AccessLogger{
|
||||
l := &accessLogger{
|
||||
task: parent.Subtask("accesslog."+writer.Name(), true),
|
||||
cfg: cfg,
|
||||
rawWriter: writer,
|
||||
bufSize: InitialBufferSize,
|
||||
errRateLimiter: rate.NewLimiter(rate.Every(errRateLimit), errBurst),
|
||||
logger: log.With().Str("file", writer.Name()).Logger(),
|
||||
}
|
||||
|
||||
if writer != nil {
|
||||
l.writeLock, _ = writerLocks.LoadOrStore(writer.Name(), &sync.Mutex{})
|
||||
|
||||
if writer.ShouldBeBuffered() {
|
||||
l.writer = ioutils.NewBufferedWriter(writer, InitialBufferSize)
|
||||
if supportRotate, ok := writer.(SupportRotate); ok {
|
||||
l.supportRotate = supportRotate
|
||||
}
|
||||
if closer, ok := writer.(io.Closer); ok {
|
||||
l.closer = closer
|
||||
}
|
||||
} else {
|
||||
l.writer = NewUnbufferedWriter(writer)
|
||||
}
|
||||
|
||||
if supportRotate, ok := writer.(SupportRotate); ok {
|
||||
l.supportRotate = supportRotate
|
||||
}
|
||||
|
||||
if cfg.req != nil {
|
||||
@@ -131,17 +150,15 @@ func NewAccessLoggerWithIO(parent task.Parent, writer WriterWithName, anyCfg Any
|
||||
l.ACLFormatter = ACLLogFormatter{}
|
||||
}
|
||||
|
||||
if l.writer != nil {
|
||||
go l.start()
|
||||
} // otherwise stdout only
|
||||
go l.start()
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *AccessLogger) Config() *Config {
|
||||
func (l *accessLogger) Config() *Config {
|
||||
return l.cfg
|
||||
}
|
||||
|
||||
func (l *AccessLogger) shouldLog(req *http.Request, res *http.Response) bool {
|
||||
func (l *accessLogger) shouldLog(req *http.Request, res *http.Response) bool {
|
||||
if !l.cfg.req.Filters.StatusCodes.CheckKeep(req, res) ||
|
||||
!l.cfg.req.Filters.Method.CheckKeep(req, res) ||
|
||||
!l.cfg.req.Filters.Headers.CheckKeep(req, res) ||
|
||||
@@ -151,7 +168,7 @@ func (l *AccessLogger) shouldLog(req *http.Request, res *http.Response) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *AccessLogger) Log(req *http.Request, res *http.Response) {
|
||||
func (l *accessLogger) Log(req *http.Request, res *http.Response) {
|
||||
if !l.shouldLog(req, res) {
|
||||
return
|
||||
}
|
||||
@@ -165,11 +182,11 @@ func (l *AccessLogger) Log(req *http.Request, res *http.Response) {
|
||||
bytesPool.Put(line)
|
||||
}
|
||||
|
||||
func (l *AccessLogger) LogError(req *http.Request, err error) {
|
||||
func (l *accessLogger) LogError(req *http.Request, err error) {
|
||||
l.Log(req, &http.Response{StatusCode: http.StatusInternalServerError, Status: err.Error()})
|
||||
}
|
||||
|
||||
func (l *AccessLogger) LogACL(info *maxmind.IPInfo, blocked bool) {
|
||||
func (l *accessLogger) LogACL(info *maxmind.IPInfo, blocked bool) {
|
||||
line := bytesPool.Get()
|
||||
line = l.AppendACLLog(line, info, blocked)
|
||||
if line[len(line)-1] != '\n' {
|
||||
@@ -179,16 +196,16 @@ func (l *AccessLogger) LogACL(info *maxmind.IPInfo, blocked bool) {
|
||||
bytesPool.Put(line)
|
||||
}
|
||||
|
||||
func (l *AccessLogger) ShouldRotate() bool {
|
||||
func (l *accessLogger) ShouldRotate() bool {
|
||||
return l.supportRotate != nil && l.cfg.Retention.IsValid()
|
||||
}
|
||||
|
||||
func (l *AccessLogger) Rotate(result *RotateResult) (rotated bool, err error) {
|
||||
func (l *accessLogger) Rotate(result *RotateResult) (rotated bool, err error) {
|
||||
if !l.ShouldRotate() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
l.writer.Flush()
|
||||
l.Flush()
|
||||
l.writeLock.Lock()
|
||||
defer l.writeLock.Unlock()
|
||||
|
||||
@@ -196,7 +213,7 @@ func (l *AccessLogger) Rotate(result *RotateResult) (rotated bool, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (l *AccessLogger) handleErr(err error) {
|
||||
func (l *accessLogger) handleErr(err error) {
|
||||
if l.errRateLimiter.Allow() {
|
||||
gperr.LogError("failed to write access log", err, &l.logger)
|
||||
} else {
|
||||
@@ -205,7 +222,7 @@ func (l *AccessLogger) handleErr(err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (l *AccessLogger) start() {
|
||||
func (l *accessLogger) start() {
|
||||
defer func() {
|
||||
l.Flush()
|
||||
l.Close()
|
||||
@@ -241,52 +258,42 @@ func (l *AccessLogger) start() {
|
||||
}
|
||||
}
|
||||
|
||||
func (l *AccessLogger) Close() error {
|
||||
func (l *accessLogger) Close() error {
|
||||
l.writeLock.Lock()
|
||||
defer l.writeLock.Unlock()
|
||||
if l.closed {
|
||||
return nil
|
||||
}
|
||||
if l.closer != nil {
|
||||
l.closer.Close()
|
||||
}
|
||||
l.writer.Release()
|
||||
l.writer.Flush()
|
||||
l.closed = true
|
||||
return nil
|
||||
return l.writer.Close()
|
||||
}
|
||||
|
||||
func (l *AccessLogger) Flush() {
|
||||
func (l *accessLogger) Flush() {
|
||||
l.writeLock.Lock()
|
||||
defer l.writeLock.Unlock()
|
||||
if l.closed {
|
||||
return
|
||||
}
|
||||
if err := l.writer.Flush(); err != nil {
|
||||
l.writer.Flush()
|
||||
}
|
||||
|
||||
func (l *accessLogger) write(data []byte) {
|
||||
l.writeLock.Lock()
|
||||
defer l.writeLock.Unlock()
|
||||
if l.closed {
|
||||
return
|
||||
}
|
||||
n, err := l.writer.Write(data)
|
||||
if err != nil {
|
||||
l.handleErr(err)
|
||||
} else if n < len(data) {
|
||||
l.handleErr(gperr.Errorf("%w, writing %d bytes, only %d written", io.ErrShortWrite, len(data), n))
|
||||
}
|
||||
atomic.AddInt64(&l.writeCount, int64(n))
|
||||
}
|
||||
|
||||
func (l *AccessLogger) write(data []byte) {
|
||||
if l.writer != nil {
|
||||
l.writeLock.Lock()
|
||||
defer l.writeLock.Unlock()
|
||||
if l.closed {
|
||||
return
|
||||
}
|
||||
n, err := l.writer.Write(data)
|
||||
if err != nil {
|
||||
l.handleErr(err)
|
||||
} else if n < len(data) {
|
||||
l.handleErr(gperr.Errorf("%w, writing %d bytes, only %d written", io.ErrShortWrite, len(data), n))
|
||||
}
|
||||
atomic.AddInt64(&l.writeCount, int64(n))
|
||||
}
|
||||
if l.cfg.Stdout {
|
||||
log.Logger.Write(data) // write to stdout immediately
|
||||
}
|
||||
}
|
||||
|
||||
func (l *AccessLogger) adjustBuffer() {
|
||||
func (l *accessLogger) adjustBuffer() {
|
||||
wps := int(atomic.SwapInt64(&l.writeCount, 0)) / int(bufferAdjustInterval.Seconds())
|
||||
origBufSize := l.bufSize
|
||||
newBufSize := origBufSize
|
||||
|
||||
@@ -58,7 +58,7 @@ func fmtLog(cfg *RequestLoggerConfig) (ts string, line string) {
|
||||
t := time.Now()
|
||||
logger := NewMockAccessLogger(testTask, cfg)
|
||||
utils.MockTimeNow(t)
|
||||
buf = logger.AppendRequestLog(buf, req, resp)
|
||||
buf = logger.(RequestFormatter).AppendRequestLog(buf, req, resp)
|
||||
return t.Format(LogTimeFormat), string(buf)
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestBackScanner(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Setup mock file
|
||||
mockFile := NewMockFile()
|
||||
mockFile := NewMockFile(false)
|
||||
_, err := mockFile.Write([]byte(tt.input))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write to mock file: %v", err)
|
||||
@@ -103,7 +103,7 @@ func TestBackScannerWithVaryingChunkSizes(t *testing.T) {
|
||||
|
||||
for _, chunkSize := range chunkSizes {
|
||||
t.Run(fmt.Sprintf("chunk_size_%d", chunkSize), func(t *testing.T) {
|
||||
mockFile := NewMockFile()
|
||||
mockFile := NewMockFile(false)
|
||||
_, err := mockFile.Write([]byte(input))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write to mock file: %v", err)
|
||||
@@ -149,7 +149,7 @@ func logEntry() []byte {
|
||||
res := httptest.NewRecorder()
|
||||
// server the request
|
||||
srv.Config.Handler.ServeHTTP(res, req)
|
||||
b := accesslog.AppendRequestLog(nil, req, res.Result())
|
||||
b := accesslog.(RequestFormatter).AppendRequestLog(nil, req, res.Result())
|
||||
if b[len(b)-1] != '\n' {
|
||||
b = append(b, '\n')
|
||||
}
|
||||
@@ -197,7 +197,7 @@ func TestReset(t *testing.T) {
|
||||
|
||||
// 100000 log entries.
|
||||
func BenchmarkBackScanner(b *testing.B) {
|
||||
mockFile := NewMockFile()
|
||||
mockFile := NewMockFile(false)
|
||||
line := logEntry()
|
||||
for range 100000 {
|
||||
_, _ = mockFile.Write(line)
|
||||
|
||||
@@ -32,7 +32,7 @@ type (
|
||||
}
|
||||
AnyConfig interface {
|
||||
ToConfig() *Config
|
||||
IO() (WriterWithName, error)
|
||||
Writers() ([]Writer, error)
|
||||
}
|
||||
|
||||
Format string
|
||||
@@ -65,17 +65,20 @@ func (cfg *ConfigBase) Validate() gperr.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IO returns a writer for the config.
|
||||
// If only stdout is enabled, it returns nil, nil.
|
||||
func (cfg *ConfigBase) IO() (WriterWithName, error) {
|
||||
// Writers returns a list of writers for the config.
|
||||
func (cfg *ConfigBase) Writers() ([]Writer, error) {
|
||||
writers := make([]Writer, 0, 2)
|
||||
if cfg.Path != "" {
|
||||
io, err := NewFileIO(cfg.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return io, nil
|
||||
writers = append(writers, io)
|
||||
}
|
||||
return nil, nil
|
||||
if cfg.Stdout {
|
||||
writers = append(writers, NewStdout())
|
||||
}
|
||||
return writers, nil
|
||||
}
|
||||
|
||||
func (cfg *ACLLoggerConfig) ToConfig() *Config {
|
||||
|
||||
@@ -29,12 +29,19 @@ var (
|
||||
// NewFileIO creates a new file writer with cleaned path.
|
||||
//
|
||||
// If the file is already opened, it will be returned.
|
||||
func NewFileIO(path string) (WriterWithName, error) {
|
||||
func NewFileIO(path string) (Writer, error) {
|
||||
openedFilesMu.Lock()
|
||||
defer openedFilesMu.Unlock()
|
||||
|
||||
var file *File
|
||||
path = filepath.Clean(path)
|
||||
var err error
|
||||
|
||||
// make it absolute path, so that we can use it as key of `openedFiles` and shared lock
|
||||
path, err = filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("access log path error: %w", err)
|
||||
}
|
||||
|
||||
if opened, ok := openedFiles[path]; ok {
|
||||
opened.refCount.Add()
|
||||
return opened, nil
|
||||
@@ -54,8 +61,13 @@ func NewFileIO(path string) (WriterWithName, error) {
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// Name returns the absolute path of the file.
|
||||
func (f *File) Name() string {
|
||||
return f.f.Name()
|
||||
return f.path
|
||||
}
|
||||
|
||||
func (f *File) ShouldBeBuffered() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *File) Write(p []byte) (n int, err error) {
|
||||
|
||||
@@ -1,89 +1,96 @@
|
||||
package accesslog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yusing/goutils/task"
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
func TestConcurrentFileLoggersShareSameAccessLogIO(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
cfg := DefaultRequestLoggerConfig()
|
||||
cfg.Path = "test.log"
|
||||
|
||||
loggerCount := 10
|
||||
accessLogIOs := make([]WriterWithName, loggerCount)
|
||||
loggerCount := runtime.GOMAXPROCS(0)
|
||||
accessLogIOs := make([]Writer, loggerCount)
|
||||
|
||||
// make test log file
|
||||
file, err := os.Create(cfg.Path)
|
||||
expect.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
file.Close()
|
||||
t.Cleanup(func() {
|
||||
expect.NoError(t, os.Remove(cfg.Path))
|
||||
assert.NoError(t, os.Remove(cfg.Path))
|
||||
})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := range loggerCount {
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
wg.Go(func() {
|
||||
file, err := NewFileIO(cfg.Path)
|
||||
expect.NoError(t, err)
|
||||
accessLogIOs[index] = file
|
||||
}(i)
|
||||
assert.NoError(t, err)
|
||||
accessLogIOs[i] = file
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
firstIO := accessLogIOs[0]
|
||||
for _, io := range accessLogIOs {
|
||||
expect.Equal(t, io, firstIO)
|
||||
assert.Equal(t, firstIO, io)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentAccessLoggerLogAndFlush(t *testing.T) {
|
||||
file := NewMockFile()
|
||||
for _, buffered := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("buffered=%t", buffered), func(t *testing.T) {
|
||||
file := NewMockFile(buffered)
|
||||
|
||||
cfg := DefaultRequestLoggerConfig()
|
||||
parent := task.RootTask("test", false)
|
||||
cfg := DefaultRequestLoggerConfig()
|
||||
parent := task.RootTask("test", false)
|
||||
|
||||
loggerCount := 5
|
||||
logCountPerLogger := 10
|
||||
loggers := make([]*AccessLogger, loggerCount)
|
||||
loggerCount := runtime.GOMAXPROCS(0)
|
||||
logCountPerLogger := 10
|
||||
loggers := make([]AccessLogger, loggerCount)
|
||||
|
||||
for i := range loggerCount {
|
||||
loggers[i] = NewAccessLoggerWithIO(parent, file, cfg)
|
||||
for i := range loggerCount {
|
||||
loggers[i] = NewAccessLoggerWithIO(parent, file, cfg)
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
resp := &http.Response{StatusCode: http.StatusOK}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, logger := range loggers {
|
||||
wg.Go(func() {
|
||||
concurrentLog(logger, req, resp, logCountPerLogger)
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for _, logger := range loggers {
|
||||
logger.Close()
|
||||
}
|
||||
|
||||
expected := loggerCount * logCountPerLogger
|
||||
actual := file.NumLines()
|
||||
assert.Equal(t, expected, actual)
|
||||
})
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
resp := &http.Response{StatusCode: http.StatusOK}
|
||||
|
||||
wg.Add(len(loggers))
|
||||
for _, logger := range loggers {
|
||||
go func(l *AccessLogger) {
|
||||
defer wg.Done()
|
||||
parallelLog(l, req, resp, logCountPerLogger)
|
||||
l.Flush()
|
||||
}(logger)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
expected := loggerCount * logCountPerLogger
|
||||
actual := file.NumLines()
|
||||
expect.Equal(t, actual, expected)
|
||||
}
|
||||
|
||||
func parallelLog(logger *AccessLogger, req *http.Request, resp *http.Response, n int) {
|
||||
func concurrentLog(logger AccessLogger, req *http.Request, resp *http.Response, n int) {
|
||||
var wg sync.WaitGroup
|
||||
for range n {
|
||||
wg.Go(func() {
|
||||
logger.Log(req, resp)
|
||||
if rand.IntN(2) == 0 {
|
||||
logger.Flush()
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
@@ -7,26 +7,27 @@ import (
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
type noLock struct{}
|
||||
|
||||
func (noLock) Lock() {}
|
||||
func (noLock) Unlock() {}
|
||||
|
||||
type MockFile struct {
|
||||
afero.File
|
||||
noLock
|
||||
|
||||
buffered bool
|
||||
}
|
||||
|
||||
var _ SupportRotate = (*MockFile)(nil)
|
||||
|
||||
func NewMockFile() *MockFile {
|
||||
func NewMockFile(buffered bool) *MockFile {
|
||||
f, _ := afero.TempFile(afero.NewMemMapFs(), "", "")
|
||||
f.Seek(0, io.SeekEnd)
|
||||
return &MockFile{
|
||||
File: f,
|
||||
File: f,
|
||||
buffered: buffered,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockFile) ShouldBeBuffered() bool {
|
||||
return m.buffered
|
||||
}
|
||||
|
||||
func (m *MockFile) Len() int64 {
|
||||
filesize, _ := m.Seek(0, io.SeekEnd)
|
||||
_, _ = m.Seek(0, io.SeekStart)
|
||||
@@ -60,3 +61,7 @@ func (m *MockFile) MustSize() int64 {
|
||||
size, _ := m.Size()
|
||||
return size
|
||||
}
|
||||
|
||||
func (m *MockFile) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
63
internal/logging/accesslog/multi_access_logger.go
Normal file
63
internal/logging/accesslog/multi_access_logger.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package accesslog
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
|
||||
"github.com/yusing/goutils/task"
|
||||
)
|
||||
|
||||
type MultiAccessLogger struct {
|
||||
accessLoggers []AccessLogger
|
||||
}
|
||||
|
||||
// NewMultiAccessLogger creates a new AccessLogger that writes to multiple writers.
|
||||
//
|
||||
// If there is only one writer, it will return a single AccessLogger.
|
||||
// Otherwise, it will return a MultiAccessLogger that writes to all the writers.
|
||||
func NewMultiAccessLogger(parent task.Parent, cfg AnyConfig, writers []Writer) AccessLogger {
|
||||
if len(writers) == 1 {
|
||||
return NewAccessLoggerWithIO(parent, writers[0], cfg)
|
||||
}
|
||||
|
||||
accessLoggers := make([]AccessLogger, len(writers))
|
||||
for i, writer := range writers {
|
||||
accessLoggers[i] = NewAccessLoggerWithIO(parent, writer, cfg)
|
||||
}
|
||||
return &MultiAccessLogger{accessLoggers}
|
||||
}
|
||||
|
||||
func (m *MultiAccessLogger) Config() *Config {
|
||||
return m.accessLoggers[0].Config()
|
||||
}
|
||||
|
||||
func (m *MultiAccessLogger) Log(req *http.Request, res *http.Response) {
|
||||
for _, accessLogger := range m.accessLoggers {
|
||||
accessLogger.Log(req, res)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MultiAccessLogger) LogError(req *http.Request, err error) {
|
||||
for _, accessLogger := range m.accessLoggers {
|
||||
accessLogger.LogError(req, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MultiAccessLogger) LogACL(info *maxmind.IPInfo, blocked bool) {
|
||||
for _, accessLogger := range m.accessLoggers {
|
||||
accessLogger.LogACL(info, blocked)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MultiAccessLogger) Flush() {
|
||||
for _, accessLogger := range m.accessLoggers {
|
||||
accessLogger.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MultiAccessLogger) Close() error {
|
||||
for _, accessLogger := range m.accessLoggers {
|
||||
accessLogger.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
261
internal/logging/accesslog/multi_access_logger_test.go
Normal file
261
internal/logging/accesslog/multi_access_logger_test.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package accesslog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
|
||||
"github.com/yusing/goutils/task"
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
func TestNewMultiAccessLogger(t *testing.T) {
|
||||
testTask := task.RootTask("test", false)
|
||||
cfg := DefaultRequestLoggerConfig()
|
||||
|
||||
writers := []Writer{
|
||||
NewMockFile(true),
|
||||
NewMockFile(true),
|
||||
}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
expect.NotNil(t, logger)
|
||||
}
|
||||
|
||||
func TestMultiAccessLoggerConfig(t *testing.T) {
|
||||
testTask := task.RootTask("test", false)
|
||||
cfg := DefaultRequestLoggerConfig()
|
||||
cfg.Format = FormatCommon
|
||||
|
||||
writers := []Writer{
|
||||
NewMockFile(true),
|
||||
NewMockFile(true),
|
||||
}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
retrievedCfg := logger.Config()
|
||||
|
||||
expect.Equal(t, retrievedCfg.req.Format, FormatCommon)
|
||||
}
|
||||
|
||||
func TestMultiAccessLoggerLog(t *testing.T) {
|
||||
testTask := task.RootTask("test", false)
|
||||
cfg := DefaultRequestLoggerConfig()
|
||||
cfg.Format = FormatCommon
|
||||
|
||||
writer1 := NewMockFile(true)
|
||||
writer2 := NewMockFile(true)
|
||||
writers := []Writer{writer1, writer2}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
|
||||
testURL, _ := url.Parse("http://example.com/test")
|
||||
req := &http.Request{
|
||||
RemoteAddr: "192.168.1.1",
|
||||
Method: http.MethodGet,
|
||||
Proto: "HTTP/1.1",
|
||||
Host: "example.com",
|
||||
URL: testURL,
|
||||
Header: http.Header{
|
||||
"User-Agent": []string{"test-agent"},
|
||||
},
|
||||
}
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
ContentLength: 100,
|
||||
}
|
||||
|
||||
logger.Log(req, resp)
|
||||
logger.Flush()
|
||||
|
||||
expect.Equal(t, writer1.NumLines(), 1)
|
||||
expect.Equal(t, writer2.NumLines(), 1)
|
||||
}
|
||||
|
||||
func TestMultiAccessLoggerLogError(t *testing.T) {
|
||||
testTask := task.RootTask("test", false)
|
||||
cfg := DefaultRequestLoggerConfig()
|
||||
|
||||
writer1 := NewMockFile(true)
|
||||
writer2 := NewMockFile(true)
|
||||
writers := []Writer{writer1, writer2}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
|
||||
testURL, _ := url.Parse("http://example.com/test")
|
||||
req := &http.Request{
|
||||
RemoteAddr: "192.168.1.1",
|
||||
Method: http.MethodGet,
|
||||
URL: testURL,
|
||||
}
|
||||
testErr := errors.New("test error")
|
||||
|
||||
logger.LogError(req, testErr)
|
||||
logger.Flush()
|
||||
|
||||
expect.Equal(t, writer1.NumLines(), 1)
|
||||
expect.Equal(t, writer2.NumLines(), 1)
|
||||
}
|
||||
|
||||
func TestMultiAccessLoggerLogACL(t *testing.T) {
|
||||
testTask := task.RootTask("test", false)
|
||||
cfg := DefaultACLLoggerConfig()
|
||||
cfg.LogAllowed = true
|
||||
|
||||
writer1 := NewMockFile(true)
|
||||
writer2 := NewMockFile(true)
|
||||
writers := []Writer{writer1, writer2}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
|
||||
info := &maxmind.IPInfo{
|
||||
IP: net.ParseIP("192.168.1.1"),
|
||||
Str: "192.168.1.1",
|
||||
}
|
||||
|
||||
logger.LogACL(info, false)
|
||||
logger.Flush()
|
||||
|
||||
expect.Equal(t, writer1.NumLines(), 1)
|
||||
expect.Equal(t, writer2.NumLines(), 1)
|
||||
}
|
||||
|
||||
func TestMultiAccessLoggerFlush(t *testing.T) {
|
||||
testTask := task.RootTask("test", false)
|
||||
cfg := DefaultRequestLoggerConfig()
|
||||
|
||||
writer1 := NewMockFile(true)
|
||||
writer2 := NewMockFile(true)
|
||||
writers := []Writer{writer1, writer2}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
|
||||
testURL, _ := url.Parse("http://example.com/test")
|
||||
req := &http.Request{
|
||||
RemoteAddr: "192.168.1.1",
|
||||
Method: http.MethodGet,
|
||||
URL: testURL,
|
||||
}
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
logger.Log(req, resp)
|
||||
logger.Flush()
|
||||
|
||||
expect.Equal(t, writer1.NumLines(), 1)
|
||||
expect.Equal(t, writer2.NumLines(), 1)
|
||||
}
|
||||
|
||||
func TestMultiAccessLoggerClose(t *testing.T) {
|
||||
testTask := task.RootTask("test", false)
|
||||
cfg := DefaultRequestLoggerConfig()
|
||||
|
||||
writer1 := NewMockFile(true)
|
||||
writer2 := NewMockFile(true)
|
||||
writers := []Writer{writer1, writer2}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
|
||||
err := logger.Close()
|
||||
expect.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestMultiAccessLoggerMultipleLogs(t *testing.T) {
|
||||
testTask := task.RootTask("test", false)
|
||||
cfg := DefaultRequestLoggerConfig()
|
||||
|
||||
writer1 := NewMockFile(true)
|
||||
writer2 := NewMockFile(true)
|
||||
writers := []Writer{writer1, writer2}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
|
||||
testURL, _ := url.Parse("http://example.com/test")
|
||||
|
||||
for range 3 {
|
||||
req := &http.Request{
|
||||
RemoteAddr: "192.168.1.1",
|
||||
Method: http.MethodGet,
|
||||
URL: testURL,
|
||||
}
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
}
|
||||
logger.Log(req, resp)
|
||||
}
|
||||
|
||||
logger.Flush()
|
||||
|
||||
expect.Equal(t, writer1.NumLines(), 3)
|
||||
expect.Equal(t, writer2.NumLines(), 3)
|
||||
}
|
||||
|
||||
func TestMultiAccessLoggerSingleWriter(t *testing.T) {
|
||||
testTask := task.RootTask("test", false)
|
||||
cfg := DefaultRequestLoggerConfig()
|
||||
|
||||
writer := NewMockFile(true)
|
||||
writers := []Writer{writer}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
expect.NotNil(t, logger)
|
||||
|
||||
testURL, _ := url.Parse("http://example.com/test")
|
||||
req := &http.Request{
|
||||
RemoteAddr: "192.168.1.1",
|
||||
Method: http.MethodGet,
|
||||
URL: testURL,
|
||||
}
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
logger.Log(req, resp)
|
||||
logger.Flush()
|
||||
|
||||
expect.Equal(t, writer.NumLines(), 1)
|
||||
}
|
||||
|
||||
func TestMultiAccessLoggerMixedOperations(t *testing.T) {
|
||||
testTask := task.RootTask("test", false)
|
||||
cfg := DefaultRequestLoggerConfig()
|
||||
|
||||
writer1 := NewMockFile(true)
|
||||
writer2 := NewMockFile(true)
|
||||
writers := []Writer{writer1, writer2}
|
||||
|
||||
logger := NewMultiAccessLogger(testTask, cfg, writers)
|
||||
|
||||
testURL, _ := url.Parse("http://example.com/test")
|
||||
|
||||
req := &http.Request{
|
||||
RemoteAddr: "192.168.1.1",
|
||||
Method: http.MethodGet,
|
||||
URL: testURL,
|
||||
}
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
logger.Log(req, resp)
|
||||
logger.Flush()
|
||||
|
||||
info := &maxmind.IPInfo{
|
||||
IP: net.ParseIP("192.168.1.1"),
|
||||
Str: "192.168.1.1",
|
||||
}
|
||||
|
||||
cfg2 := DefaultACLLoggerConfig()
|
||||
cfg2.LogAllowed = true
|
||||
aclLogger := NewMultiAccessLogger(testTask, cfg2, writers)
|
||||
aclLogger.LogACL(info, false)
|
||||
|
||||
logger.Flush()
|
||||
|
||||
expect.Equal(t, writer1.NumLines(), 1)
|
||||
expect.Equal(t, writer2.NumLines(), 1)
|
||||
}
|
||||
@@ -55,7 +55,7 @@ func TestParseLogTime(t *testing.T) {
|
||||
func TestRotateKeepLast(t *testing.T) {
|
||||
for _, format := range ReqLoggerFormats {
|
||||
t.Run(string(format)+" keep last", func(t *testing.T) {
|
||||
file := NewMockFile()
|
||||
file := NewMockFile(true)
|
||||
utils.MockTimeNow(testTime)
|
||||
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
Format: format,
|
||||
@@ -77,7 +77,7 @@ func TestRotateKeepLast(t *testing.T) {
|
||||
logger.Config().Retention = retention
|
||||
|
||||
var result RotateResult
|
||||
rotated, err := logger.Rotate(&result)
|
||||
rotated, err := logger.(AccessLogRotater).Rotate(&result)
|
||||
expect.NoError(t, err)
|
||||
expect.Equal(t, rotated, true)
|
||||
expect.Equal(t, file.NumLines(), int(retention.Last))
|
||||
@@ -86,7 +86,7 @@ func TestRotateKeepLast(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run(string(format)+" keep days", func(t *testing.T) {
|
||||
file := NewMockFile()
|
||||
file := NewMockFile(true)
|
||||
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
Format: format,
|
||||
})
|
||||
@@ -107,7 +107,7 @@ func TestRotateKeepLast(t *testing.T) {
|
||||
|
||||
utils.MockTimeNow(testTime)
|
||||
var result RotateResult
|
||||
rotated, err := logger.Rotate(&result)
|
||||
rotated, err := logger.(AccessLogRotater).Rotate(&result)
|
||||
expect.NoError(t, err)
|
||||
expect.Equal(t, rotated, true)
|
||||
expect.Equal(t, file.NumLines(), int(retention.Days))
|
||||
@@ -132,7 +132,7 @@ func TestRotateKeepLast(t *testing.T) {
|
||||
func TestRotateKeepFileSize(t *testing.T) {
|
||||
for _, format := range ReqLoggerFormats {
|
||||
t.Run(string(format)+" keep size no rotation", func(t *testing.T) {
|
||||
file := NewMockFile()
|
||||
file := NewMockFile(true)
|
||||
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
Format: format,
|
||||
})
|
||||
@@ -153,7 +153,7 @@ func TestRotateKeepFileSize(t *testing.T) {
|
||||
|
||||
utils.MockTimeNow(testTime)
|
||||
var result RotateResult
|
||||
rotated, err := logger.Rotate(&result)
|
||||
rotated, err := logger.(AccessLogRotater).Rotate(&result)
|
||||
expect.NoError(t, err)
|
||||
|
||||
// file should be untouched as 100KB > 10 lines * bytes per line
|
||||
@@ -164,7 +164,7 @@ func TestRotateKeepFileSize(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("keep size with rotation", func(t *testing.T) {
|
||||
file := NewMockFile()
|
||||
file := NewMockFile(true)
|
||||
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
Format: FormatJSON,
|
||||
})
|
||||
@@ -185,7 +185,7 @@ func TestRotateKeepFileSize(t *testing.T) {
|
||||
|
||||
utils.MockTimeNow(testTime)
|
||||
var result RotateResult
|
||||
rotated, err := logger.Rotate(&result)
|
||||
rotated, err := logger.(AccessLogRotater).Rotate(&result)
|
||||
expect.NoError(t, err)
|
||||
expect.Equal(t, rotated, true)
|
||||
expect.Equal(t, result.NumBytesKeep, int64(retention.KeepSize))
|
||||
@@ -198,7 +198,7 @@ func TestRotateKeepFileSize(t *testing.T) {
|
||||
func TestRotateSkipInvalidTime(t *testing.T) {
|
||||
for _, format := range ReqLoggerFormats {
|
||||
t.Run(string(format), func(t *testing.T) {
|
||||
file := NewMockFile()
|
||||
file := NewMockFile(true)
|
||||
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
Format: format,
|
||||
})
|
||||
@@ -221,7 +221,7 @@ func TestRotateSkipInvalidTime(t *testing.T) {
|
||||
logger.Config().Retention = retention
|
||||
|
||||
var result RotateResult
|
||||
rotated, err := logger.Rotate(&result)
|
||||
rotated, err := logger.(AccessLogRotater).Rotate(&result)
|
||||
expect.NoError(t, err)
|
||||
expect.Equal(t, rotated, true)
|
||||
// should read one invalid line after every valid line
|
||||
@@ -240,7 +240,7 @@ func BenchmarkRotate(b *testing.B) {
|
||||
}
|
||||
for _, retention := range tests {
|
||||
b.Run(fmt.Sprintf("retention_%s", retention.String()), func(b *testing.B) {
|
||||
file := NewMockFile()
|
||||
file := NewMockFile(true)
|
||||
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
ConfigBase: ConfigBase{
|
||||
Retention: retention,
|
||||
@@ -256,11 +256,11 @@ func BenchmarkRotate(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
b.StopTimer()
|
||||
file = NewMockFile()
|
||||
file = NewMockFile(true)
|
||||
_, _ = file.Write(content)
|
||||
b.StartTimer()
|
||||
var result RotateResult
|
||||
_, _ = logger.Rotate(&result)
|
||||
_, _ = logger.(AccessLogRotater).Rotate(&result)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -274,7 +274,7 @@ func BenchmarkRotateWithInvalidTime(b *testing.B) {
|
||||
}
|
||||
for _, retention := range tests {
|
||||
b.Run(fmt.Sprintf("retention_%s", retention.String()), func(b *testing.B) {
|
||||
file := NewMockFile()
|
||||
file := NewMockFile(true)
|
||||
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
|
||||
ConfigBase: ConfigBase{
|
||||
Retention: retention,
|
||||
@@ -293,11 +293,11 @@ func BenchmarkRotateWithInvalidTime(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
b.StopTimer()
|
||||
file = NewMockFile()
|
||||
file = NewMockFile(true)
|
||||
_, _ = file.Write(content)
|
||||
b.StartTimer()
|
||||
var result RotateResult
|
||||
_, _ = logger.Rotate(&result)
|
||||
_, _ = logger.(AccessLogRotater).Rotate(&result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
32
internal/logging/accesslog/stdout.go
Normal file
32
internal/logging/accesslog/stdout.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package accesslog
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/godoxy/internal/logging"
|
||||
)
|
||||
|
||||
type Stdout struct {
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
func NewStdout() Writer {
|
||||
return &Stdout{logger: logging.NewLoggerWithFixedLevel(zerolog.InfoLevel, os.Stdout)}
|
||||
}
|
||||
|
||||
func (s Stdout) Name() string {
|
||||
return "stdout"
|
||||
}
|
||||
|
||||
func (s Stdout) ShouldBeBuffered() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s Stdout) Write(p []byte) (n int, err error) {
|
||||
return s.logger.Write(p)
|
||||
}
|
||||
|
||||
func (s Stdout) Close() error {
|
||||
return nil
|
||||
}
|
||||
47
internal/logging/accesslog/writer.go
Normal file
47
internal/logging/accesslog/writer.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package accesslog
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type BufferedWriter interface {
|
||||
io.Writer
|
||||
io.Closer
|
||||
Flush() error
|
||||
Resize(size int) error
|
||||
}
|
||||
|
||||
type unbufferedWriter struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func NewUnbufferedWriter(w io.Writer) BufferedWriter {
|
||||
return unbufferedWriter{w: w}
|
||||
}
|
||||
|
||||
func (w unbufferedWriter) Write(p []byte) (n int, err error) {
|
||||
return w.w.Write(p)
|
||||
}
|
||||
|
||||
func (w unbufferedWriter) Close() error {
|
||||
if closer, ok := w.w.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w unbufferedWriter) Flush() error {
|
||||
if flusher, ok := w.w.(interface{ Flush() }); ok {
|
||||
flusher.Flush()
|
||||
} else if errFlusher, ok := w.w.(interface{ FlushError() error }); ok {
|
||||
return errFlusher.FlushError()
|
||||
} else if errFlusher2, ok := w.w.(interface{ Flush() error }); ok {
|
||||
return errFlusher2.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w unbufferedWriter) Resize(size int) error {
|
||||
// No-op for unbuffered writer
|
||||
return nil
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package loadbalancer
|
||||
|
||||
import (
|
||||
"hash/fnv"
|
||||
"net"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/bytedance/gopkg/util/xxhash3"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
@@ -19,6 +20,9 @@ type ipHash struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var _ impl = (*ipHash)(nil)
|
||||
var _ customServeHTTP = (*ipHash)(nil)
|
||||
|
||||
func (lb *LoadBalancer) newIPHash() impl {
|
||||
impl := &ipHash{LoadBalancer: lb}
|
||||
if len(lb.Options) == 0 {
|
||||
@@ -36,16 +40,6 @@ func (impl *ipHash) OnAddServer(srv types.LoadBalancerServer) {
|
||||
impl.mu.Lock()
|
||||
defer impl.mu.Unlock()
|
||||
|
||||
for i, s := range impl.pool {
|
||||
if s == srv {
|
||||
return
|
||||
}
|
||||
if s == nil {
|
||||
impl.pool[i] = srv
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
impl.pool = append(impl.pool, srv)
|
||||
}
|
||||
|
||||
@@ -55,7 +49,7 @@ func (impl *ipHash) OnRemoveServer(srv types.LoadBalancerServer) {
|
||||
|
||||
for i, s := range impl.pool {
|
||||
if s == srv {
|
||||
impl.pool[i] = nil
|
||||
impl.pool = slices.Delete(impl.pool, i, 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -63,30 +57,32 @@ func (impl *ipHash) OnRemoveServer(srv types.LoadBalancerServer) {
|
||||
|
||||
func (impl *ipHash) ServeHTTP(_ types.LoadBalancerServers, rw http.ResponseWriter, r *http.Request) {
|
||||
if impl.realIP != nil {
|
||||
impl.realIP.ModifyRequest(impl.serveHTTP, rw, r)
|
||||
} else {
|
||||
impl.serveHTTP(rw, r)
|
||||
// resolve real client IP
|
||||
if proceed := impl.realIP.TryModifyRequest(rw, r); !proceed {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (impl *ipHash) serveHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
http.Error(rw, "Internal error", http.StatusInternalServerError)
|
||||
impl.l.Err(err).Msg("invalid remote address " + r.RemoteAddr)
|
||||
return
|
||||
}
|
||||
idx := hashIP(ip) % uint32(len(impl.pool))
|
||||
|
||||
srv := impl.pool[idx]
|
||||
srv := impl.ChooseServer(impl.pool, r)
|
||||
if srv == nil || srv.Status().Bad() {
|
||||
http.Error(rw, "Service unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
srv.ServeHTTP(rw, r)
|
||||
}
|
||||
|
||||
func hashIP(ip string) uint32 {
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(ip))
|
||||
return h.Sum32()
|
||||
func (impl *ipHash) ChooseServer(_ types.LoadBalancerServers, r *http.Request) types.LoadBalancerServer {
|
||||
impl.mu.Lock()
|
||||
defer impl.mu.Unlock()
|
||||
|
||||
if len(impl.pool) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
ip = r.RemoteAddr
|
||||
}
|
||||
return impl.pool[xxhash3.HashString(ip)%uint64(len(impl.pool))]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ type leastConn struct {
|
||||
nConn *xsync.Map[types.LoadBalancerServer, *atomic.Int64]
|
||||
}
|
||||
|
||||
var _ impl = (*leastConn)(nil)
|
||||
var _ customServeHTTP = (*leastConn)(nil)
|
||||
|
||||
func (lb *LoadBalancer) newLeastConn() impl {
|
||||
return &leastConn{
|
||||
LoadBalancer: lb,
|
||||
@@ -29,26 +32,42 @@ func (impl *leastConn) OnRemoveServer(srv types.LoadBalancerServer) {
|
||||
}
|
||||
|
||||
func (impl *leastConn) ServeHTTP(srvs types.LoadBalancerServers, rw http.ResponseWriter, r *http.Request) {
|
||||
srv := srvs[0]
|
||||
srv := impl.ChooseServer(srvs, r)
|
||||
if srv == nil {
|
||||
http.Error(rw, "Service unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
minConn, ok := impl.nConn.Load(srv)
|
||||
if !ok {
|
||||
impl.l.Error().Msgf("[BUG] server %s not found", srv.Name())
|
||||
http.Error(rw, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for i := 1; i < len(srvs); i++ {
|
||||
minConn.Add(1)
|
||||
defer minConn.Add(-1)
|
||||
srv.ServeHTTP(rw, r)
|
||||
}
|
||||
|
||||
func (impl *leastConn) ChooseServer(srvs types.LoadBalancerServers, r *http.Request) types.LoadBalancerServer {
|
||||
if len(srvs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var srv types.LoadBalancerServer
|
||||
var minConn *atomic.Int64
|
||||
|
||||
for i := range srvs {
|
||||
nConn, ok := impl.nConn.Load(srvs[i])
|
||||
if !ok {
|
||||
impl.l.Error().Msgf("[BUG] server %s not found", srv.Name())
|
||||
http.Error(rw, "Internal error", http.StatusInternalServerError)
|
||||
continue
|
||||
}
|
||||
if nConn.Load() < minConn.Load() {
|
||||
if minConn == nil || nConn.Load() < minConn.Load() {
|
||||
minConn = nConn
|
||||
srv = srvs[i]
|
||||
}
|
||||
}
|
||||
|
||||
minConn.Add(1)
|
||||
srv.ServeHTTP(rw, r)
|
||||
minConn.Add(-1)
|
||||
return srv
|
||||
}
|
||||
|
||||
@@ -8,20 +8,24 @@ import (
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
"github.com/yusing/godoxy/internal/utils/pool"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/task"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// TODO: stats of each server.
|
||||
// TODO: support weighted mode.
|
||||
type (
|
||||
impl interface {
|
||||
ServeHTTP(srvs types.LoadBalancerServers, rw http.ResponseWriter, r *http.Request)
|
||||
OnAddServer(srv types.LoadBalancerServer)
|
||||
OnRemoveServer(srv types.LoadBalancerServer)
|
||||
ChooseServer(srvs types.LoadBalancerServers, r *http.Request) types.LoadBalancerServer
|
||||
}
|
||||
customServeHTTP interface {
|
||||
ServeHTTP(srvs types.LoadBalancerServers, rw http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
LoadBalancer struct {
|
||||
@@ -218,17 +222,49 @@ func (lb *LoadBalancer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
http.Error(rw, "Service unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
if r.Header.Get(httpheaders.HeaderGoDoxyCheckRedirect) != "" {
|
||||
if r.URL.Path == idlewatcher.WakeEventsPath {
|
||||
var errs errgroup.Group
|
||||
// wake all servers
|
||||
for _, srv := range srvs {
|
||||
if err := srv.TryWake(); err != nil {
|
||||
lb.l.Warn().Err(err).
|
||||
Str("server", srv.Name()).
|
||||
Msg("failed to wake server")
|
||||
}
|
||||
errs.Go(func() error {
|
||||
err := srv.TryWake()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to wake server %q: %w", srv.Name(), err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := errs.Wait(); err != nil {
|
||||
gperr.LogWarn("failed to wake some servers", err, &lb.l)
|
||||
}
|
||||
}
|
||||
lb.impl.ServeHTTP(srvs, rw, r)
|
||||
|
||||
// Check for idlewatcher requests or sticky sessions
|
||||
if lb.Sticky || isIdlewatcherRequest(r) {
|
||||
if selectedServer := getStickyServer(r, srvs); selectedServer != nil {
|
||||
selectedServer.ServeHTTP(rw, r)
|
||||
return
|
||||
}
|
||||
// No sticky session, choose a server and set cookie
|
||||
selectedServer := lb.impl.ChooseServer(srvs, r)
|
||||
if selectedServer != nil {
|
||||
setStickyCookie(rw, r, selectedServer, lb.StickyMaxAge)
|
||||
selectedServer.ServeHTTP(rw, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if customServeHTTP, ok := lb.impl.(customServeHTTP); ok {
|
||||
customServeHTTP.ServeHTTP(srvs, rw, r)
|
||||
return
|
||||
}
|
||||
|
||||
selectedServer := lb.ChooseServer(srvs, r)
|
||||
if selectedServer == nil {
|
||||
http.Error(rw, "Service unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
selectedServer.ServeHTTP(rw, r)
|
||||
}
|
||||
|
||||
// MarshalJSON implements health.HealthMonitor.
|
||||
@@ -317,3 +353,16 @@ func (lb *LoadBalancer) availServers() []types.LoadBalancerServer {
|
||||
}
|
||||
return avail
|
||||
}
|
||||
|
||||
// isIdlewatcherRequest checks if this is an idlewatcher-related request
|
||||
func isIdlewatcherRequest(r *http.Request) bool {
|
||||
// Check for explicit idlewatcher paths
|
||||
if r.URL.Path == idlewatcher.WakeEventsPath ||
|
||||
r.URL.Path == idlewatcher.FavIconPath ||
|
||||
r.URL.Path == idlewatcher.LoadingPageCSSPath ||
|
||||
r.URL.Path == idlewatcher.LoadingPageJSPath {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -11,14 +11,16 @@ type roundRobin struct {
|
||||
index atomic.Uint32
|
||||
}
|
||||
|
||||
var _ impl = (*roundRobin)(nil)
|
||||
|
||||
func (*LoadBalancer) newRoundRobin() impl { return &roundRobin{} }
|
||||
func (lb *roundRobin) OnAddServer(srv types.LoadBalancerServer) {}
|
||||
func (lb *roundRobin) OnRemoveServer(srv types.LoadBalancerServer) {}
|
||||
|
||||
func (lb *roundRobin) ServeHTTP(srvs types.LoadBalancerServers, rw http.ResponseWriter, r *http.Request) {
|
||||
index := lb.index.Add(1) % uint32(len(srvs))
|
||||
srvs[index].ServeHTTP(rw, r)
|
||||
if lb.index.Load() >= 2*uint32(len(srvs)) {
|
||||
lb.index.Store(0)
|
||||
func (lb *roundRobin) ChooseServer(srvs types.LoadBalancerServers, r *http.Request) types.LoadBalancerServer {
|
||||
if len(srvs) == 0 {
|
||||
return nil
|
||||
}
|
||||
index := (lb.index.Add(1) - 1) % uint32(len(srvs))
|
||||
return srvs[index]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package loadbalancer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
|
||||
@@ -66,7 +67,7 @@ func (srv *server) String() string {
|
||||
func (srv *server) TryWake() error {
|
||||
waker, ok := srv.Handler.(idlewatcher.Waker)
|
||||
if ok {
|
||||
return waker.Wake()
|
||||
return waker.Wake(context.Background())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
51
internal/net/gphttp/loadbalancer/sticky.go
Normal file
51
internal/net/gphttp/loadbalancer/sticky.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package loadbalancer
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/bytedance/gopkg/util/xxhash3"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
)
|
||||
|
||||
func hashServerKey(key string) string {
|
||||
h := xxhash3.HashString(key)
|
||||
as8bytes := *(*[8]byte)(unsafe.Pointer(&h))
|
||||
return hex.EncodeToString(as8bytes[:])
|
||||
}
|
||||
|
||||
// getStickyServer extracts the sticky session cookie and returns the corresponding server
|
||||
func getStickyServer(r *http.Request, srvs []types.LoadBalancerServer) types.LoadBalancerServer {
|
||||
cookie, err := r.Cookie("godoxy_lb_sticky")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
serverKeyHash := cookie.Value
|
||||
for _, srv := range srvs {
|
||||
if hashServerKey(srv.Key()) == serverKeyHash {
|
||||
return srv
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setStickyCookie sets a cookie to maintain sticky session with a specific server
|
||||
func setStickyCookie(rw http.ResponseWriter, r *http.Request, srv types.LoadBalancerServer, maxAge time.Duration) {
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: "godoxy_lb_sticky",
|
||||
Value: hashServerKey(srv.Key()),
|
||||
Path: "/",
|
||||
MaxAge: int(maxAge.Seconds()),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
HttpOnly: true,
|
||||
Secure: isSecure(r),
|
||||
})
|
||||
}
|
||||
|
||||
func isSecure(r *http.Request) bool {
|
||||
return r.TLS != nil || strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https")
|
||||
}
|
||||
@@ -2,7 +2,10 @@ package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/auth"
|
||||
"github.com/yusing/godoxy/internal/route/rules"
|
||||
)
|
||||
|
||||
@@ -11,6 +14,7 @@ type Bypass []rules.RuleOn
|
||||
func (b Bypass) ShouldBypass(w http.ResponseWriter, r *http.Request) bool {
|
||||
for _, rule := range b {
|
||||
if rule.Check(w, r) {
|
||||
log.Debug().Str("rule_matched", rule.String()).Str("url", r.Host+r.URL.Path).Msg("bypassing request")
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -18,22 +22,38 @@ func (b Bypass) ShouldBypass(w http.ResponseWriter, r *http.Request) bool {
|
||||
}
|
||||
|
||||
type checkBypass struct {
|
||||
name string
|
||||
|
||||
bypass Bypass
|
||||
modReq RequestModifier
|
||||
modRes ResponseModifier
|
||||
|
||||
// when request path matches any of these prefixes, bypass is not applied
|
||||
enforcedPathPrefixes []string
|
||||
}
|
||||
|
||||
func (c *checkBypass) isEnforced(r *http.Request) bool {
|
||||
for _, prefix := range c.enforcedPathPrefixes {
|
||||
if strings.HasPrefix(r.URL.Path, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *checkBypass) before(w http.ResponseWriter, r *http.Request) (proceedNext bool) {
|
||||
if c.modReq == nil || c.bypass.ShouldBypass(w, r) {
|
||||
if c.modReq == nil || (!c.isEnforced(r) && c.bypass.ShouldBypass(w, r)) {
|
||||
return true
|
||||
}
|
||||
log.Debug().Str("middleware", c.name).Str("url", r.Host+r.URL.Path).Msg("modifying request")
|
||||
return c.modReq.before(w, r)
|
||||
}
|
||||
|
||||
func (c *checkBypass) modifyResponse(resp *http.Response) error {
|
||||
if c.modRes == nil || c.bypass.ShouldBypass(rules.ResponseAsRW(resp), resp.Request) {
|
||||
if c.modRes == nil || (!c.isEnforced(resp.Request) && c.bypass.ShouldBypass(rules.ResponseAsRW(resp), resp.Request)) {
|
||||
return nil
|
||||
}
|
||||
log.Debug().Str("middleware", c.name).Str("url", resp.Request.Host+resp.Request.URL.Path).Msg("modifying response")
|
||||
return c.modRes.modifyResponse(resp)
|
||||
}
|
||||
|
||||
@@ -42,10 +62,23 @@ func (m *Middleware) withCheckBypass() any {
|
||||
modReq, _ := m.impl.(RequestModifier)
|
||||
modRes, _ := m.impl.(ResponseModifier)
|
||||
return &checkBypass{
|
||||
bypass: m.Bypass,
|
||||
modReq: modReq,
|
||||
modRes: modRes,
|
||||
name: m.Name(),
|
||||
bypass: m.Bypass,
|
||||
enforcedPathPrefixes: getEnforcedPathPrefixes(modReq, modRes),
|
||||
modReq: modReq,
|
||||
modRes: modRes,
|
||||
}
|
||||
}
|
||||
return m.impl
|
||||
}
|
||||
|
||||
func getEnforcedPathPrefixes(modReq RequestModifier, modRes ResponseModifier) []string {
|
||||
if modReq == nil && modRes == nil {
|
||||
return nil
|
||||
}
|
||||
switch modReq.(type) {
|
||||
case *oidcMiddleware:
|
||||
return []string{auth.OIDCAuthBasePath}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
@@ -71,7 +72,7 @@ func (m *forwardAuthMiddleware) before(w http.ResponseWriter, r *http.Request) (
|
||||
}
|
||||
|
||||
proto := "http"
|
||||
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
|
||||
if r.TLS != nil || strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") {
|
||||
proto = "https"
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,13 @@ import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/route/rules"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
"github.com/yusing/goutils/http/reverseproxy"
|
||||
)
|
||||
|
||||
@@ -76,7 +75,7 @@ func NewMiddleware[ImplType any]() *Middleware {
|
||||
panic("MiddlewareFinalizer and MiddlewareFinalizerWithError are mutually exclusive")
|
||||
}
|
||||
return &Middleware{
|
||||
name: strings.ToLower(reflect.TypeFor[ImplType]().Name()),
|
||||
name: reflect.TypeFor[ImplType]().Name(),
|
||||
construct: func() any { return new(ImplType) },
|
||||
}
|
||||
}
|
||||
@@ -123,7 +122,7 @@ func (m *Middleware) finalize() error {
|
||||
func (m *Middleware) New(optsRaw OptionsRaw) (*Middleware, gperr.Error) {
|
||||
if m.construct == nil { // likely a middleware from compose
|
||||
if len(optsRaw) != 0 {
|
||||
return nil, gperr.New("additional options not allowed for middleware ").Subject(m.name)
|
||||
return nil, gperr.New("additional options not allowed for middleware").Subject(m.name)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
@@ -170,6 +169,13 @@ func (m *Middleware) ModifyRequest(next http.HandlerFunc, w http.ResponseWriter,
|
||||
next(w, r)
|
||||
}
|
||||
|
||||
func (m *Middleware) TryModifyRequest(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
||||
if exec, ok := m.impl.(RequestModifier); ok {
|
||||
return exec.before(w, r)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Middleware) ModifyResponse(resp *http.Response) error {
|
||||
if exec, ok := m.impl.(ResponseModifier); ok {
|
||||
return exec.modifyResponse(resp)
|
||||
@@ -178,17 +184,42 @@ func (m *Middleware) ModifyResponse(resp *http.Response) error {
|
||||
}
|
||||
|
||||
func (m *Middleware) ServeHTTP(next http.HandlerFunc, w http.ResponseWriter, r *http.Request) {
|
||||
if exec, ok := m.impl.(ResponseModifier); ok {
|
||||
w = httputils.NewModifyResponseWriter(w, r, func(resp *http.Response) error {
|
||||
return exec.modifyResponse(resp)
|
||||
})
|
||||
}
|
||||
if exec, ok := m.impl.(RequestModifier); ok {
|
||||
if proceed := exec.before(w, r); !proceed {
|
||||
return
|
||||
}
|
||||
}
|
||||
next(w, r)
|
||||
|
||||
if exec, ok := m.impl.(ResponseModifier); ok {
|
||||
rm := rules.NewResponseModifier(w)
|
||||
defer rm.FlushRelease()
|
||||
next(rm, r)
|
||||
|
||||
currentBody := rm.BodyReader()
|
||||
currentResp := &http.Response{
|
||||
StatusCode: rm.StatusCode(),
|
||||
Header: rm.Header(),
|
||||
ContentLength: int64(rm.ContentLength()),
|
||||
Body: currentBody,
|
||||
Request: r,
|
||||
}
|
||||
if err := exec.modifyResponse(currentResp); err != nil {
|
||||
log.Err(err).Str("middleware", m.Name()).Str("url", fullURL(r)).Msg("failed to modify response")
|
||||
}
|
||||
|
||||
// override the response status code
|
||||
rm.WriteHeader(currentResp.StatusCode)
|
||||
|
||||
// overriding the response header is not necessary
|
||||
// modifyResponse is supposed to write to Header directly instead of assigning a new header map)
|
||||
|
||||
// override the content length and body if changed
|
||||
if currentResp.Body != currentBody {
|
||||
rm.SetBody(currentResp.Body)
|
||||
}
|
||||
} else {
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Middleware) LogWarn(req *http.Request) *zerolog.Event {
|
||||
|
||||
@@ -47,9 +47,10 @@ func (m *modifyHTML) modifyResponse(resp *http.Response) error {
|
||||
|
||||
// Skip modification for streaming/chunked responses to avoid blocking reads
|
||||
// Unknown content length or any transfer encoding indicates streaming.
|
||||
if resp.ContentLength < 0 || len(resp.TransferEncoding) > 0 {
|
||||
return nil
|
||||
}
|
||||
// if resp.ContentLength < 0 || len(resp.TransferEncoding) > 0 {
|
||||
// log.Debug().Str("url", fullURL(resp.Request)).Strs("transfer-encoding", resp.TransferEncoding).Msg("skipping modification for streaming/chunked response")
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// NOTE: do not put it in the defer, it will be used as resp.Body
|
||||
content, release, err := httputils.ReadAllBody(resp)
|
||||
|
||||
@@ -74,6 +74,11 @@ func (amw *oidcMiddleware) initSlow() error {
|
||||
}
|
||||
// If no custom credentials, authProvider remains the global one
|
||||
|
||||
// Always trigger login on unknown paths.
|
||||
// This prevents falling back to the default login page, which applies bypass rules.
|
||||
// Without this, redirecting to the global login page could circumvent the intended route restrictions.
|
||||
authProvider.SetOnUnknownPathHandler(authProvider.LoginHandler)
|
||||
|
||||
// Apply per-route user/group restrictions (these always override global)
|
||||
if len(amw.AllowedUsers) > 0 {
|
||||
authProvider.SetAllowedUsers(amw.AllowedUsers)
|
||||
|
||||
@@ -19,7 +19,7 @@ var RedirectHTTP = NewMiddleware[redirectHTTP]()
|
||||
|
||||
// before implements RequestModifier.
|
||||
func (m *redirectHTTP) before(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
||||
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
|
||||
if r.TLS != nil || strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ type Node struct {
|
||||
var Nodes = pool.New[*Node]("proxmox_nodes")
|
||||
|
||||
func AvailableNodeNames() string {
|
||||
if Nodes.Size() == 0 {
|
||||
return ""
|
||||
}
|
||||
var sb strings.Builder
|
||||
for _, node := range Nodes.Iter {
|
||||
sb.WriteString(node.name)
|
||||
|
||||
@@ -20,7 +20,7 @@ type (
|
||||
|
||||
middleware *middleware.Middleware
|
||||
handler http.Handler
|
||||
accessLogger *accesslog.AccessLogger
|
||||
accessLogger accesslog.AccessLogger
|
||||
}
|
||||
)
|
||||
|
||||
@@ -86,6 +86,10 @@ func (s *FileServer) Start(parent task.Parent) gperr.Error {
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.Rules) > 0 {
|
||||
s.handler = s.Rules.BuildHandler(s.handler.ServeHTTP)
|
||||
}
|
||||
|
||||
if s.UseHealthCheck() {
|
||||
s.HealthMon = monitor.NewFileServerHealthMonitor(s.HealthCheck, s.Root)
|
||||
if err := s.HealthMon.Start(s.task); err != nil {
|
||||
|
||||
@@ -121,6 +121,7 @@ func (p *DockerProvider) routesFromContainerLabels(container *types.Container) (
|
||||
// init entries map for all aliases
|
||||
for _, a := range container.Aliases {
|
||||
routes[a] = &route.Route{
|
||||
Alias: a,
|
||||
Metadata: route.Metadata{
|
||||
Container: container,
|
||||
},
|
||||
@@ -134,7 +135,7 @@ func (p *DockerProvider) routesFromContainerLabels(container *types.Container) (
|
||||
|
||||
for alias, entryMapAny := range m {
|
||||
if len(alias) == 0 {
|
||||
errs.Add(gperr.New("empty alias"))
|
||||
errs.Adds("empty alias")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -172,6 +173,7 @@ func (p *DockerProvider) routesFromContainerLabels(container *types.Container) (
|
||||
r, ok := routes[alias]
|
||||
if !ok {
|
||||
r = &route.Route{
|
||||
Alias: alias,
|
||||
Metadata: route.Metadata{
|
||||
Container: container,
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestParseDockerLabels(t *testing.T) {
|
||||
Names: []string{"container"},
|
||||
Labels: labels,
|
||||
State: "running",
|
||||
Ports: []container.Port{
|
||||
Ports: []container.PortSummary{
|
||||
{Type: "tcp", PrivatePort: 1234, PublicPort: 1234},
|
||||
},
|
||||
}, "/var/run/docker.sock"),
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
"github.com/moby/moby/client"
|
||||
D "github.com/yusing/godoxy/internal/docker"
|
||||
"github.com/yusing/godoxy/internal/route"
|
||||
routeTypes "github.com/yusing/godoxy/internal/route/types"
|
||||
@@ -274,7 +275,7 @@ func TestPrivateIPLocalhost(t *testing.T) {
|
||||
NetworkSettings: &container.NetworkSettingsSummary{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"network": {
|
||||
IPAddress: testDockerIP,
|
||||
IPAddress: netip.MustParseAddr(testDockerIP),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -292,7 +293,7 @@ func TestPrivateIPRemote(t *testing.T) {
|
||||
NetworkSettings: &container.NetworkSettingsSummary{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"network": {
|
||||
IPAddress: testDockerIP,
|
||||
IPAddress: netip.MustParseAddr(testDockerIP),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -314,11 +315,11 @@ func TestStreamDefaultValues(t *testing.T) {
|
||||
NetworkSettings: &container.NetworkSettingsSummary{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"network": {
|
||||
IPAddress: privIP,
|
||||
IPAddress: netip.MustParseAddr(privIP),
|
||||
},
|
||||
},
|
||||
},
|
||||
Ports: []container.Port{
|
||||
Ports: []container.PortSummary{
|
||||
{Type: "udp", PrivatePort: privPort, PublicPort: pubPort},
|
||||
},
|
||||
}
|
||||
@@ -371,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.Port{
|
||||
Ports: []container.PortSummary{
|
||||
{Type: "tcp", PrivatePort: 5432, PublicPort: 5432},
|
||||
},
|
||||
})["a"]
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
|
||||
@@ -153,6 +153,7 @@ func (r *ReveseProxyRoute) Start(parent task.Parent) gperr.Error {
|
||||
}
|
||||
|
||||
func (r *ReveseProxyRoute) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// req.Header.Set("Accept-Encoding", "identity")
|
||||
r.handler.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
@@ -50,7 +49,7 @@ type (
|
||||
PathPatterns []string `json:"path_patterns,omitempty" extensions:"x-nullable"`
|
||||
Rules rules.Rules `json:"rules,omitempty" extension:"x-nullable"`
|
||||
RuleFile string `json:"rule_file,omitempty" extensions:"x-nullable"`
|
||||
HealthCheck *types.HealthCheckConfig `json:"healthcheck"`
|
||||
HealthCheck *types.HealthCheckConfig `json:"healthcheck,omitempty" extensions:"x-nullable"` // null on load-balancer routes
|
||||
LoadBalance *types.LoadBalancerConfig `json:"load_balance,omitempty" extensions:"x-nullable"`
|
||||
Middlewares map[string]types.LabelMap `json:"middlewares,omitempty" extensions:"x-nullable"`
|
||||
Homepage *homepage.ItemConfig `json:"homepage"`
|
||||
@@ -82,19 +81,39 @@ type (
|
||||
impl types.Route
|
||||
task *task.Task
|
||||
|
||||
isValidated bool
|
||||
lastError gperr.Error
|
||||
provider types.RouteProvider
|
||||
// ensure err is read after validation or start
|
||||
valErr lockedError
|
||||
startErr lockedError
|
||||
|
||||
provider types.RouteProvider
|
||||
|
||||
agent *agent.AgentConfig
|
||||
|
||||
started chan struct{}
|
||||
once sync.Once
|
||||
started chan struct{}
|
||||
onceStart sync.Once
|
||||
onceValidate sync.Once
|
||||
}
|
||||
Routes map[string]*Route
|
||||
Port = route.Port
|
||||
)
|
||||
|
||||
type lockedError struct {
|
||||
err gperr.Error
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (le *lockedError) Get() gperr.Error {
|
||||
le.lock.Lock()
|
||||
defer le.lock.Unlock()
|
||||
return le.err
|
||||
}
|
||||
|
||||
func (le *lockedError) Set(err gperr.Error) {
|
||||
le.lock.Lock()
|
||||
defer le.lock.Unlock()
|
||||
le.err = err
|
||||
}
|
||||
|
||||
const DefaultHost = "localhost"
|
||||
|
||||
func (r Routes) Contains(alias string) bool {
|
||||
@@ -103,11 +122,24 @@ func (r Routes) Contains(alias string) bool {
|
||||
}
|
||||
|
||||
func (r *Route) Validate() gperr.Error {
|
||||
if r.isValidated {
|
||||
return r.lastError
|
||||
}
|
||||
r.isValidated = true
|
||||
// pcs := make([]uintptr, 1)
|
||||
// runtime.Callers(2, pcs)
|
||||
// f := runtime.FuncForPC(pcs[0])
|
||||
// fname := f.Name()
|
||||
r.onceValidate.Do(func() {
|
||||
// filename, line := f.FileLine(pcs[0])
|
||||
// if strings.HasPrefix(r.Alias, "godoxy") {
|
||||
// log.Debug().Str("route", r.Alias).Str("caller", fname).Str("file", filename).Int("line", line).Msg("validating route")
|
||||
// }
|
||||
r.valErr.Set(r.validate())
|
||||
})
|
||||
return r.valErr.Get()
|
||||
}
|
||||
|
||||
func (r *Route) validate() gperr.Error {
|
||||
// if strings.HasPrefix(r.Alias, "godoxy") {
|
||||
// log.Debug().Any("route", r).Msg("validating route")
|
||||
// }
|
||||
if r.Agent != "" {
|
||||
if r.Container != nil {
|
||||
return gperr.Errorf("specifying agent is not allowed for docker container routes")
|
||||
@@ -250,7 +282,6 @@ func (r *Route) Validate() gperr.Error {
|
||||
}
|
||||
|
||||
if errs.HasError() {
|
||||
r.lastError = errs.Error()
|
||||
return errs.Error()
|
||||
}
|
||||
|
||||
@@ -266,7 +297,6 @@ func (r *Route) Validate() gperr.Error {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
r.lastError = err
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -279,6 +309,21 @@ func (r *Route) Validate() gperr.Error {
|
||||
}
|
||||
|
||||
func (r *Route) validateRules() error {
|
||||
// FIXME: hardcoded here as a workaround
|
||||
// there's already a label "proxy.#1.rule_file=embed://webui.yml"
|
||||
// but it's not working as expected sometimes.
|
||||
// TODO: investigate why it's not working and fix it.
|
||||
if cont := r.ContainerInfo(); cont != nil {
|
||||
if cont.Image.Name == "godoxy-frontend" {
|
||||
rules, ok := rulepresets.GetRulePreset("webui.yml")
|
||||
if !ok {
|
||||
return errors.New("rule preset `webui.yml` not found")
|
||||
}
|
||||
r.Rules = rules
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.RuleFile != "" && len(r.Rules) > 0 {
|
||||
return errors.New("`rule_file` and `rules` cannot be used together")
|
||||
} else if r.RuleFile != "" {
|
||||
@@ -320,13 +365,10 @@ func (r *Route) Task() *task.Task {
|
||||
}
|
||||
|
||||
func (r *Route) Start(parent task.Parent) gperr.Error {
|
||||
if r.lastError != nil {
|
||||
return r.lastError
|
||||
}
|
||||
r.once.Do(func() {
|
||||
r.lastError = r.start(parent)
|
||||
r.onceStart.Do(func() {
|
||||
r.startErr.Set(r.start(parent))
|
||||
})
|
||||
return r.lastError
|
||||
return r.startErr.Get()
|
||||
}
|
||||
|
||||
func (r *Route) start(parent task.Parent) gperr.Error {
|
||||
@@ -480,6 +522,45 @@ func (r *Route) DisplayName() string {
|
||||
return r.Homepage.Name
|
||||
}
|
||||
|
||||
// PreferOver implements pool.Preferable to resolve duplicate route keys deterministically.
|
||||
// Preference policy:
|
||||
// - Prefer routes with rules over routes without rules.
|
||||
// - If rules tie, prefer non-docker routes (explicit config) over docker-discovered routes.
|
||||
// - Otherwise, prefer the new route to preserve existing semantics.
|
||||
func (r *Route) PreferOver(other any) bool {
|
||||
// Try to get the underlying *Route of the other value
|
||||
var or *Route
|
||||
switch v := other.(type) {
|
||||
case *Route:
|
||||
or = v
|
||||
case *ReveseProxyRoute:
|
||||
or = v.Route
|
||||
case *FileServer:
|
||||
or = v.Route
|
||||
case *StreamRoute:
|
||||
or = v.Route
|
||||
default:
|
||||
// Unknown type, allow replacement
|
||||
return true
|
||||
}
|
||||
|
||||
// Prefer routes that have rules
|
||||
if len(r.Rules) > 0 && len(or.Rules) == 0 {
|
||||
return true
|
||||
}
|
||||
if len(r.Rules) == 0 && len(or.Rules) > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Prefer explicit (non-docker) over docker auto-discovered
|
||||
if (r.Container == nil) != (or.Container == nil) {
|
||||
return r.Container == nil
|
||||
}
|
||||
|
||||
// Default: allow replacement
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Route) ContainerInfo() *types.Container {
|
||||
return r.Container
|
||||
}
|
||||
@@ -496,7 +577,7 @@ func (r *Route) IsZeroPort() bool {
|
||||
}
|
||||
|
||||
func (r *Route) ShouldExclude() bool {
|
||||
if r.lastError != nil {
|
||||
if r.valErr.Get() != nil {
|
||||
return true
|
||||
}
|
||||
if r.Excluded {
|
||||
@@ -565,7 +646,7 @@ func (re ExcludedReason) MarshalJSON() ([]byte, error) {
|
||||
// no need to unmarshal json because we don't store this
|
||||
|
||||
func (r *Route) findExcludedReason() ExcludedReason {
|
||||
if r.lastError != nil {
|
||||
if r.valErr.Get() != nil {
|
||||
return ExcludedReasonError
|
||||
}
|
||||
if r.ExcludedReason != ExcludedReasonNone {
|
||||
@@ -796,7 +877,7 @@ var preferredPortOrder = []int{
|
||||
8443,
|
||||
}
|
||||
|
||||
func preferredPort(portMapping map[int]container.Port) (res int) {
|
||||
func preferredPort(portMapping types.PortMapping) (res int) {
|
||||
for _, port := range preferredPortOrder {
|
||||
if _, ok := portMapping[port]; ok {
|
||||
return port
|
||||
|
||||
@@ -3,7 +3,6 @@ package route
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
route "github.com/yusing/godoxy/internal/route/types"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
@@ -137,7 +136,7 @@ func TestRouteValidate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPreferredPort(t *testing.T) {
|
||||
ports := map[int]container.Port{
|
||||
ports := types.PortMapping{
|
||||
22: {PrivatePort: 22},
|
||||
1000: {PrivatePort: 1000},
|
||||
3000: {PrivatePort: 80},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
!path glob("/auth/*")
|
||||
!path regex("[A-Za-z0-9_-]+\.(svg|png|jpg|jpeg|gif|ico|webp|woff2?|eot|ttf|otf|txt)(\?.+)?")
|
||||
!path /api/v1/version
|
||||
!path /manifest.json
|
||||
do: require_auth
|
||||
- name: proxy to backend
|
||||
on: path glob("/api/v1/*")
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -110,6 +111,15 @@ func (rm *ResponseModifier) WriteHeader(code int) {
|
||||
rm.statusCode = code
|
||||
}
|
||||
|
||||
// BodyReader returns a reader for the response body.
|
||||
// Every call to this function will return a new reader that starts from the beginning of the buffer.
|
||||
func (rm *ResponseModifier) BodyReader() io.ReadCloser {
|
||||
if rm.buf == nil {
|
||||
return io.NopCloser(bytes.NewReader(nil))
|
||||
}
|
||||
return io.NopCloser(bytes.NewReader(rm.buf.Bytes()))
|
||||
}
|
||||
|
||||
func (rm *ResponseModifier) ResetBody() {
|
||||
if rm.buf == nil {
|
||||
return
|
||||
@@ -117,6 +127,21 @@ func (rm *ResponseModifier) ResetBody() {
|
||||
rm.buf.Reset()
|
||||
}
|
||||
|
||||
func (rm *ResponseModifier) SetBody(r io.ReadCloser) error {
|
||||
if rm.buf == nil {
|
||||
rm.buf = rm.bufPool.GetBuffer()
|
||||
} else {
|
||||
rm.buf.Reset()
|
||||
}
|
||||
|
||||
_, err := io.Copy(rm.buf, r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy body: %w", err)
|
||||
}
|
||||
r.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rm *ResponseModifier) ContentLength() int {
|
||||
if rm.buf == nil {
|
||||
return 0
|
||||
@@ -193,7 +218,7 @@ func (rm *ResponseModifier) FlushRelease() (int, error) {
|
||||
if werr != nil {
|
||||
rm.errs.Addf("write error: %w", werr)
|
||||
}
|
||||
if err := http.NewResponseController(rm.w).Flush(); err != nil {
|
||||
if err := http.NewResponseController(rm.w).Flush(); err != nil && !errors.Is(err, http.ErrNotSupported) {
|
||||
rm.errs.Addf("flush error: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -239,14 +242,6 @@ func isTerminatingHandler(handler CommandHandler) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (rules Rules) MarshalJSON() ([]byte, error) {
|
||||
names := make([]string, len(rules))
|
||||
for i, rule := range rules {
|
||||
names[i] = rule.Name
|
||||
}
|
||||
return sonic.Marshal(names)
|
||||
}
|
||||
|
||||
func (rule *Rule) String() string {
|
||||
return rule.Name
|
||||
}
|
||||
@@ -263,6 +258,30 @@ func (rule *Rule) Handle(w http.ResponseWriter, r *http.Request) error {
|
||||
return rule.Do.exec.Handle(w, r)
|
||||
}
|
||||
|
||||
//go:linkname errStreamClosed golang.org/x/net/http2.errStreamClosed
|
||||
var errStreamClosed error
|
||||
|
||||
func logError(err error, r *http.Request) {
|
||||
if errors.Is(err, errStreamClosed) {
|
||||
return
|
||||
}
|
||||
var h2Err http2.StreamError
|
||||
if errors.As(err, &h2Err) {
|
||||
// ignore these errors
|
||||
switch h2Err.Code {
|
||||
case http2.ErrCodeStreamClosed:
|
||||
return
|
||||
}
|
||||
}
|
||||
var h3Err *http3.Error
|
||||
if errors.As(err, &h3Err) {
|
||||
// ignore these errors
|
||||
switch h3Err.ErrorCode {
|
||||
case
|
||||
http3.ErrCodeNoError,
|
||||
http3.ErrCodeRequestCanceled:
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Err(err).Str("method", r.Method).Str("url", r.Host+r.URL.Path).Msg("error executing rules")
|
||||
}
|
||||
|
||||
@@ -52,10 +52,7 @@ func ValidateVars(s string) error {
|
||||
|
||||
func ExpandVars(w *ResponseModifier, req *http.Request, src string, dstW io.Writer) error {
|
||||
dst := ioutils.NewBufferedWriter(dstW, 1024)
|
||||
defer func() {
|
||||
dst.Flush()
|
||||
dst.Release()
|
||||
}()
|
||||
defer dst.Close()
|
||||
|
||||
for i := 0; i < len(src); i++ {
|
||||
ch := src[i]
|
||||
|
||||
@@ -41,12 +41,17 @@ func ValidateWithCustomValidator(v reflect.Value) gperr.Error {
|
||||
} else {
|
||||
vt := v.Type()
|
||||
if vt.PkgPath() != "" { // not a builtin type
|
||||
// prioritize pointer method
|
||||
if v.CanAddr() {
|
||||
vAddr := v.Addr()
|
||||
if vAddr.Type().Implements(validatorType) {
|
||||
return vAddr.Interface().(CustomValidator).Validate()
|
||||
}
|
||||
}
|
||||
// fallback to value method
|
||||
if vt.Implements(validatorType) {
|
||||
return v.Interface().(CustomValidator).Validate()
|
||||
}
|
||||
if v.CanAddr() {
|
||||
return validateWithValidator(v.Addr())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -2,7 +2,7 @@ package types
|
||||
|
||||
import (
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/yusing/ds/ordered"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/utils"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
type (
|
||||
LabelMap = map[string]any
|
||||
|
||||
PortMapping = map[int]container.Port
|
||||
PortMapping = map[int]container.PortSummary
|
||||
Container struct {
|
||||
_ utils.NoCopy
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package types
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
@@ -9,10 +10,12 @@ import (
|
||||
|
||||
type (
|
||||
LoadBalancerConfig struct {
|
||||
Link string `json:"link"`
|
||||
Mode LoadBalancerMode `json:"mode"`
|
||||
Weight int `json:"weight"`
|
||||
Options map[string]any `json:"options,omitempty"`
|
||||
Link string `json:"link"`
|
||||
Mode LoadBalancerMode `json:"mode"`
|
||||
Weight int `json:"weight"`
|
||||
Sticky bool `json:"sticky"`
|
||||
StickyMaxAge time.Duration `json:"sticky_max_age"`
|
||||
Options map[string]any `json:"options,omitempty"`
|
||||
} // @name LoadBalancerConfig
|
||||
LoadBalancerMode string // @name LoadBalancerMode
|
||||
LoadBalancerServer interface {
|
||||
@@ -35,6 +38,8 @@ const (
|
||||
LoadbalanceModeIPHash LoadBalancerMode = "iphash"
|
||||
)
|
||||
|
||||
const StickyMaxAgeDefault = 1 * time.Hour
|
||||
|
||||
func (mode *LoadBalancerMode) ValidateUpdate() bool {
|
||||
switch strutils.ToLowerNoSnake(string(*mode)) {
|
||||
case "":
|
||||
|
||||
@@ -14,6 +14,12 @@ type (
|
||||
name string
|
||||
disableLog atomic.Bool
|
||||
}
|
||||
// Preferable allows an object to express deterministic replacement preference
|
||||
// when multiple objects with the same key are added to the pool.
|
||||
// If new.PreferOver(old) returns true, the new object replaces the old one.
|
||||
Preferable interface {
|
||||
PreferOver(other any) bool
|
||||
}
|
||||
Object interface {
|
||||
Key() string
|
||||
Name() string
|
||||
@@ -37,12 +43,18 @@ func (p *Pool[T]) Name() string {
|
||||
}
|
||||
|
||||
func (p *Pool[T]) Add(obj T) {
|
||||
p.checkExists(obj.Key())
|
||||
p.m.Store(obj.Key(), obj)
|
||||
p.logAction("added", obj)
|
||||
p.AddKey(obj.Key(), obj)
|
||||
}
|
||||
|
||||
func (p *Pool[T]) AddKey(key string, obj T) {
|
||||
if cur, exists := p.m.Load(key); exists {
|
||||
if newPref, ok := any(obj).(Preferable); ok {
|
||||
if !newPref.PreferOver(cur) {
|
||||
// keep existing
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
p.checkExists(key)
|
||||
p.m.Store(key, obj)
|
||||
p.logAction("added", obj)
|
||||
|
||||
@@ -10,7 +10,7 @@ var (
|
||||
TimeNow = DefaultTimeNow
|
||||
shouldCallTimeNow atomic.Bool
|
||||
timeNowTicker = time.NewTicker(shouldCallTimeNowInterval)
|
||||
lastTimeNow = time.Now()
|
||||
lastTimeNow = atomic.NewTime(time.Now())
|
||||
)
|
||||
|
||||
const shouldCallTimeNowInterval = 100 * time.Millisecond
|
||||
@@ -26,11 +26,13 @@ func MockTimeNow(t time.Time) {
|
||||
//
|
||||
// Returned value may have +-100ms error.
|
||||
func DefaultTimeNow() time.Time {
|
||||
if shouldCallTimeNow.Load() {
|
||||
lastTimeNow = time.Now()
|
||||
shouldCallTimeNow.Store(false)
|
||||
swapped := shouldCallTimeNow.CompareAndSwap(false, true)
|
||||
if swapped { // first call
|
||||
now := time.Now()
|
||||
lastTimeNow.Store(now)
|
||||
return now
|
||||
}
|
||||
return lastTimeNow
|
||||
return lastTimeNow.Load()
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -5,16 +5,18 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var sink time.Time
|
||||
|
||||
func BenchmarkTimeNow(b *testing.B) {
|
||||
b.Run("default", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
time.Now()
|
||||
sink = time.Now()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("reduced_call", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
DefaultTimeNow()
|
||||
sink = DefaultTimeNow()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
dockerEvents "github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
dockerEvents "github.com/moby/moby/api/types/events"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
"github.com/yusing/godoxy/internal/watcher/events"
|
||||
@@ -16,23 +15,42 @@ import (
|
||||
|
||||
type (
|
||||
DockerWatcher string
|
||||
DockerListOptions = dockerEvents.ListOptions
|
||||
DockerListOptions = client.EventsListOptions
|
||||
DockerFilters = client.Filters
|
||||
)
|
||||
|
||||
type DockerFilter struct {
|
||||
Term string
|
||||
Values []string
|
||||
}
|
||||
|
||||
func NewDockerFilter(term string, values ...string) DockerFilter {
|
||||
return DockerFilter{
|
||||
Term: term,
|
||||
Values: values,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDockerFilters(filters ...DockerFilter) client.Filters {
|
||||
f := make(client.Filters, len(filters))
|
||||
for _, filter := range filters {
|
||||
f.Add(filter.Term, filter.Values...)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// https://docs.docker.com/reference/api/engine/version/v1.47/#tag/System/operation/SystemPingHead
|
||||
var (
|
||||
DockerFilterContainer = filters.Arg("type", string(dockerEvents.ContainerEventType))
|
||||
DockerFilterStart = filters.Arg("event", string(dockerEvents.ActionStart))
|
||||
DockerFilterStop = filters.Arg("event", string(dockerEvents.ActionStop))
|
||||
DockerFilterDie = filters.Arg("event", string(dockerEvents.ActionDie))
|
||||
DockerFilterDestroy = filters.Arg("event", string(dockerEvents.ActionDestroy))
|
||||
DockerFilterKill = filters.Arg("event", string(dockerEvents.ActionKill))
|
||||
DockerFilterPause = filters.Arg("event", string(dockerEvents.ActionPause))
|
||||
DockerFilterUnpause = filters.Arg("event", string(dockerEvents.ActionUnPause))
|
||||
DockerFilterContainer = NewDockerFilter("type", string(dockerEvents.ContainerEventType))
|
||||
DockerFilterStart = NewDockerFilter("event", string(dockerEvents.ActionStart))
|
||||
DockerFilterStop = NewDockerFilter("event", string(dockerEvents.ActionStop))
|
||||
DockerFilterDie = NewDockerFilter("event", string(dockerEvents.ActionDie))
|
||||
DockerFilterDestroy = NewDockerFilter("event", string(dockerEvents.ActionDestroy))
|
||||
DockerFilterKill = NewDockerFilter("event", string(dockerEvents.ActionKill))
|
||||
DockerFilterPause = NewDockerFilter("event", string(dockerEvents.ActionPause))
|
||||
DockerFilterUnpause = NewDockerFilter("event", string(dockerEvents.ActionUnPause))
|
||||
|
||||
NewDockerFilter = filters.NewArgs
|
||||
|
||||
optionsDefault = DockerListOptions{Filters: NewDockerFilter(
|
||||
optionsDefault = DockerListOptions{Filters: NewDockerFilters(
|
||||
DockerFilterContainer,
|
||||
DockerFilterStart,
|
||||
// DockerFilterStop,
|
||||
@@ -51,8 +69,8 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func DockerFilterContainerNameID(nameOrID string) filters.KeyValuePair {
|
||||
return filters.Arg("container", nameOrID)
|
||||
func DockerFilterContainerNameID(nameOrID string) DockerFilter {
|
||||
return NewDockerFilter("container", nameOrID)
|
||||
}
|
||||
|
||||
func NewDockerWatcher(host string) DockerWatcher {
|
||||
@@ -80,15 +98,15 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
|
||||
client.Close()
|
||||
}()
|
||||
|
||||
cEventCh, cErrCh := client.Events(ctx, options)
|
||||
chs := client.Events(ctx, options)
|
||||
defer log.Debug().Str("host", client.Address()).Msg("docker watcher closed")
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case msg := <-cEventCh:
|
||||
case msg := <-chs.Messages:
|
||||
w.handleEvent(msg, eventCh)
|
||||
case err := <-cErrCh:
|
||||
case err := <-chs.Err:
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
@@ -117,7 +135,7 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
|
||||
// connection successful, trigger reload (reload routes)
|
||||
eventCh <- reloadTrigger
|
||||
// reopen event channel
|
||||
cEventCh, cErrCh = client.Events(ctx, options)
|
||||
chs = client.Events(ctx, options)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -3,7 +3,7 @@ package events
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
dockerEvents "github.com/docker/docker/api/types/events"
|
||||
dockerEvents "github.com/moby/moby/api/types/events"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
@@ -5,12 +5,13 @@ import (
|
||||
|
||||
agentPkg "github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
"github.com/yusing/goutils/synk"
|
||||
)
|
||||
|
||||
type (
|
||||
AgentProxiedMonitor struct {
|
||||
agent *agentPkg.AgentConfig
|
||||
query string
|
||||
query synk.Value[string]
|
||||
*monitor
|
||||
}
|
||||
AgentCheckHealthTarget struct {
|
||||
@@ -47,14 +48,14 @@ func (target *AgentCheckHealthTarget) displayURL() *url.URL {
|
||||
func NewAgentProxiedMonitor(agent *agentPkg.AgentConfig, config *types.HealthCheckConfig, target *AgentCheckHealthTarget) *AgentProxiedMonitor {
|
||||
mon := &AgentProxiedMonitor{
|
||||
agent: agent,
|
||||
query: target.buildQuery(),
|
||||
}
|
||||
mon.monitor = newMonitor(target.displayURL(), config, mon.CheckHealth)
|
||||
mon.query.Store(target.buildQuery())
|
||||
return mon
|
||||
}
|
||||
|
||||
func (mon *AgentProxiedMonitor) CheckHealth() (types.HealthCheckResult, error) {
|
||||
resp, err := mon.agent.DoHealthCheck(mon.config.Timeout, mon.query)
|
||||
resp, err := mon.agent.DoHealthCheck(mon.config.Timeout, mon.query.Load())
|
||||
result := types.HealthCheckResult{
|
||||
Healthy: resp.Healthy,
|
||||
Detail: resp.Detail,
|
||||
@@ -62,3 +63,9 @@ func (mon *AgentProxiedMonitor) CheckHealth() (types.HealthCheckResult, error) {
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (mon *AgentProxiedMonitor) UpdateURL(url *url.URL) {
|
||||
mon.monitor.UpdateURL(url)
|
||||
newTarget := AgentTargetFromURL(url)
|
||||
mon.query.Store(newTarget.buildQuery())
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ package monitor
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
@@ -49,6 +51,13 @@ func (mon *DockerHealthMonitor) Start(parent task.Parent) gperr.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mon *DockerHealthMonitor) UpdateURL(url *url.URL) {
|
||||
mon.monitor.UpdateURL(url)
|
||||
if mon.fallback != nil {
|
||||
mon.fallback.UpdateURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
func (mon *DockerHealthMonitor) interceptInspectResponse(resp *http.Response) (intercepted bool, err error) {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false, nil
|
||||
@@ -80,7 +89,7 @@ func (mon *DockerHealthMonitor) CheckHealth() (types.HealthCheckResult, error) {
|
||||
defer cancel()
|
||||
|
||||
// the actual inspect response is intercepted and returned as RequestInterceptedError
|
||||
_, err := mon.client.ContainerInspect(ctx, mon.containerID)
|
||||
_, err := mon.client.ContainerInspect(ctx, mon.containerID, client.ContainerInspectOptions{})
|
||||
|
||||
var interceptedErr *httputils.RequestInterceptedError
|
||||
if !httputils.AsRequestInterceptedError(err, &interceptedErr) {
|
||||
@@ -103,7 +112,7 @@ func (mon *DockerHealthMonitor) CheckHealth() (types.HealthCheckResult, error) {
|
||||
mon.numDockerFailures = 0
|
||||
return types.HealthCheckResult{
|
||||
Healthy: false,
|
||||
Detail: "container is " + status,
|
||||
Detail: "container is " + string(status),
|
||||
}, nil
|
||||
case "created":
|
||||
mon.numDockerFailures = 0
|
||||
|
||||
@@ -186,6 +186,10 @@ func (mon *monitor) Finish(reason any) {
|
||||
|
||||
// UpdateURL implements HealthChecker.
|
||||
func (mon *monitor) UpdateURL(url *url.URL) {
|
||||
if url == nil {
|
||||
log.Warn().Msg("attempting to update health monitor URL with nil")
|
||||
return
|
||||
}
|
||||
mon.url.Store(url)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/url"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
@@ -32,12 +34,24 @@ func (mon *RawHealthMonitor) CheckHealth() (types.HealthCheckResult, error) {
|
||||
url := mon.url.Load()
|
||||
start := time.Now()
|
||||
conn, err := mon.dialer.DialContext(ctx, url.Scheme, url.Host)
|
||||
lat := time.Since(start)
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) ||
|
||||
errors.Is(err, syscall.ECONNREFUSED) ||
|
||||
errors.Is(err, syscall.ECONNRESET) ||
|
||||
errors.Is(err, syscall.ECONNABORTED) ||
|
||||
errors.Is(err, syscall.EPIPE) {
|
||||
return types.HealthCheckResult{
|
||||
Latency: lat,
|
||||
Healthy: false,
|
||||
Detail: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
return types.HealthCheckResult{}, err
|
||||
}
|
||||
defer conn.Close()
|
||||
return types.HealthCheckResult{
|
||||
Latency: time.Since(start),
|
||||
Latency: lat,
|
||||
Healthy: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,20 @@ path = "internal/api/v1/docs/swagger.json"
|
||||
with open(path, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
if isinstance(data["definitions"]["netip.Addr"], dict):
|
||||
# MarshalText()
|
||||
data["definitions"]["netip.Addr"] = {
|
||||
"anyOf": [{
|
||||
"type": "string",
|
||||
"format": "ipv4"
|
||||
}, {
|
||||
"type": "string",
|
||||
"format": "ipv6"
|
||||
}],
|
||||
"x-nullable": False,
|
||||
"x-omitempty": False
|
||||
}
|
||||
|
||||
def set_non_nullable(data):
|
||||
if not isinstance(data, dict):
|
||||
return
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/yusing/godoxy/socketproxy
|
||||
|
||||
go 1.25.3
|
||||
go 1.25.4
|
||||
|
||||
exclude github.com/yusing/goutils v0.4.2
|
||||
|
||||
@@ -9,7 +9,7 @@ replace github.com/yusing/goutils => ../goutils
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/yusing/goutils v0.7.0
|
||||
golang.org/x/net v0.46.0
|
||||
golang.org/x/net v0.47.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -19,6 +19,6 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 // indirect
|
||||
github.com/rs/zerolog v1.34.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
)
|
||||
|
||||
@@ -21,14 +21,14 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
1
sonar-project.properties
Normal file
1
sonar-project.properties
Normal file
@@ -0,0 +1 @@
|
||||
sonar.projectKey=yusing_godoxy_33d53d4d-f93d-4390-8726-4635ec5f7610
|
||||
Reference in New Issue
Block a user