mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-16 08:26:49 +01:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a47170da39 | ||
|
|
89a4ca767d | ||
|
|
3dbbde164b | ||
|
|
e75eede332 | ||
|
|
e4658a8f09 | ||
|
|
e25ccdbd24 | ||
|
|
5087800fd7 | ||
|
|
d7f33b7390 | ||
|
|
1978329314 | ||
|
|
dba8441e8a | ||
|
|
44fc678496 | ||
|
|
0b410311da | ||
|
|
dc39f0cb6e | ||
|
|
e232b9d122 | ||
|
|
41f8d3cfc0 | ||
|
|
5ab0392cd3 | ||
|
|
09702266a9 | ||
|
|
14f3ed95ea | ||
|
|
eb3aa21e37 | ||
|
|
a6e86ea420 | ||
|
|
dd96e09a7a | ||
|
|
4d08efbd4f | ||
|
|
f67480d085 | ||
|
|
736985b79d | ||
|
|
1fb1ee0279 | ||
|
|
4b2a6023bb | ||
|
|
5852053ef9 | ||
|
|
c687795cd8 | ||
|
|
93af695e95 | ||
|
|
58325e60b4 | ||
|
|
b134b92704 | ||
|
|
376ac61279 | ||
|
|
dca701e044 | ||
|
|
4bb3af3671 | ||
|
|
95efc127cf | ||
|
|
6e55c4624b | ||
|
|
e9374364dd | ||
|
|
216679eb8d |
39
.github/workflows/merge-main-into-compat.yml
vendored
Normal file
39
.github/workflows/merge-main-into-compat.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Cherry-pick into Compat
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
paths:
|
||||
- ".github/workflows/merge-main-into-compat.yml"
|
||||
|
||||
jobs:
|
||||
cherry-pick:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Configure git user
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
- name: Cherry-pick commits from last tag
|
||||
run: |
|
||||
git fetch origin compat
|
||||
git checkout compat
|
||||
CURRENT_TAG=${{ github.ref_name }}
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 $CURRENT_TAG^ 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
echo "No previous tag found. Cherry-picking all commits up to $CURRENT_TAG"
|
||||
git rev-list --reverse --no-merges $CURRENT_TAG | xargs -r git cherry-pick
|
||||
else
|
||||
echo "Cherry-picking commits from $PREV_TAG to $CURRENT_TAG"
|
||||
git rev-list --reverse --no-merges $PREV_TAG..$CURRENT_TAG | xargs -r git cherry-pick
|
||||
fi
|
||||
- name: Push compat
|
||||
run: |
|
||||
git push origin compat
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -40,3 +40,4 @@ tsconfig.tsbuildinfo
|
||||
|
||||
!agent.compose.yml
|
||||
!agent/pkg/**
|
||||
dev-data/
|
||||
5
Makefile
5
Makefile
@@ -35,7 +35,7 @@ else ifeq ($(debug), 1)
|
||||
CGO_ENABLED = 1
|
||||
GODOXY_DEBUG = 1
|
||||
GO_TAGS += debug
|
||||
BUILD_FLAGS += -asan # FIXME: -gcflags=all='-N -l'
|
||||
# FIXME: BUILD_FLAGS += -asan -gcflags=all='-N -l'
|
||||
else ifeq ($(pprof), 1)
|
||||
CGO_ENABLED = 0
|
||||
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
|
||||
@@ -142,12 +142,13 @@ ci-test:
|
||||
act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)"
|
||||
|
||||
cloc:
|
||||
cloc --include-lang=Go --not-match-f '_test.go$$' .
|
||||
scc -w -i go --not-match '_test.go$'
|
||||
|
||||
push-github:
|
||||
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
gen-swagger:
|
||||
# go install github.com/swaggo/swag/cmd/swag@latest
|
||||
swag init --parseDependency --parseInternal --parseFuncBody -g handler.go -d internal/api -o internal/api/v1/docs
|
||||
python3 scripts/fix-swagger-json.py
|
||||
# we don't need this
|
||||
|
||||
14
agent/go.mod
14
agent/go.mod
@@ -25,8 +25,8 @@ require (
|
||||
github.com/yusing/godoxy v0.20.10
|
||||
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
||||
github.com/yusing/goutils v0.7.0
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20251216064709-6c698b1d55d0
|
||||
github.com/yusing/goutils/server v0.0.0-20251216064709-6c698b1d55d0
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20251217162119-cb0f79b51ce2
|
||||
github.com/yusing/goutils/server v0.0.0-20251217162119-cb0f79b51ce2
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -60,10 +60,10 @@ require (
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.29.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.0 // indirect
|
||||
github.com/goccy/go-yaml v1.19.1 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gotify/server/v2 v2.7.3 // indirect
|
||||
github.com/jinzhu/copier v0.4.0 // indirect
|
||||
@@ -73,7 +73,7 @@ require (
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/luthermonson/go-proxmox v0.2.3 // indirect
|
||||
github.com/luthermonson/go-proxmox v0.2.4 // indirect
|
||||
github.com/magefile/mage v1.15.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -91,7 +91,7 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.57.1 // indirect
|
||||
github.com/quic-go/quic-go v0.58.0 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/samber/slog-common v0.19.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
||||
@@ -106,7 +106,7 @@ require (
|
||||
github.com/vincent-petithory/dataurl v1.0.0 // indirect
|
||||
github.com/yusing/ds v0.3.1 // indirect
|
||||
github.com/yusing/gointernals v0.1.16 // indirect
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20251216064709-6c698b1d55d0 // indirect
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20251217162119-cb0f79b51ce2 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||
|
||||
16
agent/go.sum
16
agent/go.sum
@@ -77,16 +77,16 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.29.0 h1:lQlF5VNJWNlRbRZNeOIkWElR+1LL/OuHcc0Kp14w1xk=
|
||||
github.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2ccrZ7AGgHkzKmmTMZhk/Kc4=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
|
||||
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
@@ -124,8 +124,8 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/luthermonson/go-proxmox v0.2.3 h1:NAjUJ5Jd1ynIK6UHMGd/VLGgNZWpGXhfL+DBmAVSEaA=
|
||||
github.com/luthermonson/go-proxmox v0.2.3/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
github.com/luthermonson/go-proxmox v0.2.4 h1:XQ6YNUTVvHS7N4EJxWpuqWLW2s1VPtsIblxLV/rGHLw=
|
||||
github.com/luthermonson/go-proxmox v0.2.4/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@@ -172,8 +172,8 @@ github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wv
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
|
||||
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
|
||||
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
|
||||
257
cmd/debug_page.go
Normal file
257
cmd/debug_page.go
Normal file
@@ -0,0 +1,257 @@
|
||||
//go:build !production
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/api"
|
||||
apiV1 "github.com/yusing/godoxy/internal/api/v1"
|
||||
agentApi "github.com/yusing/godoxy/internal/api/v1/agent"
|
||||
authApi "github.com/yusing/godoxy/internal/api/v1/auth"
|
||||
certApi "github.com/yusing/godoxy/internal/api/v1/cert"
|
||||
dockerApi "github.com/yusing/godoxy/internal/api/v1/docker"
|
||||
fileApi "github.com/yusing/godoxy/internal/api/v1/file"
|
||||
homepageApi "github.com/yusing/godoxy/internal/api/v1/homepage"
|
||||
metricsApi "github.com/yusing/godoxy/internal/api/v1/metrics"
|
||||
routeApi "github.com/yusing/godoxy/internal/api/v1/route"
|
||||
"github.com/yusing/godoxy/internal/auth"
|
||||
"github.com/yusing/godoxy/internal/idlewatcher"
|
||||
idlewatcherTypes "github.com/yusing/godoxy/internal/idlewatcher/types"
|
||||
)
|
||||
|
||||
type debugMux struct {
|
||||
endpoints []debugEndpoint
|
||||
mux http.ServeMux
|
||||
}
|
||||
|
||||
type debugEndpoint struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
}
|
||||
|
||||
func newDebugMux() *debugMux {
|
||||
return &debugMux{
|
||||
endpoints: make([]debugEndpoint, 0),
|
||||
mux: *http.NewServeMux(),
|
||||
}
|
||||
}
|
||||
|
||||
func (mux *debugMux) registerEndpoint(name, method, path string) {
|
||||
mux.endpoints = append(mux.endpoints, debugEndpoint{name: name, method: method, path: path})
|
||||
}
|
||||
|
||||
func (mux *debugMux) HandleFunc(name, method, path string, handler http.HandlerFunc) {
|
||||
mux.registerEndpoint(name, method, path)
|
||||
mux.mux.HandleFunc(method+" "+path, handler)
|
||||
}
|
||||
|
||||
func (mux *debugMux) Finalize() {
|
||||
mux.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: #f8f9fa;
|
||||
background-color: #121212;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
th {
|
||||
background-color: #1e1e1e;
|
||||
font-weight: 600;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
td {
|
||||
color: #e9ecef;
|
||||
}
|
||||
.link {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.method {
|
||||
color: #6c757d;
|
||||
font-family: monospace;
|
||||
}
|
||||
.path {
|
||||
color: #6c757d;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Method</th>
|
||||
<th>Path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>`)
|
||||
for _, endpoint := range mux.endpoints {
|
||||
fmt.Fprintf(w, "<tr><td><a class='link' href=%q>%s</a></td><td class='method'>%s</td><td class='path'>%s</td></tr>", endpoint.path, endpoint.name, endpoint.method, endpoint.path)
|
||||
}
|
||||
fmt.Fprintln(w, `
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>`)
|
||||
})
|
||||
}
|
||||
|
||||
func listenDebugServer() {
|
||||
mux := newDebugMux()
|
||||
mux.mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "image/svg+xml")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text x="50" y="50" text-anchor="middle" dominant-baseline="middle">🐙</text></svg>`))
|
||||
})
|
||||
|
||||
mux.HandleFunc("Auth block page", "GET", "/auth/block", AuthBlockPageHandler)
|
||||
mux.HandleFunc("Idlewatcher loading page", "GET", idlewatcherTypes.PathPrefix, idlewatcher.DebugHandler)
|
||||
apiHandler := newApiHandler(mux)
|
||||
mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP)
|
||||
|
||||
mux.Finalize()
|
||||
|
||||
go http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Expires", "0")
|
||||
mux.mux.ServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
func newApiHandler(debugMux *debugMux) *gin.Engine {
|
||||
r := gin.New()
|
||||
r.Use(api.ErrorHandler())
|
||||
r.Use(api.ErrorLoggingMiddleware())
|
||||
r.Use(api.NoCache())
|
||||
|
||||
registerGinRoute := func(router gin.IRouter, method, name string, path string, handler gin.HandlerFunc) {
|
||||
if group, ok := router.(*gin.RouterGroup); ok {
|
||||
debugMux.registerEndpoint(name, method, group.BasePath()+path)
|
||||
} else {
|
||||
debugMux.registerEndpoint(name, method, path)
|
||||
}
|
||||
router.Handle(method, path, handler)
|
||||
}
|
||||
|
||||
registerGinRoute(r, "GET", "App version", "/api/v1/version", apiV1.Version)
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
if auth.IsEnabled() {
|
||||
v1Auth := v1.Group("/auth")
|
||||
{
|
||||
registerGinRoute(v1Auth, "HEAD", "Auth check", "/check", authApi.Check)
|
||||
registerGinRoute(v1Auth, "POST", "Auth login", "/login", authApi.Login)
|
||||
registerGinRoute(v1Auth, "GET", "Auth callback", "/callback", authApi.Callback)
|
||||
registerGinRoute(v1Auth, "POST", "Auth callback", "/callback", authApi.Callback)
|
||||
registerGinRoute(v1Auth, "POST", "Auth logout", "/logout", authApi.Logout)
|
||||
registerGinRoute(v1Auth, "GET", "Auth logout", "/logout", authApi.Logout)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// enable cache for favicon
|
||||
registerGinRoute(v1, "GET", "Route favicon", "/favicon", apiV1.FavIcon)
|
||||
registerGinRoute(v1, "GET", "Route health", "/health", apiV1.Health)
|
||||
registerGinRoute(v1, "GET", "List icons", "/icons", apiV1.Icons)
|
||||
registerGinRoute(v1, "POST", "Config reload", "/reload", apiV1.Reload)
|
||||
registerGinRoute(v1, "GET", "Route stats", "/stats", apiV1.Stats)
|
||||
|
||||
route := v1.Group("/route")
|
||||
{
|
||||
registerGinRoute(route, "GET", "List routes", "/list", routeApi.Routes)
|
||||
registerGinRoute(route, "GET", "Get route", "/:which", routeApi.Route)
|
||||
registerGinRoute(route, "GET", "List providers", "/providers", routeApi.Providers)
|
||||
registerGinRoute(route, "GET", "List routes by provider", "/by_provider", routeApi.ByProvider)
|
||||
registerGinRoute(route, "POST", "Playground", "/playground", routeApi.Playground)
|
||||
}
|
||||
|
||||
file := v1.Group("/file")
|
||||
{
|
||||
registerGinRoute(file, "GET", "List files", "/list", fileApi.List)
|
||||
registerGinRoute(file, "GET", "Get file", "/content", fileApi.Get)
|
||||
registerGinRoute(file, "PUT", "Set file", "/content", fileApi.Set)
|
||||
registerGinRoute(file, "POST", "Set file", "/content", fileApi.Set)
|
||||
registerGinRoute(file, "POST", "Validate file", "/validate", fileApi.Validate)
|
||||
}
|
||||
|
||||
homepage := v1.Group("/homepage")
|
||||
{
|
||||
registerGinRoute(homepage, "GET", "List categories", "/categories", homepageApi.Categories)
|
||||
registerGinRoute(homepage, "GET", "List items", "/items", homepageApi.Items)
|
||||
registerGinRoute(homepage, "POST", "Set item", "/set/item", homepageApi.SetItem)
|
||||
registerGinRoute(homepage, "POST", "Set items batch", "/set/items_batch", homepageApi.SetItemsBatch)
|
||||
registerGinRoute(homepage, "POST", "Set item visible", "/set/item_visible", homepageApi.SetItemVisible)
|
||||
registerGinRoute(homepage, "POST", "Set item favorite", "/set/item_favorite", homepageApi.SetItemFavorite)
|
||||
registerGinRoute(homepage, "POST", "Set item sort order", "/set/item_sort_order", homepageApi.SetItemSortOrder)
|
||||
registerGinRoute(homepage, "POST", "Set item all sort order", "/set/item_all_sort_order", homepageApi.SetItemAllSortOrder)
|
||||
registerGinRoute(homepage, "POST", "Set item fav sort order", "/set/item_fav_sort_order", homepageApi.SetItemFavSortOrder)
|
||||
registerGinRoute(homepage, "POST", "Set category order", "/set/category_order", homepageApi.SetCategoryOrder)
|
||||
registerGinRoute(homepage, "POST", "Item click", "/item_click", homepageApi.ItemClick)
|
||||
}
|
||||
|
||||
cert := v1.Group("/cert")
|
||||
{
|
||||
registerGinRoute(cert, "GET", "Get cert info", "/info", certApi.Info)
|
||||
registerGinRoute(cert, "GET", "Renew cert", "/renew", certApi.Renew)
|
||||
}
|
||||
|
||||
agent := v1.Group("/agent")
|
||||
{
|
||||
registerGinRoute(agent, "GET", "List agents", "/list", agentApi.List)
|
||||
registerGinRoute(agent, "POST", "Create agent", "/create", agentApi.Create)
|
||||
registerGinRoute(agent, "POST", "Verify agent", "/verify", agentApi.Verify)
|
||||
}
|
||||
|
||||
metrics := v1.Group("/metrics")
|
||||
{
|
||||
registerGinRoute(metrics, "GET", "Get system info", "/system_info", metricsApi.SystemInfo)
|
||||
registerGinRoute(metrics, "GET", "Get all system info", "/all_system_info", metricsApi.AllSystemInfo)
|
||||
registerGinRoute(metrics, "GET", "Get uptime", "/uptime", metricsApi.Uptime)
|
||||
}
|
||||
|
||||
docker := v1.Group("/docker")
|
||||
{
|
||||
registerGinRoute(docker, "GET", "Get container", "/container/:id", dockerApi.GetContainer)
|
||||
registerGinRoute(docker, "GET", "List containers", "/containers", dockerApi.Containers)
|
||||
registerGinRoute(docker, "GET", "Get docker info", "/info", dockerApi.Info)
|
||||
registerGinRoute(docker, "GET", "Get docker logs", "/logs/:id", dockerApi.Logs)
|
||||
registerGinRoute(docker, "POST", "Start docker container", "/start", dockerApi.Start)
|
||||
registerGinRoute(docker, "POST", "Stop docker container", "/stop", dockerApi.Stop)
|
||||
registerGinRoute(docker, "POST", "Restart docker container", "/restart", dockerApi.Restart)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func AuthBlockPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
auth.WriteBlockPage(w, http.StatusForbidden, "Forbidden", "Login", "/login")
|
||||
}
|
||||
7
cmd/debug_page_prod.go
Normal file
7
cmd/debug_page_prod.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build production
|
||||
|
||||
package main
|
||||
|
||||
func listenDebugServer() {
|
||||
// no-op
|
||||
}
|
||||
@@ -72,6 +72,8 @@ func main() {
|
||||
Handler: api.NewHandler(),
|
||||
})
|
||||
|
||||
listenDebugServer()
|
||||
|
||||
uptime.Poller.Start()
|
||||
config.WatchChanges()
|
||||
|
||||
|
||||
34
go.mod
34
go.mod
@@ -19,7 +19,7 @@ require (
|
||||
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||
github.com/gin-gonic/gin v1.11.0 // api server
|
||||
github.com/go-acme/lego/v4 v4.30.1 // acme client
|
||||
github.com/go-playground/validator/v10 v10.29.0 // validator
|
||||
github.com/go-playground/validator/v10 v10.30.1 // validator
|
||||
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
||||
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
||||
github.com/gotify/server/v2 v2.7.3 // reference the Message struct for json response
|
||||
@@ -39,25 +39,25 @@ require (
|
||||
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
|
||||
github.com/bytedance/sonic v1.14.2 // fast json parsing
|
||||
github.com/docker/cli v29.1.3+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
||||
github.com/goccy/go-yaml v1.19.0 // yaml parsing for different config files
|
||||
github.com/goccy/go-yaml v1.19.1 // yaml parsing for different config files
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // jwt authentication
|
||||
github.com/luthermonson/go-proxmox v0.2.3 // proxmox API client
|
||||
github.com/luthermonson/go-proxmox v0.2.4 // proxmox API client
|
||||
github.com/moby/moby/api v1.52.0 // docker API
|
||||
github.com/moby/moby/client v0.2.1 // docker client
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
|
||||
github.com/quic-go/quic-go v0.57.1 // http3 support
|
||||
github.com/quic-go/quic-go v0.58.0 // http3 support
|
||||
github.com/shirou/gopsutil/v4 v4.25.11 // system information
|
||||
github.com/spf13/afero v1.15.0 // afero for file system operations
|
||||
github.com/stretchr/testify v1.11.1 // testing framework
|
||||
github.com/valyala/fasthttp v1.68.0 // fast http for health check
|
||||
github.com/yusing/ds v0.3.1 // data structures and algorithms
|
||||
github.com/yusing/godoxy/agent v0.0.0-20251217034652-88d7255c7adc
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20251217034652-88d7255c7adc
|
||||
github.com/yusing/godoxy/agent v0.0.0-20251230135310-5087800fd763
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20251230043958-dba8441e8a5d
|
||||
github.com/yusing/gointernals v0.1.16
|
||||
github.com/yusing/goutils v0.7.0
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20251216064709-6c698b1d55d0
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20251216064709-6c698b1d55d0
|
||||
github.com/yusing/goutils/server v0.0.0-20251216064709-6c698b1d55d0
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20251217162119-cb0f79b51ce2
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20251217162119-cb0f79b51ce2
|
||||
github.com/yusing/goutils/server v0.0.0-20251217162119-cb0f79b51ce2
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -93,7 +93,7 @@ require (
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/jinzhu/copier v0.4.0 // indirect
|
||||
@@ -120,7 +120,7 @@ require (
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/samber/slog-common v0.19.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
@@ -135,9 +135,9 @@ require (
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/api v0.257.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||
google.golang.org/grpc v1.77.0 // indirect
|
||||
google.golang.org/api v0.258.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
@@ -159,7 +159,7 @@ require (
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/go-resty/resty/v2 v2.17.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/linode/linodego v1.63.0 // indirect
|
||||
@@ -175,9 +175,7 @@ require (
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/vultr/govultr/v3 v3.26.0 // indirect
|
||||
github.com/vultr/govultr/v3 v3.26.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
)
|
||||
|
||||
54
go.sum
54
go.sum
@@ -115,8 +115,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.29.0 h1:lQlF5VNJWNlRbRZNeOIkWElR+1LL/OuHcc0Kp14w1xk=
|
||||
github.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2ccrZ7AGgHkzKmmTMZhk/Kc4=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
|
||||
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
@@ -125,8 +125,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
|
||||
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
@@ -134,12 +134,11 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
@@ -147,8 +146,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/server/v2 v2.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
|
||||
@@ -189,8 +188,8 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/luthermonson/go-proxmox v0.2.3 h1:NAjUJ5Jd1ynIK6UHMGd/VLGgNZWpGXhfL+DBmAVSEaA=
|
||||
github.com/luthermonson/go-proxmox v0.2.3/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
github.com/luthermonson/go-proxmox v0.2.4 h1:XQ6YNUTVvHS7N4EJxWpuqWLW2s1VPtsIblxLV/rGHLw=
|
||||
github.com/luthermonson/go-proxmox v0.2.4/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@@ -253,8 +252,8 @@ github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wv
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
|
||||
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
|
||||
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
@@ -266,8 +265,8 @@ github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89
|
||||
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
||||
@@ -305,8 +304,8 @@ github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFn
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||
github.com/vultr/govultr/v3 v3.26.0 h1:pm/GM+RZo9T1JLQzrUti5HiNAIFZFEHcPFMOWGvvNIY=
|
||||
github.com/vultr/govultr/v3 v3.26.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||
github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
|
||||
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
@@ -431,19 +430,18 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA=
|
||||
google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4=
|
||||
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba h1:Ze6qXW0j37YCqZdCD2LkzVSxgEWez0cO4NUyd44DiDY=
|
||||
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:4FLPzLA8eGAktPOTemJGDgDYRpLYwrNu4u2JtWINhnI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
|
||||
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
2
goutils
2
goutils
Submodule goutils updated: 6c698b1d55...51a75d684b
@@ -29,13 +29,13 @@ func GetContainer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(id)
|
||||
dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||
return
|
||||
}
|
||||
|
||||
dockerClient, err := docker.NewClient(dockerHost)
|
||||
dockerClient, err := docker.NewClient(dockerCfg)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
@@ -55,7 +55,7 @@ func GetContainer(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &Container{
|
||||
Server: dockerHost,
|
||||
Server: dockerCfg.URL,
|
||||
Name: cont.Container.Name,
|
||||
ID: cont.Container.ID,
|
||||
Image: cont.Container.Image,
|
||||
|
||||
@@ -57,13 +57,13 @@ func Logs(c *gin.Context) {
|
||||
}
|
||||
|
||||
// TODO: implement levels
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(id)
|
||||
dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error(fmt.Sprintf("container %s not found", id)))
|
||||
return
|
||||
}
|
||||
|
||||
dockerClient, err := docker.NewClient(dockerHost)
|
||||
dockerClient, err := docker.NewClient(dockerCfg)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get docker client"))
|
||||
return
|
||||
@@ -105,7 +105,7 @@ func Logs(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
log.Err(err).
|
||||
Str("server", dockerHost).
|
||||
Str("server", dockerCfg.URL).
|
||||
Str("container", id).
|
||||
Msg("failed to de-multiplex logs")
|
||||
}
|
||||
|
||||
@@ -34,13 +34,13 @@ func Restart(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
|
||||
dockerCfg, ok := docker.GetDockerCfgByContainerID(req.ID)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
client, err := docker.NewClient(dockerCfg)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
|
||||
@@ -34,13 +34,13 @@ func Start(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
|
||||
dockerCfg, ok := docker.GetDockerCfgByContainerID(req.ID)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
client, err := docker.NewClient(dockerCfg)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
|
||||
@@ -34,13 +34,13 @@ func Stop(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
|
||||
dockerCfg, ok := docker.GetDockerCfgByContainerID(req.ID)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
client, err := docker.NewClient(dockerCfg)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
|
||||
@@ -2458,8 +2458,8 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"docker_host": {
|
||||
"type": "string",
|
||||
"docker_cfg": {
|
||||
"$ref": "#/definitions/DockerProviderConfig",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
@@ -2715,7 +2715,7 @@
|
||||
"required": [
|
||||
"container_id",
|
||||
"container_name",
|
||||
"docker_host"
|
||||
"docker_cfg"
|
||||
],
|
||||
"properties": {
|
||||
"container_id": {
|
||||
@@ -2728,7 +2728,24 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"docker_host": {
|
||||
"docker_cfg": {
|
||||
"$ref": "#/definitions/DockerProviderConfig",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
}
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"DockerProviderConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tls": {
|
||||
"$ref": "#/definitions/DockerTLSConfig",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
@@ -2737,6 +2754,27 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"DockerTLSConfig": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ca_file"
|
||||
],
|
||||
"properties": {
|
||||
"ca_file": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"cert_file": {
|
||||
"type": "string"
|
||||
},
|
||||
"key_file": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"ErrorResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3430,6 +3468,11 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"no_loading_page": {
|
||||
"type": "boolean",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"proxmox": {
|
||||
"$ref": "#/definitions/ProxmoxConfig",
|
||||
"x-nullable": false,
|
||||
@@ -4234,6 +4277,12 @@
|
||||
],
|
||||
"x-nullable": true
|
||||
},
|
||||
"index": {
|
||||
"description": "Index file to serve for single-page app mode",
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"load_balance": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -4315,6 +4364,12 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"spa": {
|
||||
"description": "Single-page app mode: serves index for non-existent paths",
|
||||
"type": "boolean",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"ssl_certificate": {
|
||||
"description": "Path to client certificate",
|
||||
"type": "string",
|
||||
@@ -4500,6 +4555,11 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"is_excluded": {
|
||||
"type": "boolean",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"statuses": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -5354,6 +5414,12 @@
|
||||
],
|
||||
"x-nullable": true
|
||||
},
|
||||
"index": {
|
||||
"description": "Index file to serve for single-page app mode",
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"load_balance": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -5435,6 +5501,12 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"spa": {
|
||||
"description": "Single-page app mode: serves index for non-existent paths",
|
||||
"type": "boolean",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"ssl_certificate": {
|
||||
"description": "Path to client certificate",
|
||||
"type": "string",
|
||||
|
||||
@@ -57,8 +57,8 @@ definitions:
|
||||
type: string
|
||||
container_name:
|
||||
type: string
|
||||
docker_host:
|
||||
type: string
|
||||
docker_cfg:
|
||||
$ref: '#/definitions/DockerProviderConfig'
|
||||
errors:
|
||||
type: string
|
||||
idlewatcher_config:
|
||||
@@ -192,12 +192,30 @@ definitions:
|
||||
type: string
|
||||
container_name:
|
||||
type: string
|
||||
docker_host:
|
||||
type: string
|
||||
docker_cfg:
|
||||
$ref: '#/definitions/DockerProviderConfig'
|
||||
required:
|
||||
- container_id
|
||||
- container_name
|
||||
- docker_host
|
||||
- docker_cfg
|
||||
type: object
|
||||
DockerProviderConfig:
|
||||
properties:
|
||||
tls:
|
||||
$ref: '#/definitions/DockerTLSConfig'
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
DockerTLSConfig:
|
||||
properties:
|
||||
ca_file:
|
||||
type: string
|
||||
cert_file:
|
||||
type: string
|
||||
key_file:
|
||||
type: string
|
||||
required:
|
||||
- ca_file
|
||||
type: object
|
||||
ErrorResponse:
|
||||
properties:
|
||||
@@ -517,6 +535,8 @@ definitions:
|
||||
description: "0: no idle watcher.\nPositive: idle watcher with idle timeout.\nNegative:
|
||||
idle watcher as a dependency.\tIdleTimeout time.Duration `json:\"idle_timeout\"
|
||||
json_ext:\"duration\"`"
|
||||
no_loading_page:
|
||||
type: boolean
|
||||
proxmox:
|
||||
$ref: '#/definitions/ProxmoxConfig'
|
||||
start_endpoint:
|
||||
@@ -897,6 +917,9 @@ definitions:
|
||||
allOf:
|
||||
- $ref: '#/definitions/IdlewatcherConfig'
|
||||
x-nullable: true
|
||||
index:
|
||||
description: Index file to serve for single-page app mode
|
||||
type: string
|
||||
load_balance:
|
||||
allOf:
|
||||
- $ref: '#/definitions/LoadBalancerConfig'
|
||||
@@ -944,6 +967,9 @@ definitions:
|
||||
- udp
|
||||
- fileserver
|
||||
type: string
|
||||
spa:
|
||||
description: 'Single-page app mode: serves index for non-existent paths'
|
||||
type: boolean
|
||||
ssl_certificate:
|
||||
description: Path to client certificate
|
||||
type: string
|
||||
@@ -1030,6 +1056,8 @@ definitions:
|
||||
type: number
|
||||
is_docker:
|
||||
type: boolean
|
||||
is_excluded:
|
||||
type: boolean
|
||||
statuses:
|
||||
items:
|
||||
$ref: '#/definitions/RouteStatus'
|
||||
@@ -1504,6 +1532,9 @@ definitions:
|
||||
allOf:
|
||||
- $ref: '#/definitions/IdlewatcherConfig'
|
||||
x-nullable: true
|
||||
index:
|
||||
description: Index file to serve for single-page app mode
|
||||
type: string
|
||||
load_balance:
|
||||
allOf:
|
||||
- $ref: '#/definitions/LoadBalancerConfig'
|
||||
@@ -1551,6 +1582,9 @@ definitions:
|
||||
- udp
|
||||
- fileserver
|
||||
type: string
|
||||
spa:
|
||||
description: 'Single-page app mode: serves index for non-existent paths'
|
||||
type: boolean
|
||||
ssl_certificate:
|
||||
description: Path to client certificate
|
||||
type: string
|
||||
|
||||
@@ -34,12 +34,12 @@ func Routes(c *gin.Context) {
|
||||
|
||||
provider := c.Query("provider")
|
||||
if provider == "" {
|
||||
c.JSON(http.StatusOK, slices.Collect(routes.Iter))
|
||||
c.JSON(http.StatusOK, slices.Collect(routes.IterAll))
|
||||
return
|
||||
}
|
||||
|
||||
rts := make([]types.Route, 0, routes.NumRoutes())
|
||||
for r := range routes.Iter {
|
||||
rts := make([]types.Route, 0, routes.NumAllRoutes())
|
||||
for r := range routes.IterAll {
|
||||
if r.ProviderName() == provider {
|
||||
rts = append(rts, r)
|
||||
}
|
||||
@@ -51,14 +51,14 @@ func RoutesWS(c *gin.Context) {
|
||||
provider := c.Query("provider")
|
||||
if provider == "" {
|
||||
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
|
||||
return slices.Collect(routes.Iter), nil
|
||||
return slices.Collect(routes.IterAll), nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
|
||||
rts := make([]types.Route, 0, routes.NumRoutes())
|
||||
for r := range routes.Iter {
|
||||
rts := make([]types.Route, 0, routes.NumAllRoutes())
|
||||
for r := range routes.IterAll {
|
||||
if r.ProviderName() == provider {
|
||||
rts = append(rts, r)
|
||||
}
|
||||
|
||||
@@ -65,14 +65,12 @@ func AuthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func AuthOrProceed(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
||||
if defaultAuth == nil {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
return false
|
||||
return true
|
||||
}
|
||||
err := defaultAuth.CheckToken(r)
|
||||
if err != nil {
|
||||
defaultAuth.LoginHandler(w, r)
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -12,11 +12,12 @@ var blockPageHTML string
|
||||
|
||||
var blockPageTemplate = template.Must(template.New("block_page").Parse(blockPageHTML))
|
||||
|
||||
func WriteBlockPage(w http.ResponseWriter, status int, error string, logoutURL string) {
|
||||
func WriteBlockPage(w http.ResponseWriter, status int, errorMessage, actionText, actionURL string) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
blockPageTemplate.Execute(w, map[string]string{
|
||||
"StatusText": http.StatusText(status),
|
||||
"Error": error,
|
||||
"LogoutURL": logoutURL,
|
||||
"Error": errorMessage,
|
||||
"ActionURL": actionURL,
|
||||
"ActionText": actionText,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,14 +1,231 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>Access Denied</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{.StatusText}}</h1>
|
||||
<p>{{.Error}}</p>
|
||||
<a href="{{.LogoutURL}}">Logout</a>
|
||||
</body>
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg0: #070a12;
|
||||
--bg1: #0b1020;
|
||||
--card: rgba(255, 255, 255, 0.055);
|
||||
--card2: rgba(255, 255, 255, 0.05);
|
||||
--text: rgba(255, 255, 255, 0.92);
|
||||
--muted: rgba(255, 255, 255, 0.68);
|
||||
--border: rgba(255, 255, 255, 0.12);
|
||||
--borderSoft: rgba(255, 255, 255, 0.08);
|
||||
--borderStrong: rgba(255, 255, 255, 0.14);
|
||||
--borderHover: rgba(255, 255, 255, 0.22);
|
||||
--shadow: 0 22px 60px rgba(0, 0, 0, 0.55);
|
||||
--shadowCard: 0 22px 60px rgba(0, 0, 0, 0.58);
|
||||
--shadowButton: 0 12px 28px rgba(0, 0, 0, 0.35);
|
||||
--insetHighlight: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||
--ring: rgba(120, 160, 210, 0.42);
|
||||
--accent0: #7aa3c8;
|
||||
--accent1: #9a8bc7;
|
||||
--btn: rgba(255, 255, 255, 0.06);
|
||||
--btnHover: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto,
|
||||
Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji;
|
||||
color: var(--text);
|
||||
background-color: var(--bg1);
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
min-height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 28px 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: min(720px, 100%);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadowCard), var(--insetHighlight);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 18px 18px 12px;
|
||||
border-bottom: 1px solid var(--borderSoft);
|
||||
background: var(--card2);
|
||||
}
|
||||
|
||||
.badge {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid var(--borderStrong);
|
||||
background: var(--card2);
|
||||
}
|
||||
|
||||
.badge svg {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.badge .bang {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.25;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin: 2px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 0;
|
||||
padding: 14px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
Liberation Mono, Courier New, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
a.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid var(--borderStrong);
|
||||
background: var(--btn);
|
||||
transition: transform 120ms ease, border-color 120ms ease,
|
||||
background 120ms ease, box-shadow 120ms ease;
|
||||
box-shadow: var(--shadowButton);
|
||||
}
|
||||
|
||||
a.button:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: var(--borderHover);
|
||||
background: var(--btnHover);
|
||||
}
|
||||
|
||||
a.button:focus-visible {
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 3px var(--ring), var(--shadowButton);
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.hint kbd {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
Liberation Mono, Courier New, monospace;
|
||||
font-size: 11px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--borderStrong);
|
||||
background: var(--btn);
|
||||
color: rgba(255, 255, 255, 0.86);
|
||||
}
|
||||
|
||||
kbd {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.kbd-container {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<main class="card" role="main" aria-labelledby="title">
|
||||
<header class="topbar">
|
||||
<div class="badge" aria-hidden="true">
|
||||
<span class="bang">!</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 id="title">{{.StatusText}}</h1>
|
||||
<p class="sub">
|
||||
You don’t have permission to access this resource.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="content">
|
||||
<pre class="error">{{.Error}}</pre>
|
||||
<div class="actions">
|
||||
<a class="button" href="{{.ActionURL}}">
|
||||
<span>{{.ActionText}}</span>
|
||||
<span aria-hidden="true">→</span>
|
||||
</a>
|
||||
<div class="hint">
|
||||
If you just signed in, try refreshing the page.
|
||||
<span aria-hidden="true"> </span>
|
||||
<div class="kbd-container">
|
||||
<kbd>Ctrl</kbd>
|
||||
<span>+</span>
|
||||
<kbd>R</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -32,6 +32,8 @@ type (
|
||||
allowedUsers []string
|
||||
allowedGroups []string
|
||||
|
||||
rateLimit *rate.Limiter
|
||||
|
||||
onUnknownPathHandler http.HandlerFunc
|
||||
}
|
||||
|
||||
@@ -66,9 +68,9 @@ func (auth *OIDCProvider) getAppScopedCookieName(baseName string) string {
|
||||
|
||||
const (
|
||||
OIDCAuthInitPath = "/"
|
||||
OIDCAuthBasePath = "/auth"
|
||||
OIDCPostAuthPath = OIDCAuthBasePath + "/callback"
|
||||
OIDCLogoutPath = OIDCAuthBasePath + "/logout"
|
||||
OIDCAuthBasePath = "/auth/"
|
||||
OIDCPostAuthPath = OIDCAuthBasePath + "callback"
|
||||
OIDCLogoutPath = OIDCAuthBasePath + "logout"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -123,6 +125,7 @@ func NewOIDCProvider(issuerURL, clientID, clientSecret string, allowedUsers, all
|
||||
endSessionURL: endSessionURL,
|
||||
allowedUsers: allowedUsers,
|
||||
allowedGroups: allowedGroups,
|
||||
rateLimit: rate.NewLimiter(rate.Every(common.OIDCRateLimitPeriod), common.OIDCRateLimit),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -165,6 +168,7 @@ func NewOIDCProviderWithCustomClient(baseProvider *OIDCProvider, clientID, clien
|
||||
endSessionURL: baseProvider.endSessionURL,
|
||||
allowedUsers: baseProvider.allowedUsers,
|
||||
allowedGroups: baseProvider.allowedGroups,
|
||||
rateLimit: baseProvider.rateLimit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -228,9 +232,12 @@ func (auth *OIDCProvider) HandleAuth(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
var rateLimit = rate.NewLimiter(rate.Every(time.Second), 1)
|
||||
|
||||
func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !httputils.GetAccept(r.Header).AcceptHTML() {
|
||||
http.Error(w, "authentication is required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// check for session token
|
||||
sessionToken, err := r.Cookie(auth.getAppScopedCookieName(CookieOauthSessionToken))
|
||||
if err == nil { // session token exists
|
||||
@@ -250,8 +257,8 @@ func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !rateLimit.Allow() {
|
||||
http.Error(w, "auth rate limit exceeded", http.StatusTooManyRequests)
|
||||
if !auth.rateLimit.Allow() {
|
||||
WriteBlockPage(w, http.StatusTooManyRequests, "auth rate limit exceeded", "Try again", OIDCAuthInitPath)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -318,34 +325,39 @@ func (auth *OIDCProvider) PostAuthCallbackHandler(w http.ResponseWriter, r *http
|
||||
// verify state
|
||||
state, err := r.Cookie(auth.getAppScopedCookieName(CookieOauthState))
|
||||
if err != nil {
|
||||
http.Error(w, "missing state cookie", http.StatusBadRequest)
|
||||
auth.clearCookie(w, r)
|
||||
WriteBlockPage(w, http.StatusBadRequest, "missing state cookie", "Back to Login", OIDCAuthInitPath)
|
||||
return
|
||||
}
|
||||
if r.URL.Query().Get("state") != state.Value {
|
||||
http.Error(w, "invalid oauth state", http.StatusBadRequest)
|
||||
auth.clearCookie(w, r)
|
||||
WriteBlockPage(w, http.StatusBadRequest, "invalid oauth state", "Back to Login", OIDCAuthInitPath)
|
||||
return
|
||||
}
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
oauth2Token, err := auth.oauthConfig.Exchange(r.Context(), code, optRedirectPostAuth(r))
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
httputils.LogError(r).Msg(fmt.Sprintf("failed to exchange token: %v", err))
|
||||
auth.clearCookie(w, r)
|
||||
WriteBlockPage(w, http.StatusInternalServerError, "failed to exchange token", "Try again", OIDCAuthInitPath)
|
||||
httputils.LogError(r).Msgf("failed to exchange token: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
idTokenJWT, idToken, err := auth.getIDToken(r.Context(), oauth2Token)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
httputils.LogError(r).Msg(fmt.Sprintf("failed to get ID token: %v", err))
|
||||
auth.clearCookie(w, r)
|
||||
WriteBlockPage(w, http.StatusInternalServerError, "failed to get ID token", "Try again", OIDCAuthInitPath)
|
||||
httputils.LogError(r).Msgf("failed to get ID token: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if oauth2Token.RefreshToken != "" {
|
||||
claims, err := parseClaims(idToken)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
httputils.LogError(r).Msg(fmt.Sprintf("failed to parse claims: %v", err))
|
||||
auth.clearCookie(w, r)
|
||||
WriteBlockPage(w, http.StatusInternalServerError, "failed to parse claims", "Try again", OIDCAuthInitPath)
|
||||
httputils.LogError(r).Msgf("failed to parse claims: %v", err)
|
||||
return
|
||||
}
|
||||
session := newSession(claims.Username, claims.Groups)
|
||||
|
||||
@@ -39,12 +39,14 @@ var (
|
||||
DebugDisableAuth = env.GetEnvBool("DEBUG_DISABLE_AUTH", false)
|
||||
|
||||
// OIDC Configuration.
|
||||
OIDCIssuerURL = env.GetEnvString("OIDC_ISSUER_URL", "")
|
||||
OIDCClientID = env.GetEnvString("OIDC_CLIENT_ID", "")
|
||||
OIDCClientSecret = env.GetEnvString("OIDC_CLIENT_SECRET", "")
|
||||
OIDCScopes = env.GetEnvCommaSep("OIDC_SCOPES", "openid, profile, email, groups")
|
||||
OIDCAllowedUsers = env.GetEnvCommaSep("OIDC_ALLOWED_USERS", "")
|
||||
OIDCAllowedGroups = env.GetEnvCommaSep("OIDC_ALLOWED_GROUPS", "")
|
||||
OIDCIssuerURL = env.GetEnvString("OIDC_ISSUER_URL", "")
|
||||
OIDCClientID = env.GetEnvString("OIDC_CLIENT_ID", "")
|
||||
OIDCClientSecret = env.GetEnvString("OIDC_CLIENT_SECRET", "")
|
||||
OIDCScopes = env.GetEnvCommaSep("OIDC_SCOPES", "openid, profile, email, groups")
|
||||
OIDCAllowedUsers = env.GetEnvCommaSep("OIDC_ALLOWED_USERS", "")
|
||||
OIDCAllowedGroups = env.GetEnvCommaSep("OIDC_ALLOWED_GROUPS", "")
|
||||
OIDCRateLimit = env.GetEnvInt("OIDC_RATE_LIMIT", 10)
|
||||
OIDCRateLimitPeriod = env.GetEnvDuation("OIDC_RATE_LIMIT_PERIOD", time.Second)
|
||||
|
||||
// metrics configuration
|
||||
MetricsDisableCPU = env.GetEnvBool("METRICS_DISABLE_CPU", false)
|
||||
|
||||
@@ -90,8 +90,7 @@ func Reload() gperr.Error {
|
||||
if err != nil {
|
||||
newState.Task().FinishAndWait(err)
|
||||
config.WorkingState.Store(GetState())
|
||||
logNotifyError("reload", err)
|
||||
return gperr.New(ansi.Warning("using last config")).With(err)
|
||||
return gperr.Wrap(err, ansi.Warning("using last config"))
|
||||
}
|
||||
|
||||
// flush temporary log
|
||||
@@ -117,7 +116,7 @@ func WatchChanges() {
|
||||
configEventFlushInterval,
|
||||
OnConfigChange,
|
||||
func(err gperr.Error) {
|
||||
logNotifyError("config reload", err)
|
||||
logNotifyError("reload", err)
|
||||
},
|
||||
)
|
||||
eventQueue.Start(cfgWatcher.Events(t.Context()))
|
||||
|
||||
@@ -318,9 +318,9 @@ func (state *state) loadRouteProviders() error {
|
||||
})
|
||||
}
|
||||
|
||||
for name, dockerHost := range providers.Docker {
|
||||
for name, dockerCfg := range providers.Docker {
|
||||
providersProducer.Go(func() {
|
||||
providersCh <- route.NewDockerProvider(name, dockerHost)
|
||||
providersCh <- route.NewDockerProvider(name, dockerCfg)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -32,12 +32,12 @@ type (
|
||||
HealthCheck types.HealthCheckConfig `json:"healthcheck"`
|
||||
}
|
||||
Providers struct {
|
||||
Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"`
|
||||
Docker map[string]string `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys,dive,unix_addr|url"`
|
||||
Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
|
||||
Notification []*notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"`
|
||||
Proxmox []proxmox.Config `json:"proxmox" yaml:"proxmox,omitempty"`
|
||||
MaxMind *maxmind.Config `json:"maxmind" yaml:"maxmind,omitempty"`
|
||||
Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"`
|
||||
Docker map[string]types.DockerProviderConfig `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys"`
|
||||
Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
|
||||
Notification []*notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"`
|
||||
Proxmox []proxmox.Config `json:"proxmox" yaml:"proxmox,omitempty"`
|
||||
MaxMind *maxmind.Config `json:"maxmind" yaml:"maxmind,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -68,7 +68,7 @@ func init() {
|
||||
return true
|
||||
})
|
||||
serialization.MustRegisterValidation("non_empty_docker_keys", func(fl validator.FieldLevel) bool {
|
||||
m := fl.Field().Interface().(map[string]string)
|
||||
m := fl.Field().Interface().(map[string]types.DockerProviderConfig)
|
||||
for k := range m {
|
||||
if k == "" {
|
||||
return false
|
||||
|
||||
@@ -6,7 +6,7 @@ replace github.com/yusing/godoxy => ../..
|
||||
|
||||
require (
|
||||
github.com/go-acme/lego/v4 v4.30.1
|
||||
github.com/yusing/godoxy v0.21.0
|
||||
github.com/yusing/godoxy v0.21.3
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -37,16 +37,16 @@ require (
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.29.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/go-resty/resty/v2 v2.17.1 // indirect
|
||||
github.com/goccy/go-yaml v1.19.0 // indirect
|
||||
github.com/goccy/go-yaml v1.19.1 // indirect
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||
github.com/gotify/server/v2 v2.7.3 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
@@ -68,12 +68,12 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 // indirect
|
||||
github.com/rs/zerolog v1.34.0 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/vultr/govultr/v3 v3.26.0 // indirect
|
||||
github.com/vultr/govultr/v3 v3.26.1 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/yusing/gointernals v0.1.16 // indirect
|
||||
github.com/yusing/goutils v0.7.0 // indirect
|
||||
@@ -92,9 +92,9 @@ require (
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/api v0.257.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||
google.golang.org/grpc v1.77.0 // indirect
|
||||
google.golang.org/api v0.258.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
||||
@@ -74,12 +74,12 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.29.0 h1:lQlF5VNJWNlRbRZNeOIkWElR+1LL/OuHcc0Kp14w1xk=
|
||||
github.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2ccrZ7AGgHkzKmmTMZhk/Kc4=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
|
||||
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
|
||||
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
@@ -87,19 +87,19 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
||||
github.com/gotify/server/v2 v2.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
|
||||
github.com/gotify/server/v2 v2.7.3/go.mod h1:VAtE1RIc/2j886PYs9WPQbMjqbFsoyQ0G8IdFtnAxU0=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
@@ -160,8 +160,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
|
||||
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
||||
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -180,8 +180,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/vultr/govultr/v3 v3.26.0 h1:pm/GM+RZo9T1JLQzrUti5HiNAIFZFEHcPFMOWGvvNIY=
|
||||
github.com/vultr/govultr/v3 v3.26.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||
github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
|
||||
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
||||
@@ -232,19 +232,18 @@ golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA=
|
||||
google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4=
|
||||
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba h1:Ze6qXW0j37YCqZdCD2LkzVSxgEWez0cO4NUyd44DiDY=
|
||||
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:4FLPzLA8eGAktPOTemJGDgDYRpLYwrNu4u2JtWINhnI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
|
||||
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -2,7 +2,6 @@ package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net"
|
||||
@@ -17,7 +16,7 @@ import (
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
"github.com/yusing/goutils/task"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
@@ -28,6 +27,8 @@ type (
|
||||
SharedClient struct {
|
||||
*client.Client
|
||||
|
||||
cfg types.DockerProviderConfig
|
||||
|
||||
refCount atomic.Int32
|
||||
closedOn atomic.Int64
|
||||
|
||||
@@ -110,19 +111,17 @@ func Clients() map[string]*SharedClient {
|
||||
return clients
|
||||
}
|
||||
|
||||
var versionArg = client.WithAPIVersionNegotiation()
|
||||
|
||||
// NewClient creates a new Docker client connection to the specified host.
|
||||
//
|
||||
// Returns existing client if available.
|
||||
//
|
||||
// Parameters:
|
||||
// - host: the host to connect to (either a URL or common.DockerHostFromEnv).
|
||||
// - host: the host to connect to (either a URL or client.DefaultDockerHost).
|
||||
//
|
||||
// Returns:
|
||||
// - Client: the Docker client connection.
|
||||
// - error: an error if the connection failed.
|
||||
func NewClient(host string, unique ...bool) (*SharedClient, error) {
|
||||
func NewClient(cfg types.DockerProviderConfig, unique ...bool) (*SharedClient, error) {
|
||||
initClientCleanerOnce.Do(initClientCleaner)
|
||||
|
||||
u := false
|
||||
@@ -130,6 +129,8 @@ func NewClient(host string, unique ...bool) (*SharedClient, error) {
|
||||
u = unique[0]
|
||||
}
|
||||
|
||||
host := cfg.URL
|
||||
|
||||
if !u {
|
||||
clientMapMu.Lock()
|
||||
defer clientMapMu.Unlock()
|
||||
@@ -154,45 +155,30 @@ func NewClient(host string, unique ...bool) (*SharedClient, error) {
|
||||
opt = []client.Opt{
|
||||
client.WithHost(agent.DockerHost),
|
||||
client.WithHTTPClient(cfg.NewHTTPClient()),
|
||||
versionArg,
|
||||
}
|
||||
addr = "tcp://" + cfg.Addr
|
||||
dial = cfg.DialContext
|
||||
} else {
|
||||
switch host {
|
||||
case "":
|
||||
return nil, errors.New("empty docker host")
|
||||
case common.DockerHostFromEnv:
|
||||
helper, err := connhelper.GetConnectionHelper(host)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("failed to get connection helper")
|
||||
}
|
||||
if helper != nil {
|
||||
opt = []client.Opt{
|
||||
client.WithHostFromEnv(),
|
||||
versionArg,
|
||||
client.WithHost(helper.Host),
|
||||
client.WithDialContext(helper.Dialer),
|
||||
}
|
||||
default:
|
||||
helper, err := connhelper.GetConnectionHelper(host)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("failed to get connection helper")
|
||||
}
|
||||
if helper != nil {
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: helper.Dialer,
|
||||
},
|
||||
}
|
||||
opt = []client.Opt{
|
||||
client.WithHTTPClient(httpClient),
|
||||
client.WithHost(helper.Host),
|
||||
versionArg,
|
||||
client.WithDialContext(helper.Dialer),
|
||||
}
|
||||
} else {
|
||||
opt = []client.Opt{
|
||||
client.WithHost(host),
|
||||
versionArg,
|
||||
}
|
||||
} else {
|
||||
opt = []client.Opt{
|
||||
client.WithHost(host),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.TLS != nil {
|
||||
opt = append(opt, client.WithTLSClientConfig(cfg.TLS.CAFile, cfg.TLS.CertFile, cfg.TLS.KeyFile))
|
||||
}
|
||||
|
||||
client, err := client.New(opt...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -200,6 +186,7 @@ func NewClient(host string, unique ...bool) (*SharedClient, error) {
|
||||
|
||||
c := &SharedClient{
|
||||
Client: client,
|
||||
cfg: cfg,
|
||||
addr: addr,
|
||||
key: host,
|
||||
dial: dial,
|
||||
@@ -236,7 +223,7 @@ func (c *SharedClient) InterceptHTTPClient(intercept httputils.InterceptFunc) {
|
||||
func (c *SharedClient) CloneUnique() *SharedClient {
|
||||
// there will be no error here
|
||||
// since we are using the same host from a valid client.
|
||||
c, _ = NewClient(c.key, true)
|
||||
c, _ = NewClient(c.cfg, true)
|
||||
return c
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ var (
|
||||
ErrNoNetwork = errors.New("no network found")
|
||||
)
|
||||
|
||||
func FromDocker(c *container.Summary, dockerHost string) (res *types.Container) {
|
||||
func FromDocker(c *container.Summary, dockerCfg types.DockerProviderConfig) (res *types.Container) {
|
||||
actualLabels := maps.Clone(c.Labels)
|
||||
|
||||
_, isExplicit := c.Labels[LabelAliases]
|
||||
@@ -47,7 +47,7 @@ func FromDocker(c *container.Summary, dockerHost string) (res *types.Container)
|
||||
|
||||
isExcluded, _ := strconv.ParseBool(helper.getDeleteLabel(LabelExclude))
|
||||
res = &types.Container{
|
||||
DockerHost: dockerHost,
|
||||
DockerCfg: dockerCfg,
|
||||
Image: helper.parseImage(),
|
||||
ContainerName: helper.getName(),
|
||||
ContainerID: c.ID,
|
||||
@@ -69,11 +69,11 @@ func FromDocker(c *container.Summary, dockerHost string) (res *types.Container)
|
||||
State: c.State,
|
||||
}
|
||||
|
||||
if agent.IsDockerHostAgent(dockerHost) {
|
||||
if agent.IsDockerHostAgent(dockerCfg.URL) {
|
||||
var ok bool
|
||||
res.Agent, ok = agent.GetAgent(dockerHost)
|
||||
res.Agent, ok = agent.GetAgent(dockerCfg.URL)
|
||||
if !ok {
|
||||
addError(res, fmt.Errorf("agent %q not found", dockerHost))
|
||||
addError(res, fmt.Errorf("agent %q not found", dockerCfg.URL))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func IsBlacklisted(c *types.Container) bool {
|
||||
}
|
||||
|
||||
func UpdatePorts(c *types.Container) error {
|
||||
dockerClient, err := NewClient(c.DockerHost)
|
||||
dockerClient, err := NewClient(c.DockerCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -163,14 +163,14 @@ func isDatabase(c *types.Container) bool {
|
||||
}
|
||||
|
||||
func isLocal(c *types.Container) bool {
|
||||
if strings.HasPrefix(c.DockerHost, "unix://") {
|
||||
if strings.HasPrefix(c.DockerCfg.URL, "unix://") {
|
||||
return true
|
||||
}
|
||||
// treat it as local if the docker host is the same as the environment variable
|
||||
if c.DockerHost == EnvDockerHost {
|
||||
if c.DockerCfg.URL == EnvDockerHost {
|
||||
return true
|
||||
}
|
||||
url, err := url.Parse(c.DockerHost)
|
||||
url, err := url.Parse(c.DockerCfg.URL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -190,7 +190,7 @@ func setPublicHostname(c *types.Container) {
|
||||
c.PublicHostname = "127.0.0.1"
|
||||
return
|
||||
}
|
||||
url, err := url.Parse(c.DockerHost)
|
||||
url, err := url.Parse(c.DockerCfg.URL)
|
||||
if err != nil {
|
||||
c.PublicHostname = "127.0.0.1"
|
||||
return
|
||||
@@ -241,9 +241,11 @@ func loadDeleteIdlewatcherLabels(c *types.Container, helper containerHelper) {
|
||||
hasIdleTimeout := false
|
||||
cfg := make(map[string]any, len(idlewatcherLabels))
|
||||
for lbl, key := range idlewatcherLabels {
|
||||
if value := helper.getDeleteLabel(lbl); value != "" {
|
||||
cfg[key] = value
|
||||
value := helper.getDeleteLabel(lbl)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
cfg[key] = value
|
||||
switch lbl {
|
||||
case LabelIdleTimeout:
|
||||
hasIdleTimeout = true
|
||||
@@ -252,14 +254,11 @@ func loadDeleteIdlewatcherLabels(c *types.Container, helper containerHelper) {
|
||||
}
|
||||
}
|
||||
|
||||
// ensure it's deleted from labels
|
||||
helper.getDeleteLabel(LabelDependsOn)
|
||||
|
||||
// set only if idlewatcher is enabled
|
||||
if hasIdleTimeout {
|
||||
idwCfg := new(types.IdlewatcherConfig)
|
||||
idwCfg.Docker = &types.DockerConfig{
|
||||
DockerHost: c.DockerHost,
|
||||
DockerCfg: c.DockerCfg,
|
||||
ContainerID: c.ContainerID,
|
||||
ContainerName: c.ContainerName,
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ func (c containerHelper) getAliases() []string {
|
||||
}
|
||||
|
||||
func (c containerHelper) getName() string {
|
||||
if len(c.Names) == 0 { // Why did it happen? Every container must have a name.
|
||||
return ""
|
||||
}
|
||||
return strings.TrimPrefix(c.Names[0], "/")
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
@@ -36,7 +37,7 @@ func TestContainerExplicit(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := FromDocker(&container.Summary{Names: []string{"test"}, State: "test", Labels: tt.labels}, "")
|
||||
c := FromDocker(&container.Summary{Names: []string{"test"}, State: "test", Labels: tt.labels}, types.DockerProviderConfig{})
|
||||
expect.Equal(t, c.IsExplicit, tt.isExplicit)
|
||||
})
|
||||
}
|
||||
@@ -73,7 +74,7 @@ func TestContainerHostNetworkMode(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := FromDocker(tt.container, "")
|
||||
c := FromDocker(tt.container, types.DockerProviderConfig{})
|
||||
expect.Equal(t, c.IsHostNetworkMode, tt.isHostNetworkMode)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,18 +2,19 @@ package docker
|
||||
|
||||
import (
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
)
|
||||
|
||||
var idDockerHostMap = xsync.NewMap[string, string](xsync.WithPresize(100))
|
||||
var idDockerCfgMap = xsync.NewMap[string, types.DockerProviderConfig](xsync.WithPresize(100))
|
||||
|
||||
func GetDockerHostByContainerID(id string) (string, bool) {
|
||||
return idDockerHostMap.Load(id)
|
||||
func GetDockerCfgByContainerID(id string) (types.DockerProviderConfig, bool) {
|
||||
return idDockerCfgMap.Load(id)
|
||||
}
|
||||
|
||||
func SetDockerHostByContainerID(id, host string) {
|
||||
idDockerHostMap.Store(id, host)
|
||||
func SetDockerCfgByContainerID(id string, cfg types.DockerProviderConfig) {
|
||||
idDockerCfgMap.Store(id, cfg)
|
||||
}
|
||||
|
||||
func DeleteDockerHostByContainerID(id string) {
|
||||
idDockerHostMap.Delete(id)
|
||||
func DeleteDockerCfgByContainerID(id string) {
|
||||
idDockerCfgMap.Delete(id)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
)
|
||||
|
||||
var listOptions = client.ContainerListOptions{
|
||||
@@ -19,8 +20,8 @@ var listOptions = client.ContainerListOptions{
|
||||
All: true,
|
||||
}
|
||||
|
||||
func ListContainers(ctx context.Context, clientHost string) ([]container.Summary, error) {
|
||||
dockerClient, err := NewClient(clientHost)
|
||||
func ListContainers(ctx context.Context, dockerCfg types.DockerProviderConfig) ([]container.Summary, error) {
|
||||
dockerClient, err := NewClient(dockerCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -146,11 +146,18 @@ func findRouteAnyDomain(host string) types.HTTPRoute {
|
||||
if r, ok := routes.HTTP.Get(host); ok {
|
||||
return r
|
||||
}
|
||||
// try striping the trailing :port from the host
|
||||
if before, _, ok := strings.Cut(host, ":"); ok {
|
||||
if r, ok := routes.HTTP.Get(before); ok {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findRouteByDomains(domains []string) func(host string) types.HTTPRoute {
|
||||
return func(host string) types.HTTPRoute {
|
||||
host, _, _ = strings.Cut(host, ":") // strip the trailing :port
|
||||
for _, domain := range domains {
|
||||
if target, ok := strings.CutSuffix(host, domain); ok {
|
||||
if r, ok := routes.HTTP.Get(target); ok {
|
||||
|
||||
@@ -128,3 +128,47 @@ func TestFindRouteByDomainsExactMatch(t *testing.T) {
|
||||
|
||||
run(t, tests, testsNoMatch)
|
||||
}
|
||||
|
||||
func TestFindRouteWithPort(t *testing.T) {
|
||||
t.Run("AnyDomain", func(t *testing.T) {
|
||||
addRoute("app1")
|
||||
addRoute("app2.com")
|
||||
|
||||
tests := []string{
|
||||
"app1:8080",
|
||||
"app1.domain.com:8080",
|
||||
"app2.com:8080",
|
||||
}
|
||||
testsNoMatch := []string{
|
||||
"app11",
|
||||
"app2.co",
|
||||
"app2.co:8080",
|
||||
}
|
||||
run(t, tests, testsNoMatch)
|
||||
})
|
||||
|
||||
t.Run("ByDomains", func(t *testing.T) {
|
||||
ep.SetFindRouteDomains([]string{
|
||||
".domain.com",
|
||||
})
|
||||
addRoute("app1")
|
||||
addRoute("app2")
|
||||
addRoute("app3.domain.com")
|
||||
|
||||
tests := []string{
|
||||
"app1.domain.com:8080",
|
||||
"app2:8080", // exact match fallback
|
||||
"app3.domain.com:8080",
|
||||
}
|
||||
testsNoMatch := []string{
|
||||
"app11",
|
||||
"app1.domain.co",
|
||||
"app1.domain.co:8080",
|
||||
"app2.co",
|
||||
"app2.co:8080",
|
||||
"app3.domain.co",
|
||||
"app3.domain.co:8080",
|
||||
}
|
||||
run(t, tests, testsNoMatch)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/yusing/ds/ordered"
|
||||
"github.com/yusing/godoxy/internal/homepage/widgets"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -146,13 +147,13 @@ func (c *Category) sortByClicks() {
|
||||
return 1
|
||||
}
|
||||
// fallback to alphabetical
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
return strings.Compare(strutils.Title(a.Name), strutils.Title(b.Name))
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Category) sortByAlphabetical() {
|
||||
slices.SortStableFunc(c.Items, func(a, b *Item) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
return strings.Compare(strutils.Title(a.Name), strutils.Title(b.Name))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
"github.com/yusing/goutils/intern"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
"github.com/yusing/goutils/synk"
|
||||
"github.com/yusing/goutils/task"
|
||||
@@ -402,7 +403,7 @@ func UpdateSelfhstIcons(m IconMap) error {
|
||||
}
|
||||
icon := &IconMeta{
|
||||
DisplayName: item.Name,
|
||||
Tag: tag,
|
||||
Tag: intern.Make(tag).Value(),
|
||||
SVG: item.SVG == "Yes",
|
||||
PNG: item.PNG == "Yes",
|
||||
WebP: item.WebP == "Yes",
|
||||
|
||||
73
internal/idlewatcher/handle_http_debug.go
Normal file
73
internal/idlewatcher/handle_http_debug.go
Normal file
@@ -0,0 +1,73 @@
|
||||
//go:build !production
|
||||
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
)
|
||||
|
||||
func DebugHandler(rw http.ResponseWriter, r *http.Request) {
|
||||
w := &Watcher{
|
||||
eventChs: xsync.NewMap[chan *WakeEvent, struct{}](),
|
||||
cfg: &types.IdlewatcherConfig{
|
||||
IdlewatcherProviderConfig: types.IdlewatcherProviderConfig{
|
||||
Docker: &types.DockerConfig{
|
||||
ContainerName: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
switch r.URL.Path {
|
||||
case idlewatcher.LoadingPageCSSPath:
|
||||
serveStaticContent(rw, http.StatusOK, "text/css", cssBytes)
|
||||
case idlewatcher.LoadingPageJSPath:
|
||||
serveStaticContent(rw, http.StatusOK, "application/javascript", jsBytes)
|
||||
case idlewatcher.WakeEventsPath:
|
||||
go w.handleWakeEventsSSE(rw, r)
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
events := []WakeEventType{
|
||||
WakeEventStarting,
|
||||
WakeEventWakingDep,
|
||||
WakeEventDepReady,
|
||||
WakeEventContainerWoke,
|
||||
WakeEventWaitingReady,
|
||||
WakeEventError,
|
||||
WakeEventReady,
|
||||
}
|
||||
messages := []string{
|
||||
"Starting",
|
||||
"Waking dependency",
|
||||
"Dependency ready",
|
||||
"Container woke",
|
||||
"Waiting for ready",
|
||||
"Error",
|
||||
"Ready",
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
idx := rand.IntN(len(events))
|
||||
for ch := range w.eventChs.Range {
|
||||
ch <- &WakeEvent{
|
||||
Type: string(events[idx]),
|
||||
Message: messages[idx],
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
w.writeLoadingPage(rw)
|
||||
}
|
||||
}
|
||||
@@ -20,14 +20,14 @@ type DockerProvider struct {
|
||||
|
||||
var startOptions = client.ContainerStartOptions{}
|
||||
|
||||
func NewDockerProvider(dockerHost, containerID string) (idlewatcher.Provider, error) {
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
func NewDockerProvider(dockerCfg types.DockerProviderConfig, containerID string) (idlewatcher.Provider, error) {
|
||||
client, err := docker.NewClient(dockerCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DockerProvider{
|
||||
client: client,
|
||||
watcher: watcher.NewDockerWatcher(dockerHost),
|
||||
watcher: watcher.NewDockerWatcher(dockerCfg),
|
||||
containerID: containerID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ package idlewatcher
|
||||
|
||||
const (
|
||||
FavIconPath = "/favicon.ico"
|
||||
LoadingPageCSSPath = "/$godoxy/style.css"
|
||||
LoadingPageJSPath = "/$godoxy/loading.js"
|
||||
WakeEventsPath = "/$godoxy/wake-events"
|
||||
PathPrefix = "/$godoxy/"
|
||||
LoadingPageCSSPath = PathPrefix + "style.css"
|
||||
LoadingPageJSPath = PathPrefix + "loading.js"
|
||||
WakeEventsPath = PathPrefix + "wake-events"
|
||||
)
|
||||
|
||||
@@ -200,7 +200,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
|
||||
depCfg = new(types.IdlewatcherConfig)
|
||||
depCfg.IdlewatcherConfigBase = cfg.IdlewatcherConfigBase
|
||||
depCfg.IdleTimeout = neverTick // disable auto sleep for dependencies
|
||||
} else if depCfg.IdleTimeout > 0 {
|
||||
} else if depCfg.IdleTimeout > 0 && depCfg.IdleTimeout != neverTick {
|
||||
depErrors.Addf("dependency %q has positive idle timeout %s", dep, depCfg.IdleTimeout)
|
||||
continue
|
||||
}
|
||||
@@ -209,7 +209,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
|
||||
depCont := depRoute.ContainerInfo()
|
||||
if depCont != nil {
|
||||
depCfg.Docker = &types.DockerConfig{
|
||||
DockerHost: depCont.DockerHost,
|
||||
DockerCfg: depCont.DockerCfg,
|
||||
ContainerID: depCont.ContainerID,
|
||||
ContainerName: depCont.ContainerName,
|
||||
}
|
||||
@@ -256,7 +256,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
|
||||
var kind string
|
||||
switch {
|
||||
case cfg.Docker != nil:
|
||||
p, err = provider.NewDockerProvider(cfg.Docker.DockerHost, cfg.Docker.ContainerID)
|
||||
p, err = provider.NewDockerProvider(cfg.Docker.DockerCfg, cfg.Docker.ContainerID)
|
||||
kind = "docker"
|
||||
default:
|
||||
p, err = provider.NewProxmoxProvider(cfg.Proxmox.Node, cfg.Proxmox.VMID)
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
"github.com/shirou/gopsutil/v4/sensors"
|
||||
"github.com/yusing/goutils/num"
|
||||
"github.com/yusing/goutils/intern"
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
@@ -22,32 +22,26 @@ var (
|
||||
Timestamp: 123456,
|
||||
CPUAverage: &cpuAvg,
|
||||
Memory: mem.VirtualMemoryStat{
|
||||
Total: 16000000000,
|
||||
Available: 8000000000,
|
||||
Used: 8000000000,
|
||||
UsedPercent: 50.0,
|
||||
Available: 8000000000,
|
||||
Used: 8000000000,
|
||||
},
|
||||
Disks: map[string]disk.UsageStat{
|
||||
"sda": {
|
||||
Path: "/",
|
||||
Fstype: "ext4",
|
||||
Total: 500000000000,
|
||||
Free: 250000000000,
|
||||
Used: 250000000000,
|
||||
UsedPercent: 50.0,
|
||||
Path: intern.Make("/"),
|
||||
Fstype: intern.Make("ext4"),
|
||||
Free: 250000000000,
|
||||
Used: 250000000000,
|
||||
},
|
||||
"nvme0n1": {
|
||||
Path: "/",
|
||||
Fstype: "zfs",
|
||||
Total: 500000000000,
|
||||
Free: 250000000000,
|
||||
Used: 250000000000,
|
||||
UsedPercent: 50.0,
|
||||
Path: intern.Make("/"),
|
||||
Fstype: intern.Make("zfs"),
|
||||
Free: 250000000000,
|
||||
Used: 250000000000,
|
||||
},
|
||||
},
|
||||
DisksIO: map[string]*disk.IOCountersStat{
|
||||
"media": {
|
||||
Name: "media",
|
||||
Name: intern.Make("media"),
|
||||
ReadBytes: 1000000,
|
||||
WriteBytes: 2000000,
|
||||
IOCountersStatExtra: disk.IOCountersStatExtra{
|
||||
@@ -57,7 +51,7 @@ var (
|
||||
},
|
||||
},
|
||||
"nvme0n1": {
|
||||
Name: "nvme0n1",
|
||||
Name: intern.Make("nvme0n1"),
|
||||
ReadBytes: 1000000,
|
||||
WriteBytes: 2000000,
|
||||
IOCountersStatExtra: disk.IOCountersStatExtra{
|
||||
@@ -75,16 +69,12 @@ var (
|
||||
},
|
||||
Sensors: []sensors.TemperatureStat{
|
||||
{
|
||||
SensorKey: "cpu_temp",
|
||||
Temperature: num.NewPercentage(30.0),
|
||||
High: num.NewPercentage(40.0),
|
||||
Critical: num.NewPercentage(50.0),
|
||||
SensorKey: intern.Make("cpu_temp"),
|
||||
Temperature: 30.0,
|
||||
},
|
||||
{
|
||||
SensorKey: "gpu_temp",
|
||||
Temperature: num.NewPercentage(40.0),
|
||||
High: num.NewPercentage(50.0),
|
||||
Critical: num.NewPercentage(60.0),
|
||||
SensorKey: intern.Make("gpu_temp"),
|
||||
Temperature: 40.0,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -141,10 +131,10 @@ func TestSerialize(t *testing.T) {
|
||||
expect.NoError(t, err)
|
||||
var v []map[string]any
|
||||
expect.NoError(t, json.Unmarshal(s, &v))
|
||||
expect.Equal(t, len(v), len(result.Entries))
|
||||
expect.Equal(t, len(v), len(result))
|
||||
for i, m := range v {
|
||||
for k, v := range m {
|
||||
vv := reflect.ValueOf(result.Entries[i][k])
|
||||
vv := reflect.ValueOf(result[i][k])
|
||||
expect.Equal(t, reflect.ValueOf(v).Interface(), vv.Interface())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
statequery "github.com/yusing/godoxy/internal/config/query"
|
||||
"github.com/yusing/godoxy/internal/metrics/period"
|
||||
metricsutils "github.com/yusing/godoxy/internal/metrics/utils"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
@@ -34,6 +33,7 @@ type (
|
||||
Idle float32 `json:"idle"`
|
||||
AvgLatency float32 `json:"avg_latency"`
|
||||
IsDocker bool `json:"is_docker"`
|
||||
IsExcluded bool `json:"is_excluded"`
|
||||
CurrentStatus types.HealthStatus `json:"current_status" swaggertype:"string" enums:"healthy,unhealthy,unknown,napping,starting"`
|
||||
Statuses []Status `json:"statuses"`
|
||||
} // @name RouteUptimeAggregate
|
||||
@@ -133,7 +133,7 @@ func (rs RouteStatuses) aggregate(limit int, offset int) Aggregated {
|
||||
r, ok := routes.Get(alias)
|
||||
if !ok {
|
||||
// also search for excluded routes
|
||||
r = statequery.SearchRoute(alias)
|
||||
r, ok = routes.Excluded.Get(alias)
|
||||
}
|
||||
if r != nil {
|
||||
displayName = r.DisplayName()
|
||||
@@ -157,6 +157,7 @@ func (rs RouteStatuses) aggregate(limit int, offset int) Aggregated {
|
||||
CurrentStatus: status,
|
||||
Statuses: statuses,
|
||||
IsDocker: r != nil && r.IsDocker(),
|
||||
IsExcluded: r == nil || r.ShouldExclude(),
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -117,7 +117,7 @@ func (amw *oidcMiddleware) before(w http.ResponseWriter, r *http.Request) (proce
|
||||
case errors.Is(err, auth.ErrMissingOAuthToken):
|
||||
amw.auth.HandleAuth(w, r)
|
||||
default:
|
||||
auth.WriteBlockPage(w, http.StatusForbidden, err.Error(), auth.OIDCLogoutPath)
|
||||
auth.WriteBlockPage(w, http.StatusForbidden, err.Error(), "Logout", auth.OIDCLogoutPath)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package route
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
@@ -27,8 +28,25 @@ type (
|
||||
|
||||
var _ types.FileServerRoute = (*FileServer)(nil)
|
||||
|
||||
func handler(root string) http.Handler {
|
||||
return http.FileServer(http.Dir(root))
|
||||
func handler(root string, spa bool, index string) http.Handler {
|
||||
if !spa {
|
||||
return http.FileServer(http.Dir(root))
|
||||
}
|
||||
indexPath := filepath.Join(root, index)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
urlPath := path.Clean(r.URL.Path)
|
||||
if urlPath == "/" {
|
||||
http.ServeFile(w, r, indexPath)
|
||||
return
|
||||
}
|
||||
fullPath := filepath.Join(root, filepath.FromSlash(urlPath))
|
||||
stat, err := os.Stat(fullPath)
|
||||
if err == nil && !stat.IsDir() {
|
||||
http.ServeFile(w, r, fullPath)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, indexPath)
|
||||
})
|
||||
}
|
||||
|
||||
func NewFileServer(base *Route) (*FileServer, gperr.Error) {
|
||||
@@ -39,7 +57,12 @@ func NewFileServer(base *Route) (*FileServer, gperr.Error) {
|
||||
return nil, gperr.New("`root` must be an absolute path")
|
||||
}
|
||||
|
||||
s.handler = handler(s.Root)
|
||||
if s.Index == "" {
|
||||
s.Index = "/index.html"
|
||||
} else if s.Index[0] != '/' {
|
||||
s.Index = "/" + s.Index
|
||||
}
|
||||
s.handler = handler(s.Root, s.SPA, s.Index)
|
||||
|
||||
if len(s.Middlewares) > 0 {
|
||||
mid, err := middleware.BuildMiddlewareFromMap(s.Alias, s.Middlewares)
|
||||
|
||||
@@ -18,8 +18,9 @@ import (
|
||||
)
|
||||
|
||||
type DockerProvider struct {
|
||||
name, dockerHost string
|
||||
l zerolog.Logger
|
||||
name string
|
||||
dockerCfg types.DockerProviderConfig
|
||||
l zerolog.Logger
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -29,10 +30,10 @@ const (
|
||||
|
||||
var ErrAliasRefIndexOutOfRange = gperr.New("index out of range")
|
||||
|
||||
func DockerProviderImpl(name, dockerHost string) ProviderImpl {
|
||||
func DockerProviderImpl(name string, dockerCfg types.DockerProviderConfig) ProviderImpl {
|
||||
return &DockerProvider{
|
||||
name,
|
||||
dockerHost,
|
||||
dockerCfg,
|
||||
log.With().Str("type", "docker").Str("name", name).Logger(),
|
||||
}
|
||||
}
|
||||
@@ -54,14 +55,14 @@ func (p *DockerProvider) Logger() *zerolog.Logger {
|
||||
}
|
||||
|
||||
func (p *DockerProvider) NewWatcher() watcher.Watcher {
|
||||
return watcher.NewDockerWatcher(p.dockerHost)
|
||||
return watcher.NewDockerWatcher(p.dockerCfg)
|
||||
}
|
||||
|
||||
func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
containers, err := docker.ListContainers(ctx, p.dockerHost)
|
||||
containers, err := docker.ListContainers(ctx, p.dockerCfg)
|
||||
if err != nil {
|
||||
return nil, gperr.Wrap(err)
|
||||
}
|
||||
@@ -70,7 +71,7 @@ func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) {
|
||||
routes := make(route.Routes)
|
||||
|
||||
for _, c := range containers {
|
||||
container := docker.FromDocker(&c, p.dockerHost)
|
||||
container := docker.FromDocker(&c, p.dockerCfg)
|
||||
|
||||
if container.Errors != nil {
|
||||
errs.Add(container.Errors)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
|
||||
_ "embed"
|
||||
@@ -28,7 +29,7 @@ func TestParseDockerLabels(t *testing.T) {
|
||||
Ports: []container.PortSummary{
|
||||
{Type: "tcp", PrivatePort: 1234, PublicPort: 1234},
|
||||
},
|
||||
}, "/var/run/docker.sock"),
|
||||
}, types.DockerProviderConfig{URL: "unix:///var/run/docker.sock"}),
|
||||
)
|
||||
expect.NoError(t, err)
|
||||
expect.True(t, routes.Contains("app"))
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
D "github.com/yusing/godoxy/internal/docker"
|
||||
"github.com/yusing/godoxy/internal/route"
|
||||
routeTypes "github.com/yusing/godoxy/internal/route/types"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
@@ -31,7 +32,7 @@ func makeRoutes(cont *container.Summary, dockerHostIP ...string) route.Routes {
|
||||
}
|
||||
cont.ID = "test"
|
||||
p.name = "test"
|
||||
routes := expect.Must(p.routesFromContainerLabels(D.FromDocker(cont, host)))
|
||||
routes := expect.Must(p.routesFromContainerLabels(D.FromDocker(cont, types.DockerProviderConfig{URL: host})))
|
||||
for _, r := range routes {
|
||||
r.Finalize()
|
||||
}
|
||||
@@ -39,7 +40,7 @@ func makeRoutes(cont *container.Summary, dockerHostIP ...string) route.Routes {
|
||||
}
|
||||
|
||||
func TestExplicitOnly(t *testing.T) {
|
||||
p := NewDockerProvider("a!", "")
|
||||
p := NewDockerProvider("a!", types.DockerProviderConfig{})
|
||||
expect.True(t, p.IsExplicitOnly())
|
||||
}
|
||||
|
||||
@@ -199,7 +200,7 @@ func TestApplyLabelWithRefIndexError(t *testing.T) {
|
||||
"proxy.*.port": "4444",
|
||||
"proxy.#4.scheme": "https",
|
||||
},
|
||||
}, "")
|
||||
}, types.DockerProviderConfig{})
|
||||
var p DockerProvider
|
||||
_, err := p.routesFromContainerLabels(c)
|
||||
expect.ErrorIs(t, ErrAliasRefIndexOutOfRange, err)
|
||||
@@ -211,7 +212,7 @@ func TestApplyLabelWithRefIndexError(t *testing.T) {
|
||||
D.LabelAliases: "a,b",
|
||||
"proxy.#0.host": "localhost",
|
||||
},
|
||||
}, "")
|
||||
}, types.DockerProviderConfig{})
|
||||
_, err = p.routesFromContainerLabels(c)
|
||||
expect.ErrorIs(t, ErrAliasRefIndexOutOfRange, err)
|
||||
}
|
||||
|
||||
@@ -8,17 +8,14 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
"github.com/yusing/godoxy/internal/route"
|
||||
provider "github.com/yusing/godoxy/internal/route/provider/types"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
W "github.com/yusing/godoxy/internal/watcher"
|
||||
"github.com/yusing/godoxy/internal/watcher/events"
|
||||
"github.com/yusing/goutils/env"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
"github.com/yusing/goutils/task"
|
||||
)
|
||||
@@ -69,13 +66,9 @@ func NewFileProvider(filename string) (p *Provider, err error) {
|
||||
return p, err
|
||||
}
|
||||
|
||||
func NewDockerProvider(name string, dockerHost string) *Provider {
|
||||
if dockerHost == common.DockerHostFromEnv {
|
||||
dockerHost = env.GetEnvString("DOCKER_HOST", client.DefaultDockerHost)
|
||||
}
|
||||
|
||||
func NewDockerProvider(name string, dockerCfg types.DockerProviderConfig) *Provider {
|
||||
p := newProvider(provider.ProviderTypeDocker)
|
||||
p.ProviderImpl = DockerProviderImpl(name, dockerHost)
|
||||
p.ProviderImpl = DockerProviderImpl(name, dockerCfg)
|
||||
p.watcher = p.NewWatcher()
|
||||
return p
|
||||
}
|
||||
@@ -84,7 +77,9 @@ func NewAgentProvider(cfg *agent.AgentConfig) *Provider {
|
||||
p := newProvider(provider.ProviderTypeAgent)
|
||||
agent := &AgentProvider{
|
||||
AgentConfig: cfg,
|
||||
docker: DockerProviderImpl(cfg.Name, cfg.FakeDockerHost()),
|
||||
docker: DockerProviderImpl(cfg.Name, types.DockerProviderConfig{
|
||||
URL: cfg.FakeDockerHost(),
|
||||
}),
|
||||
}
|
||||
p.ProviderImpl = agent
|
||||
p.watcher = p.NewWatcher()
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
route "github.com/yusing/godoxy/internal/route/types"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
"github.com/yusing/godoxy/internal/watcher/health/monitor"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
@@ -60,6 +61,28 @@ func NewReverseProxyRoute(base *Route) (*ReveseProxyRoute, gperr.Error) {
|
||||
service := base.Name()
|
||||
rp := reverseproxy.NewReverseProxy(service, &proxyURL.URL, trans)
|
||||
|
||||
scheme := base.Scheme
|
||||
retried := false
|
||||
retryLock := sync.Mutex{}
|
||||
rp.OnSchemeMisMatch = func() (retry bool) { // switch scheme and retry
|
||||
retryLock.Lock()
|
||||
defer retryLock.Unlock()
|
||||
|
||||
if retried {
|
||||
return false
|
||||
}
|
||||
|
||||
retried = true
|
||||
|
||||
if scheme == route.SchemeHTTP {
|
||||
rp.TargetURL.Scheme = "https"
|
||||
} else {
|
||||
rp.TargetURL.Scheme = "http"
|
||||
}
|
||||
rp.Info().Msgf("scheme mismatch detected, retrying with %s", rp.TargetURL.Scheme)
|
||||
return true
|
||||
}
|
||||
|
||||
if len(base.Middlewares) > 0 {
|
||||
err := middleware.PatchReverseProxy(rp, base.Middlewares)
|
||||
if err != nil {
|
||||
|
||||
@@ -24,12 +24,14 @@ import (
|
||||
"github.com/yusing/godoxy/internal/proxmox"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
"github.com/yusing/godoxy/internal/watcher/health/monitor"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
"github.com/yusing/goutils/task"
|
||||
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
"github.com/yusing/godoxy/internal/route/rules"
|
||||
rulepresets "github.com/yusing/godoxy/internal/route/rules/presets"
|
||||
route "github.com/yusing/godoxy/internal/route/types"
|
||||
@@ -44,7 +46,10 @@ type (
|
||||
Scheme route.Scheme `json:"scheme,omitempty" swaggertype:"string" enums:"http,https,tcp,udp,fileserver"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Port route.Port `json:"port"`
|
||||
Root string `json:"root,omitempty"`
|
||||
|
||||
Root string `json:"root,omitempty"`
|
||||
SPA bool `json:"spa,omitempty"` // Single-page app mode: serves index for non-existent paths
|
||||
Index string `json:"index,omitempty"` // Index file to serve for single-page app mode
|
||||
|
||||
route.HTTPConfig
|
||||
PathPatterns []string `json:"path_patterns,omitempty" extensions:"x-nullable"`
|
||||
@@ -387,22 +392,31 @@ func (r *Route) start(parent task.Parent) gperr.Error {
|
||||
}
|
||||
|
||||
if cont := r.ContainerInfo(); cont != nil {
|
||||
docker.SetDockerHostByContainerID(cont.ContainerID, cont.DockerHost)
|
||||
docker.SetDockerCfgByContainerID(cont.ContainerID, cont.DockerCfg)
|
||||
}
|
||||
|
||||
if !excluded {
|
||||
if err := r.impl.Start(parent); err != nil {
|
||||
return err
|
||||
}
|
||||
} else { // required by idlewatcher
|
||||
r.task = parent.Subtask("excluded."+r.Name(), false)
|
||||
} else {
|
||||
r.task = parent.Subtask("excluded."+r.Name(), true)
|
||||
routes.Excluded.Add(r.impl)
|
||||
r.task.OnCancel("remove_route_from_excluded", func() {
|
||||
routes.Excluded.Del(r.impl)
|
||||
})
|
||||
if r.UseHealthCheck() {
|
||||
r.HealthMon = monitor.NewMonitor(r.impl)
|
||||
err := r.HealthMon.Start(r.task)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Route) Finish(reason any) {
|
||||
if cont := r.ContainerInfo(); cont != nil {
|
||||
docker.DeleteDockerHostByContainerID(cont.ContainerID)
|
||||
docker.DeleteDockerCfgByContainerID(cont.ContainerID)
|
||||
}
|
||||
r.FinishAndWait(reason)
|
||||
}
|
||||
|
||||
@@ -17,17 +17,23 @@ type HealthInfoWithoutDetail struct {
|
||||
Latency time.Duration `json:"latency" swaggertype:"number"` // latency in microseconds
|
||||
} // @name HealthInfoWithoutDetail
|
||||
|
||||
// GetHealthInfo returns a map of route name to health info.
|
||||
//
|
||||
// The health info is for all routes, including excluded routes.
|
||||
func GetHealthInfo() map[string]HealthInfo {
|
||||
healthMap := make(map[string]HealthInfo, NumRoutes())
|
||||
for r := range Iter {
|
||||
healthMap := make(map[string]HealthInfo, NumAllRoutes())
|
||||
for r := range IterAll {
|
||||
healthMap[r.Name()] = getHealthInfo(r)
|
||||
}
|
||||
return healthMap
|
||||
}
|
||||
|
||||
// GetHealthInfoWithoutDetail returns a map of route name to health info without detail.
|
||||
//
|
||||
// The health info is for all routes, including excluded routes.
|
||||
func GetHealthInfoWithoutDetail() map[string]HealthInfoWithoutDetail {
|
||||
healthMap := make(map[string]HealthInfoWithoutDetail, NumRoutes())
|
||||
for r := range Iter {
|
||||
healthMap := make(map[string]HealthInfoWithoutDetail, NumAllRoutes())
|
||||
for r := range IterAll {
|
||||
healthMap[r.Name()] = getHealthInfoWithoutDetail(r)
|
||||
}
|
||||
return healthMap
|
||||
@@ -67,9 +73,12 @@ func getHealthInfoWithoutDetail(r types.Route) HealthInfoWithoutDetail {
|
||||
}
|
||||
}
|
||||
|
||||
// ByProvider returns a map of provider name to routes.
|
||||
//
|
||||
// The routes are all routes, including excluded routes.
|
||||
func ByProvider() map[string][]types.Route {
|
||||
rts := make(map[string][]types.Route)
|
||||
for r := range Iter {
|
||||
for r := range IterAll {
|
||||
rts[r.ProviderName()] = append(rts[r.ProviderName()], r)
|
||||
}
|
||||
return rts
|
||||
|
||||
@@ -8,9 +8,11 @@ import (
|
||||
var (
|
||||
HTTP = pool.New[types.HTTPRoute]("http_routes")
|
||||
Stream = pool.New[types.StreamRoute]("stream_routes")
|
||||
|
||||
Excluded = pool.New[types.Route]("excluded_routes")
|
||||
)
|
||||
|
||||
func Iter(yield func(r types.Route) bool) {
|
||||
func IterActive(yield func(r types.Route) bool) {
|
||||
for _, r := range HTTP.Iter {
|
||||
if !yield(r) {
|
||||
break
|
||||
@@ -23,26 +25,36 @@ func Iter(yield func(r types.Route) bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func IterKV(yield func(alias string, r types.Route) bool) {
|
||||
for k, r := range HTTP.Iter {
|
||||
if !yield(k, r) {
|
||||
func IterAll(yield func(r types.Route) bool) {
|
||||
for _, r := range HTTP.Iter {
|
||||
if !yield(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for k, r := range Stream.Iter {
|
||||
if !yield(k, r) {
|
||||
for _, r := range Stream.Iter {
|
||||
if !yield(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, r := range Excluded.Iter {
|
||||
if !yield(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NumRoutes() int {
|
||||
func NumActiveRoutes() int {
|
||||
return HTTP.Size() + Stream.Size()
|
||||
}
|
||||
|
||||
func NumAllRoutes() int {
|
||||
return HTTP.Size() + Stream.Size() + Excluded.Size()
|
||||
}
|
||||
|
||||
func Clear() {
|
||||
HTTP.Clear()
|
||||
Stream.Clear()
|
||||
Excluded.Clear()
|
||||
}
|
||||
|
||||
func GetHTTPRouteOrExact(alias, host string) (types.HTTPRoute, bool) {
|
||||
@@ -54,6 +66,9 @@ func GetHTTPRouteOrExact(alias, host string) (types.HTTPRoute, bool) {
|
||||
return HTTP.Get(host)
|
||||
}
|
||||
|
||||
// Get returns the route with the given alias.
|
||||
//
|
||||
// It does not return excluded routes.
|
||||
func Get(alias string) (types.Route, bool) {
|
||||
if r, ok := HTTP.Get(alias); ok {
|
||||
return r, true
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
"github.com/yusing/godoxy/internal/notif"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
"github.com/yusing/goutils/http/reverseproxy"
|
||||
@@ -38,6 +39,7 @@ const (
|
||||
CommandServe = "serve"
|
||||
CommandProxy = "proxy"
|
||||
CommandRedirect = "redirect"
|
||||
CommandRoute = "route"
|
||||
CommandError = "error"
|
||||
CommandRequireBasicAuth = "require_basic_auth"
|
||||
CommandSet = "set"
|
||||
@@ -171,6 +173,42 @@ var commands = map[string]struct {
|
||||
})
|
||||
},
|
||||
},
|
||||
CommandRoute: {
|
||||
help: Help{
|
||||
command: CommandRoute,
|
||||
description: makeLines(
|
||||
"Route the request to another route, e.g.:",
|
||||
helpExample(CommandRoute, "route1"),
|
||||
),
|
||||
args: map[string]string{
|
||||
"route": "the route to route to",
|
||||
},
|
||||
},
|
||||
validate: func(args []string) (any, gperr.Error) {
|
||||
if len(args) != 1 {
|
||||
return nil, ErrExpectOneArg
|
||||
}
|
||||
return args[0], nil
|
||||
},
|
||||
build: func(args any) CommandHandler {
|
||||
route := args.(string)
|
||||
return TerminatingCommand(func(w http.ResponseWriter, req *http.Request) error {
|
||||
r, ok := routes.HTTP.Get(route)
|
||||
if !ok {
|
||||
excluded, has := routes.Excluded.Get(route)
|
||||
if has {
|
||||
r, ok = excluded.(types.HTTPRoute)
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
r.ServeHTTP(w, req)
|
||||
} else {
|
||||
http.Error(w, fmt.Sprintf("Route %q not found", route), http.StatusNotFound)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
},
|
||||
},
|
||||
CommandError: {
|
||||
help: Help{
|
||||
command: CommandError,
|
||||
|
||||
@@ -13,10 +13,10 @@ type (
|
||||
|
||||
PortMapping = map[int]container.PortSummary
|
||||
Container struct {
|
||||
DockerHost string `json:"docker_host"`
|
||||
Image *ContainerImage `json:"image"`
|
||||
ContainerName string `json:"container_name"`
|
||||
ContainerID string `json:"container_id"`
|
||||
DockerCfg DockerProviderConfig `json:"docker_cfg"`
|
||||
Image *ContainerImage `json:"image"`
|
||||
ContainerName string `json:"container_name"`
|
||||
ContainerID string `json:"container_id"`
|
||||
|
||||
State container.ContainerState `json:"state"`
|
||||
|
||||
|
||||
88
internal/types/docker_provider_config.go
Normal file
88
internal/types/docker_provider_config.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
"github.com/yusing/goutils/env"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
)
|
||||
|
||||
type DockerProviderConfig struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
TLS *DockerTLSConfig `json:"tls,omitempty"`
|
||||
} // @name DockerProviderConfig
|
||||
|
||||
type DockerProviderConfigDetailed struct {
|
||||
Scheme string `json:"scheme,omitempty" validate:"required,oneof=http https tls"`
|
||||
Host string `json:"host,omitempty" validate:"required,hostname|ip"`
|
||||
Port int `json:"port,omitempty" validate:"required,min=1,max=65535"`
|
||||
TLS *DockerTLSConfig `json:"tls" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type DockerTLSConfig struct {
|
||||
CAFile string `json:"ca_file,omitempty" validate:"required"`
|
||||
CertFile string `json:"cert_file,omitempty" validate:"required_with=KeyFile"`
|
||||
KeyFile string `json:"key_file,omitempty" validate:"required_with=CertFile"`
|
||||
} // @name DockerTLSConfig
|
||||
|
||||
func (cfg *DockerProviderConfig) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(cfg.URL)
|
||||
}
|
||||
|
||||
func (cfg *DockerProviderConfig) Parse(value string) error {
|
||||
if value == common.DockerHostFromEnv {
|
||||
cfg.URL = env.GetEnvString("DOCKER_HOST", "unix:///var/run/docker.sock")
|
||||
return nil
|
||||
}
|
||||
|
||||
u, err := url.Parse(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "http", "https", "tls":
|
||||
default:
|
||||
return fmt.Errorf("invalid scheme: %s", u.Scheme)
|
||||
}
|
||||
|
||||
cfg.URL = u.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *DockerProviderConfig) UnmarshalMap(m map[string]any) gperr.Error {
|
||||
var tmp DockerProviderConfigDetailed
|
||||
var err = serialization.MapUnmarshalValidate(m, &tmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.URL = fmt.Sprintf("%s://%s", tmp.Scheme, net.JoinHostPort(tmp.Host, strconv.Itoa(tmp.Port)))
|
||||
cfg.TLS = tmp.TLS
|
||||
if cfg.TLS != nil {
|
||||
if err := checkFilesOk(cfg.TLS.CAFile, cfg.TLS.CertFile, cfg.TLS.KeyFile); err != nil {
|
||||
return gperr.Wrap(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkFilesOk(files ...string) error {
|
||||
if common.IsTest {
|
||||
return nil
|
||||
}
|
||||
var errs gperr.Builder
|
||||
for _, file := range files {
|
||||
if _, err := os.Stat(file); err != nil {
|
||||
errs.Add(err)
|
||||
}
|
||||
}
|
||||
return errs.Error()
|
||||
}
|
||||
138
internal/types/docker_provider_config_test.go
Normal file
138
internal/types/docker_provider_config_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
)
|
||||
|
||||
func TestDockerProviderConfigUnmarshalMap(t *testing.T) {
|
||||
t.Run("string", func(t *testing.T) {
|
||||
var cfg map[string]*DockerProviderConfig
|
||||
err := serialization.UnmarshalValidateYAML([]byte("test: http://localhost:2375"), &cfg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &DockerProviderConfig{URL: "http://localhost:2375"}, cfg["test"])
|
||||
})
|
||||
|
||||
t.Run("detailed", func(t *testing.T) {
|
||||
var cfg map[string]*DockerProviderConfig
|
||||
err := serialization.UnmarshalValidateYAML([]byte(`
|
||||
test:
|
||||
scheme: http
|
||||
host: localhost
|
||||
port: 2375
|
||||
tls:
|
||||
ca_file: /etc/ssl/ca.crt
|
||||
cert_file: /etc/ssl/cert.crt
|
||||
key_file: /etc/ssl/key.crt`), &cfg)
|
||||
assert.Error(t, err, os.ErrNotExist)
|
||||
assert.Equal(t, &DockerProviderConfig{URL: "http://localhost:2375", TLS: &DockerTLSConfig{CAFile: "/etc/ssl/ca.crt", CertFile: "/etc/ssl/cert.crt", KeyFile: "/etc/ssl/key.crt"}}, cfg["test"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestDockerProviderConfigValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
yamlStr string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "valid url", yamlStr: "test: http://localhost:2375", wantErr: false},
|
||||
{name: "invalid url", yamlStr: "test: ftp://localhost/2375", wantErr: true},
|
||||
{name: "valid scheme", yamlStr: `
|
||||
test:
|
||||
scheme: http
|
||||
host: localhost
|
||||
port: 2375
|
||||
`, wantErr: false},
|
||||
{name: "invalid scheme", yamlStr: `
|
||||
test:
|
||||
scheme: invalid
|
||||
host: localhost
|
||||
port: 2375
|
||||
`, wantErr: true},
|
||||
{name: "valid host (ipv4)", yamlStr: `
|
||||
test:
|
||||
scheme: http
|
||||
host: 127.0.0.1
|
||||
port: 2375
|
||||
`, wantErr: false},
|
||||
{name: "valid host (ipv6)", yamlStr: `
|
||||
test:
|
||||
scheme: http
|
||||
host: ::1
|
||||
port: 2375
|
||||
`, wantErr: false},
|
||||
{name: "valid host (hostname)", yamlStr: `
|
||||
test:
|
||||
scheme: http
|
||||
host: example.com
|
||||
port: 2375
|
||||
`, wantErr: false},
|
||||
{name: "invalid host", yamlStr: `
|
||||
test:
|
||||
scheme: http
|
||||
host: invalid:1234
|
||||
port: 2375
|
||||
`, wantErr: true},
|
||||
{name: "valid port", yamlStr: `
|
||||
test:
|
||||
scheme: http
|
||||
host: localhost
|
||||
port: 2375
|
||||
`, wantErr: false},
|
||||
{name: "invalid port", yamlStr: `
|
||||
test:
|
||||
scheme: http
|
||||
host: localhost
|
||||
port: 65536
|
||||
`, wantErr: true},
|
||||
{name: "valid tls", yamlStr: `
|
||||
test:
|
||||
scheme: tls
|
||||
host: localhost
|
||||
port: 2375
|
||||
tls:
|
||||
ca_file: /etc/ssl/ca.crt
|
||||
cert_file: /etc/ssl/cert.crt
|
||||
key_file: /etc/ssl/key.crt
|
||||
`, wantErr: false},
|
||||
{name: "valid tls (only ca file)", yamlStr: `
|
||||
test:
|
||||
scheme: tls
|
||||
host: localhost
|
||||
port: 2375
|
||||
tls:
|
||||
ca_file: /etc/ssl/ca.crt
|
||||
`, wantErr: false},
|
||||
{name: "invalid tls (missing cert file)", yamlStr: `
|
||||
test:
|
||||
scheme: tls
|
||||
host: localhost
|
||||
port: 2375
|
||||
tls:
|
||||
key_file: /etc/ssl/key.crt
|
||||
`, wantErr: true},
|
||||
{name: "invalid tls (missing key file)", yamlStr: `
|
||||
test:
|
||||
scheme: tls
|
||||
host: localhost
|
||||
port: 2375
|
||||
tls:
|
||||
cert_file: /etc/ssl/cert.crt
|
||||
`, wantErr: true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var cfg map[string]*DockerProviderConfig
|
||||
err := serialization.UnmarshalValidateYAML([]byte(test.yamlStr), &cfg)
|
||||
if test.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -38,9 +38,9 @@ type (
|
||||
ContainerSignal string // @name ContainerSignal
|
||||
|
||||
DockerConfig struct {
|
||||
DockerHost string `json:"docker_host" validate:"required"`
|
||||
ContainerID string `json:"container_id" validate:"required"`
|
||||
ContainerName string `json:"container_name" validate:"required"`
|
||||
DockerCfg DockerProviderConfig `json:"docker_cfg" validate:"required"`
|
||||
ContainerID string `json:"container_id" validate:"required"`
|
||||
ContainerName string `json:"container_name" validate:"required"`
|
||||
} // @name DockerConfig
|
||||
ProxmoxConfig struct {
|
||||
Node string `json:"node" validate:"required"`
|
||||
|
||||
@@ -9,12 +9,15 @@ import (
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
"github.com/yusing/godoxy/internal/watcher/events"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
)
|
||||
|
||||
type (
|
||||
DockerWatcher string
|
||||
DockerWatcher struct {
|
||||
cfg types.DockerProviderConfig
|
||||
}
|
||||
DockerListOptions = client.EventsListOptions
|
||||
DockerFilters = client.Filters
|
||||
)
|
||||
@@ -73,8 +76,10 @@ func DockerFilterContainerNameID(nameOrID string) DockerFilter {
|
||||
return NewDockerFilter("container", nameOrID)
|
||||
}
|
||||
|
||||
func NewDockerWatcher(host string) DockerWatcher {
|
||||
return DockerWatcher(host)
|
||||
func NewDockerWatcher(dockerCfg types.DockerProviderConfig) DockerWatcher {
|
||||
return DockerWatcher{
|
||||
cfg: dockerCfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (w DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan gperr.Error) {
|
||||
@@ -86,7 +91,7 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
|
||||
errCh := make(chan gperr.Error)
|
||||
|
||||
go func() {
|
||||
client, err := docker.NewClient(string(w))
|
||||
client, err := docker.NewClient(w.cfg)
|
||||
if err != nil {
|
||||
errCh <- gperr.Wrap(err, "docker watcher: failed to initialize client")
|
||||
return
|
||||
|
||||
@@ -40,6 +40,8 @@ func NewHTTPHealthMonitor(url *url.URL, config types.HealthCheckConfig) *HTTPHea
|
||||
return mon
|
||||
}
|
||||
|
||||
var userAgent = "GoDoxy/" + version.Get().String()
|
||||
|
||||
func (mon *HTTPHealthMonitor) CheckHealth() (types.HealthCheckResult, error) {
|
||||
req := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
@@ -49,7 +51,7 @@ func (mon *HTTPHealthMonitor) CheckHealth() (types.HealthCheckResult, error) {
|
||||
|
||||
req.SetRequestURI(mon.url.Load().JoinPath(mon.config.Path).String())
|
||||
req.Header.SetMethod(mon.method)
|
||||
req.Header.Set("User-Agent", "GoDoxy/"+version.Get().String())
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
req.Header.Set("Accept", "text/plain,text/html,*/*;q=0.8")
|
||||
req.Header.Set("Accept-Encoding", "identity")
|
||||
req.Header.Set("Cache-Control", "no-cache")
|
||||
|
||||
@@ -33,8 +33,6 @@ type (
|
||||
checkHealth HealthCheckFunc
|
||||
startTime time.Time
|
||||
|
||||
isZeroPort bool
|
||||
|
||||
notifyFunc notif.NotifyFunc
|
||||
numConsecFailures atomic.Int64
|
||||
downNotificationSent atomic.Bool
|
||||
@@ -63,7 +61,7 @@ func NewMonitor(r types.Route) types.HealthMonCheck {
|
||||
}
|
||||
if r.IsDocker() {
|
||||
cont := r.ContainerInfo()
|
||||
client, err := docker.NewClient(cont.DockerHost)
|
||||
client, err := docker.NewClient(cont.DockerCfg)
|
||||
if err != nil {
|
||||
return mon
|
||||
}
|
||||
@@ -74,7 +72,11 @@ func NewMonitor(r types.Route) types.HealthMonCheck {
|
||||
}
|
||||
|
||||
func newMonitor(u *url.URL, cfg types.HealthCheckConfig, healthCheckFunc HealthCheckFunc) *monitor {
|
||||
cfg.ApplyDefaults(config.WorkingState.Load().Value().Defaults.HealthCheck)
|
||||
if state := config.WorkingState.Load(); state != nil {
|
||||
cfg.ApplyDefaults(state.Value().Defaults.HealthCheck)
|
||||
} else {
|
||||
cfg.ApplyDefaults(types.HealthCheckConfig{}) // use defaults from constants
|
||||
}
|
||||
mon := &monitor{
|
||||
config: cfg,
|
||||
checkHealth: healthCheckFunc,
|
||||
@@ -87,13 +89,6 @@ func newMonitor(u *url.URL, cfg types.HealthCheckConfig, healthCheckFunc HealthC
|
||||
mon.url.Store(u)
|
||||
mon.status.Store(types.StatusHealthy)
|
||||
mon.lastResult.Store(types.HealthCheckResult{Healthy: true, Detail: "started"})
|
||||
|
||||
port := u.Port()
|
||||
mon.isZeroPort = port == "" || port == "0"
|
||||
if mon.isZeroPort {
|
||||
mon.status.Store(types.StatusUnknown)
|
||||
mon.lastResult.Store(types.HealthCheckResult{Healthy: false, Detail: "no port detected"})
|
||||
}
|
||||
return mon
|
||||
}
|
||||
|
||||
@@ -115,10 +110,6 @@ 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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user