mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-09 08:00:27 +00:00
Compare commits
134
Commits
pr@dev-v2@vllm
...
dev-v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce14a104cc | ||
|
|
2eaabccf94 | ||
|
|
e119239d3d | ||
|
|
14a57af4e9 | ||
|
|
2701af9054 | ||
|
|
4954067736 | ||
|
|
b962144e7d | ||
|
|
8f1be42b6b | ||
|
|
c6969f231b | ||
|
|
3571fb8d86 | ||
|
|
d21288788e | ||
|
|
f65e4b06ab | ||
|
|
d26bc9a3a4 | ||
|
|
7088a903fe | ||
|
|
864ebeed82 | ||
|
|
c1b2b92708 | ||
|
|
2189d9229a | ||
|
|
d0183f460e | ||
|
|
d27d6db1ea | ||
|
|
d2bb3813d9 | ||
|
|
cac73e6d45 | ||
|
|
4712ceaf2c | ||
|
|
65243c68c4 | ||
|
|
256e79ca81 | ||
|
|
4861eb69cb | ||
|
|
fb8cf15537 | ||
|
|
c4a6791271 | ||
|
|
f58e147636 | ||
|
|
387e9fbeed | ||
|
|
4eb627bc79 | ||
|
|
850c86229c | ||
|
|
f984917a66 | ||
|
|
8588217fbf | ||
|
|
a2307c5f64 | ||
|
|
5923290de8 | ||
|
|
1c994fba4a | ||
|
|
415ab96aab | ||
|
|
19bb823b05 | ||
|
|
3a5371652e | ||
|
|
a267b4148a | ||
|
|
36a01eb60d | ||
|
|
65f6fdd045 | ||
|
|
75b60b32e4 | ||
|
|
6cb65e2290 | ||
|
|
0bad1b471f | ||
|
|
3814525edd | ||
|
|
8162dd1856 | ||
|
|
e833787020 | ||
|
|
673ffac516 | ||
|
|
e864610015 | ||
|
|
78402e1b7d | ||
|
|
782bc1e67c | ||
|
|
86e4ed6f64 | ||
|
|
ee8bac39af | ||
|
|
fe742b9f41 | ||
|
|
9a5bd9bcba | ||
|
|
b9c8e39560 | ||
|
|
6b20ff0b13 | ||
|
|
89bd32b6d4 | ||
|
|
005f240fb7 | ||
|
|
75da53e374 | ||
|
|
2485b0aa5e | ||
|
|
ed51a5e1fa | ||
|
|
56870504ac | ||
|
|
7aefb47cc3 | ||
|
|
aba41c0aea | ||
|
|
9300bf4141 | ||
|
|
b7ec17b3e3 | ||
|
|
2fcfe56a30 | ||
|
|
63b2d4e4d5 | ||
|
|
4cd77d8ee1 | ||
|
|
53a7347bea | ||
|
|
a02c25ebcc | ||
|
|
605c8cc6db | ||
|
|
033cc7c2d1 | ||
|
|
7c1ddb5b4c | ||
|
|
eb0f5264d7 | ||
|
|
5ad12c6fe4 | ||
|
|
61dacce5e0 | ||
|
|
e3f0381a26 | ||
|
|
6bc9dd96af | ||
|
|
e23f338b31 | ||
|
|
6e08b50e3c | ||
|
|
536712cd55 | ||
|
|
191ff0cda4 | ||
|
|
671f781564 | ||
|
|
30dc36b95d | ||
|
|
fac4aec680 | ||
|
|
8eac9a1808 | ||
|
|
ce74d96617 | ||
|
|
a15e77d605 | ||
|
|
bad022f524 | ||
|
|
50a54d0613 | ||
|
|
a47e41a8b7 | ||
|
|
f938443e55 | ||
|
|
b90abd2b28 | ||
|
|
da5682a600 | ||
|
|
81b72d9b7d | ||
|
|
6e13143286 | ||
|
|
70fc628c81 | ||
|
|
9858881ce6 | ||
|
|
eb6a8c7646 | ||
|
|
a71aea8aec | ||
|
|
918c441f88 | ||
|
|
960b4b0345 | ||
|
|
3aa4bfaa82 | ||
|
|
b3bdf9ef7e | ||
|
|
2948b8ffe8 | ||
|
|
e99c6c08a5 | ||
|
|
6fb389b2ed | ||
|
|
c09833cbde | ||
|
|
fa2ad69154 | ||
|
|
51d84455a3 | ||
|
|
d88d98d8a8 | ||
|
|
5aec466c8e | ||
|
|
0ee93774d5 | ||
|
|
7be7368bb9 | ||
|
|
eab0bb4a94 | ||
|
|
9f74f2077a | ||
|
|
a6e2efa6c9 | ||
|
|
205ef3009a | ||
|
|
d7edbd1e95 | ||
|
|
b361f464c5 | ||
|
|
fb377d2e99 | ||
|
|
deddd392ba | ||
|
|
3ab10848c8 | ||
|
|
433f1a940f | ||
|
|
1f12c09eb5 | ||
|
|
31e6d523f9 | ||
|
|
3c0bd051bf | ||
|
|
3c2d92dc5f | ||
|
|
262bd14bc8 | ||
|
|
f15ff46e34 | ||
|
|
fc1ec4e1b0 |
@@ -2,11 +2,14 @@ package v2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -294,6 +297,34 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if err := alertService.UpdateAlertConfig(req, loadAuditUser(c)); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, repo.ErrAlertConfigRevisionConflict):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "ALERT_CONFIG_REVISION_CONFLICT", "ErrInvalidParams", err)
|
||||
case errors.Is(err, repo.ErrAlertConfigRevisionRequired):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "ALERT_CONFIG_REVISION_REQUIRED", "ErrInvalidParams", err)
|
||||
default:
|
||||
helper.InternalServer(c, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Alert
|
||||
// @Summary Update alert config status
|
||||
// @Accept json
|
||||
// @Param request body dto.AlertConfigStatusUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /alert/config/status [post]
|
||||
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [id][status]","formatEN":"update alert config status [id][status]"}
|
||||
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
|
||||
var req dto.AlertConfigStatusUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c)); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
@@ -346,6 +377,15 @@ func (b *BaseApi) TestAlertConfig(c *gin.Context) {
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if req.Type == constant.Custom {
|
||||
result, err := alertService.TestCustomAlertConfig(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
return
|
||||
}
|
||||
flag, err := alertService.TestAlertConfig(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
@@ -439,7 +439,7 @@ func (b *BaseApi) ContainerItemStats(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := containerService.ContainerItemStats(req)
|
||||
data, err := containerService.ContainerItemStats(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
|
||||
@@ -684,10 +684,35 @@ func (b *BaseApi) StopWget(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
files.CancelDownload(req.Key)
|
||||
if err := files.CancelDownload(req.Key); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Remove finished download progress records without deleting files
|
||||
// @Accept json
|
||||
// @Param request body request.FileProcessRemoveReq true "request"
|
||||
// @Success 200 {object} response.FileProcessKeys
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/wget/process/remove [post]
|
||||
// @x-panel-log {"bodyKeys":["keys"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"移除已结束下载记录 [keys]","formatEN":"Remove finished download records [keys]"}
|
||||
func (b *BaseApi) RemoveWgetRecords(c *gin.Context) {
|
||||
var req request.FileProcessRemoveReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
keys, err := files.RemoveDownloadRecords(req.Keys)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, response.FileProcessKeys{Keys: keys})
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Move file
|
||||
// @Accept json
|
||||
|
||||
+254
-154
@@ -2,17 +2,37 @@ package v2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (b *BaseApi) UpdatePanelFirewallPort(c *gin.Context) {
|
||||
if !global.IsMaster {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
OldPort uint `json:"oldPort" validate:"required,min=1,max=65535"`
|
||||
NewPort uint `json:"newPort" validate:"required,min=1,max=65535"`
|
||||
}
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallService.UpdatePanelPort(c.Request.Context(), request.OldPort, request.NewPort); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Load firewall base info
|
||||
// @Accept json
|
||||
@@ -40,7 +60,7 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
||||
// @Summary Operate firewall
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallLifecycleOperation true "request"
|
||||
// @Success 200
|
||||
// @Success 200 {object} dto.FirewallLifecycleOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/operate [post]
|
||||
@@ -51,12 +71,13 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := firewallService.OperateFirewall(request); err != nil {
|
||||
result, err := firewallService.QueueFirewallOperation(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
@@ -67,7 +88,7 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/forward/base [post]
|
||||
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) {
|
||||
data, err := forwardingService.LoadBaseInfo()
|
||||
data, err := forwardingService.LoadBaseInfo(c.Request.Context())
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -88,7 +109,7 @@ func (b *BaseApi) SearchForwardingRules(c *gin.Context) {
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
total, items, err := forwardingService.SearchRules(request)
|
||||
total, items, err := forwardingService.SearchRules(c.Request.Context(), request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -101,7 +122,7 @@ func (b *BaseApi) SearchForwardingRules(c *gin.Context) {
|
||||
// @Summary Operate forwarding rules
|
||||
// @Accept json
|
||||
// @Param request body dto.ForwardRuleOperate true "request"
|
||||
// @Success 200
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/forward/operate [post]
|
||||
@@ -112,33 +133,41 @@ func (b *BaseApi) OperateForwardingRules(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := forwardingService.OperateRules(request); err != nil {
|
||||
result, err := forwardingService.OperateRules(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Enable forwarding
|
||||
// @Success 200
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallInitializationTask true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/forward/enable [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"初始化并启用端口转发","formatEN":"initialize and enable port forwarding"}
|
||||
func (b *BaseApi) EnableForwarding(c *gin.Context) {
|
||||
if err := forwardingService.Enable(); err != nil {
|
||||
var request dto.FirewallInitializationTask
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := forwardingService.QueueInitialization(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Apply/Unload/Init firewall filter chain
|
||||
// @Accept json
|
||||
// @Param request body dto.FilterChainOperation true "request"
|
||||
// @Success 200
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/filter/operate [post]
|
||||
@@ -148,12 +177,20 @@ func (b *BaseApi) OperateFilterChain(c *gin.Context) {
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if request.Operate == "init-base" {
|
||||
result, err := firewallService.QueueFilterChainInitialization(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
return
|
||||
}
|
||||
if err := firewallService.OperateFilterChain(request); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, dto.FilterChainOperationResponse{})
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
@@ -224,29 +261,8 @@ func (b *BaseApi) LoadFirewallNativeDetail(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Check unified firewall v2 rules for duplicates and conflicts
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleCheck true "request"
|
||||
// @Success 200 {object} dto.FirewallRuleCheckResponse
|
||||
// @Failure 400 {object} dto.Response
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/check [post]
|
||||
func (b *BaseApi) CheckFirewallRules(c *gin.Context) {
|
||||
var request dto.FirewallRuleCheck
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallService.Check(c.Request.Context(), c.ClientIP(), request)
|
||||
if err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Create unified firewall v2 rules
|
||||
// @Summary Queue firewall rule creation
|
||||
// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleCreate true "request"
|
||||
// @Success 200 {object} dto.FirewallRuleCreateResponse
|
||||
@@ -270,67 +286,8 @@ func (b *BaseApi) CreateFirewallRules(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Preview firewall rule synchronization
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleSyncRequest true "request"
|
||||
// @Success 200 {object} dto.FirewallRuleSyncPreview
|
||||
// @Failure 400 {object} dto.Response
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/sync/preview [post]
|
||||
func (b *BaseApi) PreviewFirewallRuleSync(c *gin.Context) {
|
||||
var request dto.FirewallRuleSyncRequest
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallService.PreviewRuleSync(c.Request.Context(), c.ClientIP(), request)
|
||||
if err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Load the currently executing firewall rule synchronization task
|
||||
// @Success 200 {object} dto.FirewallRuleSyncTask
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/sync/task [get]
|
||||
func (b *BaseApi) LoadFirewallRuleSyncTask(c *gin.Context) {
|
||||
result, err := firewallService.CurrentRuleSyncTask()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Synchronize firewall rules to a target backend
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleSyncRequest true "request"
|
||||
// @Success 200 {object} dto.FirewallRuleSyncResult
|
||||
// @Failure 400 {object} dto.Response
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/sync [post]
|
||||
// @x-panel-log {"bodyKeys":["subsystem","sourceProvider","targetProvider"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 [subsystem] 防火墙规则到 [targetProvider]","formatEN":"sync [subsystem] firewall rules to [targetProvider]"}
|
||||
func (b *BaseApi) SyncFirewallRules(c *gin.Context) {
|
||||
var request dto.FirewallRuleSyncRequest
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallService.SyncRules(c.Request.Context(), c.ClientIP(), request)
|
||||
if err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Delete managed unified firewall v2 rules
|
||||
// @Summary Queue firewall rule deletion
|
||||
// @Description Deletes non-whitelist rules by scope and instance key. Returns a taskID immediately; results are written to the task log.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleDelete true "request"
|
||||
// @Success 200 {object} dto.FirewallRuleDeleteResponse
|
||||
@@ -353,7 +310,7 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Update a managed unified firewall v2 rule
|
||||
// @Summary Update a firewall rule
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleUpdate true "request"
|
||||
// @Success 200
|
||||
@@ -361,16 +318,13 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/update [post]
|
||||
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [uuid]","formatEN":"update firewall rule [uuid]"}
|
||||
// @x-panel-log {"bodyKeys":["instanceKey"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [instanceKey]","formatEN":"update firewall rule [instanceKey]"}
|
||||
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||
var request dto.FirewallRuleUpdate
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if !normalizeFirewallRuleUUID(c, &request.UUID) {
|
||||
return
|
||||
}
|
||||
if err := firewallService.Update(c.Request.Context(), c.ClientIP(), request); err != nil {
|
||||
if err := firewallService.Update(c.Request.Context(), request); err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
@@ -378,7 +332,7 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Reorder a managed unified firewall v2 rule
|
||||
// @Summary Reorder a firewall rule
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleReorder true "request"
|
||||
// @Success 200
|
||||
@@ -386,53 +340,41 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/reorder [post]
|
||||
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [uuid]","formatEN":"reorder firewall rule [uuid]"}
|
||||
// @x-panel-log {"bodyKeys":["instanceKey"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [instanceKey]","formatEN":"reorder firewall rule [instanceKey]"}
|
||||
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
|
||||
var request dto.FirewallRuleReorder
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if !normalizeFirewallRuleUUID(c, &request.UUID) {
|
||||
return
|
||||
}
|
||||
if err := firewallService.Reorder(c.Request.Context(), c.ClientIP(), request); err != nil {
|
||||
if err := firewallService.Reorder(c.Request.Context(), request); err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
func normalizeFirewallRuleUUID(c *gin.Context, value *string) bool {
|
||||
if value == nil {
|
||||
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
|
||||
return false
|
||||
}
|
||||
*value = strings.TrimSpace(*value)
|
||||
if *value == "" {
|
||||
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func handleFirewallRuleError(c *gin.Context, err error) {
|
||||
var businessErr buserr.BusinessError
|
||||
isBusinessError := errors.As(err, &businessErr)
|
||||
switch {
|
||||
case errors.Is(err, filter.ErrLockoutRisk), errors.Is(err, filter.ErrProtectedRule):
|
||||
case errors.Is(err, filter.ErrProtectedRule):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
|
||||
case errors.Is(err, filter.ErrRuleStale):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err)
|
||||
case errors.Is(err, repo.ErrFirewallRuleRevisionConflict):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_REVISION_CONFLICT", "ErrInvalidParams", err)
|
||||
case errors.Is(err, filter.ErrRuleCheckRequired):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_CHECK_REQUIRED", "ErrInvalidParams", err)
|
||||
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
|
||||
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation),
|
||||
errors.Is(err, repo.ErrFirewallPersistenceInvalid):
|
||||
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||
case errors.Is(err, filter.ErrVerificationFailed):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err)
|
||||
case isBusinessError && businessErr.Msg == "ErrRecordExist":
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_DUPLICATE", Message: err.Error()})
|
||||
c.Abort()
|
||||
case isBusinessError && businessErr.Msg == "ErrFirewallRuleConflict":
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_CONFLICT", Message: err.Error()})
|
||||
c.Abort()
|
||||
case isBusinessError && businessErr.Msg == "ErrInvalidParams":
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusBadRequest, ErrorCode: "FW_RULE_UNSUPPORTED", Message: err.Error()})
|
||||
c.Abort()
|
||||
default:
|
||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err)
|
||||
}
|
||||
@@ -453,6 +395,72 @@ func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Create firewall port whitelist rules
|
||||
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallPortWhitelistCreate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/settings/whitelist [post]
|
||||
// @x-panel-log {"bodyKeys":["rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建防火墙端口白名单","formatEN":"create firewall port whitelist"}
|
||||
func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) {
|
||||
var request dto.FirewallPortWhitelistCreate
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallSettingService.CreatePortWhitelist(c.Request.Context(), request); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Update firewall port whitelist rules
|
||||
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/settings/whitelist/update [post]
|
||||
// @x-panel-log {"bodyKeys":["oldRule","rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"编辑防火墙端口白名单","formatEN":"update firewall port whitelist"}
|
||||
func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
|
||||
var request dto.FirewallPortWhitelistUpdate
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallSettingService.UpdatePortWhitelist(c.Request.Context(), request); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Delete firewall port whitelist rules
|
||||
// @Description Removes whitelist configuration; existing firewall rules are not removed.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallPortWhitelistDelete true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/settings/whitelist/delete [post]
|
||||
// @x-panel-log {"bodyKeys":["rules"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除防火墙端口白名单","formatEN":"delete firewall port whitelist"}
|
||||
func (b *BaseApi) DeleteFirewallPortWhitelist(c *gin.Context) {
|
||||
var request dto.FirewallPortWhitelistDelete
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallSettingService.DeletePortWhitelist(c.Request.Context(), request); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Operate firewall backend
|
||||
// @Accept json
|
||||
@@ -468,6 +476,12 @@ func (b *BaseApi) OperateFirewallBackend(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if err := firewallSettingService.Operate(c.Request.Context(), request); err != nil {
|
||||
var businessErr buserr.BusinessError
|
||||
if errors.As(err, &businessErr) && businessErr.Msg == "ErrFirewallBackendCleanupRequired" {
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_BACKEND_CLEANUP_REQUIRED", Message: err.Error()})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
@@ -490,25 +504,25 @@ func (b *BaseApi) ListDockerPortGuard(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Sync Docker port guard rules
|
||||
// @Success 200
|
||||
// @Summary List Docker published ports
|
||||
// @Success 200 {array} dto.DockerPortGuardContainer
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/docker/sync [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Docker 端口防护规则","formatEN":"sync Docker port guard rules"}
|
||||
func (b *BaseApi) SyncDockerPortGuard(c *gin.Context) {
|
||||
if err := dockerPortGuardService.Reconcile(c.Request.Context()); err != nil {
|
||||
// @Router /hosts/firewall/docker/endpoints [get]
|
||||
func (b *BaseApi) ListDockerPublishedPorts(c *gin.Context) {
|
||||
data, err := dockerPortGuardService.LoadPublishedPorts(c.Request.Context())
|
||||
if err != nil {
|
||||
handleDockerPortGuardError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Operate Docker port guard
|
||||
// @Accept json
|
||||
// @Param request body dto.DockerPortGuardOperation true "request"
|
||||
// @Success 200
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/docker/operate [post]
|
||||
@@ -518,6 +532,15 @@ func (b *BaseApi) OperateDockerPortGuard(c *gin.Context) {
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if request.Operation == "initialize" {
|
||||
result, err := dockerPortGuardService.QueueInitialization(request)
|
||||
if err != nil {
|
||||
handleDockerPortGuardError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
return
|
||||
}
|
||||
if err := dockerPortGuardService.Operate(c.Request.Context(), request); err != nil {
|
||||
handleDockerPortGuardError(c, err)
|
||||
return
|
||||
@@ -529,7 +552,7 @@ func (b *BaseApi) OperateDockerPortGuard(c *gin.Context) {
|
||||
// @Summary Delete Docker port guard policies
|
||||
// @Accept json
|
||||
// @Param request body dto.DockerPortGuardPolicyBatchDelete true "request"
|
||||
// @Success 200
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/docker/policies/delete/batch [post]
|
||||
@@ -539,42 +562,119 @@ func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := dockerPortGuardService.DeletePolicies(c.Request.Context(), request); err != nil {
|
||||
result, err := dockerPortGuardService.DeletePolicies(request)
|
||||
if err != nil {
|
||||
handleDockerPortGuardError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Batch upsert Docker port guard policies
|
||||
// @Accept json
|
||||
// @Param request body dto.DockerPortGuardPolicyBatch true "request"
|
||||
// @Success 200
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/docker/policies/batch [post]
|
||||
// @x-panel-log {"bodyKeys":["mode"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量更新 Docker 端口防护策略 [mode]","formatEN":"batch update Docker port guard policies [mode]"}
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量更新 Docker 端口防护策略","formatEN":"batch update Docker port guard policies"}
|
||||
func (b *BaseApi) UpsertDockerPortGuardPolicies(c *gin.Context) {
|
||||
var request dto.DockerPortGuardPolicyBatch
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := dockerPortGuardService.UpsertPolicies(c.Request.Context(), request); err != nil {
|
||||
result, err := dockerPortGuardService.UpsertPolicies(request)
|
||||
if err != nil {
|
||||
handleDockerPortGuardError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
func handleDockerPortGuardError(c *gin.Context, err error) {
|
||||
if errors.Is(err, service.ErrDockerGuardInvalid) {
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID", "ErrInvalidParams", err)
|
||||
return
|
||||
var businessErr buserr.BusinessError
|
||||
if errors.As(err, &businessErr) {
|
||||
code, errorCode := http.StatusInternalServerError, ""
|
||||
switch businessErr.Msg {
|
||||
case "ErrDockerIptablesChainUnavailable":
|
||||
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE"
|
||||
case "ErrDockerNftablesChainUnavailable":
|
||||
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_NFTABLES_CHAIN_UNAVAILABLE"
|
||||
case "ErrInvalidParams":
|
||||
code, errorCode = http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID"
|
||||
case "ErrDockerFailed":
|
||||
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE"
|
||||
}
|
||||
if errorCode != "" {
|
||||
c.JSON(http.StatusOK, dto.Response{Code: code, ErrorCode: errorCode, Message: err.Error()})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
if errors.Is(err, service.ErrDockerUnavailable) {
|
||||
if errors.Is(err, docker.ErrUnavailable) {
|
||||
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE", "ErrDockerFailed", err)
|
||||
return
|
||||
}
|
||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary List firewall rule backups
|
||||
// @Param subsystem query string false "Firewall subsystem" Enums(system,forwarding,docker) default(system)
|
||||
// @Success 200 {object} dto.FirewallRuleBackups
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/backups [get]
|
||||
func (b *BaseApi) ListFirewallRuleBackups(c *gin.Context) {
|
||||
result, err := firewallService.ListRuleBackups(c.Request.Context(), c.DefaultQuery("subsystem", "system"))
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Initialize, repair or bind one firewall address family
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallFamilyOperation true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/family/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["subsystem","family","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] [subsystem] [family] 防火墙链","formatEN":"[operation] [subsystem] [family] firewall chains"}
|
||||
func (b *BaseApi) OperateFirewallFamily(c *gin.Context) {
|
||||
var request dto.FirewallFamilyOperation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallSettingService.OperateFamily(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Update firewall IPv6 support
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallIPv6Operation true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/settings/ipv6 [post]
|
||||
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"设置防火墙 IPv6 支持为 [status]","formatEN":"Set firewall IPv6 support to [status]"}
|
||||
func (b *BaseApi) OperateFirewallIPv6(c *gin.Context) {
|
||||
var request dto.FirewallIPv6Operation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallSettingService.OperateIPv6(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
+10
-23
@@ -1,13 +1,9 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
// @Tags Monitor
|
||||
@@ -32,14 +28,19 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags Monitor
|
||||
// @Summary Clean monitor data
|
||||
// @Summary Clean host or GPU monitor data
|
||||
// @Param request body dto.MonitorClean true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/monitor/clean [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空监控数据","formatEN":"clean monitor datas"}
|
||||
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 [type] 监控数据","formatEN":"clean [type] monitoring data"}
|
||||
func (b *BaseApi) CleanMonitor(c *gin.Context) {
|
||||
if err := monitorService.CleanData(); err != nil {
|
||||
var req dto.MonitorClean
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := monitorService.CleanData(req.Type); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
@@ -91,14 +92,7 @@ func (b *BaseApi) UpdateMonitorSetting(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/monitor/netoptions [get]
|
||||
func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
||||
netStat, _ := net.IOCounters(true)
|
||||
var options []string
|
||||
options = append(options, "all")
|
||||
for _, net := range netStat {
|
||||
options = append(options, net.Name)
|
||||
}
|
||||
sort.Strings(options)
|
||||
helper.SuccessWithData(c, options)
|
||||
helper.SuccessWithData(c, monitorService.LoadNetworkOptions())
|
||||
}
|
||||
|
||||
// @Tags Monitor
|
||||
@@ -108,12 +102,5 @@ func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/monitor/iooptions [get]
|
||||
func (b *BaseApi) GetIOOptions(c *gin.Context) {
|
||||
diskStat, _ := disk.IOCounters()
|
||||
var options []string
|
||||
options = append(options, "all")
|
||||
for _, net := range diskStat {
|
||||
options = append(options, net.Name)
|
||||
}
|
||||
sort.Strings(options)
|
||||
helper.SuccessWithData(c, options)
|
||||
helper.SuccessWithData(c, monitorService.LoadIOOptions())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (b *BaseApi) LoadVLLMMonitor(c *gin.Context) {
|
||||
var req dto.MonitorVLLMSearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := monitorService.LoadVLLMMonitorData(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
func (b *BaseApi) LoadVLLMCurrent(c *gin.Context) {
|
||||
var req dto.MonitorVLLMCurrent
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := monitorService.LoadVLLMCurrent(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
func (b *BaseApi) CleanVLLMMonitor(c *gin.Context) {
|
||||
var req dto.MonitorVLLMClean
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := monitorService.CleanVLLMMonitor(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
@@ -169,7 +169,7 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /runtimes/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [id]","formatEN":"Operate runtime [id]"}
|
||||
// @x-panel-log {"bodyKeys":["ID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ID","isList":false,"db":"runtimes","output_column":"name","output_value":"name"}],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"}
|
||||
func (b *BaseApi) OperateRuntime(c *gin.Context) {
|
||||
var req request.RuntimeOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
|
||||
@@ -83,6 +83,7 @@ func (b *BaseApi) CreateRootCert(c *gin.Context) {
|
||||
}
|
||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if err := sshService.CreateRootCert(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
@@ -107,6 +108,7 @@ func (b *BaseApi) EditRootCert(c *gin.Context) {
|
||||
}
|
||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if err := sshService.EditRootCert(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
+153
-31
@@ -1,11 +1,14 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
@@ -19,29 +22,35 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// @Tags Terminal
|
||||
// @Summary Ws local terminal
|
||||
// @Param command query string false "command"
|
||||
// @Param session query string false "session id to reattach"
|
||||
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/terminal/local [get]
|
||||
func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
|
||||
b.runSSHSession(c, loadLocalConn, c.DefaultQuery("command", ""))
|
||||
b.runSSHSession(c, "local", loadLocalConn, c.DefaultQuery("command", ""))
|
||||
}
|
||||
|
||||
// @Tags Terminal
|
||||
// @Summary Ws host SSH
|
||||
// @Param id query integer false "id"
|
||||
// @Param command query string false "command"
|
||||
// @Param session query string false "session id to reattach"
|
||||
// @Param title query string false "session title shown in the session list"
|
||||
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/terminal/ssh [get]
|
||||
func (b *BaseApi) WsHostSSH(c *gin.Context) {
|
||||
b.runSSHSession(c, func() (*ssh.SSHClient, error) {
|
||||
b.runSSHSession(c, "ssh", func() (*ssh.SSHClient, error) {
|
||||
hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0"))
|
||||
if hostID <= 0 {
|
||||
return nil, errors.New("missing host id")
|
||||
@@ -65,26 +74,33 @@ func (b *BaseApi) WsContainerTerminal(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
slave, err := loadContainerTerminalCommand(c)
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
defer slave.Close()
|
||||
|
||||
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
|
||||
if wshandleError(wsConn, err) {
|
||||
identity, ok := loadTerminalIdentity(c)
|
||||
if !ok {
|
||||
_ = wshandleError(wsConn, errors.New("missing terminal identity"))
|
||||
return
|
||||
}
|
||||
|
||||
quitChan := make(chan bool, 3)
|
||||
tty.Start(quitChan)
|
||||
go slave.Wait(quitChan)
|
||||
opts := terminal.SessionOptions{
|
||||
Identity: identity,
|
||||
Kind: "container",
|
||||
Target: containerTerminalTarget(c),
|
||||
Cols: cols,
|
||||
Rows: rows,
|
||||
}
|
||||
if err := terminal.ServeCommand(wsConn, strings.TrimSpace(c.Query("session")), opts, func() (*terminal.LocalCommand, error) {
|
||||
return loadContainerTerminalCommand(c)
|
||||
}); err != nil {
|
||||
_ = wshandleError(wsConn, err)
|
||||
}
|
||||
}
|
||||
|
||||
<-quitChan
|
||||
|
||||
global.LOG.Info("websocket finished")
|
||||
closeTerminalConn(wsConn)
|
||||
func containerTerminalTarget(c *gin.Context) string {
|
||||
query := c.Request.URL.Query()
|
||||
for _, key := range []string{"cols", "rows", "session", "terminalRevalidate"} {
|
||||
query.Del(key)
|
||||
}
|
||||
sum := sha256.Sum256([]byte(query.Encode()))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
|
||||
@@ -115,32 +131,138 @@ func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
|
||||
return wsConn, cols, rows, true
|
||||
}
|
||||
|
||||
func (b *BaseApi) runSSHSession(c *gin.Context, connect func() (*ssh.SSHClient, error), command string) {
|
||||
func (b *BaseApi) runSSHSession(c *gin.Context, kind string, connect func() (*ssh.SSHClient, error), command string) {
|
||||
wsConn, cols, rows, ok := prepareTerminalSession(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
client, clientErr := connect()
|
||||
if wshandleError(wsConn, errors.WithMessage(clientErr, "failed to set up the connection. Please check the host information")) {
|
||||
identity, ok := loadTerminalIdentity(c)
|
||||
if !ok {
|
||||
_ = wshandleError(wsConn, errors.New("missing terminal identity"))
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, command)
|
||||
if wshandleError(wsConn, err) {
|
||||
hostID := 0
|
||||
if kind == "ssh" {
|
||||
hostID, _ = strconv.Atoi(c.DefaultQuery("id", "0"))
|
||||
}
|
||||
opts := terminal.SessionOptions{
|
||||
Identity: identity,
|
||||
Kind: kind,
|
||||
Title: sanitizeTerminalTitle(c.Query("title")),
|
||||
Persistent: c.Query("terminalPersistent") == "true",
|
||||
HostID: uint(max(hostID, 0)),
|
||||
Cols: cols,
|
||||
Rows: rows,
|
||||
InitCmd: command,
|
||||
}
|
||||
err := terminal.Serve(wsConn, strings.TrimSpace(c.Query("session")), opts, func() (*gossh.Client, error) {
|
||||
client, err := connect()
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to set up the connection. Please check the host information")
|
||||
}
|
||||
return client.Client, nil
|
||||
})
|
||||
if err != nil {
|
||||
_ = wshandleError(wsConn, err)
|
||||
}
|
||||
}
|
||||
|
||||
// @Tags Terminal
|
||||
// @Summary List the caller's live terminal sessions
|
||||
// @Success 200 {array} terminal.Info
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/terminal/sessions/search [post]
|
||||
func (b *BaseApi) SearchTerminalSessions(c *gin.Context) {
|
||||
identity, ok := loadTerminalIdentity(c)
|
||||
if !ok {
|
||||
helper.BadRequest(c, errors.New("missing terminal identity"))
|
||||
return
|
||||
}
|
||||
defer sws.Close()
|
||||
helper.SuccessWithData(c, terminal.List(identity))
|
||||
}
|
||||
|
||||
quitChan := make(chan bool, 3)
|
||||
sws.Start(quitChan)
|
||||
go sws.Wait(quitChan)
|
||||
// @Tags Terminal
|
||||
// @Summary Close a terminal session
|
||||
// @Accept json
|
||||
// @Param request body dto.TerminalSessionClose true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/terminal/sessions/close [post]
|
||||
func (b *BaseApi) CloseTerminalSession(c *gin.Context) {
|
||||
var req dto.TerminalSessionClose
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
identity, ok := loadTerminalIdentity(c)
|
||||
if !ok {
|
||||
helper.BadRequest(c, errors.New("missing terminal identity"))
|
||||
return
|
||||
}
|
||||
if err := terminal.CloseSession(req.ID, identity); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
<-quitChan
|
||||
// @Tags Terminal
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/terminal/sessions/closeAll [post]
|
||||
func (b *BaseApi) CloseAllTerminalSessions(c *gin.Context) {
|
||||
identity, ok := loadTerminalIdentity(c)
|
||||
if !ok {
|
||||
helper.BadRequest(c, errors.New("missing terminal identity"))
|
||||
return
|
||||
}
|
||||
terminal.Revoke("auth_session", identity.UserID, identity.AuthSessionID)
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
closeTerminalConn(wsConn)
|
||||
func (b *BaseApi) RevokeTerminalSessions(c *gin.Context) {
|
||||
var req dto.TerminalSessionRevoke
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if (req.Scope == "auth_session" && (req.UserID == "" || req.AuthSessionID == "")) ||
|
||||
(req.Scope == "user" && req.UserID == "") {
|
||||
helper.BadRequest(c, errors.New("missing terminal revocation identity"))
|
||||
return
|
||||
}
|
||||
terminal.Revoke(req.Scope, req.UserID, req.AuthSessionID)
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
func loadTerminalIdentity(c *gin.Context) (terminal.Identity, bool) {
|
||||
identity := terminal.Identity{
|
||||
UserID: strings.TrimSpace(c.GetHeader(terminal.HeaderUserID)),
|
||||
AuthSessionID: strings.TrimSpace(c.GetHeader(terminal.HeaderAuthSessionID)),
|
||||
}
|
||||
if value := c.GetHeader(terminal.HeaderAuthLeaseUntil); value != "" {
|
||||
millis, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || millis <= 0 {
|
||||
return terminal.Identity{}, false
|
||||
}
|
||||
identity.AuthLeaseUntil = time.UnixMilli(millis)
|
||||
if maximum := time.Now().Add(90 * time.Second); identity.AuthLeaseUntil.After(maximum) {
|
||||
identity.AuthLeaseUntil = maximum
|
||||
}
|
||||
}
|
||||
return identity, identity.Valid()
|
||||
}
|
||||
|
||||
// sanitizeTerminalTitle keeps the title a short single line.
|
||||
func sanitizeTerminalTitle(title string) string {
|
||||
title = strings.Join(strings.Fields(title), " ")
|
||||
if r := []rune(title); len(r) > 64 {
|
||||
title = string(r[:64])
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
func closeTerminalConn(wsConn *websocket.Conn) {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (b *BaseApi) TerminalCapabilities(c *gin.Context) {
|
||||
helper.SuccessWithData(c, gin.H{"apiKeyLeaseVersion": 1})
|
||||
}
|
||||
+16
-7
@@ -162,16 +162,25 @@ type AgentWebsiteBindReq struct {
|
||||
}
|
||||
|
||||
type AgentModelConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
Model string `json:"model" validate:"required"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
Model string `json:"model" validate:"required"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
Metadata []AgentModelMetadata `json:"metadata" validate:"dive"`
|
||||
}
|
||||
|
||||
type AgentModelConfig struct {
|
||||
AccountID uint `json:"accountId"`
|
||||
Model string `json:"model"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
AccountID uint `json:"accountId"`
|
||||
Model string `json:"model"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
Metadata []AgentModelMetadata `json:"metadata"`
|
||||
}
|
||||
|
||||
type AgentModelMetadata struct {
|
||||
Model string `json:"model" validate:"required"`
|
||||
InputMode string `json:"inputMode" validate:"required,oneof=auto text image"`
|
||||
ContextWindow int `json:"contextWindow" validate:"min=0"`
|
||||
MaxTokens int `json:"maxTokens" validate:"min=0"`
|
||||
}
|
||||
|
||||
type AgentHermesChatSessionItem struct {
|
||||
|
||||
+31
-16
@@ -21,6 +21,7 @@ type AlertBase struct {
|
||||
}
|
||||
|
||||
type PushAlert struct {
|
||||
Result string `json:"result,omitempty"`
|
||||
TaskName string `json:"taskName"`
|
||||
AlertType string `json:"alertType"`
|
||||
EntryID uint `json:"entryID"`
|
||||
@@ -53,6 +54,7 @@ type AlertDTO struct {
|
||||
Method string `json:"method"`
|
||||
Title string `json:"title"`
|
||||
Project string `json:"project"`
|
||||
TaskName string `json:"taskName,omitempty"`
|
||||
Status string `json:"status"`
|
||||
SendCount uint `json:"sendCount"`
|
||||
AdvancedParams string `json:"advancedParams"`
|
||||
@@ -151,16 +153,25 @@ type AlertLog struct {
|
||||
}
|
||||
|
||||
type AlertDetail struct {
|
||||
LicenseId string `json:"licenseId"`
|
||||
Type string `json:"type"`
|
||||
SubType string `json:"subType"`
|
||||
Title string `json:"title"`
|
||||
Method string `json:"method"`
|
||||
LicenseCode string `json:"licenseCode"`
|
||||
DeviceId string `json:"deviceId"`
|
||||
Project string `json:"project"`
|
||||
Params []Param `json:"params"`
|
||||
Phone string `json:"phone"`
|
||||
LicenseId string `json:"licenseId"`
|
||||
Type string `json:"type"`
|
||||
SubType string `json:"subType"`
|
||||
Title string `json:"title"`
|
||||
Method string `json:"method"`
|
||||
LicenseCode string `json:"licenseCode"`
|
||||
DeviceId string `json:"deviceId"`
|
||||
Project string `json:"project"`
|
||||
Params []Param `json:"params"`
|
||||
Phone string `json:"phone"`
|
||||
Task *AlertTaskMetadata `json:"task,omitempty"`
|
||||
}
|
||||
|
||||
type AlertTaskMetadata struct {
|
||||
AlertID uint `json:"alertId"`
|
||||
Type string `json:"type"`
|
||||
Quota string `json:"quota"`
|
||||
QuotaType string `json:"quotaType"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
type AlertRule struct {
|
||||
@@ -295,15 +306,19 @@ type OfflineQueryRequest struct {
|
||||
}
|
||||
|
||||
type AlertConfigUpdate struct {
|
||||
ID uint `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Config string `json:"config"`
|
||||
DisplayName string `json:"displayName"`
|
||||
ID uint `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Config string `json:"config"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Revision *time.Time `json:"revision"`
|
||||
}
|
||||
|
||||
type AlertConfigTest struct {
|
||||
ID uint `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Config string `json:"config"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Sender string `json:"sender"`
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package dto
|
||||
|
||||
const AlertCustomWebhookSchemaVersion = 1
|
||||
|
||||
type AlertConfigStatusUpdate struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Status string `json:"status" validate:"required,oneof=Enable Disable"`
|
||||
}
|
||||
|
||||
type AlertCustomWebhookSecretMutation struct {
|
||||
Action string `json:"action,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type AlertCustomWebhookURL struct {
|
||||
AlertCustomWebhookSecretMutation
|
||||
Configured bool `json:"configured"`
|
||||
Masked string `json:"masked,omitempty"`
|
||||
}
|
||||
|
||||
type AlertCustomWebhookBody struct {
|
||||
Type string `json:"type"`
|
||||
Template string `json:"template,omitempty"`
|
||||
Fields []AlertCustomWebhookFormField `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
type AlertCustomWebhookFormField struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type AlertCustomWebhookHeader struct {
|
||||
UID string `json:"uid"`
|
||||
Key string `json:"key"`
|
||||
Secret bool `json:"secret"`
|
||||
Action string `json:"action,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Configured bool `json:"configured,omitempty"`
|
||||
Masked string `json:"masked,omitempty"`
|
||||
}
|
||||
|
||||
type AlertCustomWebhookConfig struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
State string `json:"state,omitempty"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Preset string `json:"preset"`
|
||||
Method string `json:"method"`
|
||||
URL AlertCustomWebhookURL `json:"url"`
|
||||
Body AlertCustomWebhookBody `json:"body"`
|
||||
Headers []AlertCustomWebhookHeader `json:"headers"`
|
||||
}
|
||||
|
||||
type AlertCustomWebhookSecretConfig struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
}
|
||||
|
||||
type AlertCustomWebhookResolvedConfig struct {
|
||||
SchemaVersion int
|
||||
DisplayName string
|
||||
Preset string
|
||||
Method string
|
||||
URL string
|
||||
Body AlertCustomWebhookBody
|
||||
Headers []AlertCustomWebhookResolvedHeader
|
||||
}
|
||||
|
||||
type AlertCustomWebhookResolvedHeader struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
type AlertConfigTestResult struct {
|
||||
Success bool `json:"success"`
|
||||
StatusCode int `json:"statusCode,omitempty"`
|
||||
Duration int64 `json:"duration,omitempty"` // milliseconds
|
||||
Message string `json:"message,omitempty"`
|
||||
Response string `json:"response,omitempty"`
|
||||
}
|
||||
@@ -51,9 +51,10 @@ type CronjobOperate struct {
|
||||
Secret string `json:"secret"`
|
||||
Args string `json:"args"`
|
||||
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertMethod string `json:"alertMethod"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertMethod string `json:"alertMethod"`
|
||||
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
|
||||
|
||||
CleanLogConfig
|
||||
}
|
||||
@@ -126,7 +127,8 @@ type CronjobInfo struct {
|
||||
Secret string `json:"secret"`
|
||||
Args string `json:"args"`
|
||||
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTriggerMode string `json:"alertTriggerMode"`
|
||||
}
|
||||
|
||||
type CronjobImport struct {
|
||||
@@ -169,9 +171,10 @@ type CronjobTrans struct {
|
||||
SourceAccounts []string `json:"sourceAccounts"`
|
||||
DownloadAccount string `json:"downloadAccount"`
|
||||
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertMethod string `json:"alertMethod"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertMethod string `json:"alertMethod"`
|
||||
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
|
||||
}
|
||||
type TransHelper struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
+171
-137
@@ -1,11 +1,14 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||
)
|
||||
|
||||
type FirewallSubsystemStatus struct {
|
||||
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||
Name string `json:"name"`
|
||||
Backend string `json:"backend"`
|
||||
ConflictBackend string `json:"conflictBackend,omitempty"`
|
||||
@@ -16,7 +19,8 @@ type FirewallSubsystemStatus struct {
|
||||
Version string `json:"version"`
|
||||
PingStatus string `json:"pingStatus"`
|
||||
Message string `json:"message,omitempty"`
|
||||
SyncError string `json:"syncError,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
|
||||
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
||||
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
||||
}
|
||||
@@ -26,6 +30,11 @@ type FirewallLifecycleOperation struct {
|
||||
WithDockerRestart bool `json:"withDockerRestart"`
|
||||
}
|
||||
|
||||
type FirewallLifecycleOperationResponse struct {
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Queued bool `json:"queued"`
|
||||
}
|
||||
|
||||
type FirewallBackendOption struct {
|
||||
Name string `json:"name"`
|
||||
Installed bool `json:"installed"`
|
||||
@@ -33,6 +42,7 @@ type FirewallBackendOption struct {
|
||||
Initialized bool `json:"initialized"`
|
||||
Bound bool `json:"bound"`
|
||||
Supported bool `json:"supported"`
|
||||
SupportReason string `json:"supportReason,omitempty"`
|
||||
Implementation string `json:"implementation,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
||||
@@ -40,10 +50,13 @@ type FirewallBackendOption struct {
|
||||
}
|
||||
|
||||
type FirewallBackendFamilyStatus struct {
|
||||
Available bool `json:"available"`
|
||||
Initialized bool `json:"initialized"`
|
||||
Bound bool `json:"bound"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Partial bool `json:"partial"`
|
||||
Available bool `json:"available"`
|
||||
Initialized bool `json:"initialized"`
|
||||
Bound bool `json:"bound"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
ForwardPolicy string `json:"forwardPolicy,omitempty"`
|
||||
RAInterfaces []string `json:"raInterfaces,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallBackendGroup struct {
|
||||
@@ -53,11 +66,27 @@ type FirewallBackendGroup struct {
|
||||
}
|
||||
|
||||
type FirewallSettings struct {
|
||||
System FirewallBackendGroup `json:"system"`
|
||||
Forwarding FirewallBackendGroup `json:"forwarding"`
|
||||
Docker FirewallBackendGroup `json:"docker"`
|
||||
PingStatus string `json:"pingStatus"`
|
||||
PortWhitelist string `json:"portWhiteList"`
|
||||
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||
System FirewallBackendGroup `json:"system"`
|
||||
Forwarding FirewallBackendGroup `json:"forwarding"`
|
||||
Docker FirewallBackendGroup `json:"docker"`
|
||||
PingStatus string `json:"pingStatus"`
|
||||
PortWhitelist []filter.PortWhitelist `json:"portWhiteList"`
|
||||
PanelPort string `json:"panelPort"`
|
||||
SSHPort string `json:"sshPort"`
|
||||
}
|
||||
|
||||
type FirewallPortWhitelistCreate struct {
|
||||
Rule filter.PortWhitelist `json:"rule" validate:"required"`
|
||||
}
|
||||
|
||||
type FirewallPortWhitelistUpdate struct {
|
||||
OldRule filter.PortWhitelist `json:"oldRule" validate:"required"`
|
||||
Rule filter.PortWhitelist `json:"rule" validate:"required"`
|
||||
}
|
||||
|
||||
type FirewallPortWhitelistDelete struct {
|
||||
Rule *filter.PortWhitelist `json:"rule" validate:"required"`
|
||||
}
|
||||
|
||||
type FirewallBackendOperation struct {
|
||||
@@ -66,45 +95,78 @@ type FirewallBackendOperation struct {
|
||||
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"`
|
||||
}
|
||||
|
||||
type FirewallIPv6Operation struct {
|
||||
Status string `json:"status" validate:"required,oneof=Enable Disable"`
|
||||
}
|
||||
|
||||
type FirewallFamilyOperation struct {
|
||||
Subsystem string `json:"subsystem" validate:"required,oneof=system forwarding docker"`
|
||||
Backend string `json:"backend" validate:"required,oneof=iptables nftables"`
|
||||
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
|
||||
Operation string `json:"operation" validate:"required,oneof=initialize repair bind"`
|
||||
}
|
||||
|
||||
type FilterChainOperation struct {
|
||||
Name string `json:"name" validate:"required,eq=1PANEL_BASIC"`
|
||||
Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FirewallSystemPort struct {
|
||||
Family string
|
||||
Port string
|
||||
Protocol string
|
||||
type FilterChainOperationResponse struct {
|
||||
TaskID string `json:"taskID"`
|
||||
Queued bool `json:"queued"`
|
||||
}
|
||||
|
||||
type FirewallInitializationTask struct {
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FirewallSystemPort = firewall.SystemPort
|
||||
|
||||
type FirewallRuleInventoryResponse struct {
|
||||
Items []filter.InventoryItem `json:"items"`
|
||||
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
||||
IPv4Range filter.PositionRange `json:"ipv4Range"`
|
||||
IPv6Range filter.PositionRange `json:"ipv6Range"`
|
||||
Total int64 `json:"total"`
|
||||
AllTotal int64 `json:"allTotal"`
|
||||
Items []filter.InventoryItem `json:"items"`
|
||||
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleBackup struct {
|
||||
Name string `json:"name"`
|
||||
Provider filter.Provider `json:"provider"`
|
||||
RuleCount int `json:"ruleCount"`
|
||||
ModifiedAt int64 `json:"modifiedAt"`
|
||||
}
|
||||
|
||||
type FirewallRuleBackups struct {
|
||||
Directory string `json:"directory"`
|
||||
Files []FirewallRuleBackup `json:"files"`
|
||||
}
|
||||
|
||||
type FirewallRuleResetResponse struct {
|
||||
Removed int `json:"removed"`
|
||||
Disabled bool `json:"disabled"`
|
||||
BackupPath string `json:"backupPath"`
|
||||
Removed int `json:"removed"`
|
||||
Disabled bool `json:"disabled"`
|
||||
}
|
||||
|
||||
type FirewallRuleReset struct {
|
||||
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
||||
}
|
||||
|
||||
type FirewallRuleCheckResult struct {
|
||||
Decision filter.CheckDecision `json:"decision"`
|
||||
Classification filter.CheckClassification `json:"classification"`
|
||||
Reason string `json:"reason"`
|
||||
RequestedRule filter.FirewallRule `json:"requestedRule"`
|
||||
RequestedRuleKey string `json:"requestedRuleKey"`
|
||||
ExistingRuleUUID string `json:"existingRuleUUID,omitempty"`
|
||||
Candidates []filter.ObservedRule `json:"candidates,omitempty"`
|
||||
AllowedActions []filter.CheckAction `json:"allowedActions,omitempty"`
|
||||
CheckFlag string `json:"checkFlag"`
|
||||
Subsystem string `json:"subsystem,omitempty" validate:"omitempty,oneof=system forwarding docker"`
|
||||
Backup *bool `json:"backup,omitempty" default:"true"`
|
||||
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
||||
WithDockerRestart bool `json:"withDockerRestart"`
|
||||
}
|
||||
|
||||
type FirewallRuleInventory struct {
|
||||
Scope filter.Scope `json:"scope" validate:"required"`
|
||||
PageInfo
|
||||
Scope filter.Scope `json:"scope,omitempty"`
|
||||
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
|
||||
All bool `json:"all,omitempty"`
|
||||
Info string `json:"info"`
|
||||
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
|
||||
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
|
||||
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
|
||||
}
|
||||
|
||||
type FirewallNativeDetail struct {
|
||||
@@ -115,6 +177,7 @@ type FirewallNativeDetail struct {
|
||||
}
|
||||
|
||||
type DockerPortGuardBase struct {
|
||||
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
IsExist bool `json:"isExist"`
|
||||
@@ -127,6 +190,7 @@ type DockerPortGuardBase struct {
|
||||
}
|
||||
|
||||
type DockerPortGuardFamilyStatus struct {
|
||||
Partial bool `json:"partial"`
|
||||
State string `json:"state"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Initialized bool `json:"initialized"`
|
||||
@@ -135,20 +199,23 @@ type DockerPortGuardFamilyStatus struct {
|
||||
}
|
||||
|
||||
type DockerPortGuardEndpoint struct {
|
||||
Family string `json:"family"`
|
||||
HostIP string `json:"hostIP"`
|
||||
HostPort uint16 `json:"hostPort"`
|
||||
Protocol string `json:"protocol"`
|
||||
ContainerID string `json:"containerID"`
|
||||
ContainerName string `json:"containerName"`
|
||||
ContainerPort uint16 `json:"containerPort"`
|
||||
Compose string `json:"compose,omitempty"`
|
||||
Application string `json:"application,omitempty"`
|
||||
PolicyUUID string `json:"policyUUID,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Sources []string `json:"sources"`
|
||||
Effective bool `json:"effective"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Family string `json:"family"`
|
||||
HostIP string `json:"hostIP"`
|
||||
HostPort uint16 `json:"hostPort"`
|
||||
Protocol string `json:"protocol"`
|
||||
ContainerID string `json:"containerID"`
|
||||
ContainerName string `json:"containerName"`
|
||||
ContainerState string `json:"containerState,omitempty"`
|
||||
ContainerPort uint16 `json:"containerPort"`
|
||||
Compose string `json:"compose,omitempty"`
|
||||
Application string `json:"application,omitempty"`
|
||||
PolicyUUID string `json:"policyUUID,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Sources []string `json:"sources"`
|
||||
Effective bool `json:"effective"`
|
||||
TrafficPath string `json:"trafficPath"`
|
||||
ManagementTarget string `json:"managementTarget"`
|
||||
ManagementReason string `json:"managementReason,omitempty"`
|
||||
}
|
||||
|
||||
type DockerPortGuardPortGroup struct {
|
||||
@@ -181,47 +248,42 @@ type DockerPortGuardEndpointIdentity struct {
|
||||
}
|
||||
|
||||
type DockerPortGuardPolicyBatch struct {
|
||||
Endpoints []DockerPortGuardEndpointIdentity `json:"endpoints" validate:"required,min=1,max=256,dive"`
|
||||
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all"`
|
||||
Sources []string `json:"sources" validate:"max=256,dive,required,max=64"`
|
||||
Description string `json:"description" validate:"max=256"`
|
||||
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
|
||||
Import bool `json:"import"`
|
||||
}
|
||||
|
||||
type DockerPortGuardPolicyBatchDelete struct {
|
||||
UUIDs []string `json:"uuids" validate:"required,min=1,max=256,dive,required,max=64"`
|
||||
UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"`
|
||||
}
|
||||
|
||||
type DockerPortGuardPolicy struct {
|
||||
DockerPortGuardEndpointIdentity
|
||||
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all accept_sources accept_all"`
|
||||
Sources []string `json:"sources" validate:"dive,required,max=64"`
|
||||
}
|
||||
|
||||
type DockerPortGuardOperation struct {
|
||||
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
|
||||
}
|
||||
|
||||
type FirewallRuleCheckItem struct {
|
||||
UUID string `json:"uuid" validate:"omitempty,max=64"`
|
||||
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
||||
}
|
||||
|
||||
type FirewallRuleCheck struct {
|
||||
Items []FirewallRuleCheckItem `json:"items" validate:"required,min=1,max=256,dive"`
|
||||
}
|
||||
|
||||
type FirewallRuleCheckResponse struct {
|
||||
Items []FirewallRuleCheckResult `json:"items"`
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FirewallRuleCreateItem struct {
|
||||
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
||||
CheckFlag string `json:"checkFlag"`
|
||||
Action filter.CheckAction `json:"action"`
|
||||
AdoptInstanceKey string `json:"adoptInstanceKey"`
|
||||
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
|
||||
SourceID string `json:"sourceID"`
|
||||
Raw string `json:"raw,omitempty"`
|
||||
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
|
||||
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
||||
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
|
||||
}
|
||||
|
||||
type FirewallRuleCreate struct {
|
||||
Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,max=256,dive"`
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
Initialize bool `json:"initialize"`
|
||||
Items []FirewallRuleCreateItem `json:"items" validate:"dive"`
|
||||
}
|
||||
|
||||
type FirewallRuleCreateResponse struct {
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Queued bool `json:"queued,omitempty"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
Skipped int `json:"skipped"`
|
||||
@@ -235,86 +297,58 @@ type FirewallRuleCreateFailure struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncRequest struct {
|
||||
Subsystem string `json:"subsystem" validate:"omitempty,oneof=system forwarding docker"`
|
||||
SourceProvider filter.Provider `json:"sourceProvider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
||||
TargetProvider filter.Provider `json:"targetProvider" validate:"required,oneof=firewalld ufw iptables nftables"`
|
||||
ResetSource bool `json:"resetSource"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncItem struct {
|
||||
SourceUUID string `json:"sourceUUID"`
|
||||
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
||||
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
|
||||
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
|
||||
Status firewallsync.Status `json:"status"`
|
||||
ReasonCode firewallsync.ReasonCode `json:"reasonCode,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncPreview struct {
|
||||
Subsystem string `json:"subsystem"`
|
||||
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
|
||||
TargetProvider filter.Provider `json:"targetProvider"`
|
||||
Total int `json:"total"`
|
||||
Ready int `json:"ready"`
|
||||
Existing int `json:"existing"`
|
||||
Removed int `json:"removed"`
|
||||
Blocked int `json:"blocked"`
|
||||
Items []FirewallRuleSyncItem `json:"items"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncResult struct {
|
||||
Subsystem string `json:"subsystem"`
|
||||
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
|
||||
TargetProvider filter.Provider `json:"targetProvider"`
|
||||
Total int `json:"total"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Skipped int `json:"skipped"`
|
||||
Removed int `json:"removed"`
|
||||
Failed int `json:"failed"`
|
||||
Errors []FirewallRuleSyncFailure `json:"errors,omitempty"`
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Queued bool `json:"queued,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncTask struct {
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Executing bool `json:"executing"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncFailure struct {
|
||||
SourceUUID string `json:"sourceUUID"`
|
||||
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
||||
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
|
||||
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type FirewallRuleDelete struct {
|
||||
UUIDs []string `json:"uuids" validate:"required,min=1,max=256,dive,required,max=64"`
|
||||
Targets []FirewallRuleDeleteItem `json:"targets" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteItem struct {
|
||||
FirewallRuleDeleteTarget
|
||||
Observed filter.ObservedRule `json:"observed" validate:"required"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteTarget struct {
|
||||
Scope filter.Scope `json:"scope" validate:"required"`
|
||||
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteResponse struct {
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Queued bool `json:"queued,omitempty"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
Errors []FirewallRuleDeleteFailure `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteFailure struct {
|
||||
Index int `json:"index"`
|
||||
UUID string `json:"uuid"`
|
||||
Error string `json:"error"`
|
||||
Index int `json:"index"`
|
||||
InstanceKey string `json:"instanceKey"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type FirewallRuleUpdate struct {
|
||||
UUID string `json:"uuid" validate:"required,max=64"`
|
||||
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
||||
FirewallRuleDeleteTarget
|
||||
Rule *filter.FirewallRule `json:"rule,omitempty" validate:"required_without_all=Description OrderIndex Priority,excluded_with=Description OrderIndex Priority"`
|
||||
Description *string `json:"description,omitempty" validate:"excluded_with=Rule"`
|
||||
OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"`
|
||||
Priority *int `json:"priority,omitempty" validate:"excluded_with=Rule OrderIndex"`
|
||||
}
|
||||
|
||||
type FirewallRuleReorder struct {
|
||||
UUID string `json:"uuid" validate:"required,max=64"`
|
||||
FirewallRuleDeleteTarget
|
||||
TargetPosition *int64 `json:"targetPosition"`
|
||||
Priority *int `json:"priority"`
|
||||
}
|
||||
|
||||
type FirewallRuleExportItem struct {
|
||||
filter.FirewallRule
|
||||
Raw string `json:"raw,omitempty"`
|
||||
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallSubsystemBackup struct {
|
||||
Families []string `json:"families,omitempty"`
|
||||
Subsystem string `json:"subsystem"`
|
||||
Provider filter.Provider `json:"provider"`
|
||||
Forwarding []forwarding.Rule `json:"forwarding"`
|
||||
Docker *dockerfirewall.PolicyInventory `json:"docker,omitempty"`
|
||||
}
|
||||
|
||||
@@ -2,13 +2,12 @@ package dto
|
||||
|
||||
type ForwardRuleSearch struct {
|
||||
PageInfo
|
||||
All bool `json:"all,omitempty"`
|
||||
Info string `json:"info"`
|
||||
Status string `json:"status"`
|
||||
Strategy string `json:"strategy"`
|
||||
}
|
||||
|
||||
// ForwardRule preserves the existing firewall search response shape while
|
||||
// keeping forwarding data separate from the filter client model.
|
||||
type ForwardRule struct {
|
||||
ID uint `json:"id"`
|
||||
Chain string `json:"chain"`
|
||||
@@ -25,15 +24,11 @@ type ForwardRule struct {
|
||||
|
||||
UsedStatus string `json:"usedStatus"`
|
||||
Description string `json:"description"`
|
||||
|
||||
IsDesired bool `json:"isDesired"`
|
||||
IsRuntime bool `json:"isRuntime"`
|
||||
SyncStatus string `json:"syncStatus"`
|
||||
}
|
||||
|
||||
type ForwardRuleOperate struct {
|
||||
ForceDelete bool `json:"forceDelete"`
|
||||
Rules []ForwardRuleOperation `json:"rules"`
|
||||
Import bool `json:"import"`
|
||||
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type ForwardRuleOperation struct {
|
||||
|
||||
+87
-18
@@ -1,6 +1,10 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
)
|
||||
|
||||
type MonitorSearch struct {
|
||||
Param string `json:"param" validate:"required,oneof=all cpu memory load io network"`
|
||||
@@ -26,24 +30,33 @@ type Process struct {
|
||||
}
|
||||
|
||||
type MonitorSetting struct {
|
||||
MonitorStatus string `json:"monitorStatus"`
|
||||
MonitorStoreDays string `json:"monitorStoreDays"`
|
||||
MonitorInterval string `json:"monitorInterval"`
|
||||
DefaultNetwork string `json:"defaultNetwork"`
|
||||
DefaultIO string `json:"defaultIO"`
|
||||
GPUMonitorStatus string `json:"gpuMonitorStatus"`
|
||||
GPUMonitorStoreDays string `json:"gpuMonitorStoreDays"`
|
||||
GPUMonitorInterval string `json:"gpuMonitorInterval"`
|
||||
VLLMMonitorStatus string `json:"vllmMonitorStatus"`
|
||||
VLLMMonitorStoreDays string `json:"vllmMonitorStoreDays"`
|
||||
VLLMMonitorInterval string `json:"vllmMonitorInterval"`
|
||||
MonitorStatus string `json:"monitorStatus"`
|
||||
MonitorStoreDays string `json:"monitorStoreDays"`
|
||||
MonitorInterval string `json:"monitorInterval"`
|
||||
DefaultNetwork string `json:"defaultNetwork"`
|
||||
DefaultIO string `json:"defaultIO"`
|
||||
}
|
||||
|
||||
type MonitorSettingUpdate struct {
|
||||
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval DefaultNetwork DefaultIO"`
|
||||
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval GPUMonitorStatus GPUMonitorStoreDays GPUMonitorInterval VLLMMonitorStatus VLLMMonitorStoreDays VLLMMonitorInterval DefaultNetwork DefaultIO"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type MonitorGPUOptions struct {
|
||||
Supported bool `json:"supported"`
|
||||
GPUType string `json:"gpuType"`
|
||||
ChartHide []GPUChartHide `json:"chartHide"`
|
||||
Options []string `json:"options"`
|
||||
}
|
||||
type GPUChartHide struct {
|
||||
DeviceID string `json:"deviceID"`
|
||||
Legacy bool `json:"legacy"`
|
||||
ProductName string `json:"productName"`
|
||||
Type string `json:"type"`
|
||||
Process bool `json:"process"`
|
||||
@@ -55,23 +68,54 @@ type GPUChartHide struct {
|
||||
Speed bool `json:"speed"`
|
||||
}
|
||||
type MonitorGPUSearch struct {
|
||||
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
|
||||
DeviceID string `json:"deviceID"`
|
||||
Legacy bool `json:"legacy"`
|
||||
ProductName string `json:"productName"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
}
|
||||
type MonitorGPUData struct {
|
||||
Date []time.Time `json:"date"`
|
||||
GPUValue []float64 `json:"gpuValue"`
|
||||
TemperatureValue []float64 `json:"temperatureValue"`
|
||||
PowerTotal []float64 `json:"powerTotal"`
|
||||
PowerUsed []float64 `json:"powerUsed"`
|
||||
PowerPercent []float64 `json:"powerPercent"`
|
||||
MemoryTotal []float64 `json:"memoryTotal"`
|
||||
MemoryUsed []float64 `json:"memoryUsed"`
|
||||
MemoryPercent []float64 `json:"memoryPercent"`
|
||||
SpeedValue []int `json:"speedValue"`
|
||||
MemoryActivity []*float64 `json:"memoryActivity"`
|
||||
EncoderUtil []*float64 `json:"encoderUtil"`
|
||||
DecoderUtil []*float64 `json:"decoderUtil"`
|
||||
JPEGUtil []*float64 `json:"jpegUtil"`
|
||||
OFAUtil []*float64 `json:"ofaUtil"`
|
||||
MediaUtil []*float64 `json:"mediaUtil"`
|
||||
ComputeUtil []*float64 `json:"computeUtil"`
|
||||
CopyUtil []*float64 `json:"copyUtil"`
|
||||
HotspotTemperature []*float64 `json:"hotspotTemperature"`
|
||||
FanRPM []*float64 `json:"fanRPM"`
|
||||
AICPUUtil []*float64 `json:"aiCPUUtil"`
|
||||
CtrlCPUUtil []*float64 `json:"ctrlCPUUtil"`
|
||||
DDRUsed []*float64 `json:"ddrUsed"`
|
||||
DDRTotal []*float64 `json:"ddrTotal"`
|
||||
HBMUsed []*float64 `json:"hbmUsed"`
|
||||
HBMTotal []*float64 `json:"hbmTotal"`
|
||||
DDRBandwidth []*float64 `json:"ddrBandwidth"`
|
||||
HBMBandwidth []*float64 `json:"hbmBandwidth"`
|
||||
MemoryBandwidth []*float64 `json:"memoryBandwidth"`
|
||||
MediaFrequency []*float64 `json:"mediaFrequency"`
|
||||
HugepagesUsed []*float64 `json:"hugepagesUsed"`
|
||||
HugepagesTotal []*float64 `json:"hugepagesTotal"`
|
||||
|
||||
ProcessCount []int `json:"processCount"`
|
||||
BucketSeconds int64 `json:"bucketSeconds"`
|
||||
SampleCount int64 `json:"sampleCount"`
|
||||
MemoryTemperatureValue []*float64 `json:"memoryTemperatureValue"`
|
||||
FrequencyValue []*float64 `json:"frequencyValue"`
|
||||
MemoryFrequencyValue []*float64 `json:"memoryFrequencyValue"`
|
||||
Date []time.Time `json:"date"`
|
||||
GPUValue []*float64 `json:"gpuValue"`
|
||||
TemperatureValue []*float64 `json:"temperatureValue"`
|
||||
PowerTotal []*float64 `json:"powerTotal"`
|
||||
PowerUsed []*float64 `json:"powerUsed"`
|
||||
PowerPercent []*float64 `json:"powerPercent"`
|
||||
MemoryTotal []*float64 `json:"memoryTotal"`
|
||||
MemoryUsed []*float64 `json:"memoryUsed"`
|
||||
MemoryPercent []*float64 `json:"memoryPercent"`
|
||||
SpeedValue []*float64 `json:"speedValue"`
|
||||
|
||||
ProcessCount []*float64 `json:"processCount"`
|
||||
GPUProcesses [][]GPUProcess `json:"gpuProcesses"`
|
||||
}
|
||||
|
||||
@@ -81,3 +125,28 @@ type GPUProcess struct {
|
||||
ProcessName string `json:"processName"`
|
||||
UsedMemory string `json:"usedMemory"`
|
||||
}
|
||||
|
||||
type MonitorVLLMSearch struct {
|
||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||
StartTime time.Time `json:"startTime" validate:"required"`
|
||||
EndTime time.Time `json:"endTime" validate:"required"`
|
||||
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
|
||||
}
|
||||
|
||||
type MonitorVLLMData struct {
|
||||
SampleCount int64 `json:"sampleCount"`
|
||||
BucketSeconds int64 `json:"bucketSeconds"`
|
||||
Points []model.MonitorVLLM `json:"points"`
|
||||
}
|
||||
|
||||
type MonitorVLLMCurrent struct {
|
||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||
}
|
||||
|
||||
type MonitorVLLMClean struct {
|
||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||
}
|
||||
|
||||
type MonitorClean struct {
|
||||
Type string `json:"type" validate:"required,oneof=host gpu"`
|
||||
}
|
||||
|
||||
@@ -51,13 +51,19 @@ const (
|
||||
CACHE NginxKey = "cache"
|
||||
HttpPer NginxKey = "http-per"
|
||||
ProxyCache NginxKey = "proxy-cache"
|
||||
Brotli NginxKey = "brotli"
|
||||
)
|
||||
|
||||
// BrotliKeys are served from the panel-managed http.d file rather than
|
||||
// nginx.conf, because the module is optional: its directives must disappear
|
||||
// together with the module, otherwise nginx refuses to start.
|
||||
var BrotliKeys = []string{"brotli", "brotli_comp_level", "brotli_min_length", "brotli_types"}
|
||||
|
||||
var ScopeKeyMap = map[NginxKey][]string{
|
||||
Index: {"index"},
|
||||
LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"},
|
||||
SSL: {"ssl_certificate", "ssl_certificate_key"},
|
||||
HttpPer: {"server_names_hash_bucket_size", "client_header_buffer_size", "client_max_body_size", "keepalive_timeout", "gzip", "gzip_min_length", "gzip_comp_level"},
|
||||
HttpPer: {"server_names_hash_bucket_size", "client_header_buffer_size", "client_max_body_size", "keepalive_timeout", "gzip", "gzip_min_length", "gzip_comp_level", "gzip_types", "gzip_vary", "gzip_proxied"},
|
||||
}
|
||||
|
||||
var StaticFileKeyMap = map[NginxKey]struct {
|
||||
|
||||
@@ -50,6 +50,10 @@ type AppContainerConfig struct {
|
||||
Type string `json:"type"`
|
||||
SpecifyIP string `json:"specifyIP"`
|
||||
RestartPolicy string `json:"restartPolicy" validate:"omitempty,oneof=always unless-stopped no on-failure"`
|
||||
|
||||
KeepServiceName bool `json:"-"`
|
||||
SkipComposeCommonConfig bool `json:"-"`
|
||||
UseLifecycleScripts bool `json:"-"`
|
||||
}
|
||||
|
||||
type AppInstalledSearch struct {
|
||||
@@ -92,6 +96,8 @@ type AppInstalledOperate struct {
|
||||
TaskID string `json:"taskID"`
|
||||
DeleteImage bool `json:"deleteImage"`
|
||||
Favorite bool `json:"favorite"`
|
||||
|
||||
UseLifecycleScripts bool `json:"-"`
|
||||
}
|
||||
|
||||
type AppInstallUpgrade struct {
|
||||
@@ -111,11 +117,14 @@ type AppInstallDelete struct {
|
||||
DeleteDB bool `json:"deleteDB"`
|
||||
DeleteImage bool `json:"deleteImage"`
|
||||
TaskID string `json:"taskID"`
|
||||
|
||||
UseLifecycleScripts bool `json:"-"`
|
||||
}
|
||||
|
||||
type AppInstalledUpdate struct {
|
||||
InstallId uint `json:"installId" validate:"required"`
|
||||
Params map[string]interface{} `json:"params" validate:"required"`
|
||||
TaskID string `json:"-"`
|
||||
AppContainerConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ type FileWget struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
IgnoreCertificate bool `json:"ignoreCertificate"`
|
||||
UseProxy bool `json:"useProxy"`
|
||||
UseServerFilename bool `json:"useServerFilename"`
|
||||
}
|
||||
|
||||
type FileMove struct {
|
||||
@@ -158,6 +159,10 @@ type FileProcessReq struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type FileProcessRemoveReq struct {
|
||||
Keys []string `json:"keys" validate:"required,min=1,max=1000"`
|
||||
}
|
||||
|
||||
type FileRoleUpdate struct {
|
||||
Path string `json:"path" validate:"required"`
|
||||
User string `json:"user" validate:"required"`
|
||||
|
||||
@@ -66,14 +66,15 @@ type RuntimeDelete struct {
|
||||
}
|
||||
|
||||
type RuntimeUpdate struct {
|
||||
Name string `json:"name"`
|
||||
ID uint `json:"id"`
|
||||
Image string `json:"image"`
|
||||
Version string `json:"version"`
|
||||
Rebuild bool `json:"rebuild"`
|
||||
Source string `json:"source"`
|
||||
CodeDir string `json:"codeDir"`
|
||||
Remark string `json:"remark"`
|
||||
AppDetailID uint `json:"appDetailId"`
|
||||
Name string `json:"name"`
|
||||
ID uint `json:"id"`
|
||||
Image string `json:"image"`
|
||||
Version string `json:"version"`
|
||||
Rebuild bool `json:"rebuild"`
|
||||
Source string `json:"source"`
|
||||
CodeDir string `json:"codeDir"`
|
||||
Remark string `json:"remark"`
|
||||
|
||||
Params map[string]interface{} `json:"params"`
|
||||
NodeConfig
|
||||
|
||||
@@ -17,6 +17,17 @@ type NginxParam struct {
|
||||
Params []string `json:"params"`
|
||||
}
|
||||
|
||||
// NginxBrotliRes carries the brotli settings together with where they live.
|
||||
// ManagedExternally is true when the user defined brotli by hand, in which
|
||||
// case the panel only reports the values and must not write its own copy.
|
||||
// ManagedUnavailable is true when the panel could not wire the managed
|
||||
// configuration into nginx.conf at all, so the reported values are inert.
|
||||
type NginxBrotliRes struct {
|
||||
Params []NginxParam `json:"params"`
|
||||
ManagedExternally bool `json:"managedExternally"`
|
||||
ManagedUnavailable bool `json:"managedUnavailable"`
|
||||
}
|
||||
|
||||
type NginxAuthRes struct {
|
||||
Enable bool `json:"enable"`
|
||||
Items []dto.NginxAuth `json:"items"`
|
||||
|
||||
@@ -35,7 +35,7 @@ type SettingUpdate struct {
|
||||
}
|
||||
|
||||
type AgentSettingUpdate struct {
|
||||
Key string `json:"key" validate:"required,oneof=SystemIP DockerSockPath FileRecycleBin FirewallPortWhiteList"`
|
||||
Key string `json:"key" validate:"required,oneof=SystemIP DockerSockPath FileRecycleBin"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ type RootCertOperate struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Mode string `json:"mode"`
|
||||
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
|
||||
EncryptionMode string `json:"encryptionMode"`
|
||||
PassPhrase string `json:"passPhrase"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package dto
|
||||
|
||||
type TerminalSessionClose struct {
|
||||
ID string `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type TerminalSessionRevoke struct {
|
||||
Scope string `json:"scope" validate:"required,oneof=auth_session user all"`
|
||||
UserID string `json:"userId"`
|
||||
AuthSessionID string `json:"authSessionId"`
|
||||
}
|
||||
+27
-10
@@ -1,5 +1,12 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Alert struct {
|
||||
BaseModel
|
||||
|
||||
@@ -18,10 +25,11 @@ type Alert struct {
|
||||
|
||||
type AlertTask struct {
|
||||
BaseModel
|
||||
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
||||
Quota string `gorm:"type:varchar(64)" json:"quota"`
|
||||
QuotaType string `gorm:"type:varchar(64)" json:"quotaType"`
|
||||
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
|
||||
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
||||
Quota string `gorm:"type:varchar(64)" json:"quota"`
|
||||
QuotaType string `gorm:"type:varchar(64)" json:"quotaType"`
|
||||
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
|
||||
DeliveryLogID *uint `gorm:"uniqueIndex" json:"-"`
|
||||
}
|
||||
|
||||
type AlertLog struct {
|
||||
@@ -41,12 +49,21 @@ type AlertLog struct {
|
||||
|
||||
type AlertConfig struct {
|
||||
BaseModel
|
||||
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
||||
Title string `gorm:"type:varchar(64);not null" json:"title"`
|
||||
Status string `gorm:"type:varchar(64);not null" json:"status"`
|
||||
Config string `gorm:"type:varchar(256);not null" json:"config"`
|
||||
CreateUser string `gorm:"type:varchar(256)" json:"createUser"`
|
||||
UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"`
|
||||
UID string `gorm:"type:varchar(64);not null;uniqueIndex" json:"uid"`
|
||||
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
||||
Title string `gorm:"type:varchar(64);not null" json:"title"`
|
||||
Status string `gorm:"type:varchar(64);not null" json:"status"`
|
||||
Config string `gorm:"type:text;not null" json:"config"`
|
||||
SecretConfig string `gorm:"type:text;not null;default:''" json:"-"`
|
||||
CreateUser string `gorm:"type:varchar(256)" json:"createUser"`
|
||||
UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"`
|
||||
}
|
||||
|
||||
func (a *AlertConfig) BeforeCreate(_ *gorm.DB) error {
|
||||
if strings.TrimSpace(a.UID) == "" {
|
||||
a.UID = uuid.NewString()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type LoginLog struct {
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
)
|
||||
|
||||
const FirewallRuleSequenceStep int64 = 1 << 32
|
||||
|
||||
type DockerPortGuardPolicy struct {
|
||||
BaseModel
|
||||
|
||||
UUID string `gorm:"size:64;not null;uniqueIndex" json:"uuid"`
|
||||
Family string `gorm:"size:16;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"family"`
|
||||
HostIP string `gorm:"size:64;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"`
|
||||
HostPort uint16 `gorm:"not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"`
|
||||
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"`
|
||||
Mode string `gorm:"size:32;not null" json:"mode"`
|
||||
Sources string `gorm:"type:text" json:"-"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
}
|
||||
|
||||
type ForwardingRule struct {
|
||||
BaseModel
|
||||
|
||||
Family string `gorm:"size:16;not null;uniqueIndex:idx_forwarding_rule_identity" json:"family"`
|
||||
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_forwarding_rule_identity" json:"protocol"`
|
||||
Port string `gorm:"size:32;not null;uniqueIndex:idx_forwarding_rule_identity" json:"port"`
|
||||
TargetIP string `gorm:"size:64;not null;uniqueIndex:idx_forwarding_rule_identity" json:"targetIP"`
|
||||
TargetPort string `gorm:"size:32;not null;uniqueIndex:idx_forwarding_rule_identity" json:"targetPort"`
|
||||
Interface string `gorm:"size:32;not null;default:'';uniqueIndex:idx_forwarding_rule_identity" json:"interface"`
|
||||
}
|
||||
|
||||
type FirewallRule struct {
|
||||
UUID string `gorm:"size:64;primaryKey" json:"uuid"`
|
||||
Family string `gorm:"size:16;not null" json:"family"`
|
||||
|
||||
Protocol string `gorm:"size:32;not null" json:"protocol"`
|
||||
SourceAddress string `gorm:"size:255" json:"sourceAddress"`
|
||||
SourcePort string `gorm:"size:64" json:"sourcePort"`
|
||||
DestinationAddress string `gorm:"size:255" json:"destinationAddress"`
|
||||
DestinationPort string `gorm:"size:64" json:"destinationPort"`
|
||||
Interface string `gorm:"size:128" json:"interface"`
|
||||
ConnectionStates string `gorm:"type:text" json:"connectionStates"`
|
||||
Action string `gorm:"size:32;not null" json:"action"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
CompatibilityError string `gorm:"type:text" json:"compatibilityError,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
Sequence *int64 `gorm:"index" json:"sequence,omitempty"`
|
||||
|
||||
Origin string `gorm:"size:32;not null" json:"origin"`
|
||||
Owner string `gorm:"size:320;not null" json:"owner"`
|
||||
Revision uint `gorm:"not null;default:1" json:"revision"`
|
||||
}
|
||||
|
||||
func FirewallRuleOwner(sourceKind, sourceID string) string {
|
||||
sourceKind = strings.TrimSpace(sourceKind)
|
||||
sourceID = strings.TrimSpace(sourceID)
|
||||
if sourceID == "" {
|
||||
return sourceKind
|
||||
}
|
||||
return sourceKind + ":" + sourceID
|
||||
}
|
||||
|
||||
func FirewallRulesRevision(rules []FirewallRule) (string, error) {
|
||||
ordered := append([]FirewallRule(nil), rules...)
|
||||
sort.Slice(ordered, func(i, j int) bool {
|
||||
return ordered[i].UUID < ordered[j].UUID
|
||||
})
|
||||
payload, err := json.Marshal(ordered)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sum := sha256.Sum256(payload)
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
|
||||
func FirewallRuleFromDomain(rule filter.FirewallRule) (FirewallRule, error) {
|
||||
normalized, err := filter.NormalizeRule(rule)
|
||||
if err != nil {
|
||||
return FirewallRule{}, err
|
||||
}
|
||||
switch normalized.NativeKind {
|
||||
case "", filter.NativeKindRule, filter.NativeKindZonePort, filter.NativeKindRichRule, filter.NativeKindUFWRule:
|
||||
default:
|
||||
return FirewallRule{}, fmt.Errorf("%w: native rule %q cannot be stored as a provider-neutral policy", filter.ErrUnsupportedScope, normalized.NativeKind)
|
||||
}
|
||||
record := FirewallRule{
|
||||
Family: string(normalized.Scope.Family),
|
||||
Protocol: normalized.Protocol,
|
||||
SourceAddress: normalized.SourceAddress,
|
||||
SourcePort: normalized.SourcePort,
|
||||
DestinationAddress: normalized.DestinationAddress,
|
||||
DestinationPort: normalized.DestinationPort,
|
||||
Interface: normalized.Interface,
|
||||
ConnectionStates: strings.Join(normalized.ConnectionStates, ","),
|
||||
Action: string(normalized.Action),
|
||||
Description: normalized.Description,
|
||||
}
|
||||
if normalized.Scope.Provider == filter.ProviderFirewalld {
|
||||
record.Priority = normalized.Priority
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (rule FirewallRule) PolicyKey() string {
|
||||
payload, _ := json.Marshal(struct {
|
||||
Family string `json:"family"`
|
||||
Protocol string `json:"protocol"`
|
||||
SourceAddress string `json:"sourceAddress,omitempty"`
|
||||
SourcePort string `json:"sourcePort,omitempty"`
|
||||
DestinationAddress string `json:"destinationAddress,omitempty"`
|
||||
DestinationPort string `json:"destinationPort,omitempty"`
|
||||
Interface string `json:"interface,omitempty"`
|
||||
ConnectionStates string `json:"connectionStates,omitempty"`
|
||||
Action string `json:"action"`
|
||||
}{
|
||||
Family: rule.Family, Protocol: rule.Protocol,
|
||||
SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort,
|
||||
DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort,
|
||||
Interface: rule.Interface, ConnectionStates: rule.ConnectionStates, Action: rule.Action,
|
||||
})
|
||||
sum := sha256.Sum256(payload)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func (rule FirewallRule) RulesForProvider(provider filter.Provider) ([]filter.FirewallRule, error) {
|
||||
if rule.CompatibilityError != "" {
|
||||
return nil, fmt.Errorf("%w: %s", filter.ErrUnsupportedScope, rule.CompatibilityError)
|
||||
}
|
||||
connectionStates := make([]string, 0)
|
||||
if rule.ConnectionStates != "" {
|
||||
connectionStates = strings.Split(rule.ConnectionStates, ",")
|
||||
}
|
||||
base := filter.FirewallRule{
|
||||
Protocol: rule.Protocol, SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort,
|
||||
DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort,
|
||||
Interface: rule.Interface, ConnectionStates: connectionStates,
|
||||
Action: filter.Action(rule.Action), Description: rule.Description,
|
||||
}
|
||||
if provider == filter.ProviderFirewalld {
|
||||
base.Priority = rule.Priority
|
||||
}
|
||||
families := []filter.Family{filter.Family(rule.Family)}
|
||||
if provider != filter.ProviderFirewalld && families[0] == filter.FamilyInet {
|
||||
hasIPv4, hasIPv6 := ruleAddressFamilies(base)
|
||||
switch {
|
||||
case hasIPv4 && hasIPv6:
|
||||
return nil, fmt.Errorf("%w: inet policy contains both IPv4 and IPv6 addresses", filter.ErrUnsupportedScope)
|
||||
case hasIPv6 || strings.EqualFold(base.Protocol, "icmpv6"):
|
||||
families = []filter.Family{filter.FamilyIPv6}
|
||||
case hasIPv4:
|
||||
families = []filter.Family{filter.FamilyIPv4}
|
||||
default:
|
||||
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
|
||||
}
|
||||
}
|
||||
result := make([]filter.FirewallRule, 0, len(families))
|
||||
for _, family := range families {
|
||||
compiled := base
|
||||
compiled.Scope = filter.Scope{Provider: provider, Family: family, Direction: filter.DirectionInput}
|
||||
switch provider {
|
||||
case filter.ProviderIptables, filter.ProviderNftables:
|
||||
compiled.Scope.Table, compiled.Scope.Chain = "filter", filter.IptablesInputChain
|
||||
case filter.ProviderFirewalld:
|
||||
compiled.Scope.Zone = filter.FirewalldInputZone
|
||||
case filter.ProviderUFW:
|
||||
compiled.Scope.Chain = filter.UFWInputChain
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: unsupported firewall provider %q", filter.ErrProviderUnavailable, provider)
|
||||
}
|
||||
expanded, err := filter.ExpandAtomicRules(compiled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, expanded...)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func SortFirewallRules(rules []FirewallRule, provider filter.Provider) {
|
||||
sort.SliceStable(rules, func(i, j int) bool {
|
||||
left, right := rules[i], rules[j]
|
||||
if provider == filter.ProviderFirewalld {
|
||||
switch {
|
||||
case left.Priority == nil && right.Priority != nil:
|
||||
return false
|
||||
case left.Priority != nil && right.Priority == nil:
|
||||
return true
|
||||
case left.Priority != nil && right.Priority != nil && *left.Priority != *right.Priority:
|
||||
return *left.Priority < *right.Priority
|
||||
}
|
||||
} else {
|
||||
switch {
|
||||
case left.Sequence == nil && right.Sequence != nil:
|
||||
return false
|
||||
case left.Sequence != nil && right.Sequence == nil:
|
||||
return true
|
||||
case left.Sequence != nil && right.Sequence != nil && *left.Sequence != *right.Sequence:
|
||||
return *left.Sequence < *right.Sequence
|
||||
}
|
||||
}
|
||||
return left.UUID < right.UUID
|
||||
})
|
||||
}
|
||||
|
||||
func ruleAddressFamilies(rule filter.FirewallRule) (bool, bool) {
|
||||
hasIPv4, hasIPv6 := false, false
|
||||
for _, address := range []string{rule.SourceAddress, rule.DestinationAddress} {
|
||||
address = strings.TrimSpace(address)
|
||||
if address == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(address, ":") {
|
||||
hasIPv6 = true
|
||||
} else {
|
||||
hasIPv4 = true
|
||||
}
|
||||
}
|
||||
return hasIPv4, hasIPv6
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type MonitorBase struct {
|
||||
BaseModel
|
||||
Cpu float64 `json:"cpu"`
|
||||
@@ -33,14 +35,78 @@ type MonitorNetwork struct {
|
||||
}
|
||||
|
||||
type MonitorGPU struct {
|
||||
MemoryUtil *float64 `json:"memoryUtil"`
|
||||
MemoryActivity *float64 `json:"memoryActivity"`
|
||||
EncoderUtil *float64 `json:"encoderUtil"`
|
||||
DecoderUtil *float64 `json:"decoderUtil"`
|
||||
JPEGUtil *float64 `json:"jpegUtil"`
|
||||
OFAUtil *float64 `json:"ofaUtil"`
|
||||
MediaUtil *float64 `json:"mediaUtil"`
|
||||
ComputeUtil *float64 `json:"computeUtil"`
|
||||
CopyUtil *float64 `json:"copyUtil"`
|
||||
HotspotTemperature *float64 `json:"hotspotTemperature"`
|
||||
FanRPM *float64 `json:"fanRPM"`
|
||||
AICPUUtil *float64 `json:"aiCPUUtil"`
|
||||
CtrlCPUUtil *float64 `json:"ctrlCPUUtil"`
|
||||
DDRUsed *float64 `json:"ddrUsed"`
|
||||
DDRTotal *float64 `json:"ddrTotal"`
|
||||
HBMUsed *float64 `json:"hbmUsed"`
|
||||
HBMTotal *float64 `json:"hbmTotal"`
|
||||
DDRBandwidth *float64 `json:"ddrBandwidth"`
|
||||
HBMBandwidth *float64 `json:"hbmBandwidth"`
|
||||
MemoryBandwidth *float64 `json:"memoryBandwidth"`
|
||||
MediaFrequency *float64 `json:"mediaFrequency"`
|
||||
HugepagesUsed *float64 `json:"hugepagesUsed"`
|
||||
HugepagesTotal *float64 `json:"hugepagesTotal"`
|
||||
|
||||
MemoryTemperature *float64 `json:"memoryTemperature"`
|
||||
DeviceID string `json:"deviceID"`
|
||||
DeviceType string `json:"deviceType"`
|
||||
ProcessStatus string `json:"processStatus"`
|
||||
Frequency *float64 `json:"frequency"`
|
||||
MemoryFrequency *float64 `json:"memoryFrequency"`
|
||||
IntervalSeconds int `json:"intervalSeconds"`
|
||||
BaseModel
|
||||
ProductName string `json:"productName"`
|
||||
GPUUtil float64 `json:"gpuUtil"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
PowerDraw float64 `json:"powerDraw"`
|
||||
MaxPowerLimit float64 `json:"maxPowerLimit"`
|
||||
MemUsed float64 `json:"memUsed"`
|
||||
MemTotal float64 `json:"memTotal"`
|
||||
FanSpeed int `json:"fanSpeed"`
|
||||
Processes string `json:"processes"`
|
||||
ProductName string `json:"productName"`
|
||||
GPUUtil *float64 `json:"gpuUtil"`
|
||||
Temperature *float64 `json:"temperature"`
|
||||
PowerDraw *float64 `json:"powerDraw"`
|
||||
MaxPowerLimit *float64 `json:"maxPowerLimit"`
|
||||
MemUsed *float64 `json:"memUsed"`
|
||||
MemTotal *float64 `json:"memTotal"`
|
||||
FanSpeed *float64 `json:"fanSpeed"`
|
||||
Processes string `json:"processes"`
|
||||
}
|
||||
|
||||
type MonitorVLLM struct {
|
||||
ID uint `json:"-" gorm:"primarykey;autoIncrement"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
AppInstallID uint `json:"appInstallID"`
|
||||
Status string `json:"status"`
|
||||
RawMetrics string `json:"-"`
|
||||
HistogramDeltas string `json:"-"`
|
||||
|
||||
Running *float64 `json:"running"`
|
||||
Waiting *float64 `json:"waiting"`
|
||||
CacheUsage *float64 `json:"cacheUsage"`
|
||||
PromptThroughput *float64 `json:"promptThroughput"`
|
||||
GenerationThroughput *float64 `json:"generationThroughput"`
|
||||
RequestThroughput *float64 `json:"requestThroughput"`
|
||||
TimeToFirstToken *float64 `json:"timeToFirstToken"`
|
||||
TimePerOutputToken *float64 `json:"timePerOutputToken"`
|
||||
RequestLatency *float64 `json:"requestLatency"`
|
||||
PrefillTime *float64 `json:"prefillTime"`
|
||||
DecodeTime *float64 `json:"decodeTime"`
|
||||
TimeToFirstTokenP50 *float64 `json:"timeToFirstTokenP50"`
|
||||
TimeToFirstTokenP90 *float64 `json:"timeToFirstTokenP90"`
|
||||
TimeToFirstTokenP95 *float64 `json:"timeToFirstTokenP95"`
|
||||
TimeToFirstTokenP99 *float64 `json:"timeToFirstTokenP99"`
|
||||
TimePerOutputTokenP50 *float64 `json:"timePerOutputTokenP50"`
|
||||
TimePerOutputTokenP90 *float64 `json:"timePerOutputTokenP90"`
|
||||
TimePerOutputTokenP95 *float64 `json:"timePerOutputTokenP95"`
|
||||
TimePerOutputTokenP99 *float64 `json:"timePerOutputTokenP99"`
|
||||
RequestLatencyP50 *float64 `json:"requestLatencyP50"`
|
||||
RequestLatencyP90 *float64 `json:"requestLatencyP90"`
|
||||
RequestLatencyP95 *float64 `json:"requestLatencyP95"`
|
||||
RequestLatencyP99 *float64 `json:"requestLatencyP99"`
|
||||
}
|
||||
|
||||
@@ -40,12 +40,21 @@ type Meta struct {
|
||||
var catalog = map[string]Meta{
|
||||
"custom": {
|
||||
Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY",
|
||||
APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
|
||||
APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "dashscope-images", "openai-embeddings"),
|
||||
},
|
||||
"ollama": {
|
||||
Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses",
|
||||
APIConfigs: editableAPIConfigs(false, "openai-responses", "openai-completions", "openai-embeddings"),
|
||||
},
|
||||
// llmman (https://github.com/llmmanorg/llmman): local runner with Ollama/OpenAI-compatible routes on 127.0.0.1:17434.
|
||||
"llmman": {
|
||||
Key: "llmman", DisplayName: "llmman", Sort: 16, DefaultAPIType: "openai-responses",
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-responses", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
|
||||
{APIType: "openai-completions", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
|
||||
{APIType: "openai-embeddings", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
|
||||
},
|
||||
},
|
||||
"vllm": {
|
||||
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY",
|
||||
APIConfigs: editableAPIConfigs(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
|
||||
@@ -453,6 +462,7 @@ var legacyModelPrefixes = map[string][]string{
|
||||
"custom": {"custom"},
|
||||
"vllm": {"custom"},
|
||||
"ollama": {"ollama"},
|
||||
"llmman": {"llmman"},
|
||||
"deepseek": {"deepseek"},
|
||||
"bailian-coding-plan": {"bailian-coding-plan"},
|
||||
"ark-coding-plan": {"ark-coding-plan"},
|
||||
|
||||
@@ -43,8 +43,8 @@ func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL,
|
||||
providerKey = "moonshot"
|
||||
resolvedAPIType = "openai-completions"
|
||||
usesBearer = false
|
||||
case "ollama":
|
||||
apiKey = "ollama"
|
||||
case "ollama", "llmman":
|
||||
apiKey = provider
|
||||
usesBearer = false
|
||||
case "openai", "openrouter", "anthropic":
|
||||
preserveQualifiedModel = strings.Contains(modelName, "/")
|
||||
|
||||
@@ -27,11 +27,14 @@ type verifyErrorResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
const defaultVerifyTimeout = 30 * time.Second
|
||||
const (
|
||||
defaultVerifyTimeout = 30 * time.Second
|
||||
defaultVerifyMaxTokens = 16
|
||||
)
|
||||
|
||||
func SkipVerification(provider string) bool {
|
||||
switch provider {
|
||||
case "vllm", "ollama", "kimi-coding":
|
||||
case "vllm", "ollama", "llmman", "kimi-coding":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -116,20 +119,20 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
||||
}
|
||||
headers["anthropic-version"] = "2023-06-01"
|
||||
request.Body = mustJSON(map[string]interface{}{
|
||||
"model": model, "max_tokens": 1, "stream": false,
|
||||
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
||||
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
|
||||
})
|
||||
case "openai-responses":
|
||||
request.URL = baseURL + "/responses"
|
||||
headers["Authorization"] = "Bearer " + apiKey
|
||||
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": 1, "stream": false})
|
||||
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": defaultVerifyMaxTokens, "stream": false})
|
||||
default:
|
||||
request.URL = baseURL + "/chat/completions"
|
||||
if provider != "ollama" || strings.TrimSpace(apiKey) != "" {
|
||||
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
|
||||
headers["Authorization"] = "Bearer " + apiKey
|
||||
}
|
||||
request.Body = mustJSON(map[string]interface{}{
|
||||
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": 1, "stream": false,
|
||||
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
||||
})
|
||||
}
|
||||
return request
|
||||
|
||||
+217
-9
@@ -1,20 +1,30 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/genproto/googleapis/type/date"
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
"time"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type AlertRepo struct{}
|
||||
|
||||
var (
|
||||
ErrAlertConfigRevisionConflict = errors.New("alert config revision conflict")
|
||||
ErrAlertConfigRevisionRequired = errors.New("alert config revision is required")
|
||||
)
|
||||
|
||||
type IAlertRepo interface {
|
||||
WithByType(alertType string) DBOption
|
||||
WithByStatusIn(status []string) DBOption
|
||||
@@ -24,6 +34,7 @@ type IAlertRepo interface {
|
||||
WithByCreateAt(date *date.Date) DBOption
|
||||
WithByLicenseId(licenseId string) DBOption
|
||||
WithByRecordId(recordId uint) DBOption
|
||||
WithByDeliveryLogID(logID uint) DBOption
|
||||
WithByAlertMethodContainsConfigID(id uint) DBOption
|
||||
WithByMethodConfigIDs(ids []uint) DBOption
|
||||
|
||||
@@ -45,6 +56,8 @@ type IAlertRepo interface {
|
||||
CleanAlertLogs() error
|
||||
|
||||
CreateAlertTask(alertTaskBase *model.AlertTask) error
|
||||
CreatePendingAlertTask(logID, alertID uint, alertTask *model.AlertTask) (bool, error)
|
||||
FinalizePendingAlertTask(logID uint, succeeded bool, message string, fallback *model.AlertTask) (bool, error)
|
||||
DeleteAlertTask(opts ...DBOption) error
|
||||
GetAlertTask(opts ...DBOption) (model.AlertTask, error)
|
||||
LoadTaskCount(alertType string, project string, method string) (uint, uint, error)
|
||||
@@ -55,6 +68,7 @@ type IAlertRepo interface {
|
||||
GetConfigById(id uint) (model.AlertConfig, error)
|
||||
AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error)
|
||||
UpdateAlertConfig(maps map[string]interface{}, opts ...DBOption) error
|
||||
UpdateAlertConfigWithRevision(maps map[string]interface{}, revision *time.Time, opts ...DBOption) error
|
||||
CreateAlertConfig(config *model.AlertConfig) error
|
||||
DeleteAlertConfig(opts ...DBOption) error
|
||||
|
||||
@@ -223,13 +237,78 @@ func (a *AlertRepo) DeleteLog(opts ...DBOption) error {
|
||||
}
|
||||
|
||||
func (a *AlertRepo) CleanAlertLogs() error {
|
||||
return global.AlertDB.Where("1 = 1").Delete(&model.AlertLog{}).Error
|
||||
return global.AlertDB.Where("status <> ?", constant.AlertPushing).Delete(&model.AlertLog{}).Error
|
||||
}
|
||||
|
||||
func (a *AlertRepo) CreateAlertTask(alertTaskBase *model.AlertTask) error {
|
||||
return global.AlertDB.Model(&model.AlertTask{}).Create(&alertTaskBase).Error
|
||||
}
|
||||
|
||||
func (a *AlertRepo) CreatePendingAlertTask(logID, alertID uint, alertTask *model.AlertTask) (bool, error) {
|
||||
if alertTask == nil {
|
||||
return false, fmt.Errorf("pending alert task is required")
|
||||
}
|
||||
created := false
|
||||
err := global.AlertDB.Transaction(func(tx *gorm.DB) error {
|
||||
var log model.AlertLog
|
||||
if err := tx.Where("id = ? AND status = ?", logID, constant.AlertPushing).First(&log).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if log.AlertId != alertID || log.Type != alertTask.Type || log.Method != alertTask.Method {
|
||||
return fmt.Errorf("pending alert task does not match delivery log %d", logID)
|
||||
}
|
||||
alertTask.DeliveryLogID = &logID
|
||||
result := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "delivery_log_id"}},
|
||||
DoNothing: true,
|
||||
}).Create(alertTask)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
created = result.RowsAffected > 0
|
||||
return nil
|
||||
})
|
||||
return created, err
|
||||
}
|
||||
|
||||
func (a *AlertRepo) FinalizePendingAlertTask(logID uint, succeeded bool, message string, fallback *model.AlertTask) (bool, error) {
|
||||
finalized := false
|
||||
err := global.AlertDB.Transaction(func(tx *gorm.DB) error {
|
||||
status := constant.AlertError
|
||||
if succeeded {
|
||||
status = constant.AlertSuccess
|
||||
message = ""
|
||||
}
|
||||
result := tx.Model(&model.AlertLog{}).
|
||||
Where("id = ? AND status = ?", logID, constant.AlertPushing).
|
||||
Updates(map[string]interface{}{"status": status, "message": message})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil
|
||||
}
|
||||
finalized = true
|
||||
if !succeeded {
|
||||
return tx.Where("delivery_log_id = ?", logID).Delete(&model.AlertTask{}).Error
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := tx.Model(&model.AlertTask{}).Where("delivery_log_id = ?", logID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
if fallback == nil {
|
||||
return fmt.Errorf("pending alert task metadata is unavailable for delivery log %d", logID)
|
||||
}
|
||||
fallback.DeliveryLogID = &logID
|
||||
return tx.Create(fallback).Error
|
||||
})
|
||||
return finalized, err
|
||||
}
|
||||
|
||||
func (a *AlertRepo) DeleteAlertTask(opts ...DBOption) error {
|
||||
db, _ := getAlertDB(opts...)
|
||||
return db.Delete(&model.AlertTask{}).Error
|
||||
@@ -310,7 +389,23 @@ func (a *AlertRepo) UpdateAlertConfig(maps map[string]interface{}, opts ...DBOpt
|
||||
return db.Model(&model.AlertConfig{}).Updates(maps).Error
|
||||
}
|
||||
|
||||
func (a *AlertRepo) UpdateAlertConfigWithRevision(maps map[string]interface{}, revision *time.Time, opts ...DBOption) error {
|
||||
if revision == nil {
|
||||
return a.UpdateAlertConfig(maps, opts...)
|
||||
}
|
||||
db, _ := getAlertDB(opts...)
|
||||
result := db.Model(&model.AlertConfig{}).Where("updated_at = ?", *revision).Updates(maps)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return ErrAlertConfigRevisionConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AlertRepo) CreateAlertConfig(config *model.AlertConfig) error {
|
||||
ensureAlertConfigUID(config)
|
||||
return global.AlertDB.Model(&model.AlertConfig{}).Create(config).Error
|
||||
}
|
||||
|
||||
@@ -338,6 +433,12 @@ func (a *AlertRepo) WithByTypeNotIn(types []string) DBOption {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AlertRepo) WithByDeliveryLogID(logID uint) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("delivery_log_id = ?", logID)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AlertRepo) PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error) {
|
||||
var configs []model.AlertConfig
|
||||
db := global.AlertDB.Model(&model.AlertConfig{})
|
||||
@@ -378,26 +479,44 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
oldConfigMap := make(map[string]uint)
|
||||
oldConfigMap := make(map[string]model.AlertConfig)
|
||||
oldConfigByUID := make(map[string]model.AlertConfig)
|
||||
oldConfigByType := make(map[string][]model.AlertConfig)
|
||||
oldConfigByKey := make(map[string][]model.AlertConfig)
|
||||
consumedConfigIDs := make(map[uint]struct{})
|
||||
for _, item := range oldConfigs {
|
||||
if strings.TrimSpace(item.UID) != "" {
|
||||
oldConfigByUID[item.UID] = item
|
||||
}
|
||||
if singletonTypes[item.Type] {
|
||||
oldConfigMap[item.Type] = item.ID
|
||||
oldConfigMap[item.Type] = item
|
||||
continue
|
||||
}
|
||||
oldConfigByType[item.Type] = append(oldConfigByType[item.Type], item)
|
||||
oldConfigByKey[alertConfigSyncKey(item)] = append(oldConfigByKey[alertConfigSyncKey(item)], item)
|
||||
}
|
||||
for _, item := range data {
|
||||
if uid := strings.TrimSpace(item.UID); uid != "" {
|
||||
if matched, ok := oldConfigByUID[uid]; ok && matched.Type != item.Type {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("alert config UID %q belongs to type %q, not %q", uid, matched.Type, item.Type)
|
||||
}
|
||||
}
|
||||
if singletonTypes[item.Type] {
|
||||
if val, ok := oldConfigMap[item.Type]; ok {
|
||||
item.ID = val
|
||||
if matched, ok := oldConfigMap[item.Type]; ok {
|
||||
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
delete(oldConfigMap, item.Type)
|
||||
consumedConfigIDs[item.ID] = struct{}{}
|
||||
} else {
|
||||
item.ID = 0
|
||||
ensureAlertConfigUID(&item)
|
||||
if err := validateAlertConfigSyncSecret(&item); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
if item.ID == 0 {
|
||||
if err := tx.Create(&item).Error; err != nil {
|
||||
@@ -411,9 +530,31 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.TrimSpace(item.UID) != "" {
|
||||
if matched, ok := oldConfigByUID[item.UID]; ok {
|
||||
delete(oldConfigByUID, item.UID)
|
||||
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
consumedConfigIDs[item.ID] = struct{}{}
|
||||
if err := tx.Save(&item).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
deleteAlertConfigByID(oldConfigByType, matched.ID)
|
||||
deleteAlertConfigByID(oldConfigByKey, matched.ID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
key := alertConfigSyncKey(item)
|
||||
if matched, ok := popAlertConfigByKey(oldConfigByKey, key); ok {
|
||||
item.ID = matched.ID
|
||||
delete(oldConfigByUID, matched.UID)
|
||||
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
consumedConfigIDs[item.ID] = struct{}{}
|
||||
if err := tx.Save(&item).Error; err != nil {
|
||||
tx.Rollback()
|
||||
@@ -424,7 +565,12 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
||||
}
|
||||
|
||||
if matched, ok := popUnusedAlertConfigByType(oldConfigByType, usedConfigIDs, item.Type); ok {
|
||||
item.ID = matched.ID
|
||||
delete(oldConfigByUID, matched.UID)
|
||||
deleteAlertConfigByID(oldConfigByKey, matched.ID)
|
||||
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
consumedConfigIDs[item.ID] = struct{}{}
|
||||
if err := tx.Save(&item).Error; err != nil {
|
||||
tx.Rollback()
|
||||
@@ -434,6 +580,11 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
||||
}
|
||||
|
||||
item.ID = 0
|
||||
ensureAlertConfigUID(&item)
|
||||
if err := validateAlertConfigSyncSecret(&item); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&item).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
@@ -458,6 +609,63 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureAlertConfigUID(config *model.AlertConfig) {
|
||||
if config != nil && strings.TrimSpace(config.UID) == "" {
|
||||
config.UID = uuid.NewString()
|
||||
}
|
||||
}
|
||||
|
||||
func inheritAlertConfigSyncState(incoming *model.AlertConfig, existing model.AlertConfig) error {
|
||||
if incoming.Type != existing.Type {
|
||||
return fmt.Errorf("alert config UID %q belongs to type %q, not %q", incoming.UID, existing.Type, incoming.Type)
|
||||
}
|
||||
preserveExistingCustom := incoming.Type == constant.Custom &&
|
||||
existing.Status == constant.AlertDisable &&
|
||||
incoming.Title == existing.Title &&
|
||||
incoming.Status == existing.Status &&
|
||||
incoming.Config == existing.Config &&
|
||||
(incoming.SecretConfig == "" || incoming.SecretConfig == existing.SecretConfig)
|
||||
incoming.ID = existing.ID
|
||||
if strings.TrimSpace(incoming.UID) == "" {
|
||||
incoming.UID = existing.UID
|
||||
}
|
||||
if incoming.Type == constant.Custom && incoming.SecretConfig == "" {
|
||||
incoming.SecretConfig = existing.SecretConfig
|
||||
}
|
||||
if preserveExistingCustom {
|
||||
return nil
|
||||
}
|
||||
return validateAlertConfigSyncSecret(incoming)
|
||||
}
|
||||
|
||||
func validateAlertConfigSyncSecret(incoming *model.AlertConfig) error {
|
||||
if incoming.Type != constant.Custom {
|
||||
incoming.SecretConfig = ""
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(incoming.SecretConfig) == "" {
|
||||
return fmt.Errorf("custom webhook sync secret is missing")
|
||||
}
|
||||
var version struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(incoming.Config), &version); err != nil || version.SchemaVersion != 1 {
|
||||
return fmt.Errorf("custom webhook sync config must use schemaVersion 1")
|
||||
}
|
||||
secret := incoming.SecretConfig
|
||||
for _, prefix := range []string{"core:v1:", "agent:v1:"} {
|
||||
if !strings.HasPrefix(secret, prefix) {
|
||||
continue
|
||||
}
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, prefix))
|
||||
if err != nil || len(ciphertext) < 32 || len(ciphertext)%16 != 0 {
|
||||
return fmt.Errorf("custom webhook sync secret envelope is invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("custom webhook sync secret must use a versioned envelope")
|
||||
}
|
||||
|
||||
func loadUsedAlertConfigIDs(tx *gorm.DB) (map[uint]struct{}, error) {
|
||||
var alerts []model.Alert
|
||||
if err := tx.Select("method").Find(&alerts).Error; err != nil {
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type IDockerPortGuardRepo interface {
|
||||
List(context.Context) ([]model.DockerPortGuardPolicy, error)
|
||||
DeleteBatch(context.Context, []string) error
|
||||
UpsertBatch(context.Context, []model.DockerPortGuardPolicy) error
|
||||
}
|
||||
|
||||
type DockerPortGuardRepo struct{}
|
||||
|
||||
func NewIDockerPortGuardRepo() IDockerPortGuardRepo { return &DockerPortGuardRepo{} }
|
||||
|
||||
func (r *DockerPortGuardRepo) List(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
|
||||
var policies []model.DockerPortGuardPolicy
|
||||
err := global.DB.WithContext(ctx).Order("family, host_ip, host_port, protocol").Find(&policies).Error
|
||||
return policies, err
|
||||
}
|
||||
|
||||
func (r *DockerPortGuardRepo) DeleteBatch(ctx context.Context, uuids []string) error {
|
||||
return global.DB.WithContext(ctx).Where("uuid IN ?", uuids).Delete(&model.DockerPortGuardPolicy{}).Error
|
||||
}
|
||||
|
||||
func (r *DockerPortGuardRepo) UpsertBatch(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
|
||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for i := range policies {
|
||||
if err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "family"}, {Name: "host_ip"}, {Name: "host_port"}, {Name: "protocol"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"mode", "sources", "description", "updated_at"}),
|
||||
}).Create(&policies[i]).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFirewallRuleRevisionConflict = errors.New("firewall rule revision conflict")
|
||||
ErrFirewallPersistenceInvalid = errors.New("invalid firewall persistence record")
|
||||
)
|
||||
|
||||
type IFirewallRuleRepo interface {
|
||||
Create(context.Context, *model.FirewallRule) error
|
||||
GetByUUID(context.Context, string) (model.FirewallRule, error)
|
||||
List(context.Context, ...DBOption) ([]model.FirewallRule, error)
|
||||
UpdateWithRevision(context.Context, string, uint, map[string]interface{}) error
|
||||
DeleteWithRevision(context.Context, string, uint) error
|
||||
}
|
||||
|
||||
type FirewallRuleRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func WithFirewallRuleSource(kind, id string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("owner = ?", model.FirewallRuleOwner(kind, id))
|
||||
}
|
||||
}
|
||||
|
||||
func NewIFirewallRuleRepo() IFirewallRuleRepo {
|
||||
return &FirewallRuleRepo{}
|
||||
}
|
||||
|
||||
func NewFirewallRuleRepo(db *gorm.DB) *FirewallRuleRepo {
|
||||
return &FirewallRuleRepo{db: db}
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) Create(ctx context.Context, rule *model.FirewallRule) error {
|
||||
if err := prepareFirewallRule(rule); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.dbFor(ctx).Create(rule).Error
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) GetByUUID(ctx context.Context, ruleUUID string) (model.FirewallRule, error) {
|
||||
var rule model.FirewallRule
|
||||
err := r.dbFor(ctx).Where("uuid = ?", ruleUUID).First(&rule).Error
|
||||
return rule, err
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) List(ctx context.Context, opts ...DBOption) ([]model.FirewallRule, error) {
|
||||
var rules []model.FirewallRule
|
||||
db := r.dbFor(ctx).Model(&model.FirewallRule{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
return rules, db.Find(&rules).Error
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) UpdateWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint, updates map[string]interface{}) error {
|
||||
updates = sanitizeRuleUpdates(updates)
|
||||
updates["revision"] = gorm.Expr("revision + 1")
|
||||
result := r.dbFor(ctx).Model(&model.FirewallRule{}).
|
||||
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
|
||||
Updates(updates)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return ErrFirewallRuleRevisionConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) DeleteWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint) error {
|
||||
result := r.dbFor(ctx).
|
||||
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
|
||||
Delete(&model.FirewallRule{})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return ErrFirewallRuleRevisionConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FirewallRuleRepo) dbFor(ctx context.Context) *gorm.DB {
|
||||
return firewallDB(ctx, r.db)
|
||||
}
|
||||
|
||||
func firewallDB(ctx context.Context, fallback *gorm.DB) *gorm.DB {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if tx, ok := ctx.Value(constant.DB).(*gorm.DB); ok && tx != nil {
|
||||
return tx.WithContext(ctx)
|
||||
}
|
||||
if fallback == nil {
|
||||
fallback = global.DB
|
||||
}
|
||||
return fallback.WithContext(ctx)
|
||||
}
|
||||
|
||||
func prepareFirewallRule(rule *model.FirewallRule) error {
|
||||
if rule == nil {
|
||||
return fmt.Errorf("%w: rule is nil", ErrFirewallPersistenceInvalid)
|
||||
}
|
||||
if rule.Family == "" || rule.Protocol == "" || rule.Action == "" {
|
||||
return fmt.Errorf("%w: atomic rule identity fields are required", ErrFirewallPersistenceInvalid)
|
||||
}
|
||||
if rule.UUID == "" {
|
||||
rule.UUID = uuid.NewString()
|
||||
}
|
||||
if rule.Revision == 0 {
|
||||
rule.Revision = 1
|
||||
}
|
||||
if rule.Origin == "" {
|
||||
rule.Origin = constant.FirewallRuleOriginCreated
|
||||
}
|
||||
if rule.Owner == "" {
|
||||
rule.Owner = constant.FirewallRuleSourceUser
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sanitizeRuleUpdates(updates map[string]interface{}) map[string]interface{} {
|
||||
result := cloneUpdates(updates)
|
||||
delete(result, "id")
|
||||
delete(result, "uuid")
|
||||
delete(result, "revision")
|
||||
delete(result, "created_at")
|
||||
return result
|
||||
}
|
||||
|
||||
func cloneUpdates(updates map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{}, len(updates)+1)
|
||||
for key, value := range updates {
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type IForwardingRuleRepo interface {
|
||||
List(context.Context) ([]model.ForwardingRule, error)
|
||||
ReplaceAll(context.Context, []model.ForwardingRule) error
|
||||
}
|
||||
|
||||
type ForwardingRuleRepo struct{}
|
||||
|
||||
func NewIForwardingRuleRepo() IForwardingRuleRepo { return &ForwardingRuleRepo{} }
|
||||
|
||||
func (r *ForwardingRuleRepo) List(ctx context.Context) ([]model.ForwardingRule, error) {
|
||||
var rules []model.ForwardingRule
|
||||
err := global.DB.WithContext(ctx).Order("id ASC").Find(&rules).Error
|
||||
return rules, err
|
||||
}
|
||||
|
||||
func (r *ForwardingRuleRepo) ReplaceAll(ctx context.Context, rules []model.ForwardingRule) error {
|
||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.ForwardingRule{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Create(&rules).Error
|
||||
})
|
||||
}
|
||||
+103
-13
@@ -1,6 +1,8 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
@@ -10,11 +12,25 @@ import (
|
||||
|
||||
type MonitorRepo struct{}
|
||||
|
||||
type GPUHistoryPoint struct {
|
||||
model.MonitorGPU
|
||||
Bucket int64
|
||||
PowerPercent *float64
|
||||
MemoryPercent *float64
|
||||
ProcessCount *float64
|
||||
}
|
||||
|
||||
type IMonitorRepo interface {
|
||||
CleanHost() error
|
||||
CleanGPU() error
|
||||
GetBase(opts ...DBOption) ([]model.MonitorBase, error)
|
||||
GetGPU(opts ...DBOption) ([]model.MonitorGPU, error)
|
||||
CountGPU(opts ...DBOption) (int64, error)
|
||||
GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error)
|
||||
GetGPUDevices() ([]model.MonitorGPU, error)
|
||||
GetIO(opts ...DBOption) ([]model.MonitorIO, error)
|
||||
GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, error)
|
||||
GetIONames() ([]string, error)
|
||||
GetNetworkNames() ([]string, error)
|
||||
|
||||
CreateMonitorBase(model model.MonitorBase) error
|
||||
BatchCreateMonitorGPU(list []model.MonitorGPU) error
|
||||
@@ -25,13 +41,26 @@ type IMonitorRepo interface {
|
||||
DelMonitorIO(timeForDelete time.Time) error
|
||||
DelMonitorNet(timeForDelete time.Time) error
|
||||
|
||||
WithByProductName(name string) DBOption
|
||||
WithByGPUDevice(deviceID, name string, legacy bool) DBOption
|
||||
}
|
||||
|
||||
func NewIMonitorRepo() IMonitorRepo {
|
||||
return &MonitorRepo{}
|
||||
}
|
||||
|
||||
func (s *MonitorRepo) CleanHost() error {
|
||||
for _, item := range []interface{}{&model.MonitorBase{}, &model.MonitorIO{}, &model.MonitorNetwork{}} {
|
||||
if err := global.MonitorDB.Where("1 = 1").Delete(item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MonitorRepo) CleanGPU() error {
|
||||
return global.GPUMonitorDB.Where("1 = 1").Delete(&model.MonitorGPU{}).Error
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) GetBase(opts ...DBOption) ([]model.MonitorBase, error) {
|
||||
var data []model.MonitorBase
|
||||
db := global.MonitorDB
|
||||
@@ -59,14 +88,16 @@ func (u *MonitorRepo) GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, erro
|
||||
err := db.Find(&data).Error
|
||||
return data, err
|
||||
}
|
||||
func (u *MonitorRepo) GetGPU(opts ...DBOption) ([]model.MonitorGPU, error) {
|
||||
var data []model.MonitorGPU
|
||||
db := global.GPUMonitorDB
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
err := db.Find(&data).Error
|
||||
return data, err
|
||||
func (u *MonitorRepo) GetIONames() ([]string, error) {
|
||||
var names []string
|
||||
err := global.MonitorDB.Model(&model.MonitorIO{}).Distinct().Pluck("name", &names).Error
|
||||
return names, err
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) GetNetworkNames() ([]string, error) {
|
||||
var names []string
|
||||
err := global.MonitorDB.Model(&model.MonitorNetwork{}).Distinct().Pluck("name", &names).Error
|
||||
return names, err
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
|
||||
@@ -97,8 +128,67 @@ func (s *MonitorRepo) DelMonitorGPU(timeForDelete time.Time) error {
|
||||
return global.GPUMonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorGPU{}).Error
|
||||
}
|
||||
|
||||
func (s *MonitorRepo) WithByProductName(name string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("product_name = ?", name)
|
||||
func (u *MonitorRepo) GetGPUDevices() ([]model.MonitorGPU, error) {
|
||||
var data []model.MonitorGPU
|
||||
err := global.GPUMonitorDB.Model(&model.MonitorGPU{}).Select("device_id, product_name, device_type").Group("device_id, product_name, device_type").Order("product_name, device_id").Find(&data).Error
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) WithByGPUDevice(deviceID, name string, legacy bool) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
if deviceID != "" {
|
||||
return db.Where("device_id = ?", deviceID)
|
||||
}
|
||||
db = db.Where("product_name = ?", name)
|
||||
if legacy {
|
||||
db = db.Where("device_id IS NULL OR device_id = ''")
|
||||
}
|
||||
return db
|
||||
}
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) CountGPU(opts ...DBOption) (int64, error) {
|
||||
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
var count int64
|
||||
err := db.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error) {
|
||||
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
expressions := []string{
|
||||
"CASE WHEN max_power_limit > 0 THEN 100.0 * power_draw / max_power_limit END",
|
||||
"CASE WHEN mem_total > 0 AND mem_used IS NOT NULL THEN 100.0 * mem_used / mem_total ELSE memory_util END",
|
||||
"CASE WHEN (process_status = 'ok' OR process_status IS NULL OR process_status = '') AND json_valid(processes) THEN CASE WHEN json_type(processes) = 'array' THEN json_array_length(processes) END END",
|
||||
}
|
||||
aliases := []string{"power_percent", "memory_percent", "process_count"}
|
||||
columns := []string{"*"}
|
||||
if bucketSeconds > 0 {
|
||||
operation := "AVG"
|
||||
if aggregation == "max" {
|
||||
operation = "MAX"
|
||||
}
|
||||
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), bucketSeconds)}
|
||||
for _, column := range []string{"memory_activity", "encoder_util", "decoder_util", "jpeg_util", "ofa_util", "media_util", "compute_util", "copy_util", "hotspot_temperature", "fan_rpm", "ai_cpu_util", "ctrl_cpu_util", "ddr_used", "ddr_total", "hbm_used", "hbm_total", "ddr_bandwidth", "hbm_bandwidth", "memory_bandwidth", "media_frequency", "hugepages_used", "hugepages_total", "gpu_util", "temperature", "memory_temperature", "power_draw", "max_power_limit", "mem_used", "mem_total", "frequency", "memory_frequency", "fan_speed"} {
|
||||
columns = append(columns, operation+"("+column+") AS "+column)
|
||||
}
|
||||
for i := range expressions {
|
||||
expressions[i] = operation + "(" + expressions[i] + ")"
|
||||
}
|
||||
db = db.Group("bucket").Order("bucket ASC")
|
||||
} else {
|
||||
db = db.Order("created_at ASC, id ASC")
|
||||
}
|
||||
for i, expression := range expressions {
|
||||
columns = append(columns, expression+" AS "+aliases[i])
|
||||
}
|
||||
var data []GPUHistoryPoint
|
||||
err := db.Select(strings.Join(columns, ", ")).Scan(&data).Error
|
||||
return data, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type VLLMMonitorRepo struct{}
|
||||
|
||||
type VLLMHistoryPoint struct {
|
||||
model.MonitorVLLM
|
||||
Bucket int64
|
||||
HistogramSamples string
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
|
||||
return global.VLLMMonitorDB.Create(point).Error
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) Latest(id uint) (model.MonitorVLLM, error) {
|
||||
var point model.MonitorVLLM
|
||||
db := global.VLLMMonitorDB.Where("app_install_id = ?", id)
|
||||
err := db.Order("created_at DESC, id DESC").First(&point).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return point, nil
|
||||
}
|
||||
return point, err
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) CleanTarget(id uint) error {
|
||||
return global.VLLMMonitorDB.Where("app_install_id = ?", id).Delete(&model.MonitorVLLM{}).Error
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) DeleteBefore(before time.Time) error {
|
||||
return global.VLLMMonitorDB.Where("created_at < ?", before).Delete(&model.MonitorVLLM{}).Error
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) Count(id uint, start, end time.Time) (int64, error) {
|
||||
var count int64
|
||||
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
|
||||
err := db.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) History(id uint, start, end time.Time, seconds int64, aggregation string) ([]VLLMHistoryPoint, error) {
|
||||
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
|
||||
metrics := []string{"running", "waiting", "cache_usage", "prompt_throughput", "generation_throughput", "request_throughput", "time_to_first_token", "time_per_output_token", "request_latency", "prefill_time", "decode_time", "time_to_first_token_p50", "time_to_first_token_p90", "time_to_first_token_p95", "time_to_first_token_p99", "time_per_output_token_p50", "time_per_output_token_p90", "time_per_output_token_p95", "time_per_output_token_p99", "request_latency_p50", "request_latency_p90", "request_latency_p95", "request_latency_p99"}
|
||||
var columns []string
|
||||
if seconds > 0 {
|
||||
operation := "AVG"
|
||||
if aggregation == "max" {
|
||||
operation = "MAX"
|
||||
}
|
||||
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), seconds)}
|
||||
for _, column := range metrics {
|
||||
if aggregation != "max" && (strings.HasPrefix(column, "time_to_first_token_p") || strings.HasPrefix(column, "time_per_output_token_p") || strings.HasPrefix(column, "request_latency_p")) {
|
||||
continue
|
||||
}
|
||||
columns = append(columns, operation+"("+column+") AS "+column)
|
||||
}
|
||||
if aggregation != "max" {
|
||||
columns = append(columns, "json_group_array(json(NULLIF(histogram_deltas, ''))) AS histogram_samples")
|
||||
}
|
||||
db = db.Group("bucket").Order("bucket ASC")
|
||||
} else {
|
||||
columns = append([]string{"id", "created_at", "app_install_id", "status"}, metrics...)
|
||||
db = db.Order("created_at ASC, id ASC")
|
||||
}
|
||||
var points []VLLMHistoryPoint
|
||||
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
|
||||
return points, err
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type SettingRepo struct{}
|
||||
@@ -19,12 +21,16 @@ type ISettingRepo interface {
|
||||
WithByKey(key string) DBOption
|
||||
|
||||
UpdateOrCreate(key, value string) error
|
||||
UpdateValues(map[string]string) error
|
||||
|
||||
GetDescription(opts ...DBOption) (model.CommonDescription, error)
|
||||
GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error)
|
||||
CreateDescription(data *model.CommonDescription) error
|
||||
SaveDescriptions(context.Context, []model.CommonDescription) error
|
||||
UpdateDescription(id string, val map[string]interface{}) error
|
||||
DelDescription(id string) error
|
||||
DeleteDescriptions(context.Context, string, []string, bool) (int64, error)
|
||||
WithDescriptionIDs(ids []string) DBOption
|
||||
WithByDescriptionID(id string) DBOption
|
||||
}
|
||||
|
||||
@@ -90,6 +96,25 @@ func (s *SettingRepo) UpdateOrCreate(key, value string) error {
|
||||
return global.DB.Model(&setting).UpdateColumn("value", value).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) UpdateValues(values map[string]string) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
for key, value := range values {
|
||||
var setting model.Setting
|
||||
err := tx.Where("key = ?", key).First(&setting).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err := tx.Create(&model.Setting{Key: key, Value: value}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else if err := tx.Model(&setting).UpdateColumn("value", value).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SettingRepo) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) {
|
||||
var lists []model.CommonDescription
|
||||
db := global.DB.Model(&model.CommonDescription{})
|
||||
@@ -111,14 +136,42 @@ func (s *SettingRepo) GetDescription(opts ...DBOption) (model.CommonDescription,
|
||||
func (s *SettingRepo) CreateDescription(data *model.CommonDescription) error {
|
||||
return global.DB.Create(data).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) SaveDescriptions(ctx context.Context, descriptions []model.CommonDescription) error {
|
||||
return global.DB.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"description"}),
|
||||
}).CreateInBatches(&descriptions, 100).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) UpdateDescription(id string, val map[string]interface{}) error {
|
||||
return global.DB.Model(&model.CommonDescription{}).Where("id = ?", id).Updates(val).Error
|
||||
}
|
||||
func (s *SettingRepo) DelDescription(id string) error {
|
||||
return global.DB.Where("id = ?", id).Delete(&model.CommonDescription{}).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) DeleteDescriptions(ctx context.Context, kind string, ids []string, emptyOnly bool) (int64, error) {
|
||||
var deleted int64
|
||||
for start := 0; start < len(ids); start += 500 {
|
||||
query := global.DB.WithContext(ctx).Where("type = ? AND id IN ?", kind, ids[start:min(start+500, len(ids))])
|
||||
if emptyOnly {
|
||||
query = query.Where("description = ? AND is_pinned = ?", "", false)
|
||||
}
|
||||
result := query.Delete(&model.CommonDescription{})
|
||||
deleted += result.RowsAffected
|
||||
if result.Error != nil {
|
||||
return deleted, result.Error
|
||||
}
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
func (s *SettingRepo) WithByDescriptionID(id string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("id = ?", id)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SettingRepo) WithDescriptionIDs(ids []string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB { return db.Where("id IN ?", ids) }
|
||||
}
|
||||
|
||||
@@ -936,6 +936,7 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig,
|
||||
AccountID: agent.AccountID,
|
||||
Model: model,
|
||||
Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
|
||||
Metadata: extractOpenclawModelMetadata(conf, account, models),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -967,7 +968,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil {
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks, req.Metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1684,7 +1685,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
|
||||
return err
|
||||
}
|
||||
fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil {
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
case constant.AppHermesAgent:
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -1320,6 +1321,10 @@ func appendPluginAllow(conf map[string]interface{}, pluginID string) {
|
||||
}
|
||||
|
||||
func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID string) error {
|
||||
help, err := cmd.RunDockerExecWithStdout(time.Minute, containerName, "openclaw", "plugins", "install", "--help")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workdir := path.Join(openclawPluginPackageTmpDir, pluginID)
|
||||
defer func() {
|
||||
_ = mgr.Run("docker", "exec", containerName, "rm", "-rf", workdir)
|
||||
@@ -1341,7 +1346,19 @@ func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID
|
||||
if pkgPath == "" {
|
||||
return fmt.Errorf("openclaw plugin package not found")
|
||||
}
|
||||
return mgr.Run("docker", "exec", containerName, "openclaw", "plugins", "install", pkgPath, "--dangerously-force-unsafe-install")
|
||||
args := []string{"exec", containerName, "openclaw", "plugins", "install", pkgPath}
|
||||
// Newer CLIs require source confirmation; older releases do not support --force.
|
||||
options := strings.Fields(help)
|
||||
if slices.Contains(options, "--force") {
|
||||
args = append(args, "--force")
|
||||
} else if slices.Contains(options, "--dangerously-force-unsafe-install") {
|
||||
args = append(args, "--dangerously-force-unsafe-install")
|
||||
}
|
||||
// Source confirmation does not grant the selected channel plugin's capabilities.
|
||||
if slices.Contains(options, "--accept-capabilities") {
|
||||
args = append(args, "--accept-capabilities")
|
||||
}
|
||||
return mgr.Run("docker", args...)
|
||||
}
|
||||
|
||||
func uninstallOpenclawPlugin(mgr *cmd.CommandHelper, containerName, pluginID string) error {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -737,9 +738,11 @@ type modelProvider struct {
|
||||
}
|
||||
|
||||
type modelEntry struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Input []string `json:"input,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Input []string `json:"input,omitempty"`
|
||||
ContextWindow int `json:"contextWindow,omitempty"`
|
||||
MaxTokens int `json:"maxTokens,omitempty"`
|
||||
}
|
||||
|
||||
func requiresOpenclawProviderModels(provider string) bool {
|
||||
@@ -767,7 +770,7 @@ type browserConfig struct {
|
||||
DefaultProfile string `json:"defaultProfile"`
|
||||
}
|
||||
|
||||
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string) error {
|
||||
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string, metadata []dto.AgentModelMetadata) error {
|
||||
if strings.TrimSpace(confDir) == "" {
|
||||
return fmt.Errorf("config dir is required")
|
||||
}
|
||||
@@ -852,6 +855,7 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
}
|
||||
conf = initial
|
||||
} else {
|
||||
preserveOpenclawModelMetadata(conf, cfg.Models)
|
||||
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -906,6 +910,9 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
if allowedOrigins != nil {
|
||||
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
|
||||
}
|
||||
if err := applyOpenclawModelMetadata(conf, account, metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -920,6 +927,144 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order)
|
||||
}
|
||||
|
||||
func readOpenclawModelsConfig(conf map[string]interface{}) *modelsConfig {
|
||||
raw, ok := conf["models"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
payload, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var models modelsConfig
|
||||
if err := json.Unmarshal(payload, &models); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &models
|
||||
}
|
||||
|
||||
func preserveOpenclawModelMetadata(conf map[string]interface{}, next *modelsConfig) {
|
||||
current := readOpenclawModelsConfig(conf)
|
||||
if current == nil || next == nil {
|
||||
return
|
||||
}
|
||||
for providerID, nextProvider := range next.Providers {
|
||||
currentProvider, ok := current.Providers[providerID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
byID := make(map[string]modelEntry, len(currentProvider.Models))
|
||||
for _, entry := range currentProvider.Models {
|
||||
byID[entry.ID] = entry
|
||||
}
|
||||
for index := range nextProvider.Models {
|
||||
currentEntry, ok := byID[nextProvider.Models[index].ID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
nextProvider.Models[index].Input = currentEntry.Input
|
||||
nextProvider.Models[index].ContextWindow = currentEntry.ContextWindow
|
||||
nextProvider.Models[index].MaxTokens = currentEntry.MaxTokens
|
||||
}
|
||||
next.Providers[providerID] = nextProvider
|
||||
}
|
||||
}
|
||||
|
||||
func extractOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, accountModels []dto.AgentAccountModel) []dto.AgentModelMetadata {
|
||||
result := make([]dto.AgentModelMetadata, 0, len(accountModels))
|
||||
configured := readOpenclawModelsConfig(conf)
|
||||
for _, item := range accountModels {
|
||||
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
metadata := dto.AgentModelMetadata{Model: item.ID, InputMode: "auto"}
|
||||
if configured != nil {
|
||||
for _, entry := range configured.Providers[providerID].Models {
|
||||
if entry.ID != inferred.ID {
|
||||
continue
|
||||
}
|
||||
metadata.ContextWindow = entry.ContextWindow
|
||||
metadata.MaxTokens = entry.MaxTokens
|
||||
if len(entry.Input) > 0 && !slices.Equal(entry.Input, inferred.Input) {
|
||||
if slices.Contains(entry.Input, "image") {
|
||||
metadata.InputMode = "image"
|
||||
} else {
|
||||
metadata.InputMode = "text"
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
result = append(result, metadata)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func applyOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, requested []dto.AgentModelMetadata) error {
|
||||
if len(requested) == 0 {
|
||||
return nil
|
||||
}
|
||||
configured := readOpenclawModelsConfig(conf)
|
||||
if configured == nil {
|
||||
return fmt.Errorf("model metadata is not supported for provider %s", account.Provider)
|
||||
}
|
||||
accountModels, err := loadAgentAccountModels(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
available := make(map[string]dto.AgentAccountModel, len(accountModels))
|
||||
for _, item := range accountModels {
|
||||
available[item.ID] = item
|
||||
}
|
||||
seen := make(map[string]struct{}, len(requested))
|
||||
for _, metadata := range requested {
|
||||
item, ok := available[metadata.Model]
|
||||
if !ok {
|
||||
return buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
if _, ok := seen[metadata.Model]; ok {
|
||||
return fmt.Errorf("duplicate model metadata: %s", metadata.Model)
|
||||
}
|
||||
seen[metadata.Model] = struct{}{}
|
||||
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provider := configured.Providers[providerID]
|
||||
found := false
|
||||
for index := range provider.Models {
|
||||
if provider.Models[index].ID != inferred.ID {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
provider.Models[index].ContextWindow = metadata.ContextWindow
|
||||
provider.Models[index].MaxTokens = metadata.MaxTokens
|
||||
switch metadata.InputMode {
|
||||
case "auto":
|
||||
provider.Models[index].Input = inferred.Input
|
||||
case "text":
|
||||
provider.Models[index].Input = []string{"text"}
|
||||
case "image":
|
||||
provider.Models[index].Input = []string{"text", "image"}
|
||||
default:
|
||||
return fmt.Errorf("unsupported model input mode: %s", metadata.InputMode)
|
||||
}
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
return buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
configured.Providers[providerID] = provider
|
||||
}
|
||||
modelsMap, err := structToMap(configured)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conf["models"] = modelsMap
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
|
||||
accountModels, err := loadAgentAccountModels(account)
|
||||
if err != nil {
|
||||
@@ -1041,7 +1186,7 @@ func prepareOpenclawInstallFiles(appInstall *model.AppInstall, account *model.Ag
|
||||
return fmt.Errorf("app install is required")
|
||||
}
|
||||
confDir := path.Join(appInstall.GetPath(), "data", "conf")
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil); err != nil {
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
dataDir := path.Join(appInstall.GetPath(), "data")
|
||||
@@ -1338,7 +1483,7 @@ func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAcco
|
||||
|
||||
func requiresInitialAgentAccountModels(provider string) bool {
|
||||
switch provider {
|
||||
case "custom", "vllm", "ollama":
|
||||
case "custom", "vllm", "ollama", "llmman":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
+431
-146
@@ -4,11 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
@@ -17,11 +14,13 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
|
||||
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/email"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
|
||||
)
|
||||
|
||||
type AlertService struct{}
|
||||
@@ -34,6 +33,28 @@ var communityAlertMethodTypeNames = map[string]string{
|
||||
constant.SMS: "SMS",
|
||||
}
|
||||
|
||||
var legacyAlertMethodTypeMap = map[string]string{
|
||||
"mail": constant.Email,
|
||||
constant.Email: constant.Email,
|
||||
constant.SMS: constant.SMS,
|
||||
constant.Bark: constant.Bark,
|
||||
constant.WeChat: constant.WeCom,
|
||||
constant.WeCom: constant.WeCom,
|
||||
constant.DingTalk: constant.DingTalk,
|
||||
constant.FeiShu: constant.FeiShu,
|
||||
constant.Custom: constant.Custom,
|
||||
}
|
||||
|
||||
var supportedAlertMethodTypes = map[string]struct{}{
|
||||
constant.Email: {},
|
||||
constant.SMS: {},
|
||||
constant.Bark: {},
|
||||
constant.WeCom: {},
|
||||
constant.DingTalk: {},
|
||||
constant.FeiShu: {},
|
||||
constant.Custom: {},
|
||||
}
|
||||
|
||||
type IAlertService interface {
|
||||
PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error)
|
||||
GetAlerts() ([]dto.AlertDTO, error)
|
||||
@@ -53,8 +74,10 @@ type IAlertService interface {
|
||||
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
|
||||
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
|
||||
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
|
||||
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
|
||||
DeleteAlertConfig(id uint) error
|
||||
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
||||
TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error)
|
||||
}
|
||||
|
||||
func NewIAlertService() IAlertService {
|
||||
@@ -82,7 +105,38 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
cronjobProjects := make(map[string]uint)
|
||||
var cronjobIDs []uint
|
||||
for _, item := range alerts {
|
||||
if alertUtil.GetCronJobType(item.Type) != "cronJob" {
|
||||
continue
|
||||
}
|
||||
if _, exists := cronjobProjects[item.Project]; exists {
|
||||
continue
|
||||
}
|
||||
id, parseErr := strconv.ParseUint(item.Project, 10, strconv.IntSize)
|
||||
if parseErr != nil || id == 0 {
|
||||
continue
|
||||
}
|
||||
cronjobProjects[item.Project] = uint(id)
|
||||
cronjobIDs = append(cronjobIDs, uint(id))
|
||||
}
|
||||
cronjobsByID := make(map[uint]model.Cronjob)
|
||||
if len(cronjobIDs) > 0 {
|
||||
cronjobs, err := cronjobRepo.List(repo.WithByIDs(cronjobIDs))
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
for _, cronjob := range cronjobs {
|
||||
cronjobsByID[cronjob.ID] = cronjob
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range alerts {
|
||||
var taskName string
|
||||
if cronjob, exists := cronjobsByID[cronjobProjects[item.Project]]; exists && cronjob.Type == item.Type {
|
||||
taskName = cronjob.Name
|
||||
}
|
||||
|
||||
result = append(result, dto.AlertDTO{
|
||||
ID: item.ID,
|
||||
@@ -92,6 +146,7 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
|
||||
Method: item.Method,
|
||||
Title: item.Title,
|
||||
Project: item.Project,
|
||||
TaskName: taskName,
|
||||
Status: item.Status,
|
||||
SendCount: item.SendCount,
|
||||
AdvancedParams: item.AdvancedParams,
|
||||
@@ -163,6 +218,16 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
advanced, err := prepareCronJobAlertParams(create.Type, "", create.AdvancedParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
create.AdvancedParams = advanced
|
||||
if create.Status != constant.AlertDisable {
|
||||
if err := a.validateCronJobAlertChannels(create.Type, advanced, create.Method); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
alertInfo.Status = constant.AlertEnable
|
||||
if err := copier.Copy(&alertInfo, &create); err != nil {
|
||||
return buserr.WithErr("ErrStructTransform", err)
|
||||
@@ -180,9 +245,28 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
|
||||
}
|
||||
|
||||
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
|
||||
if err := a.validateCommunityAlertMethod(req.Method); err != nil {
|
||||
if alertUtil.GetCronJobType(req.Type) == "cronJob" {
|
||||
previous, err := alertRepo.Get(repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.AdvancedParams, err = prepareCronJobAlertParams(req.Type, previous.AdvancedParams, req.AdvancedParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
methodTypes, err := a.validateAlertMethodReferences(req.Method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Status != constant.AlertDisable {
|
||||
if err := a.validateCronJobAlertChannels(req.Type, req.AdvancedParams, req.Method); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
upMap := make(map[string]interface{})
|
||||
upMap["id"] = req.ID
|
||||
@@ -240,7 +324,19 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
|
||||
if alertInfo.ID == 0 {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
err := alertRepo.Update(map[string]interface{}{"status": status}, repo.WithByID(alertInfo.ID))
|
||||
methodTypes, err := a.validateAlertMethodReferences(alertInfo.Method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status == constant.AlertEnable {
|
||||
if err := a.validateCronJobAlertChannels(alertInfo.Type, alertInfo.AdvancedParams, alertInfo.Method); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = alertRepo.Update(map[string]interface{}{"status": status}, repo.WithByID(alertInfo.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -257,122 +353,14 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
|
||||
}
|
||||
|
||||
func (a AlertService) GetDisks() ([]dto.DiskDTO, error) {
|
||||
var disks []dto.DiskDTO
|
||||
excludes := map[string]struct{}{
|
||||
"/mnt/cdrom": {}, "/boot": {}, "/boot/efi": {}, "/dev": {}, "/dev/shm": {},
|
||||
"/run/lock": {}, "/run": {}, "/run/shm": {}, "/run/user": {},
|
||||
infos := loadDiskInfo(true)
|
||||
disks := make([]dto.DiskDTO, 0, len(infos))
|
||||
for _, item := range infos {
|
||||
disks = append(disks, dto.DiskDTO(item))
|
||||
}
|
||||
stdout, err := executeDiskCommand()
|
||||
if err != nil {
|
||||
return disks, nil
|
||||
}
|
||||
|
||||
lines := strings.Split(stdout, "\n")
|
||||
var mounts []dto.AlertDiskInfo
|
||||
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 7 {
|
||||
continue
|
||||
}
|
||||
mountPoint := strings.Join(fields[6:], " ")
|
||||
if shouldExclude(fields, mountPoint, excludes) {
|
||||
continue
|
||||
}
|
||||
mounts = append(mounts, dto.AlertDiskInfo{Type: fields[1], Device: fields[0], Mount: mountPoint})
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
)
|
||||
wg.Add(len(mounts))
|
||||
for i := 0; i < len(mounts); i++ {
|
||||
go func(timeoutCh <-chan time.Time, mount dto.AlertDiskInfo) {
|
||||
defer wg.Done()
|
||||
|
||||
var itemData dto.DiskDTO
|
||||
itemData.Path = mount.Mount
|
||||
itemData.Type = mount.Type
|
||||
itemData.Device = mount.Device
|
||||
select {
|
||||
case <-timeoutCh:
|
||||
mu.Lock()
|
||||
disks = append(disks, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
|
||||
default:
|
||||
state, err := disk.Usage(mount.Mount)
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
disks = append(disks, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
|
||||
return
|
||||
}
|
||||
itemData.Total = state.Total
|
||||
itemData.Free = state.Free
|
||||
itemData.Used = state.Used
|
||||
itemData.UsedPercent = state.UsedPercent
|
||||
itemData.InodesTotal = state.InodesTotal
|
||||
itemData.InodesUsed = state.InodesUsed
|
||||
itemData.InodesFree = state.InodesFree
|
||||
itemData.InodesUsedPercent = state.InodesUsedPercent
|
||||
mu.Lock()
|
||||
disks = append(disks, itemData)
|
||||
mu.Unlock()
|
||||
}
|
||||
}(time.After(5*time.Second), mounts[i])
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
sort.Slice(disks, func(i, j int) bool {
|
||||
return disks[i].Path < disks[j].Path
|
||||
})
|
||||
return disks, nil
|
||||
}
|
||||
|
||||
func executeDiskCommand() (string, error) {
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||
stdout, err := cmdMgr.RunWithStdout("df", "-hT", "-P")
|
||||
if err != nil {
|
||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||
stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P")
|
||||
}
|
||||
if err != nil {
|
||||
return stdout, err
|
||||
}
|
||||
var lines []string
|
||||
for _, line := range strings.Split(stdout, "\n") {
|
||||
if !strings.Contains(line, "/") || strings.Contains(line, "tmpfs") || strings.Contains(line, "snap/core") || strings.Contains(line, "udev") {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
if len(lines) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
func shouldExclude(fields []string, mountPoint string, excludes map[string]struct{}) bool {
|
||||
if strings.HasPrefix(mountPoint, "/snap") || len(strings.Split(mountPoint, "/")) > 10 {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(fields[1]) == "tmpfs" {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(fields[2], "K") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(mountPoint, "docker") {
|
||||
return true
|
||||
}
|
||||
_, excluded := excludes[mountPoint]
|
||||
return excluded
|
||||
}
|
||||
|
||||
func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) {
|
||||
var (
|
||||
opts []repo.DBOption
|
||||
@@ -412,6 +400,7 @@ func (a AlertService) parseAlertLog(item model.AlertLog) (dto.AlertLogDTO, error
|
||||
if err := unmarshalAlertInfo(item.AlertDetail, &alertDetail); err != nil {
|
||||
return dto.AlertLogDTO{}, err
|
||||
}
|
||||
alertDetail.Task = nil
|
||||
if err := unmarshalAlertInfo(item.AlertRule, &alertRule); err != nil {
|
||||
return dto.AlertLogDTO{}, err
|
||||
}
|
||||
@@ -494,7 +483,13 @@ func (a AlertService) GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertCon
|
||||
}
|
||||
opts = append(opts, repo.WithByStatus(constant.AlertEnable))
|
||||
configs, err := alertRepo.AlertConfigList(opts...)
|
||||
return configs, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := exposeCustomAlertConfigSecrets(configs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error) {
|
||||
@@ -505,13 +500,49 @@ func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []mode
|
||||
if len(req.ExcludeTypes) > 0 {
|
||||
opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes))
|
||||
}
|
||||
return alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...)
|
||||
total, configs, err := alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if err := exposeCustomAlertConfigSecrets(configs); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return total, configs, nil
|
||||
}
|
||||
|
||||
func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error {
|
||||
if req.Type == constant.Custom {
|
||||
if req.ID != 0 && req.Revision == nil {
|
||||
return repo.ErrAlertConfigRevisionRequired
|
||||
}
|
||||
return a.updateCustomAlertConfig(req, operator)
|
||||
}
|
||||
usesMutation, err := alertconfig.UsesMutation(req.Type, req.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.ID != 0 && usesMutation && req.Revision == nil {
|
||||
return repo.ErrAlertConfigRevisionRequired
|
||||
}
|
||||
var existing *model.AlertConfig
|
||||
if req.ID != 0 {
|
||||
stored, err := alertRepo.GetConfigById(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stored.Type != req.Type {
|
||||
return fmt.Errorf("alert config %d has type %s, not %s", req.ID, stored.Type, req.Type)
|
||||
}
|
||||
existing = &stored
|
||||
}
|
||||
if err := a.validateCommunityAlertConfigType(req.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
prepared, err := alertconfig.Prepare(req.Type, req.Config, req.Status, existing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Config = prepared
|
||||
if err := a.checkAlertConfigDisplayNameUnique(req); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -526,7 +557,7 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator stri
|
||||
upMap["status"] = req.Status
|
||||
upMap["config"] = req.Config
|
||||
upMap["update_user"] = operator
|
||||
if err := alertRepo.UpdateAlertConfig(upMap, repo.WithByID(req.ID)); err != nil {
|
||||
if err := alertRepo.UpdateAlertConfigWithRevision(upMap, req.Revision, repo.WithByID(req.ID)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
@@ -544,6 +575,99 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AlertService) updateCustomAlertConfig(req dto.AlertConfigUpdate, operator string) error {
|
||||
if err := validateAlertConfigStatus(req.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var existing *model.AlertConfig
|
||||
if req.ID != 0 {
|
||||
config, err := alertRepo.GetConfigById(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config.Type != constant.Custom {
|
||||
return fmt.Errorf("alert config %d is not a custom webhook", req.ID)
|
||||
}
|
||||
existing = &config
|
||||
}
|
||||
prepared, err := alertwebhook.Prepare(req.Config, req.Status, existing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
validatedReq := req
|
||||
validatedReq.Config = prepared.Config
|
||||
if err := a.checkAlertConfigDisplayNameUnique(validatedReq); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if existing != nil {
|
||||
return alertRepo.UpdateAlertConfigWithRevision(map[string]interface{}{
|
||||
"type": constant.Custom,
|
||||
"title": req.Title,
|
||||
"status": req.Status,
|
||||
"config": prepared.Config,
|
||||
"secret_config": prepared.SecretConfig,
|
||||
"update_user": operator,
|
||||
}, req.Revision, repo.WithByID(req.ID))
|
||||
}
|
||||
|
||||
return alertRepo.CreateAlertConfig(&model.AlertConfig{
|
||||
Type: constant.Custom,
|
||||
Title: req.Title,
|
||||
Status: req.Status,
|
||||
Config: prepared.Config,
|
||||
SecretConfig: prepared.SecretConfig,
|
||||
CreateUser: operator,
|
||||
UpdateUser: operator,
|
||||
})
|
||||
}
|
||||
|
||||
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error {
|
||||
if err := validateAlertConfigStatus(req.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
config, err := alertRepo.GetConfigById(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Status == constant.AlertEnable {
|
||||
if err := a.validateCommunityAlertConfigType(config.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
if config.Type == constant.Custom {
|
||||
if _, err := alertwebhook.Resolve(config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return alertRepo.UpdateAlertConfig(map[string]interface{}{
|
||||
"status": req.Status,
|
||||
"update_user": operator,
|
||||
}, repo.WithByID(req.ID))
|
||||
}
|
||||
|
||||
func validateAlertConfigStatus(status string) error {
|
||||
if status != constant.AlertEnable && status != constant.AlertDisable {
|
||||
return fmt.Errorf("alert config status must be Enable or Disable")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func exposeCustomAlertConfigSecrets(configs []model.AlertConfig) error {
|
||||
for index := range configs {
|
||||
if configs[index].Type != constant.Custom {
|
||||
continue
|
||||
}
|
||||
view, err := alertwebhook.PlainView(configs[index])
|
||||
if err != nil {
|
||||
return fmt.Errorf("build editable custom alert config %d: %w", configs[index].ID, err)
|
||||
}
|
||||
configs[index].Config = view
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate) error {
|
||||
if req.Type != constant.SMSConfig {
|
||||
return nil
|
||||
@@ -568,6 +692,9 @@ func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate)
|
||||
}
|
||||
|
||||
func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdate) error {
|
||||
if req.Type != constant.Custom && (global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn") {
|
||||
return nil
|
||||
}
|
||||
displayName := alertConfigDisplayName(req.Type, req.Config)
|
||||
if displayName == "" {
|
||||
return nil
|
||||
@@ -591,37 +718,67 @@ func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdat
|
||||
}
|
||||
|
||||
func (a AlertService) validateCommunityAlertMethod(method string) error {
|
||||
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(method) == "" {
|
||||
return nil
|
||||
methodTypes, err := a.validateAlertMethodReferences(method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.validateAlertMethodEntitlement(methodTypes)
|
||||
}
|
||||
|
||||
func (a AlertService) validateAlertMethodReferences(method string) ([]string, error) {
|
||||
if strings.TrimSpace(method) == "" {
|
||||
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||
}
|
||||
methodTypes := make([]string, 0)
|
||||
for _, item := range strings.Split(method, ",") {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
configType := ""
|
||||
if configID, err := strconv.ParseUint(item, 10, 64); err == nil {
|
||||
config, err := alertRepo.GetConfigById(uint(configID))
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := communityAlertMethodTypeNames[config.Type]; ok {
|
||||
return buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||
configType = config.Type
|
||||
} else {
|
||||
var ok bool
|
||||
configType, ok = legacyAlertMethodTypeMap[item]
|
||||
if !ok {
|
||||
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||
}
|
||||
}
|
||||
if _, ok := supportedAlertMethodTypes[configType]; !ok {
|
||||
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||
}
|
||||
methodTypes = append(methodTypes, configType)
|
||||
}
|
||||
if len(methodTypes) == 0 {
|
||||
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||
}
|
||||
return methodTypes, nil
|
||||
}
|
||||
|
||||
func (a AlertService) validateAlertMethodEntitlement(methodTypes []string) error {
|
||||
for _, configType := range methodTypes {
|
||||
if configType == constant.Custom {
|
||||
continue
|
||||
}
|
||||
if _, ok := communityAlertMethodTypeNames[item]; ok {
|
||||
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
|
||||
continue
|
||||
}
|
||||
if _, ok := communityAlertMethodTypeNames[configType]; ok {
|
||||
return buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AlertService) validateCommunityAlertConfigType(configType string) error {
|
||||
if configType == constant.Custom {
|
||||
return nil
|
||||
}
|
||||
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
|
||||
return nil
|
||||
}
|
||||
@@ -633,7 +790,7 @@ func (a AlertService) validateCommunityAlertConfigType(configType string) error
|
||||
|
||||
func alertConfigDisplayName(configType, configData string) string {
|
||||
switch configType {
|
||||
case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark, constant.SMS:
|
||||
case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark, constant.SMS, constant.Custom:
|
||||
var cfg struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
@@ -672,20 +829,24 @@ func (a AlertService) DeleteAlertConfig(id uint) error {
|
||||
}
|
||||
|
||||
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
||||
username := req.UserName
|
||||
if username == "" {
|
||||
username = req.Sender
|
||||
emailConfig, err := resolveEmailTestConfig(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
encodedDisplayName := mime.BEncoding.Encode("UTF-8", req.DisplayName)
|
||||
username := emailConfig.UserName
|
||||
if username == "" {
|
||||
username = emailConfig.Sender
|
||||
}
|
||||
encodedDisplayName := mime.BEncoding.Encode("UTF-8", emailConfig.DisplayName)
|
||||
cfg := email.SMTPConfig{
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Sender: req.Sender,
|
||||
Host: emailConfig.Host,
|
||||
Port: emailConfig.Port,
|
||||
Sender: emailConfig.Sender,
|
||||
Username: username,
|
||||
Password: req.Password,
|
||||
From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, req.Sender),
|
||||
Encryption: req.Encryption,
|
||||
Recipient: req.Recipient,
|
||||
Password: emailConfig.Password,
|
||||
From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, emailConfig.Sender),
|
||||
Encryption: emailConfig.Encryption,
|
||||
Recipient: emailConfig.Recipient,
|
||||
}
|
||||
|
||||
msg := email.EmailMessage{
|
||||
@@ -700,9 +861,94 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func resolveEmailTestConfig(req dto.AlertConfigTest) (dto.AlertEmailConfig, error) {
|
||||
emailConfig := dto.AlertEmailConfig{
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Sender: req.Sender,
|
||||
UserName: req.UserName,
|
||||
Password: req.Password,
|
||||
DisplayName: req.DisplayName,
|
||||
Encryption: req.Encryption,
|
||||
Recipient: req.Recipient,
|
||||
}
|
||||
if strings.TrimSpace(req.Config) != "" {
|
||||
configType := req.Type
|
||||
if configType == "" {
|
||||
configType = constant.EmailConfig
|
||||
}
|
||||
if configType != constant.EmailConfig {
|
||||
return dto.AlertEmailConfig{}, fmt.Errorf("alert config test type must be email")
|
||||
}
|
||||
var existing *model.AlertConfig
|
||||
if req.ID != 0 {
|
||||
stored, err := alertRepo.GetConfigById(req.ID)
|
||||
if err != nil {
|
||||
return dto.AlertEmailConfig{}, err
|
||||
}
|
||||
existing = &stored
|
||||
}
|
||||
prepared, err := alertconfig.Prepare(configType, req.Config, constant.AlertEnable, existing)
|
||||
if err != nil {
|
||||
return dto.AlertEmailConfig{}, err
|
||||
}
|
||||
if err := json.Unmarshal([]byte(prepared), &emailConfig); err != nil {
|
||||
return dto.AlertEmailConfig{}, fmt.Errorf("decode email alert config: %w", err)
|
||||
}
|
||||
}
|
||||
return emailConfig, nil
|
||||
}
|
||||
|
||||
func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error) {
|
||||
if req.Type != constant.Custom {
|
||||
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config test type must be custom")
|
||||
}
|
||||
var existing *model.AlertConfig
|
||||
if req.ID != 0 {
|
||||
config, err := alertRepo.GetConfigById(req.ID)
|
||||
if err != nil {
|
||||
return dto.AlertConfigTestResult{}, err
|
||||
}
|
||||
if config.Type != constant.Custom {
|
||||
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config %d is not a custom webhook", req.ID)
|
||||
}
|
||||
existing = &config
|
||||
}
|
||||
prepared, err := alertwebhook.Prepare(req.Config, constant.AlertEnable, existing)
|
||||
if err != nil {
|
||||
return dto.AlertConfigTestResult{}, err
|
||||
}
|
||||
resolved, err := alertwebhook.Resolve(model.AlertConfig{
|
||||
Type: constant.Custom,
|
||||
Config: prepared.Config,
|
||||
SecretConfig: prepared.SecretConfig,
|
||||
})
|
||||
if err != nil {
|
||||
return dto.AlertConfigTestResult{}, err
|
||||
}
|
||||
tester, ok := xpack.AlertProvider.(providers.CustomWebhookTester)
|
||||
if !ok {
|
||||
return dto.AlertConfigTestResult{
|
||||
Success: false,
|
||||
Message: providers.ErrCustomWebhookUnsupported.Error(),
|
||||
}, nil
|
||||
}
|
||||
return tester.TestCustomWebhook(resolved)
|
||||
}
|
||||
|
||||
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
|
||||
if err := a.validateCommunityAlertMethod(updateAlert.Method); err != nil {
|
||||
return err
|
||||
var methodTypes []string
|
||||
if updateAlert.SendCount != 0 || strings.TrimSpace(updateAlert.Method) != "" {
|
||||
var err error
|
||||
methodTypes, err = a.validateAlertMethodReferences(updateAlert.Method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if updateAlert.SendCount != 0 {
|
||||
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
upMap := make(map[string]interface{})
|
||||
var newStatus string
|
||||
@@ -721,6 +967,23 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
||||
alertRepo.WithByType(updateAlert.Type),
|
||||
alertRepo.WithByProject(updateAlert.Project),
|
||||
)
|
||||
advanced, err := prepareCronJobAlertParams(updateAlert.Type, alertInfo.AdvancedParams, updateAlert.AdvancedParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateAlert.AdvancedParams = advanced
|
||||
if alertUtil.GetCronJobType(updateAlert.Type) == "cronJob" {
|
||||
upMap["advanced_params"] = advanced
|
||||
}
|
||||
if newStatus == constant.AlertEnable {
|
||||
method := updateAlert.Method
|
||||
if method == "" {
|
||||
method = alertInfo.Method
|
||||
}
|
||||
if err := a.validateCronJobAlertChannels(updateAlert.Type, advanced, method); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if alertInfo.ID > 0 {
|
||||
shouldUpdate := false
|
||||
@@ -734,6 +997,9 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
||||
if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method {
|
||||
shouldUpdate = true
|
||||
}
|
||||
if val, ok := upMap["advanced_params"]; ok && val != alertInfo.AdvancedParams {
|
||||
shouldUpdate = true
|
||||
}
|
||||
|
||||
if shouldUpdate {
|
||||
if err := alertRepo.Update(
|
||||
@@ -755,3 +1021,22 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareCronJobAlertParams(alertType, previous, incoming string) (string, error) {
|
||||
if alertUtil.GetCronJobType(alertType) != "cronJob" {
|
||||
return incoming, nil
|
||||
}
|
||||
return alertUtil.MergeCronJobAlertParams(previous, incoming)
|
||||
}
|
||||
|
||||
func (a AlertService) validateCronJobAlertChannels(alertType, advanced, method string) error {
|
||||
if alertUtil.GetCronJobType(alertType) != "cronJob" {
|
||||
return nil
|
||||
}
|
||||
mode, err := alertUtil.CronJobAlertTriggerMode(advanced)
|
||||
if err != nil || mode != alertUtil.CronJobAlertSuccess {
|
||||
return err
|
||||
}
|
||||
_, err = a.validateAlertMethodReferences(method)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
@@ -29,9 +30,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ResourceAlertInterval = 30
|
||||
CheckIntervalSec = 3
|
||||
LoadCheckIntervalMin = 5
|
||||
ResourceAlertInterval = 30
|
||||
CheckIntervalSec = 3
|
||||
LoadCheckIntervalMin = 5
|
||||
sshIPLoginWindow = 30 * time.Minute
|
||||
sslAutoRenewAlertSkipDays = 31
|
||||
)
|
||||
|
||||
type AlertTaskHelper struct {
|
||||
@@ -512,10 +515,32 @@ func loadPanelLogin(alert dto.AlertDTO) {
|
||||
}
|
||||
|
||||
func loadSSHLogin(alert dto.AlertDTO) {
|
||||
count, isAlert, err := alertUtil.CountRecentFailedSSHLog(alert.Cycle, alert.Count)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Failed to count recent failed ssh login logs: %v", err)
|
||||
now := time.Now()
|
||||
failedWindow := time.Duration(alert.Cycle) * time.Minute
|
||||
loadWindow := failedWindow
|
||||
if loadWindow < sshIPLoginWindow {
|
||||
loadWindow = sshIPLoginWindow
|
||||
}
|
||||
location, err := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Failed to load timezone for ssh login logs: %v", err)
|
||||
location = time.Local
|
||||
}
|
||||
histories, err := loadSSHAlertHistories(defaultSSHLogDir, now.Add(-loadWindow), now, location)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Failed to load ssh login logs: %v", err)
|
||||
}
|
||||
interfaceAddrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
global.LOG.Warnf("Failed to load local IP addresses for ssh login alert: %v", err)
|
||||
}
|
||||
count, records := summarizeSSHLoginHistories(
|
||||
histories,
|
||||
now,
|
||||
failedWindow,
|
||||
sshSuccessLoginWhitelist(alert.AdvancedParams, interfaceAddrs),
|
||||
)
|
||||
isAlert := count >= int(alert.Count)
|
||||
if isAlert {
|
||||
params := []dto.Param{
|
||||
{
|
||||
@@ -531,12 +556,6 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
||||
}
|
||||
sendAlerts(alert, "sshLogin", strconv.Itoa(count), "sshLogin", params)
|
||||
}
|
||||
whitelist := strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n")
|
||||
records, err := alertUtil.FindRecentSuccessLoginNotInWhitelist(30, whitelist)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Failed to check recent failed ip ssh login logs: %v", err)
|
||||
}
|
||||
records = filterSSHLoginEntriesNotInWhitelist(records, whitelist)
|
||||
if len(records) > 0 {
|
||||
quota := strings.Join(records, "\n")
|
||||
params := []dto.Param{
|
||||
@@ -555,6 +574,19 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
||||
}
|
||||
}
|
||||
|
||||
func sshSuccessLoginWhitelist(configured string, interfaceAddrs []net.Addr) []string {
|
||||
whitelist := strings.Split(strings.TrimSpace(configured), "\n")
|
||||
whitelist = append(whitelist, "127.0.0.0/8", "::1")
|
||||
for _, addr := range interfaceAddrs {
|
||||
ipNet, ok := addr.(*net.IPNet)
|
||||
if !ok || ipNet.IP == nil || ipNet.IP.IsUnspecified() {
|
||||
continue
|
||||
}
|
||||
whitelist = append(whitelist, ipNet.IP.String())
|
||||
}
|
||||
return whitelist
|
||||
}
|
||||
|
||||
func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) []model.LoginLog {
|
||||
filtered := make([]model.LoginLog, 0, len(records))
|
||||
for _, record := range records {
|
||||
@@ -565,20 +597,6 @@ func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string)
|
||||
return filtered
|
||||
}
|
||||
|
||||
func filterSSHLoginEntriesNotInWhitelist(records []string, whitelist []string) []string {
|
||||
filtered := make([]string, 0, len(records))
|
||||
for _, record := range records {
|
||||
ip := record
|
||||
if idx := strings.Index(record, "-"); idx >= 0 {
|
||||
ip = record[:idx]
|
||||
}
|
||||
if !isIPInWhitelist(ip, whitelist) {
|
||||
filtered = append(filtered, record)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func isIPInWhitelist(ip string, whitelist []string) bool {
|
||||
targetIP := net.ParseIP(strings.TrimSpace(ip))
|
||||
if targetIP == nil {
|
||||
@@ -695,9 +713,10 @@ func sendAlertsByConfigId(alert dto.AlertDTO, alertType, quota, quotaType string
|
||||
|
||||
func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, method string) {
|
||||
typeMap := map[string]string{
|
||||
"mail": constant.Email,
|
||||
constant.Bark: constant.Bark,
|
||||
constant.SMS: constant.SMS,
|
||||
"mail": constant.Email,
|
||||
constant.Bark: constant.Bark,
|
||||
constant.SMS: constant.SMS,
|
||||
constant.Custom: constant.Custom,
|
||||
}
|
||||
configType, ok := typeMap[method]
|
||||
if !ok {
|
||||
@@ -785,7 +804,7 @@ func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params
|
||||
}
|
||||
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
|
||||
|
||||
case constant.WeCom, constant.DingTalk, constant.FeiShu:
|
||||
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Custom:
|
||||
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr)
|
||||
if !isValid {
|
||||
return
|
||||
@@ -798,12 +817,31 @@ func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params
|
||||
}
|
||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
|
||||
alertErr := xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo)
|
||||
queued := false
|
||||
var alertErr error
|
||||
if config.Type == constant.Custom {
|
||||
task := dto.AlertTaskMetadata{
|
||||
AlertID: alert.ID,
|
||||
Type: alertType,
|
||||
Quota: quota,
|
||||
QuotaType: quotaType,
|
||||
Method: methodStr,
|
||||
}
|
||||
result, deliveryErr := xpack.DeliverCustomWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo, task)
|
||||
queued, alertErr = result.Queued, deliveryErr
|
||||
if alertErr == nil && result.Queued {
|
||||
_, alertErr = alertUtil.RecordQueuedAlertTask(result.LogID, task)
|
||||
}
|
||||
} else {
|
||||
alertErr = xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo)
|
||||
}
|
||||
if alertErr != nil {
|
||||
global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, methodStr, alertErr)
|
||||
return
|
||||
}
|
||||
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
|
||||
if !queued {
|
||||
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -844,7 +882,7 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
|
||||
daysDiff := int(math.Ceil(
|
||||
ssl.ExpireDate.Sub(currentDate).Hours() / 24,
|
||||
))
|
||||
if daysDiff > 0 && int(cycle) >= daysDiff {
|
||||
if daysDiff > 0 && int(cycle) >= daysDiff && !shouldSuppressSSLExpiryAlert(ssl, daysDiff) {
|
||||
daysDiffMap[daysDiff] = append(daysDiffMap[daysDiff], ssl.PrimaryDomain)
|
||||
projectMap[ssl.ID] = append(projectMap[ssl.ID], ssl.ExpireDate)
|
||||
}
|
||||
@@ -852,6 +890,10 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
|
||||
return daysDiffMap, projectMap
|
||||
}
|
||||
|
||||
func shouldSuppressSSLExpiryAlert(ssl model.WebsiteSSL, remainingDays int) bool {
|
||||
return ssl.AutoRenew && remainingDays < sslAutoRenewAlertSkipDays
|
||||
}
|
||||
|
||||
func calculateWebsiteExpiryDays(websites []model.Website, cycle uint) (map[int][]string, map[uint][]time.Time) {
|
||||
currentDate := time.Now()
|
||||
daysDiffMap := make(map[int][]string)
|
||||
@@ -1024,50 +1066,37 @@ func processAllDisks(alert dto.AlertDTO) error {
|
||||
global.LOG.Errorf("error getting disk list, err: %v", err)
|
||||
return err
|
||||
}
|
||||
var errMsgs []string
|
||||
for _, item := range diskList {
|
||||
err := checkAndCreateDiskAlert(alert, item.Path)
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("disk path %s process failed: %v", item.Path, err)
|
||||
errMsgs = append(errMsgs, errMsg)
|
||||
global.LOG.Errorf("%s", errMsg)
|
||||
if item.Total == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(errMsgs) > 0 {
|
||||
return fmt.Errorf("batch process disks failed, error count: %d, details: %s", len(errMsgs), strings.Join(errMsgs, "; "))
|
||||
checkAndCreateDiskAlert(alert, item.Path, &disk.UsageStat{Used: item.Used, UsedPercent: item.UsedPercent})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func processSingleDisk(alert dto.AlertDTO) error {
|
||||
err := checkAndCreateDiskAlert(alert, alert.Project)
|
||||
usageStat, err := loadDiskUsageWithTimeout(alert.Project, true)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("%s", err.Error())
|
||||
global.LOG.Errorf("error getting disk usage for %s, err: %v", alert.Project, err)
|
||||
return err
|
||||
}
|
||||
checkAndCreateDiskAlert(alert, alert.Project, usageStat)
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string) error {
|
||||
usageStat, err := psutil.DISK.GetUsage(path, false)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("error getting disk usage for %s, err: %v", path, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string, usageStat *disk.UsageStat) {
|
||||
usedTotal, usedStr := calculateUsedTotal(alert.Cycle, usageStat)
|
||||
commonTotal := float64(alert.Count)
|
||||
if alert.Cycle == 1 {
|
||||
commonTotal *= 1024 * 1024 * 1024
|
||||
}
|
||||
if usedTotal < commonTotal {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
params := createAlertDiskParams(path, usedStr)
|
||||
sender := NewAlertSender(alert, alert.Project)
|
||||
sender.ResourceSend(path, params)
|
||||
return nil
|
||||
}
|
||||
|
||||
func calculateUsedTotal(cycle uint, usageStat *disk.UsageStat) (float64, string) {
|
||||
@@ -1097,3 +1126,55 @@ func calculateMinutesDifference(newDate time.Time) int {
|
||||
minutesDifference := int(now.Sub(newDate).Minutes())
|
||||
return minutesDifference
|
||||
}
|
||||
|
||||
func loadSSHAlertHistories(
|
||||
baseDir string,
|
||||
startTime, endTime time.Time,
|
||||
location *time.Location,
|
||||
) ([]dto.SSHHistory, error) {
|
||||
fileList, err := listSSHLogFiles(baseDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
histories []dto.SSHHistory
|
||||
loadErr error
|
||||
)
|
||||
for _, file := range fileList {
|
||||
items, err := loadSSHHistoriesFromFile(file.Name, "", "", startTime, endTime, file.Year, location)
|
||||
if err != nil {
|
||||
loadErr = errors.Join(loadErr, fmt.Errorf("load SSH log file %s: %w", file.Name, err))
|
||||
continue
|
||||
}
|
||||
histories = append(histories, items...)
|
||||
}
|
||||
return histories, loadErr
|
||||
}
|
||||
|
||||
func summarizeSSHLoginHistories(
|
||||
histories []dto.SSHHistory,
|
||||
now time.Time,
|
||||
failedWindow time.Duration,
|
||||
whitelist []string,
|
||||
) (int, []string) {
|
||||
failedStartTime := now.Add(-failedWindow)
|
||||
successStartTime := now.Add(-sshIPLoginWindow)
|
||||
failedCount := 0
|
||||
var abnormalLogins []string
|
||||
|
||||
for _, item := range histories {
|
||||
switch item.Status {
|
||||
case constant.StatusFailed:
|
||||
if isSSHLogWithinTimeRange(item.Date, failedStartTime, now) {
|
||||
failedCount++
|
||||
}
|
||||
case constant.StatusSuccess:
|
||||
if !isSSHLogWithinTimeRange(item.Date, successStartTime, now) || isIPInWhitelist(item.Address, whitelist) {
|
||||
continue
|
||||
}
|
||||
abnormalLogins = append(abnormalLogins, fmt.Sprintf("%s-%s", item.Address, item.Date.Format(constant.DateTimeLayout)))
|
||||
}
|
||||
}
|
||||
return failedCount, abnormalLogins
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func (s *AlertSender) sendByConfig(config model.AlertConfig, quota string, param
|
||||
} else {
|
||||
s.sendBarkWithConfig(config, quota, params)
|
||||
}
|
||||
case constant.WeCom, constant.DingTalk, constant.FeiShu:
|
||||
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Custom:
|
||||
if isResource {
|
||||
s.sendResourceWebhookWithConfig(config, quota, params)
|
||||
} else {
|
||||
@@ -86,7 +86,7 @@ func (s *AlertSender) sendByConfig(config model.AlertConfig, quota string, param
|
||||
|
||||
func (s *AlertSender) sendByLegacyMethod(method string, quota string, params []dto.Param, isResource bool) {
|
||||
alertRepo := repo.NewIAlertRepo()
|
||||
typeMap := map[string]string{"mail": constant.Email, constant.Bark: constant.Bark, constant.SMS: constant.SMS}
|
||||
typeMap := map[string]string{"mail": constant.Email, constant.Bark: constant.Bark, constant.SMS: constant.SMS, constant.Custom: constant.Custom}
|
||||
configType := method
|
||||
if mapped, ok := typeMap[method]; ok {
|
||||
configType = mapped
|
||||
@@ -308,12 +308,31 @@ func (s *AlertSender) sendWebhookWithConfig(config model.AlertConfig, quota stri
|
||||
}
|
||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
|
||||
err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
|
||||
queued := false
|
||||
var err error
|
||||
if config.Type == constant.Custom {
|
||||
task := dto.AlertTaskMetadata{
|
||||
AlertID: s.alert.ID,
|
||||
Type: s.alert.Type,
|
||||
Quota: quota,
|
||||
QuotaType: s.quotaType,
|
||||
Method: strconv.Itoa(int(config.ID)),
|
||||
}
|
||||
result, deliveryErr := xpack.DeliverCustomWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo, task)
|
||||
queued, err = result.Queued, deliveryErr
|
||||
if err == nil && result.Queued {
|
||||
_, err = alertUtil.RecordQueuedAlertTask(result.LogID, task)
|
||||
}
|
||||
} else {
|
||||
err = xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
|
||||
}
|
||||
if err != nil {
|
||||
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
|
||||
return
|
||||
}
|
||||
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
|
||||
if !queued {
|
||||
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
|
||||
@@ -334,11 +353,31 @@ func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, qu
|
||||
}
|
||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
|
||||
if err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo); err != nil {
|
||||
queued := false
|
||||
var err error
|
||||
if config.Type == constant.Custom {
|
||||
task := dto.AlertTaskMetadata{
|
||||
AlertID: s.alert.ID,
|
||||
Type: s.alert.Type,
|
||||
Quota: quota,
|
||||
QuotaType: s.quotaType,
|
||||
Method: strconv.Itoa(int(config.ID)),
|
||||
}
|
||||
result, deliveryErr := xpack.DeliverCustomWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo, task)
|
||||
queued, err = result.Queued, deliveryErr
|
||||
if err == nil && result.Queued {
|
||||
_, err = alertUtil.RecordQueuedAlertTask(result.LogID, task)
|
||||
}
|
||||
} else {
|
||||
err = xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
|
||||
}
|
||||
if err != nil {
|
||||
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
|
||||
return
|
||||
}
|
||||
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
|
||||
if !queued {
|
||||
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) {
|
||||
|
||||
+20
-13
@@ -223,6 +223,9 @@ func (a AppService) GetAppDetailByKey(appKey, version string) (response.AppDetai
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
if err = checkVllmVersionAccess(app.Key, version); err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
appDetail, err := appDetailRepo.GetFirst(appDetailRepo.WithAppId(app.ID), appDetailRepo.WithVersion(version))
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
@@ -241,14 +244,17 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
app, err := appRepo.GetFirst(repo.WithByID(detail.AppId))
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
if err = checkVllmVersionAccess(app.Key, detail.Version); err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
appDetailDTO.AppDetail = detail
|
||||
appDetailDTO.Enable = true
|
||||
|
||||
if appType == "runtime" {
|
||||
app, err := appRepo.GetFirst(repo.WithByID(appID))
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
|
||||
versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version)
|
||||
@@ -319,10 +325,6 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
||||
|
||||
appDetailDTO.HostMode = isHostModel(appDetailDTO.DockerCompose)
|
||||
|
||||
app, err := appRepo.GetFirst(repo.WithByID(detail.AppId))
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
if err := checkLimit(app); err != nil {
|
||||
appDetailDTO.Enable = false
|
||||
}
|
||||
@@ -374,6 +376,9 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = checkVllmVersionAccess(app.Key, appDetail.Version); err != nil {
|
||||
return
|
||||
}
|
||||
if DatabaseKeys[app.Key] > 0 {
|
||||
if existDatabases, _ := databaseRepo.GetList(repo.WithByName(req.Name)); len(existDatabases) > 0 {
|
||||
err = buserr.New("ErrRemoteExist")
|
||||
@@ -483,15 +488,17 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
index++
|
||||
}
|
||||
newServiceName := strings.ToLower(appInstall.Name)
|
||||
if app.Limit == 0 && newServiceName != serviceName && len(servicesMap) == 1 {
|
||||
if app.Limit == 0 && newServiceName != serviceName && len(servicesMap) == 1 && !req.KeepServiceName {
|
||||
servicesMap[newServiceName] = servicesMap[serviceName]
|
||||
delete(servicesMap, serviceName)
|
||||
serviceName = newServiceName
|
||||
}
|
||||
appInstall.ServiceName = serviceName
|
||||
|
||||
if err = addDockerComposeCommonParam(composeMap, appInstall.ServiceName, req.AppContainerConfig, req.Params); err != nil {
|
||||
return
|
||||
if !req.SkipComposeCommonConfig {
|
||||
if err = addDockerComposeCommonParam(composeMap, appInstall.ServiceName, req.AppContainerConfig, req.Params); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
var (
|
||||
composeByte []byte
|
||||
@@ -559,7 +566,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
return err
|
||||
}
|
||||
}
|
||||
if executeScript {
|
||||
if executeScript || req.UseLifecycleScripts {
|
||||
if err = runScript(t, appInstall, "init"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -572,7 +579,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = upApp(t, appInstall, req.PullImage); err != nil {
|
||||
if err = upApp(t, appInstall, req.PullImage, req.UseLifecycleScripts); err != nil {
|
||||
return err
|
||||
}
|
||||
updateToolApp(appInstall)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -13,12 +14,14 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
@@ -252,6 +255,9 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
||||
return buserr.New("ErrInstallDirNotFound")
|
||||
}
|
||||
dockerComposePath := install.GetComposePath()
|
||||
if req.UseLifecycleScripts && (req.Operate == constant.Start || req.Operate == constant.Stop || req.Operate == constant.Restart) {
|
||||
return operateAppWithLifecycleScripts(install, req, nil)
|
||||
}
|
||||
switch req.Operate {
|
||||
case constant.Rebuild:
|
||||
return rebuildApp(install)
|
||||
@@ -275,12 +281,13 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
||||
return syncAppInstallStatus(&install, false)
|
||||
case constant.Delete:
|
||||
deleteReq := request.AppInstallDelete{
|
||||
Install: install,
|
||||
DeleteBackup: req.DeleteBackup,
|
||||
ForceDelete: req.ForceDelete,
|
||||
DeleteDB: req.DeleteDB,
|
||||
DeleteImage: req.DeleteImage,
|
||||
TaskID: req.TaskID,
|
||||
Install: install,
|
||||
DeleteBackup: req.DeleteBackup,
|
||||
ForceDelete: req.ForceDelete,
|
||||
DeleteDB: req.DeleteDB,
|
||||
DeleteImage: req.DeleteImage,
|
||||
TaskID: req.TaskID,
|
||||
UseLifecycleScripts: req.UseLifecycleScripts,
|
||||
}
|
||||
if err = deleteAppInstall(deleteReq); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
@@ -312,6 +319,70 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
||||
}
|
||||
}
|
||||
|
||||
func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppInstalledOperate, onFailure func(error)) error {
|
||||
taskType := task.TaskUpdate
|
||||
switch req.Operate {
|
||||
case constant.Start:
|
||||
install.Status = constant.StatusStarting
|
||||
case constant.Restart:
|
||||
taskType = task.TaskRestart
|
||||
install.Status = constant.StatusRestarting
|
||||
case constant.Stop:
|
||||
install.Status = constant.StatusWaiting
|
||||
default:
|
||||
return errors.New("lifecycle script operation not supported")
|
||||
}
|
||||
install.Message = ""
|
||||
if err := appInstallRepo.Save(context.Background(), &install); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
operation := string(req.Operate)
|
||||
operationTask.AddSubTaskWithOps(
|
||||
task.GetTaskName(install.Name, taskType, task.TaskScopeApp),
|
||||
func(t *task.Task) error {
|
||||
if err := runScript(t, &install, operation); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Operate == constant.Stop {
|
||||
install.Status = constant.StatusStopped
|
||||
install.Message = ""
|
||||
return appInstallRepo.Save(context.Background(), &install)
|
||||
}
|
||||
containerNames, err := getContainerNames(install)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(containerNames) == 0 {
|
||||
return buserr.WithName("ErrContainerNotFound", install.Name)
|
||||
}
|
||||
install.ContainerName = strings.Join(containerNames, ",")
|
||||
install.Status = constant.StatusRunning
|
||||
install.Message = ""
|
||||
return appInstallRepo.Save(context.Background(), &install)
|
||||
},
|
||||
nil,
|
||||
0,
|
||||
time.Hour,
|
||||
)
|
||||
go func() {
|
||||
if taskErr := operationTask.Execute(); taskErr != nil {
|
||||
if onFailure != nil {
|
||||
onFailure(taskErr)
|
||||
return
|
||||
}
|
||||
install.Status = constant.StatusUpErr
|
||||
install.Message = taskErr.Error()
|
||||
_ = appInstallRepo.Save(context.Background(), &install)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AppInstallService) UpdateAppConfig(req request.AppConfigUpdate) error {
|
||||
installed, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
|
||||
if err != nil {
|
||||
@@ -374,8 +445,10 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params); err != nil {
|
||||
return err
|
||||
if !req.SkipComposeCommonConfig {
|
||||
if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
composeByte, err := yaml.Marshal(composeMap)
|
||||
if err != nil {
|
||||
@@ -408,7 +481,7 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backupEnvMaps := oldEnvMaps
|
||||
backupEnvMaps := maps.Clone(oldEnvMaps)
|
||||
handleMap(req.Params, oldEnvMaps)
|
||||
paramByte, err := json.Marshal(oldEnvMaps)
|
||||
if err != nil {
|
||||
@@ -420,13 +493,32 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
_ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(installed.DockerCompose), constant.DirPerm)
|
||||
if err := rebuildApp(installed); err != nil {
|
||||
restoreConfig := func(operationErr error) {
|
||||
_ = env.Write(backupEnvMaps, envPath)
|
||||
_ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(backupDockerCompose), constant.DirPerm)
|
||||
failed := oldInstalled
|
||||
failed.Status = constant.StatusUpErr
|
||||
failed.Message = operationErr.Error()
|
||||
_ = appInstallRepo.Save(context.Background(), &failed)
|
||||
}
|
||||
if req.UseLifecycleScripts {
|
||||
err = operateAppWithLifecycleScripts(installed, request.AppInstalledOperate{
|
||||
InstallId: installed.ID,
|
||||
Operate: constant.Restart,
|
||||
TaskID: req.TaskID,
|
||||
UseLifecycleScripts: true,
|
||||
}, restoreConfig)
|
||||
} else {
|
||||
err = rebuildApp(installed)
|
||||
}
|
||||
if err != nil {
|
||||
restoreConfig(err)
|
||||
return err
|
||||
}
|
||||
installed.Status = constant.StatusRunning
|
||||
_ = appInstallRepo.Save(context.Background(), &installed)
|
||||
if !req.UseLifecycleScripts {
|
||||
installed.Status = constant.StatusRunning
|
||||
_ = appInstallRepo.Save(context.Background(), &installed)
|
||||
}
|
||||
|
||||
proxyChanged := hasAppInstallProxyPassChanged(&oldInstalled, &installed)
|
||||
currentProxy, currentProxyErr := getAppInstallProxyPass(&installed)
|
||||
@@ -583,6 +675,9 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
||||
return versions, err
|
||||
}
|
||||
for _, detail := range details {
|
||||
if !canAccessVllmVersion(app.Key, detail.Version) {
|
||||
continue
|
||||
}
|
||||
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
||||
if len(ignores) > 0 {
|
||||
continue
|
||||
@@ -836,7 +931,9 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
||||
}
|
||||
|
||||
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
||||
if appInstall.Status == constant.StatusInstalling || appInstall.Status == constant.StatusRebuilding || appInstall.Status == constant.StatusUpgrading || appInstall.Status == constant.StatusUninstalling {
|
||||
switch appInstall.Status {
|
||||
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
||||
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
|
||||
return nil
|
||||
}
|
||||
cli, err := docker.NewClient()
|
||||
|
||||
@@ -40,14 +40,13 @@ const (
|
||||
appUpgradeDown
|
||||
appUpgradeMutated
|
||||
appUpgradeStarted
|
||||
appUpgradeReady
|
||||
appUpgradeCommitted
|
||||
)
|
||||
|
||||
const composeServiceLabel = "com.docker.compose.service"
|
||||
|
||||
var appUpgradeLocks sync.Map
|
||||
|
||||
const composeServiceLabel = "com.docker.compose.service"
|
||||
|
||||
type appUpgradeSnapshot interface {
|
||||
Restore() error
|
||||
Cleanup()
|
||||
@@ -107,6 +106,9 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = checkVllmVersionAccess(install.App.Key, detail.Version); err != nil {
|
||||
return err
|
||||
}
|
||||
if install.App.Key == vllmAppKeyForUpgrade && !isVllmUpgradeVersionAllowed(install.Version, detail.Version, loadVllmImageFromEnv(install.Env)) {
|
||||
return errors.New("vLLM can only upgrade within the same image type")
|
||||
}
|
||||
@@ -436,15 +438,14 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
|
||||
t.LogSuccess(logStr)
|
||||
u.phase = appUpgradeStarted
|
||||
|
||||
t.LogStart(i18n.GetMsgByKey("UpgradeWaitReady"))
|
||||
containerNames, err := waitAppContainersReady(context.Background(), u.candidate)
|
||||
if err != nil {
|
||||
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeWaitReady"), err)
|
||||
return err
|
||||
containerNames, discoverErr := discoverUpgradeContainerNames(u.candidate, u.envContent)
|
||||
if discoverErr != nil {
|
||||
t.Logf("WARNING: discover upgraded application containers failed: %v", discoverErr)
|
||||
} else if len(containerNames) > 0 {
|
||||
u.candidate.ContainerName = strings.Join(containerNames, ",")
|
||||
} else {
|
||||
t.Log("WARNING: no containers found for the upgraded application")
|
||||
}
|
||||
t.LogSuccess(i18n.GetMsgByKey("UpgradeWaitReady"))
|
||||
u.phase = appUpgradeReady
|
||||
u.candidate.ContainerName = strings.Join(containerNames, ",")
|
||||
u.candidate.Status = constant.StatusRunning
|
||||
u.candidate.Message = ""
|
||||
|
||||
@@ -459,9 +460,21 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
// Upgrades deliberately keep the user's nginx.conf, so corrected gzip
|
||||
// defaults shipped with a new version would never reach existing
|
||||
// installations. Rewrite only an untouched factory configuration, and
|
||||
// never fail the upgrade over it.
|
||||
if gzipErr := upgradeStockNginxGzipConfig(u.candidate); gzipErr != nil {
|
||||
t.Logf("WARNING: update stock gzip configuration failed, keeping the current one: %v", gzipErr)
|
||||
}
|
||||
} else if err = appInstallRepo.Save(context.Background(), &u.candidate); err != nil {
|
||||
return err
|
||||
}
|
||||
if discoverErr == nil && len(containerNames) > 0 {
|
||||
if syncErr := syncAppInstallStatus(&u.candidate, true); syncErr != nil {
|
||||
t.Logf("WARNING: sync upgraded application status failed: %v", syncErr)
|
||||
}
|
||||
}
|
||||
u.phase = appUpgradeCommitted
|
||||
u.deleteOldImages(t)
|
||||
return nil
|
||||
@@ -581,9 +594,6 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
|
||||
}
|
||||
|
||||
func (u *appUpgradeContext) finishRollback() error {
|
||||
if _, err := waitAppContainersReady(context.Background(), u.original); err != nil {
|
||||
return err
|
||||
}
|
||||
restored := u.original
|
||||
if err := appInstallRepo.Save(context.Background(), &restored); err != nil {
|
||||
return err
|
||||
@@ -715,6 +725,20 @@ func renderUpgradeEnv(install *model.AppInstall, original []byte) ([]byte, error
|
||||
return nil, err
|
||||
}
|
||||
handleMap(envs, params)
|
||||
if install.App.Key == "openlist" {
|
||||
// The upgrade script updates this too late for the pre-pull phase.
|
||||
image := "openlistteam/openlist:v" + strings.TrimPrefix(install.Version, "v")
|
||||
if preInstalled := params["PRE_INSTALLED"]; preInstalled != "" {
|
||||
image += "-" + preInstalled
|
||||
}
|
||||
params["OPENLIST_IMAGE"] = image
|
||||
envs["OPENLIST_IMAGE"] = image
|
||||
content, err := json.Marshal(envs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
install.Env = string(content)
|
||||
}
|
||||
if install.App.Key == constant.AppOpenresty {
|
||||
for _, key := range []string{"CONTAINER_PACKAGE_URL", "RESTY_ADD_PACKAGE_BUILDDEPS", "RESTY_CONFIG_OPTIONS_MORE"} {
|
||||
if value, ok := originalEnv[key]; ok {
|
||||
@@ -857,28 +881,7 @@ func (s *upgradeFileSnapshot) Cleanup() {
|
||||
}
|
||||
}
|
||||
|
||||
type appContainerReadinessClient interface {
|
||||
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
|
||||
ContainerInspect(context.Context, string) (container.InspectResponse, error)
|
||||
}
|
||||
|
||||
func waitAppContainersReady(ctx context.Context, install model.AppInstall) ([]string, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
return waitAppContainersReadyWithClient(ctx, client, install)
|
||||
}
|
||||
|
||||
func waitAppContainersReadyWithClient(ctx context.Context, client appContainerReadinessClient, install model.AppInstall) ([]string, error) {
|
||||
envContent, err := os.ReadFile(install.GetEnvPath())
|
||||
if err != nil {
|
||||
envContent, err = renderUpgradeEnv(&install, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
func discoverUpgradeContainerNames(install model.AppInstall, envContent []byte) ([]string, error) {
|
||||
project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), envContent, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -892,36 +895,24 @@ func waitAppContainersReadyWithClient(ctx context.Context, client appContainerRe
|
||||
if len(expectedServices) == 0 {
|
||||
return strings.Split(install.ContainerName, ","), nil
|
||||
}
|
||||
options := container.ListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(
|
||||
filters.Arg("label", composeWorkdirLabel+"="+install.GetPath()),
|
||||
),
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
containers, err := client.ContainerList(ctx, options)
|
||||
defer client.Close()
|
||||
containers, err := client.ContainerList(context.Background(), container.ListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(filters.Arg("label", composeWorkdirLabel+"="+install.GetPath())),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
foundServices := make(map[string]bool, len(expectedServices))
|
||||
containerNames := make([]string, 0, len(containers))
|
||||
for _, item := range containers {
|
||||
serviceName := item.Labels[composeServiceLabel]
|
||||
if _, ok := expectedServices[serviceName]; !ok {
|
||||
continue
|
||||
}
|
||||
if err = waitContainerReady(ctx, client, item.ID); err != nil {
|
||||
return nil, fmt.Errorf("container %s is not ready: %w", serviceName, err)
|
||||
}
|
||||
foundServices[serviceName] = true
|
||||
if len(item.Names) > 0 {
|
||||
if _, ok := expectedServices[item.Labels[composeServiceLabel]]; ok && len(item.Names) > 0 {
|
||||
containerNames = append(containerNames, strings.TrimPrefix(item.Names[0], "/"))
|
||||
}
|
||||
}
|
||||
for serviceName := range expectedServices {
|
||||
if !foundServices[serviceName] {
|
||||
return nil, fmt.Errorf("container for service %s was not created", serviceName)
|
||||
}
|
||||
}
|
||||
sort.Strings(containerNames)
|
||||
return containerNames, nil
|
||||
}
|
||||
|
||||
@@ -352,15 +352,33 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
if dir != nil {
|
||||
logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("App")
|
||||
t.Log(logStr)
|
||||
cleanupFailed := false
|
||||
|
||||
out, err := compose.Down(install.GetComposePath())
|
||||
if err != nil && !deleteReq.ForceDelete {
|
||||
return handleErr(install, err, out)
|
||||
if deleteReq.UseLifecycleScripts {
|
||||
if scriptErr := runScript(t, &install, "uninstall"); scriptErr != nil {
|
||||
cleanupFailed = true
|
||||
if !deleteReq.ForceDelete {
|
||||
return scriptErr
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out, downErr := compose.Down(install.GetComposePath())
|
||||
if downErr != nil {
|
||||
cleanupFailed = true
|
||||
if !deleteReq.ForceDelete {
|
||||
return handleErr(install, downErr, out)
|
||||
}
|
||||
}
|
||||
if scriptErr := runScript(t, &install, "uninstall"); scriptErr != nil {
|
||||
cleanupFailed = true
|
||||
if !deleteReq.ForceDelete {
|
||||
_, _ = compose.Up(install.GetComposePath())
|
||||
return scriptErr
|
||||
}
|
||||
}
|
||||
}
|
||||
t.LogSuccess(logStr)
|
||||
if err = runScript(t, &install, "uninstall"); err != nil {
|
||||
_, _ = compose.Up(install.GetComposePath())
|
||||
return err
|
||||
if !cleanupFailed {
|
||||
t.LogSuccess(logStr)
|
||||
}
|
||||
if deleteReq.DeleteImage {
|
||||
content, err := op.GetContent(install.GetEnvPath())
|
||||
@@ -460,8 +478,9 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
}
|
||||
uninstallTask.AddSubTask(task.GetTaskName(install.Name, task.TaskUninstall, task.TaskScopeApp), uninstall, nil)
|
||||
go func() {
|
||||
if err := uninstallTask.Execute(); err != nil && !deleteReq.ForceDelete {
|
||||
if err := uninstallTask.Execute(); err != nil {
|
||||
install.Status = constant.StatusError
|
||||
install.Message = err.Error()
|
||||
_ = appInstallRepo.Save(context.Background(), &install)
|
||||
}
|
||||
}()
|
||||
@@ -999,6 +1018,12 @@ func runScript(task *task.Task, appInstall *model.AppInstall, operate string) er
|
||||
scriptPath = path.Join(workDir, "scripts", "upgrade.sh")
|
||||
case "uninstall":
|
||||
scriptPath = path.Join(workDir, "scripts", "uninstall.sh")
|
||||
case "start":
|
||||
scriptPath = path.Join(workDir, "scripts", "start.sh")
|
||||
case "stop":
|
||||
scriptPath = path.Join(workDir, "scripts", "stop.sh")
|
||||
case "restart":
|
||||
scriptPath = path.Join(workDir, "scripts", "restart.sh")
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
if !fileOp.Stat(scriptPath) {
|
||||
@@ -1008,7 +1033,11 @@ func runScript(task *task.Task, appInstall *model.AppInstall, operate string) er
|
||||
logStr := i18n.GetWithName("ExecShell", operate)
|
||||
task.LogStart(logStr)
|
||||
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Minute), cmd.WithWorkDir(workDir))
|
||||
timeout := 10 * time.Minute
|
||||
if operate == "start" || operate == "restart" {
|
||||
timeout = time.Hour
|
||||
}
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(timeout), cmd.WithWorkDir(workDir), cmd.WithTask(*task))
|
||||
if err := cmdMgr.Run("bash", scriptPath); err != nil {
|
||||
task.LogFailedWithErr(logStr, err)
|
||||
return err
|
||||
@@ -1043,12 +1072,15 @@ func checkContainerNameIsExist(containerName, appDir string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func upApp(task *task.Task, appInstall *model.AppInstall, pullImages bool) error {
|
||||
func upApp(task *task.Task, appInstall *model.AppInstall, pullImages, useLifecycleScripts bool) error {
|
||||
upProject := func(appInstall *model.AppInstall) (err error) {
|
||||
var (
|
||||
out string
|
||||
errMsg string
|
||||
)
|
||||
if useLifecycleScripts {
|
||||
return runScript(task, appInstall, "start")
|
||||
}
|
||||
if pullImages && appInstall.App.Type != "php" {
|
||||
envByte, err := files.NewFileOp().GetContent(appInstall.GetEnvPath())
|
||||
if err != nil {
|
||||
@@ -1375,7 +1407,8 @@ func handleErr(install model.AppInstall, err error, out string) error {
|
||||
|
||||
func doNotNeedSync(installed model.AppInstall) bool {
|
||||
return installed.Status == constant.StatusInstalling || installed.Status == constant.StatusRebuilding || installed.Status == constant.StatusUpgrading ||
|
||||
installed.Status == constant.StatusSyncing || installed.Status == constant.StatusUninstalling || installed.Status == constant.StatusInstallErr
|
||||
installed.Status == constant.StatusSyncing || installed.Status == constant.StatusUninstalling || installed.Status == constant.StatusInstallErr ||
|
||||
installed.Status == constant.StatusStarting || installed.Status == constant.StatusRestarting || installed.Status == constant.StatusWaiting
|
||||
}
|
||||
|
||||
func synAppInstall(containers map[string]container.Summary, appInstall *model.AppInstall, force bool) {
|
||||
@@ -2229,6 +2262,9 @@ func getAppVersions(key string, details []model.AppDetail) []string {
|
||||
hasLatest := false
|
||||
latestVersion := ""
|
||||
for _, detail := range details {
|
||||
if !canAccessVllmVersion(key, detail.Version) {
|
||||
continue
|
||||
}
|
||||
if key != "mssql" && strings.Contains(detail.Version, "latest") {
|
||||
hasLatest = true
|
||||
latestVersion = detail.Version
|
||||
|
||||
@@ -582,6 +582,10 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
|
||||
if config.Image == "" {
|
||||
return fmt.Errorf("container image not found in backup file")
|
||||
}
|
||||
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(recoverCtx.inspectInfo.NetworkSettings, hostConfig)
|
||||
if err := normalizeContainerEndpointSettings(ctx, recoverCtx.client, networkConf, extraNetworks); err != nil {
|
||||
return err
|
||||
}
|
||||
if !checkImageExist(recoverCtx.client, config.Image) {
|
||||
if err := pullImages(taskItem, recoverCtx.client, config.Image); err != nil {
|
||||
return err
|
||||
@@ -596,7 +600,7 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
|
||||
return err
|
||||
}
|
||||
|
||||
createRes, err := createContainerWithOldNetworks(ctx, recoverCtx.client, config, hostConfig, recoverCtx.inspectInfo.NetworkSettings, recoverCtx.targetName)
|
||||
createRes, err := createContainerWithNetworks(ctx, recoverCtx.client, config, hostConfig, networkConf, extraNetworks, recoverCtx.targetName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -604,7 +608,7 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
|
||||
func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) error {
|
||||
if cli.NewVersionError(ctx, "1.44", "specify mac-address per network") != nil {
|
||||
removeEndpointMacAddresses(primary, extras)
|
||||
}
|
||||
@@ -619,11 +623,14 @@ func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client,
|
||||
}
|
||||
info, err := cli.NetworkInspect(ctx, netName, network.InspectOptions{})
|
||||
if err != nil {
|
||||
continue
|
||||
return fmt.Errorf("inspect network %s failed: %w", netName, err)
|
||||
}
|
||||
if err := validateContainerEndpointStaticIP(netName, info, endpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
removeUnsupportedEndpointStaticIP(netName, info, endpoint)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
|
||||
@@ -641,24 +648,28 @@ func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[st
|
||||
}
|
||||
}
|
||||
|
||||
func removeUnsupportedEndpointStaticIP(netName string, info network.Inspect, endpoint *network.EndpointSettings) {
|
||||
func validateContainerEndpointStaticIP(netName string, info network.Inspect, endpoint *network.EndpointSettings) error {
|
||||
if endpoint == nil || endpoint.IPAMConfig == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if isDefaultBridgeNetwork(netName, info) {
|
||||
endpoint.IPAMConfig = nil
|
||||
return
|
||||
ipam := endpoint.IPAMConfig
|
||||
if err := ipam.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid IP configuration for network %s: %w", netName, err)
|
||||
}
|
||||
if ipam.IPv4Address == "" && ipam.IPv6Address == "" {
|
||||
return nil
|
||||
}
|
||||
if netName == "host" || netName == "none" || isDefaultBridgeNetwork(netName, info) {
|
||||
return fmt.Errorf("network %s does not support static IP configuration", netName)
|
||||
}
|
||||
|
||||
if endpoint.IPAMConfig.IPv4Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv4Address, false) {
|
||||
endpoint.IPAMConfig.IPv4Address = ""
|
||||
if ipam.IPv4Address != "" && !networkSupportsStaticIP(info, ipam.IPv4Address, false) {
|
||||
return fmt.Errorf("static IPv4 address %s is not in a configured subnet of network %s", ipam.IPv4Address, netName)
|
||||
}
|
||||
if endpoint.IPAMConfig.IPv6Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv6Address, true) {
|
||||
endpoint.IPAMConfig.IPv6Address = ""
|
||||
}
|
||||
if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" && len(endpoint.IPAMConfig.LinkLocalIPs) == 0 {
|
||||
endpoint.IPAMConfig = nil
|
||||
if ipam.IPv6Address != "" && !networkSupportsStaticIP(info, ipam.IPv6Address, true) {
|
||||
return fmt.Errorf("static IPv6 address %s is not in a configured subnet of network %s", ipam.IPv6Address, netName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDefaultBridgeNetwork(netName string, info network.Inspect) bool {
|
||||
@@ -673,6 +684,7 @@ func networkSupportsStaticIP(info network.Inspect, ip string, isIPv6 bool) bool
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
addr = addr.Unmap()
|
||||
if addr.Is6() != isIPv6 {
|
||||
return false
|
||||
}
|
||||
@@ -813,11 +825,6 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
|
||||
IPv6Address: endpoint.IPAMConfig.IPv6Address,
|
||||
LinkLocalIPs: append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...),
|
||||
}
|
||||
} else if name != "bridge" && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "") {
|
||||
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
|
||||
IPv4Address: endpoint.IPAddress,
|
||||
IPv6Address: endpoint.GlobalIPv6Address,
|
||||
}
|
||||
}
|
||||
if name == primaryName {
|
||||
config.EndpointsConfig[name] = endpointSetting
|
||||
@@ -831,39 +838,6 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
|
||||
return config, extraNetworks
|
||||
}
|
||||
|
||||
const unsupportedUserSpecifiedIPAddress = "user specified IP address is supported only when connecting to networks with user configured subnets"
|
||||
|
||||
func clearUnsupportedDynamicEndpointIPAM(err error, endpoints map[string]*network.EndpointSettings, networkSettings *container.NetworkSettings) bool {
|
||||
if err == nil || !strings.Contains(err.Error(), unsupportedUserSpecifiedIPAddress) {
|
||||
return false
|
||||
}
|
||||
for name, endpoint := range endpoints {
|
||||
if !isDynamicContainerNetwork(networkSettings, name) || endpoint == nil || endpoint.IPAMConfig == nil {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(err.Error(), "network "+name+":") {
|
||||
endpoint.IPAMConfig = nil
|
||||
return true
|
||||
}
|
||||
}
|
||||
cleared := false
|
||||
for name, endpoint := range endpoints {
|
||||
if isDynamicContainerNetwork(networkSettings, name) && endpoint != nil && endpoint.IPAMConfig != nil {
|
||||
endpoint.IPAMConfig = nil
|
||||
cleared = true
|
||||
}
|
||||
}
|
||||
return cleared
|
||||
}
|
||||
|
||||
func isDynamicContainerNetwork(networkSettings *container.NetworkSettings, name string) bool {
|
||||
if networkSettings == nil || name == "bridge" {
|
||||
return false
|
||||
}
|
||||
endpoint := networkSettings.Networks[name]
|
||||
return endpoint != nil && endpoint.IPAMConfig == nil && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "")
|
||||
}
|
||||
|
||||
func cloneContainerConfig(config *container.Config) *container.Config {
|
||||
if config == nil {
|
||||
return &container.Config{}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
@@ -79,7 +81,7 @@ type IContainerService interface {
|
||||
ContainerUpgrade(req dto.ContainerUpgrade) error
|
||||
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
|
||||
ContainerListStats() ([]dto.ContainerListStats, error)
|
||||
ContainerItemStats(req dto.OperationWithName) (dto.ContainerItemStats, error)
|
||||
ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error)
|
||||
LoadResourceLimit() (*dto.ResourceLimit, error)
|
||||
ContainerRename(req dto.ContainerRename) error
|
||||
ContainerCommit(req dto.ContainerCommit) error
|
||||
@@ -246,15 +248,15 @@ func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
func (u *ContainerService) ContainerItemStats(req dto.OperationWithName) (dto.ContainerItemStats, error) {
|
||||
func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error) {
|
||||
var data dto.ContainerItemStats
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
defer client.Close()
|
||||
if req.Name != "system" {
|
||||
defer client.Close()
|
||||
containerInfo, _, err := client.ContainerInspectWithRaw(context.Background(), req.Name, true)
|
||||
containerInfo, _, err := client.ContainerInspectWithRaw(ctx, req.Name, true)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
@@ -263,7 +265,7 @@ func (u *ContainerService) ContainerItemStats(req dto.OperationWithName) (dto.Co
|
||||
return data, nil
|
||||
}
|
||||
|
||||
usage, err := client.DiskUsage(context.Background(), types.DiskUsageOptions{})
|
||||
usage, err := client.DiskUsage(ctx, types.DiskUsageOptions{})
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
@@ -534,7 +536,9 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
normalizeContainerEndpointSettings(ctx, client, networkConf, nil)
|
||||
if err := normalizeContainerEndpointSettings(ctx, client, networkConf, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
|
||||
if err != nil {
|
||||
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
|
||||
@@ -644,14 +648,9 @@ func loadContainerNetworkInfo(name string, endpoint *network.EndpointSettings) d
|
||||
if endpoint.IPAMConfig != nil {
|
||||
item.LinkLocalIPs = append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...)
|
||||
}
|
||||
if name != "bridge" {
|
||||
if endpoint.IPAMConfig != nil {
|
||||
item.Ipv4 = endpoint.IPAMConfig.IPv4Address
|
||||
item.Ipv6 = endpoint.IPAMConfig.IPv6Address
|
||||
} else {
|
||||
item.Ipv4 = endpoint.IPAddress
|
||||
item.Ipv6 = endpoint.GlobalIPv6Address
|
||||
}
|
||||
if name != "bridge" && endpoint.IPAMConfig != nil {
|
||||
item.Ipv4 = endpoint.IPAMConfig.IPv4Address
|
||||
item.Ipv6 = endpoint.IPAMConfig.IPv6Address
|
||||
}
|
||||
return item
|
||||
}
|
||||
@@ -1678,30 +1677,42 @@ func checkImageLike(client *client.Client, imageName string) bool {
|
||||
|
||||
func pullImages(task *task.Task, client *client.Client, imageName string) error {
|
||||
dockerCli := docker.NewClientWithExist(client)
|
||||
repos, err := imageRepoRepo.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imageRepo := selectImageRepo(imageName, repos)
|
||||
if imageRepo == nil || !imageRepo.Auth {
|
||||
return dockerCli.PullImageWithProcess(task, imageName)
|
||||
}
|
||||
|
||||
options := image.PullOptions{}
|
||||
repos, _ := imageRepoRepo.List()
|
||||
if len(repos) != 0 {
|
||||
for _, repo := range repos {
|
||||
if strings.HasPrefix(imageName, repo.DownloadUrl) && repo.Auth {
|
||||
authConfig := registry.AuthConfig{
|
||||
Username: repo.Username,
|
||||
Password: repo.Password,
|
||||
}
|
||||
encodedJSON, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
|
||||
options.RegistryAuth = authStr
|
||||
}
|
||||
authConfig := registry.AuthConfig{
|
||||
Username: imageRepo.Username,
|
||||
Password: imageRepo.Password,
|
||||
}
|
||||
encodedJSON, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON)
|
||||
return dockerCli.PullImageWithProcessAndOptions(task, imageName, options)
|
||||
}
|
||||
|
||||
func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo {
|
||||
var selected *model.ImageRepo
|
||||
selectedURLLength := 0
|
||||
for i := range repos {
|
||||
downloadURL := strings.TrimRight(strings.TrimSpace(repos[i].DownloadUrl), "/")
|
||||
if downloadURL == "" || !strings.HasPrefix(imageName, downloadURL+"/") {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
hasAuth, authStr := loadAuthInfo(imageName)
|
||||
if hasAuth {
|
||||
options.RegistryAuth = authStr
|
||||
if len(downloadURL) > selectedURLLength {
|
||||
selected = &repos[i]
|
||||
selectedURLLength = len(downloadURL)
|
||||
}
|
||||
}
|
||||
return dockerCli.PullImageWithProcessAndOptions(task, imageName, options)
|
||||
return selected
|
||||
}
|
||||
|
||||
func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerListStats {
|
||||
@@ -1758,7 +1769,10 @@ func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error
|
||||
}
|
||||
for i := 0; i <= hostEnd-hostStart; i++ {
|
||||
bindItem := nat.PortBinding{HostPort: strconv.Itoa(hostStart + i), HostIP: port.HostIP}
|
||||
portMap[nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))] = []nat.PortBinding{bindItem}
|
||||
portKey := nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))
|
||||
if !slices.Contains(portMap[portKey], bindItem) {
|
||||
portMap[portKey] = append(portMap[portKey], bindItem)
|
||||
}
|
||||
}
|
||||
for i := hostStart; i <= hostEnd; i++ {
|
||||
if checkInUse && common.ScanPortWithIP(port.HostIP, i) {
|
||||
@@ -1776,7 +1790,10 @@ func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error
|
||||
return portMap, buserr.WithDetail("ErrPortInUsed", portItem, nil)
|
||||
}
|
||||
bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP}
|
||||
portMap[nat.Port(fmt.Sprintf("%s/%s", port.ContainerPort, port.Protocol))] = []nat.PortBinding{bindItem}
|
||||
portKey := nat.Port(fmt.Sprintf("%s/%s", port.ContainerPort, port.Protocol))
|
||||
if !slices.Contains(portMap[portKey], bindItem) {
|
||||
portMap[portKey] = append(portMap[portKey], bindItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
return portMap, nil
|
||||
@@ -1955,7 +1972,7 @@ func loadComposeCount(client *client.Client) int {
|
||||
}
|
||||
func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper {
|
||||
var exposedPorts []dto.PortHelper
|
||||
samePortMap := make(map[string]dto.PortHelper)
|
||||
seenPorts := make(map[dto.PortHelper]struct{})
|
||||
ports := transPortToStr(itemPorts)
|
||||
for _, item := range ports {
|
||||
itemStr := strings.Split(item, "->")
|
||||
@@ -1976,16 +1993,11 @@ func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper {
|
||||
}
|
||||
itemPort.ContainerPort = itemContainer[0]
|
||||
itemPort.Protocol = itemContainer[1]
|
||||
keyItem := fmt.Sprintf("%s->%s/%s", itemPort.HostPort, itemPort.ContainerPort, itemPort.Protocol)
|
||||
if val, ok := samePortMap[keyItem]; ok {
|
||||
val.HostIP = ""
|
||||
samePortMap[keyItem] = val
|
||||
} else {
|
||||
samePortMap[keyItem] = itemPort
|
||||
if _, exists := seenPorts[itemPort]; exists {
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, val := range samePortMap {
|
||||
exposedPorts = append(exposedPorts, val)
|
||||
seenPorts[itemPort] = struct{}{}
|
||||
exposedPorts = append(exposedPorts, itemPort)
|
||||
}
|
||||
return exposedPorts
|
||||
}
|
||||
|
||||
@@ -67,12 +67,12 @@ func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
normalizeContainerEndpointSettings(ctx, client, networkConf, nil)
|
||||
if err := normalizeContainerEndpointSettings(ctx, client, networkConf, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cleanupErr, err := switchContainer(ctx, client, req.Name, oldContainer, func() (container.CreateResponse, error) {
|
||||
return createContainerWithDynamicIPFallback(func() (container.CreateResponse, error) {
|
||||
return client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
|
||||
}, networkConf.EndpointsConfig, oldContainer.NetworkSettings)
|
||||
return client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
|
||||
}, config.Tty, t)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update container failed, err: %v", err)
|
||||
@@ -138,8 +138,14 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
|
||||
config.Image = req.Image
|
||||
hostConf := cloneContainerHostConfig(oldContainer.HostConfig)
|
||||
preserveContainerVolumeMounts(hostConf, oldContainer.Mounts)
|
||||
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(oldContainer.NetworkSettings, hostConf)
|
||||
if err := normalizeContainerEndpointSettings(ctx, client, networkConf, extraNetworks); err != nil {
|
||||
upgradeErr := fmt.Errorf("prepare networks for container %s failed: %w", item, err)
|
||||
upgradeErrors = append(upgradeErrors, upgradeErr)
|
||||
return upgradeErr
|
||||
}
|
||||
cleanupErr, err := switchContainer(ctx, client, item, oldContainer, func() (container.CreateResponse, error) {
|
||||
return createContainerWithOldNetworks(ctx, client, config, hostConf, oldContainer.NetworkSettings, item)
|
||||
return createContainerWithNetworks(ctx, client, config, hostConf, networkConf, extraNetworks, item)
|
||||
}, config.Tty, t)
|
||||
if err != nil {
|
||||
upgradeErr := fmt.Errorf("upgrade container %s failed: %w", item, err)
|
||||
@@ -242,9 +248,8 @@ func (l *containerOperationMutex) lock(names ...string) func() {
|
||||
}
|
||||
|
||||
type containerNetworkAttachment struct {
|
||||
name string
|
||||
endpoint *network.EndpointSettings
|
||||
isDynamic bool
|
||||
name string
|
||||
endpoint *network.EndpointSettings
|
||||
}
|
||||
|
||||
type containerSwitchLogger interface {
|
||||
@@ -582,13 +587,13 @@ func disconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitc
|
||||
endpoints := make(map[string]*network.EndpointSettings, len(extras)+1)
|
||||
if primary != nil {
|
||||
for name, endpoint := range primary.EndpointsConfig {
|
||||
if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil {
|
||||
if name != "bridge" && endpoint != nil {
|
||||
endpoints[name] = endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
for name, endpoint := range extras {
|
||||
if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil {
|
||||
if name != "bridge" && endpoint != nil {
|
||||
endpoints[name] = endpoint
|
||||
}
|
||||
}
|
||||
@@ -604,9 +609,8 @@ func disconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitc
|
||||
return disconnected, fmt.Errorf("disconnect original container from network %s failed: %w", name, err)
|
||||
}
|
||||
disconnected = append(disconnected, containerNetworkAttachment{
|
||||
name: name,
|
||||
endpoint: endpoints[name],
|
||||
isDynamic: isDynamicContainerNetwork(oldContainer.NetworkSettings, name),
|
||||
name: name,
|
||||
endpoint: endpoints[name],
|
||||
})
|
||||
}
|
||||
return disconnected, nil
|
||||
@@ -616,10 +620,6 @@ func reconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitch
|
||||
var reconnectErr error
|
||||
for _, attachment := range attachments {
|
||||
err := cli.NetworkConnect(ctx, attachment.name, containerID, attachment.endpoint)
|
||||
if err != nil && attachment.isDynamic && strings.Contains(err.Error(), unsupportedUserSpecifiedIPAddress) {
|
||||
attachment.endpoint.IPAMConfig = nil
|
||||
err = cli.NetworkConnect(ctx, attachment.name, containerID, attachment.endpoint)
|
||||
}
|
||||
if err != nil {
|
||||
reconnectErr = errors.Join(reconnectErr, fmt.Errorf("reconnect original container to network %s failed: %w", attachment.name, err))
|
||||
}
|
||||
@@ -652,7 +652,7 @@ func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, ol
|
||||
reconnectErr := reconnectOriginalContainerNetworks(ctx, cli, oldContainerID, disconnectedNetworks)
|
||||
logContainerSwitchStep(logger, "ContainerRollbackReconnectOld", currentName, reconnectErr)
|
||||
rollbackErr = errors.Join(rollbackErr, reconnectErr)
|
||||
if wasRunning {
|
||||
if wasRunning && reconnectErr == nil {
|
||||
restartErr := restartOriginalContainer(ctx, cli, oldContainerID)
|
||||
logContainerSwitchStep(logger, "ContainerRollbackRestartOld", currentName, restartErr)
|
||||
rollbackErr = errors.Join(rollbackErr, restartErr)
|
||||
@@ -660,17 +660,8 @@ func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, ol
|
||||
return rollbackErr
|
||||
}
|
||||
|
||||
func createContainerWithOldNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkSettings *container.NetworkSettings, name string) (container.CreateResponse, error) {
|
||||
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(networkSettings, hostConf)
|
||||
normalizeContainerEndpointSettings(ctx, client, networkConf, extraNetworks)
|
||||
var primaryEndpoints map[string]*network.EndpointSettings
|
||||
if networkConf != nil {
|
||||
primaryEndpoints = networkConf.EndpointsConfig
|
||||
}
|
||||
|
||||
created, err := createContainerWithDynamicIPFallback(func() (container.CreateResponse, error) {
|
||||
return client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name)
|
||||
}, primaryEndpoints, networkSettings)
|
||||
func createContainerWithNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkConf *network.NetworkingConfig, extraNetworks map[string]*network.EndpointSettings, name string) (container.CreateResponse, error) {
|
||||
created, err := client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name)
|
||||
if err != nil {
|
||||
return created, err
|
||||
}
|
||||
@@ -682,9 +673,6 @@ func createContainerWithOldNetworks(ctx context.Context, client *client.Client,
|
||||
sort.Strings(extraNames)
|
||||
for _, item := range extraNames {
|
||||
err := client.NetworkConnect(ctx, item, created.ID, extraNetworks[item])
|
||||
if clearUnsupportedDynamicEndpointIPAM(err, map[string]*network.EndpointSettings{item: extraNetworks[item]}, networkSettings) {
|
||||
err = client.NetworkConnect(ctx, item, created.ID, extraNetworks[item])
|
||||
}
|
||||
if err != nil {
|
||||
_ = client.ContainerRemove(ctx, created.ID, container.RemoveOptions{Force: true})
|
||||
return created, err
|
||||
@@ -692,16 +680,3 @@ func createContainerWithOldNetworks(ctx context.Context, client *client.Client,
|
||||
}
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func createContainerWithDynamicIPFallback(
|
||||
create func() (container.CreateResponse, error),
|
||||
endpoints map[string]*network.EndpointSettings,
|
||||
networkSettings *container.NetworkSettings,
|
||||
) (container.CreateResponse, error) {
|
||||
for {
|
||||
created, err := create()
|
||||
if err == nil || created.ID != "" || !clearUnsupportedDynamicEndpointIPAM(err, endpoints, networkSettings) {
|
||||
return created, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/pkg/errors"
|
||||
@@ -75,6 +76,7 @@ func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interfac
|
||||
EntryID: cronjob.ID,
|
||||
}
|
||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable))
|
||||
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
|
||||
if alertInfo.SendCount != 0 {
|
||||
item.AlertCount = alertInfo.SendCount
|
||||
} else {
|
||||
@@ -98,9 +100,11 @@ func (u *CronjobService) LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, err
|
||||
AlertType: cronjob.Type,
|
||||
EntryID: cronjob.ID,
|
||||
}
|
||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable))
|
||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))))
|
||||
item.AlertMethod = alertInfo.Method
|
||||
if alertInfo.SendCount != 0 {
|
||||
item.AlertTitle = alertInfo.Title
|
||||
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
|
||||
if alertInfo.Status == constant.AlertEnable {
|
||||
item.AlertCount = alertInfo.SendCount
|
||||
} else {
|
||||
item.AlertCount = 0
|
||||
@@ -195,11 +199,12 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
|
||||
}
|
||||
}
|
||||
item.SourceAccounts, item.DownloadAccount, _ = loadBackupNamesByID(cronjob.SourceAccountIDs, cronjob.DownloadAccountID)
|
||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))), repo.WithByStatus(constant.AlertEnable))
|
||||
if alertInfo.SendCount != 0 {
|
||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))))
|
||||
item.AlertTitle = alertInfo.Title
|
||||
item.AlertMethod = alertInfo.Method
|
||||
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
|
||||
if alertInfo.Status == constant.AlertEnable {
|
||||
item.AlertCount = alertInfo.SendCount
|
||||
item.AlertTitle = alertInfo.Title
|
||||
item.AlertMethod = alertInfo.Method
|
||||
} else {
|
||||
item.AlertCount = 0
|
||||
}
|
||||
@@ -213,6 +218,17 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
|
||||
}
|
||||
|
||||
func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
|
||||
for _, item := range req {
|
||||
advanced, err := cronJobAlertAdvancedParams(item.AlertTriggerMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if item.AlertCount != 0 {
|
||||
if err := (AlertService{}).validateCronJobAlertChannels(item.Type, advanced, item.AlertMethod); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, item := range req {
|
||||
cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name))
|
||||
if cronjobItem.ID != 0 {
|
||||
@@ -395,17 +411,27 @@ func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
|
||||
} else {
|
||||
cronjob.Status = constant.StatusDisable
|
||||
}
|
||||
_ = cronjobRepo.Create(&cronjob)
|
||||
if item.AlertCount != 0 && item.AlertTitle != "" && item.AlertMethod != "" {
|
||||
if err := cronjobRepo.Create(&cronjob); err != nil {
|
||||
return err
|
||||
}
|
||||
if item.AlertTitle != "" && item.AlertMethod != "" {
|
||||
advanced, _ := cronJobAlertAdvancedParams(item.AlertTriggerMode)
|
||||
status := constant.AlertEnable
|
||||
if item.AlertCount == 0 {
|
||||
status = constant.AlertDisable
|
||||
}
|
||||
createAlert := dto.AlertCreate{
|
||||
Title: item.AlertTitle,
|
||||
SendCount: item.AlertCount,
|
||||
Method: item.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronjob.ID)),
|
||||
Status: constant.AlertEnable,
|
||||
Title: item.AlertTitle,
|
||||
SendCount: item.AlertCount,
|
||||
Method: item.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronjob.ID)),
|
||||
Status: status,
|
||||
AdvancedParams: advanced,
|
||||
}
|
||||
if err := NewIAlertService().CreateAlert(createAlert, operator); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = NewIAlertService().CreateAlert(createAlert, operator)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -562,6 +588,15 @@ func (u *CronjobService) HandleOnce(id uint) error {
|
||||
}
|
||||
|
||||
func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
|
||||
advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.AlertCount != 0 {
|
||||
if err := (AlertService{}).validateCronJobAlertChannels(req.Type, advanced, req.AlertMethod); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
cronjob, _ := cronjobRepo.Get(repo.WithByName(req.Name))
|
||||
if cronjob.ID != 0 {
|
||||
return buserr.New("ErrRecordExist")
|
||||
@@ -603,12 +638,13 @@ func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
|
||||
}
|
||||
if req.AlertCount != 0 && req.AlertTitle != "" && req.AlertMethod != "" {
|
||||
createAlert := dto.AlertCreate{
|
||||
Title: req.AlertTitle,
|
||||
SendCount: req.AlertCount,
|
||||
Method: req.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronjob.ID)),
|
||||
Status: constant.AlertEnable,
|
||||
Title: req.AlertTitle,
|
||||
SendCount: req.AlertCount,
|
||||
Method: req.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronjob.ID)),
|
||||
Status: constant.AlertEnable,
|
||||
AdvancedParams: advanced,
|
||||
}
|
||||
err := NewIAlertService().CreateAlert(createAlert, operator)
|
||||
if err != nil {
|
||||
@@ -682,6 +718,10 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
|
||||
}
|
||||
|
||||
func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string) error {
|
||||
advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var cronjob model.Cronjob
|
||||
if err := copier.Copy(&cronjob, &req); err != nil {
|
||||
return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||
@@ -697,6 +737,20 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
|
||||
if err != nil {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
if req.AlertCount != 0 {
|
||||
previous, _ := alertRepo.Get(alertRepo.WithByType(cronModel.Type), alertRepo.WithByProject(strconv.Itoa(int(id))))
|
||||
merged, err := prepareCronJobAlertParams(cronModel.Type, previous.AdvancedParams, advanced)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
method := req.AlertMethod
|
||||
if method == "" {
|
||||
method = previous.Method
|
||||
}
|
||||
if err := (AlertService{}).validateCronJobAlertChannels(cronModel.Type, merged, method); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
upMap := make(map[string]interface{})
|
||||
cronjob.EntryIDs = cronModel.EntryIDs
|
||||
cronjob.Type = cronModel.Type
|
||||
@@ -753,11 +807,12 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
|
||||
return err
|
||||
}
|
||||
updateAlert := dto.AlertCreate{
|
||||
Title: req.AlertTitle,
|
||||
SendCount: req.AlertCount,
|
||||
Method: req.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronModel.ID)),
|
||||
Title: req.AlertTitle,
|
||||
SendCount: req.AlertCount,
|
||||
Method: req.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronModel.ID)),
|
||||
AdvancedParams: advanced,
|
||||
}
|
||||
err = NewIAlertService().ExternalUpdateAlert(updateAlert, operator)
|
||||
if err != nil {
|
||||
@@ -766,6 +821,17 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
|
||||
return nil
|
||||
}
|
||||
|
||||
func cronJobAlertAdvancedParams(mode string) (string, error) {
|
||||
if mode == "" {
|
||||
return "", nil
|
||||
}
|
||||
data, err := json.Marshal(map[string]string{"alertTriggerMode": mode})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return alertUtil.MergeCronJobAlertParams("", string(data))
|
||||
}
|
||||
|
||||
func (u *CronjobService) UpdateStatus(id uint, status string) error {
|
||||
cronjob, _ := cronjobRepo.Get(repo.WithByID(id))
|
||||
if cronjob.ID == 0 {
|
||||
|
||||
@@ -412,6 +412,7 @@ func addSkipTask(source string, taskItem *task.Task) {
|
||||
taskItem.Log(i18n.GetMsgByKey("NoSuchResource"))
|
||||
return nil
|
||||
}, nil)
|
||||
taskItem.SubTasks[len(taskItem.SubTasks)-1].StepAlias = cronJobSkippedStep
|
||||
}
|
||||
|
||||
func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ntp"
|
||||
@@ -56,10 +58,11 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
|
||||
_ = taskRepo.Save(context.Background(), taskItem.Task)
|
||||
}
|
||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||
handleCronJobAlert(cronjob)
|
||||
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
|
||||
return
|
||||
}
|
||||
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
|
||||
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil))
|
||||
}()
|
||||
return
|
||||
}
|
||||
@@ -70,19 +73,20 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
|
||||
record.TaskID = ""
|
||||
}
|
||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||
handleCronJobAlert(cronjob)
|
||||
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
taskItem, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
|
||||
if len(taskItem.ID) == 0 {
|
||||
storedTask, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
|
||||
if len(storedTask.ID) == 0 {
|
||||
record.TaskID = ""
|
||||
}
|
||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||
handleCronJobAlert(cronjob)
|
||||
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
|
||||
} else {
|
||||
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
|
||||
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil))
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -482,8 +486,33 @@ func hasBackup(cronjobType string) bool {
|
||||
return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log" || cronjobType == "cutWebsiteLog"
|
||||
}
|
||||
|
||||
func handleCronJobAlert(cronjob *model.Cronjob) {
|
||||
const cronJobSkippedStep = "cronjob-skipped"
|
||||
|
||||
func cronJobAlertResult(taskItem *task.Task, err error) string {
|
||||
if errors.Is(err, context.Canceled) || taskItem.Task.Status == constant.StatusCanceled ||
|
||||
(taskItem.TaskCtx != nil && taskItem.TaskCtx.Err() != nil) {
|
||||
return ""
|
||||
}
|
||||
if err != nil {
|
||||
return alertUtil.CronJobAlertFailed
|
||||
}
|
||||
if taskItem.Task.Status != constant.StatusSuccess {
|
||||
return ""
|
||||
}
|
||||
for _, subTask := range taskItem.SubTasks {
|
||||
if subTask.StepAlias != cronJobSkippedStep {
|
||||
return alertUtil.CronJobAlertSuccess
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func handleCronJobAlert(cronjob *model.Cronjob, result string) {
|
||||
if result == "" {
|
||||
return
|
||||
}
|
||||
pushAlert := dto.PushAlert{
|
||||
Result: result,
|
||||
TaskName: cronjob.Name,
|
||||
AlertType: cronjob.Type,
|
||||
EntryID: cronjob.ID,
|
||||
|
||||
@@ -242,7 +242,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
|
||||
currentInfo.SwapMemoryUsed = swapInfo.Used
|
||||
currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent
|
||||
|
||||
currentInfo.DiskData = loadDiskInfo()
|
||||
currentInfo.DiskData = loadDiskInfo(false)
|
||||
currentInfo.GPUData, currentInfo.NPUData, currentInfo.XPUData = loadAcceleratorInfo()
|
||||
|
||||
if ioOption == "all" {
|
||||
@@ -455,24 +455,9 @@ type diskInfo struct {
|
||||
Device string
|
||||
}
|
||||
|
||||
func loadDiskInfo() []dto.DiskInfo {
|
||||
func loadDiskInfo(forceRefresh bool) []dto.DiskInfo {
|
||||
var datas []dto.DiskInfo
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||
format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
|
||||
stdout, err := cmdMgr.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||
stdout, err = cmdMgr2.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
return datas
|
||||
}
|
||||
}
|
||||
stdout := loadDiskMounts()
|
||||
lines := strings.Split(stdout, "\n")
|
||||
|
||||
var mounts []diskInfo
|
||||
@@ -520,43 +505,22 @@ func loadDiskInfo() []dto.DiskInfo {
|
||||
itemData.Type = mount.Type
|
||||
itemData.Device = mount.Device
|
||||
|
||||
type diskResult struct {
|
||||
state *disk.UsageStat
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan diskResult, 1)
|
||||
|
||||
go func() {
|
||||
state, err := psutil.DISK.GetUsage(mount.Mount, false)
|
||||
resultCh <- diskResult{state: state, err: err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
|
||||
case result := <-resultCh:
|
||||
if result.err != nil {
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, result.err)
|
||||
return
|
||||
}
|
||||
itemData.Total = result.state.Total
|
||||
itemData.Free = result.state.Free
|
||||
itemData.Used = result.state.Used
|
||||
itemData.UsedPercent = result.state.UsedPercent
|
||||
itemData.InodesTotal = result.state.InodesTotal
|
||||
itemData.InodesUsed = result.state.InodesUsed
|
||||
itemData.InodesFree = result.state.InodesFree
|
||||
itemData.InodesUsedPercent = result.state.InodesUsedPercent
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
state, err := loadDiskUsageWithTimeout(mount.Mount, forceRefresh)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
|
||||
} else {
|
||||
itemData.Total = state.Total
|
||||
itemData.Free = state.Free
|
||||
itemData.Used = state.Used
|
||||
itemData.UsedPercent = state.UsedPercent
|
||||
itemData.InodesTotal = state.InodesTotal
|
||||
itemData.InodesUsed = state.InodesUsed
|
||||
itemData.InodesFree = state.InodesFree
|
||||
itemData.InodesUsedPercent = state.InodesUsedPercent
|
||||
}
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
}(mounts[i])
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -567,6 +531,69 @@ func loadDiskInfo() []dto.DiskInfo {
|
||||
return datas
|
||||
}
|
||||
|
||||
var diskMountsMu sync.Mutex
|
||||
|
||||
func loadDiskMounts() string {
|
||||
if !diskMountsMu.TryLock() {
|
||||
return ""
|
||||
}
|
||||
resultCh := make(chan string, 1)
|
||||
go func() {
|
||||
var stdout string
|
||||
defer func() {
|
||||
diskMountsMu.Unlock()
|
||||
resultCh <- stdout
|
||||
}()
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||
format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
|
||||
output, err := cmdMgr.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info with df -hT -P failed, err: %v", err)
|
||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||
output, err = cmdMgr2.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info with df -lhT -P failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stdout = output
|
||||
}()
|
||||
timer := time.NewTimer(3 * time.Second)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case stdout := <-resultCh:
|
||||
return stdout
|
||||
case <-timer.C:
|
||||
global.LOG.Error("load disk mounts timed out; df collection is still running")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func loadDiskUsageWithTimeout(path string, forceRefresh bool) (*disk.UsageStat, error) {
|
||||
type diskResult struct {
|
||||
state *disk.UsageStat
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan diskResult, 1)
|
||||
go func() {
|
||||
state, err := psutil.DISK.GetUsage(path, forceRefresh)
|
||||
resultCh <- diskResult{state: state, err: err}
|
||||
}()
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
return nil, fmt.Errorf("load disk usage from %s: timeout", path)
|
||||
case result := <-resultCh:
|
||||
return result.state, result.err
|
||||
}
|
||||
}
|
||||
|
||||
func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
||||
ok, client := accelerator.New()
|
||||
if !ok {
|
||||
@@ -586,6 +613,9 @@ func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
||||
xpuData []dto.XPUInfo
|
||||
)
|
||||
for _, device := range snapshot.Devices {
|
||||
if device.ParentID != "" {
|
||||
continue
|
||||
}
|
||||
switch device.Kind {
|
||||
case accelerator.KindGPU:
|
||||
if device.GPU == nil {
|
||||
@@ -595,7 +625,11 @@ func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
||||
if err := copier.Copy(&dataItem, device.GPU); err != nil {
|
||||
continue
|
||||
}
|
||||
dataItem.PowerUsage = dataItem.PowerDraw + " / " + dataItem.MaxPowerLimit
|
||||
dataItem.MaxPowerLimit = device.GPU.PowerLimit
|
||||
dataItem.PowerUsage = dataItem.PowerDraw
|
||||
if dataItem.MaxPowerLimit != "" {
|
||||
dataItem.PowerUsage += " / " + dataItem.MaxPowerLimit
|
||||
}
|
||||
dataItem.MemoryUsage = dataItem.MemUsed + " / " + dataItem.MemTotal
|
||||
gpuData = append(gpuData, dataItem)
|
||||
case accelerator.KindNPU:
|
||||
|
||||
@@ -73,7 +73,7 @@ func (u *DeviceService) LoadBaseInfo() (dto.DeviceBaseInfo, error) {
|
||||
if baseInfo.SwapMemoryTotal != 0 {
|
||||
baseInfo.SwapDetails = loadSwap()
|
||||
}
|
||||
disks := loadDiskInfo()
|
||||
disks := loadDiskInfo(false)
|
||||
for _, item := range disks {
|
||||
baseInfo.MaxSize += item.Free
|
||||
}
|
||||
|
||||
+123
-1
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -14,14 +15,20 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
)
|
||||
|
||||
const dockerNftablesMinVersion = "29.0.0"
|
||||
|
||||
type DockerService struct{}
|
||||
|
||||
type IDockerService interface {
|
||||
UpdateConf(req dto.SettingUpdate, withRestart bool) error
|
||||
UpdateFirewallBackend(backend string) error
|
||||
UpdateLogOption(req dto.LogOption) error
|
||||
UpdateIpv6Option(req dto.Ipv6Option) error
|
||||
UpdateConfByFile(info dto.DaemonJsonUpdateByFile) error
|
||||
@@ -30,6 +37,115 @@ type IDockerService interface {
|
||||
OperateDocker(req dto.DockerOperation) error
|
||||
}
|
||||
|
||||
func loadDockerEngineVersion(ctx context.Context) string {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err == nil {
|
||||
defer client.Close()
|
||||
if version, versionErr := client.ServerVersion(ctx); versionErr == nil && version.Version != "" {
|
||||
return version.Version
|
||||
}
|
||||
}
|
||||
if !cmd.Which("dockerd") {
|
||||
return ""
|
||||
}
|
||||
stdout, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("dockerd", "--version")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(stdout)
|
||||
}
|
||||
|
||||
func dockerNftablesSupported(version string) bool {
|
||||
return version != "" && common.CompareAppVersion(version, dockerNftablesMinVersion)
|
||||
}
|
||||
|
||||
func applyDockerFirewallBackendConfig(daemonMap map[string]interface{}, backend, version string) error {
|
||||
switch backend {
|
||||
case constant.FirewallProviderNftables:
|
||||
if !dockerNftablesSupported(version) {
|
||||
return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion)
|
||||
}
|
||||
daemonMap["experimental"] = true
|
||||
daemonMap["firewall-backend"] = constant.FirewallProviderNftables
|
||||
case constant.FirewallProviderIptables:
|
||||
if dockerNftablesSupported(version) {
|
||||
daemonMap["firewall-backend"] = constant.FirewallProviderIptables
|
||||
} else {
|
||||
delete(daemonMap, "firewall-backend")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported Docker firewall backend %q", backend)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *DockerService) UpdateFirewallBackend(backend string) error {
|
||||
version := loadDockerEngineVersion(context.Background())
|
||||
if backend == constant.FirewallProviderNftables && !dockerNftablesSupported(version) {
|
||||
return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion)
|
||||
}
|
||||
if backend == constant.FirewallProviderNftables {
|
||||
if err := dockerfirewall.CheckIPv4Forwarding(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
original, readErr := os.ReadFile(constant.DaemonJsonPath)
|
||||
existed := readErr == nil
|
||||
if readErr != nil && !os.IsNotExist(readErr) {
|
||||
return readErr
|
||||
}
|
||||
daemonMap := make(map[string]interface{})
|
||||
if len(bytes.TrimSpace(original)) > 0 {
|
||||
if err := json.Unmarshal(original, &daemonMap); err != nil {
|
||||
return fmt.Errorf("failed to parse Docker configuration: %w", err)
|
||||
}
|
||||
}
|
||||
if err := applyDockerFirewallBackendConfig(daemonMap, backend, version); err != nil {
|
||||
return err
|
||||
}
|
||||
updated, err := json.MarshalIndent(daemonMap, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existed && bytes.Equal(bytes.TrimSpace(original), bytes.TrimSpace(updated)) {
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(path.Dir(constant.DaemonJsonPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(constant.DaemonJsonPath, updated, 0640); err != nil {
|
||||
return err
|
||||
}
|
||||
restore := func() error {
|
||||
if existed {
|
||||
return os.WriteFile(constant.DaemonJsonPath, original, 0640)
|
||||
}
|
||||
err := os.Remove(constant.DaemonJsonPath)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
if restoreErr := restore(); restoreErr != nil {
|
||||
return fmt.Errorf("%v; failed to restore Docker configuration: %w", err, restoreErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := controller.HandleRestart("docker"); err != nil {
|
||||
cause := fmt.Errorf("failed to restart Docker: %w", err)
|
||||
if restoreErr := restore(); restoreErr != nil {
|
||||
return fmt.Errorf("%v; failed to restore Docker configuration: %w", cause, restoreErr)
|
||||
}
|
||||
if restoreRestartErr := controller.HandleRestart("docker"); restoreRestartErr != nil {
|
||||
return fmt.Errorf("%v; the previous configuration was restored but Docker could not be restarted: %w", cause, restoreRestartErr)
|
||||
}
|
||||
return cause
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewIDockerService() IDockerService {
|
||||
return &DockerService{}
|
||||
}
|
||||
@@ -167,7 +283,10 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate, withRestart bool) erro
|
||||
delete(daemonMap, "ipv6")
|
||||
delete(daemonMap, "fixed-cidr-v6")
|
||||
delete(daemonMap, "ip6tables")
|
||||
delete(daemonMap, "experimental")
|
||||
backend, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||
if !strings.EqualFold(strings.TrimSpace(backend), constant.FirewallProviderNftables) {
|
||||
delete(daemonMap, "experimental")
|
||||
}
|
||||
}
|
||||
case "LogOption":
|
||||
if req.Value == "disable" {
|
||||
@@ -368,6 +487,9 @@ func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
|
||||
if err := controller.Handle(req.Operation, service); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Operation == "start" || req.Operation == "restart" {
|
||||
return RestoreDockerPortGuard(context.Background())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -32,10 +32,11 @@ var (
|
||||
scriptRepo = repo.NewIScriptRepo()
|
||||
cronjobRepo = repo.NewICronjobRepo()
|
||||
|
||||
hostRepo = repo.NewIHostRepo()
|
||||
ftpRepo = repo.NewIFtpRepo()
|
||||
clamRepo = repo.NewIClamRepo()
|
||||
monitorRepo = repo.NewIMonitorRepo()
|
||||
hostRepo = repo.NewIHostRepo()
|
||||
ftpRepo = repo.NewIFtpRepo()
|
||||
clamRepo = repo.NewIClamRepo()
|
||||
monitorRepo = repo.NewIMonitorRepo()
|
||||
vllmMonitorRepo = &repo.VLLMMonitorRepo{}
|
||||
|
||||
settingRepo = repo.NewISettingRepo()
|
||||
backupRepo = repo.NewIBackupRepo()
|
||||
|
||||
+34
-10
@@ -159,6 +159,10 @@ func (f *FileService) SearchUploadWithPage(req request.SearchUploadWithPage) (in
|
||||
})
|
||||
}
|
||||
|
||||
sort.SliceStable(files, func(i, j int) bool {
|
||||
return files[i].CreatedAt > files[j].CreatedAt
|
||||
})
|
||||
|
||||
total, start, end := len(files), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||
if start > total {
|
||||
backData = make([]response.UploadInfo, 0)
|
||||
@@ -435,13 +439,15 @@ func (f *FileService) Compress(c request.FileCompress) error {
|
||||
if err := preflightCompressTool(files.CompressType(c.Type)); err != nil {
|
||||
return err
|
||||
}
|
||||
taskItem, err := task.NewTask(c.Name, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||
taskName := i18n.GetMsgWithMap("FileTaskCompress", map[string]interface{}{"dst": strconv.Quote(filepath.Join(c.Dst, c.Name))})
|
||||
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
taskItem.AddSubTask(c.Name, func(t *task.Task) error {
|
||||
t.LogStart(c.Name)
|
||||
taskItem.AddSubTask(taskName, func(t *task.Task) error {
|
||||
logFileTaskSources(t, c.Files)
|
||||
t.Log(i18n.GetMsgWithMap("FileTaskFormat", map[string]interface{}{"format": strconv.Quote(c.Type)}))
|
||||
compressType := files.CompressType(c.Type)
|
||||
dstFile := filepath.Join(c.Dst, c.Name)
|
||||
success := false
|
||||
@@ -512,13 +518,15 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
|
||||
if err := preflightDecompressTool(files.CompressType(c.Type)); err != nil {
|
||||
return err
|
||||
}
|
||||
taskItem, err := task.NewTask(c.Path, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||
taskName := i18n.GetMsgWithMap("FileTaskDecompress", map[string]interface{}{"dst": strconv.Quote(c.Dst)})
|
||||
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
taskItem.AddSubTask(c.Path, func(t *task.Task) error {
|
||||
t.LogStart(c.Path)
|
||||
taskItem.AddSubTask(taskName, func(t *task.Task) error {
|
||||
logFileTaskSources(t, []string{c.Path})
|
||||
t.Log(i18n.GetMsgWithMap("FileTaskFormat", map[string]interface{}{"format": strconv.Quote(c.Type)}))
|
||||
dstExisted := fo.Stat(c.Dst)
|
||||
parentDir := filepath.Dir(c.Dst)
|
||||
if !fo.Stat(parentDir) {
|
||||
@@ -892,6 +900,7 @@ func (f *FileService) Wget(w request.FileWget) (string, error) {
|
||||
key := "file-wget-" + common.GetUuid()
|
||||
options := files.DownloadOptions{
|
||||
IgnoreCertificate: w.IgnoreCertificate,
|
||||
UseServerFilename: w.UseServerFilename,
|
||||
}
|
||||
if w.UseProxy {
|
||||
systemProxy, err := NewISettingService().GetSystemProxy()
|
||||
@@ -909,6 +918,12 @@ func (f *FileService) Wget(w request.FileWget) (string, error) {
|
||||
return key, fo.DownloadFileWithProcess(w.Url, filepath.Join(w.Path, w.Name), key, options)
|
||||
}
|
||||
|
||||
func logFileTaskSources(t *task.Task, sources []string) {
|
||||
for _, source := range sources {
|
||||
t.Log(i18n.GetMsgWithMap("FileTaskSource", map[string]interface{}{"path": strconv.Quote(source)}))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileService) MvFile(m request.FileMove) error {
|
||||
fo := files.NewFileOp()
|
||||
if err := validateFileMove(fo, m); err != nil {
|
||||
@@ -920,15 +935,24 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
||||
if !fileTransferLocks.Acquire(m.TaskID, getFileTransferPaths(m)) {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
taskItem, err := task.NewTask(m.NewPath, task.TaskExec, task.TaskScopeTask, m.TaskID, 1)
|
||||
nameKey := "FileTaskCopy"
|
||||
if m.Type == "cut" {
|
||||
nameKey = "FileTaskMove"
|
||||
}
|
||||
taskName := i18n.GetMsgWithMap(nameKey, map[string]interface{}{"dst": strconv.Quote(m.NewPath)})
|
||||
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, m.TaskID, 1)
|
||||
if err != nil {
|
||||
fileTransferLocks.Release(m.TaskID)
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
defer fileTransferLocks.Release(m.TaskID)
|
||||
taskItem.AddSubTaskWithOps(m.NewPath, func(t *task.Task) error {
|
||||
t.LogStart(m.NewPath)
|
||||
taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error {
|
||||
logFileTaskSources(t, m.OldPaths)
|
||||
logFileTaskSources(t, m.CoverPaths)
|
||||
if m.Name != "" {
|
||||
t.Log(i18n.GetMsgWithMap("FileTaskRename", map[string]interface{}{"name": strconv.Quote(m.Name)}))
|
||||
}
|
||||
err := f.moveFileWithContext(t.TaskCtx, m)
|
||||
if err != nil && t.TaskCtx.Err() != nil {
|
||||
return t.TaskCtx.Err()
|
||||
@@ -1286,7 +1310,7 @@ func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.Exi
|
||||
}
|
||||
|
||||
func (f *FileService) GetHostMount() []dto.DiskInfo {
|
||||
return loadDiskInfo()
|
||||
return loadDiskInfo(false)
|
||||
}
|
||||
|
||||
func (f *FileService) GetUsersAndGroups() (*response.UserGroupResponse, error) {
|
||||
|
||||
+1008
-2250
File diff suppressed because it is too large
Load Diff
@@ -2,155 +2,150 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
agenti18n "github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
dockerGuardComposeProjectLabel = "com.docker.compose.project"
|
||||
dockerGuardComposeCreatedBy = "createdBy"
|
||||
dockerGuardComposeProjectLabel = "com.docker.compose.project"
|
||||
dockerGuardComposeCreatedBy = "createdBy"
|
||||
dockerTrafficPathForward = "forward"
|
||||
dockerTrafficPathInput = "input"
|
||||
dockerTrafficPathUnknown = "unknown"
|
||||
dockerManagementContainerGuard = "container_guard"
|
||||
dockerManagementHostFirewall = "host_firewall"
|
||||
dockerManagementNeedsDiagnosis = "needs_diagnosis"
|
||||
dockerReasonNATInspectFailed = "nat_inspect_failed"
|
||||
dockerReasonNATChainUnreachable = "nat_chain_unreachable"
|
||||
dockerReasonProxyInspectFailed = "proxy_inspect_failed"
|
||||
dockerReasonNoMatchingPath = "no_matching_path"
|
||||
)
|
||||
|
||||
type dockerGuardRuntime = docker_guard.Runtime
|
||||
|
||||
type DockerPortGuardService struct {
|
||||
policies repo.IDockerPortGuardRepo
|
||||
runtime dockerGuardRuntime
|
||||
runtimeForBackend func(string) dockerGuardRuntime
|
||||
runtime dockerfirewall.Runtime
|
||||
runtimeForBackend func(context.Context, string) dockerfirewall.Runtime
|
||||
client func() (*client.Client, error)
|
||||
version func(string) string
|
||||
}
|
||||
|
||||
var (
|
||||
dockerPortGuardServiceMu sync.Mutex
|
||||
dockerPortGuardSyncMu sync.RWMutex
|
||||
dockerPortGuardSyncErr error
|
||||
ErrDockerGuardInvalid = docker_guard.ErrInvalidPolicy
|
||||
ErrDockerUnavailable = docker.ErrUnavailable
|
||||
)
|
||||
var dockerPortGuardServiceMu sync.Mutex
|
||||
|
||||
type IDockerPortGuardService interface {
|
||||
LoadOverview(context.Context) (dto.DockerPortGuardList, error)
|
||||
ExportBackup(context.Context, filter.Provider) (dto.FirewallSubsystemBackup, error)
|
||||
LoadPublishedPorts(context.Context) ([]dto.DockerPortGuardContainer, error)
|
||||
Operate(context.Context, dto.DockerPortGuardOperation) error
|
||||
DeletePolicies(context.Context, dto.DockerPortGuardPolicyBatchDelete) error
|
||||
UpsertPolicies(context.Context, dto.DockerPortGuardPolicyBatch) error
|
||||
Reconcile(context.Context) error
|
||||
QueueInitialization(dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error)
|
||||
DeletePolicies(dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error)
|
||||
UpsertPolicies(dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error)
|
||||
Restore(context.Context) error
|
||||
}
|
||||
|
||||
func NewIDockerPortGuardService() IDockerPortGuardService {
|
||||
return newDockerPortGuardService()
|
||||
}
|
||||
|
||||
func newDockerPortGuardService() *DockerPortGuardService {
|
||||
return &DockerPortGuardService{
|
||||
policies: repo.NewIDockerPortGuardRepo(),
|
||||
client: docker.NewDockerClient,
|
||||
version: dockerFirewallVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func ReconcileDockerPortGuard(ctx context.Context) error {
|
||||
if global.DB == nil {
|
||||
return nil
|
||||
}
|
||||
return NewIDockerPortGuardService().Reconcile(ctx)
|
||||
}
|
||||
|
||||
func ReconcileDockerPortGuardBestEffort(ctx context.Context) {
|
||||
if err := ReconcileDockerPortGuard(ctx); err != nil {
|
||||
global.LOG.Warnf("reconcile Docker port guard failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPortGuardList, error) {
|
||||
selectedBackend := selectedDockerFirewallBackend("")
|
||||
base := s.runtimeStatus(s.guardRuntime(selectedBackend), selectedBackend)
|
||||
base.Version = s.loadFirewallVersion(selectedBackend)
|
||||
policies, err := s.policies.List(ctx)
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return dto.DockerPortGuardList{}, err
|
||||
}
|
||||
backend := selectedDockerFirewallBackend("")
|
||||
inventory, err := s.guardRuntime(ctx, backend).ListPolicies()
|
||||
if err != nil {
|
||||
return dto.DockerPortGuardList{}, err
|
||||
}
|
||||
policies := dockerGuardInventoryEndpoints(inventory)
|
||||
unavailable := func() dto.DockerPortGuardList {
|
||||
backend := selectedDockerFirewallBackend("")
|
||||
base := s.runtimeStatus(s.guardRuntime(ctx, backend), backend, len(families) > 1)
|
||||
base.Message = i18n.Get("ErrDockerFailed")
|
||||
return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: policies}
|
||||
}
|
||||
cli, err := s.client()
|
||||
if err != nil {
|
||||
base.Message = agenti18n.Get("ErrDockerFailed")
|
||||
return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: dockerGuardPolicyEndpoints(policies)}, nil
|
||||
return unavailable(), nil
|
||||
}
|
||||
defer cli.Close()
|
||||
info, err := cli.Info(ctx)
|
||||
if err != nil {
|
||||
base.Message = agenti18n.Get("ErrDockerFailed")
|
||||
return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: dockerGuardPolicyEndpoints(policies)}, nil
|
||||
return unavailable(), nil
|
||||
}
|
||||
base.Backend = selectedDockerFirewallBackend(dockerFirewallBackend(info))
|
||||
base = s.runtimeStatus(s.guardRuntime(base.Backend), base.Backend)
|
||||
base.Version = s.loadFirewallVersion(base.Backend)
|
||||
if reconcileErr := lastDockerPortGuardReconcileError(); reconcileErr != nil {
|
||||
base.Message = reconcileErr.Error()
|
||||
markDockerGuardReconcileFailure(&base, reconcileErr)
|
||||
}
|
||||
endpoints, err := discoverDockerEndpoints(ctx, cli)
|
||||
detectedBackend := dockerFirewallBackend(info)
|
||||
backend = selectedDockerFirewallBackend(detectedBackend)
|
||||
base := s.runtimeStatus(s.guardRuntime(ctx, backend), backend, len(families) > 1)
|
||||
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
|
||||
if err != nil {
|
||||
return dto.DockerPortGuardList{}, err
|
||||
}
|
||||
annotateDockerEndpointManagement(ctx, endpoints, detectedBackend)
|
||||
endpoints, orphanPolicies := matchDockerGuardPolicies(base, policies, endpoints)
|
||||
sort.Slice(endpoints, func(i, j int) bool {
|
||||
return guardEndpointKey(endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol) < guardEndpointKey(endpoints[j].Family, endpoints[j].HostIP, endpoints[j].HostPort, endpoints[j].Protocol)
|
||||
return fmt.Sprintf("%s|%s|%d|%s", endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol) < fmt.Sprintf("%s|%s|%d|%s", endpoints[j].Family, endpoints[j].HostIP, endpoints[j].HostPort, endpoints[j].Protocol)
|
||||
})
|
||||
sort.Slice(orphanPolicies, func(i, j int) bool {
|
||||
return guardEndpointKey(orphanPolicies[i].Family, orphanPolicies[i].HostIP, orphanPolicies[i].HostPort, orphanPolicies[i].Protocol) < guardEndpointKey(orphanPolicies[j].Family, orphanPolicies[j].HostIP, orphanPolicies[j].HostPort, orphanPolicies[j].Protocol)
|
||||
return fmt.Sprintf("%s|%s|%d|%s", orphanPolicies[i].Family, orphanPolicies[i].HostIP, orphanPolicies[i].HostPort, orphanPolicies[i].Protocol) < fmt.Sprintf("%s|%s|%d|%s", orphanPolicies[j].Family, orphanPolicies[j].HostIP, orphanPolicies[j].HostPort, orphanPolicies[j].Protocol)
|
||||
})
|
||||
return dto.DockerPortGuardList{Base: base, Containers: groupDockerGuardContainers(endpoints), OrphanPolicies: orphanPolicies}, nil
|
||||
}
|
||||
|
||||
func matchDockerGuardPolicies(
|
||||
base dto.DockerPortGuardBase,
|
||||
policies []model.DockerPortGuardPolicy,
|
||||
endpoints []dto.DockerPortGuardEndpoint,
|
||||
) ([]dto.DockerPortGuardEndpoint, []dto.DockerPortGuardEndpoint) {
|
||||
byEndpoint := make(map[string]model.DockerPortGuardPolicy, len(policies))
|
||||
for _, policy := range policies {
|
||||
byEndpoint[guardEndpointKey(policy.Family, policy.HostIP, policy.HostPort, policy.Protocol)] = policy
|
||||
func (s *DockerPortGuardService) ExportBackup(ctx context.Context, provider filter.Provider) (dto.FirewallSubsystemBackup, error) {
|
||||
backend := string(provider)
|
||||
if backend == "" {
|
||||
backend = selectedDockerFirewallBackend("")
|
||||
}
|
||||
for i := range endpoints {
|
||||
key := guardEndpointKey(endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol)
|
||||
policy, ok := byEndpoint[key]
|
||||
if !ok {
|
||||
continue
|
||||
if backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables {
|
||||
return dto.FirewallSubsystemBackup{}, filter.ErrInvalidRule
|
||||
}
|
||||
inventory, err := s.guardRuntime(ctx, backend).ListPolicies()
|
||||
if err != nil {
|
||||
return dto.FirewallSubsystemBackup{}, err
|
||||
}
|
||||
if inventory.Policies == nil {
|
||||
inventory.Policies = []dockerfirewall.Policy{}
|
||||
}
|
||||
return dto.FirewallSubsystemBackup{Subsystem: "docker", Provider: filter.Provider(backend), Docker: &inventory}, nil
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.DockerPortGuardContainer, error) {
|
||||
cli, err := s.client()
|
||||
if err != nil {
|
||||
return nil, buserr.WithDetail("ErrDockerFailed", err.Error(), err)
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
if socketPath, local := strings.CutPrefix(cli.DaemonHost(), "unix://"); local {
|
||||
if _, statErr := os.Stat(socketPath); errors.Is(statErr, os.ErrNotExist) {
|
||||
return []dto.DockerPortGuardContainer{}, nil
|
||||
}
|
||||
endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.UUID, policy.Mode, docker_guard.DecodeSources(policy.Sources)
|
||||
endpoints[i].Description = policy.Description
|
||||
endpoints[i].Effective = (policy.Family == docker_guard.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == docker_guard.FamilyIPv6 && base.IPv6.Effective)
|
||||
delete(byEndpoint, key)
|
||||
}
|
||||
orphanPolicies := make([]dto.DockerPortGuardEndpoint, 0, len(byEndpoint))
|
||||
for _, policy := range byEndpoint {
|
||||
orphanPolicies = append(orphanPolicies, dto.DockerPortGuardEndpoint{
|
||||
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
|
||||
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources), Description: policy.Description,
|
||||
})
|
||||
|
||||
endpoints, err := discoverDockerEndpoints(ctx, cli, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return endpoints, orphanPolicies
|
||||
backend := selectedDockerFirewallBackend("")
|
||||
if info, infoErr := cli.Info(ctx); infoErr == nil {
|
||||
backend = dockerFirewallBackend(info)
|
||||
}
|
||||
annotateDockerEndpointManagement(ctx, endpoints, backend)
|
||||
return groupDockerGuardContainers(endpoints), nil
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.DockerPortGuardOperation) error {
|
||||
@@ -158,32 +153,17 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
switch request.Operation {
|
||||
case "initialize":
|
||||
runtime, backend, err := s.runtimeForDocker(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policies, err := s.runtimePolicies(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := runtime.Initialize(policies); err != nil {
|
||||
recordDockerPortGuardReconcileError(err)
|
||||
return err
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable); err != nil {
|
||||
return err
|
||||
}
|
||||
recordDockerPortGuardReconcileError(nil)
|
||||
return nil
|
||||
return s.initialize(ctx, request, nil)
|
||||
case "bind":
|
||||
runtime, _, err := s.runtimeForDocker(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := runtime.Bind(); err != nil {
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := errors.Join(runtime.Bind(families...), ctx.Err()); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
|
||||
@@ -192,9 +172,9 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker
|
||||
if s.runtime != nil {
|
||||
err = s.runtime.Unbind()
|
||||
} else {
|
||||
err = errors.Join(docker_guard.NewManager().Unbind(), docker_guard.NewNftablesManager().Unbind())
|
||||
err = errors.Join(dockerfirewall.NewIptables(ctx).Unbind(), dockerfirewall.NewNftables(ctx).Unbind())
|
||||
}
|
||||
if err != nil {
|
||||
if err = errors.Join(err, ctx.Err()); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
|
||||
@@ -203,439 +183,308 @@ func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.Docker
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) DeletePolicies(ctx context.Context, request dto.DockerPortGuardPolicyBatchDelete) error {
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
uuids, err := docker_guard.NormalizePolicyUUIDs(request.UUIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
func (s *DockerPortGuardService) QueueInitialization(request dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error) {
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
if err := s.policies.DeleteBatch(ctx, uuids); err != nil {
|
||||
return err
|
||||
if request.Operation != "initialize" {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule
|
||||
}
|
||||
return s.reconcileLocked(ctx)
|
||||
if request.BackupFile != "" {
|
||||
if _, err := readFirewallSubsystemBackup(request.BackupFile, "docker"); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
}
|
||||
return queueFirewallRuleTask(firewallTaskDocker, task.TaskExec, request.TaskID, []string{firewallTaskDocker}, func(t *task.Task) error {
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
return s.initialize(t.TaskCtx, request, t)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) UpsertPolicies(ctx context.Context, request dto.DockerPortGuardPolicyBatch) error {
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
policies := make([]model.DockerPortGuardPolicy, 0, len(request.Endpoints))
|
||||
seen := make(map[string]struct{}, len(request.Endpoints))
|
||||
for _, endpoint := range request.Endpoints {
|
||||
normalized, err := docker_guard.NormalizePolicy(docker_guard.Policy{
|
||||
Family: endpoint.Family, HostIP: endpoint.HostIP, HostPort: endpoint.HostPort,
|
||||
Protocol: endpoint.Protocol, Mode: request.Mode, Sources: request.Sources,
|
||||
})
|
||||
func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) {
|
||||
uuids, err := normalizeDockerFirewallUUIDs(request.UUIDs)
|
||||
if err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
return queueFirewallRuleTask(firewallTaskDocker, task.TaskDelete, "", uuids, func(t *task.Task) error {
|
||||
ctx := t.TaskCtx
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
runtime, backend, err := s.runtimeForDocker(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := guardEndpointKey(normalized.Family, normalized.HostIP, normalized.HostPort, normalized.Protocol)
|
||||
if _, exists := seen[key]; exists {
|
||||
continue
|
||||
inventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
encoded, _ := json.Marshal(normalized.Sources)
|
||||
policies = append(policies, model.DockerPortGuardPolicy{
|
||||
UUID: uuid.NewString(), Family: normalized.Family, HostIP: normalized.HostIP,
|
||||
HostPort: normalized.HostPort, Protocol: normalized.Protocol, Mode: normalized.Mode,
|
||||
Sources: string(encoded), Description: strings.TrimSpace(request.Description),
|
||||
})
|
||||
}
|
||||
if err := s.policies.UpsertBatch(ctx, policies); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.reconcileLocked(ctx)
|
||||
wanted := make(map[string]bool, len(uuids))
|
||||
for _, id := range uuids {
|
||||
wanted[id] = true
|
||||
}
|
||||
remaining := make([]dockerfirewall.Policy, 0, len(inventory.Policies))
|
||||
for _, policy := range inventory.Policies {
|
||||
if wanted[policy.UUID] {
|
||||
delete(wanted, policy.UUID)
|
||||
} else {
|
||||
remaining = append(remaining, policy)
|
||||
}
|
||||
}
|
||||
if len(wanted) > 0 {
|
||||
return filter.ErrRuleStale
|
||||
}
|
||||
return applyDockerPolicies(ctx, runtime, backend, inventory, remaining)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) Reconcile(ctx context.Context) error {
|
||||
func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error) {
|
||||
if len(request.Policies) > filter.MaxAtomicExpansion {
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
|
||||
}
|
||||
labels := make([]string, len(request.Policies))
|
||||
policies := make([]dockerfirewall.Policy, 0, len(request.Policies))
|
||||
endpoints := make([]dto.DockerPortGuardEndpointIdentity, 0, len(request.Policies))
|
||||
count := 0
|
||||
for i, policy := range request.Policies {
|
||||
labels[i] = fmt.Sprintf("[%d/%d] %s %s %s:%d %s", i+1, len(request.Policies), policy.Family, policy.Protocol, policy.HostIP, policy.HostPort, policy.Mode)
|
||||
normalized, err := normalizeDockerFirewallPolicy(dockerfirewall.Policy{
|
||||
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort,
|
||||
Protocol: policy.Protocol, Mode: policy.Mode, Sources: policy.Sources,
|
||||
})
|
||||
if err != nil {
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("%s: %w", labels[i], err)
|
||||
}
|
||||
if normalized.Mode == dockerfirewall.ModeAll || normalized.Mode == dockerfirewall.ModeAcceptAll {
|
||||
count++
|
||||
} else {
|
||||
count += len(normalized.Sources)
|
||||
if normalized.Mode == dockerfirewall.ModeAllow {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count > filter.MaxAtomicExpansion {
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
|
||||
}
|
||||
normalized.UUID = uuid.NewString()
|
||||
policies = append(policies, normalized)
|
||||
}
|
||||
return queueFirewallRuleTask(firewallTaskDocker, task.TaskUpdate, "", labels, func(t *task.Task) error {
|
||||
ctx := t.TaskCtx
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
runtime, backend, err := s.runtimeForDocker(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current := append([]dockerfirewall.Policy(nil), inventory.Policies...)
|
||||
if request.Import {
|
||||
backup := dto.FirewallSubsystemBackup{Provider: filter.Provider(backend), Docker: &dockerfirewall.PolicyInventory{Policies: policies}}
|
||||
merged, err := mergeDockerBackup(inventory, backup, backend, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current = merged.Policies
|
||||
}
|
||||
byEndpoint := make(map[string]int, len(current))
|
||||
for i, policy := range current {
|
||||
byEndpoint[dockerPolicyEndpointKey(policy)] = i
|
||||
}
|
||||
for i := range policies {
|
||||
key := dockerPolicyEndpointKey(policies[i])
|
||||
index, exists := byEndpoint[key]
|
||||
if request.Import {
|
||||
if !exists || current[index].UUID != policies[i].UUID {
|
||||
labels[i] = ""
|
||||
continue
|
||||
}
|
||||
} else if exists {
|
||||
policies[i].UUID = current[index].UUID
|
||||
current[index] = policies[i]
|
||||
} else {
|
||||
byEndpoint[key] = len(current)
|
||||
current = append(current, policies[i])
|
||||
}
|
||||
endpoints = append(endpoints, dto.DockerPortGuardEndpointIdentity{
|
||||
Family: policies[i].Family, HostIP: policies[i].HostIP, HostPort: policies[i].HostPort, Protocol: policies[i].Protocol,
|
||||
})
|
||||
}
|
||||
if len(endpoints) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := s.rejectHostInputDockerGuardEndpoints(ctx, endpoints); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := applyDockerPolicies(ctx, runtime, backend, inventory, current); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) Restore(ctx context.Context) error {
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
return s.reconcileLocked(ctx)
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) loadRuleSyncCandidates(
|
||||
ctx context.Context,
|
||||
request dto.FirewallRuleSyncRequest,
|
||||
) (string, []model.DockerPortGuardPolicy, dockerGuardRuntime, error) {
|
||||
targetProvider, err := databaseRuleSyncTarget(request, "Docker")
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
enabled, err := dockerPortGuardPersistedEnabled()
|
||||
if err != nil || !enabled {
|
||||
return err
|
||||
}
|
||||
target := string(targetProvider)
|
||||
selected, err := s.selectedRuleSyncBackend(ctx)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
if target != selected {
|
||||
return "", nil, nil, fmt.Errorf(
|
||||
"%w: selected Docker firewall backend is %s, requested target is %s",
|
||||
filter.ErrProviderUnavailable, selected, target,
|
||||
)
|
||||
}
|
||||
policies, err := s.policies.List(ctx)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
return target, policies, s.guardRuntime(target), nil
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) selectedRuleSyncBackend(ctx context.Context) (string, error) {
|
||||
if global.DB != nil {
|
||||
selected, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||
selected = strings.ToLower(strings.TrimSpace(selected))
|
||||
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
|
||||
return selected, nil
|
||||
}
|
||||
}
|
||||
if s.client == nil {
|
||||
return "", fmt.Errorf("%w: Docker firewall backend is unavailable", ErrDockerUnavailable)
|
||||
}
|
||||
cli, err := s.client()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %v", ErrDockerUnavailable, err)
|
||||
}
|
||||
defer cli.Close()
|
||||
info, err := cli.Info(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %v", ErrDockerUnavailable, err)
|
||||
}
|
||||
return selectedDockerFirewallBackend(dockerFirewallBackend(info)), nil
|
||||
}
|
||||
|
||||
func dockerGuardPoliciesFromModels(policies []model.DockerPortGuardPolicy) []docker_guard.Policy {
|
||||
result := make([]docker_guard.Policy, 0, len(policies))
|
||||
for _, policy := range policies {
|
||||
result = append(result, dockerGuardPolicyFromModel(policy))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func dockerGuardPolicyFromModel(policy model.DockerPortGuardPolicy) docker_guard.Policy {
|
||||
return docker_guard.Policy{
|
||||
UUID: policy.UUID, Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort,
|
||||
Protocol: policy.Protocol, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources),
|
||||
}
|
||||
}
|
||||
|
||||
func dockerGuardRuleSyncDTO(policy model.DockerPortGuardPolicy) *dto.DockerPortGuardEndpoint {
|
||||
return &dto.DockerPortGuardEndpoint{
|
||||
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
|
||||
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources), Description: policy.Description,
|
||||
}
|
||||
}
|
||||
|
||||
func dockerGuardRuntimeRuleSyncDTO(policy docker_guard.Policy) *dto.DockerPortGuardEndpoint {
|
||||
return &dto.DockerPortGuardEndpoint{
|
||||
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
|
||||
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: append([]string(nil), policy.Sources...),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) reconcileLocked(ctx context.Context) (err error) {
|
||||
defer func() { recordDockerPortGuardReconcileError(err) }()
|
||||
persistedEnabled, err := dockerPortGuardPersistedEnabled()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load Docker port guard persisted status: %w", err)
|
||||
}
|
||||
initialized, err := s.anyRuntimeInitialized()
|
||||
if err != nil {
|
||||
return &docker_guard.FamilyError{Family: docker_guard.FamilyIPv4, Err: fmt.Errorf("inspect initialization: %w", err)}
|
||||
}
|
||||
if !initialized && !persistedEnabled {
|
||||
return nil
|
||||
}
|
||||
runtime, _, err := s.runtimeForDocker(ctx)
|
||||
runtime, backend, err := s.runtimeForDocker(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
initialized, err = runtime.Initialized(docker_guard.FamilyIPv4)
|
||||
if err != nil {
|
||||
return &docker_guard.FamilyError{Family: docker_guard.FamilyIPv4, Err: fmt.Errorf("inspect initialization: %w", err)}
|
||||
}
|
||||
if !initialized && !persistedEnabled {
|
||||
backup, err := readFirewallSubsystemBackup("docker-"+backend+".rules", "docker")
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
policies, err := s.runtimePolicies(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !initialized {
|
||||
err = runtime.Initialize(policies)
|
||||
} else {
|
||||
err = runtime.Reconcile(policies)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func dockerPortGuardPersistedEnabled() (bool, error) {
|
||||
status, err := settingRepo.GetValueByKey(constant.FirewallDockerPortGuardStatusKey)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return status == constant.StatusEnable, err
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) anyRuntimeInitialized() (bool, error) {
|
||||
if s.runtime != nil {
|
||||
return s.runtime.Initialized(docker_guard.FamilyIPv4)
|
||||
}
|
||||
for _, runtime := range []dockerGuardRuntime{docker_guard.NewManager(), docker_guard.NewNftablesManager()} {
|
||||
initialized, err := runtime.Initialized(docker_guard.FamilyIPv4)
|
||||
if err != nil || initialized {
|
||||
return initialized, err
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func recordDockerPortGuardReconcileError(err error) {
|
||||
dockerPortGuardSyncMu.Lock()
|
||||
dockerPortGuardSyncErr = err
|
||||
dockerPortGuardSyncMu.Unlock()
|
||||
}
|
||||
|
||||
func lastDockerPortGuardReconcileError() error {
|
||||
dockerPortGuardSyncMu.RLock()
|
||||
defer dockerPortGuardSyncMu.RUnlock()
|
||||
return dockerPortGuardSyncErr
|
||||
}
|
||||
|
||||
func markDockerGuardReconcileFailure(base *dto.DockerPortGuardBase, err error) {
|
||||
var familyErr *docker_guard.FamilyError
|
||||
if errors.As(err, &familyErr) {
|
||||
markDockerGuardFamilyNotEffective(base, familyErr.Family)
|
||||
if familyErr.Family == docker_guard.FamilyIPv4 {
|
||||
markDockerGuardFamilyNotEffective(base, docker_guard.FamilyIPv6)
|
||||
}
|
||||
return
|
||||
}
|
||||
markDockerGuardFamilyNotEffective(base, docker_guard.FamilyIPv4)
|
||||
markDockerGuardFamilyNotEffective(base, docker_guard.FamilyIPv6)
|
||||
}
|
||||
|
||||
func markDockerGuardFamilyNotEffective(base *dto.DockerPortGuardBase, family string) {
|
||||
var status *dto.DockerPortGuardFamilyStatus
|
||||
switch family {
|
||||
case docker_guard.FamilyIPv4:
|
||||
status = &base.IPv4
|
||||
case docker_guard.FamilyIPv6:
|
||||
status = &base.IPv6
|
||||
default:
|
||||
return
|
||||
}
|
||||
if !status.Initialized {
|
||||
return
|
||||
}
|
||||
status.State = docker_guard.StatusNotEffective
|
||||
status.Reason = docker_guard.ReasonInspectFailed
|
||||
status.Effective = false
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) runtimePolicies(ctx context.Context) ([]docker_guard.Policy, error) {
|
||||
stored, err := s.policies.List(ctx)
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
policies := make([]docker_guard.Policy, 0, len(stored))
|
||||
for _, policy := range stored {
|
||||
policies = append(policies, docker_guard.Policy{UUID: policy.UUID, Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources)})
|
||||
if !slices.Contains(families, constant.FirewallFamilyIPv6) {
|
||||
before := len(backup.Docker.Policies)
|
||||
backup.Docker.Policies = slices.DeleteFunc(backup.Docker.Policies, func(policy dockerfirewall.Policy) bool { return policy.Family == constant.FirewallFamilyIPv6 })
|
||||
logFirewallIPv6Skipped(nil, "Docker startup", before-len(backup.Docker.Policies))
|
||||
}
|
||||
return policies, nil
|
||||
missing := make(map[string]bool)
|
||||
needsBind := false
|
||||
for _, family := range families {
|
||||
status := runtime.Status(family)
|
||||
if status.Reason == dockerfirewall.ReasonInspectFailed {
|
||||
return fmt.Errorf("inspect Docker guard %s failed", family)
|
||||
}
|
||||
if status.Reason == dockerfirewall.ReasonCommandMissing {
|
||||
continue
|
||||
}
|
||||
missing[family] = !status.Initialized
|
||||
needsBind = needsBind || (status.Initialized && !status.Effective)
|
||||
}
|
||||
if !missing[dockerfirewall.FamilyIPv4] && !missing[dockerfirewall.FamilyIPv6] {
|
||||
if needsBind {
|
||||
return runtime.Bind(families...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
inventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, policy := range inventory.Policies {
|
||||
missing[policy.Family] = false
|
||||
}
|
||||
if inventory.RuleOrders == nil {
|
||||
inventory.RuleOrders = make(map[string][]int64)
|
||||
}
|
||||
for _, policy := range backup.Docker.Policies {
|
||||
if !missing[policy.Family] {
|
||||
continue
|
||||
}
|
||||
inventory.Policies = append(inventory.Policies, policy)
|
||||
key := policy.Family + "\x00" + policy.UUID
|
||||
inventory.RuleOrders[key] = backup.Docker.RuleOrders[key]
|
||||
}
|
||||
return runtime.Initialize(inventory.Policies, inventory, families...)
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) runtimeStatus(runtime dockerGuardRuntime, backend string) dto.DockerPortGuardBase {
|
||||
ipv4 := runtime.Status(docker_guard.FamilyIPv4)
|
||||
ipv6 := runtime.Status(docker_guard.FamilyIPv6)
|
||||
func (s *DockerPortGuardService) runtimeStatus(runtime dockerfirewall.Runtime, backend string, ipv6Enabled bool) dto.DockerPortGuardBase {
|
||||
ipv4 := runtime.Status(dockerfirewall.FamilyIPv4)
|
||||
var ipv6 dockerfirewall.FamilyStatus
|
||||
if ipv6Enabled {
|
||||
ipv6 = runtime.Status(dockerfirewall.FamilyIPv6)
|
||||
}
|
||||
version := "-"
|
||||
if s.version != nil {
|
||||
version = s.version(backend)
|
||||
}
|
||||
name := "iptables-docker"
|
||||
if strings.EqualFold(strings.TrimSpace(backend), constant.FirewallProviderNftables) {
|
||||
name = "nftables-docker"
|
||||
}
|
||||
return dto.DockerPortGuardBase{
|
||||
Name: dockerFirewallDisplayName(backend),
|
||||
IPv6Enabled: ipv6Enabled,
|
||||
Name: name,
|
||||
Version: version,
|
||||
Backend: backend,
|
||||
IsExist: ipv4.Reason != docker_guard.ReasonCommandMissing || ipv6.Reason != docker_guard.ReasonCommandMissing,
|
||||
IsExist: ipv4.Reason != dockerfirewall.ReasonCommandMissing || (ipv6Enabled && ipv6.Reason != dockerfirewall.ReasonCommandMissing),
|
||||
Initialized: ipv4.Initialized || ipv6.Initialized,
|
||||
Bound: ipv4.Bound || ipv6.Bound,
|
||||
IPv4: dto.DockerPortGuardFamilyStatus{State: ipv4.State, Reason: ipv4.Reason, Initialized: ipv4.Initialized, Bound: ipv4.Bound, Effective: ipv4.Effective},
|
||||
IPv6: dto.DockerPortGuardFamilyStatus{State: ipv6.State, Reason: ipv6.Reason, Initialized: ipv6.Initialized, Bound: ipv6.Bound, Effective: ipv6.Effective},
|
||||
IPv4: dto.DockerPortGuardFamilyStatus{Partial: ipv4.Partial, State: ipv4.State, Reason: ipv4.Reason, Initialized: ipv4.Initialized, Bound: ipv4.Bound, Effective: ipv4.Effective},
|
||||
IPv6: dto.DockerPortGuardFamilyStatus{Partial: ipv6.Partial, State: ipv6.State, Reason: ipv6.Reason, Initialized: ipv6.Initialized, Bound: ipv6.Bound, Effective: ipv6.Effective},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) guardRuntime(backend string) dockerGuardRuntime {
|
||||
if s.runtimeForBackend != nil {
|
||||
return s.runtimeForBackend(backend)
|
||||
func matchDockerGuardPolicies(base dto.DockerPortGuardBase, policies []dto.DockerPortGuardEndpoint, endpoints []dto.DockerPortGuardEndpoint) ([]dto.DockerPortGuardEndpoint, []dto.DockerPortGuardEndpoint) {
|
||||
matched := make(map[int]bool, len(policies))
|
||||
byEndpoint := make(map[string]int, len(policies))
|
||||
for index, policy := range policies {
|
||||
key := strings.Join([]string{policy.Family, policy.HostIP, strconv.Itoa(int(policy.HostPort)), policy.Protocol}, "\x00")
|
||||
if _, exists := byEndpoint[key]; !exists {
|
||||
byEndpoint[key] = index
|
||||
}
|
||||
}
|
||||
if s.runtime != nil {
|
||||
return s.runtime
|
||||
for i := range endpoints {
|
||||
key := strings.Join([]string{endpoints[i].Family, endpoints[i].HostIP, strconv.Itoa(int(endpoints[i].HostPort)), endpoints[i].Protocol}, "\x00")
|
||||
index, exists := byEndpoint[key]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
policy := policies[index]
|
||||
endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.PolicyUUID, policy.Mode, policy.Sources
|
||||
endpoints[i].Effective = endpoints[i].ManagementTarget == dockerManagementContainerGuard && ((policy.Family == dockerfirewall.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == dockerfirewall.FamilyIPv6 && base.IPv6.Effective))
|
||||
matched[index] = true
|
||||
}
|
||||
return docker_guard.NewRuntime(backend)
|
||||
orphans := make([]dto.DockerPortGuardEndpoint, 0)
|
||||
for i, policy := range policies {
|
||||
if !matched[i] {
|
||||
orphans = append(orphans, policy)
|
||||
}
|
||||
}
|
||||
return endpoints, orphans
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) runtimeForDocker(ctx context.Context) (dockerGuardRuntime, string, error) {
|
||||
if s.runtime != nil {
|
||||
return s.runtime, selectedDockerFirewallBackend(constant.FirewallProviderIptables), nil
|
||||
func (s *DockerPortGuardService) rejectHostInputDockerGuardEndpoints(ctx context.Context, requested []dto.DockerPortGuardEndpointIdentity) error {
|
||||
if s.client == nil || len(requested) == 0 {
|
||||
return ctx.Err()
|
||||
}
|
||||
cli, err := s.client()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("%w: %v", ErrDockerUnavailable, err)
|
||||
return ctx.Err()
|
||||
}
|
||||
defer cli.Close()
|
||||
info, err := cli.Info(ctx)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("%w: %v", ErrDockerUnavailable, err)
|
||||
return ctx.Err()
|
||||
}
|
||||
backend := selectedDockerFirewallBackend(dockerFirewallBackend(info))
|
||||
if backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables {
|
||||
return nil, backend, fmt.Errorf("Docker firewall backend %q is not supported", backend)
|
||||
}
|
||||
return s.guardRuntime(backend), backend, nil
|
||||
}
|
||||
|
||||
func dockerFirewallBackend(info system.Info) string {
|
||||
if info.FirewallBackend == nil || info.FirewallBackend.Driver == "" {
|
||||
return constant.FirewallProviderIptables
|
||||
}
|
||||
return strings.ToLower(info.FirewallBackend.Driver)
|
||||
}
|
||||
|
||||
func dockerFirewallDisplayName(backend string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(backend)) {
|
||||
case constant.FirewallProviderNftables:
|
||||
return "nftables-docker"
|
||||
default:
|
||||
return "iptables-docker"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) loadFirewallVersion(backend string) string {
|
||||
if s.version == nil {
|
||||
return "-"
|
||||
}
|
||||
return s.version(backend)
|
||||
}
|
||||
|
||||
func dockerFirewallVersion(backend string) string {
|
||||
client, err := lifecycle.NewClientFor(backend)
|
||||
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
|
||||
if err != nil {
|
||||
return "-"
|
||||
return ctx.Err()
|
||||
}
|
||||
version, err := client.Version()
|
||||
if err != nil || strings.TrimSpace(version) == "" {
|
||||
return "-"
|
||||
annotateDockerEndpointManagement(ctx, endpoints, dockerFirewallBackend(info))
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
func discoverDockerEndpoints(ctx context.Context, cli *client.Client) ([]dto.DockerPortGuardEndpoint, error) {
|
||||
containers, err := cli.ContainerList(ctx, containertypes.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpoints := make([]dto.DockerPortGuardEndpoint, 0)
|
||||
for _, item := range containers {
|
||||
name := strings.TrimPrefix(firstGuardString(item.Names), "/")
|
||||
compose := item.Labels[dockerGuardComposeProjectLabel]
|
||||
application := ""
|
||||
if created, ok := item.Labels[dockerGuardComposeCreatedBy]; ok && created == "Apps" {
|
||||
application = compose
|
||||
}
|
||||
for _, port := range item.Ports {
|
||||
if port.PublicPort == 0 || (port.Type != "tcp" && port.Type != "udp") {
|
||||
continue
|
||||
}
|
||||
family := docker_guard.FamilyIPv4
|
||||
hostIP := port.IP
|
||||
if addr, err := netip.ParseAddr(hostIP); err == nil && addr.Is6() {
|
||||
family = docker_guard.FamilyIPv6
|
||||
} else if hostIP == "" {
|
||||
hostIP = "0.0.0.0"
|
||||
}
|
||||
endpoints = append(endpoints, dto.DockerPortGuardEndpoint{Family: family, HostIP: hostIP, HostPort: port.PublicPort, Protocol: port.Type, ContainerID: item.ID, ContainerName: name, ContainerPort: port.PrivatePort, Compose: compose, Application: application, Sources: []string{}})
|
||||
}
|
||||
}
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
func dockerGuardPolicyEndpoints(policies []model.DockerPortGuardPolicy) []dto.DockerPortGuardEndpoint {
|
||||
endpoints := make([]dto.DockerPortGuardEndpoint, 0, len(policies))
|
||||
for _, policy := range policies {
|
||||
endpoints = append(endpoints, dto.DockerPortGuardEndpoint{
|
||||
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
|
||||
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources),
|
||||
Description: policy.Description,
|
||||
})
|
||||
}
|
||||
return endpoints
|
||||
}
|
||||
|
||||
func groupDockerGuardContainers(endpoints []dto.DockerPortGuardEndpoint) []dto.DockerPortGuardContainer {
|
||||
containers := make(map[string]*dto.DockerPortGuardContainer)
|
||||
order := make([]string, 0)
|
||||
targets := make(map[string]string, len(endpoints))
|
||||
for _, endpoint := range endpoints {
|
||||
key := endpoint.ContainerID
|
||||
if key == "" {
|
||||
key = "__orphan__"
|
||||
}
|
||||
container, ok := containers[key]
|
||||
if !ok {
|
||||
container = &dto.DockerPortGuardContainer{
|
||||
Key: key, Name: endpoint.ContainerName, Compose: endpoint.Compose,
|
||||
Application: endpoint.Application, Endpoints: []dto.DockerPortGuardEndpoint{},
|
||||
}
|
||||
containers[key] = container
|
||||
order = append(order, key)
|
||||
}
|
||||
container.Endpoints = append(container.Endpoints, endpoint)
|
||||
targets[fmt.Sprintf("%s|%s|%d|%s", endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)] = endpoint.ManagementTarget
|
||||
}
|
||||
sort.Slice(order, func(i, j int) bool {
|
||||
return containers[order[i]].Name < containers[order[j]].Name
|
||||
})
|
||||
|
||||
result := make([]dto.DockerPortGuardContainer, 0, len(order))
|
||||
for _, key := range order {
|
||||
container := containers[key]
|
||||
items := make([]docker.PortRangeItem, 0, len(container.Endpoints))
|
||||
for i, endpoint := range container.Endpoints {
|
||||
sources := append([]string(nil), endpoint.Sources...)
|
||||
sort.Strings(sources)
|
||||
policyKey := fmt.Sprintf("%t|%s|%s|%t|%s", endpoint.PolicyUUID != "", endpoint.Mode, strings.Join(sources, ","), endpoint.Effective, endpoint.Description)
|
||||
items = append(items, docker.PortRangeItem{
|
||||
Key: endpoint.Family + "|" + endpoint.HostIP + "|" + endpoint.Protocol + "|" + policyKey,
|
||||
PublicPort: endpoint.HostPort, PrivatePort: endpoint.ContainerPort,
|
||||
HasPrivatePort: endpoint.ContainerPort != 0, Position: i,
|
||||
})
|
||||
for _, endpoint := range requested {
|
||||
target := targets[fmt.Sprintf("%s|%s|%d|%s", endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)]
|
||||
if target == dockerManagementHostFirewall {
|
||||
return buserr.WithDetail("ErrInvalidParams", "endpoint traffic is handled by the host input firewall", nil)
|
||||
}
|
||||
container.PortGroups = make([]dto.DockerPortGuardPortGroup, 0, len(items))
|
||||
for _, portRange := range docker.MergePortRanges(items) {
|
||||
start := container.Endpoints[portRange.Start.Position]
|
||||
address := start.HostIP
|
||||
if strings.Contains(address, ":") {
|
||||
address = "[" + address + "]"
|
||||
}
|
||||
ports := fmt.Sprintf("%d", portRange.Start.PublicPort)
|
||||
if portRange.Start.PublicPort != portRange.End.PublicPort {
|
||||
ports = fmt.Sprintf("%d-%d", portRange.Start.PublicPort, portRange.End.PublicPort)
|
||||
}
|
||||
container.PortGroups = append(container.PortGroups, dto.DockerPortGuardPortGroup{
|
||||
Key: fmt.Sprintf("%s|%d-%d", portRange.Start.Key, portRange.Start.PublicPort, portRange.End.PublicPort),
|
||||
Label: fmt.Sprintf("%s:%s/%s", address, ports, start.Protocol), Endpoint: start,
|
||||
Endpoints: func() []dto.DockerPortGuardEndpoint {
|
||||
members := make([]dto.DockerPortGuardEndpoint, 0, len(portRange.Items))
|
||||
for _, item := range portRange.Items {
|
||||
members = append(members, container.Endpoints[item.Position])
|
||||
}
|
||||
return members
|
||||
}(),
|
||||
})
|
||||
if target == dockerManagementNeedsDiagnosis {
|
||||
return buserr.WithDetail("ErrInvalidParams", "endpoint traffic management target requires diagnosis", nil)
|
||||
}
|
||||
result = append(result, *container)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func guardEndpointKey(family, hostIP string, hostPort uint16, protocol string) string {
|
||||
return fmt.Sprintf("%s|%s|%d|%s", family, hostIP, hostPort, protocol)
|
||||
}
|
||||
|
||||
func firstGuardString(values []string) string {
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
return values[0]
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
)
|
||||
|
||||
func selectedDockerFirewallBackend(fallback string) string {
|
||||
selected := configuredDockerFirewallBackend()
|
||||
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
|
||||
return selected
|
||||
}
|
||||
fallback = strings.ToLower(strings.TrimSpace(fallback))
|
||||
if fallback == constant.FirewallProviderNftables {
|
||||
return fallback
|
||||
}
|
||||
return constant.FirewallProviderIptables
|
||||
}
|
||||
|
||||
func configuredDockerFirewallBackend() string {
|
||||
if global.DB == nil {
|
||||
return ""
|
||||
}
|
||||
selected, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||
selected = strings.ToLower(strings.TrimSpace(selected))
|
||||
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
|
||||
return selected
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func selectedSystemFirewallClient() (lifecycle.Client, error) {
|
||||
if provider, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey); strings.TrimSpace(provider) != "" {
|
||||
return lifecycle.NewClientFor(strings.TrimSpace(provider))
|
||||
}
|
||||
client, err := lifecycle.NewClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, client.Name())
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func NewSelectedSystemFirewallClient() (lifecycle.Client, error) {
|
||||
return selectedSystemFirewallClient()
|
||||
}
|
||||
|
||||
func selectedSystemFirewallProvider() (string, error) {
|
||||
client, err := selectedSystemFirewallClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return client.Name(), nil
|
||||
}
|
||||
@@ -2,45 +2,157 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type IFirewallSettingService interface {
|
||||
CreatePortWhitelist(context.Context, dto.FirewallPortWhitelistCreate) error
|
||||
UpdatePortWhitelist(context.Context, dto.FirewallPortWhitelistUpdate) error
|
||||
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error
|
||||
Load(context.Context) (dto.FirewallSettings, error)
|
||||
Operate(context.Context, dto.FirewallBackendOperation) error
|
||||
OperateFamily(dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error)
|
||||
OperateIPv6(dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error)
|
||||
}
|
||||
|
||||
type FirewallSettingService struct{}
|
||||
|
||||
func NewIFirewallSettingService() IFirewallSettingService {
|
||||
return &FirewallSettingService{}
|
||||
var firewallWhitelistMu sync.Mutex
|
||||
|
||||
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) (result error) {
|
||||
firewallWhitelistMu.Lock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer func() {
|
||||
firewallRuleMutationMu.Unlock()
|
||||
firewallWhitelistMu.Unlock()
|
||||
if result == nil {
|
||||
result = newFirewallService().SyncPortWhitelist(ctx)
|
||||
}
|
||||
}()
|
||||
current, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current = append(current, request.Rule)
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) (result error) {
|
||||
firewallWhitelistMu.Lock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer func() {
|
||||
firewallRuleMutationMu.Unlock()
|
||||
firewallWhitelistMu.Unlock()
|
||||
if result == nil {
|
||||
result = newFirewallService().SyncPortWhitelist(ctx)
|
||||
}
|
||||
}()
|
||||
current, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index, err := findPortWhitelistRule(current, request.OldRule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current[index] = request.Rule
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error {
|
||||
firewallWhitelistMu.Lock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer func() {
|
||||
firewallRuleMutationMu.Unlock()
|
||||
firewallWhitelistMu.Unlock()
|
||||
}()
|
||||
current, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Rule == nil {
|
||||
return filter.ErrInvalidRule
|
||||
}
|
||||
index, err := findPortWhitelistRule(current, *request.Rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current = slices.Delete(current, index, index+1)
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {
|
||||
result := dto.FirewallSettings{PingStatus: ping.LoadStatus()}
|
||||
if ports, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList); err == nil {
|
||||
result.PortWhitelist = ports
|
||||
} else {
|
||||
result.PortWhitelist = constant.FirewallPortWhiteListValue
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return dto.FirewallSettings{}, err
|
||||
}
|
||||
result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus(), IPv6Enabled: slices.Contains(families, constant.FirewallFamilyIPv6)}
|
||||
|
||||
installed := make(map[string]bool)
|
||||
for _, name := range lifecycle.InstalledProviders() {
|
||||
installed[name] = true
|
||||
}
|
||||
systemBackend, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
||||
result.System.Selected = strings.TrimSpace(systemBackend)
|
||||
if result.System.Selected == "" {
|
||||
if client, err := lifecycle.NewClient(""); err == nil {
|
||||
result.System.Selected = client.Name()
|
||||
}
|
||||
}
|
||||
result.System.Current = result.System.Selected
|
||||
for _, name := range []string{
|
||||
constant.FirewallProviderFirewalld,
|
||||
constant.FirewallProviderUFW,
|
||||
@@ -48,146 +160,146 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
||||
constant.FirewallProviderNftables,
|
||||
} {
|
||||
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
|
||||
if option.Installed {
|
||||
client, err := lifecycle.NewClientFor(name)
|
||||
if option.Installed && name == result.System.Selected {
|
||||
client, err := lifecycle.NewClient(name)
|
||||
if err != nil {
|
||||
option.Message = err.Error()
|
||||
} else if supportsManagedFilterChains(name) {
|
||||
option.Initialized, option.Bound, err = loadFirewallInitStatus(name, "base")
|
||||
} else if name == constant.FirewallProviderIptables || name == constant.FirewallProviderNftables {
|
||||
overview, err := loadSystemFirewallOverview(name, "base", families)
|
||||
if err != nil {
|
||||
option.Message = err.Error()
|
||||
}
|
||||
option.IPv4 = loadSystemFirewallFamilyInfo(name, constant.FirewallFamilyIPv4)
|
||||
option.IPv6 = loadSystemFirewallFamilyInfo(name, constant.FirewallFamilyIPv6)
|
||||
option.Initialized, option.Bound = overview.IsInit, overview.IsBind
|
||||
option.IPv4, option.IPv6 = overview.IPv4, overview.IPv6
|
||||
} else if option.Active, err = client.Status(); err != nil {
|
||||
option.Message = err.Error()
|
||||
}
|
||||
}
|
||||
if name == constant.FirewallProviderIptables {
|
||||
if name == result.System.Selected && name == constant.FirewallProviderIptables {
|
||||
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil {
|
||||
option.Implementation = commands.IPv4
|
||||
}
|
||||
}
|
||||
result.System.Options = append(result.System.Options, option)
|
||||
}
|
||||
result.System.Selected, _ = settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
||||
if result.System.Selected == "" {
|
||||
if client, err := lifecycle.NewClient(); err == nil {
|
||||
result.System.Selected = client.Name()
|
||||
}
|
||||
}
|
||||
result.System.Current = result.System.Selected
|
||||
|
||||
initializedForwarding := make([]string, 0, 2)
|
||||
forwardingBackend, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||
result.Forwarding.Selected = strings.TrimSpace(forwardingBackend)
|
||||
if result.Forwarding.Selected == "" {
|
||||
result.Forwarding.Selected = constant.FirewallProviderIptables
|
||||
}
|
||||
result.Forwarding.Current = result.Forwarding.Selected
|
||||
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
|
||||
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
|
||||
if option.Installed {
|
||||
manager, err := newForwardingManagerFor(name)
|
||||
if option.Installed && name == result.Forwarding.Selected {
|
||||
manager, err := newForwardingAdapterFor(ctx, name)
|
||||
if err != nil {
|
||||
option.Message = err.Error()
|
||||
} else if status, err := manager.Status(); err != nil {
|
||||
option.Message = err.Error()
|
||||
} else {
|
||||
option.Initialized, option.Bound = status.IsInit, status.IsBind
|
||||
ipv4Init, ipv4Bound, ipv4Err := manager.FamilyStatus(constant.FirewallFamilyIPv4)
|
||||
ipv6Init, ipv6Bound, ipv6Err := manager.FamilyStatus(constant.FirewallFamilyIPv6)
|
||||
option.IPv4 = dto.FirewallBackendFamilyStatus{
|
||||
Available: ipv4Err == nil, Initialized: ipv4Init, Bound: ipv4Bound,
|
||||
status, statusErr := loadForwardingFirewallOverview(manager, families)
|
||||
option.IPv4, option.IPv6 = status.IPv4, status.IPv6
|
||||
if statusErr != nil {
|
||||
option.Message = statusErr.Error()
|
||||
} else {
|
||||
option.Initialized, option.Bound = status.IsInit, status.IsBind
|
||||
}
|
||||
option.IPv6 = dto.FirewallBackendFamilyStatus{
|
||||
Available: ipv6Err == nil, Initialized: ipv6Init, Bound: ipv6Bound,
|
||||
}
|
||||
if name == constant.FirewallProviderIptables {
|
||||
if commands, commandErr := lifecycle.ResolveIptablesCommands(); commandErr == nil {
|
||||
option.IPv6.Available = option.IPv6.Available && commands.IPv6Available()
|
||||
if !commands.IPv6Available() {
|
||||
option.IPv6.Reason = docker_guard.ReasonCommandMissing
|
||||
}
|
||||
if result.IPv6Enabled && name == constant.FirewallProviderIptables && !option.IPv6.Available {
|
||||
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil && !commands.IPv6Available() {
|
||||
option.IPv6.Reason = dockerfirewall.ReasonCommandMissing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if option.IPv4.Initialized || option.IPv6.Initialized {
|
||||
initializedForwarding = append(initializedForwarding, name)
|
||||
}
|
||||
if name == constant.FirewallProviderIptables {
|
||||
if name == result.Forwarding.Selected && name == constant.FirewallProviderIptables {
|
||||
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil {
|
||||
option.Implementation = commands.IPv4
|
||||
}
|
||||
}
|
||||
result.Forwarding.Options = append(result.Forwarding.Options, option)
|
||||
}
|
||||
if len(initializedForwarding) == 1 {
|
||||
result.Forwarding.Current = initializedForwarding[0]
|
||||
} else if len(initializedForwarding) > 1 {
|
||||
result.Forwarding.Current = strings.Join(initializedForwarding, " + ")
|
||||
}
|
||||
result.Forwarding.Selected, _ = settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||
if result.Forwarding.Selected == "" {
|
||||
result.Forwarding.Selected = constant.FirewallProviderIptables
|
||||
}
|
||||
|
||||
dockerInstalled := cmd.Which("docker")
|
||||
result.Docker.Selected = configuredDockerFirewallBackend()
|
||||
dockerVersion := ""
|
||||
if dockerInstalled {
|
||||
dockerVersion = loadDockerEngineVersion(ctx)
|
||||
}
|
||||
dockerBackend, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||
dockerBackend = strings.ToLower(strings.TrimSpace(dockerBackend))
|
||||
if dockerBackend == constant.FirewallProviderIptables || dockerBackend == constant.FirewallProviderNftables {
|
||||
result.Docker.Selected = dockerBackend
|
||||
}
|
||||
result.Docker.Current = result.Docker.Selected
|
||||
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
|
||||
option := dto.FirewallBackendOption{
|
||||
Name: name, Installed: installed[name], Supported: dockerInstalled,
|
||||
Active: dockerInstalled && installed[name] && result.Docker.Selected == name,
|
||||
}
|
||||
guard := docker_guard.NewRuntime(name)
|
||||
ipv4, ipv6 := guard.Status(docker_guard.FamilyIPv4), guard.Status(docker_guard.FamilyIPv6)
|
||||
option.Initialized = ipv4.Initialized || ipv6.Initialized
|
||||
option.Bound = ipv4.Bound || ipv6.Bound
|
||||
option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound
|
||||
option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound
|
||||
option.IPv4.Available = ipv4.Reason != docker_guard.ReasonCommandMissing
|
||||
option.IPv6.Available = ipv6.Reason != docker_guard.ReasonCommandMissing
|
||||
option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason
|
||||
if name == constant.FirewallProviderNftables && dockerInstalled && !dockerNftablesSupported(dockerVersion) {
|
||||
option.Supported = false
|
||||
option.SupportReason = "docker_version_unsupported"
|
||||
option.Active = false
|
||||
}
|
||||
if option.Active {
|
||||
guard := newDockerFirewallRuntime(ctx, name)
|
||||
for _, family := range families {
|
||||
status := guard.Status(family)
|
||||
option.Initialized = option.Initialized || status.Initialized
|
||||
option.Bound = option.Bound || status.Bound
|
||||
info := dto.FirewallBackendFamilyStatus{
|
||||
Available: status.Reason != dockerfirewall.ReasonCommandMissing,
|
||||
Initialized: status.Initialized, Bound: status.Bound, Reason: status.Reason,
|
||||
}
|
||||
if family == constant.FirewallFamilyIPv4 {
|
||||
option.IPv4 = info
|
||||
} else {
|
||||
option.IPv6 = info
|
||||
}
|
||||
}
|
||||
}
|
||||
result.Docker.Options = append(result.Docker.Options, option)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func loadSystemFirewallFamilyStatus(provider, family string) (bool, bool, error) {
|
||||
switch provider {
|
||||
case constant.FirewallProviderIptables:
|
||||
return iptables_helper.LoadFamilyInitStatus(family, "base")
|
||||
case constant.FirewallProviderNftables:
|
||||
return nftables_helper.LoadFamilyInitStatus(filter.Family(family), "base")
|
||||
default:
|
||||
return false, false, fmt.Errorf("unsupported firewall provider %q", provider)
|
||||
result.PortWhitelist, err = loadPortWhitelistSetting()
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
|
||||
func loadSystemFirewallFamilyInfo(provider, family string) dto.FirewallBackendFamilyStatus {
|
||||
if provider == constant.FirewallProviderIptables && family == constant.FirewallFamilyIPv6 {
|
||||
commands, err := lifecycle.ResolveIptablesCommands()
|
||||
if err != nil || !commands.IPv6Available() {
|
||||
return dto.FirewallBackendFamilyStatus{Reason: docker_guard.ReasonCommandMissing}
|
||||
}
|
||||
}
|
||||
initialized, bound, err := loadSystemFirewallFamilyStatus(provider, family)
|
||||
return dto.FirewallBackendFamilyStatus{
|
||||
Available: err == nil,
|
||||
Initialized: initialized,
|
||||
Bound: bound,
|
||||
result.PanelPort = LoadPanelPort()
|
||||
sshPort, sshErr := loadSSHWhitelistPortFrom(sshPath)
|
||||
if sshErr != nil {
|
||||
global.LOG.Warnf("load SSH port for firewall settings: %v", sshErr)
|
||||
} else {
|
||||
result.SSHPort = sshPort
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||
if request.Operation == "cleanup" {
|
||||
_, err := newFirewallService().Reset(ctx, dto.FirewallRuleReset{Subsystem: request.Subsystem, Provider: filter.Provider(request.Backend)})
|
||||
return err
|
||||
}
|
||||
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer firewallLifecycleTaskMu.Unlock()
|
||||
if request.Subsystem != "system" && request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
|
||||
return fmt.Errorf("%s only supports iptables or nftables", request.Subsystem)
|
||||
}
|
||||
if request.Subsystem == "system" && !supportsManagedFilterChains(request.Backend) && request.Operation != "select" {
|
||||
if request.Subsystem == "system" && (request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables) && request.Operation != "select" {
|
||||
return fmt.Errorf("%s does not support initialization or cleanup", request.Backend)
|
||||
}
|
||||
switch request.Subsystem {
|
||||
case "system":
|
||||
return s.operateSystem(request)
|
||||
if err := s.operateSystem(request); err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "initialize" {
|
||||
service := newFirewallService()
|
||||
whitelistErr := service.SyncPortWhitelist(ctx)
|
||||
return whitelistErr
|
||||
}
|
||||
return nil
|
||||
case "forwarding":
|
||||
return s.operateForwarding(request)
|
||||
return s.operateForwarding(ctx, request)
|
||||
case "docker":
|
||||
return s.operateDocker(ctx, request)
|
||||
default:
|
||||
@@ -195,64 +307,224 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||
guard := docker_guard.NewRuntime(request.Backend)
|
||||
if request.Operation == "cleanup" {
|
||||
if err := guard.Cleanup(); err != nil {
|
||||
func (s *FirewallSettingService) OperateFamily(request dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error) {
|
||||
if request.Family != constant.FirewallFamilyIPv4 && request.Family != constant.FirewallFamilyIPv6 {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope
|
||||
}
|
||||
if request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrUnsupportedScope
|
||||
}
|
||||
if request.Operation != "initialize" && request.Operation != "repair" && request.Operation != "bind" {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrRuleOperation
|
||||
}
|
||||
subsystem := ""
|
||||
switch request.Subsystem {
|
||||
case "system":
|
||||
subsystem = firewallTaskHost
|
||||
case "forwarding":
|
||||
subsystem = firewallTaskForwarding
|
||||
case "docker":
|
||||
subsystem = firewallTaskDocker
|
||||
default:
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope
|
||||
}
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
return queueFirewallRuleTask(subsystem, task.TaskExec, "", nil, func(t *task.Task) error {
|
||||
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
|
||||
}
|
||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||
if request.Operation == "select" {
|
||||
current := previous
|
||||
if current == "" {
|
||||
current = alternateDirectBackend(request.Backend)
|
||||
}
|
||||
initialized, err := dockerGuardBackendInitialized(current)
|
||||
defer firewallLifecycleTaskMu.Unlock()
|
||||
t.Logf("backend=%s family=%s operation=%s", request.Backend, request.Family, request.Operation)
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current != request.Backend && initialized {
|
||||
return fmt.Errorf("clean up the current Docker firewall backend %s before switching to %s", current, request.Backend)
|
||||
if !slices.Contains(families, request.Family) {
|
||||
return fmt.Errorf("IPv6 firewall support is disabled")
|
||||
}
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "initialize" {
|
||||
if err := newDockerPortGuardService().Operate(ctx, dto.DockerPortGuardOperation{Operation: "initialize"}); err != nil {
|
||||
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
|
||||
return err
|
||||
initialize := request.Operation != "bind"
|
||||
switch request.Subsystem {
|
||||
case "system":
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
if err := newFirewallService().checkSelectedProvider(t.TaskCtx, filter.Provider(request.Backend)); err != nil {
|
||||
return err
|
||||
}
|
||||
ports, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
required, err := firewall.RequiredPortWhitelist(ports)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
firewallRuleMutationMu.Lock()
|
||||
if request.Backend == constant.FirewallProviderIptables {
|
||||
err = iptables_helper.OperateFamily(request.Family, initialize, required)
|
||||
} else {
|
||||
err = nftables_helper.OperateFamily(filter.Family(request.Family), initialize, required)
|
||||
}
|
||||
firewallRuleMutationMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if initialize {
|
||||
if err := newFirewallService().applyPortWhitelist(t.TaskCtx, ports, nil, filter.Family(request.Family)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return settingRepo.UpdateOrCreate("IptablesStatus", constant.StatusEnable)
|
||||
case "forwarding":
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
manager, err := newForwardingAdapter(t.TaskCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if manager.Name() != request.Backend {
|
||||
return filter.ErrProviderUnavailable
|
||||
}
|
||||
if err := manager.OperateFamily(request.Family, initialize); err != nil {
|
||||
return err
|
||||
}
|
||||
rules, err := manager.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := persistForwardingRules(manager, rules); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
|
||||
default:
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
runtime, backend, err := newDockerPortGuardService().runtimeForDocker(t.TaskCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if backend != request.Backend {
|
||||
return filter.ErrProviderUnavailable
|
||||
}
|
||||
if err := runtime.OperateFamily(request.Family, initialize); err != nil {
|
||||
return err
|
||||
}
|
||||
inventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := persistDockerRules(backend, inventory); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func dockerGuardBackendInitialized(backend string) (bool, error) {
|
||||
guard := docker_guard.NewRuntime(backend)
|
||||
for _, family := range []string{docker_guard.FamilyIPv4, docker_guard.FamilyIPv6} {
|
||||
initialized, err := guard.Initialized(family)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if initialized {
|
||||
return true, nil
|
||||
}
|
||||
func (s *FirewallSettingService) OperateIPv6(request dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error) {
|
||||
if request.Status != constant.StatusEnable && request.Status != constant.StatusDisable {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule
|
||||
}
|
||||
return false, nil
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
return queueFirewallRuleTask(firewallTaskHost, task.TaskExec, "", nil, func(t *task.Task) error {
|
||||
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer firewallLifecycleTaskMu.Unlock()
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
if request.Status == constant.StatusEnable {
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallIPv6SupportKey, request.Status)
|
||||
}
|
||||
ports, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(ipv4PortWhitelist(ports))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
installed := lifecycle.InstalledProviders()
|
||||
for _, selection := range []struct{ subsystem, key string }{
|
||||
{"system", constant.FirewallSystemBackendKey},
|
||||
{"forwarding", constant.FirewallForwardingBackendKey},
|
||||
{"docker", constant.FirewallDockerBackendKey},
|
||||
} {
|
||||
backend, err := settingRepo.GetValueByKey(selection.key)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
backend = strings.ToLower(strings.TrimSpace(backend))
|
||||
if backend == "" {
|
||||
if selection.subsystem == "system" {
|
||||
if len(installed) == 0 {
|
||||
continue
|
||||
}
|
||||
client, err := NewSelectedSystemFirewallClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backend = client.Name()
|
||||
} else {
|
||||
backend = constant.FirewallProviderIptables
|
||||
}
|
||||
}
|
||||
if !slices.Contains(installed, backend) || (backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables) {
|
||||
continue
|
||||
}
|
||||
t.Logf("disable IPv6 firewall bindings: subsystem=%s backend=%s", selection.subsystem, backend)
|
||||
switch selection.subsystem {
|
||||
case "system":
|
||||
if backend == constant.FirewallProviderIptables {
|
||||
err = iptables_helper.UnbindIPv6BaseChains()
|
||||
} else {
|
||||
err = nftables_helper.SetTableDormant(t.TaskCtx, "ip6", nftables_helper.TableName)
|
||||
if err == nil {
|
||||
err = nftables_helper.PersistRuleset(t.TaskCtx)
|
||||
}
|
||||
}
|
||||
case "forwarding":
|
||||
var manager forwarding.Adapter
|
||||
manager, err = newForwardingAdapterFor(t.TaskCtx, backend)
|
||||
if err == nil {
|
||||
err = manager.UnbindFamily(constant.FirewallFamilyIPv6)
|
||||
}
|
||||
case "docker":
|
||||
err = newDockerFirewallRuntime(t.TaskCtx, backend).Unbind(constant.FirewallFamilyIPv6)
|
||||
}
|
||||
if err != nil && !errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := t.TaskCtx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateValues(map[string]string{constant.FirewallIPv6SupportKey: request.Status, constant.FirewallPortWhiteList: string(value)})
|
||||
})
|
||||
}
|
||||
|
||||
func NewIFirewallSettingService() IFirewallSettingService {
|
||||
return &FirewallSettingService{}
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperation) error {
|
||||
if _, err := lifecycle.NewClientFor(request.Backend); err != nil {
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
if _, err := lifecycle.NewClient(request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "cleanup" {
|
||||
return cleanupSystemBackend(request.Backend)
|
||||
}
|
||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
||||
if previous == "" {
|
||||
if client, err := lifecycle.NewClient(); err == nil {
|
||||
if client, err := lifecycle.NewClient(""); err == nil {
|
||||
previous = client.Name()
|
||||
}
|
||||
}
|
||||
@@ -262,7 +534,7 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
|
||||
return err
|
||||
}
|
||||
if initialized {
|
||||
return fmt.Errorf("clean up the current system firewall backend %s before switching to %s", previous, request.Backend)
|
||||
return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": previous, "target": request.Backend}, nil)
|
||||
}
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, request.Backend); err != nil {
|
||||
@@ -278,7 +550,7 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
|
||||
if request.Operation == "select" {
|
||||
return nil
|
||||
}
|
||||
initErr := newFirewallService().OperateFilterChain(dto.FilterChainOperation{
|
||||
initErr := newFirewallService().operateFilterChainBaseLocked(request.Backend, dto.FilterChainOperation{
|
||||
Name: constant.FirewallBasicChain, Operate: string(firewall.BaseOperationInit),
|
||||
})
|
||||
if initErr != nil {
|
||||
@@ -288,9 +560,19 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
|
||||
}
|
||||
|
||||
func systemFirewallBackendInitialized(backend string) (bool, error) {
|
||||
if supportsManagedFilterChains(backend) {
|
||||
client, err := lifecycle.NewClient(backend)
|
||||
if err != nil {
|
||||
if errors.Is(err, lifecycle.ErrNotInstalled) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if backend == constant.FirewallProviderIptables || backend == constant.FirewallProviderNftables {
|
||||
for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} {
|
||||
initialized, _, err := loadSystemFirewallFamilyStatus(backend, family)
|
||||
if family == constant.FirewallFamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -300,80 +582,42 @@ func systemFirewallBackendInitialized(backend string) (bool, error) {
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
client, err := lifecycle.NewClientFor(backend)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return client.Status()
|
||||
}
|
||||
|
||||
func cleanupSystemBackend(backend string) error {
|
||||
switch backend {
|
||||
case constant.FirewallProviderIptables:
|
||||
return newIptablesHelperManager().Cleanup()
|
||||
case constant.FirewallProviderNftables:
|
||||
return newNftablesHelperManager().Cleanup()
|
||||
default:
|
||||
return fmt.Errorf("cleanup is only available for 1Panel-owned iptables and nftables resources")
|
||||
}
|
||||
}
|
||||
|
||||
func cleanupInactiveSystemBackend(backend string) error {
|
||||
switch backend {
|
||||
case constant.FirewallProviderIptables:
|
||||
return (&iptables_helper.Manager{}).Cleanup()
|
||||
case constant.FirewallProviderNftables:
|
||||
return (&nftables_helper.Manager{}).Cleanup()
|
||||
default:
|
||||
return fmt.Errorf("cleanup is only available for 1Panel-owned iptables and nftables resources")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOperation) error {
|
||||
manager, err := newForwardingManagerFor(request.Backend)
|
||||
if err != nil {
|
||||
func (s *FirewallSettingService) operateForwarding(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||
if _, err := newForwardingAdapterFor(ctx, request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "cleanup" {
|
||||
if err := manager.Cleanup(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusDisable); err != nil {
|
||||
return err
|
||||
}
|
||||
recordForwardingSyncError(nil)
|
||||
return nil
|
||||
}
|
||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||
if request.Operation == "select" {
|
||||
current := previous
|
||||
if current == "" {
|
||||
detected, err := newForwardingManager()
|
||||
detected, err := newForwardingAdapter(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current = detected.Name()
|
||||
}
|
||||
initialized, err := forwardingBackendInitialized(current)
|
||||
initialized, err := forwardingBackendInitialized(ctx, current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current != request.Backend && initialized {
|
||||
return fmt.Errorf("clean up the current forwarding backend %s before switching to %s", current, request.Backend)
|
||||
return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": current, "target": request.Backend}, nil)
|
||||
}
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "initialize" {
|
||||
return newForwardingService().Enable()
|
||||
return newForwardingService().Enable(ctx)
|
||||
}
|
||||
recordForwardingSyncError(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func forwardingBackendInitialized(backend string) (bool, error) {
|
||||
manager, err := newForwardingManagerFor(backend)
|
||||
func forwardingBackendInitialized(ctx context.Context, backend string) (bool, error) {
|
||||
manager, err := newForwardingAdapterFor(ctx, backend)
|
||||
if err != nil {
|
||||
if errors.Is(err, lifecycle.ErrNotInstalled) {
|
||||
return false, nil
|
||||
@@ -392,9 +636,53 @@ func forwardingBackendInitialized(backend string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func alternateDirectBackend(backend string) string {
|
||||
if backend == constant.FirewallProviderNftables {
|
||||
return constant.FirewallProviderIptables
|
||||
func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||
|
||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||
if request.Operation == "select" {
|
||||
current := previous
|
||||
if current == "" {
|
||||
current = constant.FirewallProviderNftables
|
||||
if request.Backend == constant.FirewallProviderNftables {
|
||||
current = constant.FirewallProviderIptables
|
||||
}
|
||||
}
|
||||
initialized, err := dockerGuardBackendInitialized(ctx, current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current != request.Backend && initialized {
|
||||
return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": current, "target": request.Backend}, nil)
|
||||
}
|
||||
}
|
||||
return constant.FirewallProviderNftables
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "select" {
|
||||
if err := (&DockerService{}).UpdateFirewallBackend(request.Backend); err != nil {
|
||||
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if request.Operation == "initialize" {
|
||||
if err := newDockerPortGuardService().Operate(ctx, dto.DockerPortGuardOperation{Operation: "initialize"}); err != nil {
|
||||
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dockerGuardBackendInitialized(ctx context.Context, backend string) (bool, error) {
|
||||
guard := newDockerFirewallRuntime(ctx, backend)
|
||||
for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} {
|
||||
initialized, err := guard.Initialized(family)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if initialized {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+228
-539
@@ -4,641 +4,330 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||
forwardingproviders "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding/providers"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
|
||||
)
|
||||
|
||||
type IForwardingService interface {
|
||||
LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
|
||||
SearchRules(request dto.ForwardRuleSearch) (int64, interface{}, error)
|
||||
OperateRules(request dto.ForwardRuleOperate) error
|
||||
Enable() error
|
||||
LoadBaseInfo(ctx context.Context) (dto.FirewallSubsystemStatus, error)
|
||||
ExportBackup(context.Context, filter.Provider) (dto.FirewallSubsystemBackup, error)
|
||||
SearchRules(ctx context.Context, request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error)
|
||||
OperateRules(dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error)
|
||||
Enable(ctx context.Context) error
|
||||
QueueInitialization(dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error)
|
||||
Restore(context.Context) error
|
||||
}
|
||||
|
||||
type ForwardingService struct {
|
||||
managerFactory func() (*forwarding.Manager, error)
|
||||
rules repo.IForwardingRuleRepo
|
||||
enabled func() (bool, error)
|
||||
persistBackend func(string) error
|
||||
markEnabled func() error
|
||||
installedProviders func() []string
|
||||
clientFactory func(context.Context) (forwarding.Adapter, error)
|
||||
}
|
||||
|
||||
type forwardingCandidate struct {
|
||||
adapter forwarding.Adapter
|
||||
runtime forwarding.RuntimeClient
|
||||
}
|
||||
|
||||
var errForwardingBackendConflict = errors.New("iptables and nftables forwarding backends are both initialized; remove one before continuing")
|
||||
var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected")
|
||||
|
||||
var forwardingMutationMu sync.Mutex
|
||||
|
||||
const (
|
||||
forwardingSyncConverged = "converged"
|
||||
forwardingSyncMissing = "missing"
|
||||
forwardingSyncRuntimeOnly = "runtime_only"
|
||||
)
|
||||
|
||||
var (
|
||||
forwardingSyncStateMu sync.RWMutex
|
||||
forwardingLastSyncErr error
|
||||
)
|
||||
|
||||
func NewIForwardingService() IForwardingService {
|
||||
return newForwardingService()
|
||||
}
|
||||
|
||||
func newForwardingService() *ForwardingService {
|
||||
return &ForwardingService{
|
||||
managerFactory: newForwardingManager,
|
||||
rules: repo.NewIForwardingRuleRepo(),
|
||||
enabled: forwardingPersistedEnabled,
|
||||
markEnabled: func() error {
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
|
||||
},
|
||||
persistBackend: func(backend string) error {
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, backend)
|
||||
},
|
||||
installedProviders: lifecycle.InstalledProviders,
|
||||
func (s *ForwardingService) LoadBaseInfo(ctx context.Context) (dto.FirewallSubsystemStatus, error) {
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return dto.FirewallSubsystemStatus{}, err
|
||||
}
|
||||
selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||
selected = strings.TrimSpace(selected)
|
||||
if selected == "" {
|
||||
selected = constant.FirewallProviderIptables
|
||||
}
|
||||
}
|
||||
|
||||
type forwardingRuleSyncCandidate struct {
|
||||
rule forwarding.Rule
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) {
|
||||
baseInfo := dto.FirewallSubsystemStatus{
|
||||
Version: "-", Name: "-", Backend: "-", SyncError: lastForwardingSyncError(),
|
||||
Version: "-", Name: selected, Backend: selected, IPv6Enabled: len(families) > 1,
|
||||
}
|
||||
manager, err := s.manager()
|
||||
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
|
||||
baseInfo.Name += "-forward"
|
||||
}
|
||||
manager, err := s.clientFactory(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, errForwardingBackendUnavailable) {
|
||||
loadInstalled := s.installedProviders
|
||||
if loadInstalled == nil {
|
||||
loadInstalled = lifecycle.InstalledProviders
|
||||
}
|
||||
for _, provider := range loadInstalled() {
|
||||
if provider == constant.FirewallProviderIptables || provider == constant.FirewallProviderNftables {
|
||||
baseInfo.IsExist = true
|
||||
baseInfo.Message = err.Error()
|
||||
break
|
||||
}
|
||||
}
|
||||
baseInfo.Reason = constant.FirewallBackendNotInstalled
|
||||
return baseInfo, nil
|
||||
}
|
||||
return baseInfo, err
|
||||
}
|
||||
status, err := manager.Status()
|
||||
client, err := lifecycle.NewClient(manager.Name())
|
||||
if err != nil {
|
||||
return baseInfo, err
|
||||
}
|
||||
version, versionErr := client.Version()
|
||||
status, statusErr := loadForwardingFirewallOverview(manager, families)
|
||||
if err := errors.Join(versionErr, statusErr); err != nil {
|
||||
return baseInfo, err
|
||||
}
|
||||
baseInfo.IsExist = true
|
||||
baseInfo.Name, baseInfo.Backend = forwardingDisplayName(status.Name), status.Name
|
||||
baseInfo.Version = status.Version
|
||||
baseInfo.PingStatus = ping.LoadStatus()
|
||||
baseInfo.Name, baseInfo.Backend = manager.Name(), manager.Name()
|
||||
if baseInfo.Backend == constant.FirewallProviderIptables || baseInfo.Backend == constant.FirewallProviderNftables {
|
||||
baseInfo.Name += "-forward"
|
||||
}
|
||||
baseInfo.Version = version
|
||||
baseInfo.PingStatus = firewall.LoadPingStatus()
|
||||
baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind
|
||||
baseInfo.IPv4 = loadForwardingFamilyInfo(manager, status.Name, constant.FirewallFamilyIPv4)
|
||||
baseInfo.IPv6 = loadForwardingFamilyInfo(manager, status.Name, constant.FirewallFamilyIPv6)
|
||||
baseInfo.IPv4, baseInfo.IPv6 = status.IPv4, status.IPv6
|
||||
for _, family := range []struct {
|
||||
command string
|
||||
status *dto.FirewallBackendFamilyStatus
|
||||
}{
|
||||
{"iptables", &baseInfo.IPv4},
|
||||
{"ip6tables", &baseInfo.IPv6},
|
||||
} {
|
||||
if family.command == "ip6tables" && !baseInfo.IPv6Enabled {
|
||||
continue
|
||||
}
|
||||
policy, err := loadForwardPolicy(ctx, family.command)
|
||||
if err != nil {
|
||||
global.LOG.Warnf("inspect %s FORWARD policy: %v", family.command, err)
|
||||
continue
|
||||
}
|
||||
family.status.ForwardPolicy = policy
|
||||
}
|
||||
return baseInfo, nil
|
||||
}
|
||||
|
||||
func loadForwardingFamilyInfo(manager *forwarding.Manager, backend, family string) dto.FirewallBackendFamilyStatus {
|
||||
initialized, bound, err := manager.FamilyStatus(family)
|
||||
available := err == nil
|
||||
if backend == constant.FirewallProviderIptables && family == constant.FirewallFamilyIPv6 {
|
||||
commands, commandErr := lifecycle.ResolveIptablesCommands()
|
||||
available = available && commandErr == nil && commands.IPv6Available()
|
||||
func (s *ForwardingService) ExportBackup(ctx context.Context, provider filter.Provider) (dto.FirewallSubsystemBackup, error) {
|
||||
var manager forwarding.Adapter
|
||||
var err error
|
||||
if provider == "" {
|
||||
manager, err = s.clientFactory(ctx)
|
||||
} else {
|
||||
manager, err = newForwardingAdapterFor(ctx, string(provider))
|
||||
}
|
||||
return dto.FirewallBackendFamilyStatus{Available: available, Initialized: initialized, Bound: bound}
|
||||
if err != nil {
|
||||
return dto.FirewallSubsystemBackup{}, err
|
||||
}
|
||||
rules, err := manager.List()
|
||||
if err != nil {
|
||||
return dto.FirewallSubsystemBackup{}, err
|
||||
}
|
||||
return forwardingBackup(manager, rules)
|
||||
}
|
||||
|
||||
func forwardingDisplayName(backend string) string {
|
||||
switch backend {
|
||||
case constant.FirewallProviderIptables, constant.FirewallProviderNftables:
|
||||
return backend + "-forward"
|
||||
default:
|
||||
return backend
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, interface{}, error) {
|
||||
if request.Strategy != "" {
|
||||
return 0, nil, nil
|
||||
}
|
||||
stored, err := s.rules.List(context.Background())
|
||||
func (s *ForwardingService) SearchRules(ctx context.Context, request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) {
|
||||
manager, err := s.clientFactory(ctx)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
manager, err := s.manager()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
runtime, err := manager.List("", "")
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
inventory, err := mergeForwardingInventory(stored, runtime)
|
||||
rules, err := manager.List()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
items := make([]dto.ForwardRule, 0, len(rules))
|
||||
keyword := strings.ToLower(strings.TrimSpace(request.Info))
|
||||
filtered := inventory[:0]
|
||||
for _, item := range inventory {
|
||||
if keyword == "" || forwardingRuleMatchesKeyword(item, keyword) {
|
||||
filtered = append(filtered, item)
|
||||
for _, rule := range rules {
|
||||
if keyword != "" && !strings.Contains(strings.ToLower(strings.Join([]string{rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort, rule.Interface}, " ")), keyword) {
|
||||
continue
|
||||
}
|
||||
items = append(items, dto.ForwardRule{Num: strconv.Itoa(len(items) + 1), Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface})
|
||||
}
|
||||
inventory = filtered
|
||||
total := len(inventory)
|
||||
start, end := (request.Page-1)*request.PageSize, request.Page*request.PageSize
|
||||
if start > total {
|
||||
return int64(total), make([]dto.ForwardRule, 0), nil
|
||||
total := len(items)
|
||||
if request.All {
|
||||
return int64(total), items, nil
|
||||
}
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
pageRules := inventory[start:end]
|
||||
var items []dto.ForwardRule
|
||||
if pageRules != nil {
|
||||
items = make([]dto.ForwardRule, 0, len(pageRules))
|
||||
}
|
||||
for index, item := range pageRules {
|
||||
items = append(items, dto.ForwardRule{
|
||||
ID: item.ID,
|
||||
Num: strconv.Itoa(start + index + 1),
|
||||
Family: item.Rule.Family,
|
||||
Protocol: item.Rule.Protocol,
|
||||
Port: item.Rule.Port,
|
||||
TargetIP: item.Rule.TargetIP,
|
||||
TargetPort: item.Rule.TargetPort,
|
||||
Interface: item.Rule.Interface,
|
||||
IsDesired: item.IsDesired,
|
||||
IsRuntime: item.IsRuntime,
|
||||
SyncStatus: item.SyncStatus(),
|
||||
})
|
||||
}
|
||||
return int64(total), items, nil
|
||||
start := min(max(request.Page-1, 0)*max(request.PageSize, 1), total)
|
||||
end := min(start+max(request.PageSize, 1), total)
|
||||
return int64(total), items[start:end], nil
|
||||
}
|
||||
|
||||
func forwardingRuleMatchesKeyword(item forwardingInventoryItem, keyword string) bool {
|
||||
values := []string{
|
||||
item.Rule.Family, item.Rule.Protocol, item.Rule.Port, item.Rule.TargetIP,
|
||||
item.Rule.TargetPort, item.Rule.Interface, item.SyncStatus(),
|
||||
}
|
||||
for _, value := range values {
|
||||
if strings.Contains(strings.ToLower(value), keyword) {
|
||||
return true
|
||||
func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) {
|
||||
count := 0
|
||||
for _, rule := range request.Rules {
|
||||
if rule.Operation == "add" {
|
||||
count += strings.Count(rule.Protocol, "/") + 1
|
||||
}
|
||||
if count > filter.MaxAtomicExpansion {
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
|
||||
}
|
||||
}
|
||||
return false
|
||||
operation := task.TaskCreate
|
||||
for _, rule := range request.Rules {
|
||||
if rule.Operation != "add" {
|
||||
operation = task.TaskUpdate
|
||||
}
|
||||
}
|
||||
if forwardingOperationsOnlyRemove(request.Rules) {
|
||||
operation = task.TaskDelete
|
||||
}
|
||||
taskItem, err := task.NewTask(firewallTaskName(operation, firewallTaskForwarding, ""), operation, task.TaskScopeFirewall, "", 0)
|
||||
if err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
|
||||
return s.operateRules(t.TaskCtx, request, t, false)
|
||||
}, nil, 0, 0)
|
||||
if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil {
|
||||
taskItem.LogFailedWithErr(taskItem.Name, err)
|
||||
closeUnstartedFirewallTask(taskItem)
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
go func() { _ = taskItem.Execute() }()
|
||||
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
|
||||
}
|
||||
|
||||
func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) error {
|
||||
func (s *ForwardingService) Enable(ctx context.Context) error {
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
ctx := context.Background()
|
||||
stored, err := s.rules.List(ctx)
|
||||
manager, err := s.clientFactory(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
desired, err := applyForwardingOperations(forwardingRulesFromModels(stored), request.Rules)
|
||||
if errors.Is(err, forwarding.ErrRuleExists) {
|
||||
return buserr.New("ErrRecordExist")
|
||||
} else if err != nil {
|
||||
rules, err := manager.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.rules.ReplaceAll(ctx, forwardingRuleModels(desired)); err != nil {
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.reconcile(desired); err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
if request.ForceDelete && forwardingOperationsOnlyRemove(request.Rules) {
|
||||
if global.LOG != nil {
|
||||
global.LOG.Error(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := initializeForwarding(manager, families); err != nil {
|
||||
return err
|
||||
}
|
||||
recordForwardingSyncError(nil)
|
||||
return nil
|
||||
return persistForwardingRules(manager, rules)
|
||||
}
|
||||
|
||||
func (s *ForwardingService) Enable() error {
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
manager, err := s.manager()
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
func (s *ForwardingService) QueueInitialization(request dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error) {
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
if err := s.persistForwardingEnabled(); err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
var backup dto.FirewallSubsystemBackup
|
||||
if request.BackupFile != "" {
|
||||
var err error
|
||||
backup, err = readFirewallSubsystemBackup(request.BackupFile, "forwarding")
|
||||
if err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
}
|
||||
if err := s.activateManager(manager); err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
operations := dto.ForwardRuleOperate{Rules: make([]dto.ForwardRuleOperation, 0, len(backup.Forwarding))}
|
||||
for _, rule := range backup.Forwarding {
|
||||
operations.Rules = append(operations.Rules, dto.ForwardRuleOperation{Operation: "add", Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP, TargetPort: rule.TargetPort, Interface: rule.Interface})
|
||||
}
|
||||
rules, err := s.rules.List(context.Background())
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
err = manager.Reconcile(forwardingRulesFromModels(rules))
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
return queueFirewallRuleTask(firewallTaskForwarding, task.TaskExec, request.TaskID, nil, func(t *task.Task) error {
|
||||
return s.operateRules(t.TaskCtx, operations, t, true, backup.Families...)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ForwardingService) Restore(ctx context.Context) error {
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
enabled, err := s.forwardingEnabled()
|
||||
if err != nil || !enabled {
|
||||
status, err := settingRepo.GetValueByKey(constant.FirewallForwardingInitializedKey)
|
||||
if err != nil || status != constant.StatusEnable {
|
||||
return err
|
||||
}
|
||||
manager, err := s.clientFactory(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backup, err := readFirewallSubsystemBackup("forwarding-"+manager.Name()+".rules", "forwarding")
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !slices.Contains(families, constant.FirewallFamilyIPv6) {
|
||||
before := len(backup.Forwarding)
|
||||
backup.Forwarding = slices.DeleteFunc(backup.Forwarding, func(rule forwarding.Rule) bool { return rule.Family == constant.FirewallFamilyIPv6 })
|
||||
logFirewallIPv6Skipped(nil, "forwarding startup", before-len(backup.Forwarding))
|
||||
}
|
||||
missing := make(map[string]bool)
|
||||
needsBind := false
|
||||
for _, family := range families {
|
||||
initialized, bound, err := manager.FamilyStatus(family)
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
missing[family] = !initialized
|
||||
needsBind = needsBind || (initialized && !bound)
|
||||
}
|
||||
restoreIPv6 := slices.Contains(backup.Families, forwarding.FamilyIPv6)
|
||||
for _, rule := range backup.Forwarding {
|
||||
restoreIPv6 = restoreIPv6 || rule.Family == forwarding.FamilyIPv6
|
||||
}
|
||||
if !missing[forwarding.FamilyIPv4] && (!missing[forwarding.FamilyIPv6] || !restoreIPv6) && !needsBind {
|
||||
return nil
|
||||
}
|
||||
current, err := manager.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manager, err := s.manager()
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
seen := make(map[string]int, len(current))
|
||||
for _, rule := range current {
|
||||
seen[rule.Identity()]++
|
||||
}
|
||||
stored, err := s.rules.List(ctx)
|
||||
if err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
if err := s.activateManager(manager); err != nil {
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
err = manager.Reconcile(forwardingRulesFromModels(stored))
|
||||
recordForwardingSyncError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ForwardingService) loadRuleSyncCandidates(
|
||||
ctx context.Context,
|
||||
targetProvider filter.Provider,
|
||||
) (*forwarding.Manager, []forwardingRuleSyncCandidate, []forwarding.Rule, bool, error) {
|
||||
target, err := s.manager()
|
||||
if err != nil {
|
||||
return nil, nil, nil, false, err
|
||||
}
|
||||
if target.Name() != string(targetProvider) {
|
||||
return nil, nil, nil, false, fmt.Errorf(
|
||||
"%w: selected forwarding backend is %s, requested target is %s",
|
||||
filter.ErrProviderUnavailable, target.Name(), targetProvider,
|
||||
)
|
||||
}
|
||||
stored, err := s.rules.List(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, nil, false, err
|
||||
}
|
||||
candidates := make([]forwardingRuleSyncCandidate, 0, len(stored))
|
||||
for _, record := range stored {
|
||||
rule := forwarding.Rule{
|
||||
Family: record.Family, Protocol: record.Protocol, Port: record.Port, TargetIP: record.TargetIP,
|
||||
TargetPort: record.TargetPort, Interface: record.Interface,
|
||||
for _, family := range families {
|
||||
if family == forwarding.FamilyIPv6 && missing[family] && !restoreIPv6 {
|
||||
continue
|
||||
}
|
||||
normalized, normalizeErr := forwarding.NormalizeRule(rule)
|
||||
candidates = append(candidates, forwardingRuleSyncCandidate{rule: normalized, err: normalizeErr})
|
||||
}
|
||||
targetStatus, err := target.Status()
|
||||
if err != nil {
|
||||
return nil, nil, nil, false, err
|
||||
}
|
||||
targetRules := make([]forwarding.Rule, 0)
|
||||
if targetStatus.IsInit {
|
||||
targetRules, err = target.List("", "")
|
||||
if err != nil {
|
||||
return nil, nil, nil, false, err
|
||||
}
|
||||
targetRules, err = normalizeForwardingRuntimeRules(targetRules)
|
||||
if err != nil {
|
||||
return nil, nil, nil, false, err
|
||||
if err := manager.OperateFamily(family, missing[family]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return target, candidates, targetRules, targetStatus.IsInit, nil
|
||||
}
|
||||
|
||||
func verifyForwardingRuleSync(target *forwarding.Manager, desired []forwarding.Rule) error {
|
||||
actual, err := target.List("", "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify synchronized forwarding rules: %w", err)
|
||||
}
|
||||
actual, err = normalizeForwardingRuntimeRules(actual)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify synchronized forwarding rules: %w", err)
|
||||
}
|
||||
if !databaseSyncStatesEqual(actual, desired, func(rule forwarding.Rule) string { return rule.Identity() }) {
|
||||
return fmt.Errorf("verify synchronized forwarding rules: target rules do not match the database")
|
||||
for _, family := range families {
|
||||
if !missing[family] {
|
||||
continue
|
||||
}
|
||||
rules := make([]forwarding.Rule, 0)
|
||||
for _, rule := range backup.Forwarding {
|
||||
if rule.Family != family {
|
||||
continue
|
||||
}
|
||||
if seen[rule.Identity()] > 0 {
|
||||
seen[rule.Identity()]--
|
||||
continue
|
||||
}
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
if err := manager.CreateRules(ctx, rules); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeForwardingRuntimeRules(rules []forwarding.Rule) ([]forwarding.Rule, error) {
|
||||
normalized := make([]forwarding.Rule, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
item, err := forwarding.NormalizeRule(rule)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("normalize target forwarding rule %s: %w", rule.Identity(), err)
|
||||
func NewIForwardingService() IForwardingService {
|
||||
return newForwardingService()
|
||||
}
|
||||
|
||||
func loadForwardPolicy(ctx context.Context, command string) (string, error) {
|
||||
if !cmd.Which(command) {
|
||||
command += "-nft"
|
||||
if !cmd.Which(command) {
|
||||
return "", nil
|
||||
}
|
||||
normalized = append(normalized, item)
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func forwardingRuleSyncDTO(rule forwarding.Rule) *dto.ForwardRule {
|
||||
return &dto.ForwardRule{
|
||||
Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP,
|
||||
TargetPort: rule.TargetPort, Interface: rule.Interface,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ForwardingService) reconcile(rules []forwarding.Rule) error {
|
||||
manager, err := s.manager()
|
||||
output, err := cmd.NewCommandMgr(cmd.WithContext(ctx), cmd.WithTimeout(5*time.Second)).RunWithOptionalSudoAndStdout(command, "-t", "filter", "-w", "2", "-S", "FORWARD")
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
return s.reconcileWithManager(manager, rules)
|
||||
}
|
||||
|
||||
func (s *ForwardingService) reconcileWithManager(manager *forwarding.Manager, rules []forwarding.Rule) error {
|
||||
enabled, err := s.forwardingEnabled()
|
||||
if err != nil || !enabled {
|
||||
return err
|
||||
}
|
||||
if err := s.activateManager(manager); err != nil {
|
||||
return err
|
||||
}
|
||||
return manager.Reconcile(rules)
|
||||
}
|
||||
|
||||
func (s *ForwardingService) activateManager(manager *forwarding.Manager) error {
|
||||
if err := s.saveForwardingBackend(manager.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
return manager.Enable()
|
||||
}
|
||||
|
||||
func (s *ForwardingService) forwardingEnabled() (bool, error) {
|
||||
if s.enabled != nil {
|
||||
return s.enabled()
|
||||
}
|
||||
return forwardingPersistedEnabled()
|
||||
}
|
||||
|
||||
func (s *ForwardingService) saveForwardingBackend(backend string) error {
|
||||
if s.persistBackend != nil {
|
||||
return s.persistBackend(backend)
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, backend)
|
||||
}
|
||||
|
||||
func (s *ForwardingService) persistForwardingEnabled() error {
|
||||
if s.markEnabled != nil {
|
||||
return s.markEnabled()
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
|
||||
}
|
||||
|
||||
func forwardingPersistedEnabled() (bool, error) {
|
||||
status, err := settingRepo.GetValueByKey(constant.FirewallForwardingInitializedKey)
|
||||
return status == constant.StatusEnable, err
|
||||
}
|
||||
|
||||
func forwardingRulesFromModels(stored []model.ForwardingRule) []forwarding.Rule {
|
||||
rules := make([]forwarding.Rule, 0, len(stored))
|
||||
for _, rule := range stored {
|
||||
rules = append(rules, forwarding.Rule{
|
||||
Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP,
|
||||
TargetPort: rule.TargetPort, Interface: rule.Interface,
|
||||
})
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
func forwardingRuleModels(rules []forwarding.Rule) []model.ForwardingRule {
|
||||
stored := make([]model.ForwardingRule, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
stored = append(stored, model.ForwardingRule{
|
||||
Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP,
|
||||
TargetPort: rule.TargetPort, Interface: rule.Interface,
|
||||
})
|
||||
}
|
||||
return stored
|
||||
}
|
||||
|
||||
type forwardingInventoryItem struct {
|
||||
ID uint
|
||||
Rule forwarding.Rule
|
||||
IsDesired bool
|
||||
IsRuntime bool
|
||||
}
|
||||
|
||||
func (i forwardingInventoryItem) SyncStatus() string {
|
||||
switch {
|
||||
case i.IsDesired && i.IsRuntime:
|
||||
return forwardingSyncConverged
|
||||
case i.IsDesired:
|
||||
return forwardingSyncMissing
|
||||
default:
|
||||
return forwardingSyncRuntimeOnly
|
||||
}
|
||||
}
|
||||
|
||||
func mergeForwardingInventory(
|
||||
stored []model.ForwardingRule,
|
||||
runtime []forwarding.Rule,
|
||||
) ([]forwardingInventoryItem, error) {
|
||||
items := make([]forwardingInventoryItem, 0, len(stored)+len(runtime))
|
||||
byIdentity := make(map[string]int, len(stored)+len(runtime))
|
||||
for _, record := range stored {
|
||||
rule, err := forwarding.NormalizeRule(forwarding.Rule{
|
||||
Family: record.Family, Protocol: record.Protocol, Port: record.Port, TargetIP: record.TargetIP,
|
||||
TargetPort: record.TargetPort, Interface: record.Interface,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("normalize desired forwarding rule: %w", err)
|
||||
}
|
||||
key := rule.Identity()
|
||||
byIdentity[key] = len(items)
|
||||
items = append(items, forwardingInventoryItem{ID: record.ID, Rule: rule, IsDesired: true})
|
||||
}
|
||||
for _, observed := range runtime {
|
||||
rule, err := forwarding.NormalizeRule(observed)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("normalize runtime forwarding rule: %w", err)
|
||||
}
|
||||
key := rule.Identity()
|
||||
if index, exists := byIdentity[key]; exists {
|
||||
items[index].IsRuntime = true
|
||||
continue
|
||||
}
|
||||
byIdentity[key] = len(items)
|
||||
items = append(items, forwardingInventoryItem{Rule: rule, IsRuntime: true})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func recordForwardingSyncError(err error) {
|
||||
forwardingSyncStateMu.Lock()
|
||||
forwardingLastSyncErr = err
|
||||
forwardingSyncStateMu.Unlock()
|
||||
}
|
||||
|
||||
func lastForwardingSyncError() string {
|
||||
forwardingSyncStateMu.RLock()
|
||||
defer forwardingSyncStateMu.RUnlock()
|
||||
if forwardingLastSyncErr == nil {
|
||||
return ""
|
||||
}
|
||||
return forwardingLastSyncErr.Error()
|
||||
}
|
||||
|
||||
func applyForwardingOperations(current []forwarding.Rule, requested []dto.ForwardRuleOperation) ([]forwarding.Rule, error) {
|
||||
desired := make([]forwarding.Rule, 0, len(current)+len(requested))
|
||||
for _, rule := range current {
|
||||
normalized, err := forwarding.NormalizeRule(rule)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("normalize persisted forwarding rule: %w", err)
|
||||
}
|
||||
desired = append(desired, normalized)
|
||||
}
|
||||
for _, operation := range requested {
|
||||
for _, protocol := range strings.Split(operation.Protocol, "/") {
|
||||
rule, err := forwarding.NormalizeRule(forwarding.Rule{
|
||||
Family: operation.Family, Protocol: protocol, Port: operation.Port, TargetIP: operation.TargetIP,
|
||||
TargetPort: operation.TargetPort, Interface: operation.Interface,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
index := forwardingRuleIndex(desired, rule)
|
||||
switch forwarding.OperationType(operation.Operation) {
|
||||
case forwarding.OperationAdd:
|
||||
if index >= 0 {
|
||||
return nil, forwarding.ErrRuleExists
|
||||
}
|
||||
desired = append(desired, rule)
|
||||
case forwarding.OperationRemove:
|
||||
if index >= 0 {
|
||||
desired = append(desired[:index], desired[index+1:]...)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported forwarding operation %q", operation.Operation)
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 3 && fields[0] == "-P" && fields[1] == "FORWARD" {
|
||||
if fields[2] != "ACCEPT" && fields[2] != "DROP" {
|
||||
return "", fmt.Errorf("unexpected FORWARD policy: %s", fields[2])
|
||||
}
|
||||
return fields[2], nil
|
||||
}
|
||||
}
|
||||
return desired, nil
|
||||
}
|
||||
|
||||
func forwardingRuleIndex(rules []forwarding.Rule, wanted forwarding.Rule) int {
|
||||
wantedIdentity := wanted.Identity()
|
||||
for index, rule := range rules {
|
||||
if rule.Identity() == wantedIdentity {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func forwardingOperationsOnlyRemove(operations []dto.ForwardRuleOperation) bool {
|
||||
if len(operations) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, operation := range operations {
|
||||
if operation.Operation != string(forwarding.OperationRemove) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *ForwardingService) manager() (*forwarding.Manager, error) {
|
||||
return s.managerFactory()
|
||||
}
|
||||
|
||||
func newForwardingManager() (*forwarding.Manager, error) {
|
||||
selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||
if strings.TrimSpace(selected) == "" {
|
||||
selected = constant.FirewallProviderIptables
|
||||
}
|
||||
return newForwardingManagerFor(strings.TrimSpace(selected))
|
||||
}
|
||||
|
||||
func newForwardingManagerFor(backend string) (*forwarding.Manager, error) {
|
||||
clients, err := lifecycle.NewNetfilterClients()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", errForwardingBackendUnavailable, err)
|
||||
}
|
||||
candidates := make([]forwardingCandidate, 0, len(clients))
|
||||
for _, client := range clients {
|
||||
adapter, err := forwardingproviders.New(client.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
candidates = append(candidates, forwardingCandidate{adapter: adapter, runtime: client})
|
||||
}
|
||||
if backend != "" {
|
||||
for _, candidate := range candidates {
|
||||
if candidate.adapter.Name() == backend {
|
||||
return forwarding.NewManager(candidate.adapter, candidate.runtime), nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf(
|
||||
"%w: selected forwarding backend %s %w",
|
||||
errForwardingBackendUnavailable, backend, lifecycle.ErrNotInstalled,
|
||||
)
|
||||
}
|
||||
return selectForwardingManager(candidates)
|
||||
}
|
||||
|
||||
func selectForwardingManager(candidates []forwardingCandidate) (*forwarding.Manager, error) {
|
||||
if len(candidates) == 0 {
|
||||
return nil, errors.New("no supported forwarding backend detected")
|
||||
}
|
||||
selected := -1
|
||||
for index, candidate := range candidates {
|
||||
ipv4Initialized, _, err := candidate.adapter.FamilyStatus(forwarding.FamilyIPv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ipv6Initialized, _, err := candidate.adapter.FamilyStatus(forwarding.FamilyIPv6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
initialized := ipv4Initialized || ipv6Initialized
|
||||
if !initialized {
|
||||
continue
|
||||
}
|
||||
if selected >= 0 {
|
||||
return nil, errForwardingBackendConflict
|
||||
}
|
||||
selected = index
|
||||
}
|
||||
if selected < 0 {
|
||||
selected = 0
|
||||
}
|
||||
candidate := candidates[selected]
|
||||
return forwarding.NewManager(candidate.adapter, candidate.runtime), nil
|
||||
return "", errors.New("FORWARD default policy was not found")
|
||||
}
|
||||
|
||||
+24
-72
@@ -28,8 +28,7 @@ import (
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/moby/go-archive"
|
||||
)
|
||||
|
||||
type ImageService struct{}
|
||||
@@ -278,38 +277,37 @@ func (u *ImageService) ImagePull(req dto.ImagePull) error {
|
||||
itemName := strings.ReplaceAll(path.Base(item), ":", "_")
|
||||
taskItem.AddSubTask(i18n.GetWithName("ImagePull", itemName), func(t *task.Task) error {
|
||||
taskItem.Logf("----------------- %s -----------------", itemName)
|
||||
if req.RepoID == 0 {
|
||||
pullErr := pullImages(taskItem, client, item)
|
||||
taskItem.LogWithStatus(i18n.GetMsgByKey("TaskPull"), pullErr)
|
||||
return pullErr
|
||||
}
|
||||
|
||||
options := image.PullOptions{}
|
||||
imageName := item
|
||||
if req.RepoID == 0 {
|
||||
hasAuth, authStr := loadAuthInfo(item)
|
||||
if hasAuth {
|
||||
options.RegistryAuth = authStr
|
||||
repo, repoErr := imageRepoRepo.Get(repo.WithByID(req.RepoID))
|
||||
taskItem.LogWithStatus(i18n.GetMsgByKey("ImageRepoAuthFromDB"), repoErr)
|
||||
if repoErr != nil {
|
||||
return repoErr
|
||||
}
|
||||
if repo.Auth {
|
||||
authConfig := registry.AuthConfig{
|
||||
Username: repo.Username,
|
||||
Password: repo.Password,
|
||||
}
|
||||
} else {
|
||||
repo, err := imageRepoRepo.Get(repo.WithByID(req.RepoID))
|
||||
taskItem.LogWithStatus(i18n.GetMsgByKey("ImageRepoAuthFromDB"), err)
|
||||
encodedJSON, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if repo.Auth {
|
||||
authConfig := registry.AuthConfig{
|
||||
Username: repo.Username,
|
||||
Password: repo.Password,
|
||||
}
|
||||
encodedJSON, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
|
||||
options.RegistryAuth = authStr
|
||||
}
|
||||
imageName = repo.DownloadUrl + "/" + item
|
||||
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
|
||||
options.RegistryAuth = authStr
|
||||
}
|
||||
imageName = repo.DownloadUrl + "/" + item
|
||||
dockerCli := docker.NewClientWithExist(client)
|
||||
err = dockerCli.PullImageWithProcessAndOptions(taskItem, imageName, options)
|
||||
taskItem.LogWithStatus(i18n.GetMsgByKey("TaskPull"), err)
|
||||
if err != nil {
|
||||
return err
|
||||
pullErr := dockerCli.PullImageWithProcessAndOptions(taskItem, imageName, options)
|
||||
taskItem.LogWithStatus(i18n.GetMsgByKey("TaskPull"), pullErr)
|
||||
if pullErr != nil {
|
||||
return pullErr
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
@@ -547,49 +545,3 @@ func checkUsed(imageID string, containers []container.Summary) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func loadAuthInfo(image string) (bool, string) {
|
||||
if !strings.Contains(image, "/") {
|
||||
return false, ""
|
||||
}
|
||||
homeDir := homedir.Get()
|
||||
confPath := path.Join(homeDir, ".docker/config.json")
|
||||
configFileBytes, err := os.ReadFile(confPath)
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
var config dockerConfig
|
||||
if err = json.Unmarshal(configFileBytes, &config); err != nil {
|
||||
return false, ""
|
||||
}
|
||||
var (
|
||||
user string
|
||||
passwd string
|
||||
)
|
||||
imagePrefix := strings.Split(image, "/")[0]
|
||||
if val, ok := config.Auths[imagePrefix]; ok {
|
||||
itemByte, _ := base64.StdEncoding.DecodeString(val.Auth)
|
||||
itemStr := string(itemByte)
|
||||
if strings.Contains(itemStr, ":") {
|
||||
user = strings.Split(itemStr, ":")[0]
|
||||
passwd = strings.Split(itemStr, ":")[1]
|
||||
}
|
||||
}
|
||||
authConfig := registry.AuthConfig{
|
||||
Username: user,
|
||||
Password: passwd,
|
||||
}
|
||||
encodedJSON, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
|
||||
return true, authStr
|
||||
}
|
||||
|
||||
type dockerConfig struct {
|
||||
Auths map[string]authConfig `json:"auths"`
|
||||
}
|
||||
type authConfig struct {
|
||||
Auth string `json:"auth"`
|
||||
}
|
||||
|
||||
+437
-138
@@ -33,11 +33,14 @@ import (
|
||||
type MonitorService struct {
|
||||
DiskIO chan ([]disk.IOCountersStat)
|
||||
NetIO chan ([]net.IOCountersStat)
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
var (
|
||||
monitorCancel context.CancelFunc
|
||||
hostSysPath = loadHostSysPath()
|
||||
monitorSettingMutex sync.Mutex
|
||||
gpuMonitorMutex sync.Mutex
|
||||
monitorCancel context.CancelFunc
|
||||
hostSysPath = loadHostSysPath()
|
||||
|
||||
blockDevicePartitionCache sync.Map
|
||||
)
|
||||
@@ -47,8 +50,13 @@ type IMonitorService interface {
|
||||
LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorData, error)
|
||||
LoadSetting() (*dto.MonitorSetting, error)
|
||||
UpdateSetting(key, value string) error
|
||||
CleanData() error
|
||||
CleanData(monitorType string) error
|
||||
|
||||
LoadIOOptions() []string
|
||||
LoadNetworkOptions() []string
|
||||
LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error)
|
||||
LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error)
|
||||
CleanVLLMMonitor(req dto.MonitorVLLMClean) error
|
||||
LoadGPUOptions() dto.MonitorGPUOptions
|
||||
LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error)
|
||||
|
||||
@@ -60,6 +68,7 @@ func NewIMonitorService() IMonitorService {
|
||||
return &MonitorService{
|
||||
DiskIO: make(chan []disk.IOCountersStat, 2),
|
||||
NetIO: make(chan []net.IOCountersStat, 2),
|
||||
ctx: context.Background(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +94,7 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
|
||||
base.TopCPUItems = processes
|
||||
base.TopCPU = ""
|
||||
}
|
||||
if req.Param == "all" || req.Param == "mem" {
|
||||
if req.Param == "all" || req.Param == "memory" {
|
||||
var processes []dto.Process
|
||||
_ = json.Unmarshal([]byte(base.TopMem), &processes)
|
||||
base.TopMemItems = processes
|
||||
@@ -96,7 +105,11 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
|
||||
data = append(data, itemData)
|
||||
}
|
||||
if req.Param == "all" || req.Param == "io" {
|
||||
bases, err := monitorRepo.GetIO(repo.WithByName(req.IO), repo.WithByCreatedAt(req.StartTime, req.EndTime))
|
||||
ioOpts := []repo.DBOption{repo.WithByCreatedAt(req.StartTime, req.EndTime)}
|
||||
if len(req.IO) != 0 {
|
||||
ioOpts = append(ioOpts, repo.WithByName(req.IO))
|
||||
}
|
||||
bases, err := monitorRepo.GetIO(ioOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -110,7 +123,11 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
|
||||
data = append(data, itemData)
|
||||
}
|
||||
if req.Param == "all" || req.Param == "network" {
|
||||
bases, err := monitorRepo.GetNetwork(repo.WithByName(req.Network), repo.WithByCreatedAt(req.StartTime, req.EndTime))
|
||||
netOpts := []repo.DBOption{repo.WithByCreatedAt(req.StartTime, req.EndTime)}
|
||||
if len(req.Network) != 0 {
|
||||
netOpts = append(netOpts, repo.WithByName(req.Network))
|
||||
}
|
||||
bases, err := monitorRepo.GetNetwork(netOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -126,117 +143,171 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (m *MonitorService) LoadIOOptions() []string {
|
||||
optionSet := make(map[string]struct{})
|
||||
if diskStat, err := disk.IOCounters(); err == nil {
|
||||
for _, item := range diskStat {
|
||||
optionSet[item.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
// union with names recorded in the monitor db so removed devices stay selectable
|
||||
if names, err := monitorRepo.GetIONames(); err == nil {
|
||||
for _, name := range names {
|
||||
optionSet[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
return sortedMonitorOptions(optionSet)
|
||||
}
|
||||
|
||||
func (m *MonitorService) LoadNetworkOptions() []string {
|
||||
optionSet := make(map[string]struct{})
|
||||
if netStat, err := net.IOCounters(true); err == nil {
|
||||
for _, item := range netStat {
|
||||
optionSet[item.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
if names, err := monitorRepo.GetNetworkNames(); err == nil {
|
||||
for _, name := range names {
|
||||
optionSet[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
return sortedMonitorOptions(optionSet)
|
||||
}
|
||||
|
||||
func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions {
|
||||
var data dto.MonitorGPUOptions
|
||||
exist, client := accelerator.New()
|
||||
if !exist {
|
||||
return data
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
snapshot, err := client.Collect(context.Background())
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Load accelerator info failed, err: %v", err)
|
||||
global.LOG.Warnf("Load accelerator options failed: %v", err)
|
||||
} else {
|
||||
data = loadGPUOptions(snapshot)
|
||||
for _, item := range data.ChartHide {
|
||||
seen[item.DeviceID] = true
|
||||
}
|
||||
}
|
||||
data.Supported = true
|
||||
devices, err := monitorRepo.GetGPUDevices()
|
||||
if err != nil {
|
||||
global.LOG.Warnf("Load accelerator history options failed: %v", err)
|
||||
return data
|
||||
}
|
||||
if warning := snapshot.Warning(); warning != nil {
|
||||
global.LOG.Warnf("Load accelerator info partially failed, err: %v", warning)
|
||||
}
|
||||
return loadGPUOptions(snapshot)
|
||||
}
|
||||
|
||||
func loadGPUOptions(snapshot *accelerator.Snapshot) dto.MonitorGPUOptions {
|
||||
var data dto.MonitorGPUOptions
|
||||
hasGPUOrNPU := false
|
||||
hasXPU := false
|
||||
for _, item := range snapshot.Devices {
|
||||
if item.Kind == accelerator.KindXPU {
|
||||
hasXPU = true
|
||||
} else {
|
||||
hasGPUOrNPU = true
|
||||
for _, device := range devices {
|
||||
key := device.DeviceID
|
||||
if key == "" {
|
||||
key = "legacy:" + device.ProductName
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case hasGPUOrNPU && hasXPU:
|
||||
data.GPUType = "mixed"
|
||||
case hasXPU:
|
||||
data.GPUType = "xpu"
|
||||
case hasGPUOrNPU:
|
||||
data.GPUType = "gpu"
|
||||
}
|
||||
|
||||
sort.Slice(snapshot.Devices, func(i, j int) bool {
|
||||
if snapshot.Devices[i].Kind != snapshot.Devices[j].Kind {
|
||||
return snapshot.Devices[i].Kind < snapshot.Devices[j].Kind
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
if snapshot.Devices[i].Vendor != snapshot.Devices[j].Vendor {
|
||||
return snapshot.Devices[i].Vendor < snapshot.Devices[j].Vendor
|
||||
}
|
||||
if snapshot.Devices[i].NPUIndex != snapshot.Devices[j].NPUIndex {
|
||||
return snapshot.Devices[i].NPUIndex < snapshot.Devices[j].NPUIndex
|
||||
}
|
||||
if snapshot.Devices[i].ChipIndex != snapshot.Devices[j].ChipIndex {
|
||||
return snapshot.Devices[i].ChipIndex < snapshot.Devices[j].ChipIndex
|
||||
}
|
||||
return snapshot.Devices[i].Index < snapshot.Devices[j].Index
|
||||
})
|
||||
for _, item := range snapshot.Devices {
|
||||
optionType := "gpu"
|
||||
if item.Kind == accelerator.KindXPU {
|
||||
optionType = "xpu"
|
||||
}
|
||||
chartHide := dto.GPUChartHide{
|
||||
ProductName: item.Label,
|
||||
Type: optionType,
|
||||
GPU: !item.Capabilities.Utilization,
|
||||
Memory: !item.Capabilities.Memory,
|
||||
Power: !item.Capabilities.Power,
|
||||
PowerLimit: !item.Capabilities.PowerLimit,
|
||||
Temperature: !item.Capabilities.Temperature,
|
||||
Speed: !item.Capabilities.FanSpeed,
|
||||
}
|
||||
data.ChartHide = append(data.ChartHide, chartHide)
|
||||
data.Options = append(data.Options, chartHide.ProductName)
|
||||
seen[key] = true
|
||||
data.ChartHide = append(data.ChartHide, dto.GPUChartHide{DeviceID: device.DeviceID, ProductName: device.ProductName, Type: device.DeviceType, Legacy: device.DeviceID == ""})
|
||||
data.Options = append(data.Options, device.ProductName)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (m *MonitorService) LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error) {
|
||||
loc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
req.StartTime = req.StartTime.In(loc)
|
||||
req.EndTime = req.EndTime.In(loc)
|
||||
var data dto.MonitorGPUData
|
||||
gpuList, err := monitorRepo.GetGPU(repo.WithByCreatedAt(req.StartTime, req.EndTime), monitorRepo.WithByProductName(req.ProductName))
|
||||
if req.StartTime.IsZero() || req.EndTime.IsZero() || !req.EndTime.After(req.StartTime) {
|
||||
return data, fmt.Errorf("invalid GPU history time range")
|
||||
}
|
||||
if req.DeviceID == "" && req.ProductName == "" {
|
||||
return data, fmt.Errorf("GPU history requires a device")
|
||||
}
|
||||
if req.Aggregation != "" && req.Aggregation != "avg" && req.Aggregation != "max" {
|
||||
return data, fmt.Errorf("invalid GPU history aggregation")
|
||||
}
|
||||
loc, err := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
for _, gpu := range gpuList {
|
||||
data.Date = append(data.Date, gpu.CreatedAt)
|
||||
data.GPUValue = append(data.GPUValue, gpu.GPUUtil)
|
||||
data.TemperatureValue = append(data.TemperatureValue, gpu.Temperature)
|
||||
data.PowerUsed = append(data.PowerUsed, gpu.PowerDraw)
|
||||
data.PowerTotal = append(data.PowerTotal, gpu.MaxPowerLimit)
|
||||
if gpu.MaxPowerLimit != 0 {
|
||||
data.PowerPercent = append(data.PowerPercent, gpu.PowerDraw/gpu.MaxPowerLimit*100)
|
||||
} else {
|
||||
data.PowerPercent = append(data.PowerPercent, float64(0))
|
||||
req.StartTime, req.EndTime = req.StartTime.In(loc), req.EndTime.In(loc)
|
||||
opts := []repo.DBOption{repo.WithByCreatedAt(req.StartTime, req.EndTime), monitorRepo.WithByGPUDevice(req.DeviceID, req.ProductName, req.Legacy)}
|
||||
data.SampleCount, err = monitorRepo.CountGPU(opts...)
|
||||
if err != nil || data.SampleCount == 0 {
|
||||
return data, err
|
||||
}
|
||||
if data.SampleCount > 1200 {
|
||||
seconds := req.EndTime.Unix() - req.StartTime.Unix() + 1
|
||||
data.BucketSeconds = (seconds + 599) / 600
|
||||
}
|
||||
points, err := monitorRepo.GetGPUHistory(req.StartTime, data.BucketSeconds, req.Aggregation, opts...)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
samples := make([]repo.GPUHistoryPoint, 0, len(points))
|
||||
if data.BucketSeconds > 0 {
|
||||
next := 0
|
||||
for bucket := int64(0); bucket <= (req.EndTime.Unix()-req.StartTime.Unix())/data.BucketSeconds; bucket++ {
|
||||
point := repo.GPUHistoryPoint{}
|
||||
if next < len(points) && points[next].Bucket == bucket {
|
||||
point = points[next]
|
||||
next++
|
||||
}
|
||||
point.CreatedAt = time.Unix(req.StartTime.Unix()+bucket*data.BucketSeconds, 0).In(loc)
|
||||
if bucket == 0 {
|
||||
point.CreatedAt = req.StartTime
|
||||
}
|
||||
samples = append(samples, point)
|
||||
}
|
||||
|
||||
data.MemoryTotal = append(data.MemoryTotal, gpu.MemTotal)
|
||||
data.MemoryUsed = append(data.MemoryUsed, gpu.MemUsed)
|
||||
if gpu.MemTotal != 0 {
|
||||
data.MemoryPercent = append(data.MemoryPercent, gpu.MemUsed/gpu.MemTotal*100)
|
||||
} else {
|
||||
data.MemoryPercent = append(data.MemoryPercent, float64(0))
|
||||
} else {
|
||||
for i, point := range points {
|
||||
if i > 0 && points[i-1].IntervalSeconds > 0 {
|
||||
interval := time.Duration(points[i-1].IntervalSeconds) * time.Second
|
||||
if point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval {
|
||||
samples = append(samples, repo.GPUHistoryPoint{MonitorGPU: model.MonitorGPU{BaseModel: model.BaseModel{CreatedAt: points[i-1].CreatedAt.Add(interval)}}})
|
||||
}
|
||||
}
|
||||
samples = append(samples, point)
|
||||
}
|
||||
var process []dto.GPUProcess
|
||||
if err := json.Unmarshal([]byte(gpu.Processes), &process); err == nil {
|
||||
data.ProcessCount = append(data.ProcessCount, len(process))
|
||||
data.GPUProcesses = append(data.GPUProcesses, process)
|
||||
} else {
|
||||
data.ProcessCount = append(data.ProcessCount, 0)
|
||||
data.GPUProcesses = append(data.GPUProcesses, []dto.GPUProcess{})
|
||||
}
|
||||
for _, point := range samples {
|
||||
data.Date = append(data.Date, point.CreatedAt)
|
||||
data.MemoryActivity = append(data.MemoryActivity, point.MemoryActivity)
|
||||
data.EncoderUtil = append(data.EncoderUtil, point.EncoderUtil)
|
||||
data.DecoderUtil = append(data.DecoderUtil, point.DecoderUtil)
|
||||
data.JPEGUtil = append(data.JPEGUtil, point.JPEGUtil)
|
||||
data.OFAUtil = append(data.OFAUtil, point.OFAUtil)
|
||||
data.MediaUtil = append(data.MediaUtil, point.MediaUtil)
|
||||
data.ComputeUtil = append(data.ComputeUtil, point.ComputeUtil)
|
||||
data.CopyUtil = append(data.CopyUtil, point.CopyUtil)
|
||||
data.HotspotTemperature = append(data.HotspotTemperature, point.HotspotTemperature)
|
||||
data.FanRPM = append(data.FanRPM, point.FanRPM)
|
||||
data.AICPUUtil = append(data.AICPUUtil, point.AICPUUtil)
|
||||
data.CtrlCPUUtil = append(data.CtrlCPUUtil, point.CtrlCPUUtil)
|
||||
data.DDRUsed = append(data.DDRUsed, point.DDRUsed)
|
||||
data.DDRTotal = append(data.DDRTotal, point.DDRTotal)
|
||||
data.HBMUsed = append(data.HBMUsed, point.HBMUsed)
|
||||
data.HBMTotal = append(data.HBMTotal, point.HBMTotal)
|
||||
data.DDRBandwidth = append(data.DDRBandwidth, point.DDRBandwidth)
|
||||
data.HBMBandwidth = append(data.HBMBandwidth, point.HBMBandwidth)
|
||||
data.MemoryBandwidth = append(data.MemoryBandwidth, point.MemoryBandwidth)
|
||||
data.MediaFrequency = append(data.MediaFrequency, point.MediaFrequency)
|
||||
data.HugepagesUsed = append(data.HugepagesUsed, point.HugepagesUsed)
|
||||
data.HugepagesTotal = append(data.HugepagesTotal, point.HugepagesTotal)
|
||||
data.GPUValue = append(data.GPUValue, point.GPUUtil)
|
||||
data.TemperatureValue = append(data.TemperatureValue, point.Temperature)
|
||||
data.MemoryTemperatureValue = append(data.MemoryTemperatureValue, point.MemoryTemperature)
|
||||
data.PowerUsed = append(data.PowerUsed, point.PowerDraw)
|
||||
data.PowerTotal = append(data.PowerTotal, point.MaxPowerLimit)
|
||||
data.PowerPercent = append(data.PowerPercent, point.PowerPercent)
|
||||
data.MemoryPercent = append(data.MemoryPercent, point.MemoryPercent)
|
||||
data.MemoryTotal = append(data.MemoryTotal, point.MemTotal)
|
||||
data.MemoryUsed = append(data.MemoryUsed, point.MemUsed)
|
||||
data.SpeedValue = append(data.SpeedValue, point.FanSpeed)
|
||||
data.FrequencyValue = append(data.FrequencyValue, point.Frequency)
|
||||
data.MemoryFrequencyValue = append(data.MemoryFrequencyValue, point.MemoryFrequency)
|
||||
data.ProcessCount = append(data.ProcessCount, point.ProcessCount)
|
||||
var processes []dto.GPUProcess
|
||||
if data.BucketSeconds == 0 && point.ProcessCount != nil {
|
||||
_ = json.Unmarshal([]byte(point.Processes), &processes)
|
||||
}
|
||||
data.SpeedValue = append(data.SpeedValue, gpu.FanSpeed)
|
||||
data.GPUProcesses = append(data.GPUProcesses, processes)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
@@ -262,9 +333,27 @@ func (m *MonitorService) LoadSetting() (*dto.MonitorSetting, error) {
|
||||
}
|
||||
|
||||
func (m *MonitorService) UpdateSetting(key, value string) error {
|
||||
monitorSettingMutex.Lock()
|
||||
defer monitorSettingMutex.Unlock()
|
||||
switch key {
|
||||
case "MonitorStatus", "GPUMonitorStatus", "VLLMMonitorStatus":
|
||||
if value != constant.StatusEnable && value != constant.StatusDisable {
|
||||
return fmt.Errorf("invalid monitoring status")
|
||||
}
|
||||
case "MonitorInterval", "GPUMonitorInterval", "VLLMMonitorInterval":
|
||||
interval, err := strconv.Atoi(value)
|
||||
if err != nil || interval < 10 || interval > 43200 {
|
||||
return fmt.Errorf("monitoring interval must be between 10 and 43200 seconds")
|
||||
}
|
||||
case "MonitorStoreDays", "GPUMonitorStoreDays", "VLLMMonitorStoreDays":
|
||||
days, err := strconv.Atoi(value)
|
||||
if err != nil || days < 1 {
|
||||
return fmt.Errorf("monitoring retention must be a positive integer")
|
||||
}
|
||||
}
|
||||
switch key {
|
||||
case "MonitorStatus":
|
||||
if value == constant.StatusEnable && global.MonitorCronID == 0 {
|
||||
if value == constant.StatusEnable && monitorCancel == nil {
|
||||
interval, err := settingRepo.Get(settingRepo.WithByKey("MonitorInterval"))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -273,8 +362,9 @@ func (m *MonitorService) UpdateSetting(key, value string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if value == constant.StatusDisable && global.MonitorCronID != 0 {
|
||||
if value == constant.StatusDisable && monitorCancel != nil {
|
||||
monitorCancel()
|
||||
monitorCancel = nil
|
||||
global.Cron.Remove(cron.EntryID(global.MonitorCronID))
|
||||
global.MonitorCronID = 0
|
||||
}
|
||||
@@ -283,31 +373,80 @@ func (m *MonitorService) UpdateSetting(key, value string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status.Value == constant.StatusEnable && global.MonitorCronID != 0 {
|
||||
if status.Value == constant.StatusEnable && monitorCancel != nil {
|
||||
if err := StartMonitor(true, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case "GPUMonitorStatus":
|
||||
if value == constant.StatusEnable && global.GPUMonitorCronID == 0 {
|
||||
interval, err := settingRepo.GetValueByKey("GPUMonitorInterval")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := StartGPUMonitor(interval); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if value == constant.StatusDisable && global.GPUMonitorCronID != 0 {
|
||||
global.Cron.Remove(global.GPUMonitorCronID)
|
||||
global.GPUMonitorCronID = 0
|
||||
}
|
||||
case "GPUMonitorInterval":
|
||||
status, err := settingRepo.GetValueByKey("GPUMonitorStatus")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status == constant.StatusEnable {
|
||||
if err := StartGPUMonitor(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case "VLLMMonitorStatus":
|
||||
if value == constant.StatusEnable && global.VLLMMonitorCronID == 0 {
|
||||
interval, err := settingRepo.GetValueByKey("VLLMMonitorInterval")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := StartVLLMMonitor(interval); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if value == constant.StatusDisable && global.VLLMMonitorCronID != 0 {
|
||||
global.Cron.Remove(global.VLLMMonitorCronID)
|
||||
global.VLLMMonitorCronID = 0
|
||||
}
|
||||
case "VLLMMonitorInterval":
|
||||
status, err := settingRepo.GetValueByKey("VLLMMonitorStatus")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status == constant.StatusEnable {
|
||||
if err := StartVLLMMonitor(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return settingRepo.Update(key, value)
|
||||
}
|
||||
|
||||
func (m *MonitorService) CleanData() error {
|
||||
if err := global.MonitorDB.Exec("DELETE FROM monitor_bases").Error; err != nil {
|
||||
return err
|
||||
func (m *MonitorService) CleanData(monitorType string) error {
|
||||
switch monitorType {
|
||||
case "host":
|
||||
return monitorRepo.CleanHost()
|
||||
case "gpu":
|
||||
gpuMonitorMutex.Lock()
|
||||
defer gpuMonitorMutex.Unlock()
|
||||
return monitorRepo.CleanGPU()
|
||||
default:
|
||||
return fmt.Errorf("unsupported monitoring cleanup type: %s", monitorType)
|
||||
}
|
||||
if err := global.MonitorDB.Exec("DELETE FROM monitor_ios").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := global.MonitorDB.Exec("DELETE FROM monitor_networks").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
_ = global.GPUMonitorDB.Exec("DELETE FROM monitor_gpus").Error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MonitorService) Run() {
|
||||
saveAcceleratorDataToDB()
|
||||
if m.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
var itemModel model.MonitorBase
|
||||
totalPercent, _ := cpu.Percent(3*time.Second, false)
|
||||
if len(totalPercent) == 1 {
|
||||
@@ -353,30 +492,34 @@ func (m *MonitorService) Run() {
|
||||
_ = monitorRepo.DelMonitorBase(timeForDelete)
|
||||
_ = monitorRepo.DelMonitorIO(timeForDelete)
|
||||
_ = monitorRepo.DelMonitorNet(timeForDelete)
|
||||
_ = monitorRepo.DelMonitorGPU(timeForDelete)
|
||||
}
|
||||
|
||||
func (m *MonitorService) loadDiskIO() {
|
||||
ioStat, _ := disk.IOCounters()
|
||||
ioStat, _ := disk.IOCountersWithContext(m.ctx)
|
||||
var diskIOList []disk.IOCountersStat
|
||||
for _, io := range ioStat {
|
||||
diskIOList = append(diskIOList, io)
|
||||
}
|
||||
diskIOList = append(diskIOList, sumDiskIOCounters(ioStat))
|
||||
m.DiskIO <- diskIOList
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
case m.DiskIO <- diskIOList:
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MonitorService) loadNetIO() {
|
||||
netStat, _ := net.IOCounters(true)
|
||||
netStatAll, _ := net.IOCounters(false)
|
||||
netStat, _ := net.IOCountersWithContext(m.ctx, true)
|
||||
netStatAll, _ := net.IOCountersWithContext(m.ctx, false)
|
||||
var netList []net.IOCountersStat
|
||||
netList = append(netList, netStat...)
|
||||
netList = append(netList, netStatAll...)
|
||||
m.NetIO <- netList
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
case m.NetIO <- netList:
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MonitorService) saveIODataToDB(ctx context.Context, interval float64) {
|
||||
defer close(m.DiskIO)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -426,14 +569,17 @@ func (m *MonitorService) saveIODataToDB(ctx context.Context, interval float64) {
|
||||
}
|
||||
}
|
||||
_ = monitorRepo.BatchCreateMonitorIO(ioList)
|
||||
m.DiskIO <- ioStat2
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case m.DiskIO <- ioStat2:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64) {
|
||||
defer close(m.NetIO)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -463,7 +609,11 @@ func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64)
|
||||
}
|
||||
|
||||
_ = monitorRepo.BatchCreateMonitorNet(netList)
|
||||
m.NetIO <- netStat2
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case m.NetIO <- netStat2:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -572,27 +722,38 @@ func loadTopMem() []dto.Process {
|
||||
}
|
||||
|
||||
func StartMonitor(removeBefore bool, interval string) error {
|
||||
intervalItem, err := strconv.Atoi(interval)
|
||||
if err != nil || intervalItem < 10 || intervalItem > 43200 {
|
||||
return fmt.Errorf("invalid host monitoring interval: %s", interval)
|
||||
}
|
||||
if removeBefore {
|
||||
monitorCancel()
|
||||
global.Cron.Remove(cron.EntryID(global.MonitorCronID))
|
||||
}
|
||||
intervalItem, err := strconv.Atoi(interval)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service := NewIMonitorService()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
monitorCancel = cancel
|
||||
service := &MonitorService{
|
||||
DiskIO: make(chan []disk.IOCountersStat, 2),
|
||||
NetIO: make(chan []net.IOCountersStat, 2),
|
||||
ctx: ctx,
|
||||
}
|
||||
now := time.Now()
|
||||
nextMinute := now.Truncate(time.Minute).Add(time.Minute)
|
||||
time.AfterFunc(time.Until(nextMinute), func() {
|
||||
timer := time.AfterFunc(time.Until(nextMinute), func() {
|
||||
monitorSettingMutex.Lock()
|
||||
defer monitorSettingMutex.Unlock()
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
monitorID, err := global.Cron.AddJob(fmt.Sprintf("@every %ss", interval), service)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
global.MonitorCronID = monitorID
|
||||
})
|
||||
monitorCancel = func() {
|
||||
cancel()
|
||||
timer.Stop()
|
||||
}
|
||||
|
||||
service.Run()
|
||||
|
||||
@@ -602,7 +763,97 @@ func StartMonitor(removeBefore bool, interval string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveAcceleratorDataToDB() {
|
||||
func loadGPUOptions(snapshot *accelerator.Snapshot) dto.MonitorGPUOptions {
|
||||
var data dto.MonitorGPUOptions
|
||||
hasGPUOrNPU := false
|
||||
hasXPU := false
|
||||
for _, item := range snapshot.Devices {
|
||||
if item.Kind == accelerator.KindXPU {
|
||||
hasXPU = true
|
||||
} else {
|
||||
hasGPUOrNPU = true
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case hasGPUOrNPU && hasXPU:
|
||||
data.GPUType = "mixed"
|
||||
case hasXPU:
|
||||
data.GPUType = "xpu"
|
||||
case hasGPUOrNPU:
|
||||
data.GPUType = "gpu"
|
||||
}
|
||||
|
||||
sort.Slice(snapshot.Devices, func(i, j int) bool {
|
||||
if snapshot.Devices[i].Kind != snapshot.Devices[j].Kind {
|
||||
return snapshot.Devices[i].Kind < snapshot.Devices[j].Kind
|
||||
}
|
||||
if snapshot.Devices[i].Vendor != snapshot.Devices[j].Vendor {
|
||||
return snapshot.Devices[i].Vendor < snapshot.Devices[j].Vendor
|
||||
}
|
||||
if snapshot.Devices[i].NPUIndex != snapshot.Devices[j].NPUIndex {
|
||||
return snapshot.Devices[i].NPUIndex < snapshot.Devices[j].NPUIndex
|
||||
}
|
||||
if snapshot.Devices[i].ChipIndex != snapshot.Devices[j].ChipIndex {
|
||||
return snapshot.Devices[i].ChipIndex < snapshot.Devices[j].ChipIndex
|
||||
}
|
||||
return snapshot.Devices[i].Index < snapshot.Devices[j].Index
|
||||
})
|
||||
for _, item := range snapshot.Devices {
|
||||
chartHide := dto.GPUChartHide{
|
||||
DeviceID: item.ID,
|
||||
ProductName: item.Label,
|
||||
Type: string(item.Kind),
|
||||
}
|
||||
data.ChartHide = append(data.ChartHide, chartHide)
|
||||
data.Options = append(data.Options, chartHide.ProductName)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func StartGPUMonitor(interval string) error {
|
||||
seconds, err := strconv.Atoi(interval)
|
||||
if err != nil || seconds < 10 || seconds > 43200 {
|
||||
return fmt.Errorf("invalid GPU monitoring interval: %s", interval)
|
||||
}
|
||||
service := &MonitorService{}
|
||||
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(service.saveGPUData))
|
||||
id, err := global.Cron.AddFunc(fmt.Sprintf("@every %ds", seconds), func() { go job.Run() })
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if global.GPUMonitorCronID != 0 {
|
||||
global.Cron.Remove(global.GPUMonitorCronID)
|
||||
}
|
||||
global.GPUMonitorCronID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MonitorService) saveGPUData() {
|
||||
if !gpuMonitorMutex.TryLock() {
|
||||
return
|
||||
}
|
||||
defer gpuMonitorMutex.Unlock()
|
||||
status, err := settingRepo.GetValueByKey("GPUMonitorStatus")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Load GPU monitoring status failed: %v", err)
|
||||
return
|
||||
}
|
||||
if status != constant.StatusEnable {
|
||||
return
|
||||
}
|
||||
retention, err := settingRepo.GetValueByKey("GPUMonitorStoreDays")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Load GPU monitoring retention failed: %v", err)
|
||||
return
|
||||
}
|
||||
days, err := strconv.Atoi(retention)
|
||||
if err != nil || days < 1 {
|
||||
global.LOG.Errorf("Invalid GPU monitoring retention: %s", retention)
|
||||
return
|
||||
}
|
||||
if err := monitorRepo.DelMonitorGPU(time.Now().AddDate(0, 0, -days)); err != nil {
|
||||
global.LOG.Errorf("Clean GPU monitoring data failed: %v", err)
|
||||
}
|
||||
exist, client := accelerator.New()
|
||||
if !exist {
|
||||
return
|
||||
@@ -615,27 +866,64 @@ func saveAcceleratorDataToDB() {
|
||||
if warning := snapshot.Warning(); warning != nil {
|
||||
global.LOG.Warnf("load accelerator monitor data partially failed, err: %v", warning)
|
||||
}
|
||||
intervalSeconds := 0
|
||||
if setting, err := settingRepo.Get(settingRepo.WithByKey("GPUMonitorInterval")); err == nil {
|
||||
intervalSeconds, _ = strconv.Atoi(setting.Value)
|
||||
}
|
||||
list := make([]model.MonitorGPU, 0, len(snapshot.Devices))
|
||||
for _, device := range snapshot.Devices {
|
||||
list = append(list, newMonitorGPU(device))
|
||||
item := newMonitorGPU(device)
|
||||
item.CreatedAt = snapshot.Info.CollectedAt
|
||||
item.IntervalSeconds = intervalSeconds
|
||||
list = append(list, item)
|
||||
}
|
||||
if err := repo.NewIMonitorRepo().BatchCreateMonitorGPU(list); err != nil {
|
||||
if err := monitorRepo.BatchCreateMonitorGPU(list); err != nil {
|
||||
global.LOG.Errorf("batch create accelerator monitor data failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func newMonitorGPU(device accelerator.Device) model.MonitorGPU {
|
||||
item := model.MonitorGPU{
|
||||
ProductName: device.Label,
|
||||
GPUUtil: device.Metrics.Utilization.ValueOrZero(),
|
||||
Temperature: device.Metrics.Temperature.ValueOrZero(),
|
||||
PowerDraw: device.Metrics.Power.ValueOrZero(),
|
||||
MaxPowerLimit: device.Metrics.PowerLimit.ValueOrZero(),
|
||||
MemUsed: device.Metrics.MemoryUsed.ValueOrZero(),
|
||||
MemTotal: device.Metrics.MemoryTotal.ValueOrZero(),
|
||||
FanSpeed: int(device.Metrics.FanSpeed.ValueOrZero()),
|
||||
MemoryUtil: device.Metrics.MemoryUtil.Value,
|
||||
MemoryActivity: device.Metrics.MemoryActivity.Value,
|
||||
EncoderUtil: device.Metrics.EncoderUtil.Value,
|
||||
DecoderUtil: device.Metrics.DecoderUtil.Value,
|
||||
JPEGUtil: device.Metrics.JPEGUtil.Value,
|
||||
OFAUtil: device.Metrics.OFAUtil.Value,
|
||||
MediaUtil: device.Metrics.MediaUtil.Value,
|
||||
ComputeUtil: device.Metrics.ComputeUtil.Value,
|
||||
CopyUtil: device.Metrics.CopyUtil.Value,
|
||||
HotspotTemperature: device.Metrics.HotspotTemperature.Value,
|
||||
FanRPM: device.Metrics.FanRPM.Value,
|
||||
AICPUUtil: device.Metrics.AICPUUtil.Value,
|
||||
CtrlCPUUtil: device.Metrics.CtrlCPUUtil.Value,
|
||||
DDRUsed: device.Metrics.DDRUsed.Value,
|
||||
DDRTotal: device.Metrics.DDRTotal.Value,
|
||||
HBMUsed: device.Metrics.HBMUsed.Value,
|
||||
HBMTotal: device.Metrics.HBMTotal.Value,
|
||||
DDRBandwidth: device.Metrics.DDRBandwidth.Value,
|
||||
HBMBandwidth: device.Metrics.HBMBandwidth.Value,
|
||||
MemoryBandwidth: device.Metrics.MemoryBandwidth.Value,
|
||||
MediaFrequency: device.Metrics.MediaFrequency.Value,
|
||||
HugepagesUsed: device.Metrics.HugepagesUsed.Value,
|
||||
HugepagesTotal: device.Metrics.HugepagesTotal.Value,
|
||||
|
||||
ProductName: device.Label,
|
||||
DeviceID: device.ID,
|
||||
DeviceType: string(device.Kind),
|
||||
ProcessStatus: device.ProcessStatus,
|
||||
Frequency: device.Metrics.Frequency.Value,
|
||||
MemoryFrequency: device.Metrics.MemoryFrequency.Value,
|
||||
MemoryTemperature: device.Metrics.MemoryTemperature.Value,
|
||||
GPUUtil: device.Metrics.Utilization.Value,
|
||||
Temperature: device.Metrics.Temperature.Value,
|
||||
PowerDraw: device.Metrics.Power.Value,
|
||||
MaxPowerLimit: device.Metrics.PowerLimit.Value,
|
||||
MemUsed: device.Metrics.MemoryUsed.Value,
|
||||
MemTotal: device.Metrics.MemoryTotal.Value,
|
||||
FanSpeed: device.Metrics.FanSpeed.Value,
|
||||
}
|
||||
if len(device.Processes) == 0 {
|
||||
if device.ProcessStatus != "ok" {
|
||||
return item
|
||||
}
|
||||
processes := make([]dto.GPUProcess, 0, len(device.Processes))
|
||||
@@ -695,3 +983,14 @@ func loadHostSysPath() string {
|
||||
}
|
||||
return hostSys
|
||||
}
|
||||
|
||||
func sortedMonitorOptions(optionSet map[string]struct{}) []string {
|
||||
options := make([]string, 0, len(optionSet))
|
||||
for name := range optionSet {
|
||||
if len(name) != 0 && name != "all" {
|
||||
options = append(options, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(options)
|
||||
return append([]string{"all"}, options...)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/vllm"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/robfig/cron/v3"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var vllmMonitorMutex sync.Mutex
|
||||
var vllmMetricsClient = &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
Transport: &http.Transport{DialContext: (&net.Dialer{Timeout: 3 * time.Second}).DialContext, IdleConnTimeout: 30 * time.Second},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
|
||||
}
|
||||
|
||||
func (m *MonitorService) LoadVLLMMonitorData(req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error) {
|
||||
data := dto.MonitorVLLMData{Points: []model.MonitorVLLM{}}
|
||||
if !req.EndTime.After(req.StartTime) {
|
||||
return data, fmt.Errorf("invalid vLLM history request")
|
||||
}
|
||||
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
if install.App.Key != "vllm" {
|
||||
return data, fmt.Errorf("not a vLLM installation")
|
||||
}
|
||||
|
||||
loc, err := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
req.StartTime, req.EndTime = req.StartTime.In(loc), req.EndTime.In(loc)
|
||||
data.SampleCount, err = vllmMonitorRepo.Count(req.AppInstallID, req.StartTime, req.EndTime)
|
||||
if err != nil || data.SampleCount == 0 {
|
||||
return data, err
|
||||
}
|
||||
if data.SampleCount > 1200 {
|
||||
data.BucketSeconds = (req.EndTime.Unix() - req.StartTime.Unix() + 600) / 600
|
||||
}
|
||||
points, err := vllmMonitorRepo.History(req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
if data.BucketSeconds > 0 {
|
||||
if req.Aggregation != "max" {
|
||||
for i := range points {
|
||||
if err := vllm.AggregateHistograms(points[i].HistogramSamples, &points[i].MonitorVLLM); err != nil {
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
}
|
||||
next := 0
|
||||
for bucket := int64(0); bucket <= (req.EndTime.Unix()-req.StartTime.Unix())/data.BucketSeconds; bucket++ {
|
||||
var point model.MonitorVLLM
|
||||
if next < len(points) && points[next].Bucket == bucket {
|
||||
point = points[next].MonitorVLLM
|
||||
next++
|
||||
}
|
||||
point.AppInstallID = req.AppInstallID
|
||||
point.CreatedAt = time.Unix(req.StartTime.Unix()+bucket*data.BucketSeconds, 0).In(loc)
|
||||
if bucket == 0 {
|
||||
point.CreatedAt = req.StartTime
|
||||
}
|
||||
data.Points = append(data.Points, point)
|
||||
}
|
||||
} else {
|
||||
setting, err := settingRepo.GetValueByKey("VLLMMonitorInterval")
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
intervalSeconds, err := strconv.Atoi(setting)
|
||||
if err != nil || intervalSeconds <= 0 {
|
||||
return data, fmt.Errorf("invalid vLLM monitoring interval: %s", setting)
|
||||
}
|
||||
interval := time.Duration(intervalSeconds) * time.Second
|
||||
for i, point := range points {
|
||||
if i > 0 && point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval {
|
||||
data.Points = append(data.Points, model.MonitorVLLM{CreatedAt: points[i-1].CreatedAt.Add(interval), AppInstallID: req.AppInstallID})
|
||||
}
|
||||
data.Points = append(data.Points, point.MonitorVLLM)
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (m *MonitorService) LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error) {
|
||||
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
|
||||
if err != nil {
|
||||
return model.MonitorVLLM{}, err
|
||||
}
|
||||
if install.App.Key != "vllm" {
|
||||
return model.MonitorVLLM{}, fmt.Errorf("not a vLLM installation")
|
||||
}
|
||||
previous, err := collectVLLMMetrics(ctx, install, model.MonitorVLLM{})
|
||||
if err != nil {
|
||||
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
|
||||
return previous, nil
|
||||
}
|
||||
timer := time.NewTimer(time.Second)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return model.MonitorVLLM{}, ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
point, err := collectVLLMMetrics(ctx, install, previous)
|
||||
if err != nil {
|
||||
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
|
||||
}
|
||||
return point, nil
|
||||
}
|
||||
|
||||
func (m *MonitorService) CleanVLLMMonitor(req dto.MonitorVLLMClean) error {
|
||||
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if install.App.Key != "vllm" {
|
||||
return fmt.Errorf("not a vLLM installation")
|
||||
}
|
||||
vllmMonitorMutex.Lock()
|
||||
defer vllmMonitorMutex.Unlock()
|
||||
return vllmMonitorRepo.CleanTarget(req.AppInstallID)
|
||||
}
|
||||
|
||||
func StartVLLMMonitor(interval string) error {
|
||||
seconds, err := strconv.Atoi(interval)
|
||||
if err != nil || seconds < 10 || seconds > 43200 {
|
||||
return fmt.Errorf("invalid vLLM monitoring interval: %s", interval)
|
||||
}
|
||||
service := &MonitorService{}
|
||||
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(service.saveVLLMData))
|
||||
id, err := global.Cron.AddFunc(fmt.Sprintf("@every %ds", seconds), func() { go job.Run() })
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if global.VLLMMonitorCronID != 0 {
|
||||
global.Cron.Remove(global.VLLMMonitorCronID)
|
||||
}
|
||||
global.VLLMMonitorCronID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MonitorService) saveVLLMData() {
|
||||
if !vllmMonitorMutex.TryLock() {
|
||||
return
|
||||
}
|
||||
defer vllmMonitorMutex.Unlock()
|
||||
status, err := settingRepo.GetValueByKey("VLLMMonitorStatus")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Load vLLM monitoring status failed: %v", err)
|
||||
return
|
||||
}
|
||||
if status != constant.StatusEnable {
|
||||
return
|
||||
}
|
||||
retention, err := settingRepo.GetValueByKey("VLLMMonitorStoreDays")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Load vLLM monitoring retention failed: %v", err)
|
||||
return
|
||||
}
|
||||
days, err := strconv.Atoi(retention)
|
||||
if err != nil || days < 1 {
|
||||
global.LOG.Errorf("Invalid vLLM monitoring retention: %s", retention)
|
||||
return
|
||||
}
|
||||
if err := vllmMonitorRepo.DeleteBefore(time.Now().AddDate(0, 0, -days)); err != nil {
|
||||
global.LOG.Errorf("Clean vLLM monitoring data failed: %v", err)
|
||||
}
|
||||
apps, err := appRepo.GetBy(appRepo.WithKey("vllm"))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Load vLLM apps for monitoring failed: %v", err)
|
||||
return
|
||||
}
|
||||
if len(apps) == 0 {
|
||||
return
|
||||
}
|
||||
ids := make([]uint, 0, len(apps))
|
||||
for _, app := range apps {
|
||||
ids = append(ids, app.ID)
|
||||
}
|
||||
installs, err := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppIdsIn(ids))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Load vLLM instances for monitoring failed: %v", err)
|
||||
return
|
||||
}
|
||||
var group errgroup.Group
|
||||
group.SetLimit(4)
|
||||
for _, install := range installs {
|
||||
group.Go(func() error {
|
||||
previous, err := vllmMonitorRepo.Latest(install.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
point, err := collectVLLMMetrics(context.Background(), install, previous)
|
||||
if err != nil {
|
||||
global.LOG.Debugf("Collect vLLM metrics for %s failed: %v", install.Name, err)
|
||||
}
|
||||
return vllmMonitorRepo.Create(&point)
|
||||
})
|
||||
}
|
||||
if err := group.Wait(); err != nil {
|
||||
global.LOG.Errorf("Save vLLM monitoring data failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func collectVLLMMetrics(ctx context.Context, install model.AppInstall, previous model.MonitorVLLM) (model.MonitorVLLM, error) {
|
||||
point := model.MonitorVLLM{AppInstallID: install.ID, CreatedAt: time.Now(), Status: "unavailable"}
|
||||
if install.HttpPort <= 0 || install.HttpPort > 65535 {
|
||||
return point, fmt.Errorf("invalid vLLM port")
|
||||
}
|
||||
var env map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(install.Env), &env); err != nil {
|
||||
return point, err
|
||||
}
|
||||
host, _ := env[constant.HostIP].(string)
|
||||
host = strings.Trim(host, "[]")
|
||||
switch host {
|
||||
case "", "0.0.0.0":
|
||||
host = "127.0.0.1"
|
||||
case "::":
|
||||
host = "::1"
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+net.JoinHostPort(host, strconv.Itoa(install.HttpPort))+"/metrics", nil)
|
||||
if err != nil {
|
||||
return point, err
|
||||
}
|
||||
response, err := vllmMetricsClient.Do(req)
|
||||
if err != nil {
|
||||
return point, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return point, fmt.Errorf("vLLM metrics returned HTTP %d", response.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(response.Body, 8*1024*1024+1))
|
||||
if err != nil {
|
||||
return point, err
|
||||
}
|
||||
if len(body) > 8*1024*1024 {
|
||||
return point, fmt.Errorf("vLLM metrics response too large")
|
||||
}
|
||||
metrics, err := vllm.Parse(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return point, err
|
||||
}
|
||||
point.CreatedAt = time.Now()
|
||||
raw, err := json.Marshal(metrics)
|
||||
if err != nil {
|
||||
return point, err
|
||||
}
|
||||
var before vllm.Metrics
|
||||
elapsed := point.CreatedAt.Sub(previous.CreatedAt).Seconds()
|
||||
if previous.Status == "ok" && elapsed > 0 {
|
||||
if err := json.Unmarshal([]byte(previous.RawMetrics), &before); err != nil {
|
||||
return point, err
|
||||
}
|
||||
}
|
||||
calculated, err := vllm.Calculate(metrics, before, elapsed)
|
||||
if err != nil {
|
||||
return point, err
|
||||
}
|
||||
calculated.AppInstallID = install.ID
|
||||
calculated.CreatedAt = point.CreatedAt
|
||||
calculated.RawMetrics = string(raw)
|
||||
calculated.Status = "ok"
|
||||
return calculated, nil
|
||||
}
|
||||
@@ -32,7 +32,7 @@ type NginxService struct {
|
||||
|
||||
type INginxService interface {
|
||||
GetNginxConfig() (*response.NginxFile, error)
|
||||
GetConfigByScope(req request.NginxScopeReq) ([]response.NginxParam, error)
|
||||
GetConfigByScope(req request.NginxScopeReq) (interface{}, error)
|
||||
UpdateConfigByScope(req request.NginxConfigUpdate) error
|
||||
GetStatus() (response.NginxStatus, error)
|
||||
UpdateConfigFile(req request.NginxConfigFileUpdate) error
|
||||
@@ -62,7 +62,10 @@ func (n NginxService) GetNginxConfig() (*response.NginxFile, error) {
|
||||
return &response.NginxFile{Content: string(byteContent)}, nil
|
||||
}
|
||||
|
||||
func (n NginxService) GetConfigByScope(req request.NginxScopeReq) ([]response.NginxParam, error) {
|
||||
func (n NginxService) GetConfigByScope(req request.NginxScopeReq) (interface{}, error) {
|
||||
if req.Scope == dto.Brotli {
|
||||
return getNginxBrotliParams()
|
||||
}
|
||||
keys, ok := dto.ScopeKeyMap[req.Scope]
|
||||
if !ok || len(keys) == 0 {
|
||||
return nil, nil
|
||||
@@ -71,6 +74,9 @@ func (n NginxService) GetConfigByScope(req request.NginxScopeReq) ([]response.Ng
|
||||
}
|
||||
|
||||
func (n NginxService) UpdateConfigByScope(req request.NginxConfigUpdate) error {
|
||||
if req.Scope == dto.Brotli {
|
||||
return updateNginxBrotliParams(getNginxParams(req.Params, dto.BrotliKeys))
|
||||
}
|
||||
keys, ok := dto.ScopeKeyMap[req.Scope]
|
||||
if !ok || len(keys) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
)
|
||||
|
||||
// stockNginxGzipDirectives is the gzip block shipped by the OpenResty app
|
||||
// since 1.21.4.3. The upgrade only rewrites values when the installed
|
||||
// nginx.conf still carries exactly these directives and values, which proves
|
||||
// the user never tuned compression. Any deviation aborts the rewrite.
|
||||
var stockNginxGzipDirectives = map[string]string{
|
||||
"gzip": "on",
|
||||
"gzip_min_length": "1k",
|
||||
"gzip_buffers": "4 16k",
|
||||
"gzip_http_version": "1.1",
|
||||
"gzip_comp_level": "2",
|
||||
"gzip_types": "text/plain application/javascript application/x-javascript text/javascript text/css application/xml",
|
||||
"gzip_vary": "on",
|
||||
"gzip_proxied": "expired no-cache no-store private auth",
|
||||
"gzip_disable": `"MSIE [1-6]\."`,
|
||||
}
|
||||
|
||||
// correctedNginxGzipDirectives replaces the stock values in place. gzip lives
|
||||
// in the http block of nginx.conf and must stay there: repeating it from an
|
||||
// included file would make nginx reject the configuration with a duplicate
|
||||
// directive error, and the compression settings page reads and writes these
|
||||
// same keys in nginx.conf.
|
||||
var correctedNginxGzipDirectives = map[string]string{
|
||||
"gzip_comp_level": "5",
|
||||
"gzip_types": strings.Join(nginxCompressibleTypes, " "),
|
||||
"gzip_proxied": "any",
|
||||
}
|
||||
|
||||
// obsoleteNginxGzipDirectives are dropped outright.
|
||||
var obsoleteNginxGzipDirectives = map[string]struct{}{
|
||||
// A per-request User-Agent regex for browsers with no measurable share.
|
||||
"gzip_disable": {},
|
||||
}
|
||||
|
||||
var nginxGzipDirectiveRe = regexp.MustCompile(`(?m)^[ \t]*(gzip[a-z_]*)[ \t]+([^;\n]*);[ \t]*$`)
|
||||
|
||||
func nginxMainConfigPath(install model.AppInstall) string {
|
||||
return path.Join(install.GetPath(), nginxModuleConfDir, "nginx.conf")
|
||||
}
|
||||
|
||||
// upgradeStockNginxGzipConfig rewrites the factory gzip defaults in place.
|
||||
//
|
||||
// Upgrades deliberately preserve the user's nginx.conf, so corrected defaults
|
||||
// shipped with a new OpenResty version would otherwise never reach existing
|
||||
// installations.
|
||||
//
|
||||
// The config parser is not used: its dumper regenerates the whole file, drops
|
||||
// standalone comments and reorders proxy includes, which would be destructive
|
||||
// on a user's main config. Lines are edited individually so everything outside
|
||||
// the gzip block stays byte-identical.
|
||||
func upgradeStockNginxGzipConfig(install model.AppInstall) error {
|
||||
configPath := nginxMainConfigPath(install)
|
||||
content, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !isStockNginxGzipConfig(string(content)) {
|
||||
return nil
|
||||
}
|
||||
updated := rewriteNginxGzipDirectives(string(content))
|
||||
if updated == string(content) {
|
||||
return nil
|
||||
}
|
||||
if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = nginxCheckAndReload(string(content), configPath, install.ContainerName); err != nil {
|
||||
return err
|
||||
}
|
||||
global.LOG.Info("updated the stock OpenResty gzip configuration to the current defaults")
|
||||
return nil
|
||||
}
|
||||
|
||||
// isStockNginxGzipConfig reports whether every gzip directive in the config
|
||||
// matches the factory defaults exactly, with none missing and none extra.
|
||||
func isStockNginxGzipConfig(content string) bool {
|
||||
found := make(map[string]string)
|
||||
for _, match := range nginxGzipDirectiveRe.FindAllStringSubmatch(content, -1) {
|
||||
name := match[1]
|
||||
value := strings.Join(strings.Fields(match[2]), " ")
|
||||
if _, ok := found[name]; ok {
|
||||
// A directive repeated in the http block means the config was
|
||||
// edited by hand; leave it alone.
|
||||
return false
|
||||
}
|
||||
found[name] = value
|
||||
}
|
||||
if len(found) != len(stockNginxGzipDirectives) {
|
||||
return false
|
||||
}
|
||||
for name, expected := range stockNginxGzipDirectives {
|
||||
if found[name] != expected {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// rewriteNginxGzipDirectives updates known values in place, drops obsolete
|
||||
// directives and appends directives that are missing, preserving the original
|
||||
// indentation and leaving every other line untouched.
|
||||
func rewriteNginxGzipDirectives(content string) string {
|
||||
lines := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
seen := make(map[string]struct{})
|
||||
lastGzipIndex := -1
|
||||
lastGzipIndent := " "
|
||||
|
||||
for _, line := range lines {
|
||||
match := nginxGzipDirectiveRe.FindStringSubmatch(line)
|
||||
if match == nil {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
name := match[1]
|
||||
// The indentation belongs to the line itself; a top-level directive
|
||||
// must not inherit the indent a previous, nested directive used.
|
||||
lineIndent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
if lineIndent == "" {
|
||||
lineIndent = " "
|
||||
}
|
||||
lastGzipIndent = lineIndent
|
||||
if _, obsolete := obsoleteNginxGzipDirectives[name]; obsolete {
|
||||
continue
|
||||
}
|
||||
seen[name] = struct{}{}
|
||||
if replacement, ok := correctedNginxGzipDirectives[name]; ok {
|
||||
result = append(result, lineIndent+name+" "+replacement+";")
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
lastGzipIndex = len(result) - 1
|
||||
}
|
||||
|
||||
// Directives introduced by a newer default set are appended right after
|
||||
// the existing block so they stay visually grouped.
|
||||
var missing []string
|
||||
for name := range correctedNginxGzipDirectives {
|
||||
if _, ok := seen[name]; !ok {
|
||||
missing = append(missing, name)
|
||||
}
|
||||
}
|
||||
if len(missing) == 0 || lastGzipIndex < 0 {
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
sort.Strings(missing)
|
||||
added := make([]string, 0, len(missing))
|
||||
for _, name := range missing {
|
||||
added = append(added, lastGzipIndent+name+" "+correctedNginxGzipDirectives[name]+";")
|
||||
}
|
||||
tail := append(added, result[lastGzipIndex+1:]...)
|
||||
return strings.Join(append(result[:lastGzipIndex+1], tail...), "\n")
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
|
||||
)
|
||||
|
||||
const stockNginxConf = `user root;
|
||||
worker_processes auto;
|
||||
|
||||
include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;
|
||||
|
||||
events {
|
||||
use epoll;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
server_names_hash_bucket_size 512;
|
||||
keepalive_requests 5000;
|
||||
|
||||
gzip on;
|
||||
gzip_min_length 1k;
|
||||
gzip_buffers 4 16k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_comp_level 2;
|
||||
gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml;
|
||||
gzip_vary on;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
|
||||
limit_conn_zone $binary_remote_addr zone=perip:10m;
|
||||
|
||||
include /usr/local/openresty/nginx/conf/http.d/*.conf;
|
||||
include /usr/local/openresty/nginx/conf/conf.d/*.conf;
|
||||
}
|
||||
`
|
||||
|
||||
func TestIsStockNginxGzipConfig(t *testing.T) {
|
||||
if !isStockNginxGzipConfig(stockNginxConf) {
|
||||
t.Fatal("factory configuration should be detected as stock")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsStockNginxGzipConfigRejectsTunedValues(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"comp level changed": strings.Replace(stockNginxConf, "gzip_comp_level 2;", "gzip_comp_level 6;", 1),
|
||||
"gzip disabled": strings.Replace(stockNginxConf, "gzip on;", "gzip off;", 1),
|
||||
"types extended": strings.Replace(stockNginxConf,
|
||||
"application/xml;", "application/xml application/json;", 1),
|
||||
"directive removed": strings.Replace(stockNginxConf, " gzip_vary on;\n", "", 1),
|
||||
"directive added": strings.Replace(stockNginxConf, " gzip_vary on;\n",
|
||||
" gzip_vary on;\n gzip_static on;\n", 1),
|
||||
}
|
||||
for name, content := range cases {
|
||||
if isStockNginxGzipConfig(content) {
|
||||
t.Errorf("%s: tuned configuration must not be rewritten", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsStockNginxGzipConfigRejectsDuplicateDirective(t *testing.T) {
|
||||
content := strings.Replace(stockNginxConf, " gzip on;\n", " gzip on;\n gzip on;\n", 1)
|
||||
if isStockNginxGzipConfig(content) {
|
||||
t.Fatal("a duplicated directive indicates a hand-edited config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteNginxGzipDirectives(t *testing.T) {
|
||||
result := rewriteNginxGzipDirectives(stockNginxConf)
|
||||
|
||||
for _, expected := range []string{
|
||||
" gzip_comp_level 5;",
|
||||
" gzip_proxied any;",
|
||||
" gzip on;",
|
||||
" gzip_vary on;",
|
||||
} {
|
||||
if !strings.Contains(result, expected) {
|
||||
t.Errorf("expected directive missing: %s\n%s", expected, result)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(result, "application/json") {
|
||||
t.Error("gzip_types should now cover application/json")
|
||||
}
|
||||
if strings.Contains(result, "gzip_disable") {
|
||||
t.Error("obsolete gzip_disable should have been dropped")
|
||||
}
|
||||
if strings.Contains(result, "gzip_comp_level 2;") {
|
||||
t.Error("stale comp level should have been replaced")
|
||||
}
|
||||
// Everything outside the gzip block must survive untouched.
|
||||
for _, keep := range []string{
|
||||
"server_names_hash_bucket_size 512;",
|
||||
"keepalive_requests 5000;",
|
||||
"limit_conn_zone $binary_remote_addr zone=perip:10m;",
|
||||
"include /usr/local/openresty/nginx/conf/http.d/*.conf;",
|
||||
"include /usr/local/openresty/nginx/conf/conf.d/*.conf;",
|
||||
"include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;",
|
||||
"user root;",
|
||||
} {
|
||||
if !strings.Contains(result, keep) {
|
||||
t.Errorf("unrelated line was altered or dropped: %s", keep)
|
||||
}
|
||||
}
|
||||
if !strings.HasSuffix(result, "}\n") {
|
||||
t.Error("trailing newline was not preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteNginxGzipDirectivesIsIdempotent(t *testing.T) {
|
||||
once := rewriteNginxGzipDirectives(stockNginxConf)
|
||||
twice := rewriteNginxGzipDirectives(once)
|
||||
if once != twice {
|
||||
t.Errorf("rewrite is not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", once, twice)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteNginxGzipDirectivesAppendsMissing(t *testing.T) {
|
||||
// gzip_proxied absent from the source must be appended, not silently lost.
|
||||
content := strings.Replace(stockNginxConf,
|
||||
" gzip_proxied expired no-cache no-store private auth;\n", "", 1)
|
||||
result := rewriteNginxGzipDirectives(content)
|
||||
if !strings.Contains(result, "gzip_proxied any;") {
|
||||
t.Errorf("missing directive was not appended:\n%s", result)
|
||||
}
|
||||
if !strings.Contains(result, "limit_conn_zone $binary_remote_addr zone=perip:10m;") {
|
||||
t.Error("appending must not clobber following lines")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteNginxGzipDirectivesKeepsGzipLikeNames(t *testing.T) {
|
||||
// gunzip and proxy_set_header must survive: only directives whose name
|
||||
// starts with "gzip" are managed here.
|
||||
content := "http {\n gunzip on;\n gzip on;\n proxy_set_header Accept-Encoding gzip;\n}\n"
|
||||
result := rewriteNginxGzipDirectives(content)
|
||||
if !strings.Contains(result, "gunzip on;") {
|
||||
t.Error("gunzip directive must be preserved")
|
||||
}
|
||||
if !strings.Contains(result, "proxy_set_header Accept-Encoding gzip;") {
|
||||
t.Error("proxy_set_header must be preserved")
|
||||
}
|
||||
if !strings.Contains(result, " gzip on;") {
|
||||
t.Error("gzip directive should be kept in place")
|
||||
}
|
||||
}
|
||||
|
||||
// The gzip.conf template, the upgrade maps and the appstore defaults are three
|
||||
// copies of one intent. Pin the first two so they cannot drift apart silently.
|
||||
func TestGzipTemplateMatchesCorrectedDefaults(t *testing.T) {
|
||||
template := nginx_conf.GetWebsiteFile("gzip.conf")
|
||||
if len(template) == 0 {
|
||||
t.Fatal("gzip.conf template is missing from the embedded files")
|
||||
}
|
||||
expected := make(map[string]string, len(stockNginxGzipDirectives))
|
||||
for name, value := range stockNginxGzipDirectives {
|
||||
expected[name] = value
|
||||
}
|
||||
for name := range obsoleteNginxGzipDirectives {
|
||||
delete(expected, name)
|
||||
}
|
||||
for name, value := range correctedNginxGzipDirectives {
|
||||
expected[name] = value
|
||||
}
|
||||
found := make(map[string]string)
|
||||
for _, match := range nginxGzipDirectiveRe.FindAllStringSubmatch(string(template), -1) {
|
||||
found[match[1]] = strings.Join(strings.Fields(match[2]), " ")
|
||||
}
|
||||
if len(found) != len(expected) {
|
||||
t.Fatalf("template has %d directives, corrected defaults have %d", len(found), len(expected))
|
||||
}
|
||||
for name, want := range expected {
|
||||
if got, ok := found[name]; !ok {
|
||||
t.Errorf("template is missing %s", name)
|
||||
} else if got != want {
|
||||
t.Errorf("%s: template has %q, corrected defaults have %q", name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
)
|
||||
|
||||
const (
|
||||
// nginxHTTPConfDir holds http-context directives generated by 1Panel.
|
||||
// load_module is a main-context directive and therefore lives in
|
||||
// modules-enabled, which cannot host http-context directives such as
|
||||
// "brotli on". The directory is included by nginx.conf before conf.d so
|
||||
// that per-site configuration keeps overriding these defaults.
|
||||
nginxHTTPConfDir = "http.d"
|
||||
|
||||
nginxHTTPConfigPrefix = "1panel-http-"
|
||||
nginxHTTPConfigHeader = "# Managed by 1Panel. Manual changes will be overwritten.\n"
|
||||
|
||||
// nginxHTTPIncludeDirective is the include line that loads the managed
|
||||
// directory. Fresh installs carry it in the shipped nginx.conf; existing
|
||||
// ones get it inserted by the panel the first time a module needs
|
||||
// http-context configuration.
|
||||
nginxHTTPIncludeDirective = "include /usr/local/openresty/nginx/conf/http.d/*.conf;"
|
||||
)
|
||||
|
||||
var (
|
||||
// nginxHTTPIncludeRe matches the include line wherever it appears. The
|
||||
// absolute path prefix, quoting and whitespace are all optional in the
|
||||
// match so a variant written by an older installer or by hand still
|
||||
// counts; a commented-out copy does not.
|
||||
nginxHTTPIncludeRe = regexp.MustCompile(`(?m)^[ \t]*include\s+"?(/usr/local/openresty/nginx/conf/)?http\.d/\*\.conf"?\s*;[ \t]*\r?$`)
|
||||
|
||||
// nginxConfDIncludeRe locates the site-config include, the preferred
|
||||
// insertion point, and captures its indentation.
|
||||
nginxConfDIncludeRe = regexp.MustCompile(`(?m)^([ \t]*)include\s+"?(/usr/local/openresty/nginx/conf/)?conf\.d/\*\.conf"?\s*;[ \t]*\r?$`)
|
||||
|
||||
// nginxHTTPBlockStartRe locates the http block opening, the fallback
|
||||
// insertion point, and captures its indentation.
|
||||
nginxHTTPBlockStartRe = regexp.MustCompile(`(?m)^([ \t]*)http[ \t]*\{[ \t]*\r?$`)
|
||||
)
|
||||
|
||||
// nginxHTTPIncludePresent reports whether nginx.conf already loads http.d.
|
||||
func nginxHTTPIncludePresent(install model.AppInstall) bool {
|
||||
content, err := os.ReadFile(nginxMainConfigPath(install))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return nginxHTTPIncludeRe.MatchString(string(content))
|
||||
}
|
||||
|
||||
// writeNginxFileAtomic writes through a temp file plus rename so a crash or a
|
||||
// concurrent reader never observes a half-written config.
|
||||
func writeNginxFileAtomic(filePath string, content []byte) error {
|
||||
tmpPath := filePath + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, content, constant.FilePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmpPath, filePath)
|
||||
}
|
||||
|
||||
// nginxFileLineEnding picks the file's own style so an inserted or rewritten
|
||||
// line does not mix LF into a CRLF file.
|
||||
func nginxFileLineEnding(content string) string {
|
||||
if strings.Contains(content, "\r\n") {
|
||||
return "\r\n"
|
||||
}
|
||||
return "\n"
|
||||
}
|
||||
|
||||
// insertNginxHTTPInclude returns the config with the http.d include added.
|
||||
//
|
||||
// The include goes right before the conf.d include so panel-managed defaults
|
||||
// are evaluated before per-site configuration; without one, it goes at the
|
||||
// top of the http block. The inserted line follows the file's own line-ending
|
||||
// style, and everything else stays byte-identical. A config without a
|
||||
// locatable http block is rejected, and callers degrade instead of failing
|
||||
// their operation over it.
|
||||
func insertNginxHTTPInclude(content string) (string, error) {
|
||||
if nginxHTTPIncludeRe.MatchString(content) {
|
||||
return content, nil
|
||||
}
|
||||
eol := nginxFileLineEnding(content)
|
||||
if m := nginxConfDIncludeRe.FindStringSubmatchIndex(content); m != nil {
|
||||
indent := content[m[2]:m[3]]
|
||||
return content[:m[0]] + indent + nginxHTTPIncludeDirective + eol + content[m[0]:], nil
|
||||
}
|
||||
if m := nginxHTTPBlockStartRe.FindStringSubmatchIndex(content); m != nil {
|
||||
indent := content[m[2]:m[3]] + " "
|
||||
return content[:m[1]] + eol + indent + nginxHTTPIncludeDirective + content[m[1]:], nil
|
||||
}
|
||||
return "", errors.New("no insertion point for the http.d include in nginx.conf")
|
||||
}
|
||||
|
||||
// ensureNginxHTTPIncludeActive makes nginx.conf load http.d, inserting the
|
||||
// include when missing. It returns whether the directory is loaded after the
|
||||
// call, plus the original config content so the caller can roll back the edit
|
||||
// together with the rest of its changes.
|
||||
func ensureNginxHTTPIncludeActive(install model.AppInstall) (active bool, snapshot []byte, err error) {
|
||||
configPath := nginxMainConfigPath(install)
|
||||
content, readErr := os.ReadFile(configPath)
|
||||
if readErr != nil {
|
||||
return false, nil, readErr
|
||||
}
|
||||
if nginxHTTPIncludeRe.MatchString(string(content)) {
|
||||
if err = os.MkdirAll(nginxHTTPConfigDir(install), constant.DirPerm); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
return true, nil, nil
|
||||
}
|
||||
updated, insErr := insertNginxHTTPInclude(string(content))
|
||||
if insErr != nil {
|
||||
return false, nil, insErr
|
||||
}
|
||||
if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if err = os.MkdirAll(nginxHTTPConfigDir(install), constant.DirPerm); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
return true, content, nil
|
||||
}
|
||||
|
||||
// nginxHTTPDirective is a single http-context directive rendered into a
|
||||
// managed file.
|
||||
type nginxHTTPDirective struct {
|
||||
Name string
|
||||
Params []string
|
||||
}
|
||||
|
||||
func (d nginxHTTPDirective) render() string {
|
||||
if len(d.Params) == 0 {
|
||||
return d.Name + ";"
|
||||
}
|
||||
return d.Name + " " + strings.Join(d.Params, " ") + ";"
|
||||
}
|
||||
|
||||
func nginxHTTPConfigDir(install model.AppInstall) string {
|
||||
return path.Join(install.GetPath(), nginxModuleConfDir, nginxHTTPConfDir)
|
||||
}
|
||||
|
||||
func nginxHTTPConfigFileName(order int, name string) string {
|
||||
return fmt.Sprintf("%s%04d-%s.conf", nginxHTTPConfigPrefix, order, nginxModulePathName(name))
|
||||
}
|
||||
|
||||
// renderNginxHTTPConfig builds the content of a managed http.d file.
|
||||
func renderNginxHTTPConfig(directives []nginxHTTPDirective) []byte {
|
||||
var content strings.Builder
|
||||
content.WriteString(nginxHTTPConfigHeader)
|
||||
for _, directive := range directives {
|
||||
content.WriteString(directive.render())
|
||||
content.WriteString("\n")
|
||||
}
|
||||
return []byte(content.String())
|
||||
}
|
||||
|
||||
var nginxHTTPDirectiveRe = regexp.MustCompile(`^[ \t]*([a-z_][a-z0-9_]*)[ \t]+([^;]*);[ \t]*$`)
|
||||
|
||||
// readNginxHTTPDirectives parses a managed file back into directive values.
|
||||
// A missing or unreadable file yields no directives, which makes callers fall
|
||||
// back to their defaults.
|
||||
func readNginxHTTPDirectives(filePath string) map[string][]string {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
directives := make(map[string][]string)
|
||||
for _, line := range strings.Split(string(content), "\n") {
|
||||
match := nginxHTTPDirectiveRe.FindStringSubmatch(line)
|
||||
if match == nil {
|
||||
continue
|
||||
}
|
||||
directives[match[1]] = strings.Fields(match[2])
|
||||
}
|
||||
return directives
|
||||
}
|
||||
|
||||
// snapshotManagedNginxHTTPConfigs captures every managed file so a failed
|
||||
// nginx -t can be rolled back.
|
||||
func snapshotManagedNginxHTTPConfigs(configDir string) (nginxModuleConfigSnapshot, error) {
|
||||
snapshot := make(nginxModuleConfigSnapshot)
|
||||
entries, err := os.ReadDir(configDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return snapshot, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasPrefix(entry.Name(), nginxHTTPConfigPrefix) {
|
||||
continue
|
||||
}
|
||||
content, readErr := os.ReadFile(path.Join(configDir, entry.Name()))
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
}
|
||||
snapshot[entry.Name()] = content
|
||||
}
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
// applyManagedNginxHTTPConfigs writes the desired managed files and removes
|
||||
// managed files that are no longer wanted. Files not carrying the managed
|
||||
// prefix are never touched.
|
||||
func applyManagedNginxHTTPConfigs(configDir string, desired map[string][]byte) error {
|
||||
if err := os.MkdirAll(configDir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
entries, err := os.ReadDir(configDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
names := make([]string, 0, len(desired))
|
||||
for fileName := range desired {
|
||||
names = append(names, fileName)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, fileName := range names {
|
||||
tmpPath := path.Join(configDir, "."+fileName+".tmp")
|
||||
if err = os.WriteFile(tmpPath, desired[fileName], constant.FilePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.Rename(tmpPath, path.Join(configDir, fileName)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasPrefix(entry.Name(), nginxHTTPConfigPrefix) {
|
||||
continue
|
||||
}
|
||||
if _, ok := desired[entry.Name()]; !ok {
|
||||
if err = os.Remove(path.Join(configDir, entry.Name())); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const plainNginxConf = `user root;
|
||||
worker_processes auto;
|
||||
|
||||
include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;
|
||||
|
||||
events {
|
||||
use epoll;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
gzip on;
|
||||
gzip_comp_level 5;
|
||||
|
||||
limit_conn_zone $binary_remote_addr zone=perip:10m;
|
||||
|
||||
include /usr/local/openresty/nginx/conf/conf.d/*.conf;
|
||||
include /usr/local/openresty/nginx/conf/default/*.conf;
|
||||
}
|
||||
`
|
||||
|
||||
func TestInsertNginxHTTPIncludeBeforeConfD(t *testing.T) {
|
||||
got, err := insertNginxHTTPInclude(plainNginxConf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(got, " "+nginxHTTPIncludeDirective) {
|
||||
t.Fatalf("include not inserted with matching indent:\n%s", got)
|
||||
}
|
||||
// Ordering is the point of the insertion site: panel defaults must be
|
||||
// evaluated before per-site configuration.
|
||||
httpIdx := strings.Index(got, "conf/http.d/*.conf")
|
||||
confDIdx := strings.Index(got, "conf/conf.d/*.conf")
|
||||
if httpIdx < 0 || confDIdx < 0 || httpIdx > confDIdx {
|
||||
t.Fatalf("http.d must be included before conf.d (http.d=%d conf.d=%d)", httpIdx, confDIdx)
|
||||
}
|
||||
// The rest of the file must be untouched.
|
||||
stripped := strings.Replace(got, " "+nginxHTTPIncludeDirective+"\n", "", 1)
|
||||
if stripped != plainNginxConf {
|
||||
t.Fatal("insertion altered content outside the inserted line")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertNginxHTTPIncludeIsIdempotent(t *testing.T) {
|
||||
once, err := insertNginxHTTPInclude(plainNginxConf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
twice, err := insertNginxHTTPInclude(once)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if twice != once {
|
||||
t.Fatal("a second insertion must be a no-op")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertNginxHTTPIncludeFallsBackToHTTPBlock(t *testing.T) {
|
||||
content := strings.Replace(plainNginxConf,
|
||||
" include /usr/local/openresty/nginx/conf/conf.d/*.conf;\n", "", 1)
|
||||
got, err := insertNginxHTTPInclude(content)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
httpIdx := strings.Index(got, "http {")
|
||||
incIdx := strings.Index(got, nginxHTTPIncludeDirective)
|
||||
if incIdx < 0 || incIdx < httpIdx {
|
||||
t.Fatalf("include should land inside the http block:\n%s", got)
|
||||
}
|
||||
// Indented one level deeper than the http keyword.
|
||||
if !strings.Contains(got, " "+nginxHTTPIncludeDirective) {
|
||||
t.Errorf("fallback indentation is wrong:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertNginxHTTPIncludeRejectsConfigWithoutHTTPBlock(t *testing.T) {
|
||||
if _, err := insertNginxHTTPInclude("events {}\n"); err == nil {
|
||||
t.Fatal("a config without an http block must be rejected so callers can degrade")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertNginxHTTPIncludeIgnoresCommentedIncludes(t *testing.T) {
|
||||
commented := strings.Replace(plainNginxConf,
|
||||
" include /usr/local/openresty/nginx/conf/conf.d/*.conf;",
|
||||
" # include /usr/local/openresty/nginx/conf/conf.d/*.conf;", 1)
|
||||
got, err := insertNginxHTTPInclude(commented)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The commented conf.d line is not a valid anchor; the fallback must win.
|
||||
if strings.Index(got, nginxHTTPIncludeDirective) < strings.Index(got, "http {") {
|
||||
t.Fatal("a commented include must not be used as the anchor")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertNginxHTTPIncludeHandlesCRLF(t *testing.T) {
|
||||
crlf := strings.ReplaceAll(plainNginxConf, "\n", "\r\n")
|
||||
got, err := insertNginxHTTPInclude(crlf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(got, nginxHTTPIncludeDirective) {
|
||||
t.Fatal("include missing on a CRLF file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxHTTPIncludeRe(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
content string
|
||||
want bool
|
||||
}{
|
||||
{"present", plainNginxConf + " " + nginxHTTPIncludeDirective + "\n", true},
|
||||
{"absent", plainNginxConf, false},
|
||||
{"commented out", "# " + nginxHTTPIncludeDirective, false},
|
||||
// nginx accepts quoted paths, and a hand-written or legacy installer
|
||||
// may use them; a quoted include must count as present.
|
||||
{"quoted absolute path", `include "/usr/local/openresty/nginx/conf/http.d/*.conf";`, true},
|
||||
{"quoted with extra whitespace", ` include "/usr/local/openresty/nginx/conf/http.d/*.conf" ;`, true},
|
||||
{"relative path form", ` include http.d/*.conf;`, true},
|
||||
{"a different directory does not count", ` include /usr/local/openresty/nginx/conf/conf.d/*.conf;`, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := nginxHTTPIncludeRe.MatchString(tc.content); got != tc.want {
|
||||
t.Fatalf("expected %v, got %v", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The anchor for insertion must tolerate the same variants, or a config with
|
||||
// a quoted conf.d include would take the http-block fallback for no reason.
|
||||
func TestInsertNginxHTTPIncludeWithQuotedConfDAnchor(t *testing.T) {
|
||||
quoted := strings.Replace(plainNginxConf,
|
||||
" include /usr/local/openresty/nginx/conf/conf.d/*.conf;",
|
||||
` include "/usr/local/openresty/nginx/conf/conf.d/*.conf";`, 1)
|
||||
got, err := insertNginxHTTPInclude(quoted)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
httpIdx := strings.Index(got, "conf/http.d/*.conf")
|
||||
confDIdx := strings.Index(got, "conf/conf.d/*.conf")
|
||||
if httpIdx < 0 || confDIdx < 0 || httpIdx > confDIdx {
|
||||
t.Fatalf("quoted anchor not used; include misplaced:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A CRLF file must keep its own line endings after insertion.
|
||||
func TestInsertNginxHTTPIncludeKeepsCRLFStyle(t *testing.T) {
|
||||
crlf := strings.ReplaceAll(plainNginxConf, "\n", "\r\n")
|
||||
got, err := insertNginxHTTPInclude(crlf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
idx := strings.Index(got, nginxHTTPIncludeDirective)
|
||||
if idx < 0 {
|
||||
t.Fatal("include missing")
|
||||
}
|
||||
if got[idx-1] == '\n' || (idx >= 2 && got[idx-2:idx] != "\r\n" && got[idx-1] != ' ') {
|
||||
// The inserted line must end with \r\n like the rest of the file.
|
||||
}
|
||||
end := idx + len(nginxHTTPIncludeDirective)
|
||||
if end+2 > len(got) || got[end:end+2] != "\r\n" {
|
||||
t.Fatalf("inserted line does not end with CRLF: %q", got[end:end+4])
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
@@ -164,6 +165,51 @@ func nginxModuleDynamicSupported(install model.AppInstall) bool {
|
||||
fileOp.Stat(path.Join(buildPath, nginxModuleCatalogFile))
|
||||
}
|
||||
|
||||
// nginxModuleStaticSupported reports whether the install can recompile its own
|
||||
// OpenResty image, which is what a static module build needs. Versions before
|
||||
// dynamic modules existed ship a compose file with a build section and the
|
||||
// sources under build/; the oldest ones only reference a prebuilt image and
|
||||
// cannot compile anything.
|
||||
func nginxModuleStaticSupported(install model.AppInstall) bool {
|
||||
if !files.NewFileOp().Stat(path.Join(install.GetPath(), nginxModuleBuildDir, "Dockerfile")) {
|
||||
return false
|
||||
}
|
||||
envStr, err := coverEnvJsonToStr(install.Env)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
project, err := dockerUtils.GetComposeProject(install.Name, install.GetPath(),
|
||||
[]byte(install.DockerCompose), []byte(envStr), true)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, service := range project.AllServices() {
|
||||
if service.Build != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// defaultNginxModuleBuildMode picks the mode an install can actually perform.
|
||||
//
|
||||
// Module state written before build modes existed carries no buildMode at all.
|
||||
// Rejecting it would fail loadNginxModules, and with it every module operation
|
||||
// and the upgrade itself, so the value is inferred from what the install can
|
||||
// do rather than assumed.
|
||||
func defaultNginxModuleBuildMode(install model.AppInstall) string {
|
||||
if nginxModuleDynamicSupported(install) {
|
||||
return nginxModuleBuildDynamic
|
||||
}
|
||||
if nginxModuleStaticSupported(install) {
|
||||
return nginxModuleBuildStatic
|
||||
}
|
||||
// Neither builder is available. Dynamic keeps the module inert instead of
|
||||
// triggering an image rebuild that cannot succeed; the build itself still
|
||||
// reports the missing capability.
|
||||
return nginxModuleBuildDynamic
|
||||
}
|
||||
|
||||
func syncNginxModuleBuilder(detailBuildDir, installBuildDir string) error {
|
||||
sourcePath := path.Join(detailBuildDir, nginxModuleBuilderFile)
|
||||
targetPath := path.Join(installBuildDir, nginxModuleBuilderFile)
|
||||
@@ -654,10 +700,56 @@ func reconcileDynamicNginxModuleConfig(install model.AppInstall, modules []dto.N
|
||||
return fmt.Errorf("validate combined dynamic module configuration: %w", err)
|
||||
}
|
||||
}
|
||||
if err = applyManagedNginxModuleConfigs(configDir, desired); err != nil {
|
||||
|
||||
// Runtime directives live in http.d because load_module is main-context
|
||||
// while directives such as "brotli on" are http-context. Both sets are
|
||||
// written before nginx -t runs, so nginx only ever observes the final,
|
||||
// consistent state; on failure both are rolled back together.
|
||||
//
|
||||
// The include that loads http.d is inserted on demand: only when a module
|
||||
// actually needs runtime configuration. An install whose nginx.conf cannot
|
||||
// be edited safely keeps the previous behaviour — the module loads but the
|
||||
// runtime directives are skipped — rather than failing the operation.
|
||||
httpConfigDir := nginxHTTPConfigDir(install)
|
||||
desiredHTTP := desiredNginxModuleRuntimeConfigs(install, modules, target)
|
||||
httpActive := nginxHTTPIncludePresent(install)
|
||||
var nginxConfSnapshot []byte
|
||||
if len(desiredHTTP) > 0 && !httpActive {
|
||||
active, confSnapshot, includeErr := ensureNginxHTTPIncludeActive(install)
|
||||
if includeErr != nil {
|
||||
global.LOG.Warnf("cannot insert the http.d include into nginx.conf, skipping runtime directives: %v", includeErr)
|
||||
desiredHTTP = nil
|
||||
} else {
|
||||
httpActive = active
|
||||
nginxConfSnapshot = confSnapshot
|
||||
}
|
||||
}
|
||||
var httpSnapshot nginxModuleConfigSnapshot
|
||||
if httpActive {
|
||||
if httpSnapshot, err = snapshotManagedNginxHTTPConfigs(httpConfigDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
restore := func() {
|
||||
_ = applyManagedNginxModuleConfigs(configDir, snapshot)
|
||||
if httpActive {
|
||||
_ = applyManagedNginxHTTPConfigs(httpConfigDir, httpSnapshot)
|
||||
}
|
||||
if nginxConfSnapshot != nil {
|
||||
_ = os.WriteFile(nginxMainConfigPath(install), nginxConfSnapshot, constant.FilePerm)
|
||||
}
|
||||
}
|
||||
|
||||
if err = applyManagedNginxModuleConfigs(configDir, desired); err != nil {
|
||||
restore()
|
||||
return err
|
||||
}
|
||||
if httpActive {
|
||||
if err = applyManagedNginxHTTPConfigs(httpConfigDir, desiredHTTP); err != nil {
|
||||
restore()
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !reload {
|
||||
return nil
|
||||
}
|
||||
@@ -666,11 +758,11 @@ func reconcileDynamicNginxModuleConfig(install model.AppInstall, modules []dto.N
|
||||
return nil
|
||||
}
|
||||
if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil {
|
||||
_ = applyManagedNginxModuleConfigs(configDir, snapshot)
|
||||
restore()
|
||||
return err
|
||||
}
|
||||
if err = opNginx(install.ContainerName, constant.NginxReload); err != nil {
|
||||
_ = applyManagedNginxModuleConfigs(configDir, snapshot)
|
||||
restore()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -722,6 +814,14 @@ func applyManagedNginxModuleConfigs(configDir string, desired map[string][]byte)
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasEnabledStaticNginxModules reports whether a full image rebuild is needed.
|
||||
//
|
||||
// Module state is the only input on purpose. RESTY_CONFIG_OPTIONS_MORE in .env
|
||||
// is derived state: configureStaticNginxModules rewrites it from the modules
|
||||
// below, and every build path calls that function before building. Treating a
|
||||
// leftover value as a reason to rebuild would start a full recompile that
|
||||
// configureStaticNginxModules has already reduced to an empty option list, so
|
||||
// the rebuild could only reproduce the image it started from.
|
||||
func hasEnabledStaticNginxModules(modules []dto.NginxModule) bool {
|
||||
for _, module := range modules {
|
||||
normalizeNginxModule(&module)
|
||||
@@ -732,17 +832,6 @@ func hasEnabledStaticNginxModules(modules []dto.NginxModule) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func staticNginxBuildRequired(install model.AppInstall, modules []dto.NginxModule) bool {
|
||||
if hasEnabledStaticNginxModules(modules) {
|
||||
return true
|
||||
}
|
||||
envs, err := gotenv.Read(install.GetEnvPath())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(envs["RESTY_CONFIG_OPTIONS_MORE"]) != ""
|
||||
}
|
||||
|
||||
func configureStaticNginxModules(install model.AppInstall, modules []dto.NginxModule, mirror string) error {
|
||||
buildPath := path.Join(install.GetPath(), nginxModuleBuildDir)
|
||||
var params, packages []string
|
||||
@@ -807,11 +896,17 @@ func executeNginxModuleBuild(install model.AppInstall, reqModules []string, forc
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
staticBuild := staticNginxBuildRequired(install, modules)
|
||||
if !staticBuild && hasDynamicNginxModuleBuildTask(modules, reqModules) {
|
||||
if !nginxModuleDynamicSupported(install) {
|
||||
return errors.New("the installed OpenResty version does not support dynamic module builds")
|
||||
}
|
||||
// Only the module list decides this. A leftover RESTY_CONFIG_OPTIONS_MORE
|
||||
// used to force the static path here, which meant a full image rebuild for
|
||||
// an install that has no static module left to compile.
|
||||
staticBuild := hasEnabledStaticNginxModules(modules)
|
||||
if !staticBuild && hasDynamicNginxModuleBuildTask(modules, reqModules) && !nginxModuleDynamicSupported(install) {
|
||||
// The catalog and the builder have always shipped together, and an
|
||||
// install missing the catalog fails to load its module state before
|
||||
// this point, so this branch is a guard rather than a real path. Keep
|
||||
// the error actionable instead of faking a build the state machine
|
||||
// cannot record.
|
||||
return buserr.New("ErrModuleBuildUnsupported")
|
||||
}
|
||||
if staticBuild {
|
||||
return executeStaticNginxModuleBuild(install, modules, mirror, force, parentTask)
|
||||
@@ -886,7 +981,17 @@ func loadNginxModulesWithCatalog(install model.AppInstall, catalogPath string) (
|
||||
Builds: state.Builds, LastError: state.LastError,
|
||||
})
|
||||
}
|
||||
// Catalog entries always declare a mode; state written before build modes
|
||||
// existed does not. Fill the gap from the install's capabilities so an
|
||||
// upgrade from such a version can still read its own module state.
|
||||
fallbackMode := ""
|
||||
for i := range modules {
|
||||
if modules[i].BuildMode == "" {
|
||||
if fallbackMode == "" {
|
||||
fallbackMode = defaultNginxModuleBuildMode(install)
|
||||
}
|
||||
modules[i].BuildMode = fallbackMode
|
||||
}
|
||||
if err = validateNginxModuleBuildMode(modules[i]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
)
|
||||
|
||||
// nginxCompressibleTypes is shared by gzip_types and brotli_types so both
|
||||
// encoders cover the same content. Already compressed formats (images other
|
||||
// than SVG, woff/woff2, archives, media) are deliberately excluded:
|
||||
// recompressing them costs CPU and usually grows the payload.
|
||||
var nginxCompressibleTypes = []string{
|
||||
"text/plain",
|
||||
"text/css",
|
||||
"text/xml",
|
||||
"text/javascript",
|
||||
"application/json",
|
||||
"application/ld+json",
|
||||
"application/javascript",
|
||||
"application/x-javascript",
|
||||
"application/xml",
|
||||
"application/xhtml+xml",
|
||||
"application/rss+xml",
|
||||
"application/atom+xml",
|
||||
"application/wasm",
|
||||
"image/svg+xml",
|
||||
"font/ttf",
|
||||
"font/otf",
|
||||
}
|
||||
|
||||
// nginxModuleRuntimeDefaults maps a module to the http-context directives that
|
||||
// make it actually do something once loaded. Without these, enabling a module
|
||||
// only emits load_module, leaving it loaded but inert.
|
||||
//
|
||||
// brotli_static is intentionally omitted: nginx does not verify that a .br
|
||||
// file is newer than its source, so a stale artifact would be served
|
||||
// indefinitely with no error.
|
||||
var nginxModuleRuntimeDefaults = map[string][]nginxHTTPDirective{
|
||||
"ngx_brotli": {
|
||||
{Name: "brotli", Params: []string{"on"}},
|
||||
// Brotli level 5 reaches roughly gzip level 9 ratio at a fraction of
|
||||
// the cost. The nginx default of 6 is tuned for static assets and is
|
||||
// too expensive for dynamic responses.
|
||||
{Name: "brotli_comp_level", Params: []string{"5"}},
|
||||
{Name: "brotli_min_length", Params: []string{"1k"}},
|
||||
{Name: "brotli_types", Params: nginxCompressibleTypes},
|
||||
},
|
||||
}
|
||||
|
||||
// nginxModuleRuntimeLoadOrder keeps managed file names stable and ordered
|
||||
// independently of the module load order used for load_module.
|
||||
var nginxModuleRuntimeLoadOrder = map[string]int{
|
||||
"ngx_brotli": 100,
|
||||
}
|
||||
|
||||
func nginxModuleRuntimeOrder(name string) int {
|
||||
if order, ok := nginxModuleRuntimeLoadOrder[name]; ok {
|
||||
return order
|
||||
}
|
||||
return 900
|
||||
}
|
||||
|
||||
// desiredNginxModuleRuntimeConfigs renders the managed http.d files for every
|
||||
// enabled module that has a ready build and known runtime defaults.
|
||||
//
|
||||
// Values the user changed through the compression settings page are read back
|
||||
// from the current managed file, so reconciling after an unrelated module
|
||||
// change does not silently reset them to the defaults.
|
||||
//
|
||||
// A module the user already configured by hand in nginx.conf is skipped
|
||||
// entirely. Emitting the same directive from an included file would make nginx
|
||||
// reject the configuration as a duplicate, so their setup is left as the only
|
||||
// definition.
|
||||
func desiredNginxModuleRuntimeConfigs(install model.AppInstall, modules []dto.NginxModule, target dto.NginxModuleTarget) map[string][]byte {
|
||||
desired := make(map[string][]byte)
|
||||
for _, module := range modules {
|
||||
normalizeNginxModule(&module)
|
||||
// A custom module that happens to share a built-in name must not pick
|
||||
// up the built-in's runtime defaults; the table is for catalog modules.
|
||||
if module.Custom {
|
||||
continue
|
||||
}
|
||||
directives, ok := nginxModuleRuntimeDefaults[module.Name]
|
||||
if !ok || !module.Enable {
|
||||
continue
|
||||
}
|
||||
if !nginxModuleRuntimeReady(module, target) {
|
||||
continue
|
||||
}
|
||||
if nginxModuleConfiguredByUser(install, module.Name) {
|
||||
continue
|
||||
}
|
||||
fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(module.Name), module.Name)
|
||||
current := readNginxHTTPDirectives(path.Join(nginxHTTPConfigDir(install), fileName))
|
||||
desired[fileName] = renderNginxHTTPConfig(mergeNginxRuntimeDirectives(directives, current))
|
||||
}
|
||||
return desired
|
||||
}
|
||||
|
||||
// nginxModuleConfiguredByUser reports whether the user already manages any of
|
||||
// the module's directives by hand.
|
||||
//
|
||||
// Users who enabled brotli before the panel managed it did so by editing
|
||||
// nginx.conf or a file it includes. That definition has to keep winning: it is
|
||||
// the one nginx has been running with, and adding a second one from http.d
|
||||
// would break the configuration outright.
|
||||
//
|
||||
// Any brotli* directive counts, not just the primary one. A user who only
|
||||
// tuned brotli_comp_level has still taken ownership of the block, and nginx
|
||||
// allows the same directive at http and server scope, so a site-scoped value
|
||||
// must suppress the managed one too.
|
||||
func nginxModuleConfiguredByUser(install model.AppInstall, moduleName string) bool {
|
||||
if _, ok := nginxModuleRuntimeDefaults[moduleName]; !ok {
|
||||
return false
|
||||
}
|
||||
for _, filePath := range nginxModuleUserConfigPaths(install) {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if nginxModuleUserDirectiveRe.MatchString(string(content)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// nginxModuleUserDirectiveRe matches any active (non-commented) brotli*
|
||||
// directive at the start of a line, wherever it was written.
|
||||
var nginxModuleUserDirectiveRe = regexp.MustCompile(`(?m)^[ \t]*brotli[a-z_]*[ \t]+[^;\n]*;`)
|
||||
|
||||
// nginxModuleUserConfigPaths lists the files that may carry a user's brotli
|
||||
// configuration: the main config and the http-scope files it includes. The
|
||||
// stream include is skipped on purpose — brotli is an http module and has no
|
||||
// business there.
|
||||
func nginxModuleUserConfigPaths(install model.AppInstall) []string {
|
||||
return nginxModuleUserConfigPathsWithSiteDir(install, GetWebSiteRootDir())
|
||||
}
|
||||
|
||||
// nginxModuleUserConfigPathsWithSiteDir is the testable core: the site conf
|
||||
// directory is injected so unit tests do not need the settings database.
|
||||
func nginxModuleUserConfigPathsWithSiteDir(install model.AppInstall, siteDir string) []string {
|
||||
paths := []string{nginxMainConfigPath(install)}
|
||||
paths = append(paths, globConfFiles(path.Join(siteDir, "conf.d"))...)
|
||||
paths = append(paths, globConfFiles(path.Join(install.GetPath(), nginxModuleConfDir, "default"))...)
|
||||
return paths
|
||||
}
|
||||
|
||||
func globConfFiles(dir string) []string {
|
||||
matches, err := filepath.Glob(path.Join(dir, "*.conf"))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
// nginxUserDirectivePattern matches a directive the user wrote in nginx.conf,
|
||||
// capturing its indentation so a rewrite can keep the line's shape. Leading
|
||||
// whitespace only, so a commented-out line never matches.
|
||||
func nginxUserDirectivePattern(name string) *regexp.Regexp {
|
||||
return regexp.MustCompile(`(?m)^([ \t]*)` + regexp.QuoteMeta(name) + `[ \t]+[^;\n]*;`)
|
||||
}
|
||||
|
||||
// nginxConfigDefinesDirective reports whether a directive is set anywhere in
|
||||
// the file, ignoring commented-out lines.
|
||||
func nginxConfigDefinesDirective(content, name string) bool {
|
||||
return nginxUserDirectivePattern(name).MatchString(content)
|
||||
}
|
||||
|
||||
// mergeNginxRuntimeDirectives keeps the declared directive set and ordering
|
||||
// while preferring values already present in the managed file.
|
||||
func mergeNginxRuntimeDirectives(defaults []nginxHTTPDirective, current map[string][]string) []nginxHTTPDirective {
|
||||
if len(current) == 0 {
|
||||
return defaults
|
||||
}
|
||||
merged := make([]nginxHTTPDirective, 0, len(defaults))
|
||||
for _, directive := range defaults {
|
||||
if params, ok := current[directive.Name]; ok && len(params) > 0 {
|
||||
directive.Params = params
|
||||
}
|
||||
merged = append(merged, directive)
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
// nginxBrotliModuleName is the catalog name of the brotli module.
|
||||
const nginxBrotliModuleName = "ngx_brotli"
|
||||
|
||||
// getNginxBrotliParams reports the brotli settings currently in effect, and
|
||||
// where they come from.
|
||||
//
|
||||
// Brotli is normally served from the managed http.d file instead of
|
||||
// nginx.conf, so the directives can be removed together with the module. When
|
||||
// the module is disabled the declared defaults are returned, which lets the
|
||||
// settings page show what would be applied once it is enabled.
|
||||
//
|
||||
// If the user configured brotli anywhere nginx loads it from, those values
|
||||
// are reported instead and ManagedExternally is set. Showing the managed
|
||||
// defaults there would misrepresent what the server is actually running, and
|
||||
// the panel must not write a second copy.
|
||||
func getNginxBrotliParams() (*response.NginxBrotliRes, error) {
|
||||
install, err := getAppInstallByKey(constant.AppOpenresty)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
managedExternally := nginxModuleConfiguredByUser(install, nginxBrotliModuleName)
|
||||
var current map[string][]string
|
||||
if managedExternally {
|
||||
current = readNginxUserBrotliDirectives(install)
|
||||
} else {
|
||||
fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
|
||||
current = readNginxHTTPDirectives(path.Join(nginxHTTPConfigDir(install), fileName))
|
||||
}
|
||||
res := &response.NginxBrotliRes{
|
||||
ManagedExternally: managedExternally,
|
||||
// Without the include, values the panel would write would never reach
|
||||
// nginx, so they are reported as unavailable rather than shown as if
|
||||
// they were in effect.
|
||||
ManagedUnavailable: !managedExternally && !nginxHTTPIncludePresent(install),
|
||||
}
|
||||
for _, directive := range mergeNginxRuntimeDirectives(nginxModuleRuntimeDefaults[nginxBrotliModuleName], current) {
|
||||
res.Params = append(res.Params, response.NginxParam{Name: directive.Name, Params: directive.Params})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// readNginxUserBrotliDirectives collects the brotli directives the user wrote
|
||||
// in any of the files nginx loads them from.
|
||||
func readNginxUserBrotliDirectives(install model.AppInstall) map[string][]string {
|
||||
directives := make(map[string][]string)
|
||||
for _, filePath := range nginxModuleUserConfigPaths(install) {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, name := range dto.BrotliKeys {
|
||||
pattern := regexp.MustCompile(`(?m)^[ \t]*` + regexp.QuoteMeta(name) + `[ \t]+([^;\n]*);`)
|
||||
if match := pattern.FindStringSubmatch(string(content)); match != nil {
|
||||
if _, exists := directives[name]; !exists {
|
||||
directives[name] = strings.Fields(strings.TrimSpace(match[1]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return directives
|
||||
}
|
||||
|
||||
// nginxBrotliValueRe whitelists what a brotli value may contain. The values
|
||||
// are written into nginx.conf and the managed files verbatim; rejecting
|
||||
// anything outside this set blocks both directive injection (`;`, newline,
|
||||
// braces, quotes) and the `$` group-reference expansion of
|
||||
// regexp.ReplaceAllString, which the in-place rewrite uses.
|
||||
var nginxBrotliValueRe = regexp.MustCompile(`^[a-zA-Z0-9._+\-/:* ]+$`)
|
||||
|
||||
// validateNginxBrotliValues rejects any value outside the whitelist. The UI
|
||||
// only sends on/off, numbers and sizes, but the endpoint is reachable
|
||||
// directly.
|
||||
func validateNginxBrotliValues(values map[string][]string) error {
|
||||
for name, params := range values {
|
||||
for _, param := range params {
|
||||
if !nginxBrotliValueRe.MatchString(param) {
|
||||
return buserr.WithDetail("ErrInvalidParams", fmt.Sprintf("invalid value for %s", name), nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateNginxBrotliParams persists brotli settings to the managed http.d file.
|
||||
//
|
||||
// Writing is refused unless the module is enabled and built: the directives
|
||||
// would reference a module that is not loaded and nginx would fail to start.
|
||||
func updateNginxBrotliParams(params []dto.NginxParam) error {
|
||||
install, err := getAppInstallByKey(constant.AppOpenresty)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
modules, err := loadNginxModules(install)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
values := make(map[string][]string, len(params))
|
||||
for _, param := range params {
|
||||
values[param.Name] = param.Params
|
||||
}
|
||||
if err = validateNginxBrotliValues(values); err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range modules {
|
||||
if modules[i].Name != nginxBrotliModuleName {
|
||||
continue
|
||||
}
|
||||
if !modules[i].Enable {
|
||||
return buserr.New("ErrBrotliDisabled")
|
||||
}
|
||||
// The user configured brotli in nginx.conf before the panel managed
|
||||
// it. Update those lines in place: writing a managed file as well
|
||||
// would define every directive twice and nginx would refuse to start.
|
||||
if nginxModuleConfiguredByUser(install, nginxBrotliModuleName) {
|
||||
return updateUserNginxBrotliParams(install, values)
|
||||
}
|
||||
// A managed write needs the include. Installations missing it are
|
||||
// upgraded in place here; when nginx.conf cannot be edited safely the
|
||||
// write is refused with an actionable error instead of writing values
|
||||
// nginx would never load.
|
||||
if !nginxHTTPIncludePresent(install) {
|
||||
configPath := nginxMainConfigPath(install)
|
||||
content, readErr := os.ReadFile(configPath)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
updated, insErr := insertNginxHTTPInclude(string(content))
|
||||
if insErr != nil {
|
||||
return buserr.New("ErrBrotliUnsupported")
|
||||
}
|
||||
if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.MkdirAll(nginxHTTPConfigDir(install), constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = nginxCheckAndReload(string(content), configPath, install.ContainerName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
|
||||
configDir := nginxHTTPConfigDir(install)
|
||||
snapshot, snapErr := snapshotManagedNginxHTTPConfigs(configDir)
|
||||
if snapErr != nil {
|
||||
return snapErr
|
||||
}
|
||||
merged := mergeNginxRuntimeDirectives(nginxModuleRuntimeDefaults[nginxBrotliModuleName], values)
|
||||
desired := map[string][]byte{fileName: renderNginxHTTPConfig(merged)}
|
||||
for name, content := range snapshot {
|
||||
if name != fileName {
|
||||
desired[name] = content
|
||||
}
|
||||
}
|
||||
if err = applyManagedNginxHTTPConfigs(configDir, desired); err != nil {
|
||||
_ = applyManagedNginxHTTPConfigs(configDir, snapshot)
|
||||
return err
|
||||
}
|
||||
if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil {
|
||||
_ = applyManagedNginxHTTPConfigs(configDir, snapshot)
|
||||
return err
|
||||
}
|
||||
if err = opNginx(install.ContainerName, constant.NginxReload); err != nil {
|
||||
_ = applyManagedNginxHTTPConfigs(configDir, snapshot)
|
||||
return err
|
||||
}
|
||||
// The directory is bind-mounted read-only and the include is a glob: a
|
||||
// missing mount or an unrecognised include lets nginx -t pass while
|
||||
// loading nothing. Read the effective configuration back instead of
|
||||
// trusting the files we wrote.
|
||||
if err = assertNginxBrotliActive(install.ContainerName); err != nil {
|
||||
_ = applyManagedNginxHTTPConfigs(configDir, snapshot)
|
||||
return buserr.New("ErrBrotliUnsupported")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return buserr.New("ErrBrotliDisabled")
|
||||
}
|
||||
|
||||
// assertNginxBrotliActive confirms the managed brotli directives are in the
|
||||
// running server's effective configuration. It is the only check that catches
|
||||
// a bind mount that never reached the container or an include variant the
|
||||
// detection missed — both pass nginx -t and reload silently.
|
||||
func assertNginxBrotliActive(containerName string) error {
|
||||
out, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout(
|
||||
"docker", "exec", "-i", containerName, "nginx", "-T")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !nginxModuleUserDirectiveRe.MatchString(out) {
|
||||
return errors.New("brotli directives are not in the effective nginx configuration")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateUserNginxBrotliParams rewrites the brotli directives the user wrote
|
||||
// into nginx.conf, in place.
|
||||
//
|
||||
// Only the values change: each directive keeps its original line and
|
||||
// indentation, and every other line is untouched, so a hand-maintained config
|
||||
// survives an edit from the settings page. Directives the user did not write
|
||||
// are not introduced, since the panel cannot know where they intended them.
|
||||
//
|
||||
// A managed file can still be on disk when the panel managed brotli before
|
||||
// the user wrote their own directives. Leaving it behind would make every
|
||||
// directive duplicate once the user's config is touched, so it is removed
|
||||
// first and rolled back together with the config on a failed nginx -t.
|
||||
func updateUserNginxBrotliParams(install model.AppInstall, values map[string][]string) error {
|
||||
configPath := nginxMainConfigPath(install)
|
||||
content, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configDir := nginxHTTPConfigDir(install)
|
||||
httpSnapshot, snapErr := snapshotManagedNginxHTTPConfigs(configDir)
|
||||
if snapErr != nil {
|
||||
return snapErr
|
||||
}
|
||||
managedFile := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
|
||||
if _, stale := httpSnapshot[managedFile]; stale {
|
||||
remaining := make(map[string][]byte, len(httpSnapshot))
|
||||
for name, fileContent := range httpSnapshot {
|
||||
if name != managedFile {
|
||||
remaining[name] = fileContent
|
||||
}
|
||||
}
|
||||
if err = applyManagedNginxHTTPConfigs(configDir, remaining); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
restore := func() {
|
||||
_ = writeNginxFileAtomic(configPath, content)
|
||||
_ = applyManagedNginxHTTPConfigs(configDir, httpSnapshot)
|
||||
}
|
||||
|
||||
updated := string(content)
|
||||
for _, name := range dto.BrotliKeys {
|
||||
params, ok := values[name]
|
||||
if !ok || len(params) == 0 {
|
||||
continue
|
||||
}
|
||||
pattern := nginxUserDirectivePattern(name)
|
||||
if !pattern.MatchString(updated) {
|
||||
continue
|
||||
}
|
||||
replacement := "${1}" + name + " " + strings.Join(params, " ") + ";"
|
||||
updated = pattern.ReplaceAllString(updated, replacement)
|
||||
}
|
||||
if updated == string(content) {
|
||||
return nil
|
||||
}
|
||||
if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil {
|
||||
restore()
|
||||
return err
|
||||
}
|
||||
if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil {
|
||||
restore()
|
||||
return err
|
||||
}
|
||||
if err = opNginx(install.ContainerName, constant.NginxReload); err != nil {
|
||||
restore()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nginxModuleRuntimeReady reports whether the module is actually usable.
|
||||
//
|
||||
// Dynamic modules need a ready build for the current target, otherwise the
|
||||
// .so is missing and nginx would reject the directives. Static modules are
|
||||
// compiled into the binary and carry no artifacts, so an enabled static
|
||||
// module is considered ready. This rests on a data premise: the catalog only
|
||||
// declares a module static when the image ships it. Checking for a build
|
||||
// record instead would be wrong here — reconcile runs inside the static build
|
||||
// flow, before the record for the build in progress exists, and would drop
|
||||
// the runtime configuration of the module that was just compiled in.
|
||||
func nginxModuleRuntimeReady(module dto.NginxModule, target dto.NginxModuleTarget) bool {
|
||||
if module.BuildMode == nginxModuleBuildStatic {
|
||||
return true
|
||||
}
|
||||
build := findCurrentNginxModuleBuild(module, target)
|
||||
if build == nil || build.Status != nginxModuleStatusReady {
|
||||
build = findLatestNginxModuleBuild(module, target)
|
||||
}
|
||||
return build != nil && build.Status == nginxModuleStatusReady
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
)
|
||||
|
||||
const userBrotliConf = `user root;
|
||||
worker_processes auto;
|
||||
|
||||
include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;
|
||||
|
||||
events { use epoll; }
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
gzip on;
|
||||
gzip_comp_level 5;
|
||||
|
||||
# enabled by hand, long before the panel managed it
|
||||
brotli on;
|
||||
brotli_comp_level 6;
|
||||
brotli_types text/plain text/css application/json;
|
||||
|
||||
include /usr/local/openresty/nginx/conf/http.d/*.conf;
|
||||
include /usr/local/openresty/nginx/conf/conf.d/*.conf;
|
||||
}
|
||||
`
|
||||
|
||||
func TestNginxConfigDefinesDirective(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
content string
|
||||
want bool
|
||||
}{
|
||||
{"directive present", userBrotliConf, true},
|
||||
{"absent", strings.Replace(userBrotliConf, " brotli on;\n", "", 1), false},
|
||||
{
|
||||
name: "commented out does not count",
|
||||
content: strings.Replace(userBrotliConf, " brotli on;", " # brotli on;", 1),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "a longer directive name is not a match",
|
||||
content: "http {\n brotli_comp_level 6;\n}\n",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "indentation does not matter",
|
||||
content: "http {\n\t\tbrotli on;\n}\n",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := nginxConfigDefinesDirective(tc.content, "brotli"); got != tc.want {
|
||||
t.Fatalf("expected %v, got %v", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The detection must catch any brotli* directive, in any file nginx loads it
|
||||
// from, not only the primary directive in nginx.conf.
|
||||
func TestNginxModuleUserDirectiveRe(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
content string
|
||||
want bool
|
||||
}{
|
||||
{"primary directive", "http {\n brotli on;\n}", true},
|
||||
{"a tuning directive alone", "server {\n brotli_comp_level 11;\n}", true},
|
||||
{"another variant", "http {\n brotli_types text/plain;\n}", true},
|
||||
{"server scope in a site file", "server {\n listen 80;\n brotli on;\n}", true},
|
||||
{"commented out does not count", "http {\n # brotli on;\n}", false},
|
||||
{"indented comment does not count", "http {\n # brotli_comp_level 6;\n}", false},
|
||||
{"no brotli at all", "http {\n gzip on;\n}", false},
|
||||
{"a similarly named directive is not a match", "http {\n gzip on;\n}", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := nginxModuleUserDirectiveRe.MatchString(tc.content); got != tc.want {
|
||||
t.Fatalf("expected %v, got %v", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The panel must not emit a managed file for a module the user already
|
||||
// configured: nginx rejects the same directive defined twice.
|
||||
func TestUserConfiguredBrotliSuppressesManagedFile(t *testing.T) {
|
||||
if !nginxModuleUserDirectiveRe.MatchString(userBrotliConf) {
|
||||
t.Fatal("a hand-written brotli config must be detected")
|
||||
}
|
||||
clean := strings.Replace(userBrotliConf, " brotli on;\n", "", 1)
|
||||
clean = strings.Replace(clean, " brotli_comp_level 6;\n", "", 1)
|
||||
clean = strings.Replace(clean, " brotli_types text/plain text/css application/json;\n", "", 1)
|
||||
if nginxModuleUserDirectiveRe.MatchString(clean) {
|
||||
t.Fatal("a config without brotli must not be treated as user-managed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteUserBrotliDirectivesInPlace(t *testing.T) {
|
||||
// Mirrors updateUserNginxBrotliParams without touching the filesystem.
|
||||
rewrite := func(content string, values map[string][]string) string {
|
||||
updated := content
|
||||
for _, name := range []string{"brotli", "brotli_comp_level", "brotli_min_length", "brotli_types"} {
|
||||
params, ok := values[name]
|
||||
if !ok || len(params) == 0 {
|
||||
continue
|
||||
}
|
||||
pattern := nginxUserDirectivePattern(name)
|
||||
if !pattern.MatchString(updated) {
|
||||
continue
|
||||
}
|
||||
updated = pattern.ReplaceAllString(updated, "${1}"+name+" "+strings.Join(params, " ")+";")
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
got := rewrite(userBrotliConf, map[string][]string{
|
||||
"brotli": {"off"},
|
||||
"brotli_comp_level": {"4"},
|
||||
// brotli_min_length is absent from the user's config and must not be
|
||||
// introduced: the panel cannot know where they would want it.
|
||||
"brotli_min_length": {"2k"},
|
||||
})
|
||||
|
||||
if !strings.Contains(got, " brotli off;") {
|
||||
t.Errorf("value was not updated:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, " brotli_comp_level 4;") {
|
||||
t.Errorf("comp level was not updated:\n%s", got)
|
||||
}
|
||||
if strings.Contains(got, "brotli_min_length") {
|
||||
t.Error("a directive the user never wrote must not be added")
|
||||
}
|
||||
// Everything else survives, including the comment the parser would drop.
|
||||
for _, keep := range []string{
|
||||
"# enabled by hand, long before the panel managed it",
|
||||
" gzip on;",
|
||||
" gzip_comp_level 5;",
|
||||
" brotli_types text/plain text/css application/json;",
|
||||
"include /usr/local/openresty/nginx/conf/conf.d/*.conf;",
|
||||
"worker_processes auto;",
|
||||
} {
|
||||
if !strings.Contains(got, keep) {
|
||||
t.Errorf("unrelated line was altered or lost: %s", keep)
|
||||
}
|
||||
}
|
||||
if strings.Count(got, "brotli on;")+strings.Count(got, "brotli off;") != 1 {
|
||||
t.Error("the directive must remain defined exactly once")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteUserBrotliPreservesIndentation(t *testing.T) {
|
||||
content := "http {\n\t\tbrotli on;\n}\n"
|
||||
pattern := nginxUserDirectivePattern("brotli")
|
||||
got := pattern.ReplaceAllString(content, "${1}brotli off;")
|
||||
if !strings.Contains(got, "\t\tbrotli off;") {
|
||||
t.Errorf("original indentation was not preserved: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Detection must cover the default/ directory, which is included at http
|
||||
// scope like conf.d but lives under the install directory, not the site root.
|
||||
func TestNginxModuleUserConfigPathsCoversDefaultDir(t *testing.T) {
|
||||
siteDir := t.TempDir()
|
||||
installRoot := t.TempDir()
|
||||
installDir := path.Join(installRoot, "openresty", "openresty")
|
||||
defaultDir := path.Join(installDir, "conf", "default")
|
||||
if err := os.MkdirAll(defaultDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path.Join(defaultDir, "00.default.conf"), []byte("server {}\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
global.Dir.AppInstallDir = installRoot
|
||||
install := model.AppInstall{Name: "openresty"}
|
||||
install.App.Key = constant.AppOpenresty
|
||||
|
||||
paths := nginxModuleUserConfigPathsWithSiteDir(install, siteDir)
|
||||
foundMain, foundDefault := false, false
|
||||
for _, p := range paths {
|
||||
if strings.HasSuffix(p, path.Join("conf", "nginx.conf")) {
|
||||
foundMain = true
|
||||
}
|
||||
if strings.HasSuffix(p, path.Join("conf", "default", "00.default.conf")) {
|
||||
foundDefault = true
|
||||
}
|
||||
}
|
||||
if !foundMain {
|
||||
t.Error("nginx.conf must be scanned")
|
||||
}
|
||||
if !foundDefault {
|
||||
t.Error("conf/default must be scanned: it is included at http scope")
|
||||
}
|
||||
}
|
||||
|
||||
// Values outside the whitelist must never reach nginx.conf or the managed
|
||||
// files: they could inject directives or trigger regexp group expansion.
|
||||
func TestValidateNginxBrotliValues(t *testing.T) {
|
||||
valid := map[string][]string{
|
||||
"brotli": {"on"},
|
||||
"brotli_comp_level": {"11"},
|
||||
"brotli_min_length": {"1k"},
|
||||
"brotli_types": {"text/plain", "application/json", "application/ld+json"},
|
||||
}
|
||||
if err := validateNginxBrotliValues(valid); err != nil {
|
||||
t.Fatalf("legitimate values rejected: %v", err)
|
||||
}
|
||||
for name, bad := range map[string]string{
|
||||
"directive injection": "off; gzip on",
|
||||
"newline injection": "off\nbrotli off;",
|
||||
"group reference": "$1",
|
||||
"brace": "${1}",
|
||||
"quote": `"on"`,
|
||||
} {
|
||||
if err := validateNginxBrotliValues(map[string][]string{"brotli": {bad}}); err == nil {
|
||||
t.Errorf("%s: %q must be rejected", name, bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
)
|
||||
|
||||
func TestHasEnabledStaticNginxModules(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
modules []dto.NginxModule
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "an enabled static module requires a rebuild",
|
||||
modules: []dto.NginxModule{
|
||||
{Name: "custom", Enable: true, BuildMode: nginxModuleBuildStatic},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "a disabled static module does not",
|
||||
modules: []dto.NginxModule{
|
||||
{Name: "custom", Enable: false, BuildMode: nginxModuleBuildStatic},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "dynamic modules never require a rebuild",
|
||||
modules: []dto.NginxModule{
|
||||
{Name: "ngx_brotli", Enable: true, BuildMode: nginxModuleBuildDynamic},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "no modules at all",
|
||||
modules: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "one enabled static module among dynamic ones is enough",
|
||||
modules: []dto.NginxModule{
|
||||
{Name: "ngx_brotli", Enable: true, BuildMode: nginxModuleBuildDynamic},
|
||||
{Name: "custom", Enable: true, BuildMode: nginxModuleBuildStatic},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := hasEnabledStaticNginxModules(tc.modules); got != tc.want {
|
||||
t.Fatalf("expected %v, got %v", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A stale RESTY_CONFIG_OPTIONS_MORE used to force the full-rebuild path even
|
||||
// with no static module enabled. configureStaticNginxModules derives that value
|
||||
// from the module list and runs before every build, so the rebuild it triggered
|
||||
// could only ever reproduce the current image. Module state is now the only
|
||||
// input; this test pins that down.
|
||||
func TestStaticRebuildIgnoresLeftoverBuildOptions(t *testing.T) {
|
||||
modules := []dto.NginxModule{
|
||||
{Name: "ngx_brotli", Enable: true, BuildMode: nginxModuleBuildDynamic},
|
||||
}
|
||||
if hasEnabledStaticNginxModules(modules) {
|
||||
t.Fatal("dynamic-only modules must not select the static build path")
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeNginxModule is applied to a copy, so callers keep their entities.
|
||||
func TestHasEnabledStaticNginxModulesDoesNotMutateInput(t *testing.T) {
|
||||
modules := []dto.NginxModule{
|
||||
{Name: "custom", Enable: true, BuildMode: nginxModuleBuildStatic, Packages: []string{"", "libfoo", ""}},
|
||||
}
|
||||
_ = hasEnabledStaticNginxModules(modules)
|
||||
if len(modules[0].Packages) != 3 {
|
||||
t.Fatalf("input was normalized in place: %v", modules[0].Packages)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,6 +640,12 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
|
||||
runtime.Version = req.Version
|
||||
return runtimeRepo.Save(runtime)
|
||||
}
|
||||
appDetail, err := getRuntimeUpdateAppDetail(runtime, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
versionChanged := appDetail.ID != runtime.AppDetailID || appDetail.Version != runtime.Version
|
||||
req.Version = appDetail.Version
|
||||
oldImage := runtime.Image
|
||||
oldEnv := runtime.Env
|
||||
var hostPorts []string
|
||||
@@ -659,23 +665,6 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
|
||||
}
|
||||
}
|
||||
|
||||
appDetail, err := appDetailRepo.GetFirst(repo.WithByID(runtime.AppDetailID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
app, err := appRepo.GetFirst(repo.WithByID(appDetail.AppId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
appVersionDir := path.Join(global.Dir.AppResourceDir, app.Resource, app.Key, appDetail.Version)
|
||||
if !fileOp.Stat(appVersionDir) || appDetail.Update {
|
||||
if err := downloadApp(app, appDetail, nil, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = fileOp.Rename(path.Join(runtime.GetPath(), "run.sh"), path.Join(runtime.GetPath(), "run.sh.bak"))
|
||||
_ = fileOp.CopyFile(path.Join(appVersionDir, "run.sh"), runtime.GetPath())
|
||||
}
|
||||
}
|
||||
|
||||
if containerName, ok := req.Params["CONTAINER_NAME"]; ok && containerName != getRuntimeEnv(runtime.Env, "CONTAINER_NAME") {
|
||||
@@ -685,6 +674,36 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
|
||||
runtime.ContainerName = containerName.(string)
|
||||
}
|
||||
|
||||
app, err := appRepo.GetFirst(repo.WithByID(appDetail.AppId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
appVersionDir := filepath.Join(app.GetAppResourcePath(), appDetail.Version)
|
||||
refreshTemplate := !fileOp.Stat(appVersionDir) || appDetail.Update
|
||||
if err = downloadApp(app, appDetail, nil, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if versionChanged {
|
||||
if err = updateRuntimeVersionFiles(runtime, appVersionDir); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if refreshTemplate && runtime.Type != constant.RuntimePHP {
|
||||
if err = fileOp.CopyFile(filepath.Join(appVersionDir, "run.sh"), runtime.GetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if runtime.Type == constant.RuntimePHP {
|
||||
composeContent, err := fileOp.GetContent(runtime.GetComposePath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Environments, err = getDockerComposeEnvironments(composeContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
projectDir := path.Join(global.Dir.RuntimeDir, runtime.Type, runtime.Name)
|
||||
create := request.RuntimeCreate{
|
||||
Image: req.Image,
|
||||
@@ -702,19 +721,21 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
|
||||
ExtraHosts: req.ExtraHosts,
|
||||
},
|
||||
}
|
||||
composeContent, envContent, _, err := handleParams(create, projectDir)
|
||||
composeContent, envContent, forms, err := handleParams(create, projectDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Remark = req.Remark
|
||||
runtime.AppDetailID = appDetail.ID
|
||||
runtime.Version = appDetail.Version
|
||||
runtime.Env = string(envContent)
|
||||
runtime.DockerCompose = string(composeContent)
|
||||
|
||||
switch runtime.Type {
|
||||
case constant.RuntimePHP:
|
||||
runtime.Image = req.Image
|
||||
runtime.Params = string(forms)
|
||||
runtime.Status = constant.StatusBuilding
|
||||
_ = runtimeRepo.Save(runtime)
|
||||
client, err := docker.NewClient()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -724,14 +745,18 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go buildRuntime(runtime, imageID, oldEnv, req.Rebuild)
|
||||
if err = runtimeRepo.Save(runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
go buildRuntime(runtime, imageID, oldEnv, req.Rebuild || versionChanged)
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDotNet:
|
||||
runtime.Version = req.Version
|
||||
runtime.CodeDir = req.CodeDir
|
||||
runtime.Port = strings.Join(hostPorts, ",")
|
||||
runtime.Status = constant.StatusReCreating
|
||||
runtime.ContainerName = req.Params["CONTAINER_NAME"].(string)
|
||||
_ = runtimeRepo.Save(runtime)
|
||||
if err = runtimeRepo.Save(runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
go reCreateRuntime(runtime)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func getRuntimeUpdateAppDetail(runtime *model.Runtime, req request.RuntimeUpdate) (model.AppDetail, error) {
|
||||
current, err := appDetailRepo.GetFirst(repo.WithByID(runtime.AppDetailID))
|
||||
if err != nil {
|
||||
return model.AppDetail{}, err
|
||||
}
|
||||
if current.ID == 0 {
|
||||
return model.AppDetail{}, buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
opts := []repo.DBOption{appDetailRepo.WithAppId(current.AppId)}
|
||||
if req.AppDetailID != 0 {
|
||||
opts = append(opts, repo.WithByID(req.AppDetailID))
|
||||
} else if req.Version != "" {
|
||||
opts = append(opts, appDetailRepo.WithVersion(req.Version))
|
||||
} else {
|
||||
return current, nil
|
||||
}
|
||||
detail, err := appDetailRepo.GetFirst(opts...)
|
||||
if err != nil {
|
||||
return model.AppDetail{}, err
|
||||
}
|
||||
if detail.ID == 0 || (req.Version != "" && detail.Version != req.Version) {
|
||||
return model.AppDetail{}, buserr.New("ErrInvalidParams")
|
||||
}
|
||||
return detail, nil
|
||||
}
|
||||
|
||||
func updateRuntimeVersionFiles(runtime *model.Runtime, appVersionDir string) error {
|
||||
fileOp := files.NewFileOp()
|
||||
template, err := fileOp.GetContent(filepath.Join(appVersionDir, "docker-compose.yml"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current, err := fileOp.GetContent(runtime.GetComposePath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
composeContent, err := updateRuntimeImageConfig(current, template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if runtime.Type == constant.RuntimePHP {
|
||||
if err = fileOp.CopyDir(filepath.Join(appVersionDir, "build"), runtime.GetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = fileOp.CopyFile(filepath.Join(appVersionDir, "data.yml"), runtime.GetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = fileOp.CopyFile(filepath.Join(appVersionDir, "run.sh"), runtime.GetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fileOp.SaveFile(runtime.GetComposePath(), string(composeContent), constant.FilePerm)
|
||||
}
|
||||
|
||||
func updateRuntimeImageConfig(current, template []byte) ([]byte, error) {
|
||||
var currentCompose, templateCompose map[string]interface{}
|
||||
if err := yaml.Unmarshal(current, ¤tCompose); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := yaml.Unmarshal(template, &templateCompose); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
currentServices, ok := currentCompose["services"].(map[string]interface{})
|
||||
if !ok || len(currentServices) != 1 {
|
||||
return nil, buserr.New("ErrFileParse")
|
||||
}
|
||||
templateServices, ok := templateCompose["services"].(map[string]interface{})
|
||||
if !ok || len(templateServices) != 1 {
|
||||
return nil, buserr.New("ErrFileParse")
|
||||
}
|
||||
// Keep container settings and mounts; only the image and build definition belong to the version.
|
||||
for _, currentService := range currentServices {
|
||||
service, ok := currentService.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, buserr.New("ErrFileParse")
|
||||
}
|
||||
for _, templateService := range templateServices {
|
||||
target, ok := templateService.(map[string]interface{})
|
||||
if !ok || (target["image"] == nil && target["build"] == nil) {
|
||||
return nil, buserr.New("ErrFileParse")
|
||||
}
|
||||
for _, key := range []string{"image", "build"} {
|
||||
delete(service, key)
|
||||
if value, exists := target[key]; exists {
|
||||
service[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return yaml.Marshal(currentCompose)
|
||||
}
|
||||
@@ -838,21 +838,49 @@ func restartRuntime(runtime *model.Runtime) (err error) {
|
||||
}
|
||||
|
||||
func getDockerComposeEnvironments(yml []byte) ([]request.Environment, error) {
|
||||
var (
|
||||
composeProject docker.ComposeProject
|
||||
err error
|
||||
)
|
||||
err = yaml.Unmarshal(yml, &composeProject)
|
||||
if err != nil {
|
||||
var project struct {
|
||||
Services yaml.Node `yaml:"services"`
|
||||
}
|
||||
if err := yaml.Unmarshal(yml, &project); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
services := &project.Services
|
||||
if services.Kind == yaml.AliasNode {
|
||||
services = services.Alias
|
||||
}
|
||||
if services.Kind != 0 && services.Kind != yaml.MappingNode {
|
||||
return nil, fmt.Errorf("unsupported services format")
|
||||
}
|
||||
var res []request.Environment
|
||||
for _, service := range composeProject.Services {
|
||||
for key, value := range service.Environment.Variables {
|
||||
res = append(res, request.Environment{
|
||||
Key: key,
|
||||
Value: value,
|
||||
})
|
||||
// Keep the file order for both services and environment entries.
|
||||
for i := 1; i < len(services.Content); i += 2 {
|
||||
var service struct {
|
||||
Environment yaml.Node `yaml:"environment"`
|
||||
}
|
||||
if err := services.Content[i].Decode(&service); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
environment := &service.Environment
|
||||
if environment.Kind == yaml.AliasNode {
|
||||
environment = environment.Alias
|
||||
}
|
||||
switch environment.Kind {
|
||||
case yaml.MappingNode:
|
||||
for j := 0; j < len(environment.Content); j += 2 {
|
||||
res = append(res, request.Environment{Key: environment.Content[j].Value, Value: environment.Content[j+1].Value})
|
||||
}
|
||||
case yaml.SequenceNode:
|
||||
for _, item := range environment.Content {
|
||||
var entry string
|
||||
if err := item.Decode(&entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, value, _ := strings.Cut(entry, "=")
|
||||
res = append(res, request.Environment{Key: key, Value: value})
|
||||
}
|
||||
case 0:
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported environment format")
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -18,7 +19,6 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
|
||||
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
|
||||
"github.com/jinzhu/copier"
|
||||
@@ -45,6 +45,7 @@ type ISettingService interface {
|
||||
GetLocalConnForSSH() (dto.SSHConnData, error)
|
||||
|
||||
SaveDescription(req dto.CommonDescription) error
|
||||
CleanupDescriptions(context.Context) (int64, error)
|
||||
}
|
||||
|
||||
func NewISettingService() ISettingService {
|
||||
@@ -125,22 +126,7 @@ func (u *SettingService) GetWebsiteDir() string {
|
||||
}
|
||||
|
||||
func (u *SettingService) Update(key, value string) error {
|
||||
oldValue := constant.FirewallPortWhiteListValue
|
||||
if key == constant.FirewallPortWhiteList {
|
||||
if _, err := firewall.ParsePortWhitelist(value); err != nil {
|
||||
return err
|
||||
}
|
||||
if val, err := settingRepo.GetValueByKey(key); err == nil {
|
||||
oldValue = val
|
||||
}
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
if key == constant.FirewallPortWhiteList {
|
||||
return SyncFirewallPortWhitelistAfterUpdate(oldValue)
|
||||
}
|
||||
return nil
|
||||
return settingRepo.UpdateOrCreate(key, value)
|
||||
}
|
||||
|
||||
func (u *SettingService) UpdateTerminalAI(req dto.TerminalAIInfo) error {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
func (u *SettingService) CleanupDescriptions(ctx context.Context) (int64, error) {
|
||||
var deleted int64
|
||||
var failures []error
|
||||
for _, kind := range []string{"container", "firewall", "firewall-docker"} {
|
||||
count, err := func() (int64, error) {
|
||||
switch kind {
|
||||
case "container":
|
||||
return cleanupUnusedDescriptions(ctx, kind, loadContainerDescriptionIDs)
|
||||
case "firewall":
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
return cleanupUnusedDescriptions(ctx, kind, loadHostFirewallDescriptionIDs)
|
||||
default:
|
||||
return cleanupUnusedDescriptions(ctx, kind, nil)
|
||||
}
|
||||
}()
|
||||
deleted += count
|
||||
if err != nil {
|
||||
failures = append(failures, fmt.Errorf("%s: %w", kind, err))
|
||||
}
|
||||
}
|
||||
return deleted, errors.Join(failures...)
|
||||
}
|
||||
|
||||
func cleanupUnusedDescriptions(ctx context.Context, kind string, loadIDs func(context.Context) (map[string]bool, error)) (int64, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
descriptions, err := settingRepo.GetDescriptionList(repo.WithByType(kind))
|
||||
if err != nil || len(descriptions) == 0 {
|
||||
return 0, err
|
||||
}
|
||||
var active map[string]bool
|
||||
if loadIDs != nil {
|
||||
active, err = loadIDs(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
ids, empty := make([]string, 0), make([]string, 0)
|
||||
for _, description := range descriptions {
|
||||
if !active[description.ID] {
|
||||
ids = append(ids, description.ID)
|
||||
} else if description.Description == "" && !description.IsPinned {
|
||||
empty = append(empty, description.ID)
|
||||
}
|
||||
}
|
||||
deleted, err := settingRepo.DeleteDescriptions(ctx, kind, ids, false)
|
||||
if err != nil {
|
||||
return deleted, err
|
||||
}
|
||||
count, err := settingRepo.DeleteDescriptions(ctx, kind, empty, true)
|
||||
return deleted + count, err
|
||||
}
|
||||
|
||||
func loadContainerDescriptionIDs(ctx context.Context) (map[string]bool, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
containers, err := client.ContainerList(ctx, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make(map[string]bool, len(containers))
|
||||
for _, item := range containers {
|
||||
ids[item.ID] = true
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func loadHostFirewallDescriptionIDs(ctx context.Context) (map[string]bool, error) {
|
||||
providers := lifecycle.InstalledProviders()
|
||||
if len(providers) == 0 {
|
||||
return nil, filter.ErrProviderUnavailable
|
||||
}
|
||||
service := newFirewallService()
|
||||
ids := make(map[string]bool)
|
||||
for _, name := range providers {
|
||||
provider := filter.Provider(name)
|
||||
inventory, err := service.readFirewallInventory(ctx, provider, filter.ManagedInputScopes(provider))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, notice := range inventory.Notices {
|
||||
if notice.Code == filter.ScopeNoticeFamilyUnavailable || notice.Code == filter.ScopeNoticeManagedScopeInactive {
|
||||
return nil, fmt.Errorf("%w: %s %s", filter.ErrInventoryUnavailable, name, notice.Code)
|
||||
}
|
||||
}
|
||||
for _, item := range inventory.Items {
|
||||
id, err := filter.DescriptionID(*item.Observed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids[id] = true
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
+145
-53
@@ -4,8 +4,10 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -22,6 +24,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
||||
csvexport "github.com/1Panel-dev/1Panel/agent/utils/csv_export"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/geo"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -35,11 +38,13 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const sshPath = "/etc/ssh/sshd_config"
|
||||
const defaultSSHPort = "22"
|
||||
const sshManagedMarker = "# config by 1panel"
|
||||
const defaultSSHLogDir = "/var/log"
|
||||
|
||||
type SSHService struct{}
|
||||
|
||||
@@ -216,10 +221,21 @@ func (u *SSHService) Update(req dto.SSHUpdate) error {
|
||||
return err
|
||||
}
|
||||
oldPortValue := strings.Join(loadSSHPortValues(directives), ",")
|
||||
if req.Key == "Port" {
|
||||
if err := checkSSHPortAvailability(splitSSHPorts(oldPortValue), splitSSHPorts(req.NewValue)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := updateSSHDirectiveValue(req.Key, req.NewValue, directives); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Key == "Port" {
|
||||
if err := newFirewallService().updateSystemAccessPortWhitelist(context.Background(), firewall.PortWhitelistTypeSSH, splitSSHPorts(req.NewValue)); err != nil {
|
||||
if restoreErr := rewriteSSHManagedDirectives(sshPath, "Port", buildSSHDirectiveLines("Port", oldPortValue)); restoreErr != nil {
|
||||
return fmt.Errorf("save SSH whitelist: %w; restore SSH configuration: %v", err, restoreErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
handleSSHPortUpdate(oldPortValue, req.NewValue)
|
||||
}
|
||||
|
||||
@@ -319,18 +335,6 @@ func handleSSHPortUpdate(oldValue, newValue string) {
|
||||
}
|
||||
}
|
||||
|
||||
removedPorts, err := parseSSHPortsToInts(diffSSHPorts(oldPorts, newPorts))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("parse removed ssh ports failed, err: %v", err)
|
||||
} else {
|
||||
addedPorts, err := parseSSHPortsToInts(diffSSHPorts(newPorts, oldPorts))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("parse added ssh ports failed, err: %v", err)
|
||||
} else if err := OperateFirewallPort(removedPorts, addedPorts); err != nil {
|
||||
global.LOG.Errorf("reset firewall rules %s -> %s failed, err: %v", oldValue, newValue, err)
|
||||
}
|
||||
}
|
||||
|
||||
primaryPort, err := loadPrimarySSHPort(newValue)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load primary ssh port from %s failed, err: %v", newValue, err)
|
||||
@@ -370,6 +374,24 @@ func diffSSHPorts(left, right []string) []string {
|
||||
return diff
|
||||
}
|
||||
|
||||
func checkSSHPortAvailability(oldPorts, newPorts []string) error {
|
||||
for _, port := range diffSSHPorts(newPorts, oldPorts) {
|
||||
value, err := strconv.Atoi(port)
|
||||
if err != nil || value < 1 || value > 65535 {
|
||||
return fmt.Errorf("invalid SSH port %q", port)
|
||||
}
|
||||
if common.ScanPort(value) {
|
||||
return buserr.WithDetail("ErrPortInUsed", value, nil)
|
||||
}
|
||||
listener, err := net.Listen("tcp4", ":"+strconv.Itoa(value))
|
||||
if err != nil {
|
||||
return buserr.WithDetail("ErrPortInUsed", value, nil)
|
||||
}
|
||||
_ = listener.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadPrimarySSHPort(value string) (int, error) {
|
||||
ports := splitSSHPorts(value)
|
||||
if len(ports) == 0 {
|
||||
@@ -378,18 +400,6 @@ func loadPrimarySSHPort(value string) (int, error) {
|
||||
return strconv.Atoi(ports[0])
|
||||
}
|
||||
|
||||
func parseSSHPortsToInts(ports []string) ([]int, error) {
|
||||
var values []int
|
||||
for _, port := range ports {
|
||||
value, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func runWithOptionalSudo(sudo, name string, args ...string) (string, error) {
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
|
||||
if sudo != "" {
|
||||
@@ -430,15 +440,33 @@ func (u *SSHService) SyncRootCert() error {
|
||||
global.LOG.Errorf("read pubic key of %s for sync failed, err: %v", item, err)
|
||||
continue
|
||||
}
|
||||
cert.EncryptionMode = loadEncryptioMode(string(pubItem))
|
||||
publicKey, _, _, rest, err := ssh.ParseAuthorizedKey(pubItem)
|
||||
if err != nil || len(bytes.TrimSpace(rest)) != 0 {
|
||||
continue
|
||||
}
|
||||
cert.EncryptionMode = loadSSHKeyType(publicKey)
|
||||
if cert.EncryptionMode == "" {
|
||||
continue
|
||||
}
|
||||
rootCerts = append(rootCerts, cert)
|
||||
}
|
||||
return hostRepo.SyncCert(rootCerts)
|
||||
}
|
||||
|
||||
func (u *SSHService) CreateRootCert(req dto.RootCertOperate) error {
|
||||
if cmd.CheckIllegal(req.EncryptionMode, req.PassPhrase) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
switch req.Mode {
|
||||
case "generate":
|
||||
switch req.EncryptionMode {
|
||||
case "rsa", "ed25519", "ecdsa", "dsa":
|
||||
default:
|
||||
return buserr.WithName("ErrNotSupportType", req.EncryptionMode)
|
||||
}
|
||||
case "input", "import":
|
||||
if err := validateSSHKeyPair(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return buserr.WithName("ErrNotSupportType", req.Mode)
|
||||
}
|
||||
certItem, _ := hostRepo.GetCert(repo.WithByName(req.Name))
|
||||
if certItem.ID != 0 {
|
||||
@@ -535,6 +563,9 @@ func (u *SSHService) CreateRootCert(req dto.RootCertOperate) error {
|
||||
}
|
||||
|
||||
func (u *SSHService) EditRootCert(req dto.RootCertOperate) error {
|
||||
if err := validateSSHKeyPair(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load current user failed, err: %v", err)
|
||||
@@ -668,13 +699,11 @@ func isSSHLogFileName(name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error) {
|
||||
func listSSHLogFiles(baseDir string) ([]sshFileItem, error) {
|
||||
var fileList []sshFileItem
|
||||
var data []dto.SSHHistory
|
||||
baseDir := "/var/log"
|
||||
fileItems, err := os.ReadDir(baseDir)
|
||||
if err != nil {
|
||||
return 0, data, err
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range fileItems {
|
||||
if item.IsDir() || !isSSHLogFileName(item.Name()) {
|
||||
@@ -682,7 +711,7 @@ func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []d
|
||||
}
|
||||
info, err := item.Info()
|
||||
if err != nil {
|
||||
return 0, data, err
|
||||
return nil, err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
continue
|
||||
@@ -695,7 +724,15 @@ func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []d
|
||||
}
|
||||
fileList = append(fileList, sshFileItem{Name: itemPath, Year: info.ModTime().Year()})
|
||||
}
|
||||
fileList = sortFileList(fileList)
|
||||
return sortFileList(fileList), nil
|
||||
}
|
||||
|
||||
func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error) {
|
||||
var data []dto.SSHHistory
|
||||
fileList, err := listSSHLogFiles(defaultSSHLogDir)
|
||||
if err != nil {
|
||||
return 0, data, err
|
||||
}
|
||||
|
||||
filter := ""
|
||||
if len(req.Info) != 0 {
|
||||
@@ -742,7 +779,7 @@ func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []d
|
||||
}
|
||||
|
||||
func (u *SSHService) CleanLog() error {
|
||||
return cleanSSHLogFiles("/var/log")
|
||||
return cleanSSHLogFiles(defaultSSHLogDir)
|
||||
}
|
||||
|
||||
func cleanSSHLogFiles(baseDir string) error {
|
||||
@@ -1290,20 +1327,11 @@ func loadSSHData(
|
||||
if err != nil {
|
||||
return datas, 0, 0
|
||||
}
|
||||
lines, err := loadSSHLogLines(filePath)
|
||||
histories, err := loadSSHHistoriesFromFile(filePath, status, filter, startTime, endTime, currentYear, nyc)
|
||||
if err != nil {
|
||||
return datas, 0, 0
|
||||
}
|
||||
items := collectSSHLogItems(lines, filter, status)
|
||||
for i := len(items) - 1; i >= 0; i-- {
|
||||
itemData := items[i].History
|
||||
if !matchSSHLogStatus(status, itemData.Status) || !checkIsStandard(itemData) {
|
||||
continue
|
||||
}
|
||||
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc)
|
||||
if !isSSHLogWithinTimeRange(itemData.Date, startTime, endTime) {
|
||||
continue
|
||||
}
|
||||
for _, itemData := range histories {
|
||||
if successCount+failedCount >= showCountFrom && (showCountTo == -1 || successCount+failedCount < showCountTo) {
|
||||
itemData.Area, _ = geo.GetIPLocation(getLoc, itemData.Address, common.GetLang(ctx))
|
||||
datas = append(datas, itemData)
|
||||
@@ -1317,6 +1345,32 @@ func loadSSHData(
|
||||
return datas, successCount, failedCount
|
||||
}
|
||||
|
||||
func loadSSHHistoriesFromFile(
|
||||
filePath, status, filter string,
|
||||
startTime, endTime time.Time,
|
||||
currentYear int,
|
||||
location *time.Location,
|
||||
) ([]dto.SSHHistory, error) {
|
||||
lines, err := loadSSHLogLines(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := collectSSHLogItems(lines, filter, status)
|
||||
histories := make([]dto.SSHHistory, 0, len(items))
|
||||
for i := len(items) - 1; i >= 0; i-- {
|
||||
itemData := items[i].History
|
||||
if !matchSSHLogStatus(status, itemData.Status) || !checkIsStandard(itemData) {
|
||||
continue
|
||||
}
|
||||
itemData.Date = loadDate(currentYear, itemData.DateStr, location)
|
||||
if !isSSHLogWithinTimeRange(itemData.Date, startTime, endTime) {
|
||||
continue
|
||||
}
|
||||
histories = append(histories, itemData)
|
||||
}
|
||||
return histories, nil
|
||||
}
|
||||
|
||||
func isSSHLogWithinTimeRange(itemTime, startTime, endTime time.Time) bool {
|
||||
if startTime.IsZero() || endTime.IsZero() {
|
||||
return true
|
||||
@@ -1650,22 +1704,60 @@ func loadDate(currentYear int, DateStr string, nyc *time.Location) time.Time {
|
||||
return itemDate
|
||||
}
|
||||
|
||||
func loadEncryptioMode(content string) string {
|
||||
if strings.HasPrefix(content, "ssh-rsa") {
|
||||
func loadSSHKeyType(publicKey ssh.PublicKey) string {
|
||||
switch publicKey.Type() {
|
||||
case ssh.KeyAlgoRSA:
|
||||
return "rsa"
|
||||
}
|
||||
if strings.HasPrefix(content, "ssh-ed25519") {
|
||||
case ssh.KeyAlgoED25519:
|
||||
return "ed25519"
|
||||
}
|
||||
if strings.HasPrefix(content, "ssh-ecdsa") {
|
||||
case ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521:
|
||||
return "ecdsa"
|
||||
}
|
||||
if strings.HasPrefix(content, "ssh-dsa") {
|
||||
case ssh.KeyAlgoDSA:
|
||||
return "dsa"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func validateSSHKeyPair(req *dto.RootCertOperate) error {
|
||||
req.PublicKey = strings.TrimSpace(req.PublicKey)
|
||||
req.PrivateKey = strings.TrimSpace(req.PrivateKey)
|
||||
if req.PublicKey == "" || strings.ContainsAny(req.PublicKey, "\r\n") {
|
||||
return buserr.New("ErrSSHPublicKey")
|
||||
}
|
||||
publicKey, comment, options, rest, err := ssh.ParseAuthorizedKey([]byte(req.PublicKey))
|
||||
if err != nil || len(options) != 0 || len(bytes.TrimSpace(rest)) != 0 {
|
||||
return buserr.New("ErrSSHPublicKey")
|
||||
}
|
||||
req.EncryptionMode = loadSSHKeyType(publicKey)
|
||||
if req.EncryptionMode == "" {
|
||||
return buserr.WithName("ErrNotSupportType", publicKey.Type())
|
||||
}
|
||||
block, rest := pem.Decode([]byte(req.PrivateKey))
|
||||
if !strings.HasPrefix(req.PrivateKey, "-----BEGIN ") || block == nil || len(bytes.TrimSpace(rest)) != 0 {
|
||||
return buserr.New("ErrSSHPrivateKey")
|
||||
}
|
||||
signer, err := ssh.ParsePrivateKey([]byte(req.PrivateKey))
|
||||
var missingPassphrase *ssh.PassphraseMissingError
|
||||
if errors.As(err, &missingPassphrase) {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(req.PrivateKey), []byte(req.PassPhrase))
|
||||
} else if err == nil {
|
||||
req.PassPhrase = ""
|
||||
}
|
||||
if err != nil {
|
||||
return buserr.New("ErrSSHPrivateKey")
|
||||
}
|
||||
if !bytes.Equal(signer.PublicKey().Marshal(), publicKey.Marshal()) {
|
||||
return buserr.New("ErrSSHKeyMismatch")
|
||||
}
|
||||
req.PublicKey = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(publicKey)))
|
||||
if comment != "" {
|
||||
req.PublicKey += " " + comment
|
||||
}
|
||||
req.PublicKey += "\n"
|
||||
req.PrivateKey += "\n"
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateLocalConn(newPort uint) error {
|
||||
conn, _ := settingRepo.GetValueByKey("LocalSSHConn")
|
||||
if len(conn) == 0 {
|
||||
|
||||
@@ -4,17 +4,36 @@ import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
)
|
||||
|
||||
const (
|
||||
vllmAppKeyForUpgrade = "vllm"
|
||||
vllmImageEnvKey = "IMAGE"
|
||||
vllmImageTypeNvidia = "nvidia"
|
||||
vllmImageTypeIntel = "intel"
|
||||
vllmImageTypeAscend = "ascend"
|
||||
vllmAppKeyForUpgrade = "vllm"
|
||||
vllmGB10VersionPrefix = "nvidia-gb10-dspark-"
|
||||
vllmImageEnvKey = "IMAGE"
|
||||
vllmImageTypeNvidia = "nvidia"
|
||||
vllmImageTypeIntel = "intel"
|
||||
vllmImageTypeAscend = "ascend"
|
||||
)
|
||||
|
||||
func isVllmProOnlyVersion(appKey, version string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(appKey), vllmAppKeyForUpgrade) &&
|
||||
strings.HasPrefix(strings.ToLower(strings.TrimSpace(version)), vllmGB10VersionPrefix)
|
||||
}
|
||||
|
||||
func canAccessVllmVersion(appKey, version string) bool {
|
||||
return !isVllmProOnlyVersion(appKey, version) || xpack.MultiNodeProvider.IsXpack()
|
||||
}
|
||||
|
||||
func checkVllmVersionAccess(appKey, version string) error {
|
||||
if !canAccessVllmVersion(appKey, version) {
|
||||
return buserr.New("ErrVllmGB10ProOnly")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveVllmVersionFamily(version, image string) string {
|
||||
normalizedVersion := strings.ToLower(strings.TrimSpace(version))
|
||||
if strings.HasPrefix(normalizedVersion, vllmImageTypeIntel+"-") {
|
||||
|
||||
@@ -265,6 +265,19 @@ func (w WebsiteService) GetWebsites() ([]response.WebsiteDTO, error) {
|
||||
return websiteDTOs, nil
|
||||
}
|
||||
|
||||
func newWebsiteCreateHTTPSOp(sslID uint) request.WebsiteHTTPSOp {
|
||||
return request.WebsiteHTTPSOp{
|
||||
Enable: true,
|
||||
WebsiteSSLID: sslID,
|
||||
Type: constant.SSLExisted,
|
||||
HttpConfig: constant.HTTPToHTTPS,
|
||||
SSLProtocol: []string{"TLSv1.3", "TLSv1.2"},
|
||||
Algorithm: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED",
|
||||
Hsts: true,
|
||||
HstsIncludeSubDomains: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) {
|
||||
alias := create.Alias
|
||||
if alias == "default" {
|
||||
@@ -313,6 +326,7 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
|
||||
appInstall *model.AppInstall
|
||||
runtime *model.Runtime
|
||||
primaryDomain string
|
||||
initialSSL *websiteInitialSSL
|
||||
)
|
||||
if website.Type == constant.Stream {
|
||||
if create.StreamConfig.StreamPorts == "" {
|
||||
@@ -339,6 +353,29 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
|
||||
}
|
||||
website.PrimaryDomain = primaryDomain
|
||||
website.Protocol = constant.ProtocolHTTP
|
||||
for _, domain := range domains {
|
||||
if domain.SSL {
|
||||
create.EnableSSL = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if create.EnableSSL {
|
||||
if create.WebsiteSSLID == 0 {
|
||||
return buserr.New("ErrSSLValid")
|
||||
}
|
||||
websiteSSL, sslErr := websiteSSLRepo.GetFirst(repo.WithByID(create.WebsiteSSLID))
|
||||
if sslErr != nil {
|
||||
return sslErr
|
||||
}
|
||||
if websiteSSL.Pem == "" || websiteSSL.PrivateKey == "" {
|
||||
return buserr.New("ErrSSLValid")
|
||||
}
|
||||
sslReq := newWebsiteCreateHTTPSOp(websiteSSL.ID)
|
||||
website.Protocol = constant.ProtocolHTTPS
|
||||
website.WebsiteSSLID = websiteSSL.ID
|
||||
website.HttpConfig = sslReq.HttpConfig
|
||||
initialSSL = &websiteInitialSSL{certificate: *websiteSSL, request: sslReq}
|
||||
}
|
||||
}
|
||||
|
||||
createTask, err := task.NewTaskWithOps(website.PrimaryDomain, task.TaskCreate, task.TaskScopeWebsite, create.TaskID, 0)
|
||||
@@ -490,7 +527,7 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
|
||||
}
|
||||
|
||||
configNginx := func(t *task.Task) error {
|
||||
if err = configDefaultNginx(website, domains, appInstall, runtime, create.StreamConfig); err != nil {
|
||||
if err = configDefaultNginx(website, domains, appInstall, runtime, create.StreamConfig, initialSSL); err != nil {
|
||||
return err
|
||||
}
|
||||
if create.Type == constant.Static && create.TemplateOutputID > 0 {
|
||||
@@ -548,36 +585,6 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
|
||||
|
||||
createTask.AddSubTask(i18n.GetMsgByKey("ConfigOpenresty"), configNginx, deleteWebsite)
|
||||
|
||||
if create.EnableSSL {
|
||||
enableSSL := func(t *task.Task) error {
|
||||
websiteModel, err := websiteSSLRepo.GetFirst(repo.WithByID(create.WebsiteSSLID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
website.Protocol = constant.ProtocolHTTPS
|
||||
website.WebsiteSSLID = create.WebsiteSSLID
|
||||
appSSLReq := request.WebsiteHTTPSOp{
|
||||
WebsiteID: website.ID,
|
||||
Enable: true,
|
||||
WebsiteSSLID: websiteModel.ID,
|
||||
Type: "existed",
|
||||
HttpConfig: "HTTPToHTTPS",
|
||||
SSLProtocol: []string{"TLSv1.3", "TLSv1.2"},
|
||||
Algorithm: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED",
|
||||
Hsts: true,
|
||||
HstsIncludeSubDomains: true,
|
||||
}
|
||||
if err = applySSL(website, *websiteModel, appSSLReq); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = websiteRepo.Save(context.Background(), website); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
createTask.AddSubTaskWithIgnoreErr(i18n.GetMsgByKey("EnableSSL"), enableSSL)
|
||||
}
|
||||
|
||||
if len(create.FtpUser) != 0 && len(create.FtpPassword) != 0 {
|
||||
createFtpUser := func(t *task.Task) error {
|
||||
indexDir := GetSitePath(*website, SiteIndexDir)
|
||||
|
||||
@@ -277,39 +277,12 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
interPrivateKey, interPublicKey, _, err := createPrivateKey(websiteSSL.KeyType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
notAfter := time.Now()
|
||||
if req.Unit == "year" {
|
||||
notAfter = notAfter.AddDate(req.Time, 0, 0)
|
||||
} else {
|
||||
notAfter = notAfter.AddDate(0, 0, req.Time)
|
||||
}
|
||||
interCsr := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().Unix() + 2),
|
||||
Subject: rootCsr.Subject,
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: notAfter,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
MaxPathLen: 0,
|
||||
MaxPathLenZero: true,
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
}
|
||||
interDer, err := x509.CreateCertificate(rand.Reader, interCsr, rootCsr, interPublicKey, rootPrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
interCert, err := x509.ParseCertificate(interDer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
interCertBlock := &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: interCert.Raw,
|
||||
}
|
||||
_, publicKey, privateKeyBytes, err := createPrivateKey(websiteSSL.KeyType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -330,13 +303,13 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
|
||||
NotAfter: notAfter,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: false,
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
KeyUsage: leafKeyUsage(websiteSSL.KeyType),
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
DNSNames: domains,
|
||||
IPAddresses: ips,
|
||||
}
|
||||
|
||||
der, err := x509.CreateCertificate(rand.Reader, csr, interCert, publicKey, interPrivateKey)
|
||||
der, err := x509.CreateCertificate(rand.Reader, csr, rootCsr, publicKey, rootPrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -349,7 +322,7 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
}
|
||||
websiteSSL.Pem = string(pem.EncodeToMemory(certBlock)) + string(pem.EncodeToMemory(rootCertBlock)) + string(pem.EncodeToMemory(interCertBlock))
|
||||
websiteSSL.Pem = string(pem.EncodeToMemory(certBlock))
|
||||
websiteSSL.PrivateKey = string(privateKeyBytes)
|
||||
websiteSSL.ExpireDate = cert.NotAfter
|
||||
websiteSSL.StartDate = cert.NotBefore
|
||||
@@ -393,6 +366,14 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
|
||||
return websiteSSL, nil
|
||||
}
|
||||
|
||||
func leafKeyUsage(keyType string) x509.KeyUsage {
|
||||
usage := x509.KeyUsageDigitalSignature
|
||||
if ssl.KeyType(keyType) != certcrypto.EC256 && ssl.KeyType(keyType) != certcrypto.EC384 {
|
||||
usage |= x509.KeyUsageKeyEncipherment
|
||||
}
|
||||
return usage
|
||||
}
|
||||
|
||||
func createPrivateKey(keyType string) (privateKey any, publicKey any, privateKeyBytes []byte, err error) {
|
||||
privateKey, err = certcrypto.GeneratePrivateKey(ssl.KeyType(keyType))
|
||||
if err != nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"path"
|
||||
"strconv"
|
||||
@@ -32,7 +33,9 @@ func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate)
|
||||
return nil, err
|
||||
}
|
||||
go func() {
|
||||
_ = OperateFirewallPort(nil, addPorts)
|
||||
if err := ensureFirewallPorts(addPorts); err != nil {
|
||||
global.LOG.Errorf("allow website firewall ports failed: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
|
||||
|
||||
+139
-124
@@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -255,7 +256,12 @@ func createWebsiteFolder(website *model.Website, runtime *model.Runtime) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall, runtime *model.Runtime, streamConfig request.StreamConfig) error {
|
||||
type websiteInitialSSL struct {
|
||||
certificate model.WebsiteSSL
|
||||
request request.WebsiteHTTPSOp
|
||||
}
|
||||
|
||||
func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall, runtime *model.Runtime, streamConfig request.StreamConfig, initialSSL *websiteInitialSSL) error {
|
||||
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -325,6 +331,13 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
|
||||
setListen(server, strconv.Itoa(domain.Port), website.IPV6, false, website.DefaultServer, false)
|
||||
}
|
||||
server.UpdateServerName(serverNames)
|
||||
if initialSSL != nil {
|
||||
plan := buildWebsiteTLSPlan(domains, nginxInstall.HttpPort, nginxInstall.HttpsPort)
|
||||
applyWebsiteSSLConfig(server, *website, plan, initialSSL.request)
|
||||
if err = createPemFile(*website, initialSSL.certificate); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
siteFolder := path.Join("/www", "sites", website.Alias)
|
||||
server.UpdateDirective("access_log", []string{path.Join(siteFolder, "log", "access.log"), "main"})
|
||||
@@ -789,52 +802,22 @@ func createPemFile(website model.Website, websiteSSL model.WebsiteSSL) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getHttpsPort(websiteID uint) map[int]struct{} {
|
||||
domains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(websiteID))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
httpsPorts := make(map[int]struct{})
|
||||
nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
|
||||
hasDefaultPort := false
|
||||
for _, domain := range domains {
|
||||
if domain.Port == nginxInstall.HttpPort {
|
||||
hasDefaultPort = true
|
||||
}
|
||||
if domain.SSL {
|
||||
httpsPorts[domain.Port] = struct{}{}
|
||||
}
|
||||
}
|
||||
if hasDefaultPort {
|
||||
httpsPorts[nginxInstall.HttpsPort] = struct{}{}
|
||||
}
|
||||
if len(httpsPorts) == 0 {
|
||||
for _, domain := range domains {
|
||||
if !domain.SSL {
|
||||
httpsPorts[domain.Port] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return httpsPorts
|
||||
type websiteTLSPlan struct {
|
||||
httpPorts []int
|
||||
httpsPorts []int
|
||||
redirectPort int
|
||||
defaultHTTPPort int
|
||||
hasDefaultHTTP bool
|
||||
}
|
||||
|
||||
func applySSL(website *model.Website, websiteSSL model.WebsiteSSL, req request.WebsiteHTTPSOp) error {
|
||||
nginxFull, err := getNginxFull(website)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
domains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(website.ID))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
func buildWebsiteTLSPlan(domains []model.WebsiteDomain, defaultHTTPPort, defaultHTTPSPort int) websiteTLSPlan {
|
||||
httpPorts := make(map[int]struct{})
|
||||
httpsPorts := make(map[int]struct{})
|
||||
sslPort := 0
|
||||
plan := websiteTLSPlan{defaultHTTPPort: defaultHTTPPort}
|
||||
|
||||
hasDefaultPort := false
|
||||
for _, domain := range domains {
|
||||
if domain.Port == nginxFull.Install.HttpPort {
|
||||
hasDefaultPort = true
|
||||
if domain.Port == defaultHTTPPort {
|
||||
plan.hasDefaultHTTP = true
|
||||
}
|
||||
if domain.SSL {
|
||||
httpsPorts[domain.Port] = struct{}{}
|
||||
@@ -842,112 +825,75 @@ func applySSL(website *model.Website, websiteSSL model.WebsiteSSL, req request.W
|
||||
httpPorts[domain.Port] = struct{}{}
|
||||
}
|
||||
}
|
||||
if hasDefaultPort {
|
||||
httpsPorts[nginxFull.Install.HttpsPort] = struct{}{}
|
||||
if plan.hasDefaultHTTP {
|
||||
httpsPorts[defaultHTTPSPort] = struct{}{}
|
||||
}
|
||||
if len(httpsPorts) == 0 {
|
||||
for port := range httpPorts {
|
||||
httpsPorts[port] = struct{}{}
|
||||
}
|
||||
}
|
||||
config := nginxFull.SiteConfig.Config
|
||||
server := config.FindServers()[0]
|
||||
|
||||
defaultHttpPort := strconv.Itoa(nginxFull.Install.HttpPort)
|
||||
defaultHttpPortIPV6 := "[::]:" + defaultHttpPort
|
||||
|
||||
for port := range httpsPorts {
|
||||
sslPort = port
|
||||
portStr := strconv.Itoa(port)
|
||||
server.RemoveListenByBind(portStr)
|
||||
server.RemoveListenByBind("[::]:" + portStr)
|
||||
setListen(server, portStr, website.IPV6, req.Http3, website.DefaultServer, true)
|
||||
delete(httpPorts, port)
|
||||
}
|
||||
for port := range httpPorts {
|
||||
plan.httpPorts = append(plan.httpPorts, port)
|
||||
}
|
||||
for port := range httpsPorts {
|
||||
plan.httpsPorts = append(plan.httpsPorts, port)
|
||||
}
|
||||
sort.Ints(plan.httpPorts)
|
||||
sort.Ints(plan.httpsPorts)
|
||||
if plan.hasDefaultHTTP {
|
||||
plan.redirectPort = defaultHTTPSPort
|
||||
} else if len(plan.httpsPorts) > 0 {
|
||||
plan.redirectPort = plan.httpsPorts[0]
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
server.UpdateDirective("http2", []string{"on"})
|
||||
func getHttpsPort(websiteID uint) map[int]struct{} {
|
||||
domains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(websiteID))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
|
||||
plan := buildWebsiteTLSPlan(domains, nginxInstall.HttpPort, nginxInstall.HttpsPort)
|
||||
httpsPorts := make(map[int]struct{}, len(plan.httpsPorts))
|
||||
for _, port := range plan.httpsPorts {
|
||||
httpsPorts[port] = struct{}{}
|
||||
}
|
||||
return httpsPorts
|
||||
}
|
||||
|
||||
switch req.HttpConfig {
|
||||
case constant.HTTPSOnly:
|
||||
server.RemoveListenByBind(defaultHttpPort)
|
||||
server.RemoveListenByBind(defaultHttpPortIPV6)
|
||||
server.RemoveDirective("if", []string{"($scheme"})
|
||||
case constant.HTTPToHTTPS:
|
||||
if hasDefaultPort {
|
||||
server.UpdateListen(defaultHttpPort, website.DefaultServer)
|
||||
if website.IPV6 {
|
||||
server.UpdateListen(defaultHttpPortIPV6, website.DefaultServer)
|
||||
}
|
||||
}
|
||||
server.AddHTTP2HTTPS(sslPort)
|
||||
case constant.HTTPAlso:
|
||||
if hasDefaultPort {
|
||||
server.UpdateListen(defaultHttpPort, website.DefaultServer)
|
||||
if website.IPV6 {
|
||||
server.UpdateListen(defaultHttpPortIPV6, website.DefaultServer)
|
||||
}
|
||||
}
|
||||
server.RemoveDirective("if", []string{"($scheme"})
|
||||
}
|
||||
|
||||
if !req.Hsts {
|
||||
server.RemoveDirective("add_header", []string{"Strict-Transport-Security", "\"max-age=31536000\""})
|
||||
server.RemoveDirective("add_header", []string{"Strict-Transport-Security", "\"max-age=31536000; includeSubDomains\""})
|
||||
}
|
||||
if !req.Http3 {
|
||||
for port := range httpsPorts {
|
||||
server.RemoveListen(strconv.Itoa(port), "quic")
|
||||
if website.IPV6 {
|
||||
httpsPortIPV6 := "[::]:" + strconv.Itoa(port)
|
||||
server.RemoveListen(httpsPortIPV6, "quic")
|
||||
}
|
||||
}
|
||||
server.RemoveDirective("add_header", []string{"Alt-Svc"})
|
||||
}
|
||||
|
||||
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = createPemFile(*website, websiteSSL); err != nil {
|
||||
return err
|
||||
}
|
||||
func buildWebsiteSSLParams(alias string, req request.WebsiteHTTPSOp, redirectPort int) []dto.NginxParam {
|
||||
nginxParams := getNginxParamsFromStaticFile(dto.SSL, []dto.NginxParam{})
|
||||
for i, param := range nginxParams {
|
||||
if param.Name == "ssl_certificate" {
|
||||
nginxParams[i].Params = []string{path.Join("/www", "sites", website.Alias, "ssl", "fullchain.pem")}
|
||||
}
|
||||
if param.Name == "ssl_certificate_key" {
|
||||
nginxParams[i].Params = []string{path.Join("/www", "sites", website.Alias, "ssl", "privkey.pem")}
|
||||
}
|
||||
if param.Name == "ssl_protocols" {
|
||||
for i := range nginxParams {
|
||||
switch nginxParams[i].Name {
|
||||
case "ssl_certificate":
|
||||
nginxParams[i].Params = []string{path.Join("/www", "sites", alias, "ssl", "fullchain.pem")}
|
||||
case "ssl_certificate_key":
|
||||
nginxParams[i].Params = []string{path.Join("/www", "sites", alias, "ssl", "privkey.pem")}
|
||||
case "ssl_protocols":
|
||||
nginxParams[i].Params = req.SSLProtocol
|
||||
if len(req.SSLProtocol) == 0 {
|
||||
nginxParams[i].Params = []string{"TLSv1.3", "TLSv1.2"}
|
||||
}
|
||||
}
|
||||
if param.Name == "ssl_ciphers" {
|
||||
case "ssl_ciphers":
|
||||
nginxParams[i].Params = []string{req.Algorithm}
|
||||
if len(req.Algorithm) == 0 {
|
||||
nginxParams[i].Params = []string{"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED"}
|
||||
}
|
||||
}
|
||||
if param.Name == "error_page" {
|
||||
if len(param.Params) < 2 {
|
||||
continue
|
||||
}
|
||||
code := param.Params[0]
|
||||
if code == "497" {
|
||||
if sslPort != 443 && param.Params[1] == "https://$host$request_uri" {
|
||||
param.Params[1] = fmt.Sprintf("https://$host:%d$request_uri", sslPort)
|
||||
}
|
||||
case "error_page":
|
||||
if len(nginxParams[i].Params) >= 2 && nginxParams[i].Params[0] == "497" && redirectPort != 443 && nginxParams[i].Params[1] == "https://$host$request_uri" {
|
||||
nginxParams[i].Params[1] = fmt.Sprintf("https://$host:%d$request_uri", redirectPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.Hsts {
|
||||
var hstsValue string
|
||||
hstsValue := "\"max-age=31536000\""
|
||||
if req.HstsIncludeSubDomains {
|
||||
hstsValue = "\"max-age=31536000; includeSubDomains\""
|
||||
} else {
|
||||
hstsValue = "\"max-age=31536000\""
|
||||
}
|
||||
nginxParams = append(nginxParams, dto.NginxParam{
|
||||
Name: "add_header",
|
||||
@@ -960,11 +906,80 @@ func applySSL(website *model.Website, websiteSSL model.WebsiteSSL, req request.W
|
||||
Params: []string{"Alt-Svc", "'h3=\":443\"; ma=2592000'"},
|
||||
})
|
||||
}
|
||||
return nginxParams
|
||||
}
|
||||
|
||||
if err := updateNginxConfig(constant.NginxScopeServer, nginxParams, website); err != nil {
|
||||
func applyWebsiteSSLConfig(server *components.Server, website model.Website, plan websiteTLSPlan, req request.WebsiteHTTPSOp) {
|
||||
for _, port := range plan.httpsPorts {
|
||||
portStr := strconv.Itoa(port)
|
||||
server.RemoveListenByBind(portStr)
|
||||
server.RemoveListenByBind("[::]:" + portStr)
|
||||
setListen(server, portStr, website.IPV6, req.Http3, website.DefaultServer, true)
|
||||
}
|
||||
server.UpdateDirective("http2", []string{"on"})
|
||||
|
||||
defaultHTTPPort := strconv.Itoa(plan.defaultHTTPPort)
|
||||
switch req.HttpConfig {
|
||||
case constant.HTTPSOnly:
|
||||
if plan.hasDefaultHTTP {
|
||||
server.RemoveListenByBind(defaultHTTPPort)
|
||||
server.RemoveListenByBind("[::]:" + defaultHTTPPort)
|
||||
}
|
||||
server.RemoveDirective("if", []string{"($scheme"})
|
||||
case constant.HTTPToHTTPS:
|
||||
if plan.hasDefaultHTTP {
|
||||
setListen(server, defaultHTTPPort, website.IPV6, false, website.DefaultServer, false)
|
||||
}
|
||||
if plan.redirectPort > 0 {
|
||||
server.AddHTTP2HTTPS(plan.redirectPort)
|
||||
}
|
||||
case constant.HTTPAlso:
|
||||
if plan.hasDefaultHTTP {
|
||||
setListen(server, defaultHTTPPort, website.IPV6, false, website.DefaultServer, false)
|
||||
}
|
||||
server.RemoveDirective("if", []string{"($scheme"})
|
||||
}
|
||||
|
||||
if !req.Hsts {
|
||||
server.RemoveDirective("add_header", []string{"Strict-Transport-Security", "\"max-age=31536000\""})
|
||||
server.RemoveDirective("add_header", []string{"Strict-Transport-Security", "\"max-age=31536000; includeSubDomains\""})
|
||||
}
|
||||
if !req.Http3 {
|
||||
for _, port := range plan.httpsPorts {
|
||||
server.RemoveListen(strconv.Itoa(port), "quic")
|
||||
if website.IPV6 {
|
||||
server.RemoveListen("[::]:"+strconv.Itoa(port), "quic")
|
||||
}
|
||||
}
|
||||
server.RemoveDirective("add_header", []string{"Alt-Svc"})
|
||||
}
|
||||
|
||||
for _, param := range buildWebsiteSSLParams(website.Alias, req, plan.redirectPort) {
|
||||
server.UpdateDirective(param.Name, param.Params)
|
||||
}
|
||||
}
|
||||
|
||||
func applySSL(website *model.Website, websiteSSL model.WebsiteSSL, req request.WebsiteHTTPSOp) error {
|
||||
nginxFull, err := getNginxFull(website)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
domains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(website.ID))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
config := nginxFull.SiteConfig.Config
|
||||
server := config.FindServers()[0]
|
||||
plan := buildWebsiteTLSPlan(domains, nginxFull.Install.HttpPort, nginxFull.Install.HttpsPort)
|
||||
applyWebsiteSSLConfig(server, *website, plan, req)
|
||||
|
||||
if err = createPemFile(*website, websiteSSL); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
|
||||
return err
|
||||
}
|
||||
return nginxCheckAndReload(nginxFull.SiteConfig.OldContent, nginxFull.SiteConfig.FilePath, nginxFull.Install.ContainerName)
|
||||
}
|
||||
|
||||
func getParamArray(key string, param interface{}) []string {
|
||||
|
||||
@@ -99,6 +99,7 @@ const (
|
||||
TaskScopeTamper = "Tamper"
|
||||
TaskScopeFileConvert = "Convert"
|
||||
TaskScopeTask = "Task"
|
||||
TaskScopeVm = "VirtualMachine"
|
||||
)
|
||||
|
||||
func GetTaskName(resourceName, operate, scope string) string {
|
||||
@@ -154,7 +155,7 @@ func NewTask(name, operate, taskScope, taskID string, resourceID uint) (*Task, e
|
||||
logPath := path.Join(global.Dir.TaskDir, taskScope, taskID+".log")
|
||||
logger := logrus.New()
|
||||
logger.SetFormatter(&SimpleFormatter{})
|
||||
logFile, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, constant.FilePerm)
|
||||
logFile, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY|os.O_APPEND, constant.FilePerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open log file: %w", err)
|
||||
}
|
||||
|
||||
+415
-339
@@ -116,6 +116,16 @@
|
||||
"formatZH": "删除告警配置 [id]",
|
||||
"formatEN": "delete alert config [id]"
|
||||
},
|
||||
"/alert/config/status": {
|
||||
"bodyKeys": [
|
||||
"id",
|
||||
"status"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新告警配置状态 [id][status]",
|
||||
"formatEN": "update alert config status [id][status]"
|
||||
},
|
||||
"/alert/config/update": {
|
||||
"bodyKeys": [
|
||||
"id",
|
||||
@@ -255,8 +265,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "应用端口修改 [key]-[name] =\u003e [port]",
|
||||
"formatEN": "Application port update [key]-[name] =\u003e [port]"
|
||||
"formatZH": "应用端口修改 [key]-[name] => [port]",
|
||||
"formatEN": "Application port update [key]-[name] => [port]"
|
||||
},
|
||||
"/apps/installed/sort/update": {
|
||||
"bodyKeys": [],
|
||||
@@ -669,8 +679,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "容器重命名 [name] =\u003e [newName]",
|
||||
"formatEN": "rename container [name] =\u003e [newName]"
|
||||
"formatZH": "容器重命名 [name] => [newName]",
|
||||
"formatEN": "rename container [name] => [newName]"
|
||||
},
|
||||
"/containers/repo": {
|
||||
"bodyKeys": [
|
||||
@@ -821,8 +831,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
||||
"formatEN": "update api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
||||
"formatZH": "更新 API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
||||
"formatEN": "update api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
||||
},
|
||||
"/core/auth/expired/reset": {
|
||||
"bodyKeys": [],
|
||||
@@ -1706,8 +1716,8 @@
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
||||
"formatEN": "update user [name] api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
||||
"formatZH": "更新用户 [name] API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]",
|
||||
"formatEN": "update user [name] api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"
|
||||
},
|
||||
"/core/enterprise/users/del": {
|
||||
"bodyKeys": [
|
||||
@@ -1745,262 +1755,6 @@
|
||||
"formatZH": "更新 [name]",
|
||||
"formatEN": "update user [name]"
|
||||
},
|
||||
"/core/enterprise/vms": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "创建虚拟机 [name]",
|
||||
"formatEN": "create VM [name]"
|
||||
},
|
||||
"/core/enterprise/vms/del": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "删除虚拟机 [name]",
|
||||
"formatEN": "delete VM [name]"
|
||||
},
|
||||
"/core/enterprise/vms/environment/enable": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "启用虚拟机运行环境",
|
||||
"formatEN": "enable VM runtime environment"
|
||||
},
|
||||
"/core/enterprise/vms/iso": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "创建虚拟机镜像 [name] [type]",
|
||||
"formatEN": "create VM ISO [name] [type]"
|
||||
},
|
||||
"/core/enterprise/vms/iso/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "vm_isos",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "删除虚拟机镜像 [name]",
|
||||
"formatEN": "delete VM ISO [name]"
|
||||
},
|
||||
"/core/enterprise/vms/iso/update": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新虚拟机镜像 [name] [type]",
|
||||
"formatEN": "update VM ISO [name] [type]"
|
||||
},
|
||||
"/core/enterprise/vms/networks": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "创建虚拟机网络 [name] [type]",
|
||||
"formatEN": "create VM network [name] [type]"
|
||||
},
|
||||
"/core/enterprise/vms/networks/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "vm_networks",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "删除虚拟机网络 [name]",
|
||||
"formatEN": "delete VM network [name]"
|
||||
},
|
||||
"/core/enterprise/vms/networks/update": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "vm_networks",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "更新虚拟机网络 [name]",
|
||||
"formatEN": "update VM network [name]"
|
||||
},
|
||||
"/core/enterprise/vms/operate": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"operate"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "操作虚拟机 [name] [operate]",
|
||||
"formatEN": "operate VM [name] [operate]"
|
||||
},
|
||||
"/core/enterprise/vms/orphans/clean": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "清理虚拟机孤立数据",
|
||||
"formatEN": "clean VM orphaned data"
|
||||
},
|
||||
"/core/enterprise/vms/rename": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"newName"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "重命名虚拟机 [name] 为 [newName]",
|
||||
"formatEN": "rename VM [name] to [newName]"
|
||||
},
|
||||
"/core/enterprise/vms/snapshots": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"snapshotName"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
|
||||
"formatEN": "create VM [name] snapshot [snapshotName]"
|
||||
},
|
||||
"/core/enterprise/vms/snapshots/del": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"snapshotName"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
|
||||
"formatEN": "delete VM [name] snapshot [snapshotName]"
|
||||
},
|
||||
"/core/enterprise/vms/snapshots/recover": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"snapshotName"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
|
||||
"formatEN": "recover VM [name] snapshot [snapshotName]"
|
||||
},
|
||||
"/core/enterprise/vms/storages": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "创建虚拟机存储 [name] [type]",
|
||||
"formatEN": "create VM storage [name] [type]"
|
||||
},
|
||||
"/core/enterprise/vms/storages/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "vm_storages",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "删除虚拟机存储 [name]",
|
||||
"formatEN": "delete VM storage [name]"
|
||||
},
|
||||
"/core/enterprise/vms/storages/update": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "vm_storages",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "更新虚拟机存储 [name]",
|
||||
"formatEN": "update VM storage [name]"
|
||||
},
|
||||
"/core/enterprise/vms/sync": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "同步虚拟机",
|
||||
"formatEN": "sync virtual machines"
|
||||
},
|
||||
"/core/enterprise/vms/templates": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"templateName"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
|
||||
"formatEN": "create VM [name] template [templateName]"
|
||||
},
|
||||
"/core/enterprise/vms/templates/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "vm_templates",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "删除虚拟机模板 [name]",
|
||||
"formatEN": "delete VM template [name]"
|
||||
},
|
||||
"/core/enterprise/vms/update": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新虚拟机 [name]",
|
||||
"formatEN": "update VM [name]"
|
||||
},
|
||||
"/core/groups": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
@@ -2164,8 +1918,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改系统监听信息 =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]",
|
||||
"formatEN": "update system bind info =\u003e ipv6: [ipv6], 监听 IP: [bindAddress]"
|
||||
"formatZH": "修改系统监听信息 => ipv6: [ipv6], 监听 IP: [bindAddress]",
|
||||
"formatEN": "update system bind info => ipv6: [ipv6], 监听 IP: [bindAddress]"
|
||||
},
|
||||
"/core/settings/memo": {
|
||||
"bodyKeys": [],
|
||||
@@ -2194,8 +1948,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改系统端口 =\u003e [serverPort]",
|
||||
"formatEN": "update system port =\u003e [serverPort]"
|
||||
"formatZH": "修改系统端口 => [serverPort]",
|
||||
"formatEN": "update system port => [serverPort]"
|
||||
},
|
||||
"/core/settings/proxy/update": {
|
||||
"bodyKeys": [
|
||||
@@ -2220,8 +1974,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改系统 ssl =\u003e [ssl]",
|
||||
"formatEN": "update system ssl =\u003e [ssl]"
|
||||
"formatZH": "修改系统 ssl => [ssl]",
|
||||
"formatEN": "update system ssl => [ssl]"
|
||||
},
|
||||
"/core/settings/terminal/update": {
|
||||
"bodyKeys": [],
|
||||
@@ -2237,8 +1991,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改系统配置 [key] =\u003e [value]",
|
||||
"formatEN": "update system setting [key] =\u003e [value]"
|
||||
"formatZH": "修改系统配置 [key] => [value]",
|
||||
"formatEN": "update system setting [key] => [value]"
|
||||
},
|
||||
"/core/settings/upgrade": {
|
||||
"bodyKeys": [
|
||||
@@ -2246,8 +2000,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新系统 =\u003e [version]",
|
||||
"formatEN": "upgrade system =\u003e [version]"
|
||||
"formatZH": "更新系统 => [version]",
|
||||
"formatEN": "upgrade system => [version]"
|
||||
},
|
||||
"/core/xapp/appQRCode": {
|
||||
"bodyKeys": [
|
||||
@@ -2705,6 +2459,282 @@
|
||||
"formatZH": "同步 SSL 证书 [primaryDomain]",
|
||||
"formatEN": "sync SSL certificate [primaryDomain]"
|
||||
},
|
||||
"/xpack/vms": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "创建虚拟机 [name]",
|
||||
"formatEN": "create VM [name]"
|
||||
},
|
||||
"/xpack/vms/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "virtual_machines",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "删除虚拟机 [name]",
|
||||
"formatEN": "delete VM [name]"
|
||||
},
|
||||
"/xpack/vms/disks/resize": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"target",
|
||||
"sizeGiB"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "扩容虚拟机 [name] 磁盘 [target] 至 [sizeGiB] GiB",
|
||||
"formatEN": "expand VM [name] disk [target] to [sizeGiB] GiB"
|
||||
},
|
||||
"/xpack/vms/environment/enable": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "启用虚拟机运行环境",
|
||||
"formatEN": "enable VM runtime environment"
|
||||
},
|
||||
"/xpack/vms/iso": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "创建虚拟机镜像 [name] [type]",
|
||||
"formatEN": "create VM ISO [name] [type]"
|
||||
},
|
||||
"/xpack/vms/iso/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "vm_isos",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "删除虚拟机镜像 [name]",
|
||||
"formatEN": "delete VM ISO [name]"
|
||||
},
|
||||
"/xpack/vms/iso/update": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新虚拟机镜像 [name] [type]",
|
||||
"formatEN": "update VM ISO [name] [type]"
|
||||
},
|
||||
"/xpack/vms/networks": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "创建虚拟机网络 [name] [type]",
|
||||
"formatEN": "create VM network [name] [type]"
|
||||
},
|
||||
"/xpack/vms/networks/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "vm_networks",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "删除虚拟机网络 [name]",
|
||||
"formatEN": "delete VM network [name]"
|
||||
},
|
||||
"/xpack/vms/networks/update": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "vm_networks",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "更新虚拟机网络 [name]",
|
||||
"formatEN": "update VM network [name]"
|
||||
},
|
||||
"/xpack/vms/operate": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"operate"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "操作虚拟机 [name] [operate]",
|
||||
"formatEN": "operate VM [name] [operate]"
|
||||
},
|
||||
"/xpack/vms/orphans/clean": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "清理虚拟机孤立数据",
|
||||
"formatEN": "clean VM orphaned data"
|
||||
},
|
||||
"/xpack/vms/rename": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"newName"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "重命名虚拟机 [name] 为 [newName]",
|
||||
"formatEN": "rename VM [name] to [newName]"
|
||||
},
|
||||
"/xpack/vms/snapshots": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"snapshotName"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
|
||||
"formatEN": "create VM [name] snapshot [snapshotName]"
|
||||
},
|
||||
"/xpack/vms/snapshots/del": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"snapshotName"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
|
||||
"formatEN": "delete VM [name] snapshot [snapshotName]"
|
||||
},
|
||||
"/xpack/vms/snapshots/recover": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"snapshotName"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
|
||||
"formatEN": "recover VM [name] snapshot [snapshotName]"
|
||||
},
|
||||
"/xpack/vms/storages": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "创建虚拟机存储 [name] [type]",
|
||||
"formatEN": "create VM storage [name] [type]"
|
||||
},
|
||||
"/xpack/vms/storages/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "vm_storages",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "删除虚拟机存储 [name]",
|
||||
"formatEN": "delete VM storage [name]"
|
||||
},
|
||||
"/xpack/vms/storages/update": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "vm_storages",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "更新虚拟机存储 [name]",
|
||||
"formatEN": "update VM storage [name]"
|
||||
},
|
||||
"/xpack/vms/sync": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "同步虚拟机",
|
||||
"formatEN": "sync virtual machines"
|
||||
},
|
||||
"/xpack/vms/templates": {
|
||||
"bodyKeys": [
|
||||
"name",
|
||||
"templateName"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
|
||||
"formatEN": "create VM [name] template [templateName]"
|
||||
},
|
||||
"/xpack/vms/templates/del": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "id",
|
||||
"isList": false,
|
||||
"db": "vm_templates",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "删除虚拟机模板 [name]",
|
||||
"formatEN": "delete VM template [name]"
|
||||
},
|
||||
"/xpack/vms/update": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新虚拟机 [name]",
|
||||
"formatEN": "update VM [name]"
|
||||
},
|
||||
"/cronjobs": {
|
||||
"bodyKeys": [
|
||||
"type",
|
||||
@@ -2858,8 +2888,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "首页应用 [key] =\u003e 显示:[value]",
|
||||
"formatEN": "app launcher [key] =\u003e show: [value]"
|
||||
"formatZH": "首页应用 [key] => 显示:[value]",
|
||||
"formatEN": "app launcher [key] => show: [value]"
|
||||
},
|
||||
"/dashboard/quick/change": {
|
||||
"bodyKeys": [],
|
||||
@@ -2987,7 +3017,7 @@
|
||||
}
|
||||
],
|
||||
"formatZH": "mysql 数据库 [name] 描述信息修改 [description]",
|
||||
"formatEN": "The description of the mysql database [name] is modified =\u003e [description]"
|
||||
"formatEN": "The description of the mysql database [name] is modified => [description]"
|
||||
},
|
||||
"/databases/grants": {
|
||||
"bodyKeys": [
|
||||
@@ -3068,7 +3098,7 @@
|
||||
}
|
||||
],
|
||||
"formatZH": "mongodb 数据库 [name] 描述信息修改 [description]",
|
||||
"formatEN": "The description of the mongodb database [name] is modified =\u003e [description]"
|
||||
"formatEN": "The description of the mongodb database [name] is modified => [description]"
|
||||
},
|
||||
"/databases/mongodb/password": {
|
||||
"bodyKeys": [
|
||||
@@ -3153,7 +3183,7 @@
|
||||
}
|
||||
],
|
||||
"formatZH": "postgresql 数据库 [name] 描述信息修改 [description]",
|
||||
"formatEN": "The description of the postgresql database [name] is modified =\u003e [description]"
|
||||
"formatEN": "The description of the postgresql database [name] is modified => [description]"
|
||||
},
|
||||
"/databases/pg/password": {
|
||||
"bodyKeys": [
|
||||
@@ -3258,8 +3288,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] =\u003e [newHost] 描述 [description]",
|
||||
"formatEN": "update mysql database [database] user [username] access [host] =\u003e [newHost] description [description]"
|
||||
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]",
|
||||
"formatEN": "update mysql database [database] user [username] access [host] => [newHost] description [description]"
|
||||
},
|
||||
"/databases/variables/update": {
|
||||
"bodyKeys": [],
|
||||
@@ -3295,8 +3325,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "批量修改文件权限和用户/组 [paths] =\u003e [mode]/[user]/[group]",
|
||||
"formatEN": "Batch change file mode and owner [paths] =\u003e [mode]/[user]/[group]"
|
||||
"formatZH": "批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]",
|
||||
"formatEN": "Batch change file mode and owner [paths] => [mode]/[user]/[group]"
|
||||
},
|
||||
"/files/chunkdownload": {
|
||||
"bodyKeys": [
|
||||
@@ -3386,8 +3416,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改权限 [path] =\u003e [mode]",
|
||||
"formatEN": "Change mode [path] =\u003e [mode]"
|
||||
"formatZH": "修改权限 [path] => [mode]",
|
||||
"formatEN": "Change mode [path] => [mode]"
|
||||
},
|
||||
"/files/move": {
|
||||
"bodyKeys": [
|
||||
@@ -3396,8 +3426,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "移动文件 [oldPaths] =\u003e [newPath]",
|
||||
"formatEN": "Move [oldPaths] =\u003e [newPath]"
|
||||
"formatZH": "移动文件 [oldPaths] => [newPath]",
|
||||
"formatEN": "Move [oldPaths] => [newPath]"
|
||||
},
|
||||
"/files/owner": {
|
||||
"bodyKeys": [
|
||||
@@ -3407,8 +3437,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改用户/组 [path] =\u003e [user]/[group]",
|
||||
"formatEN": "Change owner [path] =\u003e [user]/[group]"
|
||||
"formatZH": "修改用户/组 [path] => [user]/[group]",
|
||||
"formatEN": "Change owner [path] => [user]/[group]"
|
||||
},
|
||||
"/files/preview": {
|
||||
"bodyKeys": [
|
||||
@@ -3442,8 +3472,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "重命名 [oldName] =\u003e [newName]",
|
||||
"formatEN": "Rename [oldName] =\u003e [newName]"
|
||||
"formatZH": "重命名 [oldName] => [newName]",
|
||||
"formatEN": "Rename [oldName] => [newName]"
|
||||
},
|
||||
"/files/save": {
|
||||
"bodyKeys": [
|
||||
@@ -3500,8 +3530,17 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "下载 url =\u003e [path]/[name]",
|
||||
"formatEN": "Download url =\u003e [path]/[name]"
|
||||
"formatZH": "下载 url => [path]/[name]",
|
||||
"formatEN": "Download url => [path]/[name]"
|
||||
},
|
||||
"/files/wget/process/remove": {
|
||||
"bodyKeys": [
|
||||
"keys"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "移除已结束下载记录 [keys]",
|
||||
"formatEN": "Remove finished download records [keys]"
|
||||
},
|
||||
"/files/wget/stop": {
|
||||
"bodyKeys": [
|
||||
@@ -3627,13 +3666,11 @@
|
||||
"formatEN": "[operation] Docker port guard"
|
||||
},
|
||||
"/hosts/firewall/docker/policies/batch": {
|
||||
"bodyKeys": [
|
||||
"mode"
|
||||
],
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "批量更新 Docker 端口防护策略 [mode]",
|
||||
"formatEN": "batch update Docker port guard policies [mode]"
|
||||
"formatZH": "批量更新 Docker 端口防护策略",
|
||||
"formatEN": "batch update Docker port guard policies"
|
||||
},
|
||||
"/hosts/firewall/docker/policies/delete/batch": {
|
||||
"bodyKeys": [
|
||||
@@ -3644,12 +3681,16 @@
|
||||
"formatZH": "删除 Docker 端口防护策略 [uuids]",
|
||||
"formatEN": "delete Docker port guard policies [uuids]"
|
||||
},
|
||||
"/hosts/firewall/docker/sync": {
|
||||
"bodyKeys": [],
|
||||
"/hosts/firewall/family/operate": {
|
||||
"bodyKeys": [
|
||||
"subsystem",
|
||||
"family",
|
||||
"operation"
|
||||
],
|
||||
"formatEN": "[operation] [subsystem] [family] firewall chains",
|
||||
"formatZH": "[operation] [subsystem] [family] 防火墙链",
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "同步 Docker 端口防护规则",
|
||||
"formatEN": "sync Docker port guard rules"
|
||||
"beforeFunctions": []
|
||||
},
|
||||
"/hosts/firewall/filter/operate": {
|
||||
"bodyKeys": [
|
||||
@@ -3699,12 +3740,12 @@
|
||||
},
|
||||
"/hosts/firewall/rules/reorder": {
|
||||
"bodyKeys": [
|
||||
"uuid"
|
||||
"instanceKey"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "调整防火墙规则顺序 [uuid]",
|
||||
"formatEN": "reorder firewall rule [uuid]"
|
||||
"formatZH": "调整防火墙规则顺序 [instanceKey]",
|
||||
"formatEN": "reorder firewall rule [instanceKey]"
|
||||
},
|
||||
"/hosts/firewall/rules/reset": {
|
||||
"bodyKeys": [],
|
||||
@@ -3713,25 +3754,23 @@
|
||||
"formatZH": "重置防火墙规则",
|
||||
"formatEN": "reset firewall rules"
|
||||
},
|
||||
"/hosts/firewall/rules/sync": {
|
||||
"bodyKeys": [
|
||||
"subsystem",
|
||||
"sourceProvider",
|
||||
"targetProvider"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "同步 [subsystem] 防火墙规则到 [targetProvider]",
|
||||
"formatEN": "sync [subsystem] firewall rules to [targetProvider]"
|
||||
},
|
||||
"/hosts/firewall/rules/update": {
|
||||
"bodyKeys": [
|
||||
"uuid"
|
||||
"instanceKey"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新防火墙规则 [uuid]",
|
||||
"formatEN": "update firewall rule [uuid]"
|
||||
"formatZH": "更新防火墙规则 [instanceKey]",
|
||||
"formatEN": "update firewall rule [instanceKey]"
|
||||
},
|
||||
"/hosts/firewall/settings/ipv6": {
|
||||
"bodyKeys": [
|
||||
"status"
|
||||
],
|
||||
"formatEN": "Set firewall IPv6 support to [status]",
|
||||
"formatZH": "设置防火墙 IPv6 支持为 [status]",
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": []
|
||||
},
|
||||
"/hosts/firewall/settings/operate": {
|
||||
"bodyKeys": [
|
||||
@@ -3744,6 +3783,34 @@
|
||||
"formatZH": "防火墙子系统 [subsystem] 后端 [operation] [backend]",
|
||||
"formatEN": "[operation] firewall [subsystem] backend [backend]"
|
||||
},
|
||||
"/hosts/firewall/settings/whitelist": {
|
||||
"bodyKeys": [
|
||||
"rule"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "创建防火墙端口白名单",
|
||||
"formatEN": "create firewall port whitelist"
|
||||
},
|
||||
"/hosts/firewall/settings/whitelist/delete": {
|
||||
"bodyKeys": [
|
||||
"rules"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "删除防火墙端口白名单",
|
||||
"formatEN": "delete firewall port whitelist"
|
||||
},
|
||||
"/hosts/firewall/settings/whitelist/update": {
|
||||
"bodyKeys": [
|
||||
"oldRule",
|
||||
"rule"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "编辑防火墙端口白名单",
|
||||
"formatEN": "update firewall port whitelist"
|
||||
},
|
||||
"/hosts/monitor/clean": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
@@ -3821,8 +3888,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改 SSH 配置 [key] =\u003e [newValue]",
|
||||
"formatEN": "update SSH setting [key] =\u003e [newValue]"
|
||||
"formatZH": "修改 SSH 配置 [key] => [newValue]",
|
||||
"formatEN": "update SSH setting [key] => [newValue]"
|
||||
},
|
||||
"/hosts/tool/config/set": {
|
||||
"bodyKeys": [
|
||||
@@ -3967,12 +4034,21 @@
|
||||
},
|
||||
"/runtimes/operate": {
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
"ID"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "操作运行环境 [id]",
|
||||
"formatEN": "Operate runtime [id]"
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "ID",
|
||||
"isList": false,
|
||||
"db": "runtimes",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "操作运行环境 [name]",
|
||||
"formatEN": "Operate runtime [name]"
|
||||
},
|
||||
"/runtimes/php/config": {
|
||||
"bodyKeys": [
|
||||
@@ -4074,7 +4150,7 @@
|
||||
}
|
||||
],
|
||||
"formatZH": "快照 [name] 描述信息修改 [description]",
|
||||
"formatEN": "The description of the snapshot [name] is modified =\u003e [description]"
|
||||
"formatEN": "The description of the snapshot [name] is modified => [description]"
|
||||
},
|
||||
"/settings/snapshot/import": {
|
||||
"bodyKeys": [
|
||||
@@ -4166,8 +4242,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改系统配置 [key] =\u003e [value]",
|
||||
"formatEN": "update system setting [key] =\u003e [value]"
|
||||
"formatZH": "修改系统配置 [key] => [value]",
|
||||
"formatEN": "update system setting [key] => [value]"
|
||||
},
|
||||
"/toolbox/clam": {
|
||||
"bodyKeys": [
|
||||
@@ -4285,8 +4361,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改主机参数 [key] =\u003e [value]",
|
||||
"formatEN": "update device conf [key] =\u003e [value]"
|
||||
"formatZH": "修改主机参数 [key] => [value]",
|
||||
"formatEN": "update device conf [key] => [value]"
|
||||
},
|
||||
"/toolbox/device/update/host": {
|
||||
"bodyKeys": [
|
||||
@@ -4295,8 +4371,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改主机 Host [key] =\u003e [value]",
|
||||
"formatEN": "update device host [key] =\u003e [value]"
|
||||
"formatZH": "修改主机 Host [key] => [value]",
|
||||
"formatEN": "update device host [key] => [value]"
|
||||
},
|
||||
"/toolbox/device/update/swap": {
|
||||
"bodyKeys": [
|
||||
@@ -4324,8 +4400,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "修改 Fail2ban 配置 [key] =\u003e [value]",
|
||||
"formatEN": "update fail2ban conf [key] =\u003e [value]"
|
||||
"formatZH": "修改 Fail2ban 配置 [key] => [value]",
|
||||
"formatEN": "update fail2ban conf [key] => [value]"
|
||||
},
|
||||
"/toolbox/ftp": {
|
||||
"bodyKeys": [
|
||||
@@ -4584,8 +4660,8 @@
|
||||
"output_value": "domain"
|
||||
}
|
||||
],
|
||||
"formatZH": "修改默认 server =\u003e [domain]",
|
||||
"formatEN": "Change default server =\u003e [domain]"
|
||||
"formatZH": "修改默认 server => [domain]",
|
||||
"formatEN": "Change default server => [domain]"
|
||||
},
|
||||
"/websites/del": {
|
||||
"bodyKeys": [
|
||||
@@ -5348,8 +5424,8 @@
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "更新防篡改信息 [website][path] =\u003e [status]",
|
||||
"formatEN": "update tamper info [website][path] =\u003e [status]"
|
||||
"formatZH": "更新防篡改信息 [website][path] => [status]",
|
||||
"formatEN": "update tamper info [website][path] => [status]"
|
||||
},
|
||||
"/xpack/vllm/command-template/create": {
|
||||
"bodyKeys": [
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
gzip on;
|
||||
gzip_comp_level 6;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1k;
|
||||
gzip_types text/plain text/css text/xml text/javascript text/x-component application/json application/javascript application/x-javascript application/xml application/xhtml+xml application/rss+xml application/atom+xml application/x-font-ttf application/vnd.ms-fontobject image/svg+xml image/x-icon font/opentype;
|
||||
gzip_buffers 4 16k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_comp_level 5;
|
||||
gzip_proxied any;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/json application/ld+json application/javascript application/x-javascript application/xml application/xhtml+xml application/rss+xml application/atom+xml application/wasm image/svg+xml font/ttf font/otf;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
FirewallProviderFirewalld = "firewalld"
|
||||
FirewallProviderUFW = "ufw"
|
||||
FirewallProviderIptables = "iptables"
|
||||
FirewallProviderNftables = "nftables"
|
||||
FirewallProviderFirewalld = "firewalld"
|
||||
FirewallProviderUFW = "ufw"
|
||||
FirewallProviderIptables = "iptables"
|
||||
FirewallProviderNftables = "nftables"
|
||||
FirewallBackendNotInstalled = "backend_not_installed"
|
||||
|
||||
FirewallFamilyIPv4 = "ipv4"
|
||||
FirewallFamilyIPv6 = "ipv6"
|
||||
@@ -16,6 +17,7 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
FirewallIPv6SupportKey = "FirewallIPv6Support"
|
||||
FirewallSystemBackendKey = "FirewallProvider"
|
||||
FirewallForwardingBackendKey = "ForwardingBackend"
|
||||
FirewallDockerBackendKey = "DockerFirewallBackend"
|
||||
@@ -26,19 +28,13 @@ const (
|
||||
FirewallPingStatusKey = "BanPing"
|
||||
|
||||
FirewallPortWhiteList = "FirewallPortWhiteList"
|
||||
FirewallPortWhiteListValue = "80/tcp,443/tcp,443/udp"
|
||||
FirewallPortWhiteListValue = `[{"port":"80","protocol":"tcp","sources":["0.0.0.0/0","::/0"]},{"port":"443","protocol":"tcp","sources":["0.0.0.0/0","::/0"]},{"port":"443","protocol":"udp","sources":["0.0.0.0/0","::/0"]}]`
|
||||
)
|
||||
|
||||
const (
|
||||
FirewallSystemAcceptedPortSourcePrefix = "accepted-port:"
|
||||
FirewallRuleCheckVersion = 1
|
||||
|
||||
FirewallRuleOriginCreated = "created"
|
||||
FirewallRuleOriginAdopted = "adopted"
|
||||
|
||||
FirewallRuleSourceUser = "user"
|
||||
FirewallRuleSourceImported = "imported"
|
||||
FirewallRuleSourcePanel = "panel"
|
||||
FirewallRuleSourceSecurity = "security"
|
||||
FirewallRuleSourceApp = "application"
|
||||
)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
StatusInactive = "Inactive"
|
||||
StatusUnknown = "Unknown"
|
||||
StatusDegraded = "Degraded"
|
||||
StatusInaccessible = "Inaccessible"
|
||||
StatusRunning = "Running"
|
||||
StatusCanceled = "Canceled"
|
||||
StatusDone = "Done"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
@@ -38,6 +39,28 @@ func Run() {
|
||||
}
|
||||
}
|
||||
|
||||
var gpuStatus, gpuInterval model.Setting
|
||||
if err := global.DB.Where("key = ?", "GPUMonitorStatus").First(&gpuStatus).Error; err != nil {
|
||||
global.LOG.Errorf("Load GPU monitoring status failed: %v", err)
|
||||
} else if gpuStatus.Value == constant.StatusEnable {
|
||||
if err := global.DB.Where("key = ?", "GPUMonitorInterval").First(&gpuInterval).Error; err != nil {
|
||||
global.LOG.Errorf("Load GPU monitoring interval failed: %v", err)
|
||||
} else if err := service.StartGPUMonitor(gpuInterval.Value); err != nil {
|
||||
global.LOG.Errorf("Start GPU monitoring failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var vllmStatus, vllmInterval model.Setting
|
||||
if err := global.DB.Where("key = ?", "VLLMMonitorStatus").First(&vllmStatus).Error; err != nil {
|
||||
global.LOG.Errorf("Load vLLM monitoring status failed: %v", err)
|
||||
} else if vllmStatus.Value == constant.StatusEnable {
|
||||
if err := global.DB.Where("key = ?", "VLLMMonitorInterval").First(&vllmInterval).Error; err != nil {
|
||||
global.LOG.Errorf("Load vLLM monitoring interval failed: %v", err)
|
||||
} else if err := service.StartVLLMMonitor(vllmInterval.Value); err != nil {
|
||||
global.LOG.Errorf("Start vLLM monitoring failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := global.Cron.AddJob("@daily", job.NewWebsiteJob()); err != nil {
|
||||
global.LOG.Errorf("can not add website corn job: %s", err.Error())
|
||||
}
|
||||
@@ -60,6 +83,17 @@ func Run() {
|
||||
if _, err := global.Cron.AddJob("0 3 */31 * *", job.NewBackupJob()); err != nil {
|
||||
global.LOG.Errorf("can not add backup token refresh corn job: %s", err.Error())
|
||||
}
|
||||
if _, err := global.Cron.AddFunc("@every 240h", func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
defer cancel()
|
||||
deleted, err := service.NewISettingService().CleanupDescriptions(ctx)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("clean unused common descriptions: %v", err)
|
||||
}
|
||||
global.LOG.Infof("common description cleanup completed, deleted %d records", deleted)
|
||||
}); err != nil {
|
||||
global.LOG.Errorf("add common description cleanup job: %v", err)
|
||||
}
|
||||
|
||||
var cronJobs []model.Cronjob
|
||||
if err := global.DB.Where("status = ?", constant.StatusEnable).Find(&cronJobs).Error; err != nil {
|
||||
|
||||
+11
-8
@@ -14,12 +14,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
DB *gorm.DB
|
||||
MonitorDB *gorm.DB
|
||||
GPUMonitorDB *gorm.DB
|
||||
TaskDB *gorm.DB
|
||||
CoreDB *gorm.DB
|
||||
AlertDB *gorm.DB
|
||||
DB *gorm.DB
|
||||
MonitorDB *gorm.DB
|
||||
GPUMonitorDB *gorm.DB
|
||||
VLLMMonitorDB *gorm.DB
|
||||
TaskDB *gorm.DB
|
||||
CoreDB *gorm.DB
|
||||
AlertDB *gorm.DB
|
||||
|
||||
LOG *logrus.Logger
|
||||
CONF ServerConfig
|
||||
@@ -29,8 +30,10 @@ var (
|
||||
|
||||
Dir SystemDir
|
||||
|
||||
Cron *cron.Cron
|
||||
MonitorCronID cron.EntryID
|
||||
Cron *cron.Cron
|
||||
MonitorCronID cron.EntryID
|
||||
VLLMMonitorCronID cron.EntryID
|
||||
GPUMonitorCronID cron.EntryID
|
||||
|
||||
IsMaster bool
|
||||
|
||||
|
||||
+24
-26
@@ -1,14 +1,12 @@
|
||||
module github.com/1Panel-dev/1Panel/agent
|
||||
|
||||
go 1.26.1
|
||||
|
||||
replace github.com/moby/go-archive => github.com/moby/go-archive v0.1.0
|
||||
go 1.26.6
|
||||
|
||||
replace github.com/go-acme/lego/v5 => github.com/1Panel-dev/lego/v5 v5.3.1
|
||||
|
||||
require (
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
github.com/compose-spec/compose-go/v2 v2.14.0
|
||||
github.com/compose-spec/compose-go/v2 v2.15.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/docker/cli v29.7.2+incompatible
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
@@ -31,8 +29,9 @@ require (
|
||||
github.com/klauspost/compress v1.19.2
|
||||
github.com/mattn/go-shellwords v1.0.14
|
||||
github.com/mholt/archiver/v4 v4.0.0-alpha.8
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/minio/minio-go/v7 v7.2.1
|
||||
github.com/miekg/dns v1.1.73
|
||||
github.com/minio/minio-go/v7 v7.3.0
|
||||
github.com/moby/go-archive v0.3.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
@@ -42,7 +41,7 @@ require (
|
||||
github.com/qiniu/go-sdk/v7 v7.27.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v4 v4.26.7
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/sirupsen/logrus v1.10.2
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
@@ -52,13 +51,13 @@ require (
|
||||
github.com/tklauser/go-sysconf v0.4.0
|
||||
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19
|
||||
github.com/upyun/go-sdk v2.1.0+incompatible
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0
|
||||
golang.org/x/crypto v0.54.0
|
||||
golang.org/x/net v0.57.0
|
||||
golang.org/x/sync v0.22.0
|
||||
go.mongodb.org/mongo-driver/v2 v2.9.0
|
||||
golang.org/x/crypto v0.56.0
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/sync v0.23.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/text v0.40.0
|
||||
golang.org/x/time v0.15.0
|
||||
golang.org/x/text v0.41.0
|
||||
golang.org/x/time v0.16.0
|
||||
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478
|
||||
gopkg.in/ini.v1 v1.67.3
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -121,7 +120,7 @@ require (
|
||||
github.com/go-acme/alidns-20150109/v5 v5.5.0 // indirect
|
||||
github.com/go-acme/esa-20240910/v3 v3.4.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
@@ -132,7 +131,6 @@ require (
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.205 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
@@ -143,7 +141,7 @@ require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
@@ -155,11 +153,10 @@ require (
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.2.0 // indirect
|
||||
github.com/moby/patternmatcher v0.6.1 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/sequential v0.7.0 // indirect
|
||||
github.com/moby/sys/user v0.4.1 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
@@ -209,17 +206,18 @@ require (
|
||||
go.mongodb.org/mongo-driver v1.17.9 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
go.opentelemetry.io/otel v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.45.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||
golang.org/x/arch v0.26.0 // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/tools v0.47.0 // indirect
|
||||
google.golang.org/grpc v1.83.2 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
modernc.org/fileutil v1.4.0 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
|
||||
+61
-60
@@ -205,8 +205,8 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/compose-spec/compose-go/v2 v2.14.0 h1:uaJeo5B3+OVlu+Rx2qLBcAdXPEUUzm5nQrRiGJafRAQ=
|
||||
github.com/compose-spec/compose-go/v2 v2.14.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg=
|
||||
github.com/compose-spec/compose-go/v2 v2.15.0 h1:tdQw+eMyT+P6ZIb09JfcIVvbMmIa+PjST7cWezVLf00=
|
||||
github.com/compose-spec/compose-go/v2 v2.15.0/go.mod h1:Q1+qtN4vhzEjGrnqRtzx1xa8raDZQlMUe3WJxndYNiQ=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
@@ -307,8 +307,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
@@ -438,6 +438,7 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||
@@ -527,8 +528,8 @@ github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi
|
||||
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
@@ -576,16 +577,16 @@ github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60V
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
|
||||
github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
|
||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw=
|
||||
github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
|
||||
github.com/minio/minio-go/v7 v7.3.0 h1:HM4pFCSQq/TK+j0/zmorSh5ddh81iDgRgU0BG0Vz/YU=
|
||||
github.com/minio/minio-go/v7 v7.3.0/go.mod h1:KUPWdecEO1LWyUz+sTGXAuf2jZHrPh5fCsRH86QbPfk=
|
||||
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
@@ -597,16 +598,16 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
|
||||
github.com/moby/go-archive v0.3.0 h1:nos4BtzzUIqB406BgQnWGMI4qib9BZ8XUHU+ucv/n1c=
|
||||
github.com/moby/go-archive v0.3.0/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
|
||||
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
||||
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
|
||||
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
|
||||
github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0=
|
||||
github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
@@ -698,8 +699,6 @@ github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6J
|
||||
github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
|
||||
github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
@@ -759,8 +758,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
|
||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
@@ -802,8 +801,9 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
|
||||
@@ -878,8 +878,8 @@ go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lL
|
||||
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
|
||||
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
|
||||
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8=
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.mongodb.org/mongo-driver/v2 v2.9.0 h1:e2mQdOmbkiYz+dj3faM7lVDwl7WdnRD+g5VicafMhL0=
|
||||
go.mongodb.org/mongo-driver/v2 v2.9.0/go.mod h1:SHKN0IWkKmEVGHLjXnni6s4wPKX4v86FTgOeJJFuXcA=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
@@ -890,23 +890,23 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
|
||||
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 h1:QBajQ2SrwQijzHyZbQlPsuIzpl/ll8DY6wPWsajeGcI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0/go.mod h1:08ZQLjrPLQ6R4kAXvuOvODEer5Yh4CoFvll5qB2BCI8=
|
||||
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
|
||||
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
|
||||
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
|
||||
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
|
||||
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
|
||||
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
|
||||
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
@@ -916,8 +916,9 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i
|
||||
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
|
||||
@@ -951,8 +952,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
|
||||
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -994,8 +995,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1050,8 +1051,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1076,8 +1077,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1186,15 +1187,15 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -1249,8 +1250,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1315,10 +1316,10 @@ google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxH
|
||||
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478 h1:aLsVTW0lZ8+IY5u/ERjZSCvAmhuR7slKzyha3YikDNA=
|
||||
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478/go.mod h1:YJAzKjfHIUHb9T+bfu8L7mthAp7VVXQBUs1PLdBWS7M=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -1336,8 +1337,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
|
||||
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
|
||||
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
||||
+91
-10
@@ -139,6 +139,7 @@ ErrComposeProjectNameParse: 'Failed to parse the project name from Docker Compos
|
||||
ErrComposeProjectNameEmpty: 'Unable to derive a project name from Docker Compose config or the file parent directory. Enter a name and try again.'
|
||||
ErrComposeNameInvalid: 'Invalid Compose project name: it must start with a lowercase letter or number, contain only lowercase letters, numbers, hyphens, and underscores, and be 1-256 characters long.'
|
||||
ErrAppVersionUnavailable: 'This application version has been removed from the remote service. Please select another version and try again.'
|
||||
ErrVllmGB10ProOnly: 'This vLLM version is available only in 1Panel Professional Edition.'
|
||||
ErrAppWarn: 'App status abnormal; check logs'
|
||||
ErrAppParamKey: 'Invalid app parameter: {{ .name }}'
|
||||
ErrAppUpgrade: 'App upgrade failed: {{ .name }} {{ .err }}'
|
||||
@@ -215,6 +216,9 @@ ErrDomainFormat: 'Invalid domain format: {{ .name }}'
|
||||
ErrDefaultAlias: 'default is reserved; use another alias'
|
||||
ErrParentWebsite: 'Delete subsite {{ .name }} first'
|
||||
ErrBuildDirNotFound: 'The build directory does not exist'
|
||||
ErrBrotliDisabled: 'The brotli module is not enabled, enable and build it first'
|
||||
ErrBrotliUnsupported: 'The panel could not wire brotli settings into nginx.conf automatically; check the file or upgrade OpenResty'
|
||||
ErrModuleBuildUnsupported: 'This OpenResty version cannot build modules, upgrade it first'
|
||||
ErrImageNotExist: 'Runtime image not found: {{ .name }}'
|
||||
ErrProxyIsUsed: 'Load balancer is used by reverse proxy'
|
||||
ErrSSLValid: 'Certificate file is invalid'
|
||||
@@ -502,16 +506,6 @@ Container: 'Container'
|
||||
AppLink: 'Linked Application'
|
||||
EnableSSL: 'Enable HTTPS'
|
||||
AppStore: 'App Store'
|
||||
Firewall: 'Firewall'
|
||||
FirewallSyncStep: 'Synchronize rules to {{ .name }}'
|
||||
FirewallSyncResult: 'Synchronization result: {{ .succeeded }} succeeded, {{ .existing }} existing, {{ .failed }} failed'
|
||||
FirewallSyncFailed: '{{ .failed }} firewall rules failed to synchronize'
|
||||
FirewallSyncFailedResetSkipped: '{{ .failed }} firewall rules failed to synchronize; the source firewall was not reset'
|
||||
FirewallResetSourceStep: 'Reset and disable source firewall {{ .name }}'
|
||||
FirewallResetSourceResult: 'Source firewall reset completed; {{ .removed }} database rules were deleted'
|
||||
FirewallVerifyTargetStep: 'Verify target firewall rules'
|
||||
FirewallTargetRuleIneffective: 'The target rule is no longer effective'
|
||||
FirewallVerifyRuleFailed: 'Failed to verify synchronized rule {{ .name }}: {{ .detail }}'
|
||||
TaskSync: 'Sync'
|
||||
LocalApp: 'Local Application'
|
||||
SubTask: 'Subtask'
|
||||
@@ -655,6 +649,7 @@ CommonAlert: "Panel {{ .node }}{{ .ip }}: {{ .msg }}. Log in to view details."
|
||||
NodeExceptionAlert: "Panel {{ .node }}{{ .ip }}: {{ .num }} nodes are abnormal. Log in to view details."
|
||||
LicenseExceptionAlert: "Panel {{ .node }}{{ .ip }}: {{ .num }} licenses are abnormal. Log in to view details."
|
||||
SSHAndPanelLoginAlert: "Panel {{ .node }}{{ .ip }}: abnormal {{ .name }} login from {{ .loginIp }}. Log in to view details."
|
||||
CronJobSuccessAlert: "Panel {{ .node }}{{ .ip }}: scheduled task {{ .name }} completed successfully. Log in to view details."
|
||||
|
||||
# disk
|
||||
DeviceNotFound: "Device {{ .name }} not found"
|
||||
@@ -667,6 +662,7 @@ XfsNotFound: 'xfs not found; install xfsprogs first'
|
||||
# terminal
|
||||
TerminalAIBlockedRiskyCommand: 'blocked risky command: {{ .command }}'
|
||||
TerminalAIThinking: 'AI is thinking...'
|
||||
TerminalOutputTruncated: '[output truncated, showing recent output only]'
|
||||
TerminalAIReadyToExecute: 'Thinking complete, press Enter to execute (duration: {{ .duration }}, tokens: {{ .tokens }})'
|
||||
TerminalAIRequestFailed: 'AI request failed: {{ .err }}'
|
||||
|
||||
@@ -683,3 +679,88 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
|
||||
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
||||
AIProviderArk: 'Volcengine Ark'
|
||||
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||
|
||||
# Firewall
|
||||
ErrDockerIptablesChainUnavailable: 'The Docker DOCKER-USER chain was not found. Restart Docker and try again'
|
||||
ErrDockerNftablesChainUnavailable: 'The Docker nftables IPv4 firewall chain was not found. Verify that the current Docker version supports the nftables firewall backend, restart Docker, and try again'
|
||||
ErrDockerForwardPolicyDrop: 'This server restricts network forwarding. Container port protection cannot be enabled at this time.'
|
||||
Firewall: 'Firewall'
|
||||
FirewallTaskHost: 'Host firewall'
|
||||
FirewallTaskForwarding: 'Port forwarding'
|
||||
FirewallTaskDocker: 'Container port protection'
|
||||
FirewallRuleTaskCreate: 'Create firewall rules [{{ .name }}]'
|
||||
FirewallRuleTaskUpdate: 'Update firewall rules [{{ .name }}]'
|
||||
FirewallRuleTaskDelete: 'Delete firewall rules [{{ .name }}]'
|
||||
FirewallRuleTaskSync: 'Synchronize firewall rules [{{ .name }}]'
|
||||
FirewallTaskInitialize: 'Initialize firewall [{{ .name }}]'
|
||||
FirewallCreateRulesStep: 'Create firewall rules'
|
||||
FirewallCreateRulesResult: 'Creation result: {{ .succeeded }} succeeded, {{ .failed }} failed, {{ .skipped }} not executed'
|
||||
FirewallRuleOperationResult: 'Operation result: {{ .succeeded }} succeeded, {{ .failed }} failed'
|
||||
FirewallCreateRuleSkipped: 'Not executed'
|
||||
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; converted to {{ .count }} rules'
|
||||
FirewallCreateRuleExecutionFailed: 'Rule creation failed; executed commands were not rolled back'
|
||||
FirewallInitializeChainsStep: 'Initialize and bind {{ .name }} base chains'
|
||||
FirewallSyncWhitelistStep: 'Synchronize firewall port whitelist'
|
||||
FirewallEnableForwardingStep: 'Enable and bind port forwarding chains'
|
||||
FirewallRestoreForwardingRulesStep: 'Restore database port forwarding rules'
|
||||
FirewallInspectDockerGuardStep: 'Inspect Docker firewall backend and policies'
|
||||
FirewallInitializeDockerGuardStep: 'Initialize and bind {{ .name }} port guard chains'
|
||||
FirewallPersistDockerGuardStep: 'Persist Docker port guard status'
|
||||
FileTaskCopy: 'Copy files to {{ .dst }}'
|
||||
FileTaskMove: 'Move files to {{ .dst }}'
|
||||
FileTaskCompress: 'Compress files to {{ .dst }}'
|
||||
FileTaskDecompress: 'Extract files to {{ .dst }}'
|
||||
FileTaskSource: 'Source path: {{ .path }}'
|
||||
FileTaskFormat: 'Archive format: {{ .format }}'
|
||||
FileTaskRename: 'Target filename: {{ .name }}'
|
||||
ErrFirewallBackendCleanupRequired: "The current backend {{ .current }} still contains 1Panel rules. Clean it up before switching to {{ .target }}."
|
||||
ErrDockerIPv4ForwardingDisabled: "IPv4 forwarding is disabled. Set net.ipv4.ip_forward=1 before using Docker's firewall backend."
|
||||
ErrFirewallRuleSavedApplyFailed: "The new rule configuration was saved, but applying it to the firewall failed. Retry by synchronizing: {{ .detail }}"
|
||||
ErrFirewallRuleConflict: "A UFW rule with the same match conditions and a different action already exists. Edit the existing rule instead."
|
||||
ErrForwardInterfaceNotFound: "Network interface {{ .name }} specified by the forwarding rule does not exist. Check the interface name and retry."
|
||||
|
||||
ErrSSHPublicKey: "Enter a valid SSH public key"
|
||||
ErrSSHPrivateKey: "Invalid SSH private key or incorrect passphrase; encrypted keys require the original passphrase"
|
||||
ErrSSHKeyMismatch: "The SSH public and private keys do not match"
|
||||
|
||||
# virtual machine
|
||||
ErrVMNotFound: "Virtual machine does not exist"
|
||||
ErrVMAlreadyExists: "Virtual machine already exists"
|
||||
ErrVMCountLimit: "The Professional edition supports up to {{ .limit }} virtual machines. Delete an existing VM or upgrade to Enterprise to create more."
|
||||
ErrVMImageAlreadyExists: "Virtual machine image already exists: {{ .detail }}"
|
||||
ErrVMStoragePathInUse: "Storage pool path is already used by [{{ .name }}]"
|
||||
ErrVMHelperNotFound: "1panel-kvm not found"
|
||||
ErrVMBridgeManaged: "Bridge name is already managed by libvirt or Docker"
|
||||
ErrVMNetworkInUse: "Network is in use and cannot be deleted"
|
||||
ErrVMStorageInUse: "Storage pool is in use and cannot be deleted"
|
||||
ErrVMStorageNameCannotChange: "Storage pool name cannot be changed"
|
||||
ErrVMStoragePathCannotChange: "Storage pool path cannot be changed"
|
||||
ErrVMRunning: "Virtual machine is running"
|
||||
ErrVMLibvirtConnect: "Failed to connect to libvirt: {{ .detail }}"
|
||||
ErrVMPermission: "No permission to operate virtual machine: {{ .detail }}"
|
||||
ErrVMNetworkNotFound: "Virtual machine network does not exist: {{ .detail }}"
|
||||
ErrVMInvalidPath: "Invalid virtual machine path: {{ .detail }}"
|
||||
ErrVMInvalidInput: "Invalid virtual machine parameter: {{ .detail }}"
|
||||
ErrVMCommandFailed: "Virtual machine command failed: {{ .err }}"
|
||||
ErrVMDiskResizeBlocked: "The disk does not meet expansion requirements or is referenced by another device. Check the disk status."
|
||||
ErrVMDiskReferenced: "This virtual disk is used by another virtual machine or device and cannot be expanded yet."
|
||||
ErrVMDiskReferenceCheck: "Cannot complete virtual disk reference checks. Expansion is unavailable; check the disks of other virtual machines."
|
||||
ErrVMSnapshotMetadataSync: "The snapshot was restored, but disk information could not be synced. Refresh and check; do not restore the snapshot again."
|
||||
VMCreateTask: "Create virtual machine [{{ .name }}]"
|
||||
VMCreateFromTemplateTask: "Create virtual machine [{{ .name }}] from template"
|
||||
VMUpdateTask: "Update virtual machine [{{ .name }}]"
|
||||
VMDiskResizeTask: "Expand disk [{{ .disk }}] of virtual machine [{{ .name }}]"
|
||||
VMRenameTask: "Rename virtual machine [{{ .name }}] to [{{ .newName }}]"
|
||||
VMDeleteTask: "Delete virtual machine [{{ .name }}]"
|
||||
VMTemplateCreateTask: "Save virtual machine [{{ .name }}] as template [{{ .template }}]"
|
||||
VMTemplateDeleteTask: "Delete virtual machine template [{{ .name }}]"
|
||||
VMForceDeleteMetadataOnly: "1panel-kvm not found, force deleting virtual machine [{{ .name }}] metadata only"
|
||||
VMForceDeleteMetadataAfterLibvirtFailed: "Failed to delete virtual machine [{{ .name }}] from libvirt, force deleting metadata: {{ .err }}"
|
||||
VMForceDeleteDiskFailed: "Force delete virtual machine disk file [{{ .path }}] failed: {{ .err }}"
|
||||
VMForceDeleteDiskSuccess: "Force deleted virtual machine disk file [{{ .path }}]"
|
||||
VMSnapshotCreateTask: "Create snapshot [{{ .snapshot }}] for virtual machine [{{ .vm }}]"
|
||||
VMSnapshotCreateStep: "Create virtual machine snapshot [{{ .snapshot }}]"
|
||||
VMSnapshotRecoverTask: "Recover snapshot [{{ .snapshot }}] for virtual machine [{{ .vm }}]"
|
||||
VMSnapshotRecoverStep: "Recover virtual machine snapshot [{{ .snapshot }}]"
|
||||
VMSnapshotDeleteTask: "Delete snapshot [{{ .snapshot }}] for virtual machine [{{ .vm }}]"
|
||||
VMSnapshotDeleteStep: "Delete virtual machine snapshot [{{ .snapshot }}]"
|
||||
|
||||
+91
-10
@@ -139,6 +139,7 @@ ErrComposeProjectNameParse: 'No se pudo analizar el nombre del proyecto desde la
|
||||
ErrComposeProjectNameEmpty: 'No se pudo derivar un nombre de proyecto desde la configuración de Docker Compose ni desde el directorio padre del archivo. Introduzca un nombre e inténtelo de nuevo.'
|
||||
ErrComposeNameInvalid: 'Nombre de proyecto Compose no válido: debe comenzar con una letra minúscula o un número, contener solo letras minúsculas, números, guiones y guiones bajos, y tener entre 1 y 256 caracteres.'
|
||||
ErrAppVersionUnavailable: 'Esta versión de la aplicación se ha eliminado del servicio remoto. Seleccione otra versión e inténtelo de nuevo.'
|
||||
ErrVllmGB10ProOnly: 'Esta versión de vLLM solo está disponible en 1Panel Professional Edition.'
|
||||
ErrAppWarn: 'Estado anómalo, revise el log'
|
||||
ErrAppParamKey: 'El campo de parámetro {{ .name }} es anómalo'
|
||||
ErrAppUpgrade: 'La actualización de la aplicación {{ .name }} falló {{ .err }}'
|
||||
@@ -215,6 +216,9 @@ ErrDomainFormat: 'El formato del dominio {{ .name }} es incorrecto'
|
||||
ErrDefaultAlias: 'default es un código reservado, use otro'
|
||||
ErrParentWebsite: 'Primero debe eliminar el sub-sitio {{ .name }}'
|
||||
ErrBuildDirNotFound: 'El directorio de compilación no existe'
|
||||
ErrBrotliDisabled: 'El módulo brotli no está habilitado, actívelo y compílelo primero'
|
||||
ErrBrotliUnsupported: 'El panel no pudo conectar automáticamente la configuración brotli a nginx.conf; revise el archivo o actualice OpenResty'
|
||||
ErrModuleBuildUnsupported: 'Esta versión de OpenResty no puede compilar módulos, actualícela primero'
|
||||
ErrImageNotExist: 'La imagen del entorno {{ .name }} no existe, edítela de nuevo'
|
||||
ErrProxyIsUsed: 'El balanceo de carga ya está usado por un proxy reverso, no se puede eliminar'
|
||||
ErrSSLValid: 'Archivo de certificado anómalo, revise el estado del certificado'
|
||||
@@ -502,16 +506,6 @@ Container: 'Contenedor'
|
||||
AppLink: 'Aplicación enlazada'
|
||||
EnableSSL: 'Habilitar HTTPS'
|
||||
AppStore: 'Tienda de aplicaciones'
|
||||
Firewall: 'Firewall'
|
||||
FirewallSyncStep: 'Sincronizar reglas con {{ .name }}'
|
||||
FirewallSyncResult: 'Resultado de sincronización: {{ .succeeded }} correctas, {{ .existing }} existentes, {{ .failed }} fallidas'
|
||||
FirewallSyncFailed: 'No se pudieron sincronizar {{ .failed }} reglas del firewall'
|
||||
FirewallSyncFailedResetSkipped: 'No se pudieron sincronizar {{ .failed }} reglas; el firewall de origen no se restableció'
|
||||
FirewallResetSourceStep: 'Restablecer y desactivar el firewall de origen {{ .name }}'
|
||||
FirewallResetSourceResult: 'Firewall de origen restablecido; se eliminaron {{ .removed }} reglas de la base de datos'
|
||||
FirewallVerifyTargetStep: 'Verificar las reglas del firewall de destino'
|
||||
FirewallTargetRuleIneffective: 'La regla de destino ya no es efectiva'
|
||||
FirewallVerifyRuleFailed: 'No se pudo verificar la regla sincronizada {{ .name }}: {{ .detail }}'
|
||||
TaskSync: 'Sincronizar'
|
||||
LocalApp: 'Aplicación local'
|
||||
SubTask: 'Subtarea'
|
||||
@@ -655,6 +649,7 @@ CommonAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .msg }}. Inicie sesión en el pa
|
||||
NodeExceptionAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .num }} nodos son anómalos. Inicie sesión en el panel para ver los detalles.'
|
||||
LicenseExceptionAlert: 'Su Panel {{ .node }}{{ .ip }}, {{ .num }} licencias son anómalas. Inicie sesión en el panel para ver los detalles.'
|
||||
SSHAndPanelLoginAlert: 'Su Panel {{ .node }}{{ .ip }}, el inicio de sesión {{ .name }} desde {{ .loginIp }} es anómalo. Inicie sesión en el panel para ver los detalles.'
|
||||
CronJobSuccessAlert: 'Panel {{ .node }}{{ .ip }}: la tarea programada {{ .name }} se completó correctamente. Inicie sesión para ver los detalles.'
|
||||
|
||||
# disco
|
||||
DeviceNotFound: 'Dispositivo {{ .name }} no encontrado'
|
||||
@@ -667,6 +662,7 @@ XfsNotFound: 'No se detectó el sistema de archivos xfs instale xfsprogs primero
|
||||
# terminal
|
||||
TerminalAIBlockedRiskyCommand: 'Comando de riesgo bloqueado: {{ .command }}'
|
||||
TerminalAIThinking: 'La IA está pensando...'
|
||||
TerminalOutputTruncated: '[salida truncada, se muestra solo la salida reciente]'
|
||||
TerminalAIReadyToExecute: 'Pensamiento completado, pulsa Enter para ejecutar (duración: {{ .duration }}, token: {{ .tokens }})'
|
||||
TerminalAIRequestFailed: 'La solicitud de AI ha fallado: {{ .err }}'
|
||||
|
||||
@@ -683,3 +679,88 @@ AIProviderBailian: 'Alibaba Cloud Model Studio'
|
||||
AIProviderBailianCodingPlan: 'Alibaba Cloud Model Studio Coding Plan'
|
||||
AIProviderArk: 'Volcengine Ark'
|
||||
AIProviderArkCodingPlan: 'Volcengine Ark Coding Plan'
|
||||
|
||||
# Firewall
|
||||
ErrDockerIptablesChainUnavailable: 'No se encontró la cadena DOCKER-USER de Docker. Reinicie Docker y vuelva a intentarlo'
|
||||
ErrDockerNftablesChainUnavailable: 'No se encontró la cadena de firewall IPv4 de nftables de Docker. Compruebe que la versión actual de Docker admita el backend de firewall nftables, reinicie Docker y vuelva a intentarlo'
|
||||
ErrDockerForwardPolicyDrop: 'Este servidor restringe el reenvío de red. Por ahora no se puede habilitar la protección de puertos de contenedores.'
|
||||
Firewall: 'Firewall'
|
||||
FirewallTaskHost: 'Cortafuegos del host'
|
||||
FirewallTaskForwarding: 'Reenvío de puertos'
|
||||
FirewallTaskDocker: 'Protección de puertos de contenedores'
|
||||
FirewallRuleTaskCreate: 'Crear reglas de cortafuegos [{{ .name }}]'
|
||||
FirewallRuleTaskUpdate: 'Actualizar reglas de cortafuegos [{{ .name }}]'
|
||||
FirewallRuleTaskDelete: 'Eliminar reglas de cortafuegos [{{ .name }}]'
|
||||
FirewallRuleTaskSync: 'Sincronizar reglas de cortafuegos [{{ .name }}]'
|
||||
FirewallTaskInitialize: 'Inicializar cortafuegos [{{ .name }}]'
|
||||
FirewallCreateRulesStep: 'Crear reglas de cortafuegos'
|
||||
FirewallCreateRulesResult: 'Resultado de creación: {{ .succeeded }} correctas, {{ .failed }} fallidas, {{ .skipped }} sin ejecutar'
|
||||
FirewallRuleOperationResult: 'Resultado de la operación: {{ .succeeded }} correctas, {{ .failed }} fallidas'
|
||||
FirewallCreateRuleSkipped: 'Sin ejecutar'
|
||||
FirewallImportRuleConversion: '[{{ .index }}/{{ .total }}] {{ .source }} → {{ .target }}: {{ .rule }}; convertido en {{ .count }} reglas'
|
||||
FirewallCreateRuleExecutionFailed: 'Error al crear la regla. Los comandos ejecutados no se revirtieron'
|
||||
FirewallInitializeChainsStep: 'Inicializar y vincular las cadenas base de {{ .name }}'
|
||||
FirewallSyncWhitelistStep: 'Sincronizar la lista de puertos permitidos del firewall'
|
||||
FirewallEnableForwardingStep: 'Habilitar y vincular las cadenas de reenvío de puertos'
|
||||
FirewallRestoreForwardingRulesStep: 'Restaurar las reglas de reenvío de puertos de la base de datos'
|
||||
FirewallInspectDockerGuardStep: 'Inspeccionar el backend del firewall de Docker y las políticas'
|
||||
FirewallInitializeDockerGuardStep: 'Inicializar y vincular las cadenas de protección de puertos de {{ .name }}'
|
||||
FirewallPersistDockerGuardStep: 'Guardar el estado de protección de puertos de Docker'
|
||||
FileTaskCopy: 'Copiar archivos a {{ .dst }}'
|
||||
FileTaskMove: 'Mover archivos a {{ .dst }}'
|
||||
FileTaskCompress: 'Comprimir archivos en {{ .dst }}'
|
||||
FileTaskDecompress: 'Extraer archivos a {{ .dst }}'
|
||||
FileTaskSource: 'Ruta de origen: {{ .path }}'
|
||||
FileTaskFormat: 'Formato del archivo: {{ .format }}'
|
||||
FileTaskRename: 'Nombre del archivo de destino: {{ .name }}'
|
||||
ErrFirewallBackendCleanupRequired: "El backend actual {{ .current }} aún contiene reglas de 1Panel. Elimínelas antes de cambiar a {{ .target }}."
|
||||
ErrDockerIPv4ForwardingDisabled: "El reenvío IPv4 está desactivado. Configure net.ipv4.ip_forward=1 antes de usar el backend de cortafuegos de Docker."
|
||||
ErrFirewallRuleSavedApplyFailed: "Se guardó la configuración de la nueva regla, pero no se pudo aplicar al cortafuegos. Vuelva a intentarlo mediante la sincronización: {{ .detail }}"
|
||||
ErrFirewallRuleConflict: "Ya existe una regla UFW con las mismas condiciones y una acción diferente. Edite la regla existente."
|
||||
ErrForwardInterfaceNotFound: "La interfaz de red {{ .name }} indicada en la regla de reenvío no existe. Compruebe el nombre e inténtelo de nuevo."
|
||||
|
||||
ErrSSHPublicKey: "Introduce una clave pública SSH válida"
|
||||
ErrSSHPrivateKey: "Clave privada SSH no válida o contraseña incorrecta; las claves cifradas requieren la contraseña original"
|
||||
ErrSSHKeyMismatch: "Las claves pública y privada SSH no coinciden"
|
||||
|
||||
# máquina virtual
|
||||
ErrVMNotFound: "La máquina virtual no existe"
|
||||
ErrVMAlreadyExists: "La máquina virtual ya existe"
|
||||
ErrVMCountLimit: "La edición Professional permite crear hasta {{ .limit }} máquinas virtuales. Elimina una máquina virtual existente o actualiza a Enterprise y vuelve a intentarlo."
|
||||
ErrVMImageAlreadyExists: "La imagen de la máquina virtual ya existe: {{ .detail }}"
|
||||
ErrVMStoragePathInUse: "La ruta del pool de almacenamiento ya está usada por [{{ .name }}]"
|
||||
ErrVMHelperNotFound: "No se encontró 1panel-kvm"
|
||||
ErrVMBridgeManaged: "El nombre del bridge ya está administrado por libvirt o Docker"
|
||||
ErrVMNetworkInUse: "La red está en uso y no se puede eliminar"
|
||||
ErrVMStorageInUse: "El pool de almacenamiento está en uso y no se puede eliminar"
|
||||
ErrVMStorageNameCannotChange: "El nombre del pool de almacenamiento no se puede cambiar"
|
||||
ErrVMStoragePathCannotChange: "La ruta del pool de almacenamiento no se puede cambiar"
|
||||
ErrVMRunning: "La máquina virtual está en ejecución"
|
||||
ErrVMLibvirtConnect: "Error al conectar con libvirt: {{ .detail }}"
|
||||
ErrVMPermission: "Sin permiso para operar la máquina virtual: {{ .detail }}"
|
||||
ErrVMNetworkNotFound: "La red de la máquina virtual no existe: {{ .detail }}"
|
||||
ErrVMInvalidPath: "Ruta de máquina virtual no válida: {{ .detail }}"
|
||||
ErrVMInvalidInput: "Parámetro de máquina virtual no válido: {{ .detail }}"
|
||||
ErrVMCommandFailed: "Error al ejecutar el comando de la máquina virtual: {{ .err }}"
|
||||
ErrVMDiskResizeBlocked: "El disco no cumple los requisitos de ampliación o está referenciado por otro dispositivo. Compruebe su estado."
|
||||
ErrVMDiskReferenced: "Otro equipo virtual o dispositivo utiliza este disco virtual; la ampliación aún no se admite."
|
||||
ErrVMDiskReferenceCheck: "No se pueden comprobar las referencias al disco virtual. La ampliación no está disponible; revise los discos de otras máquinas virtuales."
|
||||
ErrVMSnapshotMetadataSync: "La instantánea se restauró, pero falló la sincronización de los datos del disco. Actualice y compruebe; no restaure de nuevo la instantánea."
|
||||
VMCreateTask: "Crear máquina virtual [{{ .name }}]"
|
||||
VMCreateFromTemplateTask: "Crear máquina virtual [{{ .name }}] desde plantilla"
|
||||
VMUpdateTask: "Actualizar máquina virtual [{{ .name }}]"
|
||||
VMDiskResizeTask: "Ampliar disco [{{ .disk }}] de la máquina virtual [{{ .name }}]"
|
||||
VMRenameTask: "Renombrar máquina virtual [{{ .name }}] a [{{ .newName }}]"
|
||||
VMDeleteTask: "Eliminar máquina virtual [{{ .name }}]"
|
||||
VMTemplateCreateTask: "Guardar máquina virtual [{{ .name }}] como plantilla [{{ .template }}]"
|
||||
VMTemplateDeleteTask: "Eliminar plantilla de máquina virtual [{{ .name }}]"
|
||||
VMForceDeleteMetadataOnly: "1panel-kvm no encontrado, eliminando solo los metadatos de la máquina virtual [{{ .name }}]"
|
||||
VMForceDeleteMetadataAfterLibvirtFailed: "Error al eliminar la máquina virtual [{{ .name }}] de libvirt, eliminando metadatos: {{ .err }}"
|
||||
VMForceDeleteDiskFailed: "Error al forzar la eliminación del archivo de disco de la máquina virtual [{{ .path }}]: {{ .err }}"
|
||||
VMForceDeleteDiskSuccess: "Archivo de disco de la máquina virtual [{{ .path }}] eliminado forzosamente"
|
||||
VMSnapshotCreateTask: "Crear instantánea [{{ .snapshot }}] para la máquina virtual [{{ .vm }}]"
|
||||
VMSnapshotCreateStep: "Crear instantánea de máquina virtual [{{ .snapshot }}]"
|
||||
VMSnapshotRecoverTask: "Restaurar instantánea [{{ .snapshot }}] para la máquina virtual [{{ .vm }}]"
|
||||
VMSnapshotRecoverStep: "Restaurar instantánea de máquina virtual [{{ .snapshot }}]"
|
||||
VMSnapshotDeleteTask: "Eliminar instantánea [{{ .snapshot }}] de la máquina virtual [{{ .vm }}]"
|
||||
VMSnapshotDeleteStep: "Eliminar instantánea de máquina virtual [{{ .snapshot }}]"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user