Skip to content

Commit de0467b

Browse files
committed
feat: allow disabling proxy targets availability test #1327
1 parent ccedb94 commit de0467b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+5487
-3711
lines changed

.claude/settings.local.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
"Bash(find:*)",
1616
"Bash(sed:*)",
1717
"Bash(cp:*)",
18-
"mcp__eslint__lint-files"
18+
"mcp__eslint__lint-files",
19+
"Bash(go generate:*)",
20+
"Bash(pnpm eslint:*)"
1921
],
2022
"deny": []
2123
}

api/upstream/init.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package upstream
2+
3+
import (
4+
"github.com/0xJacky/Nginx-UI/internal/upstream"
5+
"github.com/0xJacky/Nginx-UI/model"
6+
"github.com/uozi-tech/cosy/logger"
7+
)
8+
9+
func init() {
10+
// Register the disabled sockets checker callback
11+
service := upstream.GetUpstreamService()
12+
service.SetDisabledSocketsChecker(getDisabledSockets)
13+
}
14+
15+
// getDisabledSockets queries the database for disabled sockets
16+
func getDisabledSockets() map[string]bool {
17+
disabled := make(map[string]bool)
18+
19+
db := model.UseDB()
20+
if db == nil {
21+
return disabled
22+
}
23+
24+
var configs []model.UpstreamConfig
25+
if err := db.Where("enabled = ?", false).Find(&configs).Error; err != nil {
26+
logger.Error("Failed to query disabled sockets:", err)
27+
return disabled
28+
}
29+
30+
for _, config := range configs {
31+
disabled[config.Socket] = true
32+
}
33+
34+
return disabled
35+
}

api/upstream/list.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package upstream
2+
3+
import (
4+
"net/http"
5+
"sort"
6+
7+
"github.com/0xJacky/Nginx-UI/internal/upstream"
8+
"github.com/0xJacky/Nginx-UI/model"
9+
"github.com/0xJacky/Nginx-UI/query"
10+
"github.com/gin-gonic/gin"
11+
"github.com/uozi-tech/cosy"
12+
"github.com/uozi-tech/cosy/logger"
13+
)
14+
15+
// UpstreamInfo represents an upstream with its configuration and health status
16+
type UpstreamInfo struct {
17+
Name string `json:"name"`
18+
Servers []upstream.ProxyTarget `json:"servers"`
19+
ConfigPath string `json:"config_path"`
20+
LastSeen string `json:"last_seen"`
21+
Status map[string]*upstream.Status `json:"status"`
22+
Enabled bool `json:"enabled"`
23+
}
24+
25+
// GetUpstreamList returns all upstreams with their configuration and health status
26+
func GetUpstreamList(c *gin.Context) {
27+
service := upstream.GetUpstreamService()
28+
29+
// Get all upstream definitions
30+
upstreams := service.GetAllUpstreamDefinitions()
31+
32+
// Get availability map
33+
availabilityMap := service.GetAvailabilityMap()
34+
35+
// Get all upstream configurations from database
36+
u := query.UpstreamConfig
37+
configs, err := u.Find()
38+
if err != nil {
39+
cosy.ErrHandler(c, err)
40+
return
41+
}
42+
43+
// Create a map for quick lookup of enabled status by upstream name
44+
configMap := make(map[string]bool)
45+
for _, config := range configs {
46+
configMap[config.Socket] = config.Enabled
47+
}
48+
49+
// Build response
50+
result := make([]UpstreamInfo, 0, len(upstreams))
51+
for name, def := range upstreams {
52+
// Get enabled status from database, default to true if not found
53+
enabled := true
54+
if val, exists := configMap[name]; exists {
55+
enabled = val
56+
}
57+
58+
// Get status for each server in this upstream
59+
serverStatus := make(map[string]*upstream.Status)
60+
for _, server := range def.Servers {
61+
key := formatSocketAddress(server.Host, server.Port)
62+
if status, exists := availabilityMap[key]; exists {
63+
serverStatus[key] = status
64+
}
65+
}
66+
67+
info := UpstreamInfo{
68+
Name: name,
69+
Servers: def.Servers,
70+
ConfigPath: def.ConfigPath,
71+
LastSeen: def.LastSeen.Format("2006-01-02 15:04:05"),
72+
Status: serverStatus,
73+
Enabled: enabled,
74+
}
75+
result = append(result, info)
76+
}
77+
78+
// Sort by name for stable ordering
79+
sort.Slice(result, func(i, j int) bool {
80+
return result[i].Name < result[j].Name
81+
})
82+
83+
c.JSON(http.StatusOK, gin.H{
84+
"data": result,
85+
})
86+
}
87+
88+
// UpdateUpstreamConfigRequest represents the request body for updating upstream config
89+
type UpdateUpstreamConfigRequest struct {
90+
Enabled bool `json:"enabled"`
91+
}
92+
93+
// UpdateUpstreamConfig updates the enabled status of an upstream
94+
func UpdateUpstreamConfig(c *gin.Context) {
95+
name := c.Param("name")
96+
97+
var req UpdateUpstreamConfigRequest
98+
if err := c.ShouldBindJSON(&req); err != nil {
99+
cosy.ErrHandler(c, err)
100+
return
101+
}
102+
103+
u := query.UpstreamConfig
104+
105+
// Check if config exists
106+
config, err := u.Where(u.Socket.Eq(name)).First()
107+
if err != nil {
108+
// Create new config if not found
109+
config = &model.UpstreamConfig{
110+
Socket: name,
111+
Enabled: req.Enabled,
112+
}
113+
if err := u.Create(config); err != nil {
114+
logger.Error("Failed to create upstream config:", err)
115+
cosy.ErrHandler(c, err)
116+
return
117+
}
118+
} else {
119+
// Update existing config
120+
if _, err := u.Where(u.Socket.Eq(name)).Update(u.Enabled, req.Enabled); err != nil {
121+
logger.Error("Failed to update upstream config:", err)
122+
cosy.ErrHandler(c, err)
123+
return
124+
}
125+
}
126+
127+
c.JSON(http.StatusOK, gin.H{
128+
"message": "Upstream config updated successfully",
129+
})
130+
}
131+

