mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-16 08:26:49 +01:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3568d9c35 | ||
|
|
44ef351840 | ||
|
|
a39d527fc1 | ||
|
|
22ab043e06 | ||
|
|
b670cdbd49 | ||
|
|
45e34d691a | ||
|
|
e82480a639 | ||
|
|
e39407886d | ||
|
|
3135e377a9 | ||
|
|
bdb3343a7c | ||
|
|
b411c6d504 | ||
|
|
966a59b5c9 | ||
|
|
58db228e25 | ||
|
|
e737737415 | ||
|
|
9087c4f195 | ||
|
|
4705989f4b | ||
|
|
cb506120dd | ||
|
|
88aaf956e5 | ||
|
|
ecfd018b0b | ||
|
|
54bf84dcba | ||
|
|
57200bc1e9 | ||
|
|
6f9bb410f5 | ||
|
|
e62e667b49 | ||
|
|
abe81541db | ||
|
|
9e5d33714c | ||
|
|
93a81fd558 | ||
|
|
72923b8cfa | ||
|
|
24ba4c2a46 | ||
|
|
ed07bf42ce | ||
|
|
371e756307 | ||
|
|
32d8292b17 | ||
|
|
717fd0e58c | ||
|
|
2628d9e8a8 | ||
|
|
c90795e614 | ||
|
|
4a6bed7728 |
4
.github/workflows/docker-image.yml
vendored
4
.github/workflows/docker-image.yml
vendored
@@ -84,10 +84,10 @@ jobs:
|
||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ inputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}
|
||||
type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }}
|
||||
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }}
|
||||
cache-to: |
|
||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max
|
||||
type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }},mode=max
|
||||
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }},mode=max
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
MAKE_ARGS=${{ env.MAKE_ARGS }}
|
||||
|
||||
@@ -16,7 +16,7 @@ require (
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/yusing/go-proxy v0.0.0-00010101000000-000000000000
|
||||
github.com/yusing/go-proxy v0.13.6
|
||||
github.com/yusing/go-proxy/internal/utils v0.0.0
|
||||
github.com/yusing/go-proxy/socketproxy v0.0.0-00010101000000-000000000000
|
||||
)
|
||||
@@ -33,8 +33,8 @@ require (
|
||||
github.com/diskfs/go-diskfs v1.6.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/docker/cli v28.1.1+incompatible // indirect
|
||||
github.com/docker/docker v28.1.1+incompatible // indirect
|
||||
github.com/docker/cli v28.2.1+incompatible // indirect
|
||||
github.com/docker/docker v28.2.1+incompatible // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
@@ -46,7 +46,7 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/goccy/go-yaml v1.17.1 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
|
||||
20
agent/go.sum
20
agent/go.sum
@@ -29,8 +29,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k=
|
||||
github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v28.2.1+incompatible h1:AYyTcuwvhl9dXdyCiXlOGXiIqSNYzTmaDNpxIISPGsM=
|
||||
github.com/docker/cli v28.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -45,8 +45,8 @@ github.com/go-acme/lego/v4 v4.23.1 h1:lZ5fGtGESA2L9FB8dNTvrQUq3/X4QOb8ExkKyY7LSV
|
||||
github.com/go-acme/lego/v4 v4.23.1/go.mod h1:7UMVR7oQbIYw6V7mTgGwi4Er7B6Ww0c+c8feiBM0EgI=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
@@ -64,8 +64,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
|
||||
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godoxy-app/docker v0.0.0-20250523125835-a2474a6ebe30 h1:+5pYG8clUrZbUDP+x149jkRfYAGaNpAXOwut0jluoYA=
|
||||
github.com/godoxy-app/docker v0.0.0-20250523125835-a2474a6ebe30/go.mod h1:7VkicOZ3VrlxOe/EP/8uwsWLGKI2wt2MV7CgxTDIYgA=
|
||||
@@ -318,10 +318,10 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
|
||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
|
||||
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
36
go.mod
36
go.mod
@@ -17,7 +17,7 @@ replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // parsing HTML for extract fav icon
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 // oidc authentication
|
||||
github.com/docker/docker v28.1.1+incompatible // docker daemon
|
||||
github.com/docker/docker v28.2.1+incompatible // docker daemon
|
||||
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||
github.com/go-acme/lego/v4 v4.23.1 // acme client
|
||||
github.com/go-playground/validator/v10 v10.26.0 // validator
|
||||
@@ -32,12 +32,13 @@ require (
|
||||
golang.org/x/crypto v0.38.0 // encrypting password with bcrypt
|
||||
golang.org/x/net v0.40.0 // HTTP header utilities
|
||||
golang.org/x/oauth2 v0.30.0 // oauth2 authentication
|
||||
golang.org/x/sync v0.14.0
|
||||
golang.org/x/time v0.11.0 // time utilities
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/docker/cli v28.1.1+incompatible
|
||||
github.com/goccy/go-yaml v1.17.1 // yaml parsing for different config files
|
||||
github.com/docker/cli v28.2.1+incompatible
|
||||
github.com/goccy/go-yaml v1.18.0 // yaml parsing for different config files
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/luthermonson/go-proxmox v0.2.2
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
@@ -45,8 +46,8 @@ require (
|
||||
github.com/samber/slog-zerolog/v2 v2.7.3
|
||||
github.com/spf13/afero v1.14.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/yusing/go-proxy/agent v0.0.0-00010101000000-000000000000
|
||||
github.com/yusing/go-proxy/internal/dnsproviders v0.0.0-00010101000000-000000000000
|
||||
github.com/yusing/go-proxy/agent v0.0.0-20250529124236-93a81fd558e6
|
||||
github.com/yusing/go-proxy/internal/dnsproviders v0.0.0-20250529124236-93a81fd558e6
|
||||
github.com/yusing/go-proxy/internal/utils v0.0.0
|
||||
)
|
||||
|
||||
@@ -82,7 +83,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
|
||||
github.com/aws/smithy-go v1.22.3 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.228 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.229 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/boombuler/barcode v1.0.2 // indirect
|
||||
github.com/buger/goterm v1.0.4 // indirect
|
||||
@@ -97,14 +98,14 @@ require (
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/exoscale/egoscale/v3 v3.1.18 // indirect
|
||||
github.com/exoscale/egoscale/v3 v3.1.20 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
@@ -126,7 +127,7 @@ require (
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.150 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.152 // indirect
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect
|
||||
github.com/jinzhu/copier v0.4.0 // indirect
|
||||
@@ -138,7 +139,7 @@ require (
|
||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
||||
github.com/labbsr0x/goh v1.0.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/linode/linodego v1.51.0 // indirect
|
||||
github.com/linode/linodego v1.52.0 // indirect
|
||||
github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
|
||||
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
@@ -168,7 +169,7 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/oracle/oci-go-sdk/v65 v65.91.1 // indirect
|
||||
github.com/oracle/oci-go-sdk/v65 v65.92.0 // indirect
|
||||
github.com/ovh/go-ovh v1.7.0 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
@@ -202,15 +203,15 @@ require (
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/viper v1.20.1 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1172 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1170 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1177 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1174 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
|
||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.209 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.210 // indirect
|
||||
github.com/vultr/govultr/v3 v3.20.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
@@ -227,13 +228,12 @@ require (
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
google.golang.org/api v0.234.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
|
||||
google.golang.org/grpc v1.72.1 // indirect
|
||||
google.golang.org/api v0.235.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/grpc v1.72.2 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
||||
58
go.sum
58
go.sum
@@ -710,8 +710,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjK
|
||||
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
|
||||
github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/baidubce/bce-sdk-go v0.9.228 h1:XEY3/oAxXcsi7+3atib9fMI6YNE9sL5qo+WMZ+iqmNE=
|
||||
github.com/baidubce/bce-sdk-go v0.9.228/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
|
||||
github.com/baidubce/bce-sdk-go v0.9.229 h1:oL+YlYDqeMrE6NHgPhkI+4T2geeIHz4PFVivvQ0ysug=
|
||||
github.com/baidubce/bce-sdk-go v0.9.229/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
@@ -803,8 +803,8 @@ github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/dnsimple/dnsimple-go v1.7.0 h1:JKu9xJtZ3SqOC+BuYgAWeab7+EEx0sz422vu8j611ZY=
|
||||
github.com/dnsimple/dnsimple-go v1.7.0/go.mod h1:EKpuihlWizqYafSnQHGCd/gyvy3HkEQJ7ODB4KdV8T8=
|
||||
github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k=
|
||||
github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v28.2.1+incompatible h1:AYyTcuwvhl9dXdyCiXlOGXiIqSNYzTmaDNpxIISPGsM=
|
||||
github.com/docker/cli v28.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -835,8 +835,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
|
||||
github.com/exoscale/egoscale/v3 v3.1.18 h1:9LvH61Cd0L/JC04KartbICl/hVva5AAwtTzzOxLtSYA=
|
||||
github.com/exoscale/egoscale/v3 v3.1.18/go.mod h1:t9+MpSEam94na48O/xgvvPFpQPRiwZ3kBN4/UuQtKco=
|
||||
github.com/exoscale/egoscale/v3 v3.1.20 h1:AZw7VWblEM4+77bjFt9pHBvOQj+spqLkDVxGxK7cbUU=
|
||||
github.com/exoscale/egoscale/v3 v3.1.20/go.mod h1:A53enXfm8nhVMpIYw0QxiwQ2P6AdCF4F/nVYChNEzdE=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
@@ -895,8 +895,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
@@ -930,8 +930,8 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
|
||||
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godoxy-app/docker v0.0.0-20250523125835-a2474a6ebe30 h1:+5pYG8clUrZbUDP+x149jkRfYAGaNpAXOwut0jluoYA=
|
||||
github.com/godoxy-app/docker v0.0.0-20250523125835-a2474a6ebe30/go.mod h1:7VkicOZ3VrlxOe/EP/8uwsWLGKI2wt2MV7CgxTDIYgA=
|
||||
@@ -1154,8 +1154,8 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
|
||||
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
|
||||
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.150 h1:Ih+z79Ko1ClH4dlv7O1lyHRiVCjkb2NZYYk+1cSZbU8=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.150/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.152 h1:3ebdqyhjb5cvRDzokKk7TKvI8Wud7tpQET+0yhBkXu4=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.152/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY=
|
||||
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
|
||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
@@ -1239,8 +1239,8 @@ github.com/labbsr0x/goh v1.0.1 h1:97aBJkDjpyBZGPbQuOK5/gHcSFbcr5aRsq3RSRJFpPk=
|
||||
github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w=
|
||||
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.51.0 h1:8S0vNQRUZAuumGFe8h/huCzbyExxlvOUR2Uf0oDoJ/8=
|
||||
github.com/linode/linodego v1.51.0/go.mod h1:zEN2sX+cSdp67EuRY1HJiyuLujoa7HqvVwNEcJv3iXw=
|
||||
github.com/linode/linodego v1.52.0 h1:SN1PSekrZBcHtRt1pADTz0JO7NX9pQv/Gf8Jc9s9l8w=
|
||||
github.com/linode/linodego v1.52.0/go.mod h1:zEN2sX+cSdp67EuRY1HJiyuLujoa7HqvVwNEcJv3iXw=
|
||||
github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
|
||||
github.com/liquidweb/go-lwApi v0.0.5/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
|
||||
github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ=
|
||||
@@ -1402,8 +1402,8 @@ github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYr
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
|
||||
github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.91.1 h1:jRE4jUiJd+sDhJTyTCXIJdzLjTx+99gA3PgzpVTaF/I=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.91.1/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.92.0 h1:qLy3VH3aEO1c4RdKu58cmNrITtRbrgpRGOsYxgP2eXU=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.92.0/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
github.com/ovh/go-ovh v1.7.0 h1:V14nF7FwDjQrZt9g7jzcvAAQ3HN6DNShRFRMC3jLoPw=
|
||||
@@ -1619,11 +1619,11 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1170/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1172 h1:cfh0m0esFTjBZ6YZRCVWUDs772KOfxQR/mKY7zL8/qg=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1172/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1170 h1:qdjh5LP8N2bE8cPfBX6Y3S/3+5yD2JLJBx7EALVrgP0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1170/go.mod h1:OS8GJ/x+p/6Wchc6Lp6iA9Gf9sQ/it0xatH1AXGX8PY=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1174/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1177 h1:Lcr72Dtd1eRYP4jz9xch9SN5lJ80e2RFnIwTHlaxAFY=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1177/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1174 h1:LRmjetOxA7ZjHIEiYKoZmVOntxpzscgrse50PDEUs/s=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1174/go.mod h1:H0oIwVla6DcgpKnEvFcFEphENZng64Mqkq2p4alU4tE=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
@@ -1647,8 +1647,8 @@ github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8A
|
||||
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||
github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
|
||||
github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.209 h1:aYCVO2fQ4meqLZ+XjVLcKzDn7kYfrUekqp48iioH3ug=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.209/go.mod h1:stZX+EPgv1vF4nZwOlEe8iGcriUPRBKX8zA19gXycOQ=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.210 h1:cEOHlBxopRSQ939W+lxA7S8OwuyCkkBbC+NguES+3Z8=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.210/go.mod h1:stZX+EPgv1vF4nZwOlEe8iGcriUPRBKX8zA19gXycOQ=
|
||||
github.com/vultr/govultr/v3 v3.20.0 h1:O+Om6gXpN6ehwAIIKq5DyGuekpyHaoRlwrxTb44bDzA=
|
||||
github.com/vultr/govultr/v3 v3.20.0/go.mod h1:q34Wd76upKmf+vxFMgaNMH3A8BbsPBmSYZUGC8oZa5w=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
@@ -2298,8 +2298,8 @@ google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60c
|
||||
google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
|
||||
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
|
||||
google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
|
||||
google.golang.org/api v0.234.0 h1:d3sAmYq3E9gdr2mpmiWGbm9pHsA/KJmyiLkwKfHBqU4=
|
||||
google.golang.org/api v0.234.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg=
|
||||
google.golang.org/api v0.235.0 h1:C3MkpQSRxS1Jy6AkzTGKKrpSCOd2WOGrezZ+icKSkKo=
|
||||
google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -2444,8 +2444,8 @@ google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRx
|
||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -2488,8 +2488,8 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
|
||||
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
|
||||
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
|
||||
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
|
||||
@@ -19,7 +19,6 @@ func RenewCert(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
conn, err := gpwebsocket.Initiate(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -162,6 +163,8 @@ func (auth *OIDCProvider) HandleAuth(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
var rateLimit = rate.NewLimiter(rate.Every(time.Second), 1)
|
||||
|
||||
func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// check for session token
|
||||
sessionToken, err := r.Cookie(CookieOauthSessionToken)
|
||||
@@ -182,10 +185,21 @@ func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !rateLimit.Allow() {
|
||||
http.Error(w, "auth rate limit exceeded", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
state := generateState()
|
||||
SetTokenCookie(w, r, CookieOauthState, state, 300*time.Second)
|
||||
// redirect user to Idp
|
||||
http.Redirect(w, r, auth.oauthConfig.AuthCodeURL(state, optRedirectPostAuth(r)), http.StatusFound)
|
||||
url := auth.oauthConfig.AuthCodeURL(state, optRedirectPostAuth(r))
|
||||
if IsFrontend(r) {
|
||||
w.Header().Set("X-Redirect-To", url)
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
} else {
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func parseClaims(idToken *oidc.IDToken) (*IDTokenClaims, error) {
|
||||
|
||||
@@ -124,7 +124,8 @@ func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
func (auth *UserPassAuth) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/login", http.StatusFound) // redirects to WebUI login page
|
||||
w.Header().Set("X-Redirect-To", "/login")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}
|
||||
|
||||
func (auth *UserPassAuth) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -16,7 +17,15 @@ var (
|
||||
)
|
||||
|
||||
func IsFrontend(r *http.Request) bool {
|
||||
return r.Host == common.APIHTTPAddr
|
||||
return requestRemoteIP(r) == "127.0.0.1"
|
||||
}
|
||||
|
||||
func requestRemoteIP(r *http.Request) string {
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
func requestHost(r *http.Request) string {
|
||||
|
||||
@@ -117,13 +117,13 @@ func Reload() gperr.Error {
|
||||
newCfg := newConfig()
|
||||
err := newCfg.load()
|
||||
if err != nil {
|
||||
newCfg.task.Finish(err)
|
||||
newCfg.task.FinishAndWait(err)
|
||||
return gperr.New(ansi.Warning("using last config")).With(err)
|
||||
}
|
||||
|
||||
// cancel all current subtasks -> wait
|
||||
// -> replace config -> start new subtasks
|
||||
config.GetInstance().(*Config).Task().Finish("config changed")
|
||||
config.GetInstance().(*Config).Task().FinishAndWait("config changed")
|
||||
newCfg.Start(StartAllServers)
|
||||
config.SetInstance(newCfg)
|
||||
return nil
|
||||
|
||||
@@ -2,36 +2,25 @@ package config
|
||||
|
||||
import (
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/route/provider"
|
||||
)
|
||||
|
||||
func (cfg *Config) DumpRoutes() map[string]*route.Route {
|
||||
entries := make(map[string]*route.Route)
|
||||
cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
|
||||
p.RangeRoutes(func(alias string, r *route.Route) {
|
||||
entries[alias] = r
|
||||
})
|
||||
})
|
||||
return entries
|
||||
}
|
||||
|
||||
func (cfg *Config) DumpRouteProviders() map[string]*provider.Provider {
|
||||
entries := make(map[string]*provider.Provider)
|
||||
cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
|
||||
entries := make(map[string]*provider.Provider, cfg.providers.Size())
|
||||
for _, p := range cfg.providers.Range {
|
||||
entries[p.ShortName()] = p
|
||||
})
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func (cfg *Config) RouteProviderList() []config.RouteProviderListResponse {
|
||||
var list []config.RouteProviderListResponse
|
||||
cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
|
||||
list := make([]config.RouteProviderListResponse, 0, cfg.providers.Size())
|
||||
for _, p := range cfg.providers.Range {
|
||||
list = append(list, config.RouteProviderListResponse{
|
||||
ShortName: p.ShortName(),
|
||||
FullName: p.String(),
|
||||
})
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
@@ -40,13 +29,13 @@ func (cfg *Config) Statistics() map[string]any {
|
||||
var total uint16
|
||||
providerStats := make(map[string]provider.ProviderStats)
|
||||
|
||||
cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
|
||||
for _, p := range cfg.providers.Range {
|
||||
stats := p.Statistics()
|
||||
providerStats[p.ShortName()] = stats
|
||||
rps.AddOther(stats.RPs)
|
||||
streams.AddOther(stats.Streams)
|
||||
total += stats.RPs.Total + stats.Streams.Total
|
||||
})
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"total": total,
|
||||
|
||||
@@ -8,7 +8,7 @@ replace github.com/yusing/go-proxy/internal/utils => ../utils
|
||||
|
||||
require (
|
||||
github.com/go-acme/lego/v4 v4.23.1
|
||||
github.com/yusing/go-proxy v0.0.0-00010101000000-000000000000
|
||||
github.com/yusing/go-proxy v0.13.6
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -41,7 +41,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
|
||||
github.com/aws/smithy-go v1.22.3 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.228 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.229 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/boombuler/barcode v1.0.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
@@ -49,7 +49,7 @@ require (
|
||||
github.com/cloudflare/cloudflare-go v0.115.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
|
||||
github.com/exoscale/egoscale/v3 v3.1.18 // indirect
|
||||
github.com/exoscale/egoscale/v3 v3.1.20 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
@@ -57,7 +57,7 @@ require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
@@ -65,7 +65,7 @@ require (
|
||||
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.17.1 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
@@ -80,7 +80,7 @@ require (
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.150 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.152 // indirect
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
@@ -91,7 +91,7 @@ require (
|
||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
||||
github.com/labbsr0x/goh v1.0.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/linode/linodego v1.51.0 // indirect
|
||||
github.com/linode/linodego v1.52.0 // indirect
|
||||
github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
|
||||
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
@@ -115,7 +115,7 @@ require (
|
||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/oracle/oci-go-sdk/v65 v65.91.1 // indirect
|
||||
github.com/oracle/oci-go-sdk/v65 v65.92.0 // indirect
|
||||
github.com/ovh/go-ovh v1.7.0 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
@@ -148,13 +148,13 @@ require (
|
||||
github.com/spf13/viper v1.20.1 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1172 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1170 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1177 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1174 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
|
||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.209 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.210 // indirect
|
||||
github.com/vultr/govultr/v3 v3.20.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
@@ -177,9 +177,9 @@ require (
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
google.golang.org/api v0.234.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
|
||||
google.golang.org/grpc v1.72.1 // indirect
|
||||
google.golang.org/api v0.235.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/grpc v1.72.2 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
||||
@@ -702,8 +702,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjK
|
||||
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
|
||||
github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/baidubce/bce-sdk-go v0.9.228 h1:XEY3/oAxXcsi7+3atib9fMI6YNE9sL5qo+WMZ+iqmNE=
|
||||
github.com/baidubce/bce-sdk-go v0.9.228/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
|
||||
github.com/baidubce/bce-sdk-go v0.9.229 h1:oL+YlYDqeMrE6NHgPhkI+4T2geeIHz4PFVivvQ0ysug=
|
||||
github.com/baidubce/bce-sdk-go v0.9.229/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
@@ -801,8 +801,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
|
||||
github.com/exoscale/egoscale/v3 v3.1.18 h1:9LvH61Cd0L/JC04KartbICl/hVva5AAwtTzzOxLtSYA=
|
||||
github.com/exoscale/egoscale/v3 v3.1.18/go.mod h1:t9+MpSEam94na48O/xgvvPFpQPRiwZ3kBN4/UuQtKco=
|
||||
github.com/exoscale/egoscale/v3 v3.1.20 h1:AZw7VWblEM4+77bjFt9pHBvOQj+spqLkDVxGxK7cbUU=
|
||||
github.com/exoscale/egoscale/v3 v3.1.20/go.mod h1:A53enXfm8nhVMpIYw0QxiwQ2P6AdCF4F/nVYChNEzdE=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
@@ -861,8 +861,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
|
||||
@@ -890,8 +890,8 @@ github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVr
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
|
||||
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||
@@ -1100,8 +1100,8 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
|
||||
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
|
||||
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.150 h1:Ih+z79Ko1ClH4dlv7O1lyHRiVCjkb2NZYYk+1cSZbU8=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.150/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.152 h1:3ebdqyhjb5cvRDzokKk7TKvI8Wud7tpQET+0yhBkXu4=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.152/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY=
|
||||
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
|
||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
@@ -1181,8 +1181,8 @@ github.com/labbsr0x/goh v1.0.1 h1:97aBJkDjpyBZGPbQuOK5/gHcSFbcr5aRsq3RSRJFpPk=
|
||||
github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w=
|
||||
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.51.0 h1:8S0vNQRUZAuumGFe8h/huCzbyExxlvOUR2Uf0oDoJ/8=
|
||||
github.com/linode/linodego v1.51.0/go.mod h1:zEN2sX+cSdp67EuRY1HJiyuLujoa7HqvVwNEcJv3iXw=
|
||||
github.com/linode/linodego v1.52.0 h1:SN1PSekrZBcHtRt1pADTz0JO7NX9pQv/Gf8Jc9s9l8w=
|
||||
github.com/linode/linodego v1.52.0/go.mod h1:zEN2sX+cSdp67EuRY1HJiyuLujoa7HqvVwNEcJv3iXw=
|
||||
github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
|
||||
github.com/liquidweb/go-lwApi v0.0.5/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
|
||||
github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ=
|
||||
@@ -1322,8 +1322,8 @@ github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYr
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
|
||||
github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.91.1 h1:jRE4jUiJd+sDhJTyTCXIJdzLjTx+99gA3PgzpVTaF/I=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.91.1/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.92.0 h1:qLy3VH3aEO1c4RdKu58cmNrITtRbrgpRGOsYxgP2eXU=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.92.0/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA=
|
||||
github.com/ovh/go-ovh v1.7.0 h1:V14nF7FwDjQrZt9g7jzcvAAQ3HN6DNShRFRMC3jLoPw=
|
||||
github.com/ovh/go-ovh v1.7.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
@@ -1518,11 +1518,11 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1170/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1172 h1:cfh0m0esFTjBZ6YZRCVWUDs772KOfxQR/mKY7zL8/qg=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1172/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1170 h1:qdjh5LP8N2bE8cPfBX6Y3S/3+5yD2JLJBx7EALVrgP0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1170/go.mod h1:OS8GJ/x+p/6Wchc6Lp6iA9Gf9sQ/it0xatH1AXGX8PY=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1174/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1177 h1:Lcr72Dtd1eRYP4jz9xch9SN5lJ80e2RFnIwTHlaxAFY=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1177/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1174 h1:LRmjetOxA7ZjHIEiYKoZmVOntxpzscgrse50PDEUs/s=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1174/go.mod h1:H0oIwVla6DcgpKnEvFcFEphENZng64Mqkq2p4alU4tE=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
@@ -1538,8 +1538,8 @@ github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec/go.mod h1:BZr7
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
|
||||
github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.209 h1:aYCVO2fQ4meqLZ+XjVLcKzDn7kYfrUekqp48iioH3ug=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.209/go.mod h1:stZX+EPgv1vF4nZwOlEe8iGcriUPRBKX8zA19gXycOQ=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.210 h1:cEOHlBxopRSQ939W+lxA7S8OwuyCkkBbC+NguES+3Z8=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.210/go.mod h1:stZX+EPgv1vF4nZwOlEe8iGcriUPRBKX8zA19gXycOQ=
|
||||
github.com/vultr/govultr/v3 v3.20.0 h1:O+Om6gXpN6ehwAIIKq5DyGuekpyHaoRlwrxTb44bDzA=
|
||||
github.com/vultr/govultr/v3 v3.20.0/go.mod h1:q34Wd76upKmf+vxFMgaNMH3A8BbsPBmSYZUGC8oZa5w=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
@@ -2155,8 +2155,8 @@ google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60c
|
||||
google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
|
||||
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
|
||||
google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
|
||||
google.golang.org/api v0.234.0 h1:d3sAmYq3E9gdr2mpmiWGbm9pHsA/KJmyiLkwKfHBqU4=
|
||||
google.golang.org/api v0.234.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg=
|
||||
google.golang.org/api v0.235.0 h1:C3MkpQSRxS1Jy6AkzTGKKrpSCOd2WOGrezZ+icKSkKo=
|
||||
google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -2301,8 +2301,8 @@ google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRx
|
||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -2345,8 +2345,8 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
|
||||
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
|
||||
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
|
||||
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
|
||||
@@ -58,20 +58,16 @@ func initClientCleaner() {
|
||||
case <-ticker.C:
|
||||
closeTimedOutClients()
|
||||
case <-cleaner.Context().Done():
|
||||
clientMapMu.Lock()
|
||||
for _, c := range clientMap {
|
||||
delete(clientMap, c.Key())
|
||||
c.Client.Close()
|
||||
}
|
||||
clientMapMu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
task.OnProgramExit("docker_clients_cleanup", func() {
|
||||
clientMapMu.Lock()
|
||||
defer clientMapMu.Unlock()
|
||||
|
||||
for _, c := range clientMap {
|
||||
delete(clientMap, c.Key())
|
||||
c.Client.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func closeTimedOutClients() {
|
||||
|
||||
@@ -56,13 +56,15 @@ type (
|
||||
var DummyContainer = new(Container)
|
||||
|
||||
func FromDocker(c *container.SummaryTrimmed, dockerHost string) (res *Container) {
|
||||
isExplicit := false
|
||||
_, isExplicit := c.Labels[LabelAliases]
|
||||
helper := containerHelper{c}
|
||||
for lbl := range c.Labels {
|
||||
if strings.HasPrefix(lbl, NSProxy+".") {
|
||||
isExplicit = true
|
||||
} else {
|
||||
delete(c.Labels, lbl)
|
||||
if !isExplicit {
|
||||
// walk through all labels to check if any label starts with NSProxy.
|
||||
for lbl := range c.Labels {
|
||||
if strings.HasPrefix(lbl, NSProxy+".") {
|
||||
isExplicit = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,14 +126,30 @@ func (c *Container) UpdatePorts() error {
|
||||
continue
|
||||
}
|
||||
c.PublicPortMapping[portInt] = container.Port{
|
||||
PublicPort: uint16(portInt),
|
||||
PrivatePort: uint16(portInt),
|
||||
PublicPort: uint16(portInt), //nolint:gosec
|
||||
PrivatePort: uint16(portInt), //nolint:gosec
|
||||
Type: proto,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Container) DockerComposeProject() string {
|
||||
return c.Labels["com.docker.compose.project"]
|
||||
}
|
||||
|
||||
func (c *Container) DockerComposeService() string {
|
||||
return c.Labels["com.docker.compose.service"]
|
||||
}
|
||||
|
||||
func (c *Container) Dependencies() []string {
|
||||
deps := c.Labels[LabelDependsOn]
|
||||
if deps == "" {
|
||||
deps = c.Labels["com.docker.compose.depends_on"]
|
||||
}
|
||||
return strings.Split(deps, ",")
|
||||
}
|
||||
|
||||
var databaseMPs = map[string]struct{}{
|
||||
"/var/lib/postgresql/data": {},
|
||||
"/var/lib/mysql": {},
|
||||
@@ -214,17 +232,22 @@ func (c *Container) loadDeleteIdlewatcherLabels(helper containerHelper) {
|
||||
"stop_timeout": helper.getDeleteLabel(LabelStopTimeout),
|
||||
"stop_signal": helper.getDeleteLabel(LabelStopSignal),
|
||||
"start_endpoint": helper.getDeleteLabel(LabelStartEndpoint),
|
||||
"depends_on": c.Dependencies(),
|
||||
}
|
||||
|
||||
// ensure it's deleted from labels
|
||||
helper.getDeleteLabel(LabelDependsOn)
|
||||
|
||||
// set only if idlewatcher is enabled
|
||||
idleTimeout := cfg["idle_timeout"]
|
||||
if idleTimeout != "" {
|
||||
idwCfg := &idlewatcher.Config{
|
||||
Docker: &idlewatcher.DockerConfig{
|
||||
DockerHost: c.DockerHost,
|
||||
ContainerID: c.ContainerID,
|
||||
ContainerName: c.ContainerName,
|
||||
},
|
||||
idwCfg := new(idlewatcher.Config)
|
||||
idwCfg.Docker = &idlewatcher.DockerConfig{
|
||||
DockerHost: c.DockerHost,
|
||||
ContainerID: c.ContainerID,
|
||||
ContainerName: c.ContainerName,
|
||||
}
|
||||
|
||||
err := serialization.MapUnmarshalValidate(cfg, idwCfg)
|
||||
if err != nil {
|
||||
gperr.LogWarn("invalid idlewatcher config", gperr.PrependSubject(c.ContainerName, err))
|
||||
|
||||
@@ -48,10 +48,13 @@ func (c containerHelper) parseImage() *ContainerImage {
|
||||
im.Author = strings.Join(slashSep[:len(slashSep)-1], "/")
|
||||
im.Name = slashSep[len(slashSep)-1]
|
||||
} else {
|
||||
im.Author = "library"
|
||||
im.Name = slashSep[0]
|
||||
}
|
||||
if len(colonSep) > 1 {
|
||||
im.Tag = colonSep[1]
|
||||
} else {
|
||||
im.Tag = "latest"
|
||||
}
|
||||
return im
|
||||
}
|
||||
|
||||
@@ -76,3 +76,40 @@ func TestContainerHostNetworkMode(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageNameParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
full string
|
||||
author string
|
||||
image string
|
||||
tag string
|
||||
}{
|
||||
{
|
||||
full: "ghcr.io/tensorchord/pgvecto-rs",
|
||||
author: "ghcr.io/tensorchord",
|
||||
image: "pgvecto-rs",
|
||||
tag: "latest",
|
||||
},
|
||||
{
|
||||
full: "redis:latest",
|
||||
author: "library",
|
||||
image: "redis",
|
||||
tag: "latest",
|
||||
},
|
||||
{
|
||||
full: "redis:7.4.0-alpine",
|
||||
author: "library",
|
||||
image: "redis",
|
||||
tag: "7.4.0-alpine",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.full, func(t *testing.T) {
|
||||
helper := containerHelper{&container.SummaryTrimmed{Image: tt.full}}
|
||||
im := helper.parseImage()
|
||||
ExpectEqual(t, im.Author, tt.author)
|
||||
ExpectEqual(t, im.Name, tt.image)
|
||||
ExpectEqual(t, im.Tag, tt.tag)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,5 @@ const (
|
||||
LabelStopTimeout = NSProxy + ".stop_timeout"
|
||||
LabelStopSignal = NSProxy + ".stop_signal"
|
||||
LabelStartEndpoint = NSProxy + ".start_endpoint"
|
||||
LabelDependsOn = NSProxy + ".depends_on"
|
||||
)
|
||||
|
||||
@@ -11,3 +11,12 @@ func (w *Watcher) cancelled(reqCtx 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,3 +39,10 @@ func Watchers() iter.Seq2[string, watcherDebug] {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fmtErr(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
65
internal/idlewatcher/errors.go
Normal file
65
internal/idlewatcher/errors.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type watcherError struct {
|
||||
watcher *Watcher
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *watcherError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func (e *watcherError) Error() string {
|
||||
return fmt.Sprintf("watcher %q error: %s", e.watcher.cfg.ContainerName(), e.err.Error())
|
||||
}
|
||||
|
||||
func (w *Watcher) newWatcherError(err error) error {
|
||||
if errors.Is(err, causeReload) {
|
||||
return nil
|
||||
}
|
||||
if wErr, ok := err.(*watcherError); ok { //nolint:errorlint
|
||||
return wErr
|
||||
}
|
||||
return &watcherError{watcher: w, err: convertError(err)}
|
||||
}
|
||||
|
||||
type depError struct {
|
||||
action string
|
||||
dep *dependency
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *depError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func (e *depError) Error() string {
|
||||
return fmt.Sprintf("%s failed for dependency %q: %s", e.action, e.dep.cfg.ContainerName(), e.err.Error())
|
||||
}
|
||||
|
||||
func (w *Watcher) newDepError(action string, dep *dependency, err error) error {
|
||||
if errors.Is(err, causeReload) {
|
||||
return nil
|
||||
}
|
||||
if dErr, ok := err.(*depError); ok { //nolint:errorlint
|
||||
return dErr
|
||||
}
|
||||
return w.newWatcherError(&depError{action: action, dep: dep, err: convertError(err)})
|
||||
}
|
||||
|
||||
func convertError(err error) error {
|
||||
switch {
|
||||
case err == nil:
|
||||
return nil
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return errors.New("timeout")
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -38,10 +36,6 @@ func (w *Watcher) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
default:
|
||||
f := &ForceCacheControl{expires: w.expires().Format(http.TimeFormat), ResponseWriter: rw}
|
||||
w, ok := watcherMap[w.Key()] // could've been reloaded
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
w.rp.ServeHTTP(f, r)
|
||||
}
|
||||
}
|
||||
@@ -50,6 +44,14 @@ func isFaviconPath(path string) bool {
|
||||
return path == "/favicon.ico"
|
||||
}
|
||||
|
||||
func (w *Watcher) redirectToStartEndpoint(rw http.ResponseWriter, r *http.Request) {
|
||||
uri := "/"
|
||||
if w.cfg.StartEndpoint != "" {
|
||||
uri = w.cfg.StartEndpoint
|
||||
}
|
||||
http.Redirect(rw, r, uri, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldNext bool) {
|
||||
w.resetIdleTimer()
|
||||
|
||||
@@ -90,32 +92,32 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
|
||||
return false
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeoutCause(r.Context(), w.cfg.WakeTimeout, errors.New("wake timeout"))
|
||||
defer cancel()
|
||||
|
||||
ctx := r.Context()
|
||||
if w.cancelled(ctx) {
|
||||
gphttp.ServerError(rw, r, context.Cause(ctx), http.StatusServiceUnavailable)
|
||||
w.redirectToStartEndpoint(rw, r)
|
||||
return false
|
||||
}
|
||||
|
||||
w.l.Trace().Msg("signal received")
|
||||
err := w.wakeIfStopped()
|
||||
err := w.Wake(ctx)
|
||||
if err != nil {
|
||||
gphttp.ServerError(rw, r, err)
|
||||
return false
|
||||
}
|
||||
|
||||
var ready bool
|
||||
|
||||
for {
|
||||
w.resetIdleTimer()
|
||||
|
||||
if w.cancelled(ctx) {
|
||||
gphttp.ServerError(rw, r, context.Cause(ctx), http.StatusServiceUnavailable)
|
||||
w.redirectToStartEndpoint(rw, r)
|
||||
return false
|
||||
}
|
||||
|
||||
w, ready, err = checkUpdateState(w.Key())
|
||||
if !w.waitStarted(ctx) {
|
||||
return false
|
||||
}
|
||||
|
||||
ready, err := w.checkUpdateState()
|
||||
if err != nil {
|
||||
gphttp.ServerError(rw, r, err)
|
||||
return false
|
||||
|
||||
@@ -2,7 +2,6 @@ package idlewatcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
@@ -53,22 +52,13 @@ func (w *Watcher) wakeFromStream() error {
|
||||
}
|
||||
|
||||
w.l.Debug().Msg("wake signal received")
|
||||
err := w.wakeIfStopped()
|
||||
err := w.Wake(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeoutCause(w.task.Context(), w.cfg.WakeTimeout, errors.New("wake timeout"))
|
||||
defer cancel()
|
||||
|
||||
var ready bool
|
||||
|
||||
for {
|
||||
if w.cancelled(ctx) {
|
||||
return context.Cause(ctx)
|
||||
}
|
||||
|
||||
w, ready, err = checkUpdateState(w.Key())
|
||||
ready, err := w.checkUpdateState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
@@ -80,43 +79,6 @@ func (w *Watcher) Detail() string {
|
||||
return "napping"
|
||||
}
|
||||
|
||||
func checkUpdateState(key string) (w *Watcher, ready bool, err error) {
|
||||
watcherMapMu.RLock()
|
||||
w, ok := watcherMap[key]
|
||||
if !ok {
|
||||
watcherMapMu.RUnlock()
|
||||
return nil, false, errors.New("watcher not found")
|
||||
}
|
||||
watcherMapMu.RUnlock()
|
||||
|
||||
// already ready
|
||||
if w.ready() {
|
||||
return w, true, nil
|
||||
}
|
||||
|
||||
if !w.running() {
|
||||
return w, false, nil
|
||||
}
|
||||
|
||||
// the new container info not yet updated
|
||||
if w.hc.URL().Host == "" {
|
||||
return w, false, nil
|
||||
}
|
||||
|
||||
res, err := w.hc.CheckHealth()
|
||||
if err != nil {
|
||||
w.setError(err)
|
||||
return w, false, err
|
||||
}
|
||||
|
||||
if res.Healthy {
|
||||
w.setReady()
|
||||
return w, true, nil
|
||||
}
|
||||
w.setStarting()
|
||||
return w, false, nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements health.HealthMonitor.
|
||||
func (w *Watcher) MarshalJSON() ([]byte, error) {
|
||||
url := w.hc.URL()
|
||||
@@ -135,3 +97,32 @@ func (w *Watcher) MarshalJSON() ([]byte, error) {
|
||||
Detail: detail,
|
||||
}).MarshalJSON()
|
||||
}
|
||||
|
||||
func (w *Watcher) checkUpdateState() (ready bool, err error) {
|
||||
// already ready
|
||||
if w.ready() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if !w.running() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// the new container info not yet updated
|
||||
if w.hc.URL().Host == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
res, err := w.hc.CheckHealth()
|
||||
if err != nil {
|
||||
w.setError(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if res.Healthy {
|
||||
w.setReady()
|
||||
return true, nil
|
||||
}
|
||||
w.setStarting()
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -10,16 +10,26 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
Config struct {
|
||||
ProviderConfig struct {
|
||||
Proxmox *ProxmoxConfig `json:"proxmox,omitempty"`
|
||||
Docker *DockerConfig `json:"docker,omitempty"`
|
||||
}
|
||||
IdlewatcherConfig struct {
|
||||
// 0: no idle watcher.
|
||||
// Positive: idle watcher with idle timeout.
|
||||
// Negative: idle watcher as a dependency. IdleTimeout time.Duration `json:"idle_timeout" json_ext:"duration"`
|
||||
IdleTimeout time.Duration `json:"idle_timeout"`
|
||||
WakeTimeout time.Duration `json:"wake_timeout"`
|
||||
StopTimeout time.Duration `json:"stop_timeout"`
|
||||
StopMethod StopMethod `json:"stop_method"`
|
||||
StopSignal Signal `json:"stop_signal,omitempty"`
|
||||
}
|
||||
Config struct {
|
||||
ProviderConfig
|
||||
IdlewatcherConfig
|
||||
|
||||
IdleTimeout time.Duration `json:"idle_timeout" json_ext:"duration"`
|
||||
WakeTimeout time.Duration `json:"wake_timeout" json_ext:"duration"`
|
||||
StopTimeout time.Duration `json:"stop_timeout" json_ext:"duration"`
|
||||
StopMethod StopMethod `json:"stop_method"`
|
||||
StopSignal Signal `json:"stop_signal,omitempty"`
|
||||
StartEndpoint string `json:"start_endpoint,omitempty"` // Optional path that must be hit to start container
|
||||
StartEndpoint string `json:"start_endpoint,omitempty"` // Optional path that must be hit to start container
|
||||
DependsOn []string `json:"depends_on,omitempty"`
|
||||
}
|
||||
StopMethod string
|
||||
Signal string
|
||||
@@ -55,11 +65,11 @@ func (c *Config) ContainerName() string {
|
||||
if c.Docker != nil {
|
||||
return c.Docker.ContainerName
|
||||
}
|
||||
return "lxc " + strconv.Itoa(c.Proxmox.VMID)
|
||||
return "lxc-" + strconv.Itoa(c.Proxmox.VMID)
|
||||
}
|
||||
|
||||
func (c *Config) Validate() gperr.Error {
|
||||
if c.IdleTimeout == 0 { // no idle timeout means no idle watcher
|
||||
if c.IdleTimeout == 0 { // zero idle timeout means no idle watcher
|
||||
return nil
|
||||
}
|
||||
errs := gperr.NewBuilder("idlewatcher config validation error")
|
||||
|
||||
@@ -35,7 +35,8 @@ func TestValidateStartEndpoint(t *testing.T) {
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cfg := Config{StartEndpoint: tc.input}
|
||||
cfg := new(Config)
|
||||
cfg.StartEndpoint = tc.input
|
||||
err := cfg.validateStartEndpoint()
|
||||
if err == nil {
|
||||
expect.Equal(t, cfg.StartEndpoint, tc.input)
|
||||
|
||||
@@ -3,6 +3,8 @@ package idlewatcher
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"maps"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -20,10 +22,13 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
type (
|
||||
routeHelper struct {
|
||||
route routes.Route
|
||||
rp *reverseproxy.ReverseProxy
|
||||
stream net.Stream
|
||||
hc health.HealthChecker
|
||||
@@ -48,8 +53,15 @@ type (
|
||||
state atomic.Value[*containerState]
|
||||
lastReset atomic.Value[time.Time]
|
||||
|
||||
ticker *time.Ticker
|
||||
task *task.Task
|
||||
idleTicker *time.Ticker
|
||||
task *task.Task
|
||||
|
||||
dependsOn []*dependency
|
||||
}
|
||||
|
||||
dependency struct {
|
||||
*Watcher
|
||||
waitHealthy bool
|
||||
}
|
||||
|
||||
StopCallback func() error
|
||||
@@ -57,9 +69,12 @@ type (
|
||||
|
||||
const ContextKey = "idlewatcher.watcher"
|
||||
|
||||
// TODO: replace -1 with neverTick
|
||||
|
||||
var (
|
||||
watcherMap = make(map[string]*Watcher)
|
||||
watcherMapMu sync.RWMutex
|
||||
singleFlight singleflight.Group
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -79,51 +94,167 @@ var (
|
||||
|
||||
const reqTimeout = 3 * time.Second
|
||||
|
||||
// prevents dependencies from being stopped automatically.
|
||||
const neverTick = time.Duration(1<<63 - 1)
|
||||
|
||||
// TODO: fix stream type.
|
||||
func NewWatcher(parent task.Parent, r routes.Route) (*Watcher, error) {
|
||||
cfg := r.IdlewatcherConfig()
|
||||
func NewWatcher(parent task.Parent, r routes.Route, cfg *idlewatcher.Config) (*Watcher, error) {
|
||||
key := cfg.Key()
|
||||
|
||||
watcherMapMu.RLock()
|
||||
// if the watcher already exists, finish it
|
||||
w, exists := watcherMap[key]
|
||||
if exists {
|
||||
if w.cfg == cfg {
|
||||
// same address, likely two routes from the same container
|
||||
return w, nil
|
||||
}
|
||||
w.task.Finish(causeReload)
|
||||
}
|
||||
watcherMapMu.RUnlock()
|
||||
|
||||
w = &Watcher{
|
||||
ticker: time.NewTicker(cfg.IdleTimeout),
|
||||
cfg: cfg,
|
||||
routeHelper: routeHelper{
|
||||
hc: monitor.NewMonitor(r),
|
||||
},
|
||||
if exists {
|
||||
if len(cfg.DependsOn) > 0 {
|
||||
w.cfg.DependsOn = cfg.DependsOn
|
||||
}
|
||||
if cfg.IdleTimeout > 0 {
|
||||
w.cfg.IdlewatcherConfig = cfg.IdlewatcherConfig
|
||||
}
|
||||
cfg = w.cfg
|
||||
w.resetIdleTimer()
|
||||
} else {
|
||||
w = &Watcher{
|
||||
idleTicker: time.NewTicker(cfg.IdleTimeout),
|
||||
cfg: cfg,
|
||||
routeHelper: routeHelper{
|
||||
hc: monitor.NewMonitor(r),
|
||||
},
|
||||
dependsOn: make([]*dependency, 0, len(cfg.DependsOn)),
|
||||
}
|
||||
}
|
||||
|
||||
depErrors := gperr.NewBuilder()
|
||||
for i, dep := range cfg.DependsOn {
|
||||
depSegments := strings.Split(dep, ":")
|
||||
dep = depSegments[0]
|
||||
if dep == "" { // empty dependency (likely stopped container), skip; it will be removed by dedupDependencies()
|
||||
continue
|
||||
}
|
||||
cfg.DependsOn[i] = dep
|
||||
waitHealthy := false
|
||||
if len(depSegments) > 1 { // likely from `com.docker.compose.depends_on` label
|
||||
switch depSegments[1] {
|
||||
case "service_started":
|
||||
case "service_healthy":
|
||||
waitHealthy = true
|
||||
// case "service_completed_successfully":
|
||||
default:
|
||||
depErrors.Addf("dependency %q has unsupported condition %q", dep, depSegments[1])
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
cont := r.ContainerInfo()
|
||||
|
||||
var depRoute routes.Route
|
||||
var ok bool
|
||||
|
||||
// try to find the dependency in the same provider and the same docker compose project first
|
||||
if cont != nil {
|
||||
depRoute, ok = r.GetProvider().FindService(cont.DockerComposeProject(), dep)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
depRoute, ok = routes.Get(dep)
|
||||
if !ok {
|
||||
depErrors.Addf("dependency %q not found", dep)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if depRoute == r {
|
||||
depErrors.Addf("dependency %q cannot have itself as a dependency (same route)", dep)
|
||||
continue
|
||||
}
|
||||
|
||||
// wait for the dependency to be started
|
||||
<-depRoute.Started()
|
||||
|
||||
if waitHealthy && !depRoute.UseHealthCheck() {
|
||||
depErrors.Addf("dependency %q has service_healthy condition but has healthcheck disabled", dep)
|
||||
continue
|
||||
}
|
||||
|
||||
depCfg := depRoute.IdlewatcherConfig()
|
||||
if depCfg == nil {
|
||||
depCfg = new(idlewatcher.Config)
|
||||
depCfg.IdlewatcherConfig = cfg.IdlewatcherConfig
|
||||
depCfg.IdleTimeout = neverTick // disable auto sleep for dependencies
|
||||
} else if depCfg.IdleTimeout > 0 {
|
||||
depErrors.Addf("dependency %q has positive idle timeout %s", dep, depCfg.IdleTimeout)
|
||||
continue
|
||||
}
|
||||
|
||||
if depCfg.Docker == nil && depCfg.Proxmox == nil {
|
||||
depCont := depRoute.ContainerInfo()
|
||||
if depCont != nil {
|
||||
depCfg.Docker = &idlewatcher.DockerConfig{
|
||||
DockerHost: depCont.DockerHost,
|
||||
ContainerID: depCont.ContainerID,
|
||||
ContainerName: depCont.ContainerName,
|
||||
}
|
||||
depCfg.DependsOn = depCont.Dependencies()
|
||||
} else {
|
||||
depErrors.Addf("dependency %q has no idlewatcher config but is not a docker container", dep)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if depCfg.Key() == cfg.Key() {
|
||||
depErrors.Addf("dependency %q cannot have itself as a dependency (same container)", dep)
|
||||
continue
|
||||
}
|
||||
|
||||
depCfg.IdleTimeout = neverTick // disable auto sleep for dependencies
|
||||
|
||||
depWatcher, err := NewWatcher(parent, depRoute, depCfg)
|
||||
if err != nil {
|
||||
depErrors.Add(err)
|
||||
continue
|
||||
}
|
||||
w.dependsOn = append(w.dependsOn, &dependency{
|
||||
Watcher: depWatcher,
|
||||
waitHealthy: waitHealthy,
|
||||
})
|
||||
}
|
||||
|
||||
if w.provider != nil { // it's a reload, close the old provider
|
||||
w.provider.Close()
|
||||
}
|
||||
|
||||
if depErrors.HasError() {
|
||||
return nil, depErrors.Error()
|
||||
}
|
||||
|
||||
if !exists {
|
||||
watcherMapMu.Lock()
|
||||
defer watcherMapMu.Unlock()
|
||||
}
|
||||
|
||||
var p idlewatcher.Provider
|
||||
var providerType string
|
||||
var err error
|
||||
var kind string
|
||||
switch {
|
||||
case cfg.Docker != nil:
|
||||
p, err = provider.NewDockerProvider(cfg.Docker.DockerHost, cfg.Docker.ContainerID)
|
||||
providerType = "docker"
|
||||
kind = "docker"
|
||||
default:
|
||||
p, err = provider.NewProxmoxProvider(cfg.Proxmox.Node, cfg.Proxmox.VMID)
|
||||
providerType = "proxmox"
|
||||
kind = "proxmox"
|
||||
}
|
||||
w.l = log.With().
|
||||
Stringer("idle_timeout", cfg.IdleTimeout).
|
||||
Str("kind", kind).
|
||||
Str("container", cfg.ContainerName()).
|
||||
Logger()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w.provider = p
|
||||
w.l = log.With().
|
||||
Str("provider", providerType).
|
||||
Str("container", cfg.ContainerName()).
|
||||
Logger()
|
||||
|
||||
switch r := r.(type) {
|
||||
case routes.ReverseProxyRoute:
|
||||
@@ -131,18 +262,22 @@ func NewWatcher(parent task.Parent, r routes.Route) (*Watcher, error) {
|
||||
case routes.StreamRoute:
|
||||
w.stream = r
|
||||
default:
|
||||
return nil, gperr.Errorf("unexpected route type: %T", r)
|
||||
w.provider.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)
|
||||
if err != nil {
|
||||
w.provider.Close()
|
||||
return nil, gperr.Wrap(err, "failed to get container status")
|
||||
return nil, w.newWatcherError(err)
|
||||
}
|
||||
w.state.Store(&containerState{status: status})
|
||||
|
||||
switch p := w.provider.(type) {
|
||||
// when more providers are added, we need to add a new case here.
|
||||
switch p := w.provider.(type) { //nolint:gocritic
|
||||
case *provider.ProxmoxProvider:
|
||||
shutdownTimeout := max(time.Second, cfg.StopTimeout-idleWakerCheckTimeout)
|
||||
err = p.LXCSetShutdownTimeout(ctx, cfg.Proxmox.VMID, shutdownTimeout)
|
||||
@@ -151,30 +286,38 @@ func NewWatcher(parent task.Parent, r routes.Route) (*Watcher, error) {
|
||||
}
|
||||
}
|
||||
|
||||
w.state.Store(&containerState{status: status})
|
||||
if !exists {
|
||||
w.task = parent.Subtask("idlewatcher."+r.Name(), true)
|
||||
watcherMap[key] = w
|
||||
|
||||
w.task = parent.Subtask("idlewatcher."+r.Name(), true)
|
||||
go func() {
|
||||
cause := w.watchUntilDestroy()
|
||||
if errors.Is(cause, causeContainerDestroy) || errors.Is(cause, task.ErrProgramExiting) {
|
||||
watcherMapMu.Lock()
|
||||
delete(watcherMap, key)
|
||||
watcherMapMu.Unlock()
|
||||
w.l.Info().Msg("idlewatcher stopped")
|
||||
} else if !errors.Is(cause, causeReload) {
|
||||
gperr.LogError("idlewatcher stopped unexpectedly", cause, &w.l)
|
||||
}
|
||||
|
||||
watcherMapMu.Lock()
|
||||
defer watcherMapMu.Unlock()
|
||||
watcherMap[key] = w
|
||||
go func() {
|
||||
cause := w.watchUntilDestroy()
|
||||
if errors.Is(cause, causeContainerDestroy) || errors.Is(cause, task.ErrProgramExiting) {
|
||||
watcherMapMu.Lock()
|
||||
defer watcherMapMu.Unlock()
|
||||
delete(watcherMap, key)
|
||||
w.l.Info().Msg("idlewatcher stopped")
|
||||
} else if !errors.Is(cause, causeReload) {
|
||||
gperr.LogError("idlewatcher stopped unexpectedly", cause, &w.l)
|
||||
}
|
||||
w.idleTicker.Stop()
|
||||
w.provider.Close()
|
||||
w.task.Finish(cause)
|
||||
}()
|
||||
}
|
||||
|
||||
w.ticker.Stop()
|
||||
w.provider.Close()
|
||||
w.task.Finish(cause)
|
||||
}()
|
||||
hcCfg := w.hc.Config()
|
||||
hcCfg.BaseContext = func() context.Context {
|
||||
return w.task.Context()
|
||||
}
|
||||
hcCfg.Timeout = cfg.WakeTimeout
|
||||
|
||||
w.dedupDependencies()
|
||||
|
||||
w.l = w.l.With().Strs("deps", cfg.DependsOn).Logger()
|
||||
if exists {
|
||||
w.l.Info().Msg("idlewatcher reloaded")
|
||||
w.l.Debug().Msg("idlewatcher reloaded")
|
||||
} else {
|
||||
w.l.Info().Msg("idlewatcher started")
|
||||
}
|
||||
@@ -185,18 +328,75 @@ func (w *Watcher) Key() string {
|
||||
return w.cfg.Key()
|
||||
}
|
||||
|
||||
func (w *Watcher) Wake() error {
|
||||
return w.wakeIfStopped()
|
||||
// Wake wakes the container.
|
||||
//
|
||||
// It will cancel as soon as the either of the passed in context or the watcher is done.
|
||||
//
|
||||
// It uses singleflight to prevent multiple wake calls at the same time.
|
||||
//
|
||||
// It will wake the dependencies first, and then wake itself.
|
||||
// If the container is already running, it will do nothing.
|
||||
// If the container is not running, it will start it.
|
||||
// If the container is paused, it will unpause it.
|
||||
// If the container is stopped, it will do nothing.
|
||||
func (w *Watcher) Wake(ctx context.Context) error {
|
||||
// wake dependencies first.
|
||||
if err := w.wakeDependencies(ctx); err != nil {
|
||||
return w.newWatcherError(err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
})
|
||||
if err != nil {
|
||||
return w.newWatcherError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Watcher) wakeIfStopped() error {
|
||||
func (w *Watcher) wakeDependencies(ctx context.Context) error {
|
||||
if len(w.dependsOn) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
errs := errgroup.Group{}
|
||||
for _, dep := range w.dependsOn {
|
||||
errs.Go(func() error {
|
||||
if err := dep.Wake(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if dep.waitHealthy {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return w.newDepError("wait_healthy", dep, context.Cause(ctx))
|
||||
default:
|
||||
if h, err := dep.hc.CheckHealth(); err != nil {
|
||||
return err
|
||||
} else if h.Healthy {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(idleWakerCheckInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return errs.Wait()
|
||||
}
|
||||
|
||||
func (w *Watcher) wakeIfStopped(ctx context.Context) error {
|
||||
state := w.state.Load()
|
||||
if state.status == idlewatcher.ContainerStatusRunning {
|
||||
w.l.Debug().Msg("container is already running")
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(w.task.Context(), w.cfg.WakeTimeout)
|
||||
ctx, cancel := context.WithTimeout(ctx, w.cfg.WakeTimeout)
|
||||
defer cancel()
|
||||
switch state.status {
|
||||
case idlewatcher.ContainerStatusStopped:
|
||||
@@ -210,34 +410,66 @@ func (w *Watcher) wakeIfStopped() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) stopDependencies() error {
|
||||
if len(w.dependsOn) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
errs := errgroup.Group{}
|
||||
for _, dep := range w.dependsOn {
|
||||
errs.Go(dep.stopByMethod)
|
||||
}
|
||||
return errs.Wait()
|
||||
}
|
||||
|
||||
func (w *Watcher) stopByMethod() error {
|
||||
// no need singleflight here because it will only be called once every tick.
|
||||
|
||||
// if the container is not running, skip and stop dependencies.
|
||||
if !w.running() {
|
||||
if err := w.stopDependencies(); err != nil {
|
||||
return w.newWatcherError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg := w.cfg
|
||||
ctx, cancel := context.WithTimeout(w.task.Context(), cfg.StopTimeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.StopTimeout)
|
||||
defer cancel()
|
||||
|
||||
// stop itself first.
|
||||
var err error
|
||||
switch cfg.StopMethod {
|
||||
case idlewatcher.StopMethodPause:
|
||||
return w.provider.ContainerPause(ctx)
|
||||
err = w.provider.ContainerPause(ctx)
|
||||
case idlewatcher.StopMethodStop:
|
||||
return w.provider.ContainerStop(ctx, cfg.StopSignal, int(cfg.StopTimeout.Seconds()))
|
||||
err = w.provider.ContainerStop(ctx, cfg.StopSignal, int(cfg.StopTimeout.Seconds()))
|
||||
case idlewatcher.StopMethodKill:
|
||||
return w.provider.ContainerKill(ctx, cfg.StopSignal)
|
||||
err = w.provider.ContainerKill(ctx, cfg.StopSignal)
|
||||
default:
|
||||
return gperr.Errorf("unexpected stop method: %q", cfg.StopMethod)
|
||||
err = w.newWatcherError(gperr.Errorf("unexpected stop method: %q", cfg.StopMethod))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return w.newWatcherError(err)
|
||||
}
|
||||
|
||||
w.l.Info().Msg("container stopped")
|
||||
|
||||
// then stop dependencies.
|
||||
if err := w.stopDependencies(); err != nil {
|
||||
return w.newWatcherError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Watcher) resetIdleTimer() {
|
||||
w.ticker.Reset(w.cfg.IdleTimeout)
|
||||
w.idleTicker.Reset(w.cfg.IdleTimeout)
|
||||
w.lastReset.Store(time.Now())
|
||||
}
|
||||
|
||||
func (w *Watcher) expires() time.Time {
|
||||
if !w.running() {
|
||||
if !w.running() || w.cfg.IdleTimeout <= 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return w.lastReset.Load().Add(w.cfg.IdleTimeout)
|
||||
@@ -278,15 +510,15 @@ func (w *Watcher) watchUntilDestroy() (returnCause error) {
|
||||
w.l.Info().Msg("awaken")
|
||||
case e.Action.IsContainerStop(): // stop / kill / die
|
||||
w.setNapping(idlewatcher.ContainerStatusStopped)
|
||||
w.ticker.Stop()
|
||||
w.idleTicker.Stop()
|
||||
case e.Action.IsContainerPause(): // pause
|
||||
w.setNapping(idlewatcher.ContainerStatusPaused)
|
||||
w.ticker.Stop()
|
||||
w.idleTicker.Stop()
|
||||
default:
|
||||
w.l.Error().Stringer("action", e.Action).Msg("unexpected container action")
|
||||
w.l.Debug().Stringer("action", e.Action).Msg("unexpected container action")
|
||||
}
|
||||
case <-w.ticker.C:
|
||||
w.ticker.Stop()
|
||||
case <-w.idleTicker.C:
|
||||
w.idleTicker.Stop()
|
||||
if w.running() {
|
||||
err := w.stopByMethod()
|
||||
switch {
|
||||
@@ -298,16 +530,37 @@ func (w *Watcher) watchUntilDestroy() (returnCause error) {
|
||||
}
|
||||
w.l.Err(err).Msgf("container stop with method %q failed", w.cfg.StopMethod)
|
||||
default:
|
||||
w.l.Info().Str("reason", "idle timeout").Msg("container stopped")
|
||||
w.l.Info().Msg("idle timeout")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fmtErr(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
func (w *Watcher) dedupDependencies() {
|
||||
// remove from dependencies if the dependency is also a dependency of another dependency, or have duplicates.
|
||||
deps := w.dependencies()
|
||||
for _, dep := range w.dependsOn {
|
||||
depdeps := dep.dependencies()
|
||||
for depdep := range depdeps {
|
||||
delete(deps, depdep)
|
||||
}
|
||||
}
|
||||
return err.Error()
|
||||
newDepOn := make([]string, 0, len(deps))
|
||||
newDeps := make([]*dependency, 0, len(deps))
|
||||
for _, dep := range deps {
|
||||
newDepOn = append(newDepOn, dep.cfg.ContainerName())
|
||||
newDeps = append(newDeps, dep)
|
||||
}
|
||||
w.cfg.DependsOn = newDepOn
|
||||
w.dependsOn = newDeps
|
||||
}
|
||||
|
||||
func (w *Watcher) dependencies() map[string]*dependency {
|
||||
deps := make(map[string]*dependency)
|
||||
for _, dep := range w.dependsOn {
|
||||
deps[dep.Key()] = dep
|
||||
maps.Copy(deps, dep.dependencies())
|
||||
}
|
||||
return deps
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ func (m *memLogger) Write(p []byte) (n int, err error) {
|
||||
func (m *memLogger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := gpwebsocket.Initiate(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -34,11 +34,16 @@ type (
|
||||
City = maxmind.City
|
||||
)
|
||||
|
||||
var (
|
||||
const (
|
||||
updateInterval = 24 * time.Hour
|
||||
httpClient = &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
updateTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{
|
||||
Timeout: updateTimeout,
|
||||
}
|
||||
|
||||
var (
|
||||
ErrResponseNotOK = gperr.New("response not OK")
|
||||
ErrDownloadFailure = gperr.New("download failure")
|
||||
)
|
||||
|
||||
@@ -3,13 +3,13 @@ package gpwebsocket
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
)
|
||||
|
||||
func warnNoMatchDomains() {
|
||||
@@ -30,12 +30,17 @@ func SetWebsocketAllowedDomains(h http.Header, domains []string) {
|
||||
h[HeaderXGoDoxyWebsocketAllowedDomains] = domains
|
||||
}
|
||||
|
||||
var localAddresses = []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"}
|
||||
|
||||
const writeTimeout = time.Second * 10
|
||||
|
||||
// Initiate upgrades the HTTP connection to a WebSocket connection.
|
||||
// It returns a WebSocket connection and an error if the upgrade fails.
|
||||
// It logs and responds with an error if the upgrade fails.
|
||||
//
|
||||
// No further http.Error should be called after this function.
|
||||
func Initiate(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
|
||||
upgrader := websocket.Upgrader{}
|
||||
upgrader := websocket.Upgrader{
|
||||
Error: errHandler,
|
||||
}
|
||||
|
||||
allowedDomains := WebsocketAllowedDomains(r.Header)
|
||||
if len(allowedDomains) == 0 {
|
||||
@@ -49,9 +54,13 @@ func Initiate(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
|
||||
if err != nil {
|
||||
host = r.Host
|
||||
}
|
||||
if slices.Contains(localAddresses, host) {
|
||||
if host == "localhost" || host == agent.AgentHost {
|
||||
return true
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
if ip != nil {
|
||||
return ip.IsLoopback() || ip.IsPrivate()
|
||||
}
|
||||
for _, domain := range allowedDomains {
|
||||
if domain[0] == '.' {
|
||||
if host == domain[1:] || strings.HasSuffix(host, domain) {
|
||||
@@ -70,7 +79,6 @@ func Initiate(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
|
||||
func Periodic(w http.ResponseWriter, r *http.Request, interval time.Duration, do func(conn *websocket.Conn) error) {
|
||||
conn, err := Initiate(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
@@ -102,3 +110,15 @@ func WriteText(conn *websocket.Conn, msg string) error {
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
||||
return conn.WriteMessage(websocket.TextMessage, []byte(msg))
|
||||
}
|
||||
|
||||
func errHandler(w http.ResponseWriter, r *http.Request, status int, reason error) {
|
||||
log.Error().
|
||||
Str("remote", r.RemoteAddr).
|
||||
Str("host", r.Host).
|
||||
Str("url", r.URL.String()).
|
||||
Int("status", status).
|
||||
AnErr("reason", reason).
|
||||
Msg("websocket error")
|
||||
w.Header().Set("Sec-Websocket-Version", "13")
|
||||
http.Error(w, http.StatusText(status), status)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
type (
|
||||
cidrWhitelist struct {
|
||||
CIDRWhitelistOpts
|
||||
Tracer
|
||||
cachedAddr F.Map[string, bool] // cache for trusted IPs
|
||||
}
|
||||
CIDRWhitelistOpts struct {
|
||||
@@ -64,13 +63,11 @@ func (wl *cidrWhitelist) checkIP(w http.ResponseWriter, r *http.Request) bool {
|
||||
if cidr.Contains(ip) {
|
||||
wl.cachedAddr.Store(r.RemoteAddr, true)
|
||||
allow = true
|
||||
wl.AddTracef("client %s is allowed", ipStr).With("allowed CIDR", cidr)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allow {
|
||||
wl.cachedAddr.Store(r.RemoteAddr, false)
|
||||
wl.AddTracef("client %s is forbidden", ipStr).With("allowed CIDRs", wl.Allow)
|
||||
}
|
||||
}
|
||||
if !allow {
|
||||
|
||||
@@ -45,7 +45,7 @@ var CloudflareRealIP = NewMiddleware[cloudflareRealIP]()
|
||||
|
||||
// setup implements MiddlewareWithSetup.
|
||||
func (cri *cloudflareRealIP) setup() {
|
||||
cri.realIP.RealIPOpts = RealIPOpts{
|
||||
cri.realIP = realIP{
|
||||
Header: "CF-Connecting-IP",
|
||||
Recursive: true,
|
||||
}
|
||||
@@ -60,14 +60,6 @@ func (cri *cloudflareRealIP) before(w http.ResponseWriter, r *http.Request) bool
|
||||
return cri.realIP.before(w, r)
|
||||
}
|
||||
|
||||
func (cri *cloudflareRealIP) enableTrace() {
|
||||
cri.realIP.enableTrace()
|
||||
}
|
||||
|
||||
func (cri *cloudflareRealIP) getTracer() *Tracer {
|
||||
return cri.realIP.getTracer()
|
||||
}
|
||||
|
||||
func tryFetchCFCIDR() (cfCIDRs []*types.CIDR) {
|
||||
if time.Since(cfCIDRsLastUpdate.Load()) < cfCIDRsUpdateInterval {
|
||||
return
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
|
||||
@@ -52,10 +51,6 @@ type (
|
||||
MiddlewareFinalizerWithError interface {
|
||||
finalize() error
|
||||
}
|
||||
MiddlewareWithTracer interface {
|
||||
enableTrace()
|
||||
getTracer() *Tracer
|
||||
}
|
||||
)
|
||||
|
||||
const DefaultPriority = 10
|
||||
@@ -84,26 +79,6 @@ func NewMiddleware[ImplType any]() *Middleware {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Middleware) enableTrace() {
|
||||
if tracer, ok := m.impl.(MiddlewareWithTracer); ok {
|
||||
tracer.enableTrace()
|
||||
log.Trace().Msgf("middleware %s enabled trace", m.name)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Middleware) getTracer() *Tracer {
|
||||
if tracer, ok := m.impl.(MiddlewareWithTracer); ok {
|
||||
return tracer.getTracer()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Middleware) setParent(parent *Middleware) {
|
||||
if tracer := m.getTracer(); tracer != nil {
|
||||
tracer.SetParent(parent.getTracer())
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Middleware) setup() {
|
||||
if setup, ok := m.impl.(MiddlewareWithSetup); ok {
|
||||
setup.setup()
|
||||
|
||||
@@ -3,7 +3,6 @@ package middleware
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
)
|
||||
|
||||
@@ -24,14 +23,6 @@ func NewMiddlewareChain(name string, chain []*Middleware) *Middleware {
|
||||
if mr, ok := comp.impl.(ResponseModifier); ok {
|
||||
chainMid.modResps = append(chainMid.modResps, mr)
|
||||
}
|
||||
comp.setParent(m)
|
||||
}
|
||||
|
||||
if common.IsTrace {
|
||||
for _, child := range chain {
|
||||
child.enableTrace()
|
||||
}
|
||||
m.enableTrace()
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
type (
|
||||
modifyRequest struct {
|
||||
ModifyRequestOpts
|
||||
Tracer
|
||||
}
|
||||
// order: add_prefix -> set_headers -> add_headers -> hide_headers
|
||||
ModifyRequestOpts struct {
|
||||
@@ -31,11 +30,14 @@ func (mr *ModifyRequestOpts) finalize() {
|
||||
|
||||
// before implements RequestModifier.
|
||||
func (mr *modifyRequest) before(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
||||
mr.AddTraceRequest("before modify request", r)
|
||||
|
||||
mr.addPrefix(r, nil, r.URL.Path)
|
||||
mr.modifyHeaders(r, nil, r.Header)
|
||||
mr.AddTraceRequest("after modify request", r)
|
||||
if len(mr.AddPrefix) != 0 {
|
||||
mr.addPrefix(r, r.URL.Path)
|
||||
}
|
||||
if !mr.needVarSubstitution {
|
||||
mr.modifyHeaders(r, r.Header)
|
||||
} else {
|
||||
mr.modifyHeadersWithVarSubstitution(r, nil, r.Header)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -50,42 +52,40 @@ func (mr *ModifyRequestOpts) checkVarSubstitution() {
|
||||
}
|
||||
}
|
||||
|
||||
func (mr *ModifyRequestOpts) modifyHeaders(req *http.Request, resp *http.Response, headers http.Header) {
|
||||
if !mr.needVarSubstitution {
|
||||
for k, v := range mr.SetHeaders {
|
||||
if req != nil && strings.EqualFold(k, "host") {
|
||||
defer func() {
|
||||
req.Host = v
|
||||
}()
|
||||
}
|
||||
headers[k] = []string{v}
|
||||
}
|
||||
for k, v := range mr.AddHeaders {
|
||||
headers[k] = append(headers[k], v)
|
||||
}
|
||||
} else {
|
||||
for k, v := range mr.SetHeaders {
|
||||
if req != nil && strings.EqualFold(k, "host") {
|
||||
defer func() {
|
||||
req.Host = varReplace(req, resp, v)
|
||||
}()
|
||||
}
|
||||
headers[k] = []string{varReplace(req, resp, v)}
|
||||
}
|
||||
for k, v := range mr.AddHeaders {
|
||||
headers[k] = append(headers[k], varReplace(req, resp, v))
|
||||
func (mr *ModifyRequestOpts) modifyHeaders(req *http.Request, headers http.Header) {
|
||||
for k, v := range mr.SetHeaders {
|
||||
if req != nil && strings.EqualFold(k, "host") {
|
||||
defer func() {
|
||||
req.Host = v
|
||||
}()
|
||||
}
|
||||
headers[k] = []string{v}
|
||||
}
|
||||
for k, v := range mr.AddHeaders {
|
||||
headers[k] = append(headers[k], v)
|
||||
}
|
||||
|
||||
for _, k := range mr.HideHeaders {
|
||||
delete(headers, k)
|
||||
}
|
||||
}
|
||||
|
||||
func (mr *modifyRequest) addPrefix(r *http.Request, _ *http.Response, path string) {
|
||||
if len(mr.AddPrefix) == 0 {
|
||||
return
|
||||
func (mr *ModifyRequestOpts) modifyHeadersWithVarSubstitution(req *http.Request, resp *http.Response, headers http.Header) {
|
||||
for k, v := range mr.SetHeaders {
|
||||
if req != nil && strings.EqualFold(k, "host") {
|
||||
defer func() {
|
||||
req.Host = varReplace(req, resp, v)
|
||||
}()
|
||||
}
|
||||
headers[k] = []string{varReplace(req, resp, v)}
|
||||
}
|
||||
for k, v := range mr.AddHeaders {
|
||||
headers[k] = append(headers[k], varReplace(req, resp, v))
|
||||
}
|
||||
for _, k := range mr.HideHeaders {
|
||||
delete(headers, k)
|
||||
}
|
||||
}
|
||||
|
||||
func (mr *modifyRequest) addPrefix(r *http.Request, path string) {
|
||||
r.URL.Path = filepath.Join(mr.AddPrefix, path)
|
||||
}
|
||||
|
||||
@@ -6,15 +6,16 @@ import (
|
||||
|
||||
type modifyResponse struct {
|
||||
ModifyRequestOpts
|
||||
Tracer
|
||||
}
|
||||
|
||||
var ModifyResponse = NewMiddleware[modifyResponse]()
|
||||
|
||||
// modifyResponse implements ResponseModifier.
|
||||
func (mr *modifyResponse) modifyResponse(resp *http.Response) error {
|
||||
mr.AddTraceResponse("before modify response", resp)
|
||||
mr.modifyHeaders(resp.Request, resp, resp.Header)
|
||||
mr.AddTraceResponse("after modify response", resp)
|
||||
if !mr.needVarSubstitution {
|
||||
mr.modifyHeaders(resp.Request, resp.Header)
|
||||
} else {
|
||||
mr.modifyHeadersWithVarSubstitution(resp.Request, resp, resp.Header)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ type (
|
||||
requestMap = map[string]*rate.Limiter
|
||||
rateLimiter struct {
|
||||
RateLimiterOpts
|
||||
Tracer
|
||||
|
||||
requestMap requestMap
|
||||
mu sync.Mutex
|
||||
@@ -51,7 +50,6 @@ func (rl *rateLimiter) newLimiter() *rate.Limiter {
|
||||
func (rl *rateLimiter) limit(w http.ResponseWriter, r *http.Request) bool {
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
rl.AddTracef("unable to parse remote address %s", r.RemoteAddr)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -11,10 +11,7 @@ import (
|
||||
// https://nginx.org/en/docs/http/ngx_http_realip_module.html
|
||||
|
||||
type (
|
||||
realIP struct {
|
||||
RealIPOpts
|
||||
Tracer
|
||||
}
|
||||
realIP RealIPOpts
|
||||
RealIPOpts struct {
|
||||
// Header is the name of the header to use for the real client IP
|
||||
Header string `validate:"required"`
|
||||
@@ -42,7 +39,7 @@ var (
|
||||
|
||||
// setup implements MiddlewareWithSetup.
|
||||
func (ri *realIP) setup() {
|
||||
ri.RealIPOpts = realIPOptsDefault
|
||||
*ri = realIP(realIPOptsDefault)
|
||||
}
|
||||
|
||||
// before implements RequestModifier.
|
||||
@@ -77,7 +74,6 @@ func (ri *realIP) setRealIP(req *http.Request) {
|
||||
}
|
||||
}
|
||||
if !isTrusted {
|
||||
ri.AddTracef("client ip %s is not trusted", clientIP).With("allowed CIDRs", ri.From)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -90,7 +86,6 @@ func (ri *realIP) setRealIP(req *http.Request) {
|
||||
}
|
||||
|
||||
if len(realIPs) == 0 {
|
||||
ri.AddTracef("no real ip found in header %s", ri.Header).WithRequest(req)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -105,12 +100,10 @@ func (ri *realIP) setRealIP(req *http.Request) {
|
||||
}
|
||||
|
||||
if lastNonTrustedIP == "" {
|
||||
ri.AddTracef("no non-trusted ip found").With("allowed CIDRs", ri.From).With("ips", realIPs)
|
||||
return
|
||||
}
|
||||
|
||||
req.RemoteAddr = lastNonTrustedIP
|
||||
req.Header.Set(ri.Header, lastNonTrustedIP)
|
||||
req.Header.Set(httpheaders.HeaderXRealIP, lastNonTrustedIP)
|
||||
ri.AddTracef("set real ip %s", lastNonTrustedIP)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,6 @@ func TestSetRealIP(t *testing.T) {
|
||||
|
||||
result, err := newMiddlewareTest(mid, nil)
|
||||
ExpectNoError(t, err)
|
||||
t.Log(traces)
|
||||
ExpectEqual(t, result.ResponseStatus, http.StatusOK)
|
||||
ExpectEqual(t, strings.Split(result.RemoteAddr, ":")[0], testRealIP)
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
||||
)
|
||||
|
||||
type (
|
||||
Trace struct {
|
||||
Time string `json:"time,omitempty"`
|
||||
Caller string `json:"caller,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Message string `json:"msg"`
|
||||
ReqHeaders map[string]string `json:"req_headers,omitempty"`
|
||||
RespHeaders map[string]string `json:"resp_headers,omitempty"`
|
||||
RespStatus int `json:"resp_status,omitempty"`
|
||||
Additional map[string]any `json:"additional,omitempty"`
|
||||
}
|
||||
Traces []*Trace
|
||||
)
|
||||
|
||||
var (
|
||||
traces = make(Traces, 0)
|
||||
tracesMu sync.Mutex
|
||||
)
|
||||
|
||||
const MaxTraceNum = 100
|
||||
|
||||
func GetAllTrace() []*Trace {
|
||||
return traces
|
||||
}
|
||||
|
||||
func (tr *Trace) WithRequest(req *http.Request) *Trace {
|
||||
if tr == nil {
|
||||
return nil
|
||||
}
|
||||
tr.URL = req.RequestURI
|
||||
tr.ReqHeaders = httpheaders.HeaderToMap(req.Header)
|
||||
return tr
|
||||
}
|
||||
|
||||
func (tr *Trace) WithResponse(resp *http.Response) *Trace {
|
||||
if tr == nil {
|
||||
return nil
|
||||
}
|
||||
tr.URL = resp.Request.RequestURI
|
||||
tr.ReqHeaders = httpheaders.HeaderToMap(resp.Request.Header)
|
||||
tr.RespHeaders = httpheaders.HeaderToMap(resp.Header)
|
||||
tr.RespStatus = resp.StatusCode
|
||||
return tr
|
||||
}
|
||||
|
||||
func (tr *Trace) With(what string, additional any) *Trace {
|
||||
if tr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if tr.Additional == nil {
|
||||
tr.Additional = map[string]any{}
|
||||
}
|
||||
tr.Additional[what] = additional
|
||||
return tr
|
||||
}
|
||||
|
||||
func (tr *Trace) WithError(err error) *Trace {
|
||||
if tr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if tr.Additional == nil {
|
||||
tr.Additional = map[string]any{}
|
||||
}
|
||||
tr.Additional["error"] = err.Error()
|
||||
return tr
|
||||
}
|
||||
|
||||
func addTrace(t *Trace) *Trace {
|
||||
tracesMu.Lock()
|
||||
defer tracesMu.Unlock()
|
||||
if len(traces) > MaxTraceNum {
|
||||
traces = traces[1:]
|
||||
}
|
||||
traces = append(traces, t)
|
||||
return t
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
type Tracer struct {
|
||||
name string
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func _() {
|
||||
var _ MiddlewareWithTracer = &Tracer{}
|
||||
}
|
||||
|
||||
func (t *Tracer) enableTrace() {
|
||||
t.enabled = true
|
||||
}
|
||||
|
||||
func (t *Tracer) getTracer() *Tracer {
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Tracer) SetParent(parent *Tracer) {
|
||||
if parent == nil {
|
||||
return
|
||||
}
|
||||
t.name = parent.name + "." + t.name
|
||||
}
|
||||
|
||||
func (t *Tracer) addTrace(msg string) *Trace {
|
||||
return addTrace(&Trace{
|
||||
Time: strutils.FormatTime(time.Now()),
|
||||
Caller: t.name,
|
||||
Message: msg,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Tracer) AddTracef(msg string, args ...any) *Trace {
|
||||
if !t.enabled {
|
||||
return nil
|
||||
}
|
||||
return t.addTrace(fmt.Sprintf(msg, args...))
|
||||
}
|
||||
|
||||
func (t *Tracer) AddTraceRequest(msg string, req *http.Request) *Trace {
|
||||
if !t.enabled {
|
||||
return nil
|
||||
}
|
||||
return t.addTrace(msg).WithRequest(req)
|
||||
}
|
||||
|
||||
func (t *Tracer) AddTraceResponse(msg string, resp *http.Response) *Trace {
|
||||
if !t.enabled {
|
||||
return nil
|
||||
}
|
||||
return t.addTrace(msg).WithResponse(resp)
|
||||
}
|
||||
@@ -271,7 +271,7 @@ func (p *ReverseProxy) handler(rw http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Del("Forwarded")
|
||||
outreq.Header.Del("Forwarded")
|
||||
httpheaders.RemoveHopByHopHeaders(outreq.Header)
|
||||
|
||||
// Issue 21096: tell backend applications that care about trailer support
|
||||
@@ -312,7 +312,7 @@ func (p *ReverseProxy) handler(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
var reqScheme string
|
||||
if req.TLS != nil {
|
||||
if req.TLS != nil || req.Header.Get("X-Forwarded-Proto") == "https" {
|
||||
reqScheme = "https"
|
||||
} else {
|
||||
reqScheme = "http"
|
||||
|
||||
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
@@ -10,7 +11,7 @@ import (
|
||||
|
||||
func convertError(err error) error {
|
||||
switch {
|
||||
case err == nil, errors.Is(err, http.ErrServerClosed), errors.Is(err, context.Canceled):
|
||||
case err == nil, errors.Is(err, http.ErrServerClosed), errors.Is(err, context.Canceled), errors.Is(err, net.ErrClosed):
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
|
||||
@@ -3,6 +3,8 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -105,7 +107,7 @@ func (s *Server) Start(parent task.Parent) {
|
||||
}
|
||||
Start(subtask, h3, s.acl, &s.l)
|
||||
if s.http != nil {
|
||||
s.http.Handler = advertiseHTTP3(s.http.Handler, h3)
|
||||
s.http.Handler = advertiseHTTP3(s.http.Handler, h3)
|
||||
}
|
||||
// s.https is not nil (checked above)
|
||||
s.https.Handler = advertiseHTTP3(s.https.Handler, h3)
|
||||
@@ -115,7 +117,7 @@ func (s *Server) Start(parent task.Parent) {
|
||||
Start(subtask, s.https, s.acl, &s.l)
|
||||
}
|
||||
|
||||
func Start[Server httpServer](parent task.Parent, srv Server, acl *acl.Config, logger *zerolog.Logger) {
|
||||
func Start[Server httpServer](parent task.Parent, srv Server, acl *acl.Config, logger *zerolog.Logger) (port int) {
|
||||
if srv == nil {
|
||||
return
|
||||
}
|
||||
@@ -138,6 +140,7 @@ func Start[Server httpServer](parent task.Parent, srv Server, acl *acl.Config, l
|
||||
HandleError(logger, err, "failed to listen on port")
|
||||
return
|
||||
}
|
||||
port = l.Addr().(*net.TCPAddr).Port
|
||||
if srv.TLSConfig != nil {
|
||||
l = tls.NewListener(l, srv.TLSConfig)
|
||||
}
|
||||
@@ -145,32 +148,36 @@ func Start[Server httpServer](parent task.Parent, srv Server, acl *acl.Config, l
|
||||
l = acl.WrapTCP(l)
|
||||
}
|
||||
serveFunc = getServeFunc(l, srv.Serve)
|
||||
task.OnCancel("stop", func() {
|
||||
stop(srv, l, logger)
|
||||
})
|
||||
case *http3.Server:
|
||||
l, err := lc.ListenPacket(task.Context(), "udp", srv.Addr)
|
||||
if err != nil {
|
||||
HandleError(logger, err, "failed to listen on port")
|
||||
return
|
||||
}
|
||||
port = l.LocalAddr().(*net.UDPAddr).Port
|
||||
if acl != nil {
|
||||
l = acl.WrapUDP(l)
|
||||
}
|
||||
serveFunc = getServeFunc(l, srv.Serve)
|
||||
task.OnCancel("stop", func() {
|
||||
stop(srv, l, logger)
|
||||
})
|
||||
}
|
||||
task.OnCancel("stop", func() {
|
||||
stop(srv, logger)
|
||||
})
|
||||
logStarted(srv, logger)
|
||||
go func() {
|
||||
err := convertError(serveFunc())
|
||||
if err != nil {
|
||||
HandleError(logger, err, "failed to serve "+proto+" server")
|
||||
HandleError(logger, err, "failed to serve "+proto+" server")
|
||||
}
|
||||
task.Finish(err)
|
||||
}()
|
||||
return port
|
||||
}
|
||||
|
||||
func stop[Server httpServer](srv Server, logger *zerolog.Logger) {
|
||||
func stop[Server httpServer](srv Server, l io.Closer, logger *zerolog.Logger) {
|
||||
if srv == nil {
|
||||
return
|
||||
}
|
||||
@@ -180,7 +187,7 @@ func stop[Server httpServer](srv Server, logger *zerolog.Logger) {
|
||||
ctx, cancel := context.WithTimeout(task.RootContext(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := convertError(srv.Shutdown(ctx)); err != nil {
|
||||
if err := convertError(errors.Join(srv.Shutdown(ctx), l.Close())); err != nil {
|
||||
HandleError(logger, err, "failed to shutdown "+proto+" server")
|
||||
} else {
|
||||
logger.Info().Str("proto", proto).Str("addr", addr(srv)).Msg("server stopped")
|
||||
|
||||
@@ -86,7 +86,7 @@ func (f FieldsBody) Format(format *LogFormat) ([]byte, error) {
|
||||
case LogFormatRawJSON:
|
||||
return json.Marshal(f)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown format: %v", format)
|
||||
return f.Format(LogFormatMarkdown)
|
||||
}
|
||||
|
||||
func (l ListBody) Format(format *LogFormat) ([]byte, error) {
|
||||
@@ -104,7 +104,7 @@ func (l ListBody) Format(format *LogFormat) ([]byte, error) {
|
||||
case LogFormatRawJSON:
|
||||
return json.Marshal(l)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown format: %v", format)
|
||||
return l.Format(LogFormatMarkdown)
|
||||
}
|
||||
|
||||
func (m MessageBody) Format(format *LogFormat) ([]byte, error) {
|
||||
@@ -114,13 +114,13 @@ func (m MessageBody) Format(format *LogFormat) ([]byte, error) {
|
||||
case LogFormatRawJSON:
|
||||
return json.Marshal(m)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown format: %v", format)
|
||||
return m.Format(LogFormatMarkdown)
|
||||
}
|
||||
|
||||
func (e ErrorBody) Format(format *LogFormat) ([]byte, error) {
|
||||
switch format {
|
||||
case LogFormatRawJSON:
|
||||
return json.Marshal(e)
|
||||
return json.Marshal(e.Error)
|
||||
case LogFormatPlain:
|
||||
return gperr.Plain(e.Error), nil
|
||||
case LogFormatMarkdown:
|
||||
|
||||
26
internal/route/common.go
Normal file
26
internal/route/common.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
)
|
||||
|
||||
func checkExists(r routes.Route) gperr.Error {
|
||||
if r.UseLoadBalance() { // skip checking for load balanced routes
|
||||
return nil
|
||||
}
|
||||
var (
|
||||
existing routes.Route
|
||||
ok bool
|
||||
)
|
||||
switch r := r.(type) {
|
||||
case routes.HTTPRoute:
|
||||
existing, ok = routes.HTTP.Get(r.Key())
|
||||
case routes.StreamRoute:
|
||||
existing, ok = routes.Stream.Get(r.Key())
|
||||
}
|
||||
if ok {
|
||||
return gperr.Errorf("route already exists: from provider %s and %s", existing.ProviderName(), r.ProviderName())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -96,8 +96,16 @@ func (s *FileServer) Start(parent task.Parent) gperr.Error {
|
||||
}
|
||||
}
|
||||
|
||||
if s.ShouldExclude() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := checkExists(s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
routes.HTTP.Add(s)
|
||||
s.task.OnCancel("entrypoint_remove_route", func() {
|
||||
s.task.OnFinished("remove_route_from_http", func() {
|
||||
routes.HTTP.Del(s)
|
||||
})
|
||||
return nil
|
||||
|
||||
@@ -6,6 +6,8 @@ var (
|
||||
"mysql": 3306,
|
||||
"mariadb": 3306,
|
||||
"postgres": 5432,
|
||||
"pgvecto-rs": 5432,
|
||||
"pgvector": 5432,
|
||||
"rabbitmq": 5672,
|
||||
"redis": 6379,
|
||||
"memcached": 11211,
|
||||
|
||||
@@ -71,9 +71,6 @@ func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) {
|
||||
|
||||
for _, c := range containers {
|
||||
container := docker.FromDocker(&c, p.dockerHost)
|
||||
if container.IsExcluded {
|
||||
continue
|
||||
}
|
||||
|
||||
if container.IsHostNetworkMode {
|
||||
err := container.UpdatePorts()
|
||||
@@ -89,10 +86,15 @@ func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) {
|
||||
}
|
||||
for k, v := range newEntries {
|
||||
if conflict, ok := routes[k]; ok {
|
||||
errs.Add(gperr.Multiline().
|
||||
err := gperr.Multiline().
|
||||
Addf("route with alias %s already exists", k).
|
||||
Addf("container %s", container.ContainerName).
|
||||
Addf("conflicting container %s", conflict.Container.ContainerName))
|
||||
Addf("conflicting container %s", conflict.Container.ContainerName)
|
||||
if conflict.ShouldExclude() || v.ShouldExclude() {
|
||||
gperr.LogWarn("skipping conflicting route", err)
|
||||
} else {
|
||||
errs.Add(err)
|
||||
}
|
||||
} else {
|
||||
routes[k] = v
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package provider
|
||||
import (
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/route/provider/types"
|
||||
provider "github.com/yusing/go-proxy/internal/route/provider/types"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/watcher"
|
||||
eventsPkg "github.com/yusing/go-proxy/internal/watcher/events"
|
||||
@@ -23,7 +23,7 @@ func (p *Provider) newEventHandler() *EventHandler {
|
||||
}
|
||||
|
||||
func (handler *EventHandler) Handle(parent task.Parent, events []watcher.Event) {
|
||||
oldRoutes := handler.provider.routes
|
||||
oldRoutes := handler.provider.lockCloneRoutes()
|
||||
|
||||
isForceReload := false
|
||||
for _, event := range events {
|
||||
@@ -68,10 +68,10 @@ func (handler *EventHandler) matchAny(events []watcher.Event, route *route.Route
|
||||
|
||||
func (handler *EventHandler) match(event watcher.Event, route *route.Route) bool {
|
||||
switch handler.provider.GetType() {
|
||||
case types.ProviderTypeDocker, types.ProviderTypeAgent:
|
||||
case provider.ProviderTypeDocker, provider.ProviderTypeAgent:
|
||||
return route.Container.ContainerID == event.ActorID ||
|
||||
route.Container.ContainerName == event.ActorName
|
||||
case types.ProviderTypeFile:
|
||||
case provider.ProviderTypeFile:
|
||||
return true
|
||||
}
|
||||
// should never happen
|
||||
@@ -86,12 +86,11 @@ func (handler *EventHandler) Add(parent task.Parent, route *route.Route) {
|
||||
}
|
||||
|
||||
func (handler *EventHandler) Remove(route *route.Route) {
|
||||
route.Finish("route removed")
|
||||
delete(handler.provider.routes, route.Alias)
|
||||
route.FinishAndWait("route removed")
|
||||
}
|
||||
|
||||
func (handler *EventHandler) Update(parent task.Parent, oldRoute *route.Route, newRoute *route.Route) {
|
||||
oldRoute.Finish("route update")
|
||||
oldRoute.FinishAndWait("route update")
|
||||
err := handler.provider.startRoute(parent, newRoute)
|
||||
if err != nil {
|
||||
handler.errs.Add(err.Subject("update"))
|
||||
|
||||
@@ -3,14 +3,17 @@ package provider
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/route/provider/types"
|
||||
provider "github.com/yusing/go-proxy/internal/route/provider/types"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||
@@ -20,8 +23,9 @@ type (
|
||||
Provider struct {
|
||||
ProviderImpl
|
||||
|
||||
t types.ProviderType
|
||||
routes route.Routes
|
||||
t provider.Type
|
||||
routes route.Routes
|
||||
routesMu sync.RWMutex
|
||||
|
||||
watcher W.Watcher
|
||||
}
|
||||
@@ -41,7 +45,9 @@ const (
|
||||
|
||||
var ErrEmptyProviderName = errors.New("empty provider name")
|
||||
|
||||
func newProvider(t types.ProviderType) *Provider {
|
||||
var _ routes.Provider = (*Provider)(nil)
|
||||
|
||||
func newProvider(t provider.Type) *Provider {
|
||||
return &Provider{t: t}
|
||||
}
|
||||
|
||||
@@ -50,7 +56,7 @@ func NewFileProvider(filename string) (p *Provider, err error) {
|
||||
if name == "" {
|
||||
return nil, ErrEmptyProviderName
|
||||
}
|
||||
p = newProvider(types.ProviderTypeFile)
|
||||
p = newProvider(provider.ProviderTypeFile)
|
||||
p.ProviderImpl, err = FileProviderImpl(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -60,14 +66,14 @@ func NewFileProvider(filename string) (p *Provider, err error) {
|
||||
}
|
||||
|
||||
func NewDockerProvider(name string, dockerHost string) *Provider {
|
||||
p := newProvider(types.ProviderTypeDocker)
|
||||
p := newProvider(provider.ProviderTypeDocker)
|
||||
p.ProviderImpl = DockerProviderImpl(name, dockerHost)
|
||||
p.watcher = p.NewWatcher()
|
||||
return p
|
||||
}
|
||||
|
||||
func NewAgentProvider(cfg *agent.AgentConfig) *Provider {
|
||||
p := newProvider(types.ProviderTypeAgent)
|
||||
p := newProvider(provider.ProviderTypeAgent)
|
||||
agent := &AgentProvider{
|
||||
AgentConfig: cfg,
|
||||
docker: DockerProviderImpl(cfg.Name(), cfg.FakeDockerHost()),
|
||||
@@ -77,7 +83,7 @@ func NewAgentProvider(cfg *agent.AgentConfig) *Provider {
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Provider) GetType() types.ProviderType {
|
||||
func (p *Provider) GetType() provider.Type {
|
||||
return p.t
|
||||
}
|
||||
|
||||
@@ -86,25 +92,29 @@ func (p *Provider) MarshalText() ([]byte, error) {
|
||||
return []byte(p.String()), nil
|
||||
}
|
||||
|
||||
func (p *Provider) startRoute(parent task.Parent, r *route.Route) gperr.Error {
|
||||
err := r.Start(parent)
|
||||
if err != nil {
|
||||
delete(p.routes, r.Alias)
|
||||
return err.Subject(r.Alias)
|
||||
}
|
||||
p.routes[r.Alias] = r
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start implements task.TaskStarter.
|
||||
func (p *Provider) Start(parent task.Parent) gperr.Error {
|
||||
errs := gperr.NewBuilder("routes error")
|
||||
errs.EnableConcurrency()
|
||||
|
||||
t := parent.Subtask("provider."+p.String(), false)
|
||||
|
||||
errs := gperr.NewBuilder("routes error")
|
||||
// no need to lock here because we are not modifying the routes map.
|
||||
routeSlice := make([]*route.Route, 0, len(p.routes))
|
||||
for _, r := range p.routes {
|
||||
errs.Add(p.startRoute(t, r))
|
||||
routeSlice = append(routeSlice, r)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, r := range routeSlice {
|
||||
wg.Add(1)
|
||||
go func(r *route.Route) {
|
||||
defer wg.Done()
|
||||
errs.Add(p.startRoute(t, r))
|
||||
}(r)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
eventQueue := events.NewEventQueue(
|
||||
t.Subtask("event_queue", false),
|
||||
providerEventFlushInterval,
|
||||
@@ -126,15 +136,52 @@ func (p *Provider) Start(parent task.Parent) gperr.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) RangeRoutes(do func(string, *route.Route)) {
|
||||
for alias, r := range p.routes {
|
||||
do(alias, r)
|
||||
func (p *Provider) LoadRoutes() (err gperr.Error) {
|
||||
p.routes, err = p.loadRoutes()
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Provider) NumRoutes() int {
|
||||
return len(p.routes)
|
||||
}
|
||||
|
||||
func (p *Provider) IterRoutes(yield func(string, routes.Route) bool) {
|
||||
routes := p.lockCloneRoutes()
|
||||
for alias, r := range routes {
|
||||
if !yield(alias, r.Impl()) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) GetRoute(alias string) (r *route.Route, ok bool) {
|
||||
r, ok = p.routes[alias]
|
||||
return
|
||||
func (p *Provider) FindService(project, service string) (routes.Route, bool) {
|
||||
switch p.GetType() {
|
||||
case provider.ProviderTypeDocker, provider.ProviderTypeAgent:
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
if project == "" || service == "" {
|
||||
return nil, false
|
||||
}
|
||||
routes := p.lockCloneRoutes()
|
||||
for _, r := range routes {
|
||||
cont := r.ContainerInfo()
|
||||
if cont.DockerComposeProject() != project {
|
||||
continue
|
||||
}
|
||||
if cont.DockerComposeService() == service {
|
||||
return r.Impl(), true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (p *Provider) GetRoute(alias string) (routes.Route, bool) {
|
||||
r, ok := p.lockGetRoute(alias)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return r.Impl(), true
|
||||
}
|
||||
|
||||
func (p *Provider) loadRoutes() (routes route.Routes, err gperr.Error) {
|
||||
@@ -148,26 +195,51 @@ func (p *Provider) loadRoutes() (routes route.Routes, err gperr.Error) {
|
||||
// set alias and provider, then validate
|
||||
for alias, r := range routes {
|
||||
r.Alias = alias
|
||||
r.Provider = p.ShortName()
|
||||
r.SetProvider(p)
|
||||
if err := r.Validate(); err != nil {
|
||||
errs.Add(err.Subject(alias))
|
||||
delete(routes, alias)
|
||||
continue
|
||||
}
|
||||
if r.ShouldExclude() {
|
||||
delete(routes, alias)
|
||||
continue
|
||||
}
|
||||
r.FinalizeHomepageConfig()
|
||||
}
|
||||
return routes, errs.Error()
|
||||
}
|
||||
|
||||
func (p *Provider) LoadRoutes() (err gperr.Error) {
|
||||
p.routes, err = p.loadRoutes()
|
||||
return
|
||||
func (p *Provider) startRoute(parent task.Parent, r *route.Route) gperr.Error {
|
||||
err := r.Start(parent)
|
||||
if err != nil {
|
||||
p.lockDeleteRoute(r.Alias)
|
||||
return err.Subject(r.Alias)
|
||||
}
|
||||
p.lockAddRoute(r)
|
||||
r.Task().OnCancel("remove_route_from_provider", func() {
|
||||
p.lockDeleteRoute(r.Alias)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) NumRoutes() int {
|
||||
return len(p.routes)
|
||||
func (p *Provider) lockAddRoute(r *route.Route) {
|
||||
p.routesMu.Lock()
|
||||
defer p.routesMu.Unlock()
|
||||
p.routes[r.Alias] = r
|
||||
}
|
||||
|
||||
func (p *Provider) lockDeleteRoute(alias string) {
|
||||
p.routesMu.Lock()
|
||||
defer p.routesMu.Unlock()
|
||||
delete(p.routes, alias)
|
||||
}
|
||||
|
||||
func (p *Provider) lockGetRoute(alias string) (*route.Route, bool) {
|
||||
p.routesMu.RLock()
|
||||
defer p.routesMu.RUnlock()
|
||||
r, ok := p.routes[alias]
|
||||
return r, ok
|
||||
}
|
||||
|
||||
func (p *Provider) lockCloneRoutes() route.Routes {
|
||||
p.routesMu.RLock()
|
||||
defer p.routesMu.RUnlock()
|
||||
return maps.Clone(p.routes)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package provider
|
||||
|
||||
import (
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/route/provider/types"
|
||||
provider "github.com/yusing/go-proxy/internal/route/provider/types"
|
||||
route "github.com/yusing/go-proxy/internal/route/types"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
)
|
||||
@@ -17,10 +17,10 @@ type (
|
||||
NumUnknown uint16 `json:"unknown"`
|
||||
}
|
||||
ProviderStats struct {
|
||||
Total uint16 `json:"total"`
|
||||
RPs RouteStats `json:"reverse_proxies"`
|
||||
Streams RouteStats `json:"streams"`
|
||||
Type types.ProviderType `json:"type"`
|
||||
Total uint16 `json:"total"`
|
||||
RPs RouteStats `json:"reverse_proxies"`
|
||||
Streams RouteStats `json:"streams"`
|
||||
Type provider.Type `json:"type"`
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package types
|
||||
package provider
|
||||
|
||||
type ProviderType string
|
||||
type Type string
|
||||
|
||||
const (
|
||||
ProviderTypeDocker ProviderType = "docker"
|
||||
ProviderTypeFile ProviderType = "file"
|
||||
ProviderTypeAgent ProviderType = "agent"
|
||||
ProviderTypeDocker Type = "docker"
|
||||
ProviderTypeFile Type = "file"
|
||||
ProviderTypeAgent Type = "agent"
|
||||
)
|
||||
|
||||
@@ -50,11 +50,14 @@ func NewReverseProxyRoute(base *Route) (*ReveseProxyRoute, gperr.Error) {
|
||||
} else {
|
||||
trans = gphttp.NewTransport()
|
||||
if httpConfig.NoTLSVerify {
|
||||
trans.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
trans.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec
|
||||
}
|
||||
if httpConfig.ResponseHeaderTimeout > 0 {
|
||||
trans.ResponseHeaderTimeout = httpConfig.ResponseHeaderTimeout
|
||||
}
|
||||
if httpConfig.DisableCompression {
|
||||
trans.DisableCompression = true
|
||||
}
|
||||
}
|
||||
|
||||
service := base.Name()
|
||||
@@ -95,14 +98,11 @@ func (r *ReveseProxyRoute) ReverseProxy() *reverseproxy.ReverseProxy {
|
||||
|
||||
// Start implements task.TaskStarter.
|
||||
func (r *ReveseProxyRoute) Start(parent task.Parent) gperr.Error {
|
||||
if existing, ok := routes.HTTP.Get(r.Key()); ok && !r.UseLoadBalance() {
|
||||
return gperr.Errorf("route already exists: from provider %s and %s", existing.ProviderName(), r.ProviderName())
|
||||
}
|
||||
r.task = parent.Subtask("http."+r.Name(), false)
|
||||
|
||||
switch {
|
||||
case r.UseIdleWatcher():
|
||||
waker, err := idlewatcher.NewWatcher(parent, r)
|
||||
waker, err := idlewatcher.NewWatcher(parent, r, r.IdlewatcherConfig())
|
||||
if err != nil {
|
||||
r.task.Finish(err)
|
||||
return gperr.Wrap(err)
|
||||
@@ -136,11 +136,19 @@ func (r *ReveseProxyRoute) Start(parent task.Parent) gperr.Error {
|
||||
}
|
||||
}
|
||||
|
||||
if r.ShouldExclude() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := checkExists(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.UseLoadBalance() {
|
||||
r.addToLoadBalancer(parent)
|
||||
} else {
|
||||
routes.HTTP.Add(r)
|
||||
r.task.OnFinished("entrypoint_remove_route", func() {
|
||||
r.task.OnCancel("remove_route_from_http", func() {
|
||||
routes.HTTP.Del(r)
|
||||
})
|
||||
}
|
||||
@@ -184,16 +192,18 @@ func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent) {
|
||||
_ = lb.Start(parent) // always return nil
|
||||
linked = &ReveseProxyRoute{
|
||||
Route: &Route{
|
||||
Alias: cfg.Link,
|
||||
Alias: cfg.Link + "-loadbalancer",
|
||||
Homepage: r.Homepage,
|
||||
},
|
||||
HealthMon: lb,
|
||||
loadBalancer: lb,
|
||||
handler: lb,
|
||||
}
|
||||
routes.HTTP.Add(linked)
|
||||
r.task.OnFinished("entrypoint_remove_route", func() {
|
||||
routes.HTTP.Del(linked)
|
||||
routes.HTTP.AddKey(cfg.Link, linked)
|
||||
routes.All.AddKey(cfg.Link, linked)
|
||||
r.task.OnFinished("remove_loadbalancer_route", func() {
|
||||
routes.HTTP.DelKey(cfg.Link)
|
||||
routes.All.DelKey(cfg.Link)
|
||||
})
|
||||
}
|
||||
r.loadBalancer = lb
|
||||
|
||||
@@ -3,7 +3,9 @@ package route
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
@@ -57,15 +59,22 @@ type (
|
||||
Metadata struct {
|
||||
/* Docker only */
|
||||
Container *docker.Container `json:"container,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
|
||||
Provider string `json:"provider,omitempty"` // for backward compatibility
|
||||
|
||||
// private fields
|
||||
LisURL *net.URL `json:"lurl,omitempty"`
|
||||
ProxyURL *net.URL `json:"purl,omitempty"`
|
||||
|
||||
Excluded *bool `json:"excluded"`
|
||||
|
||||
impl routes.Route
|
||||
isValidated bool
|
||||
lastError gperr.Error
|
||||
provider routes.Provider
|
||||
|
||||
started chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
Routes map[string]*Route
|
||||
)
|
||||
@@ -84,6 +93,16 @@ func (r *Route) Validate() gperr.Error {
|
||||
r.isValidated = true
|
||||
r.Finalize()
|
||||
|
||||
r.started = make(chan struct{})
|
||||
// close the channel when the route is destroyed (if not closed yet).
|
||||
runtime.AddCleanup(r, func(ch chan struct{}) {
|
||||
select {
|
||||
case <-ch:
|
||||
default:
|
||||
close(ch)
|
||||
}
|
||||
}, r.started)
|
||||
|
||||
if r.Idlewatcher != nil && r.Idlewatcher.Proxmox != nil {
|
||||
node := r.Idlewatcher.Proxmox.Node
|
||||
vmid := r.Idlewatcher.Proxmox.VMID
|
||||
@@ -182,7 +201,9 @@ func (r *Route) Validate() gperr.Error {
|
||||
}
|
||||
r.ProxyURL = gperr.Collect(errs, net.ParseURL, fmt.Sprintf("%s://%s:%d", r.Scheme, r.Host, r.Port.Proxy))
|
||||
case route.SchemeTCP, route.SchemeUDP:
|
||||
r.LisURL = gperr.Collect(errs, net.ParseURL, fmt.Sprintf("%s://:%d", r.Scheme, r.Port.Listening))
|
||||
if !r.ShouldExclude() {
|
||||
r.LisURL = gperr.Collect(errs, net.ParseURL, fmt.Sprintf("%s://:%d", r.Scheme, r.Port.Listening))
|
||||
}
|
||||
r.ProxyURL = gperr.Collect(errs, net.ParseURL, fmt.Sprintf("%s://%s:%d", r.Scheme, r.Host, r.Port.Proxy))
|
||||
}
|
||||
|
||||
@@ -212,27 +233,73 @@ func (r *Route) Validate() gperr.Error {
|
||||
}
|
||||
|
||||
r.impl = impl
|
||||
excluded := r.ShouldExclude()
|
||||
r.Excluded = &excluded
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Route) Impl() routes.Route {
|
||||
return r.impl
|
||||
}
|
||||
|
||||
func (r *Route) Task() *task.Task {
|
||||
return r.impl.Task()
|
||||
}
|
||||
|
||||
func (r *Route) Start(parent task.Parent) (err gperr.Error) {
|
||||
r.once.Do(func() {
|
||||
err = r.start(parent)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Route) start(parent task.Parent) gperr.Error {
|
||||
if r.impl == nil { // should not happen
|
||||
return gperr.New("route not initialized")
|
||||
}
|
||||
defer close(r.started)
|
||||
|
||||
return r.impl.Start(parent)
|
||||
if err := r.impl.Start(parent); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if conflict, added := routes.All.AddIfNotExists(r.impl); !added {
|
||||
err := gperr.Errorf("route %s already exists: from %s and %s", r.Alias, r.ProviderName(), conflict.ProviderName())
|
||||
r.impl.Task().FinishAndWait(err)
|
||||
return err
|
||||
} else {
|
||||
// reference here because r.impl will be nil after Finish() is called.
|
||||
impl := r.impl
|
||||
impl.Task().OnCancel("remove_routes_from_all", func() {
|
||||
routes.All.Del(impl)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Route) Finish(reason any) {
|
||||
r.FinishAndWait(reason)
|
||||
}
|
||||
|
||||
func (r *Route) FinishAndWait(reason any) {
|
||||
if r.impl == nil {
|
||||
return
|
||||
}
|
||||
r.impl.Finish(reason)
|
||||
r.impl.Task().FinishAndWait(reason)
|
||||
r.impl = nil
|
||||
}
|
||||
|
||||
func (r *Route) Started() bool {
|
||||
return r.impl != nil
|
||||
func (r *Route) Started() <-chan struct{} {
|
||||
return r.started
|
||||
}
|
||||
|
||||
func (r *Route) GetProvider() routes.Provider {
|
||||
return r.provider
|
||||
}
|
||||
|
||||
func (r *Route) SetProvider(p routes.Provider) {
|
||||
r.provider = p
|
||||
r.Provider = p.ShortName()
|
||||
}
|
||||
|
||||
func (r *Route) ProviderName() string {
|
||||
@@ -260,6 +327,14 @@ func (r *Route) Name() string {
|
||||
|
||||
// Key implements pool.Object.
|
||||
func (r *Route) Key() string {
|
||||
if r.UseLoadBalance() || r.ShouldExclude() {
|
||||
// for excluded routes and load balanced routes, use provider:alias[-container_id[:8]] as key to make them unique.
|
||||
if r.Container != nil {
|
||||
return r.Provider + ":" + r.Alias + "-" + r.Container.ContainerID[:8]
|
||||
}
|
||||
return r.Provider + ":" + r.Alias
|
||||
}
|
||||
// we need to use alias as key for non-excluded routes because it's being used for subdomain / fqdn lookup for http routes.
|
||||
return r.Alias
|
||||
}
|
||||
|
||||
@@ -328,6 +403,12 @@ func (r *Route) IsZeroPort() bool {
|
||||
}
|
||||
|
||||
func (r *Route) ShouldExclude() bool {
|
||||
if r.lastError != nil {
|
||||
return true
|
||||
}
|
||||
if r.Excluded != nil {
|
||||
return *r.Excluded
|
||||
}
|
||||
if r.Container != nil {
|
||||
switch {
|
||||
case r.Container.IsExcluded:
|
||||
@@ -358,6 +439,16 @@ func (r *Route) UseIdleWatcher() bool {
|
||||
}
|
||||
|
||||
func (r *Route) UseHealthCheck() bool {
|
||||
if r.Container != nil {
|
||||
switch {
|
||||
case r.Container.Image.Name == "godoxy-agent":
|
||||
return false
|
||||
case !r.Container.Running && !r.UseIdleWatcher():
|
||||
return false
|
||||
case strings.HasPrefix(r.Container.ContainerName, "buildx_"):
|
||||
return false
|
||||
}
|
||||
}
|
||||
return !r.HealthCheck.Disable
|
||||
}
|
||||
|
||||
@@ -424,7 +515,7 @@ func (r *Route) Finalize() {
|
||||
if isDocker {
|
||||
if r.Scheme == "" {
|
||||
for _, p := range cont.PublicPortMapping {
|
||||
if p.PrivatePort == uint16(pp) && p.Type == "udp" {
|
||||
if int(p.PrivatePort) == pp && p.Type == "udp" {
|
||||
r.Scheme = "udp"
|
||||
break
|
||||
}
|
||||
@@ -482,6 +573,12 @@ func (r *Route) FinalizeHomepageConfig() {
|
||||
}
|
||||
r.Homepage = r.Homepage.GetOverride(r.Alias)
|
||||
|
||||
if r.ShouldExclude() && isDocker {
|
||||
r.Homepage.Show = false
|
||||
r.Homepage.Name = r.Container.ContainerName // still show container name in metrics page
|
||||
return
|
||||
}
|
||||
|
||||
hp := r.Homepage
|
||||
refs := r.References()
|
||||
for _, ref := range refs {
|
||||
|
||||
@@ -23,11 +23,12 @@ type (
|
||||
task.TaskFinisher
|
||||
pool.Object
|
||||
ProviderName() string
|
||||
GetProvider() Provider
|
||||
TargetURL() *net.URL
|
||||
HealthMonitor() health.HealthMonitor
|
||||
References() []string
|
||||
|
||||
Started() bool
|
||||
Started() <-chan struct{}
|
||||
|
||||
IdlewatcherConfig() *idlewatcher.Config
|
||||
HealthCheckConfig() *health.HealthCheckConfig
|
||||
@@ -57,4 +58,10 @@ type (
|
||||
Route
|
||||
net.Stream
|
||||
}
|
||||
Provider interface {
|
||||
GetRoute(alias string) (r Route, ok bool)
|
||||
IterRoutes(yield func(alias string, r Route) bool)
|
||||
FindService(project, service string) (r Route, ok bool)
|
||||
ShortName() string
|
||||
}
|
||||
)
|
||||
|
||||
@@ -7,15 +7,16 @@ import (
|
||||
var (
|
||||
HTTP = pool.New[HTTPRoute]("http_routes")
|
||||
Stream = pool.New[StreamRoute]("stream_routes")
|
||||
// All is a pool of all routes, including HTTP, Stream routes and also excluded routes.
|
||||
All = pool.New[Route]("all_routes")
|
||||
)
|
||||
|
||||
func init() {
|
||||
All.DisableLog()
|
||||
}
|
||||
|
||||
func Iter(yield func(r Route) bool) {
|
||||
for _, r := range HTTP.Iter {
|
||||
if !yield(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, r := range Stream.Iter {
|
||||
for _, r := range All.Iter {
|
||||
if !yield(r) {
|
||||
break
|
||||
}
|
||||
@@ -23,12 +24,7 @@ func Iter(yield func(r Route) bool) {
|
||||
}
|
||||
|
||||
func IterKV(yield func(alias string, r Route) bool) {
|
||||
for k, r := range HTTP.Iter {
|
||||
if !yield(k, r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for k, r := range Stream.Iter {
|
||||
for k, r := range All.Iter {
|
||||
if !yield(k, r) {
|
||||
break
|
||||
}
|
||||
@@ -36,12 +32,13 @@ func IterKV(yield func(alias string, r Route) bool) {
|
||||
}
|
||||
|
||||
func NumRoutes() int {
|
||||
return HTTP.Size() + Stream.Size()
|
||||
return All.Size()
|
||||
}
|
||||
|
||||
func Clear() {
|
||||
HTTP.Clear()
|
||||
Stream.Clear()
|
||||
All.Clear()
|
||||
}
|
||||
|
||||
func GetHTTPRouteOrExact(alias, host string) (HTTPRoute, bool) {
|
||||
@@ -54,9 +51,5 @@ func GetHTTPRouteOrExact(alias, host string) (HTTPRoute, bool) {
|
||||
}
|
||||
|
||||
func Get(alias string) (Route, bool) {
|
||||
r, ok := HTTP.Get(alias)
|
||||
if ok {
|
||||
return r, true
|
||||
}
|
||||
return Stream.Get(alias)
|
||||
return All.Get(alias)
|
||||
}
|
||||
|
||||
@@ -41,15 +41,12 @@ func NewStreamRoute(base *Route) (routes.Route, gperr.Error) {
|
||||
|
||||
// Start implements task.TaskStarter.
|
||||
func (r *StreamRoute) Start(parent task.Parent) gperr.Error {
|
||||
if existing, ok := routes.Stream.Get(r.Key()); ok {
|
||||
return gperr.Errorf("route already exists: from provider %s and %s", existing.ProviderName(), r.ProviderName())
|
||||
}
|
||||
r.task = parent.Subtask("stream."+r.Name(), true)
|
||||
r.task = parent.Subtask("stream."+r.Name(), !r.ShouldExclude())
|
||||
r.Stream = NewStream(r)
|
||||
|
||||
switch {
|
||||
case r.UseIdleWatcher():
|
||||
waker, err := idlewatcher.NewWatcher(parent, r)
|
||||
waker, err := idlewatcher.NewWatcher(parent, r, r.IdlewatcherConfig())
|
||||
if err != nil {
|
||||
r.task.Finish(err)
|
||||
return gperr.Wrap(err, "idlewatcher error")
|
||||
@@ -60,23 +57,32 @@ func (r *StreamRoute) Start(parent task.Parent) gperr.Error {
|
||||
r.HealthMon = monitor.NewMonitor(r)
|
||||
}
|
||||
|
||||
if err := r.Setup(); err != nil {
|
||||
r.task.Finish(err)
|
||||
return gperr.Wrap(err)
|
||||
if !r.ShouldExclude() {
|
||||
if err := r.Setup(); err != nil {
|
||||
r.task.Finish(err)
|
||||
return gperr.Wrap(err)
|
||||
}
|
||||
r.l.Info().Int("port", r.Port.Listening).Msg("listening")
|
||||
}
|
||||
|
||||
r.l.Info().Int("port", r.Port.Listening).Msg("listening")
|
||||
|
||||
if r.HealthMon != nil {
|
||||
if err := r.HealthMon.Start(r.task); err != nil {
|
||||
gperr.LogWarn("health monitor error", err, &r.l)
|
||||
}
|
||||
}
|
||||
|
||||
if r.ShouldExclude() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := checkExists(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go r.acceptConnections()
|
||||
|
||||
routes.Stream.Add(r)
|
||||
r.task.OnFinished("entrypoint_remove_route", func() {
|
||||
r.task.OnCancel("remove_route_from_stream", func() {
|
||||
routes.Stream.Del(r)
|
||||
})
|
||||
return nil
|
||||
|
||||
@@ -7,4 +7,5 @@ import (
|
||||
type HTTPConfig struct {
|
||||
NoTLSVerify bool `json:"no_tls_verify,omitempty"`
|
||||
ResponseHeaderTimeout time.Duration `json:"response_header_timeout,omitempty"`
|
||||
DisableCompression bool `json:"disable_compression,omitempty"`
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -198,7 +197,7 @@ func mapUnmarshalValidate(src SerializedObject, dst any, checkValidateTag bool)
|
||||
dstV.Set(reflect.Zero(dstT))
|
||||
return nil
|
||||
}
|
||||
return gperr.Errorf("deserialize: src is %w and dst is not settable\n%s", ErrNilValue, debug.Stack())
|
||||
return gperr.Errorf("deserialize: src is %w and dst is not settable", ErrNilValue)
|
||||
}
|
||||
|
||||
if dstT.Implements(mapUnmarshalerType) {
|
||||
|
||||
@@ -8,45 +8,59 @@ import (
|
||||
)
|
||||
|
||||
// debug only.
|
||||
func (t *Task) listStuckedCallbacks() []string {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
callbacks := make([]string, 0, len(t.callbacksOnFinish))
|
||||
for c := range t.callbacksOnFinish {
|
||||
callbacks = append(callbacks, c.about)
|
||||
func listStuckedCallbacks(t *Task) []string {
|
||||
callbacks := make([]string, 0)
|
||||
if t.onFinish != nil {
|
||||
for c := range t.onFinish.Range {
|
||||
callbacks = append(callbacks, c.about)
|
||||
}
|
||||
}
|
||||
if t.onCancel != nil {
|
||||
for c := range t.onCancel.Range {
|
||||
callbacks = append(callbacks, c.about)
|
||||
}
|
||||
}
|
||||
if t.children != nil {
|
||||
for c := range t.children.Range {
|
||||
callbacks = append(callbacks, listStuckedCallbacks(c)...)
|
||||
}
|
||||
}
|
||||
return callbacks
|
||||
}
|
||||
|
||||
// debug only.
|
||||
func (t *Task) listStuckedChildren() []string {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
children := make([]string, 0, len(t.children))
|
||||
for c := range t.children {
|
||||
if c.isFinished() {
|
||||
continue
|
||||
}
|
||||
children = append(children, c.String())
|
||||
if len(c.children) > 0 {
|
||||
children = append(children, c.listStuckedChildren()...)
|
||||
func listStuckedChildren(t *Task) []string {
|
||||
if t.children != nil {
|
||||
children := make([]string, 0)
|
||||
for c := range t.children.Range {
|
||||
children = append(children, c.String())
|
||||
children = append(children, listStuckedCallbacks(c)...)
|
||||
}
|
||||
return children
|
||||
}
|
||||
return children
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Task) reportStucked() {
|
||||
callbacks := t.listStuckedCallbacks()
|
||||
children := t.listStuckedChildren()
|
||||
callbacks := listStuckedCallbacks(t)
|
||||
children := listStuckedChildren(t)
|
||||
if len(callbacks) == 0 && len(children) == 0 {
|
||||
return
|
||||
}
|
||||
fmtOutput := gperr.NewBuilder(fmt.Sprintf("%s stucked callbacks: %d, stucked children: %d", t.String(), len(callbacks), len(children)))
|
||||
if len(callbacks) > 0 {
|
||||
fmtOutput.Add(gperr.New("callbacks").With(gperr.Multiline().AddLinesString(callbacks...)))
|
||||
callbackBuilder := gperr.NewBuilder("callbacks")
|
||||
for _, c := range callbacks {
|
||||
callbackBuilder.Adds(c)
|
||||
}
|
||||
fmtOutput.Add(callbackBuilder.Error())
|
||||
}
|
||||
if len(children) > 0 {
|
||||
fmtOutput.Add(gperr.New("children").With(gperr.Multiline().AddLinesString(children...)))
|
||||
childrenBuilder := gperr.NewBuilder("children")
|
||||
for _, c := range children {
|
||||
childrenBuilder.Adds(c)
|
||||
}
|
||||
fmtOutput.Add(childrenBuilder.Error())
|
||||
}
|
||||
log.Warn().Msg(fmtOutput.String())
|
||||
}
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var (
|
||||
taskPool = make(chan *Task, 100)
|
||||
|
||||
voidTask = &Task{ctx: context.Background()}
|
||||
root = newRoot()
|
||||
|
||||
cancelCtx context.Context
|
||||
)
|
||||
|
||||
func init() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
cancelCtx = ctx //nolint:fatcontext
|
||||
|
||||
voidTask.parent = root
|
||||
}
|
||||
|
||||
func testCleanup() {
|
||||
root = newRoot()
|
||||
}
|
||||
|
||||
func newRoot() *Task {
|
||||
return newTask("root", voidTask, true)
|
||||
}
|
||||
|
||||
func noCancel(error) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func newTask(name string, parent *Task, needFinish bool) *Task {
|
||||
var t *Task
|
||||
select {
|
||||
case t = <-taskPool:
|
||||
t.finished.Store(false)
|
||||
default:
|
||||
t = &Task{}
|
||||
}
|
||||
t.name = name
|
||||
t.parent = parent
|
||||
if needFinish {
|
||||
t.ctx, t.cancel = context.WithCancelCause(parent.ctx)
|
||||
} else {
|
||||
t.ctx, t.cancel = parent.ctx, noCancel
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func (t *Task) needFinish() bool {
|
||||
return t.ctx != t.parent.ctx
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func (t *Task) isCanceled() bool {
|
||||
return t.cancel == nil
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func putTask(t *Task) {
|
||||
select {
|
||||
case taskPool <- t:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func (t *Task) addCallback(about string, fn func(), waitSubTasks bool) {
|
||||
if !t.needFinish() {
|
||||
if waitSubTasks {
|
||||
t.parent.addCallback(about, func() {
|
||||
if !t.waitFinish(taskTimeout) {
|
||||
t.reportStucked()
|
||||
}
|
||||
fn()
|
||||
}, false)
|
||||
} else {
|
||||
t.parent.addCallback(about, fn, false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !waitSubTasks {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.callbacksOnCancel == nil {
|
||||
t.callbacksOnCancel = make(callbacksSet)
|
||||
go func() {
|
||||
<-t.ctx.Done()
|
||||
for c := range t.callbacksOnCancel {
|
||||
go func() {
|
||||
invokeWithRecover(c)
|
||||
t.mu.Lock()
|
||||
delete(t.callbacksOnCancel, c)
|
||||
t.mu.Unlock()
|
||||
}()
|
||||
}
|
||||
}()
|
||||
}
|
||||
t.callbacksOnCancel[&Callback{fn: fn, about: about}] = struct{}{}
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.isCanceled() {
|
||||
log.Panic().
|
||||
Str("task", t.String()).
|
||||
Str("callback", about).
|
||||
Msg("callback added to canceled task")
|
||||
return
|
||||
}
|
||||
|
||||
if t.callbacksOnFinish == nil {
|
||||
t.callbacksOnFinish = make(callbacksSet)
|
||||
}
|
||||
t.callbacksOnFinish[&Callback{
|
||||
fn: fn,
|
||||
about: about,
|
||||
}] = struct{}{}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func (t *Task) addChild(child *Task) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.isCanceled() {
|
||||
log.Panic().
|
||||
Str("task", t.String()).
|
||||
Str("child", child.Name()).
|
||||
Msg("child added to canceled task")
|
||||
return
|
||||
}
|
||||
|
||||
if t.children == nil {
|
||||
t.children = make(childrenSet)
|
||||
}
|
||||
t.children[child] = struct{}{}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func (t *Task) removeChild(child *Task) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
delete(t.children, child)
|
||||
}
|
||||
|
||||
func (t *Task) runOnFinishCallbacks() {
|
||||
if len(t.callbacksOnFinish) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for c := range t.callbacksOnFinish {
|
||||
go func() {
|
||||
invokeWithRecover(c)
|
||||
t.mu.Lock()
|
||||
delete(t.callbacksOnFinish, c)
|
||||
t.mu.Unlock()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Task) waitFinish(timeout time.Duration) bool {
|
||||
// return directly if already finished
|
||||
if t.isFinished() {
|
||||
return true
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
children, callbacksOnCancel, callbacksOnFinish := t.children, t.callbacksOnCancel, t.callbacksOnFinish
|
||||
t.mu.Unlock()
|
||||
|
||||
ok := true
|
||||
if len(children) != 0 {
|
||||
ok = waitEmpty(children, timeout)
|
||||
}
|
||||
|
||||
if len(callbacksOnCancel) != 0 {
|
||||
ok = ok && waitEmpty(callbacksOnCancel, timeout)
|
||||
}
|
||||
|
||||
if len(callbacksOnFinish) != 0 {
|
||||
ok = ok && waitEmpty(callbacksOnFinish, timeout)
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func waitEmpty[T comparable](set map[T]struct{}, timeout time.Duration) bool {
|
||||
if len(set) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
timer := time.NewTimer(timeout)
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
if len(set) == 0 {
|
||||
return true
|
||||
}
|
||||
select {
|
||||
case <-timer.C:
|
||||
return false
|
||||
default:
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func fmtCause(cause any) error {
|
||||
switch cause := cause.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case error:
|
||||
return cause
|
||||
case string:
|
||||
return errors.New(cause)
|
||||
default:
|
||||
return fmt.Errorf("%v", cause)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,10 @@
|
||||
// This file has the abstract logic of the task system.
|
||||
//
|
||||
// The implementation of the task system is in the impl.go file.
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
)
|
||||
|
||||
@@ -35,21 +30,23 @@ type (
|
||||
//
|
||||
// Use Task.Finish to stop all subtasks of the Task.
|
||||
Task struct {
|
||||
name string
|
||||
parent *Task
|
||||
name string
|
||||
ctx context.Context
|
||||
cancel context.CancelCauseFunc
|
||||
done chan struct{}
|
||||
finishCalled bool
|
||||
onCancel *withWg[*Callback]
|
||||
onFinish *withWg[*Callback]
|
||||
children *withWg[*Task]
|
||||
|
||||
parent *Task
|
||||
children childrenSet
|
||||
callbacksOnFinish callbacksSet
|
||||
callbacksOnCancel callbacksSet
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelCauseFunc
|
||||
|
||||
finished atomic.Bool
|
||||
mu sync.Mutex
|
||||
mu sync.Mutex
|
||||
}
|
||||
Parent interface {
|
||||
Context() context.Context
|
||||
// Subtask returns a new subtask with the given name, derived from the parent's context.
|
||||
//
|
||||
// This should not be called after Finish is called on the task or its parent task.
|
||||
Subtask(name string, needFinish bool) *Task
|
||||
Name() string
|
||||
Finish(reason any)
|
||||
@@ -57,124 +54,193 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
childrenSet = map[*Task]struct{}
|
||||
callbacksSet = map[*Callback]struct{}
|
||||
)
|
||||
|
||||
const taskTimeout = 3 * time.Second
|
||||
|
||||
func (t *Task) Context() context.Context {
|
||||
return t.ctx
|
||||
}
|
||||
|
||||
// FinishCause returns the reason / error that caused the task to be finished.
|
||||
func (t *Task) FinishCause() error {
|
||||
return context.Cause(t.ctx)
|
||||
}
|
||||
|
||||
// OnFinished calls fn when the task is canceled and all subtasks are finished.
|
||||
//
|
||||
// It should not be called after Finish is called.
|
||||
func (t *Task) OnFinished(about string, fn func()) {
|
||||
t.addCallback(about, fn, true)
|
||||
}
|
||||
|
||||
// OnCancel calls fn when the task is canceled.
|
||||
//
|
||||
// It should not be called after Finish is called.
|
||||
func (t *Task) OnCancel(about string, fn func()) {
|
||||
t.addCallback(about, fn, false)
|
||||
}
|
||||
|
||||
// Finish cancel all subtasks and wait for them to finish,
|
||||
// then marks the task as finished, with the given reason (if any).
|
||||
func (t *Task) Finish(reason any) {
|
||||
t.mu.Lock()
|
||||
if t.isCanceled() {
|
||||
t.mu.Unlock()
|
||||
return
|
||||
}
|
||||
t.cancel(fmtCause(reason))
|
||||
t.ctx, t.cancel = cancelCtx, nil
|
||||
|
||||
t.mu.Unlock()
|
||||
|
||||
t.finishAndWait()
|
||||
t.finished.Store(true)
|
||||
}
|
||||
|
||||
func (t *Task) finishAndWait() {
|
||||
ok := true
|
||||
|
||||
if !waitEmpty(t.children, taskTimeout) {
|
||||
t.reportStucked()
|
||||
ok = false
|
||||
}
|
||||
t.runOnFinishCallbacks()
|
||||
|
||||
if !t.waitFinish(taskTimeout) {
|
||||
t.reportStucked()
|
||||
ok = false
|
||||
}
|
||||
// clear anyway
|
||||
clear(t.children)
|
||||
clear(t.callbacksOnFinish)
|
||||
|
||||
if t != root && t.needFinish() {
|
||||
t.parent.removeChild(t)
|
||||
}
|
||||
logFinished(t)
|
||||
|
||||
if ok {
|
||||
putTask(t)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Task) isFinished() bool {
|
||||
return t.finished.Load()
|
||||
}
|
||||
|
||||
// Subtask returns a new subtask with the given name, derived from the parent's context.
|
||||
//
|
||||
// This should not be called after Finish is called on the task or its parent task.
|
||||
func (t *Task) Subtask(name string, needFinish bool) *Task {
|
||||
panicIfFinished(t, "Subtask is called")
|
||||
|
||||
child := newTask(name, t, needFinish)
|
||||
|
||||
if needFinish {
|
||||
t.addChild(child)
|
||||
}
|
||||
|
||||
logStarted(child)
|
||||
return child
|
||||
}
|
||||
|
||||
// Name returns the name of the task without parent names.
|
||||
func (t *Task) Name() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
// String returns the full name of the task.
|
||||
func (t *Task) String() string {
|
||||
if t.parent != root {
|
||||
return t.parent.String() + "." + t.name
|
||||
}
|
||||
return t.name
|
||||
return t.fullName()
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler.
|
||||
func (t *Task) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
return []byte(t.fullName()), nil
|
||||
}
|
||||
|
||||
func invokeWithRecover(cb *Callback) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Err(fmtCause(err)).Str("callback", cb.about).Msg("panic")
|
||||
panicWithDebugStack()
|
||||
}
|
||||
}()
|
||||
cb.fn()
|
||||
// Finish marks the task as finished, with the given reason (if any).
|
||||
func (t *Task) Finish(reason any) {
|
||||
t.finish(reason, false)
|
||||
}
|
||||
|
||||
// FinishCause returns the reason / error that caused the task to be finished.
|
||||
func (t *Task) FinishCause() error {
|
||||
return context.Cause(t.ctx)
|
||||
}
|
||||
|
||||
// FinishAndWait cancel all subtasks and wait for them to finish,
|
||||
// then marks the task as finished, with the given reason (if any).
|
||||
func (t *Task) FinishAndWait(reason any) {
|
||||
t.finish(reason, true)
|
||||
}
|
||||
|
||||
// OnFinished calls fn when the task is canceled and all subtasks are finished.
|
||||
//
|
||||
// It should not be called after Finish is called.
|
||||
func (t *Task) OnFinished(about string, fn func()) {
|
||||
if !t.needFinish() {
|
||||
t.OnCancel(about, fn)
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
if t.onFinish == nil {
|
||||
t.onFinish = newWithWg[*Callback]()
|
||||
t.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
<-t.ctx.Done()
|
||||
<-t.done
|
||||
for cb := range t.onFinish.Range {
|
||||
go func(cb *Callback) {
|
||||
invokeWithRecover(cb)
|
||||
t.onFinish.Delete(cb)
|
||||
}(cb)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
t.onFinish.Add(&Callback{fn: fn, about: about})
|
||||
}
|
||||
|
||||
// OnCancel calls fn when the task is canceled.
|
||||
//
|
||||
// It should not be called after Finish is called.
|
||||
func (t *Task) OnCancel(about string, fn func()) {
|
||||
t.mu.Lock()
|
||||
if t.onCancel == nil {
|
||||
t.onCancel = newWithWg[*Callback]()
|
||||
t.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
<-t.ctx.Done()
|
||||
for cb := range t.onCancel.Range {
|
||||
go func(cb *Callback) {
|
||||
invokeWithRecover(cb)
|
||||
t.onCancel.Delete(cb)
|
||||
}(cb)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
t.onCancel.Add(&Callback{fn: fn, about: about})
|
||||
}
|
||||
|
||||
// Subtask returns a new subtask with the given name, derived from the parent's context.
|
||||
//
|
||||
// This should not be called after Finish is called on the task or its parent task.
|
||||
func (t *Task) Subtask(name string, needFinish bool) *Task {
|
||||
t.mu.Lock()
|
||||
if t.children == nil {
|
||||
t.children = newWithWg[*Task]()
|
||||
t.mu.Unlock()
|
||||
} else {
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
child := &Task{
|
||||
name: name,
|
||||
parent: t,
|
||||
}
|
||||
|
||||
t.children.Add(child)
|
||||
|
||||
child.ctx, child.cancel = context.WithCancelCause(t.ctx)
|
||||
|
||||
if needFinish {
|
||||
child.done = make(chan struct{})
|
||||
} else {
|
||||
child.done = closedCh
|
||||
go func() {
|
||||
<-child.ctx.Done()
|
||||
child.Finish(t.FinishCause())
|
||||
}()
|
||||
}
|
||||
|
||||
logStarted(child)
|
||||
return child
|
||||
}
|
||||
|
||||
func (t *Task) finish(reason any, wait bool) {
|
||||
t.mu.Lock()
|
||||
if t.finishCalled {
|
||||
t.mu.Unlock()
|
||||
// wait but not report stucked (again)
|
||||
t.waitFinish(taskTimeout)
|
||||
return
|
||||
}
|
||||
|
||||
t.finishCalled = true
|
||||
t.mu.Unlock()
|
||||
|
||||
if t.needFinish() {
|
||||
close(t.done)
|
||||
}
|
||||
|
||||
t.cancel(fmtCause(reason))
|
||||
if wait && !t.waitFinish(taskTimeout) {
|
||||
t.reportStucked()
|
||||
}
|
||||
if t != root {
|
||||
t.parent.children.Delete(t)
|
||||
}
|
||||
logFinished(t)
|
||||
}
|
||||
|
||||
func (t *Task) waitFinish(timeout time.Duration) bool {
|
||||
if t.children == nil && t.onCancel == nil && t.onFinish == nil {
|
||||
return true
|
||||
}
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
if t.children != nil {
|
||||
t.children.Wait()
|
||||
}
|
||||
if t.onCancel != nil {
|
||||
t.onCancel.Wait()
|
||||
}
|
||||
if t.onFinish != nil {
|
||||
t.onFinish.Wait()
|
||||
}
|
||||
<-t.done
|
||||
close(done)
|
||||
}()
|
||||
timeoutCh := time.After(timeout)
|
||||
select {
|
||||
case <-done:
|
||||
return true
|
||||
case <-timeoutCh:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Task) fullName() string {
|
||||
if t.parent == root {
|
||||
return t.name
|
||||
}
|
||||
return t.parent.fullName() + "." + t.name
|
||||
}
|
||||
|
||||
func (t *Task) needFinish() bool {
|
||||
return t.done != closedCh
|
||||
}
|
||||
|
||||
@@ -12,12 +12,6 @@ func panicWithDebugStack() {
|
||||
panic(string(debug.Stack()))
|
||||
}
|
||||
|
||||
func panicIfFinished(t *Task, reason string) {
|
||||
if t.isFinished() {
|
||||
log.Panic().Msg("task " + t.String() + " is finished but " + reason)
|
||||
}
|
||||
}
|
||||
|
||||
func logStarted(t *Task) {
|
||||
log.Info().Msg("task " + t.String() + " started")
|
||||
}
|
||||
|
||||
@@ -6,10 +6,6 @@ func panicWithDebugStack() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func panicIfFinished(t *Task, reason string) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func logStarted(t *Task) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
expect "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func testTask() *Task {
|
||||
@@ -35,7 +35,7 @@ func TestChildTaskCancellation(t *testing.T) {
|
||||
|
||||
select {
|
||||
case <-child.Context().Done():
|
||||
ExpectError(t, context.Canceled, child.Context().Err())
|
||||
expect.ErrorIs(t, context.Canceled, child.Context().Err())
|
||||
default:
|
||||
t.Fatal("subTask context was not canceled as expected")
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func TestTaskStuck(t *testing.T) {
|
||||
})
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
task.Finish(nil)
|
||||
task.FinishAndWait(nil)
|
||||
close(done)
|
||||
}()
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
@@ -80,10 +80,10 @@ func TestTaskOnCancelOnFinished(t *testing.T) {
|
||||
shouldTrueOnFinish = true
|
||||
})
|
||||
|
||||
ExpectFalse(t, shouldTrueOnFinish)
|
||||
task.Finish(nil)
|
||||
ExpectTrue(t, shouldTrueOnCancel)
|
||||
ExpectTrue(t, shouldTrueOnFinish)
|
||||
expect.False(t, shouldTrueOnFinish)
|
||||
task.FinishAndWait(nil)
|
||||
expect.True(t, shouldTrueOnCancel)
|
||||
expect.True(t, shouldTrueOnFinish)
|
||||
}
|
||||
|
||||
func TestCommonFlowWithGracefulShutdown(t *testing.T) {
|
||||
@@ -97,7 +97,7 @@ func TestCommonFlowWithGracefulShutdown(t *testing.T) {
|
||||
})
|
||||
|
||||
go func() {
|
||||
defer task.Finish(nil)
|
||||
defer task.FinishAndWait(nil)
|
||||
for {
|
||||
select {
|
||||
case <-task.Context().Done():
|
||||
@@ -108,29 +108,28 @@ func TestCommonFlowWithGracefulShutdown(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
ExpectNoError(t, gracefulShutdown(1*time.Second))
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
ExpectTrue(t, finished)
|
||||
expect.NoError(t, gracefulShutdown(1*time.Second))
|
||||
expect.True(t, finished)
|
||||
|
||||
ExpectTrue(t, root.waitFinish(1*time.Second))
|
||||
ExpectError(t, context.Canceled, context.Cause(task.Context()))
|
||||
ExpectError(t, ErrProgramExiting, task.Context().Err())
|
||||
ExpectError(t, ErrProgramExiting, task.FinishCause())
|
||||
expect.ErrorIs(t, ErrProgramExiting, context.Cause(task.Context()))
|
||||
expect.ErrorIs(t, context.Canceled, task.Context().Err())
|
||||
expect.ErrorIs(t, ErrProgramExiting, task.FinishCause())
|
||||
}
|
||||
|
||||
func TestTimeoutOnGracefulShutdown(t *testing.T) {
|
||||
t.Cleanup(testCleanup)
|
||||
_ = testTask()
|
||||
|
||||
ExpectError(t, context.DeadlineExceeded, gracefulShutdown(time.Millisecond))
|
||||
expect.ErrorIs(t, context.DeadlineExceeded, gracefulShutdown(time.Millisecond))
|
||||
}
|
||||
|
||||
func TestFinishMultipleCalls(t *testing.T) {
|
||||
t.Cleanup(testCleanup)
|
||||
task := testTask()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(5)
|
||||
for range 5 {
|
||||
n := 20
|
||||
wg.Add(n)
|
||||
for range n {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
task.Finish(nil)
|
||||
@@ -157,8 +156,8 @@ func BenchmarkTasksNeedFinish(b *testing.B) {
|
||||
|
||||
func BenchmarkContextWithCancel(b *testing.B) {
|
||||
for b.Loop() {
|
||||
task, taskCancel := context.WithCancel(b.Context())
|
||||
taskCancel()
|
||||
task, taskCancel := context.WithCancelCause(b.Context())
|
||||
taskCancel(nil)
|
||||
<-task.Done()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package task
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
@@ -13,6 +14,31 @@ import (
|
||||
|
||||
var ErrProgramExiting = errors.New("program exiting")
|
||||
|
||||
var root *Task
|
||||
|
||||
var closedCh = make(chan struct{})
|
||||
|
||||
func init() {
|
||||
close(closedCh)
|
||||
initRoot()
|
||||
}
|
||||
|
||||
func initRoot() {
|
||||
ctx, cancel := context.WithCancelCause(context.Background())
|
||||
root = &Task{
|
||||
name: "root",
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
done: closedCh,
|
||||
}
|
||||
root.parent = root
|
||||
}
|
||||
|
||||
func testCleanup() {
|
||||
root.cancel(nil)
|
||||
initRoot()
|
||||
}
|
||||
|
||||
// RootTask returns a new Task with the given name, derived from the root context.
|
||||
//
|
||||
//go:inline
|
||||
@@ -29,7 +55,7 @@ func RootContextCanceled() <-chan struct{} {
|
||||
}
|
||||
|
||||
func OnProgramExit(about string, fn func()) {
|
||||
root.OnFinished(about, fn)
|
||||
root.OnCancel(about, fn)
|
||||
}
|
||||
|
||||
// WaitExit waits for a signal to shutdown the program, and then waits for all tasks to finish, up to the given timeout.
|
||||
@@ -59,19 +85,33 @@ func WaitExit(shutdownTimeout int) {
|
||||
// still running when the timeout was reached, and their current tree
|
||||
// of subtasks.
|
||||
func gracefulShutdown(timeout time.Duration) error {
|
||||
root.mu.Lock()
|
||||
if root.isCanceled() {
|
||||
cause := context.Cause(root.ctx)
|
||||
root.mu.Unlock()
|
||||
return cause
|
||||
}
|
||||
root.mu.Unlock()
|
||||
|
||||
root.cancel(ErrProgramExiting)
|
||||
ok := waitEmpty(root.children, timeout)
|
||||
root.runOnFinishCallbacks()
|
||||
if !ok || !root.waitFinish(timeout) {
|
||||
go root.Finish(ErrProgramExiting)
|
||||
if !root.waitFinish(timeout) {
|
||||
return context.DeadlineExceeded
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func invokeWithRecover(cb *Callback) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Err(fmtCause(err)).Str("callback", cb.about).Msg("panic")
|
||||
panicWithDebugStack()
|
||||
}
|
||||
}()
|
||||
cb.fn()
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func fmtCause(cause any) error {
|
||||
switch cause := cause.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case error:
|
||||
return cause
|
||||
case string:
|
||||
return errors.New(cause)
|
||||
default:
|
||||
return fmt.Errorf("%v", cause)
|
||||
}
|
||||
}
|
||||
|
||||
48
internal/task/with.go
Normal file
48
internal/task/with.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
)
|
||||
|
||||
type withWg[T comparable] struct {
|
||||
m *xsync.Map[T, struct{}]
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func newWithWg[T comparable]() *withWg[T] {
|
||||
return &withWg[T]{
|
||||
m: xsync.NewMap[T, struct{}](),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *withWg[T]) Add(ele T) {
|
||||
w.wg.Add(1)
|
||||
w.m.Store(ele, struct{}{})
|
||||
}
|
||||
|
||||
func (w *withWg[T]) AddWithoutWG(ele T) {
|
||||
w.m.Store(ele, struct{}{})
|
||||
}
|
||||
|
||||
func (w *withWg[T]) Delete(key T) {
|
||||
w.wg.Done()
|
||||
w.m.Delete(key)
|
||||
}
|
||||
|
||||
func (w *withWg[T]) DeleteWithoutWG(key T) {
|
||||
w.m.Delete(key)
|
||||
}
|
||||
|
||||
func (w *withWg[T]) Wait() {
|
||||
w.wg.Wait()
|
||||
}
|
||||
|
||||
func (w *withWg[T]) Range(yield func(T) bool) {
|
||||
for ele := range w.m.Range {
|
||||
if !yield(ele) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ module github.com/yusing/go-proxy/internal/utils
|
||||
go 1.24.3
|
||||
|
||||
require (
|
||||
github.com/goccy/go-yaml v1.17.1
|
||||
github.com/goccy/go-yaml v1.18.0
|
||||
github.com/puzpuzpuz/xsync/v4 v4.1.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
|
||||
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
|
||||
@@ -106,13 +106,38 @@ func (p BidirectionalPipe) Start() error {
|
||||
return errors.Join(srcErr, dstErr)
|
||||
}
|
||||
|
||||
type httpFlusher interface {
|
||||
Flush() error
|
||||
type flushErrorInterface interface {
|
||||
FlushError() error
|
||||
}
|
||||
|
||||
func getHTTPFlusher(dst io.Writer) httpFlusher {
|
||||
type flusherWrapper struct {
|
||||
rw http.Flusher
|
||||
}
|
||||
|
||||
type rwUnwrapper interface {
|
||||
Unwrap() http.ResponseWriter
|
||||
}
|
||||
|
||||
func (f *flusherWrapper) FlushError() error {
|
||||
f.rw.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
func getHTTPFlusher(dst io.Writer) flushErrorInterface {
|
||||
// pre-unwrap the flusher to prevent unwrap and check in every loop
|
||||
if rw, ok := dst.(http.ResponseWriter); ok {
|
||||
return http.NewResponseController(rw)
|
||||
for {
|
||||
switch t := rw.(type) {
|
||||
case flushErrorInterface:
|
||||
return t
|
||||
case http.Flusher:
|
||||
return &flusherWrapper{rw: t}
|
||||
case rwUnwrapper:
|
||||
rw = t.Unwrap()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -158,7 +183,6 @@ func CopyClose(dst *ContextWriter, src *ContextReader, sizeHint int) (err error)
|
||||
}()
|
||||
}
|
||||
flusher := getHTTPFlusher(dst.Writer)
|
||||
canFlush := flusher != nil
|
||||
for {
|
||||
nr, er := src.Reader.Read(buf)
|
||||
if nr > 0 {
|
||||
@@ -177,15 +201,10 @@ func CopyClose(dst *ContextWriter, src *ContextReader, sizeHint int) (err error)
|
||||
err = io.ErrShortWrite
|
||||
return
|
||||
}
|
||||
if canFlush {
|
||||
err = flusher.Flush()
|
||||
if flusher != nil {
|
||||
err = flusher.FlushError()
|
||||
if err != nil {
|
||||
if errors.Is(err, http.ErrNotSupported) {
|
||||
canFlush = false
|
||||
err = nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ import (
|
||||
|
||||
type (
|
||||
Pool[T Object] struct {
|
||||
m *xsync.Map[string, T]
|
||||
name string
|
||||
m *xsync.Map[string, T]
|
||||
name string
|
||||
disableLog bool
|
||||
}
|
||||
Object interface {
|
||||
Key() string
|
||||
@@ -19,41 +20,69 @@ type (
|
||||
)
|
||||
|
||||
func New[T Object](name string) Pool[T] {
|
||||
return Pool[T]{xsync.NewMap[string, T](), name}
|
||||
return Pool[T]{xsync.NewMap[string, T](), name, false}
|
||||
}
|
||||
|
||||
func (p Pool[T]) Name() string {
|
||||
func (p *Pool[T]) DisableLog() {
|
||||
p.disableLog = true
|
||||
}
|
||||
|
||||
func (p *Pool[T]) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p Pool[T]) Add(obj T) {
|
||||
func (p *Pool[T]) Add(obj T) {
|
||||
p.checkExists(obj.Key())
|
||||
p.m.Store(obj.Key(), obj)
|
||||
log.Info().Msgf("%s: added %s", p.name, obj.Name())
|
||||
if !p.disableLog {
|
||||
log.Info().Msgf("%s: added %s", p.name, obj.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func (p Pool[T]) Del(obj T) {
|
||||
func (p *Pool[T]) AddKey(key string, obj T) {
|
||||
p.checkExists(key)
|
||||
p.m.Store(key, obj)
|
||||
if !p.disableLog {
|
||||
log.Info().Msgf("%s: added %s", p.name, obj.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool[T]) AddIfNotExists(obj T) (actual T, added bool) {
|
||||
actual, loaded := p.m.LoadOrStore(obj.Key(), obj)
|
||||
return actual, !loaded
|
||||
}
|
||||
|
||||
func (p *Pool[T]) Del(obj T) {
|
||||
p.m.Delete(obj.Key())
|
||||
log.Info().Msgf("%s: removed %s", p.name, obj.Name())
|
||||
if !p.disableLog {
|
||||
log.Info().Msgf("%s: removed %s", p.name, obj.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func (p Pool[T]) Get(key string) (T, bool) {
|
||||
func (p *Pool[T]) DelKey(key string) {
|
||||
p.m.Delete(key)
|
||||
if !p.disableLog {
|
||||
log.Info().Msgf("%s: removed %s", p.name, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool[T]) Get(key string) (T, bool) {
|
||||
return p.m.Load(key)
|
||||
}
|
||||
|
||||
func (p Pool[T]) Size() int {
|
||||
func (p *Pool[T]) Size() int {
|
||||
return p.m.Size()
|
||||
}
|
||||
|
||||
func (p Pool[T]) Clear() {
|
||||
func (p *Pool[T]) Clear() {
|
||||
p.m.Clear()
|
||||
}
|
||||
|
||||
func (p Pool[T]) Iter(fn func(k string, v T) bool) {
|
||||
func (p *Pool[T]) Iter(fn func(k string, v T) bool) {
|
||||
p.m.Range(fn)
|
||||
}
|
||||
|
||||
func (p Pool[T]) Slice() []T {
|
||||
func (p *Pool[T]) Slice() []T {
|
||||
slice := make([]T, 0, p.m.Size())
|
||||
for _, v := range p.m.Range {
|
||||
slice = append(slice, v)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
@@ -12,6 +13,8 @@ type HealthCheckConfig struct {
|
||||
UseGet bool `json:"use_get,omitempty"`
|
||||
Interval time.Duration `json:"interval" validate:"omitempty,min=1s"`
|
||||
Timeout time.Duration `json:"timeout" validate:"omitempty,min=1s"`
|
||||
|
||||
BaseContext func() context.Context `json:"-"`
|
||||
}
|
||||
|
||||
func DefaultHealthConfig() *HealthCheckConfig {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
agentPkg "github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
@@ -57,6 +58,7 @@ func NewAgentProxiedMonitor(agent *agentPkg.AgentConfig, config *health.HealthCh
|
||||
}
|
||||
|
||||
func (mon *AgentProxiedMonitor) CheckHealth() (result *health.HealthCheckResult, err error) {
|
||||
startTime := time.Now()
|
||||
result = new(health.HealthCheckResult)
|
||||
ctx, cancel := mon.ContextWithTimeout("timeout querying agent")
|
||||
defer cancel()
|
||||
@@ -64,11 +66,16 @@ func (mon *AgentProxiedMonitor) CheckHealth() (result *health.HealthCheckResult,
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
endTime := time.Now()
|
||||
switch status {
|
||||
case http.StatusOK:
|
||||
err = json.Unmarshal(data, result)
|
||||
default:
|
||||
err = errors.New(string(data))
|
||||
}
|
||||
if err == nil && result.Latency != 0 {
|
||||
// use godoxy to agent latency
|
||||
result.Latency = endTime.Sub(startTime)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ type (
|
||||
checkHealth HealthCheckFunc
|
||||
startTime time.Time
|
||||
|
||||
isZeroPort bool
|
||||
|
||||
task *task.Task
|
||||
}
|
||||
)
|
||||
@@ -63,22 +65,37 @@ func NewMonitor(r routes.Route) health.HealthMonCheck {
|
||||
return mon
|
||||
}
|
||||
|
||||
func newMonitor(url *url.URL, config *health.HealthCheckConfig, healthCheckFunc HealthCheckFunc) *monitor {
|
||||
func newMonitor(u *url.URL, config *health.HealthCheckConfig, healthCheckFunc HealthCheckFunc) *monitor {
|
||||
mon := &monitor{
|
||||
config: config,
|
||||
checkHealth: healthCheckFunc,
|
||||
startTime: time.Now(),
|
||||
}
|
||||
mon.url.Store(url)
|
||||
if u == nil {
|
||||
u = &url.URL{}
|
||||
}
|
||||
mon.url.Store(u)
|
||||
mon.status.Store(health.StatusHealthy)
|
||||
|
||||
port := u.Port()
|
||||
mon.isZeroPort = port == "" || port == "0"
|
||||
if mon.isZeroPort {
|
||||
mon.status.Store(health.StatusUnknown)
|
||||
mon.lastResult.Store(&health.HealthCheckResult{Healthy: false, Detail: "no port detected"})
|
||||
}
|
||||
return mon
|
||||
}
|
||||
|
||||
func (mon *monitor) ContextWithTimeout(cause string) (ctx context.Context, cancel context.CancelFunc) {
|
||||
if mon.task != nil {
|
||||
return context.WithTimeoutCause(mon.task.Context(), mon.config.Timeout, errors.New(cause))
|
||||
switch {
|
||||
case mon.config.BaseContext != nil:
|
||||
ctx = mon.config.BaseContext()
|
||||
case mon.task != nil:
|
||||
ctx = mon.task.Context()
|
||||
default:
|
||||
ctx = context.Background()
|
||||
}
|
||||
return context.WithTimeoutCause(context.Background(), mon.config.Timeout, errors.New(cause))
|
||||
return context.WithTimeoutCause(ctx, mon.config.Timeout, errors.New(cause))
|
||||
}
|
||||
|
||||
// Start implements task.TaskStarter.
|
||||
@@ -87,6 +104,10 @@ func (mon *monitor) Start(parent task.Parent) gperr.Error {
|
||||
return ErrNegativeInterval
|
||||
}
|
||||
|
||||
if mon.isZeroPort {
|
||||
return nil
|
||||
}
|
||||
|
||||
mon.service = parent.Name()
|
||||
mon.task = parent.Subtask("health_monitor", true)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ replace github.com/yusing/go-proxy/internal/utils => ../internal/utils
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/yusing/go-proxy/internal/utils v0.0.0-00010101000000-000000000000
|
||||
github.com/yusing/go-proxy/internal/utils v0.0.0-20250529124236-93a81fd558e6
|
||||
golang.org/x/net v0.40.0
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user