api/upstream/router.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ import "github.com/gin-gonic/gin"
55
func InitRouter(r *gin.RouterGroup) {
66
r.GET("/upstream/availability", GetAvailability)
77
r.GET("/upstream/availability_ws", AvailabilityWebSocket)
8+
r.GET("/upstream/sockets", GetSocketList)
9+
r.PUT("/upstream/socket/:socket", UpdateSocketConfig)
810
}

api/upstream/socket.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package upstream
2+
3+
import (
4+
"net/http"
5+
"sort"
6+
7+
"github.com/0xJacky/Nginx-UI/internal/upstream"
8+
"github.com/0xJacky/Nginx-UI/model"
9+
"github.com/0xJacky/Nginx-UI/query"
10+
"github.com/gin-gonic/gin"
11+
"github.com/uozi-tech/cosy"
12+
"github.com/uozi-tech/cosy/logger"
13+
)
14+
15+
// SocketInfo represents a socket with its configuration and health status
16+
type SocketInfo struct {
17+
Socket string `json:"socket"` // host:port
18+
Host string `json:"host"` // hostname/IP
19+
Port string `json:"port"` // port number
20+
Type string `json:"type"` // proxy_pass, grpc_pass, or upstream
21+
IsConsul bool `json:"is_consul"` // whether this is a consul service
22+
UpstreamName string `json:"upstream_name"` // which upstream this belongs to (if any)
23+
LastCheck string `json:"last_check"` // last time health check was performed
24+
Status *upstream.Status `json:"status"` // health check status
25+
Enabled bool `json:"enabled"` // whether health check is enabled
26+
}
27+
28+
// GetSocketList returns all sockets with their configuration and health status
29+
func GetSocketList(c *gin.Context) {
30+
service := upstream.GetUpstreamService()
31+
32+
// Get all target infos
33+
targets := service.GetTargetInfos()
34+
35+
// Get availability map
36+
availabilityMap := service.GetAvailabilityMap()
37+
38+
// Get all socket configurations from database
39+
u := query.UpstreamConfig
40+
configs, err := u.Find()
41+
if err != nil {
42+
cosy.ErrHandler(c, err)
43+
return
44+
}
45+
46+
// Create a map for quick lookup of enabled status
47+
configMap := make(map[string]bool)
48+
for _, config := range configs {
49+
configMap[config.Socket] = config.Enabled
50+
}
51+
52+
// Build response
53+
result := make([]SocketInfo, 0, len(targets))
54+
for _, target := range targets {
55+
socketAddr := formatSocketAddress(target.Host, target.Port)
56+
57+
// Get enabled status from database, default to true if not found
58+
enabled := true
59+
if val, exists := configMap[socketAddr]; exists {
60+
enabled = val
61+
}
62+
63+
// Get health status
64+
var status *upstream.Status
65+
if s, exists := availabilityMap[socketAddr]; exists {
66+
status = s
67+
}
68+
69+
// Find which upstream this belongs to
70+
upstreamName := findUpstreamForSocket(service, target.ProxyTarget)
71+
72+
info := SocketInfo{
73+
Socket: socketAddr,
74+
Host: target.Host,
75+
Port: target.Port,
76+
Type: target.Type,
77+
IsConsul: target.IsConsul,
78+
UpstreamName: upstreamName,
79+
LastCheck: target.LastSeen.Format("2006-01-02 15:04:05"),
80+
Status: status,
81+
Enabled: enabled,
82+
}
83+
result = append(result, info)
84+
}
85+
86+
// Sort by socket address for stable ordering
87+
sort.Slice(result, func(i, j int) bool {
88+
return result[i].Socket < result[j].Socket
89+
})
90+
91+
c.JSON(http.StatusOK, gin.H{
92+
"data": result,
93+
})
94+
}
95+
96+
// UpdateSocketConfigRequest represents the request body for updating socket config
97+
type UpdateSocketConfigRequest struct {
98+
Enabled bool `json:"enabled"`
99+
}
100+
101+
// UpdateSocketConfig updates the enabled status of a socket
102+
func UpdateSocketConfig(c *gin.Context) {
103+
socket := c.Param("socket")
104+
105+
var req UpdateSocketConfigRequest
106+
if err := c.ShouldBindJSON(&req); err != nil {
107+
cosy.ErrHandler(c, err)
108+
return
109+
}
110+
111+
u := query.UpstreamConfig
112+
113+
// Check if config exists
114+
config, err := u.Where(u.Socket.Eq(socket)).First()
115+
if err != nil {
116+
// Create new config if not found
117+
config = &model.UpstreamConfig{
118+
Socket: socket,
119+
Enabled: req.Enabled,
120+
}
121+
if err := u.Create(config); err != nil {
122+
logger.Error("Failed to create socket config:", err)
123+
cosy.ErrHandler(c, err)
124+
return
125+
}
126+
} else {
127+
// Update existing config
128+
if _, err := u.Where(u.Socket.Eq(socket)).Update(u.Enabled, req.Enabled); err != nil {
129+
logger.Error("Failed to update socket config:", err)
130+
cosy.ErrHandler(c, err)
131+
return
132+
}
133+
}
134+
135+
c.JSON(http.StatusOK, gin.H{
136+
"message": "Socket config updated successfully",
137+
})
138+
}
139+
140+
// findUpstreamForSocket finds which upstream a socket belongs to
141+
func findUpstreamForSocket(service *upstream.Service, target upstream.ProxyTarget) string {
142+
socketAddr := formatSocketAddress(target.Host, target.Port)
143+
upstreams := service.GetAllUpstreamDefinitions()
144+
145+
for name, upstream := range upstreams {
146+
for _, server := range upstream.Servers {
147+
serverAddr := formatSocketAddress(server.Host, server.Port)
148+
if serverAddr == socketAddr {
149+
return name
150+
}
151+
}
152+
}
153+
return ""
154+
}
155+

api/upstream/util.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package upstream
2+
3+
// formatSocketAddress formats a host:port combination into a proper socket address
4+
// For IPv6 addresses, it adds brackets around the host if they're not already present
5+
func formatSocketAddress(host, port string) string {
6+
// Reuse the logic from service package
7+
if len(host) > 0 && host[0] != '[' && containsColon(host) {
8+
return "[" + host + "]:" + port
9+
}
10+
return host + ":" + port
11+
}
12+
13+
// containsColon checks if string contains a colon
14+
func containsColon(s string) bool {
15+
for i := 0; i < len(s); i++ {
16+
if s[i] == ':' {
17+
return true
18+
}
19+
}
20+
return false
21+
}

0 commit comments

Comments
 (0)