Compare commits

..
Author SHA1 Message Date
wanghe-fit2cloud c3c7fe320d feat: support enterprise demo mode 2026-08-19 17:31:41 +08:00
528 changed files with 15915 additions and 62290 deletions
+2 -2
View File
@@ -124,7 +124,7 @@ func (b *BaseApi) PageAgents(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
total, list, err := agentService.Page(req) total, list, err := agentService.Page(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.BadRequest(c, err) helper.BadRequest(c, err)
return return
@@ -447,7 +447,7 @@ func (b *BaseApi) PageAgentAccounts(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
total, list, err := agentService.PageAccounts(req) total, list, err := agentService.PageAccounts(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.BadRequest(c, err) helper.BadRequest(c, err)
return return
+1 -41
View File
@@ -2,14 +2,11 @@ package v2
import ( import (
"errors" "errors"
"net/http"
"net/url" "net/url"
"strings" "strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "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/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -271,7 +268,7 @@ func (b *BaseApi) PageAlertConfig(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
total, configs, err := alertService.PageAlertConfig(req) total, configs, err := alertService.PageAlertConfig(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -297,34 +294,6 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
return return
} }
if err := alertService.UpdateAlertConfig(req, loadAuditUser(c)); err != nil { 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) helper.InternalServer(c, err)
return return
} }
@@ -377,15 +346,6 @@ func (b *BaseApi) TestAlertConfig(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return 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) flag, err := alertService.TestAlertConfig(req)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
+1 -1
View File
@@ -120,7 +120,7 @@ func (b *BaseApi) GetAppDetail(c *gin.Context) {
} }
version := c.Param("version") version := c.Param("version")
appType := c.Param("type") appType := c.Param("type")
appDetailDTO, err := appService.GetAppDetail(appID, version, appType) appDetailDTO, err := appService.GetAppDetail(appID, version, appType, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+1 -1
View File
@@ -14,7 +14,7 @@ import (
// @Security Timestamp // @Security Timestamp
// @Router /apps/ignored/detail [get] // @Router /apps/ignored/detail [get]
func (b *BaseApi) ListAppIgnored(c *gin.Context) { func (b *BaseApi) ListAppIgnored(c *gin.Context) {
res, err := appIgnoreUpgradeService.List() res, err := appIgnoreUpgradeService.List(helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+6 -4
View File
@@ -21,6 +21,7 @@ func (b *BaseApi) SearchAppInstalled(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
req.ReadOnly = helper.IsDemoRequest(c)
if req.All { if req.All {
list, err := appInstallService.SearchForWebsite(req) list, err := appInstallService.SearchForWebsite(req)
if err != nil { if err != nil {
@@ -73,6 +74,7 @@ func (b *BaseApi) CheckAppInstalled(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
req.ReadOnly = helper.IsDemoRequest(c)
checkData, err := appInstallService.CheckExist(req) checkData, err := appInstallService.CheckExist(req)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
@@ -115,7 +117,7 @@ func (b *BaseApi) LoadConnInfo(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
conn, err := appInstallService.LoadConnInfo(req) conn, err := appInstallService.LoadConnInfo(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -137,7 +139,7 @@ func (b *BaseApi) DeleteCheck(c *gin.Context) {
helper.BadRequest(c, err) helper.BadRequest(c, err)
return return
} }
checkData, err := appInstallService.DeleteCheck(appInstallId) checkData, err := appInstallService.DeleteCheck(appInstallId, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -277,7 +279,7 @@ func (b *BaseApi) GetParams(c *gin.Context) {
helper.BadRequest(c, err) helper.BadRequest(c, err)
return return
} }
content, err := appInstallService.GetParams(appInstallId) content, err := appInstallService.GetParams(appInstallId, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -341,7 +343,7 @@ func (b *BaseApi) GetAppInstallInfo(c *gin.Context) {
helper.BadRequest(c, err) helper.BadRequest(c, err)
return return
} }
info, err := appInstallService.GetAppInstallInfo(appInstallId) info, err := appInstallService.GetAppInstallInfo(appInstallId, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+1 -1
View File
@@ -209,7 +209,7 @@ func (b *BaseApi) SearchBackup(c *gin.Context) {
return return
} }
total, list, err := backupService.SearchWithPage(req) total, list, err := backupService.SearchWithPage(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+1 -1
View File
@@ -106,7 +106,7 @@ func (b *BaseApi) SearchClam(c *gin.Context) {
// @Security Timestamp // @Security Timestamp
// @Router /toolbox/clam/base [post] // @Router /toolbox/clam/base [post]
func (b *BaseApi) LoadClamBaseInfo(c *gin.Context) { func (b *BaseApi) LoadClamBaseInfo(c *gin.Context) {
info, err := clamService.LoadBaseInfo() info, err := clamService.LoadBaseInfo(helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+2 -2
View File
@@ -65,7 +65,7 @@ func (b *BaseApi) SearchComposeTemplate(c *gin.Context) {
return return
} }
total, list, err := composeTemplateService.SearchWithPage(req) total, list, err := composeTemplateService.SearchWithPage(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -85,7 +85,7 @@ func (b *BaseApi) SearchComposeTemplate(c *gin.Context) {
// @Security Timestamp // @Security Timestamp
// @Router /containers/template [get] // @Router /containers/template [get]
func (b *BaseApi) ListComposeTemplate(c *gin.Context) { func (b *BaseApi) ListComposeTemplate(c *gin.Context) {
list, err := composeTemplateService.List() list, err := composeTemplateService.List(helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+3 -3
View File
@@ -273,7 +273,7 @@ func (b *BaseApi) SearchCompose(c *gin.Context) {
return return
} }
total, list, err := containerService.PageCompose(req) total, list, err := containerService.PageCompose(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -387,7 +387,7 @@ func (b *BaseApi) ContainerInfo(c *gin.Context) {
return return
} }
data, err := containerService.ContainerInfo(req) data, err := containerService.ContainerInfo(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -439,7 +439,7 @@ func (b *BaseApi) ContainerItemStats(c *gin.Context) {
return return
} }
data, err := containerService.ContainerItemStats(c.Request.Context(), req) data, err := containerService.ContainerItemStats(req)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+1 -1
View File
@@ -144,7 +144,7 @@ func (b *BaseApi) SearchCronjob(c *gin.Context) {
return return
} }
total, list, err := cronjobService.SearchWithPage(req) total, list, err := cronjobService.SearchWithPage(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+2 -2
View File
@@ -78,7 +78,7 @@ func (b *BaseApi) SearchDatabase(c *gin.Context) {
return return
} }
total, list, err := databaseService.SearchWithPage(req) total, list, err := databaseService.SearchWithPage(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -147,7 +147,7 @@ func (b *BaseApi) GetDatabase(c *gin.Context) {
helper.BadRequest(c, err) helper.BadRequest(c, err)
return return
} }
data, err := databaseService.Get(name) data, err := databaseService.Get(name, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+1 -1
View File
@@ -54,7 +54,7 @@ func (b *BaseApi) SearchMongodb(c *gin.Context) {
return return
} }
total, list, err := mongodbService.SearchWithPage(req) total, list, err := mongodbService.SearchWithPage(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+1 -1
View File
@@ -53,7 +53,7 @@ func (b *BaseApi) ListMysqlUsers(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
data, err := mysqlService.ListUsers(req) data, err := mysqlService.ListUsers(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+1 -1
View File
@@ -151,7 +151,7 @@ func (b *BaseApi) SearchPostgresql(c *gin.Context) {
return return
} }
total, list, err := postgresqlService.SearchWithPage(req) total, list, err := postgresqlService.SearchWithPage(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+1 -2
View File
@@ -42,9 +42,8 @@ var (
fileShareService = service.NewIFileShareService() fileShareService = service.NewIFileShareService()
sshService = service.NewISSHService() sshService = service.NewISSHService()
firewallService = service.NewIFirewallService() firewallService = service.NewIFirewallService()
firewallSettingService = service.NewIFirewallSettingService()
forwardingService = service.NewIForwardingService() forwardingService = service.NewIForwardingService()
dockerPortGuardService = service.NewIDockerPortGuardService() iptablesService = service.NewIIptablesService()
monitorService = service.NewIMonitorService() monitorService = service.NewIMonitorService()
systemService = service.NewISystemService() systemService = service.NewISystemService()
runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService() runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService()
+8 -33
View File
@@ -148,7 +148,7 @@ func (b *BaseApi) FileAISearch(c *gin.Context) {
if strings.TrimSpace(req.ResponseLanguage) == "" { if strings.TrimSpace(req.ResponseLanguage) == "" {
req.ResponseLanguage = strings.TrimSpace(c.GetHeader("Accept-Language")) req.ResponseLanguage = strings.TrimSpace(c.GetHeader("Accept-Language"))
} }
res, err := fileService.AISearch(req) res, err := fileService.AISearch(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -684,35 +684,10 @@ func (b *BaseApi) StopWget(c *gin.Context) {
return return
} }
if err := files.CancelDownload(req.Key); err != nil { files.CancelDownload(req.Key)
helper.InternalServer(c, err)
return
}
helper.Success(c) 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 // @Tags File
// @Summary Move file // @Summary Move file
// @Accept json // @Accept json
@@ -1658,7 +1633,7 @@ func (b *BaseApi) SearchFileShare(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
total, list, err := fileShareService.Page(req) total, list, err := fileShareService.Page(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -1682,7 +1657,7 @@ func (b *BaseApi) GetFileShareDetail(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
info, err := fileShareService.GetByPath(req.Path) info, err := fileShareService.GetByPath(req.Path, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -1701,7 +1676,7 @@ func (b *BaseApi) GetPublicFileShareInfo(c *gin.Context) {
helper.BadRequest(c, errors.New("code is required")) helper.BadRequest(c, errors.New("code is required"))
return return
} }
info, err := fileShareService.GetPublicByCode(code) info, err := fileShareService.GetPublicByCode(code, helper.IsDemoRequest(c))
if err != nil { if err != nil {
if be, ok := err.(buserr.BusinessError); ok { if be, ok := err.(buserr.BusinessError); ok {
helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be) helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
@@ -1754,7 +1729,7 @@ func (b *BaseApi) GetFileShareQRCode(c *gin.Context) {
helper.BadRequest(c, errors.New("code is required")) helper.BadRequest(c, errors.New("code is required"))
return return
} }
if _, err := fileShareService.GetByCode(code); err != nil { if _, err := fileShareService.GetByCode(code, helper.IsDemoRequest(c)); err != nil {
if be, ok := err.(buserr.BusinessError); ok { if be, ok := err.(buserr.BusinessError); ok {
helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be) helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
return return
@@ -1836,7 +1811,7 @@ func (b *BaseApi) CheckFileShare(c *gin.Context) {
helper.BadRequest(c, errors.New("code is required")) helper.BadRequest(c, errors.New("code is required"))
return return
} }
if err := fileShareService.Check(code, password); err != nil { if err := fileShareService.Check(code, password, helper.IsDemoRequest(c)); err != nil {
if be, ok := err.(buserr.BusinessError); ok { if be, ok := err.(buserr.BusinessError); ok {
helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be) helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
return return
@@ -1861,7 +1836,7 @@ func (b *BaseApi) DownloadFileShare(c *gin.Context) {
helper.BadRequest(c, errors.New("code is required")) helper.BadRequest(c, errors.New("code is required"))
return return
} }
filePath, displayName, err := fileShareService.PrepareDownload(code, password) filePath, displayName, err := fileShareService.PrepareDownload(code, password, helper.IsDemoRequest(c))
if err != nil { if err != nil {
if be, ok := err.(buserr.BusinessError); ok { if be, ok := err.(buserr.BusinessError); ok {
helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be) helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
+252 -614
View File
@@ -1,53 +1,34 @@
package v2 package v2
import ( import (
"errors"
"net/http"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "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/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/firewall/filter"
"github.com/gin-gonic/gin" "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 // @Tags Firewall
// @Summary Load firewall base info // @Summary Load firewall base info
// @Accept json // @Accept json
// @Param request body dto.OperationWithName true "request" // @Param request body dto.OperationWithName true "request"
// @Success 200 {object} dto.FirewallSubsystemStatus // @Success 200 {object} dto.FirewallBaseInfo
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/base [post] // @Router /hosts/firewall/base [post]
func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) { func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
var request dto.OperationWithName var req dto.OperationWithName
if err := helper.CheckBindAndValidate(&request, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
data, err := firewallService.LoadBaseInfo(request.Name) var (
data dto.FirewallBaseInfo
err error
)
if req.Name == "forward" {
data, err = forwardingService.LoadBaseInfo()
} else {
data, err = firewallService.LoadBaseInfo(req.Name)
}
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -56,674 +37,331 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
helper.SuccessWithData(c, data) helper.SuccessWithData(c, data)
} }
// @Tags Firewall
// @Summary Page firewall rules
// @Accept json
// @Param request body dto.RuleSearch true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/search [post]
func (b *BaseApi) SearchFirewallRule(c *gin.Context) {
var req dto.RuleSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
var (
total int64
list interface{}
err error
)
if req.Type == "forward" {
total, list, err = forwardingService.SearchWithPage(dto.ForwardRuleSearch{
PageInfo: req.PageInfo,
Info: req.Info,
Status: req.Status,
Strategy: req.Strategy,
})
} else {
total, list, err = firewallService.SearchWithPage(req)
}
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
// @Tags Firewall // @Tags Firewall
// @Summary Operate firewall // @Summary Operate firewall
// @Accept json // @Accept json
// @Param request body dto.FirewallLifecycleOperation true "request" // @Param request body dto.FirewallOperation true "request"
// @Success 200 {object} dto.FirewallLifecycleOperationResponse // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/operate [post] // @Router /hosts/firewall/operate [post]
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] 防火墙","formatEN":"[operation] firewall"} // @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] 防火墙","formatEN":"[operation] firewall"}
func (b *BaseApi) OperateFirewall(c *gin.Context) { func (b *BaseApi) OperateFirewall(c *gin.Context) {
var request dto.FirewallLifecycleOperation var req dto.FirewallOperation
if err := helper.CheckBindAndValidate(&request, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
result, err := firewallService.QueueFirewallOperation(request) if err := firewallService.OperateFirewall(req); err != nil {
if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.SuccessWithData(c, result) helper.Success(c)
} }
// @Tags Firewall // @Tags Firewall
// @Summary Load forwarding base info // @Summary Create group
// @Accept json // @Accept json
// @Success 200 {object} dto.FirewallSubsystemStatus // @Param request body dto.PortRuleOperate true "request"
// @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/forward/base [post] // @Router /hosts/firewall/port [post]
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) { // @x-panel-log {"bodyKeys":["port","strategy"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加端口规则 [strategy] [port]","formatEN":"create port rules [strategy][port]"}
data, err := forwardingService.LoadBaseInfo() func (b *BaseApi) OperatePortRule(c *gin.Context) {
if err != nil { var req dto.PortRuleOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := firewallService.OperatePortRule(req, true); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.SuccessWithData(c, data) helper.Success(c)
}
// OperateForwardRule
// @Tags Firewall
// @Summary Operate forward rule
// @Accept json
// @Param request body dto.ForwardRuleOperate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/forward [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新端口转发规则","formatEN":"update port forward rules"}
func (b *BaseApi) OperateForwardRule(c *gin.Context) {
var req dto.ForwardRuleOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := forwardingService.Operate(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
} }
// @Tags Firewall // @Tags Firewall
// @Summary Page forwarding rules // @Summary Operate Ip rule
// @Accept json // @Accept json
// @Param request body dto.ForwardRuleSearch true "request" // @Param request body dto.AddrRuleOperate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/ip [post]
// @x-panel-log {"bodyKeys":["strategy","address"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加 ip 规则 [strategy] [address]","formatEN":"create address rules [strategy][address]"}
func (b *BaseApi) OperateIPRule(c *gin.Context) {
var req dto.AddrRuleOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := firewallService.OperateAddressRule(req, true); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Batch operate rule
// @Accept json
// @Param request body dto.BatchRuleOperate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/batch [post]
func (b *BaseApi) BatchOperateRule(c *gin.Context) {
var req dto.BatchRuleOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := firewallService.BatchOperateRule(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Update rule description
// @Accept json
// @Param request body dto.UpdateFirewallDescription true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/update/description [post]
func (b *BaseApi) UpdateFirewallDescription(c *gin.Context) {
var req dto.UpdateFirewallDescription
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := firewallService.UpdateDescription(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Update port rule
// @Accept json
// @Param request body dto.PortRuleUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/update/port [post]
func (b *BaseApi) UpdatePortRule(c *gin.Context) {
var req dto.PortRuleUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := firewallService.UpdatePortRule(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Update Ip rule
// @Accept json
// @Param request body dto.AddrRuleUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/update/addr [post]
func (b *BaseApi) UpdateAddrRule(c *gin.Context) {
var req dto.AddrRuleUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := firewallService.UpdateAddrRule(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary search iptables filter rules
// @Accept json
// @Param request body dto.SearchPageWithType true "request"
// @Success 200 {object} dto.PageResult // @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/forward/search [post] // @Router /hosts/firewall/filter/rule/search [post]
func (b *BaseApi) SearchForwardingRules(c *gin.Context) { func (b *BaseApi) SearchFilterRules(c *gin.Context) {
var request dto.ForwardRuleSearch var req dto.SearchPageWithType
if err := helper.CheckBindAndValidate(&request, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
total, items, err := forwardingService.SearchRules(request)
total, list, err := iptablesService.Search(req)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.SuccessWithData(c, dto.PageResult{Items: items, Total: total}) helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
} }
// @Tags Firewall // @Tags Firewall
// @Summary Operate forwarding rules // @Summary Operate iptables filter rule
// @Accept json // @Accept json
// @Param request body dto.ForwardRuleOperate true "request" // @Param request body dto.IptablesRuleOp true "request"
// @Success 200 {object} dto.FilterChainOperationResponse // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/forward/operate [post] // @Router /hosts/firewall/filter/rule/operate [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新端口转发规则","formatEN":"update port forward rules"} // @x-panel-log {"bodyKeys":["operation","chain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] filter规则到 [chain]","formatEN":"[operation] filter rule to [chain]"}
func (b *BaseApi) OperateForwardingRules(c *gin.Context) { func (b *BaseApi) OperateFilterRule(c *gin.Context) {
var request dto.ForwardRuleOperate var req dto.IptablesRuleOp
if err := helper.CheckBindAndValidate(&request, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if err := iptablesService.OperateRule(req, true); err != nil {
result, err := forwardingService.OperateRules(request)
if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.SuccessWithData(c, result)
helper.Success(c)
} }
// @Tags Firewall // @Tags Firewall
// @Summary Enable forwarding // @Summary Batch operate iptables filter rules
// @Accept json // @Accept json
// @Param request body dto.FirewallInitializationTask true "request" // @Param request body dto.IptablesBatchOperate true "request"
// @Success 200 {object} dto.FilterChainOperationResponse // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/forward/enable [post] // @Router /hosts/firewall/filter/rule/batch [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"初始化并启用端口转发","formatEN":"initialize and enable port forwarding"} func (b *BaseApi) BatchOperateFilterRule(c *gin.Context) {
func (b *BaseApi) EnableForwarding(c *gin.Context) { var req dto.IptablesBatchOperate
var request dto.FirewallInitializationTask if err := helper.CheckBindAndValidate(&req, c); err != nil {
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return return
} }
result, err := forwardingService.QueueInitialization(request)
if err != nil { if err := iptablesService.BatchOperate(req); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.SuccessWithData(c, result)
helper.Success(c)
} }
// @Tags Firewall // @Tags Firewall
// @Summary Apply/Unload/Init firewall filter chain // @Summary Apply/Unload/Init iptables filter
// @Accept json // @Accept json
// @Param request body dto.FilterChainOperation true "request" // @Param request body dto.IptablesOp true "request"
// @Success 200 {object} dto.FilterChainOperationResponse // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/filter/operate [post] // @Router /hosts/firewall/filter/operate [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 防火墙过滤链","formatEN":"[operate] firewall filter chain"} // @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] iptables filter 防火墙","formatEN":"[operate] iptables filter firewall"}
func (b *BaseApi) OperateFilterChain(c *gin.Context) { func (b *BaseApi) OperateFilterChain(c *gin.Context) {
var request dto.FilterChainOperation var req dto.IptablesOp
if err := helper.CheckBindAndValidate(&request, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if request.Operate == "init-base" { var err error
result, err := firewallService.QueueFilterChainInitialization(request) if req.Operate == "init-forward" {
if err != nil { err = forwardingService.Enable()
helper.InternalServer(c, err) } else {
return err = iptablesService.Operate(req)
}
helper.SuccessWithData(c, result)
return
} }
if err := firewallService.OperateFilterChain(request); err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, dto.FilterChainOperationResponse{})
}
// @Tags Firewall
// @Summary List unified firewall v2 rules
// @Accept json
// @Param request body dto.FirewallRuleInventory true "request"
// @Success 200 {object} dto.FirewallRuleInventoryResponse
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/search [post]
func (b *BaseApi) SearchFirewallRules(c *gin.Context) {
var request dto.FirewallRuleInventory
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
inventory, err := firewallService.Inventory(c.Request.Context(), request)
if err != nil {
handleFirewallRuleError(c, err)
return
}
helper.SuccessWithData(c, inventory)
}
// @Tags Firewall
// @Summary Reset firewall rules
// @Accept json
// @Param request body dto.FirewallRuleReset true "request"
// @Success 200 {object} dto.FirewallRuleResetResponse
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/reset [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"重置防火墙规则","formatEN":"reset firewall rules"}
func (b *BaseApi) ResetFirewallRules(c *gin.Context) {
var request dto.FirewallRuleReset
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallService.Reset(c.Request.Context(), request)
if err != nil {
handleFirewallRuleError(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Load one provider-native firewall object definition
// @Accept json
// @Param request body dto.FirewallNativeDetail true "request"
// @Success 200 {string} string
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/native/detail [post]
func (b *BaseApi) LoadFirewallNativeDetail(c *gin.Context) {
var request dto.FirewallNativeDetail
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
info, err := firewallService.LoadFirewallNativeDetail(c.Request.Context(), request)
if err != nil {
handleFirewallRuleError(c, err)
return
}
helper.SuccessWithData(c, info)
}
// @Tags Firewall
// @Summary Adopt an external firewall rule
// @Accept json
// @Param request body dto.FirewallRuleAdopt true "request"
// @Success 200
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/adopt [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"纳管防火墙规则","formatEN":"adopt firewall rule"}
func (b *BaseApi) AdoptFirewallRule(c *gin.Context) {
var request dto.FirewallRuleAdopt
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if err := firewallService.Adopt(c.Request.Context(), request); err != nil {
handleFirewallRuleError(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @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
// @Failure 400 {object} dto.Response
// @Failure 409 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加防火墙规则","formatEN":"create firewall rules"}
func (b *BaseApi) CreateFirewallRules(c *gin.Context) {
var request dto.FirewallRuleCreate
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallService.Create(c.Request.Context(), request)
if err != nil {
handleFirewallRuleError(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @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 { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return 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 Queue firewall rule deletion
// @Description Deletes managed rules by UUID or unprotected before-chain rules by 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
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/delete [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除防火墙规则","formatEN":"delete firewall rules"}
func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
var request dto.FirewallRuleDelete
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallService.Delete(c.Request.Context(), request)
if err != nil {
handleFirewallRuleError(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Update a managed unified firewall v2 rule
// @Accept json
// @Param request body dto.FirewallRuleUpdate true "request"
// @Success 200
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/update [post]
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [uuid]","formatEN":"update firewall rule [uuid]"}
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 {
handleFirewallRuleError(c, err)
return
}
helper.Success(c) helper.Success(c)
} }
// @Tags Firewall // @Tags Firewall
// @Summary Reorder a managed unified firewall v2 rule // @Summary load chain status with name
// @Accept json // @Accept json
// @Param request body dto.FirewallRuleReorder true "request" // @Param request body dto.OperationWithName true "request"
// @Success 200
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/reorder [post]
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [uuid]","formatEN":"reorder firewall rule [uuid]"}
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 {
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) {
switch {
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.ErrManagedScopeChange):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrFirewallRuleScopeChange", 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, filter.ErrRuleConflict),
errors.Is(err, repo.ErrFirewallPersistenceInvalid):
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)
default:
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err)
}
}
// @Tags Firewall
// @Summary Load firewall settings
// @Success 200 {object} dto.FirewallSettings
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings [get]
func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
data, err := firewallSettingService.Load(c.Request.Context())
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Firewall
// @Summary Create firewall port whitelist rules
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
// @Accept json
// @Param request body dto.FirewallPortWhitelistCreate true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/settings/whitelist [post] // @Router /hosts/firewall/filter/chain/status [post]
// @x-panel-log {"bodyKeys":["rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建防火墙端口白名单","formatEN":"create firewall port whitelist"} func (b *BaseApi) LoadChainStatus(c *gin.Context) {
func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) { var req dto.OperationWithName
var request dto.FirewallPortWhitelistCreate if err := helper.CheckBindAndValidate(&req, c); err != nil {
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return return
} }
if err := firewallSettingService.CreatePortWhitelist(c.Request.Context(), request); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall helper.SuccessWithData(c, iptablesService.LoadChainStatus(req))
// @Summary Update firewall port whitelist rules
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; 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 Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing 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
// @Param request body dto.FirewallBackendOperation true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings/operate [post]
// @x-panel-log {"bodyKeys":["subsystem","backend","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"防火墙子系统 [subsystem] 后端 [operation] [backend]","formatEN":"[operation] firewall [subsystem] backend [backend]"}
func (b *BaseApi) OperateFirewallBackend(c *gin.Context) {
var request dto.FirewallBackendOperation
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if err := firewallSettingService.Operate(c.Request.Context(), request); err != nil {
if errors.Is(err, service.ErrFirewallBackendCleanupRequired) {
helper.ErrorWithBusinessCode(
c,
http.StatusConflict,
"FW_BACKEND_CLEANUP_REQUIRED",
"ErrInvalidParams",
err,
)
return
}
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary List Docker port guard status and policies
// @Success 200 {object} dto.DockerPortGuardList
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/ports [get]
func (b *BaseApi) ListDockerPortGuard(c *gin.Context) {
data, err := dockerPortGuardService.LoadOverview(c.Request.Context())
if err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Firewall
// @Summary List Docker published ports
// @Success 200 {array} dto.DockerPortGuardContainer
// @Security ApiKeyAuth
// @Security Timestamp
// @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.SuccessWithData(c, data)
}
// @Tags Firewall
// @Summary Sync Docker port guard rules
// @Success 200
// @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 {
handleDockerPortGuardError(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Operate Docker port guard
// @Accept json
// @Param request body dto.DockerPortGuardOperation true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/operate [post]
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] Docker 端口防护","formatEN":"[operation] Docker port guard"}
func (b *BaseApi) OperateDockerPortGuard(c *gin.Context) {
var request dto.DockerPortGuardOperation
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
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Delete Docker port guard policies
// @Accept json
// @Param request body dto.DockerPortGuardPolicyBatchDelete true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/policies/delete/batch [post]
// @x-panel-log {"bodyKeys":["uuids"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 Docker 端口防护策略 [uuids]","formatEN":"delete Docker port guard policies [uuids]"}
func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
var request dto.DockerPortGuardPolicyBatchDelete
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := dockerPortGuardService.DeletePolicies(request)
if err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Batch upsert Docker port guard policies
// @Accept json
// @Param request body dto.DockerPortGuardPolicyBatch true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/policies/batch [post]
// @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
}
result, err := dockerPortGuardService.UpsertPolicies(request)
if err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.SuccessWithData(c, result)
}
func handleDockerPortGuardError(c *gin.Context, err error) {
if errors.Is(err, service.ErrDockerIptablesChainUnavailable) {
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE", "ErrDockerIptablesChainUnavailable", err)
return
}
if errors.Is(err, service.ErrDockerNftablesChainUnavailable) {
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_NFTABLES_CHAIN_UNAVAILABLE", "ErrDockerNftablesChainUnavailable", err)
return
}
if errors.Is(err, service.ErrDockerGuardInvalid) {
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID", "ErrInvalidParams", err)
return
}
if errors.Is(err, service.ErrDockerUnavailable) {
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE", "ErrDockerFailed", err)
return
}
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err)
} }
+1 -1
View File
@@ -87,7 +87,7 @@ func (b *BaseApi) SearchFtp(c *gin.Context) {
return return
} }
total, list, err := ftpService.SearchWithPage(req) total, list, err := ftpService.SearchWithPage(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+17 -9
View File
@@ -3,8 +3,9 @@ package v2
import ( import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "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/dto"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator" "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu/common"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -16,20 +17,27 @@ import (
// @Security Timestamp // @Security Timestamp
// @Router /ai/gpu/load [get] // @Router /ai/gpu/load [get]
func (b *BaseApi) LoadGpuInfo(c *gin.Context) { func (b *BaseApi) LoadGpuInfo(c *gin.Context) {
ok, client := accelerator.New() ok, client := gpu.New()
if ok { if ok {
snapshot, err := client.Collect(c.Request.Context()) info, err := client.LoadGpuInfo()
if err != nil { if err != nil {
helper.BadRequest(c, err) helper.BadRequest(c, err)
return return
} }
if warning := snapshot.Warning(); warning != nil { helper.SuccessWithData(c, info)
global.LOG.Warnf("load realtime accelerator data partially failed, err: %v", warning)
}
helper.SuccessWithData(c, &snapshot.Info)
return return
} }
helper.SuccessWithData(c, &accelerator.Info{}) xpuOK, xpuClient := xpu.New()
if xpuOK {
info, err := xpuClient.LoadGpuInfo()
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, info)
return
}
helper.SuccessWithData(c, &common.GpuInfo{})
} }
// @Tags AI // @Tags AI
+4 -10
View File
@@ -30,16 +30,6 @@ func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
ctx.Abort() ctx.Abort()
} }
func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey string, err error) {
res := dto.Response{
Code: code,
ErrorCode: businessCode,
Message: i18n.GetMsgWithDetail(msgKey, err.Error()),
}
ctx.JSON(http.StatusOK, res)
ctx.Abort()
}
func ErrorWithDetailAndData(ctx *gin.Context, code int, msgKey string, err error, data interface{}) { func ErrorWithDetailAndData(ctx *gin.Context, code int, msgKey string, err error, data interface{}) {
res := dto.Response{ res := dto.Response{
Code: code, Code: code,
@@ -58,6 +48,10 @@ func BadRequest(ctx *gin.Context, err error) {
ErrorWithDetail(ctx, http.StatusBadRequest, "ErrInvalidParams", err) ErrorWithDetail(ctx, http.StatusBadRequest, "ErrInvalidParams", err)
} }
func IsDemoRequest(ctx *gin.Context) bool {
return global.CONF.Base.IsDemo || ctx.GetHeader(constant.DemoModeHeader) == strconv.FormatBool(true)
}
func SuccessWithData(ctx *gin.Context, data interface{}) { func SuccessWithData(ctx *gin.Context, data interface{}) {
if data == nil { if data == nil {
data = gin.H{} data = gin.H{}
+1 -1
View File
@@ -100,7 +100,7 @@ func (b *BaseApi) SearchHost(c *gin.Context) {
return return
} }
total, list, err := hostService.SearchWithPage(req) total, list, err := hostService.SearchWithPage(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+1 -1
View File
@@ -21,7 +21,7 @@ func (b *BaseApi) SearchRepo(c *gin.Context) {
return return
} }
total, list, err := imageRepoService.Page(req) total, list, err := imageRepoService.Page(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+1 -1
View File
@@ -19,7 +19,7 @@ func (b *BaseApi) PageMcpServers(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
list := mcpServerService.Page(req) list := mcpServerService.Page(req, helper.IsDemoRequest(c))
helper.SuccessWithData(c, list) helper.SuccessWithData(c, list)
} }
+3 -2
View File
@@ -20,6 +20,7 @@ func (b *BaseApi) SearchRuntimes(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
req.ReadOnly = helper.IsDemoRequest(c)
total, items, err := runtimeService.Page(req) total, items, err := runtimeService.Page(req)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
@@ -132,7 +133,7 @@ func (b *BaseApi) GetRuntime(c *gin.Context) {
helper.BadRequest(c, err) helper.BadRequest(c, err)
return return
} }
res, err := runtimeService.Get(id) res, err := runtimeService.Get(id, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -529,7 +530,7 @@ func (b *BaseApi) GetPHPContainerConfig(c *gin.Context) {
helper.BadRequest(c, err) helper.BadRequest(c, err)
return return
} }
data, err := runtimeService.GetPHPContainerConfig(id) data, err := runtimeService.GetPHPContainerConfig(id, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+1 -1
View File
@@ -191,7 +191,7 @@ func (b *BaseApi) LoadBaseDir(c *gin.Context) {
// @Security Timestamp // @Security Timestamp
// @Router /settings/ssh/conn [get] // @Router /settings/ssh/conn [get]
func (b *BaseApi) LoadLocalConn(c *gin.Context) { func (b *BaseApi) LoadLocalConn(c *gin.Context) {
helper.SuccessWithData(c, settingService.GetLocalConn()) helper.SuccessWithData(c, settingService.GetLocalConn(helper.IsDemoRequest(c)))
} }
// @Tags System Setting // @Tags System Setting
+1 -1
View File
@@ -144,7 +144,7 @@ func (b *BaseApi) SearchRootCert(c *gin.Context) {
return return
} }
total, data, err := sshService.SearchRootCerts(req) total, data, err := sshService.SearchRootCerts(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+1 -1
View File
@@ -19,7 +19,7 @@ func (b *BaseApi) PageTensorRTLLMs(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
list := tensorrtLLMService.Page(req) list := tensorrtLLMService.Page(req, helper.IsDemoRequest(c))
helper.SuccessWithData(c, list) helper.SuccessWithData(c, list)
} }
+32 -144
View File
@@ -1,14 +1,11 @@
package v2 package v2
import ( import (
"crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
@@ -22,35 +19,29 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/pkg/errors" "github.com/pkg/errors"
gossh "golang.org/x/crypto/ssh"
) )
// @Tags Terminal // @Tags Terminal
// @Summary Ws local terminal // @Summary Ws local terminal
// @Param command query string false "command" // @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 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/terminal/local [get] // @Router /hosts/terminal/local [get]
func (b *BaseApi) WsLocalTerminal(c *gin.Context) { func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
b.runSSHSession(c, "local", loadLocalConn, c.DefaultQuery("command", "")) b.runSSHSession(c, loadLocalConn, c.DefaultQuery("command", ""))
} }
// @Tags Terminal // @Tags Terminal
// @Summary Ws host SSH // @Summary Ws host SSH
// @Param id query integer false "id" // @Param id query integer false "id"
// @Param command query string false "command" // @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 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/terminal/ssh [get] // @Router /hosts/terminal/ssh [get]
func (b *BaseApi) WsHostSSH(c *gin.Context) { func (b *BaseApi) WsHostSSH(c *gin.Context) {
b.runSSHSession(c, "ssh", func() (*ssh.SSHClient, error) { b.runSSHSession(c, func() (*ssh.SSHClient, error) {
hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0")) hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0"))
if hostID <= 0 { if hostID <= 0 {
return nil, errors.New("missing host id") return nil, errors.New("missing host id")
@@ -74,33 +65,26 @@ func (b *BaseApi) WsContainerTerminal(c *gin.Context) {
return return
} }
defer wsConn.Close() defer wsConn.Close()
identity, ok := loadTerminalIdentity(c)
if !ok { slave, err := loadContainerTerminalCommand(c)
_ = wshandleError(wsConn, errors.New("missing terminal identity")) if wshandleError(wsConn, err) {
return
}
defer slave.Close()
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
if wshandleError(wsConn, err) {
return return
} }
opts := terminal.SessionOptions{ quitChan := make(chan bool, 3)
Identity: identity, tty.Start(quitChan)
Kind: "container", go slave.Wait(quitChan)
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)
}
}
func containerTerminalTarget(c *gin.Context) string { <-quitChan
query := c.Request.URL.Query()
for _, key := range []string{"cols", "rows", "session", "terminalRevalidate"} { global.LOG.Info("websocket finished")
query.Del(key) closeTerminalConn(wsConn)
}
sum := sha256.Sum256([]byte(query.Encode()))
return hex.EncodeToString(sum[:])
} }
func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) { func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
@@ -114,7 +98,7 @@ func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
return nil, 0, 0, false return nil, 0, 0, false
} }
if global.CONF.Base.IsDemo { if helper.IsDemoRequest(c) {
if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) { if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) {
return nil, 0, 0, false return nil, 0, 0, false
} }
@@ -131,128 +115,32 @@ func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
return wsConn, cols, rows, true return wsConn, cols, rows, true
} }
func (b *BaseApi) runSSHSession(c *gin.Context, kind string, connect func() (*ssh.SSHClient, error), command string) { func (b *BaseApi) runSSHSession(c *gin.Context, connect func() (*ssh.SSHClient, error), command string) {
wsConn, cols, rows, ok := prepareTerminalSession(c) wsConn, cols, rows, ok := prepareTerminalSession(c)
if !ok { if !ok {
return return
} }
defer wsConn.Close() defer wsConn.Close()
identity, ok := loadTerminalIdentity(c)
if !ok {
_ = wshandleError(wsConn, errors.New("missing terminal identity"))
return
}
hostID := 0 client, clientErr := connect()
if kind == "ssh" { if wshandleError(wsConn, errors.WithMessage(clientErr, "failed to set up the connection. Please check the host information")) {
hostID, _ = strconv.Atoi(c.DefaultQuery("id", "0")) return
} }
opts := terminal.SessionOptions{ defer client.Close()
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 sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, command)
// @Summary List the caller's live terminal sessions if wshandleError(wsConn, err) {
// @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 return
} }
helper.SuccessWithData(c, terminal.List(identity)) defer sws.Close()
}
// @Tags Terminal quitChan := make(chan bool, 3)
// @Summary Close a terminal session sws.Start(quitChan)
// @Accept json go sws.Wait(quitChan)
// @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)
}
// @Tags Terminal <-quitChan
// @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)
}
func (b *BaseApi) RevokeTerminalSessions(c *gin.Context) { closeTerminalConn(wsConn)
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)),
}
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) { func closeTerminalConn(wsConn *websocket.Conn) {
+2 -2
View File
@@ -255,7 +255,7 @@ func (b *BaseApi) GetHTTPSConfig(c *gin.Context) {
helper.BadRequest(c, err) helper.BadRequest(c, err)
return return
} }
res, err := websiteService.GetWebsiteHTTPS(id) res, err := websiteService.GetWebsiteHTTPS(id, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -1080,7 +1080,7 @@ func (b *BaseApi) GetWebsiteResource(c *gin.Context) {
helper.BadRequest(c, err) helper.BadRequest(c, err)
return return
} }
res, err := websiteService.GetWebsiteResource(id) res, err := websiteService.GetWebsiteResource(id, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+1 -1
View File
@@ -20,7 +20,7 @@ func (b *BaseApi) PageWebsiteAcmeAccount(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
total, accounts, err := websiteAcmeAccountService.Page(req) total, accounts, err := websiteAcmeAccountService.Page(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+2 -2
View File
@@ -24,7 +24,7 @@ func (b *BaseApi) PageWebsiteCA(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
total, cas, err := websiteCAService.Page(req) total, cas, err := websiteCAService.Page(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -70,7 +70,7 @@ func (b *BaseApi) GetWebsiteCA(c *gin.Context) {
if err != nil { if err != nil {
return return
} }
res, err := websiteCAService.GetCA(id) res, err := websiteCAService.GetCA(id, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+1 -1
View File
@@ -20,7 +20,7 @@ func (b *BaseApi) PageWebsiteDnsAccount(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
total, accounts, err := websiteDnsAccountService.Page(req) total, accounts, err := websiteDnsAccountService.Page(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+3 -3
View File
@@ -28,7 +28,7 @@ func (b *BaseApi) PageWebsiteSSL(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
total, accounts, err := websiteSSLService.Page(req) total, accounts, err := websiteSSLService.Page(req, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -159,7 +159,7 @@ func (b *BaseApi) GetWebsiteSSLByWebsiteId(c *gin.Context) {
helper.BadRequest(c, err) helper.BadRequest(c, err)
return return
} }
websiteSSL, err := websiteSSLService.GetWebsiteSSL(websiteId) websiteSSL, err := websiteSSLService.GetWebsiteSSL(websiteId, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -181,7 +181,7 @@ func (b *BaseApi) GetWebsiteSSLById(c *gin.Context) {
helper.BadRequest(c, err) helper.BadRequest(c, err)
return return
} }
websiteSSL, err := websiteSSLService.GetSSL(id) websiteSSL, err := websiteSSLService.GetSSL(id, helper.IsDemoRequest(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+7 -16
View File
@@ -162,25 +162,16 @@ type AgentWebsiteBindReq struct {
} }
type AgentModelConfigUpdateReq struct { type AgentModelConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"` AgentID uint `json:"agentId" validate:"required"`
AccountID uint `json:"accountId" validate:"required"` AccountID uint `json:"accountId" validate:"required"`
Model string `json:"model" validate:"required"` Model string `json:"model" validate:"required"`
Fallbacks []string `json:"fallbacks"` Fallbacks []string `json:"fallbacks"`
Metadata []AgentModelMetadata `json:"metadata" validate:"dive"`
} }
type AgentModelConfig struct { type AgentModelConfig struct {
AccountID uint `json:"accountId"` AccountID uint `json:"accountId"`
Model string `json:"model"` Model string `json:"model"`
Fallbacks []string `json:"fallbacks"` 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 { type AgentHermesChatSessionItem struct {
+16 -29
View File
@@ -151,25 +151,16 @@ type AlertLog struct {
} }
type AlertDetail struct { type AlertDetail struct {
LicenseId string `json:"licenseId"` LicenseId string `json:"licenseId"`
Type string `json:"type"` Type string `json:"type"`
SubType string `json:"subType"` SubType string `json:"subType"`
Title string `json:"title"` Title string `json:"title"`
Method string `json:"method"` Method string `json:"method"`
LicenseCode string `json:"licenseCode"` LicenseCode string `json:"licenseCode"`
DeviceId string `json:"deviceId"` DeviceId string `json:"deviceId"`
Project string `json:"project"` Project string `json:"project"`
Params []Param `json:"params"` Params []Param `json:"params"`
Phone string `json:"phone"` 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 { type AlertRule struct {
@@ -304,19 +295,15 @@ type OfflineQueryRequest struct {
} }
type AlertConfigUpdate struct { type AlertConfigUpdate struct {
ID uint `json:"id"` ID uint `json:"id"`
Type string `json:"type"` Type string `json:"type"`
Title string `json:"title"` Title string `json:"title"`
Status string `json:"status"` Status string `json:"status"`
Config string `json:"config"` Config string `json:"config"`
DisplayName string `json:"displayName"` DisplayName string `json:"displayName"`
Revision *time.Time `json:"revision"`
} }
type AlertConfigTest struct { type AlertConfigTest struct {
ID uint `json:"id"`
Type string `json:"type"`
Config string `json:"config"`
Host string `json:"host"` Host string `json:"host"`
Port int `json:"port"` Port int `json:"port"`
Sender string `json:"sender"` Sender string `json:"sender"`
-80
View File
@@ -1,80 +0,0 @@
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"`
}
+3 -4
View File
@@ -6,10 +6,9 @@ type PageResult struct {
} }
type Response struct { type Response struct {
Code int `json:"code"` Code int `json:"code"`
ErrorCode string `json:"errorCode,omitempty"` Message string `json:"message"`
Message string `json:"message"` Data interface{} `json:"data"`
Data interface{} `json:"data"`
} }
type Options struct { type Options struct {
+7 -34
View File
@@ -121,7 +121,6 @@ type DashboardCurrent struct {
NetBytesRecv uint64 `json:"netBytesRecv"` NetBytesRecv uint64 `json:"netBytesRecv"`
GPUData []GPUInfo `json:"gpuData"` GPUData []GPUInfo `json:"gpuData"`
NPUData []NPUInfo `json:"npuData"`
XPUData []XPUInfo `json:"xpuData"` XPUData []XPUInfo `json:"xpuData"`
TopCPUItems []Process `json:"topCPUItems"` TopCPUItems []Process `json:"topCPUItems"`
@@ -159,10 +158,7 @@ type DiskInfo struct {
type GPUInfo struct { type GPUInfo struct {
Type string `json:"type"` Type string `json:"type"`
Index uint `json:"index"` Index uint `json:"index"`
NPUIndex uint `json:"npuIndex"`
ChipIndex uint `json:"chipIndex"`
ProductName string `json:"productName"` ProductName string `json:"productName"`
BusID string `json:"busID"`
GPUUtil string `json:"gpuUtil"` GPUUtil string `json:"gpuUtil"`
Temperature string `json:"temperature"` Temperature string `json:"temperature"`
PerformanceState string `json:"performanceState"` PerformanceState string `json:"performanceState"`
@@ -175,27 +171,6 @@ type GPUInfo struct {
FanSpeed string `json:"fanSpeed"` FanSpeed string `json:"fanSpeed"`
} }
type NPUInfo struct {
Type string `json:"type"`
Index uint `json:"index"`
NPUIndex uint `json:"npuIndex"`
ChipIndex uint `json:"chipIndex"`
ProductName string `json:"productName"`
BusID string `json:"busID"`
Health string `json:"health"`
Temperature string `json:"temperature"`
PowerDraw string `json:"powerDraw"`
AICore string `json:"aiCore"`
MemUsed string `json:"memUsed"`
MemTotal string `json:"memTotal"`
MemoryUsed string `json:"memoryUsed"`
MemoryTotal string `json:"memoryTotal"`
HBMUsed string `json:"hbmUsed"`
HBMTotal string `json:"hbmTotal"`
HugepagesUsed string `json:"hugepagesUsed"`
HugepagesTotal string `json:"hugepagesTotal"`
}
type AppLauncher struct { type AppLauncher struct {
Key string `json:"key"` Key string `json:"key"`
Type string `json:"type"` Type string `json:"type"`
@@ -228,13 +203,11 @@ type LauncherOption struct {
} }
type XPUInfo struct { type XPUInfo struct {
DeviceID int `json:"deviceID"` DeviceID int `json:"deviceID"`
DeviceName string `json:"deviceName"` DeviceName string `json:"deviceName"`
PciBdfAddress string `json:"pciBdfAddress"` Memory string `json:"memory"`
Memory string `json:"memory"` Temperature string `json:"temperature"`
Temperature string `json:"temperature"` MemoryUsed string `json:"memoryUsed"`
GPUUtil string `json:"gpuUtil"` Power string `json:"power"`
MemoryUsed string `json:"memoryUsed"` MemoryUtil string `json:"memoryUtil"`
Power string `json:"power"`
MemoryUtil string `json:"memoryUtil"`
} }
+73 -358
View File
@@ -1,385 +1,100 @@
package dto package dto
import ( type FirewallBaseInfo struct {
"github.com/1Panel-dev/1Panel/agent/utils/firewall" Name string `json:"name"`
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" IsExist bool `json:"isExist"`
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync" IsActive bool `json:"isActive"`
) IsInit bool `json:"isInit"`
IsBind bool `json:"isBind"`
type FirewallSubsystemStatus struct { Version string `json:"version"`
Name string `json:"name"` PingStatus string `json:"pingStatus"`
Backend string `json:"backend"`
ConflictBackend string `json:"conflictBackend,omitempty"`
IsExist bool `json:"isExist"`
IsActive bool `json:"isActive"`
IsInit bool `json:"isInit"`
IsBind bool `json:"isBind"`
Version string `json:"version"`
PingStatus string `json:"pingStatus"`
Message string `json:"message,omitempty"`
Reason string `json:"reason,omitempty"`
SyncError string `json:"syncError,omitempty"`
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
} }
type FirewallLifecycleOperation struct { type RuleSearch struct {
PageInfo
Info string `json:"info"`
Status string `json:"status"`
Strategy string `json:"strategy"`
Type string `json:"type" validate:"required"`
}
type FirewallOperation struct {
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"` Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
WithDockerRestart bool `json:"withDockerRestart"` WithDockerRestart bool `json:"withDockerRestart"`
} }
type FirewallLifecycleOperationResponse struct { type PortRuleOperate struct {
TaskID string `json:"taskID,omitempty"` ID uint `json:"id"`
Queued bool `json:"queued"` Operation string `json:"operation" validate:"required,oneof=add remove"`
Chain string `json:"chain"`
Address string `json:"address"`
Port string `json:"port" validate:"required"`
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
Description string `json:"description"`
} }
type FirewallBackendOption struct { type UpdateFirewallDescription struct {
Name string `json:"name"` Type string `json:"type"`
Installed bool `json:"installed"` Chain string `json:"chain"`
Active bool `json:"active"` SrcIP string `json:"srcIP"`
Initialized bool `json:"initialized"` DstIP string `json:"dstIP"`
Bound bool `json:"bound"` SrcPort string `json:"srcPort"`
Supported bool `json:"supported"` DstPort string `json:"dstPort"`
SupportReason string `json:"supportReason,omitempty"` Protocol string `json:"protocol"`
Implementation string `json:"implementation,omitempty"` Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
Message string `json:"message,omitempty"`
IPv4 FirewallBackendFamilyStatus `json:"ipv4"` Description string `json:"description"`
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
} }
type FirewallBackendFamilyStatus struct { type AddrRuleOperate struct {
Available bool `json:"available"` ID uint `json:"id"`
Initialized bool `json:"initialized"` Operation string `json:"operation" validate:"required,oneof=add remove"`
Bound bool `json:"bound"` Address string `json:"address" validate:"required"`
Reason string `json:"reason,omitempty"` Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
Description string `json:"description"`
} }
type FirewallBackendGroup struct { type PortRuleUpdate struct {
Selected string `json:"selected"` OldRule PortRuleOperate `json:"oldRule"`
Current string `json:"current,omitempty"` NewRule PortRuleOperate `json:"newRule"`
Options []FirewallBackendOption `json:"options"`
} }
type FirewallSettings struct { type AddrRuleUpdate struct {
System FirewallBackendGroup `json:"system"` OldRule AddrRuleOperate `json:"oldRule"`
Forwarding FirewallBackendGroup `json:"forwarding"` NewRule AddrRuleOperate `json:"newRule"`
Docker FirewallBackendGroup `json:"docker"`
PingStatus string `json:"pingStatus"`
PortWhitelist []filter.PortWhitelist `json:"portWhiteList"`
PanelPort string `json:"panelPort"`
SSHPort string `json:"sshPort"`
} }
type FirewallPortWhitelistCreate struct { type BatchRuleOperate struct {
Rule filter.PortWhitelist `json:"rule" validate:"required"` Type string `json:"type" validate:"required"`
Rules []PortRuleOperate `json:"rules"`
} }
type FirewallPortWhitelistUpdate struct { type IptablesOp struct {
OldRule filter.PortWhitelist `json:"oldRule" validate:"required"` Name string `json:"name" validate:"required,oneof=1PANEL_INPUT 1PANEL_OUTPUT 1PANEL_BASIC 1PANEL_FORWARD"`
Rule filter.PortWhitelist `json:"rule" validate:"required"` Operate string `json:"operate" validate:"required,oneof=init-base init-forward init-advance bind-base unbind-base bind unbind"`
} }
type FirewallPortWhitelistDelete struct { type IptablesRuleOp struct {
Rule *filter.PortWhitelist `json:"rule" validate:"required"` Operation string `json:"operation" validate:"required,oneof=add remove"`
ID uint `json:"id"`
Chain string `json:"chain" validate:"required,oneof=1PANEL_BASIC 1PANEL_BASIC_BEFORE 1PANEL_INPUT 1PANEL_OUTPUT"`
Protocol string `json:"protocol"`
SrcIP string `json:"srcIP"`
SrcPort uint `json:"srcPort"`
DstIP string `json:"dstIP"`
DstPort uint `json:"dstPort"`
Strategy string `json:"strategy" validate:"required,oneof=accept drop reject"`
Description string `json:"description"`
} }
type FirewallBackendOperation struct { type IptablesBatchOperate struct {
Subsystem string `json:"subsystem" validate:"required,oneof=system forwarding docker"` Rules []IptablesRuleOp `json:"rules"`
Backend string `json:"backend" validate:"required,oneof=firewalld ufw iptables nftables"`
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"`
} }
type FilterChainOperation struct { type IptablesChainStatus struct {
Name string `json:"name" validate:"required,eq=1PANEL_BASIC"` IsBind bool `json:"isBind"`
Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"` DefaultStrategy string `json:"defaultStrategy"`
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
}
type FilterChainOperationResponse struct {
TaskID string `json:"taskID"`
Queued bool `json:"queued"`
}
type FirewallInitializationTask struct {
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
}
type FirewallSystemPort = firewall.SystemPort
type FirewallRuleInventoryResponse struct {
IPv4Range filter.PositionRange `json:"ipv4Range"`
IPv6Range filter.PositionRange `json:"ipv6Range"`
Total int64 `json:"total"`
AllTotal int64 `json:"allTotal"`
ManagedTotal int64 `json:"managedTotal"`
Items []filter.InventoryItem `json:"items"`
Notices []filter.ScopeNotice `json:"notices,omitempty"`
}
type FirewallRuleResetResponse struct {
Removed int `json:"removed"`
Disabled bool `json:"disabled"`
}
type FirewallRuleReset struct {
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
WithDockerRestart bool `json:"withDockerRestart"`
}
type FirewallRuleInventory struct {
Refresh bool `json:"refresh,omitempty"`
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"`
States []filter.InventoryState `json:"states,omitempty" validate:"omitempty,dive,oneof=managed adopted external drifted protected"`
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
}
type FirewallNativeDetail struct {
Provider filter.Provider `json:"provider" validate:"required,oneof=firewalld ufw"`
NativeKind filter.NativeKind `json:"nativeKind" validate:"required,oneof=zone_service ufw_application"`
Name string `json:"name" validate:"required"`
Permanent bool `json:"permanent"`
}
type DockerPortGuardBase struct {
Name string `json:"name"`
Version string `json:"version"`
IsExist bool `json:"isExist"`
Initialized bool `json:"initialized"`
Bound bool `json:"bound"`
IPv4 DockerPortGuardFamilyStatus `json:"ipv4"`
IPv6 DockerPortGuardFamilyStatus `json:"ipv6"`
Backend string `json:"backend"`
Message string `json:"message,omitempty"`
}
type DockerPortGuardFamilyStatus struct {
State string `json:"state"`
Reason string `json:"reason,omitempty"`
Initialized bool `json:"initialized"`
Bound bool `json:"bound"`
Effective bool `json:"effective"`
}
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"`
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"`
NativeAction string `json:"nativeAction,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
Sources []string `json:"sources"`
Effective bool `json:"effective"`
Description string `json:"description,omitempty"`
TrafficPath string `json:"trafficPath"`
ManagementTarget string `json:"managementTarget"`
ManagementReason string `json:"managementReason,omitempty"`
}
type DockerPortGuardPortGroup struct {
Key string `json:"key"`
Label string `json:"label"`
Endpoint DockerPortGuardEndpoint `json:"endpoint"`
Endpoints []DockerPortGuardEndpoint `json:"endpoints"`
}
type DockerPortGuardContainer struct {
Key string `json:"key"`
Name string `json:"name"`
Compose string `json:"compose,omitempty"`
Application string `json:"application,omitempty"`
Endpoints []DockerPortGuardEndpoint `json:"endpoints"`
PortGroups []DockerPortGuardPortGroup `json:"portGroups"`
}
type DockerPortGuardList struct {
Base DockerPortGuardBase `json:"base"`
Containers []DockerPortGuardContainer `json:"containers"`
OrphanPolicies []DockerPortGuardEndpoint `json:"orphanPolicies"`
}
type DockerPortGuardEndpointIdentity struct {
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
HostIP string `json:"hostIP" validate:"required,max=45"`
HostPort uint16 `json:"hostPort" validate:"required,min=1"`
Protocol string `json:"protocol" validate:"required,oneof=tcp udp"`
}
type DockerPortGuardPolicyBatch struct {
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
}
type DockerPortGuardPolicyBatchDelete struct {
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"`
Sources []string `json:"sources" validate:"dive,required,max=64"`
Description string `json:"description" validate:"max=256"`
}
type DockerPortGuardOperation struct {
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
}
type FirewallRuleAdopt struct {
Scope filter.Scope `json:"scope" validate:"required"`
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
}
type FirewallRuleCreateItem struct {
Rule filter.FirewallRule `json:"rule" validate:"required"`
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
SourceID string `json:"sourceID"`
}
type FirewallRuleCreate struct {
Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,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"`
Errors []FirewallRuleCreateFailure `json:"errors,omitempty"`
}
type FirewallRuleCreateFailure struct {
Index int `json:"index"`
Status string `json:"status"`
Rule filter.FirewallRule `json:"rule"`
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:"omitempty,dive,required,max=64"`
BeforeRules []FirewallRuleDeleteTarget `json:"beforeRules,omitempty" validate:"omitempty,dive"`
}
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"`
}
type FirewallRuleUpdate struct {
UUID string `json:"uuid" validate:"required,max=64"`
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"`
TargetPosition *int64 `json:"targetPosition"`
Priority *int `json:"priority"`
}
func (p *FirewallRuleSyncPreview) Add(item FirewallRuleSyncItem) {
p.Items = append(p.Items, item)
switch item.Status {
case firewallsync.StatusReady:
p.Ready++
p.Total++
case firewallsync.StatusExisting:
p.Existing++
p.Total++
case firewallsync.StatusRemove:
p.Removed++
case firewallsync.StatusBlocked:
p.Blocked++
if item.ReasonCode != firewallsync.ReasonReadOnlyRule {
p.Total++
}
}
} }
+3 -7
View File
@@ -2,12 +2,13 @@ package dto
type ForwardRuleSearch struct { type ForwardRuleSearch struct {
PageInfo PageInfo
All bool `json:"all,omitempty"`
Info string `json:"info"` Info string `json:"info"`
Status string `json:"status"` Status string `json:"status"`
Strategy string `json:"strategy"` 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 { type ForwardRule struct {
ID uint `json:"id"` ID uint `json:"id"`
Chain string `json:"chain"` Chain string `json:"chain"`
@@ -24,21 +25,16 @@ type ForwardRule struct {
UsedStatus string `json:"usedStatus"` UsedStatus string `json:"usedStatus"`
Description string `json:"description"` Description string `json:"description"`
IsDesired bool `json:"isDesired"`
IsRuntime bool `json:"isRuntime"`
SyncStatus string `json:"syncStatus"`
} }
type ForwardRuleOperate struct { type ForwardRuleOperate struct {
ForceDelete bool `json:"forceDelete"` ForceDelete bool `json:"forceDelete"`
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"` Rules []ForwardRuleOperation `json:"rules"`
} }
type ForwardRuleOperation struct { type ForwardRuleOperation struct {
Operation string `json:"operation" validate:"required,oneof=add remove"` Operation string `json:"operation" validate:"required,oneof=add remove"`
Num string `json:"num"` Num string `json:"num"`
Family string `json:"family" validate:"omitempty,oneof=ipv4 ipv6"`
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"` Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
Interface string `json:"interface"` Interface string `json:"interface"`
Port string `json:"port" validate:"required"` Port string `json:"port" validate:"required"`
-1
View File
@@ -45,7 +45,6 @@ type MonitorGPUOptions struct {
} }
type GPUChartHide struct { type GPUChartHide struct {
ProductName string `json:"productName"` ProductName string `json:"productName"`
Type string `json:"type"`
Process bool `json:"process"` Process bool `json:"process"`
GPU bool `json:"gpu"` GPU bool `json:"gpu"`
Memory bool `json:"memory"` Memory bool `json:"memory"`
+1 -7
View File
@@ -51,19 +51,13 @@ const (
CACHE NginxKey = "cache" CACHE NginxKey = "cache"
HttpPer NginxKey = "http-per" HttpPer NginxKey = "http-per"
ProxyCache NginxKey = "proxy-cache" 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{ var ScopeKeyMap = map[NginxKey][]string{
Index: {"index"}, Index: {"index"},
LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"}, LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"},
SSL: {"ssl_certificate", "ssl_certificate_key"}, 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", "gzip_types", "gzip_vary", "gzip_proxied"}, HttpPer: {"server_names_hash_bucket_size", "client_header_buffer_size", "client_max_body_size", "keepalive_timeout", "gzip", "gzip_min_length", "gzip_comp_level"},
} }
var StaticFileKeyMap = map[NginxKey]struct { var StaticFileKeyMap = map[NginxKey]struct {
+4 -11
View File
@@ -50,10 +50,6 @@ type AppContainerConfig struct {
Type string `json:"type"` Type string `json:"type"`
SpecifyIP string `json:"specifyIP"` SpecifyIP string `json:"specifyIP"`
RestartPolicy string `json:"restartPolicy" validate:"omitempty,oneof=always unless-stopped no on-failure"` 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 { type AppInstalledSearch struct {
@@ -66,11 +62,13 @@ type AppInstalledSearch struct {
All bool `json:"all"` All bool `json:"all"`
Sync bool `json:"sync"` Sync bool `json:"sync"`
CheckUpdate bool `json:"checkUpdate"` CheckUpdate bool `json:"checkUpdate"`
ReadOnly bool `json:"-"`
} }
type AppInstalledInfo struct { type AppInstalledInfo struct {
Key string `json:"key" validate:"required"` Key string `json:"key" validate:"required"`
Name string `json:"name"` Name string `json:"name"`
ReadOnly bool `json:"-"`
} }
type AppBackupSearch struct { type AppBackupSearch struct {
@@ -96,8 +94,6 @@ type AppInstalledOperate struct {
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
DeleteImage bool `json:"deleteImage"` DeleteImage bool `json:"deleteImage"`
Favorite bool `json:"favorite"` Favorite bool `json:"favorite"`
UseLifecycleScripts bool `json:"-"`
} }
type AppInstallUpgrade struct { type AppInstallUpgrade struct {
@@ -117,14 +113,11 @@ type AppInstallDelete struct {
DeleteDB bool `json:"deleteDB"` DeleteDB bool `json:"deleteDB"`
DeleteImage bool `json:"deleteImage"` DeleteImage bool `json:"deleteImage"`
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
UseLifecycleScripts bool `json:"-"`
} }
type AppInstalledUpdate struct { type AppInstalledUpdate struct {
InstallId uint `json:"installId" validate:"required"` InstallId uint `json:"installId" validate:"required"`
Params map[string]interface{} `json:"params" validate:"required"` Params map[string]interface{} `json:"params" validate:"required"`
TaskID string `json:"-"`
AppContainerConfig AppContainerConfig
} }
-5
View File
@@ -122,7 +122,6 @@ type FileWget struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
IgnoreCertificate bool `json:"ignoreCertificate"` IgnoreCertificate bool `json:"ignoreCertificate"`
UseProxy bool `json:"useProxy"` UseProxy bool `json:"useProxy"`
UseServerFilename bool `json:"useServerFilename"`
} }
type FileMove struct { type FileMove struct {
@@ -159,10 +158,6 @@ type FileProcessReq struct {
Key string `json:"key"` Key string `json:"key"`
} }
type FileProcessRemoveReq struct {
Keys []string `json:"keys" validate:"required,min=1,max=1000"`
}
type FileRoleUpdate struct { type FileRoleUpdate struct {
Path string `json:"path" validate:"required"` Path string `json:"path" validate:"required"`
User string `json:"user" validate:"required"` User string `json:"user" validate:"required"`
+12 -12
View File
@@ -6,9 +6,10 @@ import (
type RuntimeSearch struct { type RuntimeSearch struct {
dto.PageInfo dto.PageInfo
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
Status string `json:"status"` Status string `json:"status"`
ReadOnly bool `json:"-"`
} }
type RuntimeCreate struct { type RuntimeCreate struct {
@@ -66,15 +67,14 @@ type RuntimeDelete struct {
} }
type RuntimeUpdate struct { type RuntimeUpdate struct {
AppDetailID uint `json:"appDetailId"` Name string `json:"name"`
Name string `json:"name"` ID uint `json:"id"`
ID uint `json:"id"` Image string `json:"image"`
Image string `json:"image"` Version string `json:"version"`
Version string `json:"version"` Rebuild bool `json:"rebuild"`
Rebuild bool `json:"rebuild"` Source string `json:"source"`
Source string `json:"source"` CodeDir string `json:"codeDir"`
CodeDir string `json:"codeDir"` Remark string `json:"remark"`
Remark string `json:"remark"`
Params map[string]interface{} `json:"params"` Params map[string]interface{} `json:"params"`
NodeConfig NodeConfig
-11
View File
@@ -17,17 +17,6 @@ type NginxParam struct {
Params []string `json:"params"` 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 { type NginxAuthRes struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
Items []dto.NginxAuth `json:"items"` Items []dto.NginxAuth `json:"items"`
+1 -1
View File
@@ -35,7 +35,7 @@ type SettingUpdate struct {
} }
type AgentSettingUpdate struct { type AgentSettingUpdate struct {
Key string `json:"key" validate:"required,oneof=SystemIP DockerSockPath FileRecycleBin"` Key string `json:"key" validate:"required,oneof=SystemIP DockerSockPath FileRecycleBin FirewallPortWhiteList"`
Value string `json:"value"` Value string `json:"value"`
} }
-11
View File
@@ -1,11 +0,0 @@
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"`
}
+10 -27
View File
@@ -1,12 +1,5 @@
package model package model
import (
"strings"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Alert struct { type Alert struct {
BaseModel BaseModel
@@ -25,11 +18,10 @@ type Alert struct {
type AlertTask struct { type AlertTask struct {
BaseModel BaseModel
Type string `gorm:"type:varchar(64);not null" json:"type"` Type string `gorm:"type:varchar(64);not null" json:"type"`
Quota string `gorm:"type:varchar(64)" json:"quota"` Quota string `gorm:"type:varchar(64)" json:"quota"`
QuotaType string `gorm:"type:varchar(64)" json:"quotaType"` QuotaType string `gorm:"type:varchar(64)" json:"quotaType"`
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"` Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
DeliveryLogID *uint `gorm:"uniqueIndex" json:"-"`
} }
type AlertLog struct { type AlertLog struct {
@@ -49,21 +41,12 @@ type AlertLog struct {
type AlertConfig struct { type AlertConfig struct {
BaseModel BaseModel
UID string `gorm:"type:varchar(64);not null;uniqueIndex" json:"uid"` Type string `gorm:"type:varchar(64);not null" json:"type"`
Type string `gorm:"type:varchar(64);not null" json:"type"` Title string `gorm:"type:varchar(64);not null" json:"title"`
Title string `gorm:"type:varchar(64);not null" json:"title"` Status string `gorm:"type:varchar(64);not null" json:"status"`
Status string `gorm:"type:varchar(64);not null" json:"status"` Config string `gorm:"type:varchar(256);not null" json:"config"`
Config string `gorm:"type:text;not null" json:"config"` CreateUser string `gorm:"type:varchar(256)" json:"createUser"`
SecretConfig string `gorm:"type:text;not null;default:''" json:"-"` UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"`
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 { type LoginLog struct {
+13 -217
View File
@@ -1,222 +1,18 @@
package model package model
import ( type Firewall struct {
"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 BaseModel
UUID string `gorm:"size:64;not null;uniqueIndex" json:"uuid"` Type string `json:"type"`
ReadOnly bool `gorm:"not null;default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"` Port string `json:"port"` // Deprecated
Family string `gorm:"size:16;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"family"` Address string `json:"address"` // Deprecated
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"` Chain string `json:"chain"`
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"` Protocol string `json:"protocol"`
Mode string `gorm:"size:32;not null" json:"mode"` SrcIP string `json:"srcIP"`
Sources string `gorm:"type:text" json:"-"` SrcPort string `json:"srcPort"`
Description string `gorm:"type:text" json:"description"` DstIP string `json:"dstIP"`
NativeAction string `gorm:"size:32;not null;default:''" json:"-"` DstPort string `json:"dstPort"`
NativeRules string `gorm:"type:text" json:"-"` Strategy string `gorm:"not null" json:"strategy"`
Sequence int64 `gorm:"not null;default:0" json:"-"` Description string `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 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.ProviderUFW && strings.EqualFold(strings.TrimSpace(base.Protocol), "all") &&
strings.TrimSpace(base.SourcePort) == "" && strings.TrimSpace(base.DestinationPort) != "" {
base.Protocol = "tcp/udp"
}
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 -11
View File
@@ -40,21 +40,12 @@ type Meta struct {
var catalog = map[string]Meta{ var catalog = map[string]Meta{
"custom": { "custom": {
Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY", Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY",
APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "dashscope-images", "openai-embeddings"), APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
}, },
"ollama": { "ollama": {
Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses", Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses",
APIConfigs: editableAPIConfigs(false, "openai-responses", "openai-completions", "openai-embeddings"), 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": { "vllm": {
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY", 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"), APIConfigs: editableAPIConfigs(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
@@ -462,7 +453,6 @@ var legacyModelPrefixes = map[string][]string{
"custom": {"custom"}, "custom": {"custom"},
"vllm": {"custom"}, "vllm": {"custom"},
"ollama": {"ollama"}, "ollama": {"ollama"},
"llmman": {"llmman"},
"deepseek": {"deepseek"}, "deepseek": {"deepseek"},
"bailian-coding-plan": {"bailian-coding-plan"}, "bailian-coding-plan": {"bailian-coding-plan"},
"ark-coding-plan": {"ark-coding-plan"}, "ark-coding-plan": {"ark-coding-plan"},
+2 -2
View File
@@ -43,8 +43,8 @@ func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL,
providerKey = "moonshot" providerKey = "moonshot"
resolvedAPIType = "openai-completions" resolvedAPIType = "openai-completions"
usesBearer = false usesBearer = false
case "ollama", "llmman": case "ollama":
apiKey = provider apiKey = "ollama"
usesBearer = false usesBearer = false
case "openai", "openrouter", "anthropic": case "openai", "openrouter", "anthropic":
preserveQualifiedModel = strings.Contains(modelName, "/") preserveQualifiedModel = strings.Contains(modelName, "/")
+6 -9
View File
@@ -27,14 +27,11 @@ type verifyErrorResponse struct {
Message string `json:"message"` Message string `json:"message"`
} }
const ( const defaultVerifyTimeout = 30 * time.Second
defaultVerifyTimeout = 30 * time.Second
defaultVerifyMaxTokens = 16
)
func SkipVerification(provider string) bool { func SkipVerification(provider string) bool {
switch provider { switch provider {
case "vllm", "ollama", "llmman", "kimi-coding": case "vllm", "ollama", "kimi-coding":
return true return true
default: default:
return false return false
@@ -119,20 +116,20 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
} }
headers["anthropic-version"] = "2023-06-01" headers["anthropic-version"] = "2023-06-01"
request.Body = mustJSON(map[string]interface{}{ request.Body = mustJSON(map[string]interface{}{
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false, "model": model, "max_tokens": 1, "stream": false,
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}}, "messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
}) })
case "openai-responses": case "openai-responses":
request.URL = baseURL + "/responses" request.URL = baseURL + "/responses"
headers["Authorization"] = "Bearer " + apiKey headers["Authorization"] = "Bearer " + apiKey
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": defaultVerifyMaxTokens, "stream": false}) request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": 1, "stream": false})
default: default:
request.URL = baseURL + "/chat/completions" request.URL = baseURL + "/chat/completions"
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" { if provider != "ollama" || strings.TrimSpace(apiKey) != "" {
headers["Authorization"] = "Bearer " + apiKey headers["Authorization"] = "Bearer " + apiKey
} }
request.Body = mustJSON(map[string]interface{}{ request.Body = mustJSON(map[string]interface{}{
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false, "model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": 1, "stream": false,
}) })
} }
return request return request
+9 -217
View File
@@ -1,30 +1,20 @@
package repo package repo
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt"
"strconv"
"strings" "strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/google/uuid"
"google.golang.org/genproto/googleapis/type/date" "google.golang.org/genproto/googleapis/type/date"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "strconv"
"time"
) )
type AlertRepo struct{} type AlertRepo struct{}
var (
ErrAlertConfigRevisionConflict = errors.New("alert config revision conflict")
ErrAlertConfigRevisionRequired = errors.New("alert config revision is required")
)
type IAlertRepo interface { type IAlertRepo interface {
WithByType(alertType string) DBOption WithByType(alertType string) DBOption
WithByStatusIn(status []string) DBOption WithByStatusIn(status []string) DBOption
@@ -34,7 +24,6 @@ type IAlertRepo interface {
WithByCreateAt(date *date.Date) DBOption WithByCreateAt(date *date.Date) DBOption
WithByLicenseId(licenseId string) DBOption WithByLicenseId(licenseId string) DBOption
WithByRecordId(recordId uint) DBOption WithByRecordId(recordId uint) DBOption
WithByDeliveryLogID(logID uint) DBOption
WithByAlertMethodContainsConfigID(id uint) DBOption WithByAlertMethodContainsConfigID(id uint) DBOption
WithByMethodConfigIDs(ids []uint) DBOption WithByMethodConfigIDs(ids []uint) DBOption
@@ -56,8 +45,6 @@ type IAlertRepo interface {
CleanAlertLogs() error CleanAlertLogs() error
CreateAlertTask(alertTaskBase *model.AlertTask) 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 DeleteAlertTask(opts ...DBOption) error
GetAlertTask(opts ...DBOption) (model.AlertTask, error) GetAlertTask(opts ...DBOption) (model.AlertTask, error)
LoadTaskCount(alertType string, project string, method string) (uint, uint, error) LoadTaskCount(alertType string, project string, method string) (uint, uint, error)
@@ -68,7 +55,6 @@ type IAlertRepo interface {
GetConfigById(id uint) (model.AlertConfig, error) GetConfigById(id uint) (model.AlertConfig, error)
AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error) AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error)
UpdateAlertConfig(maps map[string]interface{}, opts ...DBOption) 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 CreateAlertConfig(config *model.AlertConfig) error
DeleteAlertConfig(opts ...DBOption) error DeleteAlertConfig(opts ...DBOption) error
@@ -237,78 +223,13 @@ func (a *AlertRepo) DeleteLog(opts ...DBOption) error {
} }
func (a *AlertRepo) CleanAlertLogs() error { func (a *AlertRepo) CleanAlertLogs() error {
return global.AlertDB.Where("status <> ?", constant.AlertPushing).Delete(&model.AlertLog{}).Error return global.AlertDB.Where("1 = 1").Delete(&model.AlertLog{}).Error
} }
func (a *AlertRepo) CreateAlertTask(alertTaskBase *model.AlertTask) error { func (a *AlertRepo) CreateAlertTask(alertTaskBase *model.AlertTask) error {
return global.AlertDB.Model(&model.AlertTask{}).Create(&alertTaskBase).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 { func (a *AlertRepo) DeleteAlertTask(opts ...DBOption) error {
db, _ := getAlertDB(opts...) db, _ := getAlertDB(opts...)
return db.Delete(&model.AlertTask{}).Error return db.Delete(&model.AlertTask{}).Error
@@ -389,23 +310,7 @@ func (a *AlertRepo) UpdateAlertConfig(maps map[string]interface{}, opts ...DBOpt
return db.Model(&model.AlertConfig{}).Updates(maps).Error 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 { func (a *AlertRepo) CreateAlertConfig(config *model.AlertConfig) error {
ensureAlertConfigUID(config)
return global.AlertDB.Model(&model.AlertConfig{}).Create(config).Error return global.AlertDB.Model(&model.AlertConfig{}).Create(config).Error
} }
@@ -433,12 +338,6 @@ 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) { func (a *AlertRepo) PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error) {
var configs []model.AlertConfig var configs []model.AlertConfig
db := global.AlertDB.Model(&model.AlertConfig{}) db := global.AlertDB.Model(&model.AlertConfig{})
@@ -479,44 +378,26 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
return err return err
} }
oldConfigMap := make(map[string]model.AlertConfig) oldConfigMap := make(map[string]uint)
oldConfigByUID := make(map[string]model.AlertConfig)
oldConfigByType := make(map[string][]model.AlertConfig) oldConfigByType := make(map[string][]model.AlertConfig)
oldConfigByKey := make(map[string][]model.AlertConfig) oldConfigByKey := make(map[string][]model.AlertConfig)
consumedConfigIDs := make(map[uint]struct{}) consumedConfigIDs := make(map[uint]struct{})
for _, item := range oldConfigs { for _, item := range oldConfigs {
if strings.TrimSpace(item.UID) != "" {
oldConfigByUID[item.UID] = item
}
if singletonTypes[item.Type] { if singletonTypes[item.Type] {
oldConfigMap[item.Type] = item oldConfigMap[item.Type] = item.ID
continue continue
} }
oldConfigByType[item.Type] = append(oldConfigByType[item.Type], item) oldConfigByType[item.Type] = append(oldConfigByType[item.Type], item)
oldConfigByKey[alertConfigSyncKey(item)] = append(oldConfigByKey[alertConfigSyncKey(item)], item) oldConfigByKey[alertConfigSyncKey(item)] = append(oldConfigByKey[alertConfigSyncKey(item)], item)
} }
for _, item := range data { 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 singletonTypes[item.Type] {
if matched, ok := oldConfigMap[item.Type]; ok { if val, ok := oldConfigMap[item.Type]; ok {
if err := inheritAlertConfigSyncState(&item, matched); err != nil { item.ID = val
tx.Rollback()
return err
}
delete(oldConfigMap, item.Type) delete(oldConfigMap, item.Type)
consumedConfigIDs[item.ID] = struct{}{} consumedConfigIDs[item.ID] = struct{}{}
} else { } else {
item.ID = 0 item.ID = 0
ensureAlertConfigUID(&item)
if err := validateAlertConfigSyncSecret(&item); err != nil {
tx.Rollback()
return err
}
} }
if item.ID == 0 { if item.ID == 0 {
if err := tx.Create(&item).Error; err != nil { if err := tx.Create(&item).Error; err != nil {
@@ -530,31 +411,9 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
continue 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) key := alertConfigSyncKey(item)
if matched, ok := popAlertConfigByKey(oldConfigByKey, key); ok { if matched, ok := popAlertConfigByKey(oldConfigByKey, key); ok {
delete(oldConfigByUID, matched.UID) item.ID = matched.ID
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
tx.Rollback()
return err
}
consumedConfigIDs[item.ID] = struct{}{} consumedConfigIDs[item.ID] = struct{}{}
if err := tx.Save(&item).Error; err != nil { if err := tx.Save(&item).Error; err != nil {
tx.Rollback() tx.Rollback()
@@ -565,12 +424,7 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
} }
if matched, ok := popUnusedAlertConfigByType(oldConfigByType, usedConfigIDs, item.Type); ok { if matched, ok := popUnusedAlertConfigByType(oldConfigByType, usedConfigIDs, item.Type); ok {
delete(oldConfigByUID, matched.UID) item.ID = matched.ID
deleteAlertConfigByID(oldConfigByKey, matched.ID)
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
tx.Rollback()
return err
}
consumedConfigIDs[item.ID] = struct{}{} consumedConfigIDs[item.ID] = struct{}{}
if err := tx.Save(&item).Error; err != nil { if err := tx.Save(&item).Error; err != nil {
tx.Rollback() tx.Rollback()
@@ -580,11 +434,6 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
} }
item.ID = 0 item.ID = 0
ensureAlertConfigUID(&item)
if err := validateAlertConfigSyncSecret(&item); err != nil {
tx.Rollback()
return err
}
if err := tx.Create(&item).Error; err != nil { if err := tx.Create(&item).Error; err != nil {
tx.Rollback() tx.Rollback()
return err return err
@@ -609,63 +458,6 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
return nil 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) { func loadUsedAlertConfigIDs(tx *gorm.DB) (map[uint]struct{}, error) {
var alerts []model.Alert var alerts []model.Alert
if err := tx.Select("method").Find(&alerts).Error; err != nil { if err := tx.Select("method").Find(&alerts).Error; err != nil {
-77
View File
@@ -1,77 +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 {
ListManaged(context.Context) ([]model.DockerPortGuardPolicy, error)
ListRuntimeReadOnly(context.Context) ([]model.DockerPortGuardPolicy, error)
DeleteBatch(context.Context, []string) error
UpsertBatch(context.Context, []model.DockerPortGuardPolicy) error
ReplaceRuntimeReadOnly(context.Context, []model.DockerPortGuardPolicy) error
}
type DockerPortGuardRepo struct{}
func NewIDockerPortGuardRepo() IDockerPortGuardRepo { return &DockerPortGuardRepo{} }
func (r *DockerPortGuardRepo) ListManaged(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
var policies []model.DockerPortGuardPolicy
err := global.DB.WithContext(ctx).
Where("read_only = ?", false).
Order("family, host_ip, host_port, protocol").
Find(&policies).Error
return policies, err
}
func (r *DockerPortGuardRepo) ListRuntimeReadOnly(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
var policies []model.DockerPortGuardPolicy
err := global.DB.WithContext(ctx).
Where("read_only = ?", true).
Order("family, sequence, 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("read_only = ? AND uuid IN ?", false, 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 {
policies[i].ReadOnly = false
if err := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "read_only"}, {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
})
}
func (r *DockerPortGuardRepo) ReplaceRuntimeReadOnly(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("read_only = ?", true).
Delete(&model.DockerPortGuardPolicy{}).Error; err != nil {
return err
}
if len(policies) == 0 {
return nil
}
for i := range policies {
policies[i].ReadOnly = true
}
return tx.Create(&policies).Error
})
}
-144
View File
@@ -1,144 +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 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
}
-36
View File
@@ -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
})
}
+72
View File
@@ -22,6 +22,11 @@ type IHostRepo interface {
WithByPort(port uint) DBOption WithByPort(port uint) DBOption
WithByUser(user string) DBOption WithByUser(user string) DBOption
GetFirewallRecord(opts ...DBOption) (model.Firewall, error)
ListFirewallRecord(opts ...DBOption) ([]model.Firewall, error)
SaveFirewallRecord(firewall *model.Firewall) error
DeleteFirewallRecordByID(id uint) error
SyncCert(data []model.RootCert) error SyncCert(data []model.RootCert) error
GetCert(opts ...DBOption) (model.RootCert, error) GetCert(opts ...DBOption) (model.RootCert, error)
PageCert(limit, offset int, opts ...DBOption) (int64, []model.RootCert, error) PageCert(limit, offset int, opts ...DBOption) (int64, []model.RootCert, error)
@@ -29,6 +34,8 @@ type IHostRepo interface {
SaveCert(cert *model.RootCert) error SaveCert(cert *model.RootCert) error
UpdateCert(id uint, vars map[string]interface{}) error UpdateCert(id uint, vars map[string]interface{}) error
DeleteCert(opts ...DBOption) error DeleteCert(opts ...DBOption) error
WithByChain(chain string) DBOption
} }
func NewIHostRepo() IHostRepo { func NewIHostRepo() IHostRepo {
@@ -109,6 +116,65 @@ func (h *HostRepo) Delete(opts ...DBOption) error {
return db.Delete(&model.Host{}).Error return db.Delete(&model.Host{}).Error
} }
func (h *HostRepo) GetFirewallRecord(opts ...DBOption) (model.Firewall, error) {
var firewall model.Firewall
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&firewall).Error
return firewall, err
}
func (h *HostRepo) ListFirewallRecord(opts ...DBOption) ([]model.Firewall, error) {
var firewalls []model.Firewall
db := global.DB
for _, opt := range opts {
db = opt(db)
}
if err := global.DB.Find(&firewalls).Error; err != nil {
return firewalls, nil
}
return firewalls, nil
}
func (h *HostRepo) SaveFirewallRecord(firewall *model.Firewall) error {
if firewall.ID != 0 {
return global.DB.Save(firewall).Error
}
var data model.Firewall
switch firewall.Type {
case "port":
_ = global.DB.Where("type = ? AND dst_port = ? AND protocol = ? AND src_ip = ? AND strategy = ?", "port",
firewall.DstPort,
firewall.Protocol,
firewall.SrcIP,
firewall.Strategy,
).First(&data).Error
case "ip":
_ = global.DB.Where("type = ? AND src_ip = ? AND strategy = ?", "address", firewall.SrcIP, firewall.Strategy).First(&data)
default:
_ = global.DB.Where("type = ? AND chain = ? AND src_port = ? AND dst_port = ? AND protocol = ? AND src_ip = ? AND dst_ip = ? AND strategy = ?",
firewall.Type,
firewall.Chain,
firewall.SrcPort,
firewall.DstPort,
firewall.Protocol,
firewall.SrcIP,
firewall.DstIP,
firewall.Strategy,
).First(&data).Error
}
if data.ID != 0 {
firewall.ID = data.ID
}
return global.DB.Save(firewall).Error
}
func (h *HostRepo) DeleteFirewallRecordByID(id uint) error {
return global.DB.Where("id = ?", id).Delete(&model.Firewall{}).Error
}
func (u *HostRepo) GetCert(opts ...DBOption) (model.RootCert, error) { func (u *HostRepo) GetCert(opts ...DBOption) (model.RootCert, error) {
var cert model.RootCert var cert model.RootCert
db := global.DB db := global.DB
@@ -187,3 +253,9 @@ func (u *HostRepo) SyncCert(data []model.RootCert) error {
tx.Commit() tx.Commit()
return nil return nil
} }
func (u *HostRepo) WithByChain(chain string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("chain = ?", chain)
}
}
+16 -9
View File
@@ -38,7 +38,7 @@ type IAgentService interface {
BatchUpgrade(req dto.AgentBatchUpgradeReq) ([]dto.AgentBatchUpgradeResult, error) BatchUpgrade(req dto.AgentBatchUpgradeReq) ([]dto.AgentBatchUpgradeResult, error)
BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dto.AgentBatchSkillInstallResult, error) BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dto.AgentBatchSkillInstallResult, error)
BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBatchOperateResult, error) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBatchOperateResult, error)
Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error) Page(req dto.SearchWithPage, readOnly bool) (int64, []dto.AgentItem, error)
DeleteCheck(req dto.AgentIDReq) ([]dto.AppResource, error) DeleteCheck(req dto.AgentIDReq) ([]dto.AppResource, error)
Delete(req dto.AgentDeleteReq) error Delete(req dto.AgentDeleteReq) error
ResetToken(req dto.AgentTokenResetReq) error ResetToken(req dto.AgentTokenResetReq) error
@@ -76,7 +76,7 @@ type IAgentService interface {
CreateAccount(req dto.AgentAccountCreateReq) error CreateAccount(req dto.AgentAccountCreateReq) error
UpdateAccount(req dto.AgentAccountUpdateReq) error UpdateAccount(req dto.AgentAccountUpdateReq) error
SyncAgentsByAccount(account *model.AgentAccount) error SyncAgentsByAccount(account *model.AgentAccount) error
PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error) PageAccounts(req dto.AgentAccountSearch, readOnly ...bool) (int64, []dto.AgentAccountInfo, error)
CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error)
GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error)
@@ -745,7 +745,7 @@ func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion st
params["PANEL_APP_PORT_HTTP"] = webUIPort params["PANEL_APP_PORT_HTTP"] = webUIPort
} }
func (a AgentService) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error) { func (a AgentService) Page(req dto.SearchWithPage, readOnly bool) (int64, []dto.AgentItem, error) {
var opts []repo.DBOption var opts []repo.DBOption
if strings.TrimSpace(req.Info) != "" { if strings.TrimSpace(req.Info) != "" {
opts = append(opts, repo.WithByLikeName(req.Info)) opts = append(opts, repo.WithByLikeName(req.Info))
@@ -760,11 +760,19 @@ func (a AgentService) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, erro
appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(item.AppInstallID)) appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(item.AppInstallID))
appInstalls = append(appInstalls, appInstall) appInstalls = append(appInstalls, appInstall)
} }
syncAgentAppInstalls(appInstalls) readOnlyMode := isDemoReadOnly(readOnly)
if !readOnlyMode {
syncAgentAppInstalls(appInstalls)
}
for index, item := range list { for index, item := range list {
appInstall := appInstalls[index] appInstall := appInstalls[index]
envMap := readInstallEnv(appInstall.Env) envMap := readInstallEnv(appInstall.Env)
agentItem := buildAgentItem(&item, &appInstall, envMap) agentItem := buildAgentItem(&item, &appInstall, envMap)
if readOnlyMode {
agentItem.Token = ""
agentItem.APIKey = ""
agentItem.DashboardPassword = ""
}
agentItem.Upgradable = checkAgentUpgradable(appInstall) agentItem.Upgradable = checkAgentUpgradable(appInstall)
items = append(items, agentItem) items = append(items, agentItem)
} }
@@ -936,7 +944,6 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig,
AccountID: agent.AccountID, AccountID: agent.AccountID,
Model: model, Model: model,
Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model), Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
Metadata: extractOpenclawModelMetadata(conf, account, models),
}, nil }, nil
} }
@@ -968,7 +975,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
if agent.AgentType != constant.AppOpenclaw { if agent.AgentType != constant.AppOpenclaw {
return fmt.Errorf("%s does not support", agent.AgentType) return fmt.Errorf("%s does not support", agent.AgentType)
} }
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks, req.Metadata); err != nil { if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil {
return err return err
} }
} }
@@ -1129,7 +1136,7 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
return nil return nil
} }
func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error) { func (a AgentService) PageAccounts(req dto.AgentAccountSearch, readOnly ...bool) (int64, []dto.AgentAccountInfo, error) {
var opts []repo.DBOption var opts []repo.DBOption
if strings.TrimSpace(req.Provider) != "" { if strings.TrimSpace(req.Provider) != "" {
opts = append(opts, repo.WithByProvider(req.Provider)) opts = append(opts, repo.WithByProvider(req.Provider))
@@ -1150,7 +1157,7 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
items := make([]dto.AgentAccountInfo, 0, len(list)) items := make([]dto.AgentAccountInfo, 0, len(list))
for _, item := range list { for _, item := range list {
apiKey := "" apiKey := ""
if item.RememberAPIKey { if item.RememberAPIKey && !isDemoReadOnly(readOnly...) {
apiKey = item.APIKey apiKey = item.APIKey
} }
items = append(items, dto.AgentAccountInfo{ items = append(items, dto.AgentAccountInfo{
@@ -1685,7 +1692,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
return err return err
} }
fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID) fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks, nil); err != nil { if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil {
return err return err
} }
case constant.AppHermesAgent: case constant.AppHermesAgent:
+1 -18
View File
@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"os" "os"
"path" "path"
"slices"
"sort" "sort"
"strings" "strings"
"time" "time"
@@ -1321,10 +1320,6 @@ func appendPluginAllow(conf map[string]interface{}, pluginID string) {
} }
func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID string) error { 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) workdir := path.Join(openclawPluginPackageTmpDir, pluginID)
defer func() { defer func() {
_ = mgr.Run("docker", "exec", containerName, "rm", "-rf", workdir) _ = mgr.Run("docker", "exec", containerName, "rm", "-rf", workdir)
@@ -1346,19 +1341,7 @@ func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID
if pkgPath == "" { if pkgPath == "" {
return fmt.Errorf("openclaw plugin package not found") return fmt.Errorf("openclaw plugin package not found")
} }
args := []string{"exec", containerName, "openclaw", "plugins", "install", pkgPath} return mgr.Run("docker", "exec", containerName, "openclaw", "plugins", "install", pkgPath, "--dangerously-force-unsafe-install")
// 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 { func uninstallOpenclawPlugin(mgr *cmd.CommandHelper, containerName, pluginID string) error {
+6 -151
View File
@@ -10,7 +10,6 @@ import (
"net/url" "net/url"
"path" "path"
"regexp" "regexp"
"slices"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -738,11 +737,9 @@ type modelProvider struct {
} }
type modelEntry struct { type modelEntry struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Input []string `json:"input,omitempty"` Input []string `json:"input,omitempty"`
ContextWindow int `json:"contextWindow,omitempty"`
MaxTokens int `json:"maxTokens,omitempty"`
} }
func requiresOpenclawProviderModels(provider string) bool { func requiresOpenclawProviderModels(provider string) bool {
@@ -770,7 +767,7 @@ type browserConfig struct {
DefaultProfile string `json:"defaultProfile"` DefaultProfile string `json:"defaultProfile"`
} }
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string, metadata []dto.AgentModelMetadata) error { func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string) error {
if strings.TrimSpace(confDir) == "" { if strings.TrimSpace(confDir) == "" {
return fmt.Errorf("config dir is required") return fmt.Errorf("config dir is required")
} }
@@ -855,7 +852,6 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
} }
conf = initial conf = initial
} else { } else {
preserveOpenclawModelMetadata(conf, cfg.Models)
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil { if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
return err return err
} }
@@ -910,9 +906,6 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
if allowedOrigins != nil { if allowedOrigins != nil {
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins}) setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
} }
if err := applyOpenclawModelMetadata(conf, account, metadata); err != nil {
return err
}
if err := writeOpenclawConfigRaw(configPath, conf); err != nil { if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
return err return err
} }
@@ -927,144 +920,6 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order) 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) { func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
accountModels, err := loadAgentAccountModels(account) accountModels, err := loadAgentAccountModels(account)
if err != nil { if err != nil {
@@ -1186,7 +1041,7 @@ func prepareOpenclawInstallFiles(appInstall *model.AppInstall, account *model.Ag
return fmt.Errorf("app install is required") return fmt.Errorf("app install is required")
} }
confDir := path.Join(appInstall.GetPath(), "data", "conf") confDir := path.Join(appInstall.GetPath(), "data", "conf")
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil, nil); err != nil { if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil); err != nil {
return err return err
} }
dataDir := path.Join(appInstall.GetPath(), "data") dataDir := path.Join(appInstall.GetPath(), "data")
@@ -1483,7 +1338,7 @@ func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAcco
func requiresInitialAgentAccountModels(provider string) bool { func requiresInitialAgentAccountModels(provider string) bool {
switch provider { switch provider {
case "custom", "vllm", "ollama", "llmman": case "custom", "vllm", "ollama":
return true return true
default: default:
return false return false
+44 -326
View File
@@ -17,13 +17,10 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n" "github.com/1Panel-dev/1Panel/agent/i18n"
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/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/copier" "github.com/1Panel-dev/1Panel/agent/utils/copier"
"github.com/1Panel-dev/1Panel/agent/utils/email" "github.com/1Panel-dev/1Panel/agent/utils/email"
"github.com/1Panel-dev/1Panel/agent/utils/xpack" "github.com/1Panel-dev/1Panel/agent/utils/xpack"
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
"github.com/shirou/gopsutil/v4/disk" "github.com/shirou/gopsutil/v4/disk"
) )
@@ -37,28 +34,6 @@ var communityAlertMethodTypeNames = map[string]string{
constant.SMS: "SMS", 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 { type IAlertService interface {
PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error) PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error)
GetAlerts() ([]dto.AlertDTO, error) GetAlerts() ([]dto.AlertDTO, error)
@@ -76,12 +51,10 @@ type IAlertService interface {
GetCronJobs(req dto.CronJobReq) ([]dto.CronJobDTO, error) GetCronJobs(req dto.CronJobReq) ([]dto.CronJobDTO, error)
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error) GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error) PageAlertConfig(req dto.AlertConfigPageReq, readOnly ...bool) (int64, []model.AlertConfig, error)
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
DeleteAlertConfig(id uint) error DeleteAlertConfig(id uint) error
TestAlertConfig(req dto.AlertConfigTest) (bool, error) TestAlertConfig(req dto.AlertConfigTest) (bool, error)
TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error)
} }
func NewIAlertService() IAlertService { func NewIAlertService() IAlertService {
@@ -207,15 +180,9 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
} }
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error { func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
methodTypes, err := a.validateAlertMethodReferences(req.Method) if err := a.validateCommunityAlertMethod(req.Method); err != nil {
if err != nil {
return err return err
} }
if req.Status != constant.AlertDisable {
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
return err
}
}
upMap := make(map[string]interface{}) upMap := make(map[string]interface{})
upMap["id"] = req.ID upMap["id"] = req.ID
@@ -273,16 +240,7 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
if alertInfo.ID == 0 { if alertInfo.ID == 0 {
return buserr.New("ErrRecordNotFound") return buserr.New("ErrRecordNotFound")
} }
methodTypes, err := a.validateAlertMethodReferences(alertInfo.Method) err := alertRepo.Update(map[string]interface{}{"status": status}, repo.WithByID(alertInfo.ID))
if err != nil {
return err
}
if status == constant.AlertEnable {
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
return err
}
}
err = alertRepo.Update(map[string]interface{}{"status": status}, repo.WithByID(alertInfo.ID))
if err != nil { if err != nil {
return err return err
} }
@@ -382,7 +340,7 @@ func executeDiskCommand() (string, error) {
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second)) cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P") stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P")
} }
if err != nil && strings.TrimSpace(stdout) == "" { if err != nil {
return stdout, err return stdout, err
} }
var lines []string var lines []string
@@ -454,7 +412,6 @@ func (a AlertService) parseAlertLog(item model.AlertLog) (dto.AlertLogDTO, error
if err := unmarshalAlertInfo(item.AlertDetail, &alertDetail); err != nil { if err := unmarshalAlertInfo(item.AlertDetail, &alertDetail); err != nil {
return dto.AlertLogDTO{}, err return dto.AlertLogDTO{}, err
} }
alertDetail.Task = nil
if err := unmarshalAlertInfo(item.AlertRule, &alertRule); err != nil { if err := unmarshalAlertInfo(item.AlertRule, &alertRule); err != nil {
return dto.AlertLogDTO{}, err return dto.AlertLogDTO{}, err
} }
@@ -537,16 +494,10 @@ func (a AlertService) GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertCon
} }
opts = append(opts, repo.WithByStatus(constant.AlertEnable)) opts = append(opts, repo.WithByStatus(constant.AlertEnable))
configs, err := alertRepo.AlertConfigList(opts...) configs, err := alertRepo.AlertConfigList(opts...)
if err != nil { return configs, err
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) { func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq, readOnly ...bool) (int64, []model.AlertConfig, error) {
opts := []repo.DBOption{ opts := []repo.DBOption{
alertRepo.WithByTypeNotIn([]string{"common"}), alertRepo.WithByTypeNotIn([]string{"common"}),
repo.WithOrderDesc("created_at"), repo.WithOrderDesc("created_at"),
@@ -555,48 +506,30 @@ func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []mode
opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes)) opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes))
} }
total, configs, err := alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...) total, configs, err := alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...)
if err != nil { if err != nil || !isDemoReadOnly(readOnly...) {
return 0, nil, err return total, configs, err
} }
if err := exposeCustomAlertConfigSecrets(configs); err != nil { for i := range configs {
return 0, nil, err var value interface{}
if err := json.Unmarshal([]byte(configs[i].Config), &value); err != nil {
configs[i].Config = ""
continue
}
redactSensitiveData(value)
data, err := json.Marshal(value)
if err != nil {
configs[i].Config = ""
continue
}
configs[i].Config = string(data)
} }
return total, configs, nil return total, configs, nil
} }
func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error { 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 { if err := a.validateCommunityAlertConfigType(req.Type); err != nil {
return err 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 { if err := a.checkAlertConfigDisplayNameUnique(req); err != nil {
return err return err
} }
@@ -611,7 +544,7 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator stri
upMap["status"] = req.Status upMap["status"] = req.Status
upMap["config"] = req.Config upMap["config"] = req.Config
upMap["update_user"] = operator upMap["update_user"] = operator
if err := alertRepo.UpdateAlertConfigWithRevision(upMap, req.Revision, repo.WithByID(req.ID)); err != nil { if err := alertRepo.UpdateAlertConfig(upMap, repo.WithByID(req.ID)); err != nil {
return err return err
} }
} else { } else {
@@ -629,99 +562,6 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator stri
return nil 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 { func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate) error {
if req.Type != constant.SMSConfig { if req.Type != constant.SMSConfig {
return nil return nil
@@ -746,9 +586,6 @@ func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate)
} }
func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdate) error { 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) displayName := alertConfigDisplayName(req.Type, req.Config)
if displayName == "" { if displayName == "" {
return nil return nil
@@ -772,67 +609,37 @@ func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdat
} }
func (a AlertService) validateCommunityAlertMethod(method string) error { func (a AlertService) validateCommunityAlertMethod(method string) error {
methodTypes, err := a.validateAlertMethodReferences(method) if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
if err != nil { return nil
return err
} }
return a.validateAlertMethodEntitlement(methodTypes)
}
func (a AlertService) validateAlertMethodReferences(method string) ([]string, error) {
if strings.TrimSpace(method) == "" { if strings.TrimSpace(method) == "" {
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil) return nil
} }
methodTypes := make([]string, 0)
for _, item := range strings.Split(method, ",") { for _, item := range strings.Split(method, ",") {
item = strings.TrimSpace(item) item = strings.TrimSpace(item)
if item == "" { if item == "" {
continue continue
} }
configType := ""
if configID, err := strconv.ParseUint(item, 10, 64); err == nil { if configID, err := strconv.ParseUint(item, 10, 64); err == nil {
config, err := alertRepo.GetConfigById(uint(configID)) config, err := alertRepo.GetConfigById(uint(configID))
if err != nil { if err != nil {
return nil, err return err
} }
configType = config.Type if _, ok := communityAlertMethodTypeNames[config.Type]; ok {
} else { return buserr.WithErr("ErrAlertMethodNotSupported", nil)
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 continue
} }
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" { if _, ok := communityAlertMethodTypeNames[item]; ok {
continue
}
if _, ok := communityAlertMethodTypeNames[configType]; ok {
return buserr.WithErr("ErrAlertMethodNotSupported", nil) return buserr.WithErr("ErrAlertMethodNotSupported", nil)
} }
} }
return nil return nil
} }
func (a AlertService) validateCommunityAlertConfigType(configType string) error { func (a AlertService) validateCommunityAlertConfigType(configType string) error {
if configType == constant.Custom {
return nil
}
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" { if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
return nil return nil
} }
@@ -844,7 +651,7 @@ func (a AlertService) validateCommunityAlertConfigType(configType string) error
func alertConfigDisplayName(configType, configData string) string { func alertConfigDisplayName(configType, configData string) string {
switch configType { switch configType {
case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark, constant.SMS, constant.Custom: case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark, constant.SMS:
var cfg struct { var cfg struct {
DisplayName string `json:"displayName"` DisplayName string `json:"displayName"`
} }
@@ -883,24 +690,20 @@ func (a AlertService) DeleteAlertConfig(id uint) error {
} }
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) { func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
emailConfig, err := resolveEmailTestConfig(req) username := req.UserName
if err != nil {
return false, err
}
username := emailConfig.UserName
if username == "" { if username == "" {
username = emailConfig.Sender username = req.Sender
} }
encodedDisplayName := mime.BEncoding.Encode("UTF-8", emailConfig.DisplayName) encodedDisplayName := mime.BEncoding.Encode("UTF-8", req.DisplayName)
cfg := email.SMTPConfig{ cfg := email.SMTPConfig{
Host: emailConfig.Host, Host: req.Host,
Port: emailConfig.Port, Port: req.Port,
Sender: emailConfig.Sender, Sender: req.Sender,
Username: username, Username: username,
Password: emailConfig.Password, Password: req.Password,
From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, emailConfig.Sender), From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, req.Sender),
Encryption: emailConfig.Encryption, Encryption: req.Encryption,
Recipient: emailConfig.Recipient, Recipient: req.Recipient,
} }
msg := email.EmailMessage{ msg := email.EmailMessage{
@@ -915,94 +718,9 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
return true, nil 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 { func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
var methodTypes []string if err := a.validateCommunityAlertMethod(updateAlert.Method); err != nil {
if updateAlert.SendCount != 0 || strings.TrimSpace(updateAlert.Method) != "" { return err
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{}) upMap := make(map[string]interface{})
var newStatus string var newStatus string
+28 -100
View File
@@ -2,7 +2,6 @@ package service
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"math" "math"
"net" "net"
@@ -33,7 +32,6 @@ const (
ResourceAlertInterval = 30 ResourceAlertInterval = 30
CheckIntervalSec = 3 CheckIntervalSec = 3
LoadCheckIntervalMin = 5 LoadCheckIntervalMin = 5
sshIPLoginWindow = 30 * time.Minute
) )
type AlertTaskHelper struct { type AlertTaskHelper struct {
@@ -514,28 +512,10 @@ func loadPanelLogin(alert dto.AlertDTO) {
} }
func loadSSHLogin(alert dto.AlertDTO) { func loadSSHLogin(alert dto.AlertDTO) {
now := time.Now() count, isAlert, err := alertUtil.CountRecentFailedSSHLog(alert.Cycle, alert.Count)
failedWindow := time.Duration(alert.Cycle) * time.Minute
loadWindow := failedWindow
if loadWindow < sshIPLoginWindow {
loadWindow = sshIPLoginWindow
}
location, err := time.LoadLocation(common.LoadTimeZoneByCmd())
if err != nil { if err != nil {
global.LOG.Errorf("Failed to load timezone for ssh login logs: %v", err) global.LOG.Errorf("Failed to count recent failed 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)
}
count, records := summarizeSSHLoginHistories(
histories,
now,
failedWindow,
strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n"),
)
isAlert := count >= int(alert.Count)
if isAlert { if isAlert {
params := []dto.Param{ params := []dto.Param{
{ {
@@ -551,6 +531,12 @@ func loadSSHLogin(alert dto.AlertDTO) {
} }
sendAlerts(alert, "sshLogin", strconv.Itoa(count), "sshLogin", params) 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 { if len(records) > 0 {
quota := strings.Join(records, "\n") quota := strings.Join(records, "\n")
params := []dto.Param{ params := []dto.Param{
@@ -579,6 +565,20 @@ func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string)
return filtered 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 { func isIPInWhitelist(ip string, whitelist []string) bool {
targetIP := net.ParseIP(strings.TrimSpace(ip)) targetIP := net.ParseIP(strings.TrimSpace(ip))
if targetIP == nil { if targetIP == nil {
@@ -695,10 +695,9 @@ func sendAlertsByConfigId(alert dto.AlertDTO, alertType, quota, quotaType string
func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, method string) { func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, method string) {
typeMap := map[string]string{ typeMap := map[string]string{
"mail": constant.Email, "mail": constant.Email,
constant.Bark: constant.Bark, constant.Bark: constant.Bark,
constant.SMS: constant.SMS, constant.SMS: constant.SMS,
constant.Custom: constant.Custom,
} }
configType, ok := typeMap[method] configType, ok := typeMap[method]
if !ok { if !ok {
@@ -786,7 +785,7 @@ func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params
} }
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr) alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Custom: case constant.WeCom, constant.DingTalk, constant.FeiShu:
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr) todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr)
if !isValid { if !isValid {
return return
@@ -799,31 +798,12 @@ func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params
} }
transport := xpack.MultiNodeProvider.LoadRequestTransport() transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
queued := false alertErr := xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo)
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 { if alertErr != nil {
global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, methodStr, alertErr) global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, methodStr, alertErr)
return return
} }
if !queued { alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
}
} }
} }
@@ -1117,55 +1097,3 @@ func calculateMinutesDifference(newDate time.Time) int {
minutesDifference := int(now.Sub(newDate).Minutes()) minutesDifference := int(now.Sub(newDate).Minutes())
return minutesDifference 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
}
+6 -45
View File
@@ -75,7 +75,7 @@ func (s *AlertSender) sendByConfig(config model.AlertConfig, quota string, param
} else { } else {
s.sendBarkWithConfig(config, quota, params) s.sendBarkWithConfig(config, quota, params)
} }
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Custom: case constant.WeCom, constant.DingTalk, constant.FeiShu:
if isResource { if isResource {
s.sendResourceWebhookWithConfig(config, quota, params) s.sendResourceWebhookWithConfig(config, quota, params)
} else { } 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) { func (s *AlertSender) sendByLegacyMethod(method string, quota string, params []dto.Param, isResource bool) {
alertRepo := repo.NewIAlertRepo() alertRepo := repo.NewIAlertRepo()
typeMap := map[string]string{"mail": constant.Email, constant.Bark: constant.Bark, constant.SMS: constant.SMS, constant.Custom: constant.Custom} typeMap := map[string]string{"mail": constant.Email, constant.Bark: constant.Bark, constant.SMS: constant.SMS}
configType := method configType := method
if mapped, ok := typeMap[method]; ok { if mapped, ok := typeMap[method]; ok {
configType = mapped configType = mapped
@@ -308,31 +308,12 @@ func (s *AlertSender) sendWebhookWithConfig(config model.AlertConfig, quota stri
} }
transport := xpack.MultiNodeProvider.LoadRequestTransport() transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
queued := false err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
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 { if err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err) global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
return return
} }
if !queued { alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
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) { func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
@@ -353,31 +334,11 @@ func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, qu
} }
transport := xpack.MultiNodeProvider.LoadRequestTransport() transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
queued := false if err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo); err != nil {
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) global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
return return
} }
if !queued { alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
} }
func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) { func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) {
+53 -55
View File
@@ -48,7 +48,7 @@ type IAppService interface {
PageApp(ctx *gin.Context, req request.AppSearch) (*response.AppRes, error) PageApp(ctx *gin.Context, req request.AppSearch) (*response.AppRes, error)
GetAppTags(ctx *gin.Context) ([]response.TagDTO, error) GetAppTags(ctx *gin.Context) ([]response.TagDTO, error)
GetApp(ctx *gin.Context, key string) (*response.AppDTO, error) GetApp(ctx *gin.Context, key string) (*response.AppDTO, error)
GetAppDetail(appId uint, version, appType string) (response.AppDetailDTO, error) GetAppDetail(appId uint, version, appType string, readOnly ...bool) (response.AppDetailDTO, error)
Install(req request.AppInstallCreate, executeScript bool) (*model.AppInstall, error) Install(req request.AppInstallCreate, executeScript bool) (*model.AppInstall, error)
SyncAppListFromRemote(taskID string) error SyncAppListFromRemote(taskID string) error
GetAppUpdate() (*response.AppUpdateRes, error) GetAppUpdate() (*response.AppUpdateRes, error)
@@ -223,9 +223,6 @@ func (a AppService) GetAppDetailByKey(appKey, version string) (response.AppDetai
if err != nil { if err != nil {
return appDetailDTO, err 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)) appDetail, err := appDetailRepo.GetFirst(appDetailRepo.WithAppId(app.ID), appDetailRepo.WithVersion(version))
if err != nil { if err != nil {
return appDetailDTO, err return appDetailDTO, err
@@ -234,7 +231,7 @@ func (a AppService) GetAppDetailByKey(appKey, version string) (response.AppDetai
return appDetailDTO, nil return appDetailDTO, nil
} }
func (a AppService) GetAppDetail(appID uint, version, appType string) (response.AppDetailDTO, error) { func (a AppService) GetAppDetail(appID uint, version, appType string, readOnly ...bool) (response.AppDetailDTO, error) {
var ( var (
appDetailDTO response.AppDetailDTO appDetailDTO response.AppDetailDTO
opts []repo.DBOption opts []repo.DBOption
@@ -244,56 +241,58 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
if err != nil { if err != nil {
return appDetailDTO, err 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.AppDetail = detail
appDetailDTO.Enable = true appDetailDTO.Enable = true
readOnlyMode := isDemoReadOnly(readOnly...)
if appType == "runtime" { if appType == "runtime" {
app, err := appRepo.GetFirst(repo.WithByID(appID))
if err != nil {
return appDetailDTO, err
}
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version) versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version)
if !fileOp.Stat(versionPath) || detail.Update { versionExists := fileOp.Stat(versionPath)
if (!versionExists || detail.Update) && !readOnlyMode {
if err = downloadApp(app, detail, nil, nil); err != nil && !fileOp.Stat(versionPath) { if err = downloadApp(app, detail, nil, nil); err != nil && !fileOp.Stat(versionPath) {
return appDetailDTO, err return appDetailDTO, err
} }
versionExists = fileOp.Stat(versionPath)
} }
switch app.Type { if versionExists {
case constant.RuntimePHP: switch app.Type {
paramsPath := filepath.Join(versionPath, "data.yml") case constant.RuntimePHP:
if !fileOp.Stat(paramsPath) { paramsPath := filepath.Join(versionPath, "data.yml")
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil) if !fileOp.Stat(paramsPath) {
} return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
param, err := fileOp.GetContent(paramsPath) }
if err != nil { param, err := fileOp.GetContent(paramsPath)
return appDetailDTO, err if err != nil {
} return appDetailDTO, err
paramMap := make(map[string]interface{}) }
if err = yaml.Unmarshal(param, &paramMap); err != nil { paramMap := make(map[string]interface{})
return appDetailDTO, err if err = yaml.Unmarshal(param, &paramMap); err != nil {
} return appDetailDTO, err
appDetailDTO.Params = paramMap["additionalProperties"] }
composePath := filepath.Join(versionPath, "docker-compose.yml") appDetailDTO.Params = paramMap["additionalProperties"]
if !fileOp.Stat(composePath) { composePath := filepath.Join(versionPath, "docker-compose.yml")
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", composePath, nil) if !fileOp.Stat(composePath) {
} return appDetailDTO, buserr.WithDetail("ErrFileNotExist", composePath, nil)
compose, err := fileOp.GetContent(composePath) }
if err != nil { compose, err := fileOp.GetContent(composePath)
return appDetailDTO, err if err != nil {
} return appDetailDTO, err
composeMap := make(map[string]interface{}) }
if err := yaml.Unmarshal(compose, &composeMap); err != nil { composeMap := make(map[string]interface{})
return appDetailDTO, err if err := yaml.Unmarshal(compose, &composeMap); err != nil {
} return appDetailDTO, err
if service, ok := composeMap["services"]; ok { }
servicesMap := service.(map[string]interface{}) if service, ok := composeMap["services"]; ok {
for k := range servicesMap { servicesMap := service.(map[string]interface{})
appDetailDTO.Image = k for k := range servicesMap {
appDetailDTO.Image = k
}
} }
} }
} }
@@ -305,7 +304,7 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
appDetailDTO.Params = paramMap appDetailDTO.Params = paramMap
} }
if appDetailDTO.DockerCompose == "" { if appDetailDTO.DockerCompose == "" && !readOnlyMode {
filename := filepath.Base(appDetailDTO.DownloadUrl) filename := filepath.Base(appDetailDTO.DownloadUrl)
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml") dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml")
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s) statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
@@ -325,6 +324,10 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
appDetailDTO.HostMode = isHostModel(appDetailDTO.DockerCompose) 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 { if err := checkLimit(app); err != nil {
appDetailDTO.Enable = false appDetailDTO.Enable = false
} }
@@ -376,9 +379,6 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
if err != nil { if err != nil {
return return
} }
if err = checkVllmVersionAccess(app.Key, appDetail.Version); err != nil {
return
}
if DatabaseKeys[app.Key] > 0 { if DatabaseKeys[app.Key] > 0 {
if existDatabases, _ := databaseRepo.GetList(repo.WithByName(req.Name)); len(existDatabases) > 0 { if existDatabases, _ := databaseRepo.GetList(repo.WithByName(req.Name)); len(existDatabases) > 0 {
err = buserr.New("ErrRemoteExist") err = buserr.New("ErrRemoteExist")
@@ -488,17 +488,15 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
index++ index++
} }
newServiceName := strings.ToLower(appInstall.Name) newServiceName := strings.ToLower(appInstall.Name)
if app.Limit == 0 && newServiceName != serviceName && len(servicesMap) == 1 && !req.KeepServiceName { if app.Limit == 0 && newServiceName != serviceName && len(servicesMap) == 1 {
servicesMap[newServiceName] = servicesMap[serviceName] servicesMap[newServiceName] = servicesMap[serviceName]
delete(servicesMap, serviceName) delete(servicesMap, serviceName)
serviceName = newServiceName serviceName = newServiceName
} }
appInstall.ServiceName = serviceName appInstall.ServiceName = serviceName
if !req.SkipComposeCommonConfig { if err = addDockerComposeCommonParam(composeMap, appInstall.ServiceName, req.AppContainerConfig, req.Params); err != nil {
if err = addDockerComposeCommonParam(composeMap, appInstall.ServiceName, req.AppContainerConfig, req.Params); err != nil { return
return
}
} }
var ( var (
composeByte []byte composeByte []byte
@@ -566,7 +564,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
return err return err
} }
} }
if executeScript || req.UseLifecycleScripts { if executeScript {
if err = runScript(t, appInstall, "init"); err != nil { if err = runScript(t, appInstall, "init"); err != nil {
return err return err
} }
@@ -579,7 +577,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
return err return err
} }
} }
if err = upApp(t, appInstall, req.PullImage, req.UseLifecycleScripts); err != nil { if err = upApp(t, appInstall, req.PullImage); err != nil {
return err return err
} }
updateToolApp(appInstall) updateToolApp(appInstall)
+8 -4
View File
@@ -13,7 +13,7 @@ type AppIgnoreUpgradeService struct {
} }
type IAppIgnoreUpgradeService interface { type IAppIgnoreUpgradeService interface {
List() ([]response.AppIgnoreUpgradeDTO, error) List(readOnly ...bool) ([]response.AppIgnoreUpgradeDTO, error)
CreateAppIgnore(req request.AppIgnoreUpgradeReq) error CreateAppIgnore(req request.AppIgnoreUpgradeReq) error
Delete(req request.ReqWithID) error Delete(req request.ReqWithID) error
} }
@@ -22,7 +22,7 @@ func NewIAppIgnoreUpgradeService() IAppIgnoreUpgradeService {
return AppIgnoreUpgradeService{} return AppIgnoreUpgradeService{}
} }
func (a AppIgnoreUpgradeService) List() ([]response.AppIgnoreUpgradeDTO, error) { func (a AppIgnoreUpgradeService) List(readOnly ...bool) ([]response.AppIgnoreUpgradeDTO, error) {
var res []response.AppIgnoreUpgradeDTO var res []response.AppIgnoreUpgradeDTO
ignores, err := appIgnoreUpgradeRepo.List() ignores, err := appIgnoreUpgradeRepo.List()
if err != nil { if err != nil {
@@ -37,14 +37,18 @@ func (a AppIgnoreUpgradeService) List() ([]response.AppIgnoreUpgradeDTO, error)
} }
app, err := appRepo.GetFirst(repo.WithByID(ignore.AppID)) app, err := appRepo.GetFirst(repo.WithByID(ignore.AppID))
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
_ = appIgnoreUpgradeRepo.Delete(repo.WithByID(ignore.ID)) if !isDemoReadOnly(readOnly...) {
_ = appIgnoreUpgradeRepo.Delete(repo.WithByID(ignore.ID))
}
continue continue
} }
dto.Name = app.Name dto.Name = app.Name
if ignore.Scope == "version" { if ignore.Scope == "version" {
appDetail, err := appDetailRepo.GetFirst(repo.WithByID(ignore.AppDetailID)) appDetail, err := appDetailRepo.GetFirst(repo.WithByID(ignore.AppDetailID))
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
_ = appIgnoreUpgradeRepo.Delete(repo.WithByID(ignore.ID)) if !isDemoReadOnly(readOnly...) {
_ = appIgnoreUpgradeRepo.Delete(repo.WithByID(ignore.ID))
}
continue continue
} }
dto.Version = appDetail.Version dto.Version = appDetail.Version
+48 -127
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"maps"
"math" "math"
"net/http" "net/http"
"os" "os"
@@ -14,14 +13,12 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto" "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/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response" "github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo" "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/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
@@ -45,21 +42,21 @@ type IAppInstallService interface {
Page(req request.AppInstalledSearch) (int64, []response.AppInstallDTO, error) Page(req request.AppInstalledSearch) (int64, []response.AppInstallDTO, error)
CheckExist(req request.AppInstalledInfo) (*response.AppInstalledCheck, error) CheckExist(req request.AppInstalledInfo) (*response.AppInstalledCheck, error)
LoadPort(req dto.OperationWithNameAndType) (int64, error) LoadPort(req dto.OperationWithNameAndType) (int64, error)
LoadConnInfo(req dto.OperationWithNameAndType) (response.DatabaseConn, error) LoadConnInfo(req dto.OperationWithNameAndType, readOnly ...bool) (response.DatabaseConn, error)
SearchForWebsite(req request.AppInstalledSearch) ([]response.AppInstallDTO, error) SearchForWebsite(req request.AppInstalledSearch) ([]response.AppInstallDTO, error)
Operate(req request.AppInstalledOperate) error Operate(req request.AppInstalledOperate) error
Update(req request.AppInstalledUpdate) error Update(req request.AppInstalledUpdate) error
SyncAll(systemInit bool) error SyncAll(systemInit bool) error
GetServices(key string) ([]response.AppService, error) GetServices(key string) ([]response.AppService, error)
GetUpdateVersions(req request.AppUpdateVersion) ([]dto.AppVersion, error) GetUpdateVersions(req request.AppUpdateVersion) ([]dto.AppVersion, error)
GetParams(id uint) (*response.AppConfig, error) GetParams(id uint, readOnly ...bool) (*response.AppConfig, error)
ChangeAppPort(req request.PortUpdate) error ChangeAppPort(req request.PortUpdate) error
GetDefaultConfigByKey(key, name string) (string, error) GetDefaultConfigByKey(key, name string) (string, error)
DeleteCheck(installId uint) ([]dto.AppResource, error) DeleteCheck(installId uint, readOnly ...bool) ([]dto.AppResource, error)
UpdateAppConfig(req request.AppConfigUpdate) error UpdateAppConfig(req request.AppConfigUpdate) error
GetInstallList() ([]dto.AppInstallInfo, error) GetInstallList() ([]dto.AppInstallInfo, error)
GetAppInstallInfo(appInstallID uint) (*response.AppInstallInfo, error) GetAppInstallInfo(appInstallID uint, readOnly ...bool) (*response.AppInstallInfo, error)
UpdateSort(req request.AppInstallSort) error UpdateSort(req request.AppInstallSort) error
} }
@@ -126,7 +123,7 @@ func (a *AppInstallService) Page(req request.AppInstalledSearch) (int64, []respo
} }
} }
installDTOs, _ := handleInstalled(installs, req.Update, req.Sync, req.CheckUpdate) installDTOs, _ := handleInstalled(installs, req.Update, req.Sync && !req.ReadOnly, req.CheckUpdate, req.ReadOnly)
if req.Update { if req.Update {
total = int64(len(installDTOs)) total = int64(len(installDTOs))
} }
@@ -154,8 +151,10 @@ func (a *AppInstallService) CheckExist(req request.AppInstalledInfo) (*response.
if reflect.DeepEqual(appInstall, model.AppInstall{}) { if reflect.DeepEqual(appInstall, model.AppInstall{}) {
return res, nil return res, nil
} }
if err = syncAppInstallStatus(&appInstall, false); err != nil { if !req.ReadOnly {
return nil, err if err = syncAppInstallStatus(&appInstall, false); err != nil {
return nil, err
}
} }
res.ContainerName = appInstall.ContainerName res.ContainerName = appInstall.ContainerName
@@ -185,7 +184,7 @@ func (a *AppInstallService) LoadPort(req dto.OperationWithNameAndType) (int64, e
return app.Port, nil return app.Port, nil
} }
func (a *AppInstallService) LoadConnInfo(req dto.OperationWithNameAndType) (response.DatabaseConn, error) { func (a *AppInstallService) LoadConnInfo(req dto.OperationWithNameAndType, readOnly ...bool) (response.DatabaseConn, error) {
var data response.DatabaseConn var data response.DatabaseConn
app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name) app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name)
if err != nil { if err != nil {
@@ -194,6 +193,9 @@ func (a *AppInstallService) LoadConnInfo(req dto.OperationWithNameAndType) (resp
data.Status = app.Status data.Status = app.Status
data.Username = app.UserName data.Username = app.UserName
data.Password = app.Password data.Password = app.Password
if isDemoReadOnly(readOnly...) {
data.Password = ""
}
data.ServiceName = app.ServiceName data.ServiceName = app.ServiceName
data.Port = app.Port data.Port = app.Port
data.ContainerName = app.ContainerName data.ContainerName = app.ContainerName
@@ -243,7 +245,7 @@ func (a *AppInstallService) SearchForWebsite(req request.AppInstalledSearch) ([]
} }
} }
return handleInstalled(installs, false, true, false) return handleInstalled(installs, false, !req.ReadOnly, false, req.ReadOnly)
} }
func (a *AppInstallService) Operate(req request.AppInstalledOperate) error { func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
@@ -255,9 +257,6 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
return buserr.New("ErrInstallDirNotFound") return buserr.New("ErrInstallDirNotFound")
} }
dockerComposePath := install.GetComposePath() 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 { switch req.Operate {
case constant.Rebuild: case constant.Rebuild:
return rebuildApp(install) return rebuildApp(install)
@@ -281,13 +280,12 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
return syncAppInstallStatus(&install, false) return syncAppInstallStatus(&install, false)
case constant.Delete: case constant.Delete:
deleteReq := request.AppInstallDelete{ deleteReq := request.AppInstallDelete{
Install: install, Install: install,
DeleteBackup: req.DeleteBackup, DeleteBackup: req.DeleteBackup,
ForceDelete: req.ForceDelete, ForceDelete: req.ForceDelete,
DeleteDB: req.DeleteDB, DeleteDB: req.DeleteDB,
DeleteImage: req.DeleteImage, DeleteImage: req.DeleteImage,
TaskID: req.TaskID, TaskID: req.TaskID,
UseLifecycleScripts: req.UseLifecycleScripts,
} }
if err = deleteAppInstall(deleteReq); err != nil && !req.ForceDelete { if err = deleteAppInstall(deleteReq); err != nil && !req.ForceDelete {
return err return err
@@ -319,70 +317,6 @@ 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 { func (a *AppInstallService) UpdateAppConfig(req request.AppConfigUpdate) error {
installed, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID)) installed, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
if err != nil { if err != nil {
@@ -445,10 +379,8 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
return err return err
} }
} }
if !req.SkipComposeCommonConfig { if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params); err != nil {
if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params); err != nil { return err
return err
}
} }
composeByte, err := yaml.Marshal(composeMap) composeByte, err := yaml.Marshal(composeMap)
if err != nil { if err != nil {
@@ -481,7 +413,7 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
if err != nil { if err != nil {
return err return err
} }
backupEnvMaps := maps.Clone(oldEnvMaps) backupEnvMaps := oldEnvMaps
handleMap(req.Params, oldEnvMaps) handleMap(req.Params, oldEnvMaps)
paramByte, err := json.Marshal(oldEnvMaps) paramByte, err := json.Marshal(oldEnvMaps)
if err != nil { if err != nil {
@@ -493,32 +425,13 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
} }
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
_ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(installed.DockerCompose), constant.DirPerm) _ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(installed.DockerCompose), constant.DirPerm)
restoreConfig := func(operationErr error) { if err := rebuildApp(installed); err != nil {
_ = env.Write(backupEnvMaps, envPath) _ = env.Write(backupEnvMaps, envPath)
_ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(backupDockerCompose), constant.DirPerm) _ = 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 return err
} }
if !req.UseLifecycleScripts { installed.Status = constant.StatusRunning
installed.Status = constant.StatusRunning _ = appInstallRepo.Save(context.Background(), &installed)
_ = appInstallRepo.Save(context.Background(), &installed)
}
proxyChanged := hasAppInstallProxyPassChanged(&oldInstalled, &installed) proxyChanged := hasAppInstallProxyPassChanged(&oldInstalled, &installed)
currentProxy, currentProxyErr := getAppInstallProxyPass(&installed) currentProxy, currentProxyErr := getAppInstallProxyPass(&installed)
@@ -675,9 +588,6 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
return versions, err return versions, err
} }
for _, detail := range details { for _, detail := range details {
if !canAccessVllmVersion(app.Key, detail.Version) {
continue
}
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version")) ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version"))
if len(ignores) > 0 { if len(ignores) > 0 {
continue continue
@@ -759,7 +669,7 @@ func (a *AppInstallService) ChangeAppPort(req request.PortUpdate) error {
return nil return nil
} }
func (a *AppInstallService) DeleteCheck(installID uint) ([]dto.AppResource, error) { func (a *AppInstallService) DeleteCheck(installID uint, readOnly ...bool) ([]dto.AppResource, error) {
var res []dto.AppResource var res []dto.AppResource
appInstall, err := appInstallRepo.GetFirst(repo.WithByID(installID)) appInstall, err := appInstallRepo.GetFirst(repo.WithByID(installID))
if err != nil { if err != nil {
@@ -780,7 +690,7 @@ func (a *AppInstallService) DeleteCheck(installID uint) ([]dto.AppResource, erro
Type: "app", Type: "app",
Name: linkInstall.Name, Name: linkInstall.Name,
}) })
} else { } else if !isDemoReadOnly(readOnly...) {
_ = appInstallResourceRepo.DeleteBy(context.Background(), appInstallResourceRepo.WithAppInstallId(resource.AppInstallId)) _ = appInstallResourceRepo.DeleteBy(context.Background(), appInstallResourceRepo.WithAppInstallId(resource.AppInstallId))
} }
} }
@@ -818,7 +728,7 @@ func (a *AppInstallService) GetDefaultConfigByKey(key, name string) (string, err
return string(contentByte), nil return string(contentByte), nil
} }
func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) { func (a *AppInstallService) GetParams(id uint, readOnly ...bool) (*response.AppConfig, error) {
var ( var (
params []response.AppParam params []response.AppParam
appForm dto.AppForm appForm dto.AppForm
@@ -913,8 +823,14 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
} }
} }
readOnlyMode := isDemoReadOnly(readOnly...)
if readOnlyMode {
redactSensitiveAppParams(params)
}
config := getAppCommonConfig(envs) config := getAppCommonConfig(envs)
config.DockerCompose = install.DockerCompose if !readOnlyMode {
config.DockerCompose = install.DockerCompose
}
res.Params = params res.Params = params
if config.ContainerName == "" { if config.ContainerName == "" {
config.ContainerName = install.ContainerName config.ContainerName = install.ContainerName
@@ -924,16 +840,16 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
res.RestartPolicy = getRestartPolicy(install.DockerCompose) res.RestartPolicy = getRestartPolicy(install.DockerCompose)
res.WebUI = install.WebUI res.WebUI = install.WebUI
res.Type = install.App.Type res.Type = install.App.Type
if rawCompose, err := getUpgradeCompose(install, detail); err == nil { if !readOnlyMode {
res.RawCompose = rawCompose if rawCompose, err := getUpgradeCompose(install, detail); err == nil {
res.RawCompose = rawCompose
}
} }
return &res, nil return &res, nil
} }
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error { func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
switch appInstall.Status { if appInstall.Status == constant.StatusInstalling || appInstall.Status == constant.StatusRebuilding || appInstall.Status == constant.StatusUpgrading || appInstall.Status == constant.StatusUninstalling {
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
return nil return nil
} }
cli, err := docker.NewClient() cli, err := docker.NewClient()
@@ -1055,20 +971,25 @@ func updateInstallInfoInDB(appKey, appName, param string, value interface{}) err
return nil return nil
} }
func (a *AppInstallService) GetAppInstallInfo(installID uint) (*response.AppInstallInfo, error) { func (a *AppInstallService) GetAppInstallInfo(installID uint, readOnly ...bool) (*response.AppInstallInfo, error) {
appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(installID)) appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(installID))
if appInstall.ID == 0 { if appInstall.ID == 0 {
return &response.AppInstallInfo{ return &response.AppInstallInfo{
Status: constant.StatusDeleted, Status: constant.StatusDeleted,
}, nil }, nil
} }
_ = syncAppInstallStatus(&appInstall, false) if !isDemoReadOnly(readOnly...) {
_ = syncAppInstallStatus(&appInstall, false)
}
appInstall, _ = appInstallRepo.GetFirst(repo.WithByID(installID)) appInstall, _ = appInstallRepo.GetFirst(repo.WithByID(installID))
var envMap map[string]interface{} var envMap map[string]interface{}
err := json.Unmarshal([]byte(appInstall.Env), &envMap) err := json.Unmarshal([]byte(appInstall.Env), &envMap)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if isDemoReadOnly(readOnly...) {
redactSensitiveValues(envMap)
}
res := &response.AppInstallInfo{ res := &response.AppInstallInfo{
ID: appInstall.ID, ID: appInstall.ID,
Name: appInstall.Name, Name: appInstall.Name,
-24
View File
@@ -107,9 +107,6 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil { if err != nil {
return err 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)) { 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") return errors.New("vLLM can only upgrade within the same image type")
} }
@@ -462,13 +459,6 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
}); err != nil { }); err != nil {
return err 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 { } else if err = appInstallRepo.Save(context.Background(), &u.candidate); err != nil {
return err return err
} }
@@ -725,20 +715,6 @@ func renderUpgradeEnv(install *model.AppInstall, original []byte) ([]byte, error
return nil, err return nil, err
} }
handleMap(envs, params) 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 { if install.App.Key == constant.AppOpenresty {
for _, key := range []string{"CONTAINER_PACKAGE_URL", "RESTY_ADD_PACKAGE_BUILDDEPS", "RESTY_CONFIG_OPTIONS_MORE"} { for _, key := range []string{"CONTAINER_PACKAGE_URL", "RESTY_ADD_PACKAGE_BUILDDEPS", "RESTY_CONFIG_OPTIONS_MORE"} {
if value, ok := originalEnv[key]; ok { if value, ok := originalEnv[key]; ok {
+79 -37
View File
@@ -53,6 +53,62 @@ var (
Delete DatabaseOp = "delete" Delete DatabaseOp = "delete"
) )
func isDemoReadOnly(readOnly ...bool) bool {
return global.CONF.Base.IsDemo || len(readOnly) > 0 && readOnly[0]
}
func normalizeConfigKey(key string) string {
return strings.ToLower(strings.NewReplacer("_", "", "-", "", ".", "", " ", "").Replace(key))
}
func isSensitiveConfigKey(key string) bool {
normalized := normalizeConfigKey(key)
for _, marker := range []string{"password", "passwd", "passphrase", "secret", "token", "credential", "privatekey", "authorization"} {
if strings.Contains(normalized, marker) {
return true
}
}
return normalized == "key" || strings.HasSuffix(normalized, "key")
}
func redactSensitiveValues(values map[string]interface{}) {
redactSensitiveData(values)
}
func redactSensitiveData(value interface{}) {
switch data := value.(type) {
case map[string]interface{}:
for key, item := range data {
if isSensitiveConfigKey(key) {
data[key] = ""
} else {
redactSensitiveData(item)
}
}
case []interface{}:
for _, item := range data {
redactSensitiveData(item)
}
}
}
func redactSensitiveAppParams(params []response.AppParam) {
for i := range params {
if strings.EqualFold(params[i].Type, "password") || isSensitiveConfigKey(params[i].Key) {
params[i].Value = ""
params[i].ShowValue = ""
}
}
}
func redactSensitiveEnvironments(environments []request.Environment) {
for i := range environments {
if isSensitiveConfigKey(environments[i].Key) {
environments[i].Value = ""
}
}
}
func checkPort(key string, params map[string]interface{}) (int, error) { func checkPort(key string, params map[string]interface{}) (int, error) {
port, ok := params[key] port, ok := params[key]
if ok { if ok {
@@ -353,21 +409,15 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("App") logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("App")
t.Log(logStr) t.Log(logStr)
if deleteReq.UseLifecycleScripts { out, err := compose.Down(install.GetComposePath())
if err = runScript(t, &install, "uninstall"); err != nil { if err != nil && !deleteReq.ForceDelete {
return err return handleErr(install, err, out)
}
} else {
out, err := compose.Down(install.GetComposePath())
if err != nil && !deleteReq.ForceDelete {
return handleErr(install, err, out)
}
if err = runScript(t, &install, "uninstall"); err != nil {
_, _ = compose.Up(install.GetComposePath())
return err
}
} }
t.LogSuccess(logStr) t.LogSuccess(logStr)
if err = runScript(t, &install, "uninstall"); err != nil {
_, _ = compose.Up(install.GetComposePath())
return err
}
if deleteReq.DeleteImage { if deleteReq.DeleteImage {
content, err := op.GetContent(install.GetEnvPath()) content, err := op.GetContent(install.GetEnvPath())
if err != nil { if err != nil {
@@ -1005,12 +1055,6 @@ func runScript(task *task.Task, appInstall *model.AppInstall, operate string) er
scriptPath = path.Join(workDir, "scripts", "upgrade.sh") scriptPath = path.Join(workDir, "scripts", "upgrade.sh")
case "uninstall": case "uninstall":
scriptPath = path.Join(workDir, "scripts", "uninstall.sh") 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() fileOp := files.NewFileOp()
if !fileOp.Stat(scriptPath) { if !fileOp.Stat(scriptPath) {
@@ -1020,11 +1064,7 @@ func runScript(task *task.Task, appInstall *model.AppInstall, operate string) er
logStr := i18n.GetWithName("ExecShell", operate) logStr := i18n.GetWithName("ExecShell", operate)
task.LogStart(logStr) task.LogStart(logStr)
timeout := 10 * time.Minute cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Minute), cmd.WithWorkDir(workDir))
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 { if err := cmdMgr.Run("bash", scriptPath); err != nil {
task.LogFailedWithErr(logStr, err) task.LogFailedWithErr(logStr, err)
return err return err
@@ -1059,15 +1099,12 @@ func checkContainerNameIsExist(containerName, appDir string) (bool, error) {
return false, nil return false, nil
} }
func upApp(task *task.Task, appInstall *model.AppInstall, pullImages, useLifecycleScripts bool) error { func upApp(task *task.Task, appInstall *model.AppInstall, pullImages bool) error {
upProject := func(appInstall *model.AppInstall) (err error) { upProject := func(appInstall *model.AppInstall) (err error) {
var ( var (
out string out string
errMsg string errMsg string
) )
if useLifecycleScripts {
return runScript(task, appInstall, "start")
}
if pullImages && appInstall.App.Type != "php" { if pullImages && appInstall.App.Type != "php" {
envByte, err := files.NewFileOp().GetContent(appInstall.GetEnvPath()) envByte, err := files.NewFileOp().GetContent(appInstall.GetEnvPath())
if err != nil { if err != nil {
@@ -1394,8 +1431,7 @@ func handleErr(install model.AppInstall, err error, out string) error {
func doNotNeedSync(installed model.AppInstall) bool { func doNotNeedSync(installed model.AppInstall) bool {
return installed.Status == constant.StatusInstalling || installed.Status == constant.StatusRebuilding || installed.Status == constant.StatusUpgrading || 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) { func synAppInstall(containers map[string]container.Summary, appInstall *model.AppInstall, force bool) {
@@ -1407,7 +1443,9 @@ func synAppInstall(containers map[string]container.Summary, appInstall *model.Ap
} }
appInstall.Status = constant.StatusError appInstall.Status = constant.StatusError
appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(containerNames, ",")).Error() appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(containerNames, ",")).Error()
_ = appInstallRepo.Save(context.Background(), appInstall) if !global.CONF.Base.IsDemo {
_ = appInstallRepo.Save(context.Background(), appInstall)
}
return return
} }
notFoundNames := make([]string, 0) notFoundNames := make([]string, 0)
@@ -1466,10 +1504,12 @@ func synAppInstall(containers map[string]container.Summary, appInstall *model.Ap
appInstall.Message = msg appInstall.Message = msg
appInstall.Status = constant.StatusUnHealthy appInstall.Status = constant.StatusUnHealthy
} }
_ = appInstallRepo.Save(context.Background(), appInstall) if !global.CONF.Base.IsDemo {
_ = appInstallRepo.Save(context.Background(), appInstall)
}
} }
func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpdate bool) ([]response.AppInstallDTO, error) { func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpdate, readOnly bool) ([]response.AppInstallDTO, error) {
var ( var (
res []response.AppInstallDTO res []response.AppInstallDTO
containersMap map[string]container.Summary containersMap map[string]container.Summary
@@ -1500,6 +1540,9 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
resourceKeys := getAppInstallResourceKeys(installed.ID) resourceKeys := getAppInstallResourceKeys(installed.ID)
envMap := make(map[string]interface{}) envMap := make(map[string]interface{})
_ = json.Unmarshal([]byte(installed.Env), &envMap) _ = json.Unmarshal([]byte(installed.Env), &envMap)
if isDemoReadOnly(readOnly) {
redactSensitiveValues(envMap)
}
installDTO := response.AppInstallDTO{ installDTO := response.AppInstallDTO{
ID: installed.ID, ID: installed.ID,
Name: installed.Name, Name: installed.Name,
@@ -1544,7 +1587,9 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
continue continue
} }
installDTO.DockerCompose = installed.DockerCompose if !isDemoReadOnly(readOnly) {
installDTO.DockerCompose = installed.DockerCompose
}
installDTO.IsEdit = isEditCompose(installed) installDTO.IsEdit = isEditCompose(installed)
details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(installed.App.ID)) details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(installed.App.ID))
@@ -2249,9 +2294,6 @@ func getAppVersions(key string, details []model.AppDetail) []string {
hasLatest := false hasLatest := false
latestVersion := "" latestVersion := ""
for _, detail := range details { for _, detail := range details {
if !canAccessVllmVersion(key, detail.Version) {
continue
}
if key != "mssql" && strings.Contains(detail.Version, "latest") { if key != "mssql" && strings.Contains(detail.Version, "latest") {
hasLatest = true hasLatest = true
latestVersion = detail.Version latestVersion = detail.Version
+27 -2
View File
@@ -33,7 +33,7 @@ type IBackupService interface {
CheckUsed(name string, isPublic bool) error CheckUsed(name string, isPublic bool) error
LoadBackupOptions() ([]dto.BackupOption, error) LoadBackupOptions() ([]dto.BackupOption, error)
SearchWithPage(search dto.SearchPageWithType) (int64, interface{}, error) SearchWithPage(search dto.SearchPageWithType, readOnly ...bool) (int64, interface{}, error)
Create(backupDto dto.BackupOperate) error Create(backupDto dto.BackupOperate) error
CheckConn(req dto.BackupOperate) dto.BackupCheckRes CheckConn(req dto.BackupOperate) dto.BackupCheckRes
GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error) GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error)
@@ -80,7 +80,7 @@ func (u *BackupService) GetLocalDir() (string, error) {
return account.BackupPath, nil return account.BackupPath, nil
} }
func (u *BackupService) SearchWithPage(req dto.SearchPageWithType) (int64, interface{}, error) { func (u *BackupService) SearchWithPage(req dto.SearchPageWithType, readOnly ...bool) (int64, interface{}, error) {
options := []repo.DBOption{repo.WithOrderDesc("created_at")} options := []repo.DBOption{repo.WithOrderDesc("created_at")}
if len(req.Type) != 0 { if len(req.Type) != 0 {
options = append(options, repo.WithByType(req.Type)) options = append(options, repo.WithByType(req.Type))
@@ -128,11 +128,36 @@ func (u *BackupService) SearchWithPage(req dto.SearchPageWithType) (int64, inter
itemVars, _ := json.Marshal(varMap) itemVars, _ := json.Marshal(varMap)
item.Vars = string(itemVars) item.Vars = string(itemVars)
} }
if isDemoReadOnly(readOnly...) {
item.AccessKey = ""
item.Credential = ""
item.Vars = sanitizeBackupVars(item.Vars)
}
data = append(data, item) data = append(data, item)
} }
return count, data, nil return count, data, nil
} }
func sanitizeBackupVars(vars string) string {
if vars == "" {
return vars
}
var values map[string]interface{}
if err := json.Unmarshal([]byte(vars), &values); err != nil {
return ""
}
for key := range values {
if isSensitiveConfigKey(key) || normalizeConfigKey(key) == "code" {
delete(values, key)
}
}
data, err := json.Marshal(values)
if err != nil {
return ""
}
return string(data)
}
func (u *BackupService) CheckConn(req dto.BackupOperate) dto.BackupCheckRes { func (u *BackupService) CheckConn(req dto.BackupOperate) dto.BackupCheckRes {
var res dto.BackupCheckRes var res dto.BackupCheckRes
var backup model.BackupAccount var backup model.BackupAccount
+54 -28
View File
@@ -582,10 +582,6 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
if config.Image == "" { if config.Image == "" {
return fmt.Errorf("container image not found in backup file") 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 !checkImageExist(recoverCtx.client, config.Image) {
if err := pullImages(taskItem, recoverCtx.client, config.Image); err != nil { if err := pullImages(taskItem, recoverCtx.client, config.Image); err != nil {
return err return err
@@ -600,7 +596,7 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
return err return err
} }
createRes, err := createContainerWithNetworks(ctx, recoverCtx.client, config, hostConfig, networkConf, extraNetworks, recoverCtx.targetName) createRes, err := createContainerWithOldNetworks(ctx, recoverCtx.client, config, hostConfig, recoverCtx.inspectInfo.NetworkSettings, recoverCtx.targetName)
if err != nil { if err != nil {
return err return err
} }
@@ -608,7 +604,7 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
return nil return nil
} }
func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) error { func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
if cli.NewVersionError(ctx, "1.44", "specify mac-address per network") != nil { if cli.NewVersionError(ctx, "1.44", "specify mac-address per network") != nil {
removeEndpointMacAddresses(primary, extras) removeEndpointMacAddresses(primary, extras)
} }
@@ -623,14 +619,11 @@ func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client,
} }
info, err := cli.NetworkInspect(ctx, netName, network.InspectOptions{}) info, err := cli.NetworkInspect(ctx, netName, network.InspectOptions{})
if err != nil { if err != nil {
return fmt.Errorf("inspect network %s failed: %w", netName, err) continue
}
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) { func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
@@ -648,28 +641,24 @@ func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[st
} }
} }
func validateContainerEndpointStaticIP(netName string, info network.Inspect, endpoint *network.EndpointSettings) error { func removeUnsupportedEndpointStaticIP(netName string, info network.Inspect, endpoint *network.EndpointSettings) {
if endpoint == nil || endpoint.IPAMConfig == nil { if endpoint == nil || endpoint.IPAMConfig == nil {
return nil return
} }
ipam := endpoint.IPAMConfig if isDefaultBridgeNetwork(netName, info) {
if err := ipam.Validate(); err != nil { endpoint.IPAMConfig = nil
return fmt.Errorf("invalid IP configuration for network %s: %w", netName, err) return
}
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 ipam.IPv4Address != "" && !networkSupportsStaticIP(info, ipam.IPv4Address, false) { if endpoint.IPAMConfig.IPv4Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv4Address, false) {
return fmt.Errorf("static IPv4 address %s is not in a configured subnet of network %s", ipam.IPv4Address, netName) endpoint.IPAMConfig.IPv4Address = ""
} }
if ipam.IPv6Address != "" && !networkSupportsStaticIP(info, ipam.IPv6Address, true) { if endpoint.IPAMConfig.IPv6Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv6Address, true) {
return fmt.Errorf("static IPv6 address %s is not in a configured subnet of network %s", ipam.IPv6Address, netName) endpoint.IPAMConfig.IPv6Address = ""
}
if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" && len(endpoint.IPAMConfig.LinkLocalIPs) == 0 {
endpoint.IPAMConfig = nil
} }
return nil
} }
func isDefaultBridgeNetwork(netName string, info network.Inspect) bool { func isDefaultBridgeNetwork(netName string, info network.Inspect) bool {
@@ -684,7 +673,6 @@ func networkSupportsStaticIP(info network.Inspect, ip string, isIPv6 bool) bool
if err != nil { if err != nil {
return false return false
} }
addr = addr.Unmap()
if addr.Is6() != isIPv6 { if addr.Is6() != isIPv6 {
return false return false
} }
@@ -825,6 +813,11 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
IPv6Address: endpoint.IPAMConfig.IPv6Address, IPv6Address: endpoint.IPAMConfig.IPv6Address,
LinkLocalIPs: append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...), 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 { if name == primaryName {
config.EndpointsConfig[name] = endpointSetting config.EndpointsConfig[name] = endpointSetting
@@ -838,6 +831,39 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
return config, extraNetworks 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 { func cloneContainerConfig(config *container.Config) *container.Config {
if config == nil { if config == nil {
return &container.Config{} return &container.Config{}
+3 -3
View File
@@ -33,7 +33,7 @@ type ClamService struct {
} }
type IClamService interface { type IClamService interface {
LoadBaseInfo() (dto.ClamBaseInfo, error) LoadBaseInfo(readOnly bool) (dto.ClamBaseInfo, error)
Operate(operate string) error Operate(operate string) error
SearchWithPage(search dto.SearchClamWithPage) (int64, interface{}, error) SearchWithPage(search dto.SearchClamWithPage) (int64, interface{}, error)
Create(req dto.ClamCreate, operator string) error Create(req dto.ClamCreate, operator string) error
@@ -53,7 +53,7 @@ func NewIClamService() IClamService {
return &ClamService{} return &ClamService{}
} }
func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) { func (c *ClamService) LoadBaseInfo(readOnly bool) (dto.ClamBaseInfo, error) {
var baseInfo dto.ClamBaseInfo var baseInfo dto.ClamBaseInfo
baseInfo.Version = "-" baseInfo.Version = "-"
baseInfo.FreshVersion = "-" baseInfo.FreshVersion = "-"
@@ -96,7 +96,7 @@ func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
baseInfo.Version = strings.TrimPrefix(version, "ClamAV ") baseInfo.Version = strings.TrimPrefix(version, "ClamAV ")
} }
} }
} else { } else if !readOnly && !global.CONF.Base.IsDemo {
_ = clam.CheckWithStopAll(false, clamRepo) _ = clam.CheckWithStopAll(false, clamRepo)
} }
if baseInfo.FreshIsActive { if baseInfo.FreshIsActive {
+10 -4
View File
@@ -11,8 +11,8 @@ import (
type ComposeTemplateService struct{} type ComposeTemplateService struct{}
type IComposeTemplateService interface { type IComposeTemplateService interface {
List() ([]dto.ComposeTemplateInfo, error) List(readOnly ...bool) ([]dto.ComposeTemplateInfo, error)
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error) SearchWithPage(search dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error)
Create(req dto.ComposeTemplateCreate) error Create(req dto.ComposeTemplateCreate) error
Update(id uint, upMap map[string]interface{}) error Update(id uint, upMap map[string]interface{}) error
Batch(req dto.ComposeTemplateBatch) error Batch(req dto.ComposeTemplateBatch) error
@@ -23,7 +23,7 @@ func NewIComposeTemplateService() IComposeTemplateService {
return &ComposeTemplateService{} return &ComposeTemplateService{}
} }
func (u *ComposeTemplateService) List() ([]dto.ComposeTemplateInfo, error) { func (u *ComposeTemplateService) List(readOnly ...bool) ([]dto.ComposeTemplateInfo, error) {
composes, err := composeRepo.List() composes, err := composeRepo.List()
if err != nil { if err != nil {
return nil, buserr.New("ErrRecordNotFound") return nil, buserr.New("ErrRecordNotFound")
@@ -34,12 +34,15 @@ func (u *ComposeTemplateService) List() ([]dto.ComposeTemplateInfo, error) {
if err := copier.Copy(&item, &compose); err != nil { if err := copier.Copy(&item, &compose); err != nil {
return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
} }
if isDemoReadOnly(readOnly...) {
item.Content = ""
}
dtoLists = append(dtoLists, item) dtoLists = append(dtoLists, item)
} }
return dtoLists, err return dtoLists, err
} }
func (u *ComposeTemplateService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) { func (u *ComposeTemplateService) SearchWithPage(req dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error) {
total, composes, err := composeRepo.Page(req.Page, req.PageSize, repo.WithByLikeName(req.Info)) total, composes, err := composeRepo.Page(req.Page, req.PageSize, repo.WithByLikeName(req.Info))
var dtoComposeTemplates []dto.ComposeTemplateInfo var dtoComposeTemplates []dto.ComposeTemplateInfo
for _, compose := range composes { for _, compose := range composes {
@@ -47,6 +50,9 @@ func (u *ComposeTemplateService) SearchWithPage(req dto.SearchWithPage) (int64,
if err := copier.Copy(&item, &compose); err != nil { if err := copier.Copy(&item, &compose); err != nil {
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
} }
if isDemoReadOnly(readOnly...) {
item.Content = ""
}
dtoComposeTemplates = append(dtoComposeTemplates, item) dtoComposeTemplates = append(dtoComposeTemplates, item)
} }
return total, dtoComposeTemplates, err return total, dtoComposeTemplates, err
+137 -63
View File
@@ -16,7 +16,6 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"regexp" "regexp"
"slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -25,7 +24,6 @@ import (
"time" "time"
"github.com/1Panel-dev/1Panel/agent/app/dto" "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/repo"
"github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
@@ -67,7 +65,7 @@ type IContainerService interface {
PageVolume(req dto.SearchWithPage) (int64, interface{}, error) PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
ListVolume() ([]dto.Options, error) ListVolume() ([]dto.Options, error)
PageCompose(req dto.SearchWithPage) (int64, interface{}, error) PageCompose(req dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error)
LoadComposeEnv(name string) (string, error) LoadComposeEnv(name string) (string, error)
CreateCompose(req dto.ComposeCreate) error CreateCompose(req dto.ComposeCreate) error
ComposeOperation(req dto.ComposeOperation) error ComposeOperation(req dto.ComposeOperation) error
@@ -79,9 +77,9 @@ type IContainerService interface {
ContainerCreate(req dto.ContainerOperate, inThread bool) error ContainerCreate(req dto.ContainerOperate, inThread bool) error
ContainerUpdate(req dto.ContainerOperate) error ContainerUpdate(req dto.ContainerOperate) error
ContainerUpgrade(req dto.ContainerUpgrade) error ContainerUpgrade(req dto.ContainerUpgrade) error
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error) ContainerInfo(req dto.OperationWithName, readOnly ...bool) (*dto.ContainerOperate, error)
ContainerListStats() ([]dto.ContainerListStats, error) ContainerListStats() ([]dto.ContainerListStats, error)
ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error) ContainerItemStats(req dto.OperationWithName) (dto.ContainerItemStats, error)
LoadResourceLimit() (*dto.ResourceLimit, error) LoadResourceLimit() (*dto.ResourceLimit, error)
ContainerRename(req dto.ContainerRename) error ContainerRename(req dto.ContainerRename) error
ContainerCommit(req dto.ContainerCommit) error ContainerCommit(req dto.ContainerCommit) error
@@ -248,15 +246,15 @@ func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
} }
return data, nil return data, nil
} }
func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error) { func (u *ContainerService) ContainerItemStats(req dto.OperationWithName) (dto.ContainerItemStats, error) {
var data dto.ContainerItemStats var data dto.ContainerItemStats
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
return data, err return data, err
} }
defer client.Close()
if req.Name != "system" { if req.Name != "system" {
containerInfo, _, err := client.ContainerInspectWithRaw(ctx, req.Name, true) defer client.Close()
containerInfo, _, err := client.ContainerInspectWithRaw(context.Background(), req.Name, true)
if err != nil { if err != nil {
return data, err return data, err
} }
@@ -265,7 +263,7 @@ func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.Opera
return data, nil return data, nil
} }
usage, err := client.DiskUsage(ctx, types.DiskUsageOptions{}) usage, err := client.DiskUsage(context.Background(), types.DiskUsageOptions{})
if err != nil { if err != nil {
return data, err return data, err
} }
@@ -536,9 +534,7 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
if err != nil { if err != nil {
return err return err
} }
if err := normalizeContainerEndpointSettings(ctx, client, networkConf, nil); err != nil { normalizeContainerEndpointSettings(ctx, client, networkConf, nil)
return err
}
con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name) con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
if err != nil { if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed")) taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
@@ -572,7 +568,7 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
return taskItem.Execute() return taskItem.Execute()
} }
func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error) { func (u *ContainerService) ContainerInfo(req dto.OperationWithName, readOnly ...bool) (*dto.ContainerOperate, error) {
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -616,6 +612,9 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
data.Tty = oldContainer.Config.Tty data.Tty = oldContainer.Config.Tty
data.Entrypoint = oldContainer.Config.Entrypoint data.Entrypoint = oldContainer.Config.Entrypoint
data.Env = oldContainer.Config.Env data.Env = oldContainer.Config.Env
if isDemoReadOnly(readOnly...) {
data.Env = nil
}
data.CPUShares = oldContainer.HostConfig.CPUShares data.CPUShares = oldContainer.HostConfig.CPUShares
for key, val := range oldContainer.Config.Labels { for key, val := range oldContainer.Config.Labels {
data.Labels = append(data.Labels, fmt.Sprintf("%s=%s", key, val)) data.Labels = append(data.Labels, fmt.Sprintf("%s=%s", key, val))
@@ -648,9 +647,14 @@ func loadContainerNetworkInfo(name string, endpoint *network.EndpointSettings) d
if endpoint.IPAMConfig != nil { if endpoint.IPAMConfig != nil {
item.LinkLocalIPs = append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...) item.LinkLocalIPs = append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...)
} }
if name != "bridge" && endpoint.IPAMConfig != nil { if name != "bridge" {
item.Ipv4 = endpoint.IPAMConfig.IPv4Address if endpoint.IPAMConfig != nil {
item.Ipv6 = endpoint.IPAMConfig.IPv6Address item.Ipv4 = endpoint.IPAMConfig.IPv4Address
item.Ipv6 = endpoint.IPAMConfig.IPv6Address
} else {
item.Ipv4 = endpoint.IPAddress
item.Ipv6 = endpoint.GlobalIPv6Address
}
} }
return item return item
} }
@@ -1677,44 +1681,32 @@ func checkImageLike(client *client.Client, imageName string) bool {
func pullImages(task *task.Task, client *client.Client, imageName string) error { func pullImages(task *task.Task, client *client.Client, imageName string) error {
dockerCli := docker.NewClientWithExist(client) 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{} options := image.PullOptions{}
authConfig := registry.AuthConfig{ repos, _ := imageRepoRepo.List()
Username: imageRepo.Username, if len(repos) != 0 {
Password: imageRepo.Password, 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
}
}
} else {
hasAuth, authStr := loadAuthInfo(imageName)
if hasAuth {
options.RegistryAuth = authStr
}
} }
encodedJSON, err := json.Marshal(authConfig)
if err != nil {
return err
}
options.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON)
return dockerCli.PullImageWithProcessAndOptions(task, imageName, options) 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
}
if len(downloadURL) > selectedURLLength {
selected = &repos[i]
selectedURLLength = len(downloadURL)
}
}
return selected
}
func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerListStats { func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerListStats {
data := dto.ContainerListStats{ data := dto.ContainerListStats{
ContainerID: containerItem, ContainerID: containerItem,
@@ -1769,10 +1761,7 @@ func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error
} }
for i := 0; i <= hostEnd-hostStart; i++ { for i := 0; i <= hostEnd-hostStart; i++ {
bindItem := nat.PortBinding{HostPort: strconv.Itoa(hostStart + i), HostIP: port.HostIP} bindItem := nat.PortBinding{HostPort: strconv.Itoa(hostStart + i), HostIP: port.HostIP}
portKey := nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol)) portMap[nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))] = []nat.PortBinding{bindItem}
if !slices.Contains(portMap[portKey], bindItem) {
portMap[portKey] = append(portMap[portKey], bindItem)
}
} }
for i := hostStart; i <= hostEnd; i++ { for i := hostStart; i <= hostEnd; i++ {
if checkInUse && common.ScanPortWithIP(port.HostIP, i) { if checkInUse && common.ScanPortWithIP(port.HostIP, i) {
@@ -1790,10 +1779,7 @@ func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error
return portMap, buserr.WithDetail("ErrPortInUsed", portItem, nil) return portMap, buserr.WithDetail("ErrPortInUsed", portItem, nil)
} }
bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP} bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP}
portKey := nat.Port(fmt.Sprintf("%s/%s", port.ContainerPort, port.Protocol)) portMap[nat.Port(fmt.Sprintf("%s/%s", port.ContainerPort, port.Protocol))] = []nat.PortBinding{bindItem}
if !slices.Contains(portMap[portKey], bindItem) {
portMap[portKey] = append(portMap[portKey], bindItem)
}
} }
} }
return portMap, nil return portMap, nil
@@ -1939,7 +1925,90 @@ func loadPortByInspect(id string, client *client.Client) ([]container.Port, erro
return itemPorts, nil return itemPorts, nil
} }
func transPortToStr(ports []container.Port) []string { func transPortToStr(ports []container.Port) []string {
return docker.SimplifyPorts(ports) var (
ipv4Ports []container.Port
ipv6Ports []container.Port
)
for _, port := range ports {
if strings.Contains(port.IP, ":") {
ipv6Ports = append(ipv6Ports, port)
} else {
ipv4Ports = append(ipv4Ports, port)
}
}
list1 := simplifyPort(ipv4Ports)
list2 := simplifyPort(ipv6Ports)
return append(list1, list2...)
}
func simplifyPort(ports []container.Port) []string {
var datas []string
if len(ports) == 0 {
return datas
}
if len(ports) == 1 {
ip := ""
if len(ports[0].IP) != 0 {
ip = ports[0].IP + ":"
}
itemPortStr := fmt.Sprintf("%s%v/%s", ip, ports[0].PrivatePort, ports[0].Type)
if ports[0].PublicPort != 0 {
itemPortStr = fmt.Sprintf("%s%v->%v/%s", ip, ports[0].PublicPort, ports[0].PrivatePort, ports[0].Type)
}
datas = append(datas, itemPortStr)
return datas
}
sort.Slice(ports, func(i, j int) bool {
return ports[i].PrivatePort < ports[j].PrivatePort
})
start := ports[0]
for i := 1; i < len(ports); i++ {
if ports[i].PrivatePort != ports[i-1].PrivatePort+1 || ports[i].IP != ports[i-1].IP || ports[i].PublicPort != ports[i-1].PublicPort+1 || ports[i].Type != ports[i-1].Type {
if ports[i-1].PrivatePort == start.PrivatePort {
itemPortStr := fmt.Sprintf("%s:%v/%s", start.IP, start.PrivatePort, start.Type)
if start.PublicPort != 0 {
itemPortStr = fmt.Sprintf("%s:%v->%v/%s", start.IP, start.PublicPort, start.PrivatePort, start.Type)
}
if len(start.IP) == 0 {
itemPortStr = strings.TrimPrefix(itemPortStr, ":")
}
datas = append(datas, itemPortStr)
} else {
itemPortStr := fmt.Sprintf("%s:%v-%v/%s", start.IP, start.PrivatePort, ports[i-1].PrivatePort, start.Type)
if start.PublicPort != 0 {
itemPortStr = fmt.Sprintf("%s:%v-%v->%v-%v/%s", start.IP, start.PublicPort, ports[i-1].PublicPort, start.PrivatePort, ports[i-1].PrivatePort, start.Type)
}
if len(start.IP) == 0 {
itemPortStr = strings.TrimPrefix(itemPortStr, ":")
}
datas = append(datas, itemPortStr)
}
start = ports[i]
}
if i == len(ports)-1 {
if ports[i].PrivatePort == start.PrivatePort {
itemPortStr := fmt.Sprintf("%s:%v/%s", start.IP, start.PrivatePort, start.Type)
if start.PublicPort != 0 {
itemPortStr = fmt.Sprintf("%s:%v->%v/%s", start.IP, start.PublicPort, start.PrivatePort, start.Type)
}
if len(start.IP) == 0 {
itemPortStr = strings.TrimPrefix(itemPortStr, ":")
}
datas = append(datas, itemPortStr)
} else {
itemPortStr := fmt.Sprintf("%s:%v-%v/%s", start.IP, start.PrivatePort, ports[i].PrivatePort, start.Type)
if start.PublicPort != 0 {
itemPortStr = fmt.Sprintf("%s:%v-%v->%v-%v/%s", start.IP, start.PublicPort, ports[i].PublicPort, start.PrivatePort, ports[i].PrivatePort, start.Type)
}
if len(start.IP) == 0 {
itemPortStr = strings.TrimPrefix(itemPortStr, ":")
}
datas = append(datas, itemPortStr)
}
}
}
return datas
} }
func loadComposeCount(client *client.Client) int { func loadComposeCount(client *client.Client) int {
@@ -1972,7 +2041,7 @@ func loadComposeCount(client *client.Client) int {
} }
func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper { func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper {
var exposedPorts []dto.PortHelper var exposedPorts []dto.PortHelper
seenPorts := make(map[dto.PortHelper]struct{}) samePortMap := make(map[string]dto.PortHelper)
ports := transPortToStr(itemPorts) ports := transPortToStr(itemPorts)
for _, item := range ports { for _, item := range ports {
itemStr := strings.Split(item, "->") itemStr := strings.Split(item, "->")
@@ -1993,11 +2062,16 @@ func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper {
} }
itemPort.ContainerPort = itemContainer[0] itemPort.ContainerPort = itemContainer[0]
itemPort.Protocol = itemContainer[1] itemPort.Protocol = itemContainer[1]
if _, exists := seenPorts[itemPort]; exists { keyItem := fmt.Sprintf("%s->%s/%s", itemPort.HostPort, itemPort.ContainerPort, itemPort.Protocol)
continue if val, ok := samePortMap[keyItem]; ok {
val.HostIP = ""
samePortMap[keyItem] = val
} else {
samePortMap[keyItem] = itemPort
} }
seenPorts[itemPort] = struct{}{} }
exposedPorts = append(exposedPorts, itemPort) for _, val := range samePortMap {
exposedPorts = append(exposedPorts, val)
} }
return exposedPorts return exposedPorts
} }
+17 -19
View File
@@ -28,7 +28,6 @@ import (
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"gorm.io/gorm"
) )
const composeProjectLabel = "com.docker.compose.project" const composeProjectLabel = "com.docker.compose.project"
@@ -36,7 +35,7 @@ const composeConfigLabel = "com.docker.compose.project.config_files"
const composeWorkdirLabel = "com.docker.compose.project.working_dir" const composeWorkdirLabel = "com.docker.compose.project.working_dir"
const composeCreatedBy = "createdBy" const composeCreatedBy = "createdBy"
func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface{}, error) { func (u *ContainerService) PageCompose(req dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error) {
var ( var (
records []dto.ComposeInfo records []dto.ComposeInfo
BackDatas []dto.ComposeInfo BackDatas []dto.ComposeInfo
@@ -188,7 +187,10 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
} }
BackDatas = records[start:end] BackDatas = records[start:end]
} }
listItem := loadEnv(BackDatas) listItem := BackDatas
if !isDemoReadOnly(readOnly...) {
listItem = loadEnv(BackDatas)
}
return int64(total), listItem, nil return int64(total), listItem, nil
} }
@@ -253,10 +255,6 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
return err return err
} }
req.Name = projectName req.Name = projectName
recordName := strings.ToLower(req.Name)
if err := saveComposeRecord(recordName, req.Path); err != nil {
return fmt.Errorf("save compose record failed, err: %v", err)
}
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeCompose, req.TaskID, 1) taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeCompose, req.TaskID, 1)
if err != nil { if err != nil {
return fmt.Errorf("new task for image build failed, err: %v", err) return fmt.Errorf("new task for image build failed, err: %v", err)
@@ -265,7 +263,18 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
taskItem.AddSubTask(i18n.GetMsgByKey("ComposeCreate"), func(t *task.Task) error { taskItem.AddSubTask(i18n.GetMsgByKey("ComposeCreate"), func(t *task.Task) error {
err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name) err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name)
t.LogWithStatus(i18n.GetMsgByKey("ComposeCreate"), err) t.LogWithStatus(i18n.GetMsgByKey("ComposeCreate"), err)
return err if err != nil {
_, _ = compose.Down(req.Path, req.Name)
return err
}
recordName := strings.ToLower(req.Name)
record, _ := composeRepo.GetRecord(repo.WithByName(recordName))
if record.ID == 0 {
_ = composeRepo.CreateRecord(&model.Compose{Name: recordName, Path: req.Path})
} else {
_ = composeRepo.UpdateRecord(recordName, map[string]interface{}{"path": req.Path})
}
return nil
}, nil) }, nil)
_ = taskItem.Execute() _ = taskItem.Execute()
}() }()
@@ -273,17 +282,6 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
return nil return nil
} }
func saveComposeRecord(name, composePath string) error {
record, err := composeRepo.GetRecord(repo.WithByName(name))
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if record.ID == 0 {
return composeRepo.CreateRecord(&model.Compose{Name: name, Path: composePath})
}
return composeRepo.UpdateRecord(name, map[string]interface{}{"path": composePath})
}
func checkComposeRecordName(name string) error { func checkComposeRecordName(name string) error {
composeItem, _ := composeRepo.GetRecord(repo.WithByName(name)) composeItem, _ := composeRepo.GetRecord(repo.WithByName(name))
if composeItem.ID != 0 && len(composeItem.Path) != 0 { if composeItem.ID != 0 && len(composeItem.Path) != 0 {
+57 -76
View File
@@ -1,11 +1,9 @@
package service package service
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@@ -20,7 +18,6 @@ import (
"github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
v1 "github.com/opencontainers/image-spec/specs-go/v1" v1 "github.com/opencontainers/image-spec/specs-go/v1"
) )
@@ -67,13 +64,13 @@ func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
if err != nil { if err != nil {
return err return err
} }
if err := normalizeContainerEndpointSettings(ctx, client, networkConf, nil); err != nil { normalizeContainerEndpointSettings(ctx, client, networkConf, nil)
return err
}
cleanupErr, err := switchContainer(ctx, client, req.Name, oldContainer, func() (container.CreateResponse, error) { cleanupErr, err := switchContainer(ctx, client, req.Name, oldContainer, func() (container.CreateResponse, error) {
return client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name) return createContainerWithDynamicIPFallback(func() (container.CreateResponse, error) {
}, config.Tty, t) return client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
}, networkConf.EndpointsConfig, oldContainer.NetworkSettings)
}, newContainerSwitchTaskLogger(t))
if err != nil { if err != nil {
return fmt.Errorf("update container failed, err: %v", err) return fmt.Errorf("update container failed, err: %v", err)
} }
@@ -138,15 +135,9 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
config.Image = req.Image config.Image = req.Image
hostConf := cloneContainerHostConfig(oldContainer.HostConfig) hostConf := cloneContainerHostConfig(oldContainer.HostConfig)
preserveContainerVolumeMounts(hostConf, oldContainer.Mounts) 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) { cleanupErr, err := switchContainer(ctx, client, item, oldContainer, func() (container.CreateResponse, error) {
return createContainerWithNetworks(ctx, client, config, hostConf, networkConf, extraNetworks, item) return createContainerWithOldNetworks(ctx, client, config, hostConf, oldContainer.NetworkSettings, item)
}, config.Tty, t) }, newContainerSwitchTaskLogger(t))
if err != nil { if err != nil {
upgradeErr := fmt.Errorf("upgrade container %s failed: %w", item, err) upgradeErr := fmt.Errorf("upgrade container %s failed: %w", item, err)
upgradeErrors = append(upgradeErrors, upgradeErr) upgradeErrors = append(upgradeErrors, upgradeErr)
@@ -175,7 +166,6 @@ type containerSwitchClient interface {
ContainerStart(context.Context, string, container.StartOptions) error ContainerStart(context.Context, string, container.StartOptions) error
ContainerRemove(context.Context, string, container.RemoveOptions) error ContainerRemove(context.Context, string, container.RemoveOptions) error
ContainerInspect(context.Context, string) (container.InspectResponse, error) ContainerInspect(context.Context, string) (container.InspectResponse, error)
ContainerLogs(context.Context, string, container.LogsOptions) (io.ReadCloser, error)
NetworkConnect(context.Context, string, string, *network.EndpointSettings) error NetworkConnect(context.Context, string, string, *network.EndpointSettings) error
NetworkDisconnect(context.Context, string, string, bool) error NetworkDisconnect(context.Context, string, string, bool) error
} }
@@ -248,18 +238,22 @@ func (l *containerOperationMutex) lock(names ...string) func() {
} }
type containerNetworkAttachment struct { type containerNetworkAttachment struct {
name string name string
endpoint *network.EndpointSettings endpoint *network.EndpointSettings
isDynamic bool
} }
type containerSwitchLogger interface { type containerSwitchLogFunc func(messageKey, containerName string, err error)
LogWithStatus(string, error)
Log(string) func newContainerSwitchTaskLogger(t *task.Task) containerSwitchLogFunc {
return func(messageKey, containerName string, err error) {
t.LogWithStatus(i18n.GetWithName(messageKey, containerName), err)
}
} }
func logContainerSwitchStep(logger containerSwitchLogger, messageKey, containerName string, err error) { func logContainerSwitchStep(logger containerSwitchLogFunc, messageKey, containerName string, err error) {
if logger != nil { if logger != nil {
logger.LogWithStatus(i18n.GetWithName(messageKey, containerName), err) logger(messageKey, containerName, err)
} }
} }
@@ -270,8 +264,7 @@ func switchContainer(
name string, name string,
oldContainer container.InspectResponse, oldContainer container.InspectResponse,
createNew func() (container.CreateResponse, error), createNew func() (container.CreateResponse, error),
tty bool, logger containerSwitchLogFunc,
logger containerSwitchLogger,
) (cleanupErr error, err error) { ) (cleanupErr error, err error) {
if oldContainer.ID == "" { if oldContainer.ID == "" {
return nil, fmt.Errorf("original container ID is empty") return nil, fmt.Errorf("original container ID is empty")
@@ -321,7 +314,6 @@ func switchContainer(
} }
if err := cli.ContainerStart(ctx, created.ID, container.StartOptions{}); err != nil { if err := cli.ContainerStart(ctx, created.ID, container.StartOptions{}); err != nil {
logContainerSwitchStep(logger, "ContainerStartReplacement", name, err) logContainerSwitchStep(logger, "ContainerStartReplacement", name, err)
logContainerStartupLogs(ctx, cli, created.ID, name, tty, logger)
rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger) rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger)
return nil, errors.Join(fmt.Errorf("start new container failed: %w", err), rollbackErr) return nil, errors.Join(fmt.Errorf("start new container failed: %w", err), rollbackErr)
} }
@@ -329,7 +321,6 @@ func switchContainer(
if wasRunning { if wasRunning {
if err := waitContainerReady(ctx, cli, created.ID); err != nil { if err := waitContainerReady(ctx, cli, created.ID); err != nil {
logContainerSwitchStep(logger, "ContainerWaitReplacement", name, err) logContainerSwitchStep(logger, "ContainerWaitReplacement", name, err)
logContainerStartupLogs(ctx, cli, created.ID, name, tty, logger)
rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger) rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger)
return nil, errors.Join(fmt.Errorf("new container readiness check failed: %w", err), rollbackErr) return nil, errors.Join(fmt.Errorf("new container readiness check failed: %w", err), rollbackErr)
} }
@@ -346,48 +337,8 @@ const (
containerStartPollInterval = time.Second containerStartPollInterval = time.Second
containerHealthCheckMinWait = 30 * time.Second containerHealthCheckMinWait = 30 * time.Second
containerHealthCheckMaxWait = 10 * time.Minute containerHealthCheckMaxWait = 10 * time.Minute
containerDiagnosticLogTail = "200"
) )
func logContainerStartupLogs(ctx context.Context, cli containerSwitchClient, containerID, name string, tty bool, logger containerSwitchLogger) {
if logger == nil {
return
}
logger.Log(fmt.Sprintf("========== %s ==========", i18n.GetWithName("ContainerStartupDiagnostic", name)))
diagnosticCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
reader, err := cli.ContainerLogs(diagnosticCtx, containerID, container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Timestamps: true,
Tail: containerDiagnosticLogTail,
})
if err != nil {
logger.Log(i18n.GetWithNameAndErr("ContainerDiagnosticLogsFailed", name, err))
return
}
defer reader.Close()
var output bytes.Buffer
if tty {
_, err = io.Copy(&output, reader)
} else {
_, err = stdcopy.StdCopy(&output, &output, reader)
}
if err != nil {
logger.Log(i18n.GetWithNameAndErr("ContainerDiagnosticLogsFailed", name, err))
return
}
logs := strings.TrimSpace(output.String())
logger.Log(fmt.Sprintf("---------- %s ----------", i18n.GetMsgByKey("ContainerRecentLogs")))
if logs == "" {
logger.Log(i18n.GetMsgByKey("ContainerDiagnosticLogsEmpty"))
return
}
logger.Log(logs)
}
func waitContainerReady(ctx context.Context, cli containerInspectClient, containerID string) error { func waitContainerReady(ctx context.Context, cli containerInspectClient, containerID string) error {
info, err := cli.ContainerInspect(ctx, containerID) info, err := cli.ContainerInspect(ctx, containerID)
if err != nil { if err != nil {
@@ -587,13 +538,13 @@ func disconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitc
endpoints := make(map[string]*network.EndpointSettings, len(extras)+1) endpoints := make(map[string]*network.EndpointSettings, len(extras)+1)
if primary != nil { if primary != nil {
for name, endpoint := range primary.EndpointsConfig { for name, endpoint := range primary.EndpointsConfig {
if name != "bridge" && endpoint != nil { if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil {
endpoints[name] = endpoint endpoints[name] = endpoint
} }
} }
} }
for name, endpoint := range extras { for name, endpoint := range extras {
if name != "bridge" && endpoint != nil { if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil {
endpoints[name] = endpoint endpoints[name] = endpoint
} }
} }
@@ -609,8 +560,9 @@ func disconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitc
return disconnected, fmt.Errorf("disconnect original container from network %s failed: %w", name, err) return disconnected, fmt.Errorf("disconnect original container from network %s failed: %w", name, err)
} }
disconnected = append(disconnected, containerNetworkAttachment{ disconnected = append(disconnected, containerNetworkAttachment{
name: name, name: name,
endpoint: endpoints[name], endpoint: endpoints[name],
isDynamic: isDynamicContainerNetwork(oldContainer.NetworkSettings, name),
}) })
} }
return disconnected, nil return disconnected, nil
@@ -620,6 +572,10 @@ func reconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitch
var reconnectErr error var reconnectErr error
for _, attachment := range attachments { for _, attachment := range attachments {
err := cli.NetworkConnect(ctx, attachment.name, containerID, attachment.endpoint) 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 { if err != nil {
reconnectErr = errors.Join(reconnectErr, fmt.Errorf("reconnect original container to network %s failed: %w", attachment.name, err)) reconnectErr = errors.Join(reconnectErr, fmt.Errorf("reconnect original container to network %s failed: %w", attachment.name, err))
} }
@@ -627,7 +583,7 @@ func reconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitch
return reconnectErr return reconnectErr
} }
func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, oldContainerID, originalName string, wasRunning bool, newContainer string, disconnectedNetworks []containerNetworkAttachment, logger containerSwitchLogger) error { func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, oldContainerID, originalName string, wasRunning bool, newContainer string, disconnectedNetworks []containerNetworkAttachment, logger containerSwitchLogFunc) error {
var rollbackErr error var rollbackErr error
backupName := containerSwitchBackupName(oldContainerID) backupName := containerSwitchBackupName(oldContainerID)
if newContainer != "" { if newContainer != "" {
@@ -652,7 +608,7 @@ func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, ol
reconnectErr := reconnectOriginalContainerNetworks(ctx, cli, oldContainerID, disconnectedNetworks) reconnectErr := reconnectOriginalContainerNetworks(ctx, cli, oldContainerID, disconnectedNetworks)
logContainerSwitchStep(logger, "ContainerRollbackReconnectOld", currentName, reconnectErr) logContainerSwitchStep(logger, "ContainerRollbackReconnectOld", currentName, reconnectErr)
rollbackErr = errors.Join(rollbackErr, reconnectErr) rollbackErr = errors.Join(rollbackErr, reconnectErr)
if wasRunning && reconnectErr == nil { if wasRunning {
restartErr := restartOriginalContainer(ctx, cli, oldContainerID) restartErr := restartOriginalContainer(ctx, cli, oldContainerID)
logContainerSwitchStep(logger, "ContainerRollbackRestartOld", currentName, restartErr) logContainerSwitchStep(logger, "ContainerRollbackRestartOld", currentName, restartErr)
rollbackErr = errors.Join(rollbackErr, restartErr) rollbackErr = errors.Join(rollbackErr, restartErr)
@@ -660,8 +616,17 @@ func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, ol
return rollbackErr return rollbackErr
} }
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) { func createContainerWithOldNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkSettings *container.NetworkSettings, name string) (container.CreateResponse, error) {
created, err := client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name) 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)
if err != nil { if err != nil {
return created, err return created, err
} }
@@ -673,6 +638,9 @@ func createContainerWithNetworks(ctx context.Context, client *client.Client, con
sort.Strings(extraNames) sort.Strings(extraNames)
for _, item := range extraNames { for _, item := range extraNames {
err := client.NetworkConnect(ctx, item, created.ID, extraNetworks[item]) 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 { if err != nil {
_ = client.ContainerRemove(ctx, created.ID, container.RemoveOptions{Force: true}) _ = client.ContainerRemove(ctx, created.ID, container.RemoveOptions{Force: true})
return created, err return created, err
@@ -680,3 +648,16 @@ func createContainerWithNetworks(ctx context.Context, client *client.Client, con
} }
return created, nil 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
}
}
}
+5 -2
View File
@@ -25,7 +25,7 @@ import (
type CronjobService struct{} type CronjobService struct{}
type ICronjobService interface { type ICronjobService interface {
SearchWithPage(search dto.PageCronjob) (int64, interface{}, error) SearchWithPage(search dto.PageCronjob, readOnly ...bool) (int64, interface{}, error)
SearchRecords(search dto.SearchRecord) (int64, interface{}, error) SearchRecords(search dto.SearchRecord) (int64, interface{}, error)
Create(cronjobDto dto.CronjobOperate, operator string) error Create(cronjobDto dto.CronjobOperate, operator string) error
LoadNextHandle(spec string) ([]string, error) LoadNextHandle(spec string) ([]string, error)
@@ -50,7 +50,7 @@ func NewICronjobService() ICronjobService {
return &CronjobService{} return &CronjobService{}
} }
func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interface{}, error) { func (u *CronjobService) SearchWithPage(search dto.PageCronjob, readOnly ...bool) (int64, interface{}, error) {
total, cronjobs, err := cronjobRepo.Page(search.Page, total, cronjobs, err := cronjobRepo.Page(search.Page,
search.PageSize, search.PageSize,
repo.WithByGroups(search.GroupIDs), repo.WithByGroups(search.GroupIDs),
@@ -83,6 +83,9 @@ func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interfac
if cronjob.Type == "snapshot" && len(cronjob.SnapshotRule) != 0 { if cronjob.Type == "snapshot" && len(cronjob.SnapshotRule) != 0 {
_ = json.Unmarshal([]byte(cronjob.SnapshotRule), &item.SnapshotRule) _ = json.Unmarshal([]byte(cronjob.SnapshotRule), &item.SnapshotRule)
} }
if isDemoReadOnly(readOnly...) {
item.Secret = ""
}
dtoCronjobs = append(dtoCronjobs, item) dtoCronjobs = append(dtoCronjobs, item)
} }
return total, dtoCronjobs, err return total, dtoCronjobs, err
+55 -59
View File
@@ -18,7 +18,8 @@ import (
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator" "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/controller" "github.com/1Panel-dev/1Panel/agent/utils/controller"
@@ -243,7 +244,8 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent
currentInfo.DiskData = loadDiskInfo() currentInfo.DiskData = loadDiskInfo()
currentInfo.GPUData, currentInfo.NPUData, currentInfo.XPUData = loadAcceleratorInfo() currentInfo.GPUData = loadGPUInfo()
currentInfo.XPUData = loadXpuInfo()
if ioOption == "all" { if ioOption == "all" {
diskInfo, _ := disk.IOCounters() diskInfo, _ := disk.IOCounters()
@@ -469,7 +471,7 @@ func loadDiskInfo() []dto.DiskInfo {
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}}, cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
cmd.PipeCommand{Name: "awk", Args: []string{format}}, cmd.PipeCommand{Name: "awk", Args: []string{format}},
) )
if err != nil && strings.TrimSpace(stdout) == "" { if err != nil {
return datas return datas
} }
} }
@@ -567,64 +569,32 @@ func loadDiskInfo() []dto.DiskInfo {
return datas return datas
} }
func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) { func loadGPUInfo() []dto.GPUInfo {
ok, client := accelerator.New() ok, client := gpu.New()
if !ok { var list []interface{}
return nil, nil, nil if ok {
} info, err := client.LoadGpuInfo()
snapshot, err := client.Collect(context.Background()) if err != nil || len(info.GPUs) == 0 {
if err != nil || len(snapshot.Devices) == 0 { return nil
return nil, nil, nil }
} for _, item := range info.GPUs {
if warning := snapshot.Warning(); warning != nil { list = append(list, item)
global.LOG.Warnf("load accelerator dashboard data partially failed, err: %v", warning)
}
var (
gpuData []dto.GPUInfo
npuData []dto.NPUInfo
xpuData []dto.XPUInfo
)
for _, device := range snapshot.Devices {
switch device.Kind {
case accelerator.KindGPU:
if device.GPU == nil {
continue
}
var dataItem dto.GPUInfo
if err := copier.Copy(&dataItem, device.GPU); err != nil {
continue
}
dataItem.PowerUsage = dataItem.PowerDraw + " / " + dataItem.MaxPowerLimit
dataItem.MemoryUsage = dataItem.MemUsed + " / " + dataItem.MemTotal
gpuData = append(gpuData, dataItem)
case accelerator.KindNPU:
if device.NPU == nil {
continue
}
var dataItem dto.NPUInfo
if err := copier.Copy(&dataItem, device.NPU); err != nil {
continue
}
npuData = append(npuData, dataItem)
case accelerator.KindXPU:
if device.XPU == nil {
continue
}
xpuData = append(xpuData, dto.XPUInfo{
DeviceID: device.Index,
DeviceName: device.Name,
PciBdfAddress: device.BusID,
Memory: device.XPU.Basic.Memory,
Temperature: device.Metrics.Temperature.Display,
GPUUtil: device.Metrics.Utilization.Display,
MemoryUsed: device.Metrics.MemoryUsed.Display,
Power: device.Metrics.Power.Display,
MemoryUtil: device.Metrics.MemoryUtil.Display,
})
} }
} }
return gpuData, npuData, xpuData if len(list) == 0 {
return nil
}
var data []dto.GPUInfo
for _, gpu := range list {
var dataItem dto.GPUInfo
if err := copier.Copy(&dataItem, &gpu); err != nil {
continue
}
dataItem.PowerUsage = dataItem.PowerDraw + " / " + dataItem.MaxPowerLimit
dataItem.MemoryUsage = dataItem.MemUsed + " / " + dataItem.MemTotal
data = append(data, dataItem)
}
return data
} }
type AppLauncher struct { type AppLauncher struct {
@@ -640,6 +610,32 @@ func ArryContains(arr []string, element string) bool {
return false return false
} }
func loadXpuInfo() []dto.XPUInfo {
var list []interface{}
ok, xpuClient := xpu.New()
if ok {
xpus, err := xpuClient.LoadDashData()
if err != nil || len(xpus) == 0 {
return nil
}
for _, item := range xpus {
list = append(list, item)
}
}
if len(list) == 0 {
return nil
}
var data []dto.XPUInfo
for _, gpu := range list {
var dataItem dto.XPUInfo
if err := copier.Copy(&dataItem, &gpu); err != nil {
continue
}
data = append(data, dataItem)
}
return data
}
func loadOutboundIP() string { func loadOutboundIP() string {
conn, err := network.Dial("udp", "8.8.8.8:80") conn, err := network.Dial("udp", "8.8.8.8:80")
+12 -4
View File
@@ -24,8 +24,8 @@ import (
type DatabaseService struct{} type DatabaseService struct{}
type IDatabaseService interface { type IDatabaseService interface {
Get(name string) (dto.DatabaseInfo, error) Get(name string, readOnly ...bool) (dto.DatabaseInfo, error)
SearchWithPage(search dto.DatabaseSearch) (int64, interface{}, error) SearchWithPage(search dto.DatabaseSearch, readOnly ...bool) (int64, interface{}, error)
CheckDatabase(req dto.DatabaseCreate) bool CheckDatabase(req dto.DatabaseCreate) bool
Create(req dto.DatabaseCreate) error Create(req dto.DatabaseCreate) error
Update(req dto.DatabaseUpdate) error Update(req dto.DatabaseUpdate) error
@@ -39,7 +39,7 @@ func NewIDatabaseService() IDatabaseService {
return &DatabaseService{} return &DatabaseService{}
} }
func (u *DatabaseService) SearchWithPage(search dto.DatabaseSearch) (int64, interface{}, error) { func (u *DatabaseService) SearchWithPage(search dto.DatabaseSearch, readOnly ...bool) (int64, interface{}, error) {
total, dbs, err := databaseRepo.Page(search.Page, search.PageSize, total, dbs, err := databaseRepo.Page(search.Page, search.PageSize,
databaseRepo.WithTypeList(search.Type), databaseRepo.WithTypeList(search.Type),
repo.WithByLikeName(search.Info), repo.WithByLikeName(search.Info),
@@ -52,12 +52,16 @@ func (u *DatabaseService) SearchWithPage(search dto.DatabaseSearch) (int64, inte
if err := copier.Copy(&item, &db); err != nil { if err := copier.Copy(&item, &db); err != nil {
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
} }
if isDemoReadOnly(readOnly...) {
item.Password = ""
item.ClientKey = ""
}
datas = append(datas, item) datas = append(datas, item)
} }
return total, datas, err return total, datas, err
} }
func (u *DatabaseService) Get(name string) (dto.DatabaseInfo, error) { func (u *DatabaseService) Get(name string, readOnly ...bool) (dto.DatabaseInfo, error) {
var data dto.DatabaseInfo var data dto.DatabaseInfo
remote, err := databaseRepo.Get(repo.WithByName(name)) remote, err := databaseRepo.Get(repo.WithByName(name))
if err != nil { if err != nil {
@@ -66,6 +70,10 @@ func (u *DatabaseService) Get(name string) (dto.DatabaseInfo, error) {
if err := copier.Copy(&data, &remote); err != nil { if err := copier.Copy(&data, &remote); err != nil {
return data, buserr.WithDetail("ErrStructTransform", err.Error(), nil) return data, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
} }
if isDemoReadOnly(readOnly...) {
data.Password = ""
data.ClientKey = ""
}
return data, nil return data, nil
} }
+5 -2
View File
@@ -23,7 +23,7 @@ import (
type MongodbService struct{} type MongodbService struct{}
type IMongodbService interface { type IMongodbService interface {
SearchWithPage(search dto.MongodbDBSearch) (int64, interface{}, error) SearchWithPage(search dto.MongodbDBSearch, readOnly ...bool) (int64, interface{}, error)
Create(ctx context.Context, req dto.MongodbDBCreate) (*model.DatabaseMongodb, error) Create(ctx context.Context, req dto.MongodbDBCreate) (*model.DatabaseMongodb, error)
LoadFromRemote(req dto.MongodbLoadDB) error LoadFromRemote(req dto.MongodbLoadDB) error
UpdateDescription(req dto.UpdateDescription) error UpdateDescription(req dto.UpdateDescription) error
@@ -40,7 +40,7 @@ func NewIMongodbService() IMongodbService {
return &MongodbService{} return &MongodbService{}
} }
func (u *MongodbService) SearchWithPage(search dto.MongodbDBSearch) (int64, interface{}, error) { func (u *MongodbService) SearchWithPage(search dto.MongodbDBSearch, readOnly ...bool) (int64, interface{}, error) {
total, mongodbs, err := mongodbRepo.Page( total, mongodbs, err := mongodbRepo.Page(
search.Page, search.Page,
search.PageSize, search.PageSize,
@@ -54,6 +54,9 @@ func (u *MongodbService) SearchWithPage(search dto.MongodbDBSearch) (int64, inte
if err := copier.Copy(&item, &mongodb); err != nil { if err := copier.Copy(&item, &mongodb); err != nil {
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
} }
if isDemoReadOnly(readOnly...) {
item.Password = ""
}
dtoMongodbs = append(dtoMongodbs, item) dtoMongodbs = append(dtoMongodbs, item)
} }
return total, dtoMongodbs, err return total, dtoMongodbs, err
+7 -3
View File
@@ -46,7 +46,7 @@ type IMysqlService interface {
DeleteCheck(req dto.MysqlDBDeleteCheck) ([]dto.DBResource, error) DeleteCheck(req dto.MysqlDBDeleteCheck) ([]dto.DBResource, error)
Delete(ctx context.Context, req dto.MysqlDBDelete) error Delete(ctx context.Context, req dto.MysqlDBDelete) error
ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, error) ListUsers(req dto.MysqlUserSearch, readOnly ...bool) ([]dto.MysqlUser, error)
ListGrants(req dto.MysqlUserSearch) ([]dto.MysqlGrant, error) ListGrants(req dto.MysqlUserSearch) ([]dto.MysqlGrant, error)
ListGrantSummary(req dto.MysqlGrantSummarySearch) (map[string][]dto.MysqlUser, error) ListGrantSummary(req dto.MysqlGrantSummarySearch) (map[string][]dto.MysqlUser, error)
CreateUser(req dto.MysqlUserCreate) error CreateUser(req dto.MysqlUserCreate) error
@@ -506,7 +506,7 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
return &createItem, nil return &createItem, nil
} }
func (u *MysqlService) ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, error) { func (u *MysqlService) ListUsers(req dto.MysqlUserSearch, readOnly ...bool) ([]dto.MysqlUser, error) {
dbType, err := resolveDatabaseUserType(req.Database) dbType, err := resolveDatabaseUserType(req.Database)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -520,10 +520,14 @@ func (u *MysqlService) ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, erro
if isMysqlSystemUser(user.Username) { if isMysqlSystemUser(user.Username) {
continue continue
} }
password := user.Password
if isDemoReadOnly(readOnly...) {
password = ""
}
res = append(res, dto.MysqlUser{ res = append(res, dto.MysqlUser{
Username: user.Username, Username: user.Username,
Host: user.Host, Host: user.Host,
Password: user.Password, Password: password,
Description: user.Description, Description: user.Description,
IsDelete: user.IsDelete, IsDelete: user.IsDelete,
}) })
+5 -2
View File
@@ -26,7 +26,7 @@ import (
type PostgresqlService struct{} type PostgresqlService struct{}
type IPostgresqlService interface { type IPostgresqlService interface {
SearchWithPage(search dto.PostgresqlDBSearch) (int64, interface{}, error) SearchWithPage(search dto.PostgresqlDBSearch, readOnly ...bool) (int64, interface{}, error)
ListDBOption() ([]dto.PostgresqlOption, error) ListDBOption() ([]dto.PostgresqlOption, error)
BindUser(req dto.PostgresqlBindUser) error BindUser(req dto.PostgresqlBindUser) error
Create(ctx context.Context, req dto.PostgresqlDBCreate) (*model.DatabasePostgresql, error) Create(ctx context.Context, req dto.PostgresqlDBCreate) (*model.DatabasePostgresql, error)
@@ -42,7 +42,7 @@ func NewIPostgresqlService() IPostgresqlService {
return &PostgresqlService{} return &PostgresqlService{}
} }
func (u *PostgresqlService) SearchWithPage(search dto.PostgresqlDBSearch) (int64, interface{}, error) { func (u *PostgresqlService) SearchWithPage(search dto.PostgresqlDBSearch, readOnly ...bool) (int64, interface{}, error) {
total, postgresqls, err := postgresqlRepo.Page(search.Page, search.PageSize, total, postgresqls, err := postgresqlRepo.Page(search.Page, search.PageSize,
postgresqlRepo.WithByPostgresqlName(search.Database), postgresqlRepo.WithByPostgresqlName(search.Database),
repo.WithByLikeName(search.Info), repo.WithByLikeName(search.Info),
@@ -54,6 +54,9 @@ func (u *PostgresqlService) SearchWithPage(search dto.PostgresqlDBSearch) (int64
if err := copier.Copy(&item, &pg); err != nil { if err := copier.Copy(&item, &pg); err != nil {
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
} }
if isDemoReadOnly(readOnly...) {
item.Password = ""
}
dtoPostgresqls = append(dtoPostgresqls, item) dtoPostgresqls = append(dtoPostgresqls, item)
} }
return total, dtoPostgresqls, err return total, dtoPostgresqls, err
+1 -118
View File
@@ -2,7 +2,6 @@ package service
import ( import (
"bufio" "bufio"
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -15,19 +14,14 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "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/controller"
"github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
) )
const dockerNftablesMinVersion = "29.0.0"
type DockerService struct{} type DockerService struct{}
type IDockerService interface { type IDockerService interface {
UpdateConf(req dto.SettingUpdate, withRestart bool) error UpdateConf(req dto.SettingUpdate, withRestart bool) error
UpdateFirewallBackend(backend string) error
UpdateLogOption(req dto.LogOption) error UpdateLogOption(req dto.LogOption) error
UpdateIpv6Option(req dto.Ipv6Option) error UpdateIpv6Option(req dto.Ipv6Option) error
UpdateConfByFile(info dto.DaemonJsonUpdateByFile) error UpdateConfByFile(info dto.DaemonJsonUpdateByFile) error
@@ -36,115 +30,6 @@ type IDockerService interface {
OperateDocker(req dto.DockerOperation) error 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 := docker_guard.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 { func NewIDockerService() IDockerService {
return &DockerService{} return &DockerService{}
} }
@@ -282,9 +167,7 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate, withRestart bool) erro
delete(daemonMap, "ipv6") delete(daemonMap, "ipv6")
delete(daemonMap, "fixed-cidr-v6") delete(daemonMap, "fixed-cidr-v6")
delete(daemonMap, "ip6tables") delete(daemonMap, "ip6tables")
if configuredDockerFirewallBackend() != constant.FirewallProviderNftables { delete(daemonMap, "experimental")
delete(daemonMap, "experimental")
}
} }
case "LogOption": case "LogOption":
if req.Value == "disable" { if req.Value == "disable" {
+2 -1
View File
@@ -9,6 +9,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/toolbox" "github.com/1Panel-dev/1Panel/agent/utils/toolbox"
) )
@@ -108,7 +109,7 @@ func (u *Fail2BanService) UpdateConf(req dto.Fail2BanUpdate) error {
if req.Value == "firewallcmd-ipset" { if req.Value == "firewallcmd-ipset" {
itemName = "firewalld" itemName = "firewalld"
} }
client, err := NewSelectedSystemFirewallClient() client, err := firewall.NewFirewallClient()
if err != nil { if err != nil {
return err return err
} }
+11 -11
View File
@@ -87,7 +87,7 @@ type IFileService interface {
ConvertLog(req dto.PageInfo) (int64, []response.FileConvertLog, error) ConvertLog(req dto.PageInfo) (int64, []response.FileConvertLog, error)
BatchGetRemarks(req request.FileRemarkBatch) map[string]string BatchGetRemarks(req request.FileRemarkBatch) map[string]string
SetRemark(req request.FileRemarkUpdate) error SetRemark(req request.FileRemarkUpdate) error
AISearch(req request.FileAISearch) (*response.FileAISearchResult, error) AISearch(req request.FileAISearch, readOnly ...bool) (*response.FileAISearchResult, error)
} }
const ( const (
@@ -159,10 +159,6 @@ 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 total, start, end := len(files), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total { if start > total {
backData = make([]response.UploadInfo, 0) backData = make([]response.UploadInfo, 0)
@@ -896,7 +892,6 @@ func (f *FileService) Wget(w request.FileWget) (string, error) {
key := "file-wget-" + common.GetUuid() key := "file-wget-" + common.GetUuid()
options := files.DownloadOptions{ options := files.DownloadOptions{
IgnoreCertificate: w.IgnoreCertificate, IgnoreCertificate: w.IgnoreCertificate,
UseServerFilename: w.UseServerFilename,
} }
if w.UseProxy { if w.UseProxy {
systemProxy, err := NewISettingService().GetSystemProxy() systemProxy, err := NewISettingService().GetSystemProxy()
@@ -1565,7 +1560,7 @@ func (f *FileService) ConvertLog(req dto.PageInfo) (total int64, data []response
return total, data, nil return total, data, nil
} }
func (f *FileService) AISearch(req request.FileAISearch) (*response.FileAISearchResult, error) { func (f *FileService) AISearch(req request.FileAISearch, readOnly ...bool) (*response.FileAISearchResult, error) {
root := filepath.Clean(strings.TrimSpace(req.Path)) root := filepath.Clean(strings.TrimSpace(req.Path))
if root == "" { if root == "" {
return nil, buserr.WithDetail("ErrInvalidParams", "path is required", nil) return nil, buserr.WithDetail("ErrInvalidParams", "path is required", nil)
@@ -1618,10 +1613,15 @@ func (f *FileService) AISearch(req request.FileAISearch) (*response.FileAISearch
return nil, buserr.WithDetail("ErrFileAISearchBadPattern", err.Error(), nil) return nil, buserr.WithDetail("ErrFileAISearchBadPattern", err.Error(), nil)
} }
cfg, timeout, err := terminalai.LoadFileAIRuntimeConfig() var cfg terminalai.GeneratorConfig
aiEnabled := err == nil var timeout time.Duration
if err != nil && !errors.Is(err, os.ErrNotExist) { aiEnabled := false
return nil, err if !isDemoReadOnly(readOnly...) {
cfg, timeout, err = terminalai.LoadFileAIRuntimeConfig()
aiEnabled = err == nil
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, err
}
} }
items, truncated, err := files.CollectDirInventory(root, containSub, maxItems) items, truncated, err := files.CollectDirInventory(root, containSub, maxItems)
+32 -22
View File
@@ -37,14 +37,14 @@ var fileShareCodeRegexp = regexp.MustCompile(`^[A-Za-z0-9]{10,16}$`)
type IFileShareService interface { type IFileShareService interface {
Create(req request.FileShareCreate) (*response.FileShareInfo, error) Create(req request.FileShareCreate) (*response.FileShareInfo, error)
Page(req dto.PageInfo) (int64, []response.FileShareInfo, error) Page(req dto.PageInfo, readOnly ...bool) (int64, []response.FileShareInfo, error)
GetByPath(path string) (*response.FileShareInfo, error) GetByPath(path string, readOnly ...bool) (*response.FileShareInfo, error)
GetByCode(code string) (*response.FileShareInfo, error) GetByCode(code string, readOnly ...bool) (*response.FileShareInfo, error)
GetPublicByCode(code string) (*response.FileSharePublicInfo, error) GetPublicByCode(code string, readOnly ...bool) (*response.FileSharePublicInfo, error)
DeleteByPath(path string) error DeleteByPath(path string) error
SharePathCodeMap() (map[string]string, error) SharePathCodeMap() (map[string]string, error)
Check(code, password string) error Check(code, password string, readOnly ...bool) error
PrepareDownload(code, password string) (filePath, fileName string, err error) PrepareDownload(code, password string, readOnly ...bool) (filePath, fileName string, err error)
} }
func NewIFileShareService() IFileShareService { func NewIFileShareService() IFileShareService {
@@ -212,14 +212,14 @@ func (s *FileShareService) Create(req request.FileShareCreate) (*response.FileSh
return &res, nil return &res, nil
} }
func (s *FileShareService) Page(req dto.PageInfo) (int64, []response.FileShareInfo, error) { func (s *FileShareService) Page(req dto.PageInfo, readOnly ...bool) (int64, []response.FileShareInfo, error) {
items, err := fileShareRepo.All() items, err := fileShareRepo.All()
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
result := make([]response.FileShareInfo, 0, len(items)) result := make([]response.FileShareInfo, 0, len(items))
for _, item := range items { for _, item := range items {
if err := s.pruneInvalidShare(item); err != nil { if err := s.pruneInvalidShare(item, readOnly...); err != nil {
return 0, nil, err return 0, nil, err
} }
if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix { if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
@@ -239,7 +239,7 @@ func (s *FileShareService) Page(req dto.PageInfo) (int64, []response.FileShareIn
return int64(total), result[start:end], nil return int64(total), result[start:end], nil
} }
func (s *FileShareService) GetByPath(path string) (*response.FileShareInfo, error) { func (s *FileShareService) GetByPath(path string, readOnly ...bool) (*response.FileShareInfo, error) {
item, err := fileShareRepo.GetFirst(fileShareRepo.WithByPath(strings.TrimSpace(path))) item, err := fileShareRepo.GetFirst(fileShareRepo.WithByPath(strings.TrimSpace(path)))
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -247,7 +247,7 @@ func (s *FileShareService) GetByPath(path string) (*response.FileShareInfo, erro
} }
return nil, err return nil, err
} }
if err := s.pruneInvalidShare(item); err != nil { if err := s.pruneInvalidShare(item, readOnly...); err != nil {
return nil, err return nil, err
} }
if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix { if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
@@ -258,7 +258,7 @@ func (s *FileShareService) GetByPath(path string) (*response.FileShareInfo, erro
return &info, nil return &info, nil
} }
func (s *FileShareService) GetByCode(code string) (*response.FileShareInfo, error) { func (s *FileShareService) GetByCode(code string, readOnly ...bool) (*response.FileShareInfo, error) {
item, err := fileShareRepo.GetFirst(fileShareRepo.WithByCode(strings.TrimSpace(code))) item, err := fileShareRepo.GetFirst(fileShareRepo.WithByCode(strings.TrimSpace(code)))
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -266,7 +266,7 @@ func (s *FileShareService) GetByCode(code string) (*response.FileShareInfo, erro
} }
return nil, err return nil, err
} }
if err := s.pruneInvalidShare(item); err != nil { if err := s.pruneInvalidShare(item, readOnly...); err != nil {
return nil, err return nil, err
} }
if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix { if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
@@ -276,7 +276,7 @@ func (s *FileShareService) GetByCode(code string) (*response.FileShareInfo, erro
return &info, nil return &info, nil
} }
func (s *FileShareService) GetPublicByCode(code string) (*response.FileSharePublicInfo, error) { func (s *FileShareService) GetPublicByCode(code string, readOnly ...bool) (*response.FileSharePublicInfo, error) {
item, err := fileShareRepo.GetFirst(fileShareRepo.WithByCode(strings.TrimSpace(code))) item, err := fileShareRepo.GetFirst(fileShareRepo.WithByCode(strings.TrimSpace(code)))
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -284,7 +284,7 @@ func (s *FileShareService) GetPublicByCode(code string) (*response.FileSharePubl
} }
return nil, err return nil, err
} }
if err := s.pruneInvalidShare(item); err != nil { if err := s.pruneInvalidShare(item, readOnly...); err != nil {
return nil, err return nil, err
} }
if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix { if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
@@ -324,32 +324,38 @@ func (s *FileShareService) SharePathCodeMap() (map[string]string, error) {
return result, nil return result, nil
} }
func (s *FileShareService) Check(code, password string) error { func (s *FileShareService) Check(code, password string, readOnly ...bool) error {
_, err := s.check(code, password) _, err := s.check(code, password, readOnly...)
return err return err
} }
func (s *FileShareService) PrepareDownload(code, password string) (string, string, error) { func (s *FileShareService) PrepareDownload(code, password string, readOnly ...bool) (string, string, error) {
item, err := s.check(code, password) item, err := s.check(code, password, readOnly...)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
return item.Path, item.FileName, nil return item.Path, item.FileName, nil
} }
func (s *FileShareService) pruneInvalidShare(item model.FileShare) error { func (s *FileShareService) pruneInvalidShare(item model.FileShare, readOnly ...bool) error {
now := time.Now().Unix() now := time.Now().Unix()
if item.ExpiresUnix > 0 && now > item.ExpiresUnix { if item.ExpiresUnix > 0 && now > item.ExpiresUnix {
if isDemoReadOnly(readOnly...) {
return nil
}
return fileShareRepo.Delete(repo.WithByID(item.ID)) return fileShareRepo.Delete(repo.WithByID(item.ID))
} }
info, err := os.Stat(item.Path) info, err := os.Stat(item.Path)
if err != nil || info.IsDir() { if err != nil || info.IsDir() {
if isDemoReadOnly(readOnly...) {
return nil
}
return fileShareRepo.Delete(repo.WithByID(item.ID)) return fileShareRepo.Delete(repo.WithByID(item.ID))
} }
return nil return nil
} }
func (s *FileShareService) check(code, password string) (*model.FileShare, error) { func (s *FileShareService) check(code, password string, readOnly ...bool) (*model.FileShare, error) {
code = strings.TrimSpace(code) code = strings.TrimSpace(code)
password = strings.TrimSpace(password) password = strings.TrimSpace(password)
if code == "" { if code == "" {
@@ -366,7 +372,9 @@ func (s *FileShareService) check(code, password string) (*model.FileShare, error
now := time.Now().Unix() now := time.Now().Unix()
if item.ExpiresUnix > 0 && now > item.ExpiresUnix { if item.ExpiresUnix > 0 && now > item.ExpiresUnix {
_ = fileShareRepo.Delete(repo.WithByID(item.ID)) if !isDemoReadOnly(readOnly...) {
_ = fileShareRepo.Delete(repo.WithByID(item.ID))
}
return nil, buserr.New("ErrFileShareExpired") return nil, buserr.New("ErrFileShareExpired")
} }
if item.PasswordHash != "" { if item.PasswordHash != "" {
@@ -377,7 +385,9 @@ func (s *FileShareService) check(code, password string) (*model.FileShare, error
info, err := os.Stat(item.Path) info, err := os.Stat(item.Path)
if err != nil || info.IsDir() { if err != nil || info.IsDir() {
_ = fileShareRepo.Delete(repo.WithByID(item.ID)) if !isDemoReadOnly(readOnly...) {
_ = fileShareRepo.Delete(repo.WithByID(item.ID))
}
return nil, buserr.New("ErrFileSharePath") return nil, buserr.New("ErrFileSharePath")
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,78 @@
package service
import (
"fmt"
"os"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/controller"
)
const fail2BanRestoreWithFirewallMarker = "/run/1panel_fail2ban_restore_with_firewall"
type firewallFail2BanState struct {
markerPath string
isExist func(string) bool
isActive func(string) bool
start func(string) error
}
func newFirewallFail2BanState() *firewallFail2BanState {
return &firewallFail2BanState{
markerPath: fail2BanRestoreWithFirewallMarker,
isExist: func(serviceName string) bool {
exists, err := controller.CheckExist(serviceName)
if err != nil {
global.LOG.Warnf("check %s installation before stopping the firewall failed: %v", serviceName, err)
}
return exists
},
isActive: func(serviceName string) bool {
active, err := controller.CheckActive(serviceName)
if err != nil {
global.LOG.Warnf("check %s status before stopping the firewall failed: %v", serviceName, err)
}
return active
},
start: controller.HandleStart,
}
}
func (s *firewallFail2BanState) rememberBeforeFirewallStop() error {
if !s.isExist("fail2ban.service") {
return nil
}
if !s.isActive("fail2ban.service") {
return nil
}
return s.markForRestore()
}
func (s *firewallFail2BanState) markForRestore() error {
if err := os.WriteFile(s.markerPath, nil, 0600); err != nil {
return fmt.Errorf("mark Fail2Ban for restoration with the firewall: %w", err)
}
return nil
}
func (s *firewallFail2BanState) restoreAfterFirewallStart() error {
_, err := os.Stat(s.markerPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("load Fail2Ban restore marker after starting the firewall: %w", err)
}
if err := s.start("fail2ban.service"); err != nil {
return fmt.Errorf("restore Fail2Ban after starting the firewall: %w", err)
}
return s.clearRestoreMarker()
}
func (s *firewallFail2BanState) clearRestoreMarker() error {
if err := os.Remove(s.markerPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("clear Fail2Ban firewall restore status: %w", err)
}
return nil
}
-76
View File
@@ -1,76 +0,0 @@
package service
import (
"context"
"fmt"
"io"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
)
const (
firewallTaskHost = "FirewallTaskHost"
firewallTaskForwarding = "FirewallTaskForwarding"
firewallTaskDocker = "FirewallTaskDocker"
)
func firewallTaskName(operation, subsystem, backend string) string {
name := i18n.GetMsgByKey(subsystem)
if backend != "" {
name += " · " + backend
}
key := "FirewallRule" + operation
if operation == task.TaskExec {
key = "FirewallTaskInitialize"
}
return i18n.GetMsgWithMap(key, map[string]interface{}{"name": name})
}
func queueFirewallRuleTask(subsystem, operation string, labels []string, apply func(context.Context) error) (dto.FilterChainOperationResponse, error) {
taskItem, err := task.NewTask(firewallTaskName(operation, subsystem, ""), operation, task.TaskScopeFirewall, "", 0)
if err != nil {
return dto.FilterChainOperationResponse{}, err
}
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
t.Logf("rules=%d", len(labels))
err := t.TaskCtx.Err()
if err == nil {
err = apply(t.TaskCtx)
}
succeeded, failed := 0, 0
for _, label := range labels {
if err != nil {
failed++
t.LogFailedWithErr(label, err)
} else {
succeeded++
t.LogSuccess(label)
}
}
t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{
"succeeded": succeeded, "failed": failed,
}))
return err
}, nil, 0, 0)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
taskItem.LogFailedWithErr(taskItem.Name, err)
closeUnstartedFirewallTask(taskItem)
return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall rule task: %w", err)
}
go func() { _ = taskItem.Execute() }()
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
func closeUnstartedFirewallTask(t *task.Task) {
if cancel, ok := global.LoadTaskCancel(t.TaskID); ok {
cancel()
}
global.RemoveTaskCancel(t.TaskID)
if closer, ok := t.Logger.Out.(io.Closer); ok {
_ = closer.Close()
}
}
-65
View File
@@ -1,65 +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 := configuredSystemFirewallBackend(); provider != "" {
return lifecycle.NewClientFor(provider)
}
client, err := lifecycle.NewClient()
if err != nil {
return nil, err
}
_ = settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, client.Name())
return client, nil
}
func configuredSystemFirewallBackend() string {
if global.DB == nil {
return ""
}
provider, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
return strings.TrimSpace(provider)
}
func NewSelectedSystemFirewallClient() (lifecycle.Client, error) {
return selectedSystemFirewallClient()
}
func selectedSystemFirewallProvider() (string, error) {
client, err := selectedSystemFirewallClient()
if err != nil {
return "", err
}
return client.Name(), nil
}
+123 -585
View File
@@ -1,637 +1,175 @@
package service package service
import ( import (
"context"
"encoding/json"
"errors"
"fmt" "fmt"
"os" "strconv"
"reflect" "strings"
"slices"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant" "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"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard" fireClient "github.com/1Panel-dev/1Panel/agent/utils/firewall/client"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" "github.com/1Panel-dev/1Panel/agent/utils/firewall/client/iptables"
filterruntime "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/runtime"
"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"
"gorm.io/gorm"
) )
type IFirewallSettingService interface { type firewallPortWhitelist struct {
CreatePortWhitelist(context.Context, dto.FirewallPortWhitelistCreate) error Port string
UpdatePortWhitelist(context.Context, dto.FirewallPortWhitelistUpdate) error Protocol string
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error
Load(context.Context) (dto.FirewallSettings, error)
Operate(context.Context, dto.FirewallBackendOperation) error
} }
type FirewallSettingService struct{} func loadConfiguredFirewallPortWhiteList() ([]firewallPortWhitelist, error) {
value, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList)
var firewallWhitelistMu sync.Mutex if err != nil {
value = constant.FirewallPortWhiteListValue
var ErrFirewallBackendCleanupRequired = errors.New("firewall backend cleanup required") if err := settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, value); err != nil {
func firewallBackendCleanupRequired(current, target string) error {
return fmt.Errorf(
"%w: current backend %s still contains 1Panel runtime rules; clean it up before switching to %s",
ErrFirewallBackendCleanupRequired,
current,
target,
)
}
func NewIFirewallSettingService() IFirewallSettingService {
return &FirewallSettingService{}
}
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) error {
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
return append(current, request.Rule), nil
})
}
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) error {
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
index, err := findPortWhitelistRule(current, request.OldRule)
if err != nil {
return nil, err return nil, err
} }
current[index] = request.Rule }
return current, nil return parseFirewallPortWhiteList(value)
})
} }
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error { func loadFirewallPortWhiteList() ([]firewallPortWhitelist, error) {
if request.Rule == nil { portWhiteList, err := loadConfiguredFirewallPortWhiteList()
return fmt.Errorf("select one firewall port whitelist rule to delete")
}
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
index, err := findPortWhitelistRule(current, *request.Rule)
if err != nil {
return nil, err
}
return slices.Delete(current, index, index+1), nil
})
}
func findPortWhitelistRule(rules []firewall.PortWhitelist, target firewall.PortWhitelist) (int, error) {
index := slices.IndexFunc(rules, func(rule firewall.PortWhitelist) bool {
return samePortWhitelistRule(rule, target)
})
if index < 0 {
return -1, fmt.Errorf("firewall port whitelist rule has changed or no longer exists; refresh and retry")
}
return index, nil
}
func samePortWhitelistRule(left, right firewall.PortWhitelist) bool {
if reflect.DeepEqual(left, right) {
return true
}
normalizedLeft, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{left})
if err != nil { if err != nil {
return false
}
normalizedRight, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{right})
if err != nil {
return false
}
slices.Sort(normalizedLeft[0].Sources)
slices.Sort(normalizedRight[0].Sources)
return reflect.DeepEqual(normalizedLeft[0], normalizedRight[0])
}
func savePortWhitelist(ctx context.Context, change func([]firewall.PortWhitelist) ([]firewall.PortWhitelist, error)) error {
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
defer filterruntime.InvalidateInventory()
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
current, err := loadPortWhitelistSetting(tx)
if err != nil {
return err
}
desired, err := change(current)
if err != nil {
return err
}
desired, err = firewall.ValidatePortWhitelist(desired)
if err != nil {
return err
}
value, err := json.Marshal(desired)
if err != nil {
return err
}
return tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).
FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
})
}
func checkFirewallRuleWhitelistProtection(provider filter.Provider, record model.FirewallRule) error {
ports, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
rules, err := record.RulesForProvider(provider)
if err != nil {
return err
}
for _, rule := range rules {
if filter.RuleMatchesPortWhitelist(rule, ports) {
return filter.ErrProtectedRule
}
}
return nil
}
func loadPortWhitelistSetting(db *gorm.DB) ([]firewall.PortWhitelist, error) {
var setting model.Setting
if err := db.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error; errors.Is(err, gorm.ErrRecordNotFound) {
setting.Value = constant.FirewallPortWhiteListValue
} else if err != nil {
return nil, err return nil, err
} }
var rules []firewall.PortWhitelist requiredPorts, err := loadRequiredFirewallPortWhiteList()
err := json.Unmarshal([]byte(setting.Value), &rules)
return rules, err
}
func loadSSHWhitelistPortFrom(path string) (string, error) {
directives, _, err := parseSSHConfigTree(path)
if errors.Is(err, os.ErrNotExist) {
return defaultSSHPort, nil
}
if err != nil { if err != nil {
return "", err return nil, err
} }
return loadSSHPortValues(directives)[0], nil return normalizeFirewallPortWhiteList(append(portWhiteList, requiredPorts...)), nil
} }
func customWhitelist(entries []firewall.PortWhitelist) []firewall.PortWhitelist { func loadRequiredFirewallPortWhiteList() ([]firewallPortWhitelist, error) {
result := make([]firewall.PortWhitelist, 0, len(entries)) panelPort := LoadPanelPort()
for _, entry := range entries { if panelPort == "" {
if entry.Type == "" { return nil, fmt.Errorf("find 1panel service port failed")
result = append(result, entry)
}
} }
return result return normalizeFirewallPortWhiteList([]firewallPortWhitelist{
{Port: panelPort, Protocol: "tcp"},
{Port: loadSSHPort(), Protocol: "tcp"},
}), nil
} }
func InitializeFirewallWhitelistPorts(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) { func parseFirewallPortWhiteList(value string) ([]firewallPortWhitelist, error) {
entries = slices.Clone(entries) items := strings.FieldsFunc(value, func(r rune) bool {
var sshPort string return r == ',' || r == '\n' || r == ';' || r == ' '
for i := range entries { })
entry := &entries[i] ports := make([]firewallPortWhitelist, 0, len(items))
if entry.Type == "" || entry.Port != "" { exists := make(map[string]struct{})
for _, item := range items {
item = strings.TrimSpace(item)
if item == "" {
continue continue
} }
switch entry.Type { port, protocol, ok := strings.Cut(item, "/")
case firewall.PortWhitelistTypePanel: if !ok {
entry.Port = LoadPanelPort() protocol = "tcp"
case firewall.PortWhitelistTypeSSH:
if sshPort == "" {
var err error
sshPort, err = loadSSHWhitelistPortFrom(sshPath)
if err != nil {
return nil, err
}
}
entry.Port = sshPort
} }
port = strings.TrimSpace(port)
protocol = strings.ToLower(strings.TrimSpace(protocol))
if protocol != "tcp" && protocol != "udp" {
return nil, fmt.Errorf("invalid firewall port whitelist protocol: %s", item)
}
portNum, err := strconv.Atoi(port)
if err != nil || portNum < 1 || portNum > 65535 {
return nil, fmt.Errorf("invalid firewall port whitelist: %s", item)
}
key := fmt.Sprintf("%d/%s", portNum, protocol)
if _, ok := exists[key]; ok {
continue
}
exists[key] = struct{}{}
ports = append(ports, firewallPortWhitelist{Port: strconv.Itoa(portNum), Protocol: protocol})
} }
return firewall.ValidatePortWhitelist(entries) return ports, nil
} }
func updateSystemAccessPortWhitelist(ctx context.Context, serviceType string, ports []string) error { func normalizeFirewallPortWhiteList(portWhiteList []firewallPortWhitelist) []firewallPortWhitelist {
return savePortWhitelist(ctx, func(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) { ports := make([]firewallPortWhitelist, 0, len(portWhiteList))
for i := range entries { exists := make(map[string]struct{})
if entries[i].Type == serviceType { for _, item := range portWhiteList {
if len(ports) == 0 { if item.Port == "" {
return nil, fmt.Errorf("firewall whitelist %s requires a port", serviceType) continue
}
entries[i].Port = ports[0]
}
} }
return entries, nil key := fmt.Sprintf("%s/%s", item.Port, item.Protocol)
}) if _, ok := exists[key]; ok {
continue
}
exists[key] = struct{}{}
ports = append(ports, item)
}
return ports
} }
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) { func syncFirewallPortWhiteListAfterUpdate(oldValue string) error {
result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus()} client, err := firewall.NewFirewallClient()
installed := make(map[string]bool)
for _, name := range lifecycle.InstalledProviders() {
installed[name] = true
}
result.System.Selected = configuredSystemFirewallBackend()
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,
constant.FirewallProviderIptables,
constant.FirewallProviderNftables,
} {
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
if option.Installed && name == result.System.Selected {
client, err := lifecycle.NewClientFor(name)
if err != nil {
option.Message = err.Error()
} else if supportsManagedFilterChains(name) {
option.Initialized, option.Bound, err = loadFirewallInitStatus(name, "base")
if err != nil {
option.Message = err.Error()
}
option.IPv4 = loadSystemFirewallFamilyInfo(name, constant.FirewallFamilyIPv4)
option.IPv6 = loadSystemFirewallFamilyInfo(name, constant.FirewallFamilyIPv6)
} else if option.Active, err = client.Status(); err != nil {
option.Message = err.Error()
}
}
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.Forwarding.Selected = configuredForwardingBackend()
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 && name == result.Forwarding.Selected {
manager, err := newForwardingManagerFor(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,
}
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 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)
}
dockerInstalled := cmd.Which("docker")
dockerVersion := ""
if dockerInstalled {
dockerVersion = loadDockerEngineVersion(ctx)
}
result.Docker.Selected = configuredDockerFirewallBackend()
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,
}
if name == constant.FirewallProviderNftables && dockerInstalled && !dockerNftablesSupported(dockerVersion) {
option.Supported = false
option.SupportReason = "docker_version_unsupported"
option.Active = false
}
if option.Active {
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
}
result.Docker.Options = append(result.Docker.Options, option)
}
var err error
result.PortWhitelist, err = loadPortWhitelistSetting(global.DB.WithContext(ctx))
if err != nil { if err != nil {
return result, err
}
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 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)
}
}
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,
}
}
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
if err := lockFirewallLifecycleIdle(); err != nil {
return err return err
} }
defer firewallLifecycleTaskMu.Unlock() if client.Name() == "iptables" {
if request.Subsystem != "system" && request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables { isInit, _ := iptables.LoadInitStatus("iptables", "base")
return fmt.Errorf("%s only supports iptables or nftables", request.Subsystem) if !isInit {
}
if request.Subsystem == "system" && !supportsManagedFilterChains(request.Backend) && request.Operation != "select" {
return fmt.Errorf("%s does not support initialization or cleanup", request.Backend)
}
switch request.Subsystem {
case "system":
if err := s.operateSystem(request); err != nil {
return err
}
if request.Operation == "initialize" {
service := newFirewallService()
rulesErr := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend), nil)
whitelistErr := service.SyncPortWhitelist(ctx)
return errors.Join(rulesErr, whitelistErr)
}
return nil
case "forwarding":
return s.operateForwarding(request)
case "docker":
return s.operateDocker(ctx, request)
default:
return fmt.Errorf("unsupported firewall subsystem %q", request.Subsystem)
}
}
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 {
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)
if err != nil {
return err
}
if current != request.Backend && initialized {
return firewallBackendCleanupRequired(current, request.Backend)
}
}
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(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
}
}
return false, nil
}
func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperation) error {
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
if _, err := lifecycle.NewClientFor(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 {
previous = client.Name()
}
}
if request.Operation == "select" && previous != "" && previous != request.Backend {
initialized, err := systemFirewallBackendInitialized(previous)
if err != nil {
return err
}
if initialized {
return firewallBackendCleanupRequired(previous, request.Backend)
}
}
if err := settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, request.Backend); err != nil {
return err
}
rollback := func(err error) error {
if err == nil {
return nil return nil
} }
_ = settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, previous) oldPortWhiteList, err := parseFirewallPortWhiteList(oldValue)
return err
}
if request.Operation == "select" {
return nil
}
initErr := newFirewallService().operateFilterChainBaseLocked(request.Backend, dto.FilterChainOperation{
Name: constant.FirewallBasicChain, Operate: string(firewall.BaseOperationInit),
})
if initErr != nil {
return rollback(initErr)
}
return settingRepo.UpdateOrCreate(constant.FirewallFilterInitializedKey, constant.StatusEnable)
}
func systemFirewallBackendInitialized(backend string) (bool, error) {
return systemFirewallBackendInitializedWithClientFactory(backend, lifecycle.NewClientFor)
}
func systemFirewallBackendInitializedWithClientFactory(
backend string,
newClient func(string) (lifecycle.Client, error),
) (bool, error) {
client, err := newClient(backend)
if err != nil {
if errors.Is(err, lifecycle.ErrNotInstalled) {
return false, nil
}
return false, err
}
if supportsManagedFilterChains(backend) {
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
}
if initialized {
return true, nil
}
}
return false, nil
}
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 {
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()
if err != nil {
return err
}
current = detected.Name()
}
initialized, err := forwardingBackendInitialized(current)
if err != nil { if err != nil {
return err return err
} }
if current != request.Backend && initialized { return syncIptablesFirewallPortWhiteList(true, oldPortWhiteList)
return firewallBackendCleanupRequired(current, request.Backend)
}
} }
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, request.Backend); err != nil {
isActive, _ := client.Status()
if !isActive {
return nil
}
portWhiteList, err := loadFirewallPortWhiteList()
if err != nil {
return err return err
} }
if request.Operation == "initialize" { oldPortWhiteList, err := parseFirewallPortWhiteList(oldValue)
return newForwardingService().Enable()
}
recordForwardingSyncError(nil)
return nil
}
func forwardingBackendInitialized(backend string) (bool, error) {
manager, err := newForwardingManagerFor(backend)
if err != nil { if err != nil {
if errors.Is(err, lifecycle.ErrNotInstalled) { return err
return false, nil
}
return false, err
} }
for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} { requiredPorts, err := loadRequiredFirewallPortWhiteList()
initialized, _, err := manager.FamilyStatus(family) if err != nil {
if err != nil { return err
return false, err
}
if initialized {
return true, nil
}
} }
return false, nil oldPortWhiteList = normalizeFirewallPortWhiteList(append(oldPortWhiteList, requiredPorts...))
return syncFirewallClientPortWhiteList(client, oldPortWhiteList, portWhiteList)
} }
func alternateDirectBackend(backend string) string { func syncFirewallClientPortWhiteList(client firewall.FilterClient, oldPortWhiteList, portWhiteList []firewallPortWhitelist) error {
if backend == constant.FirewallProviderNftables { oldPorts := firewallPortWhiteListMap(oldPortWhiteList)
return constant.FirewallProviderIptables newPorts := firewallPortWhiteListMap(portWhiteList)
for _, item := range oldPortWhiteList {
key := firewallPortWhiteListKey(item)
if _, ok := newPorts[key]; ok {
continue
}
if err := client.Port(fireClient.FireInfo{Port: item.Port, Protocol: item.Protocol, Strategy: "accept"}, "remove"); err != nil {
return err
}
} }
return constant.FirewallProviderNftables for _, item := range portWhiteList {
key := firewallPortWhiteListKey(item)
if _, ok := oldPorts[key]; ok {
continue
}
if err := client.Port(fireClient.FireInfo{Port: item.Port, Protocol: item.Protocol, Strategy: "accept"}, "add"); err != nil {
return err
}
}
return client.Reload()
}
func firewallPortWhiteListMap(portWhiteList []firewallPortWhitelist) map[string]struct{} {
ports := make(map[string]struct{})
for _, item := range portWhiteList {
ports[firewallPortWhiteListKey(item)] = struct{}{}
}
return ports
}
func firewallPortWhiteListKey(item firewallPortWhitelist) string {
return item.Port + "/" + item.Protocol
} }
File diff suppressed because it is too large Load Diff
-577
View File
@@ -1,577 +0,0 @@
package service
import (
"context"
"errors"
"fmt"
"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"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
)
type IForwardingService interface {
LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error)
OperateRules(dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error)
Enable() 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
}
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)
},
}
}
func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) {
selected := configuredForwardingBackend()
baseInfo := dto.FirewallSubsystemStatus{
Version: "-", Name: forwardingDisplayName(selected), Backend: selected, SyncError: lastForwardingSyncError(),
}
manager, err := s.managerFactory()
if err != nil {
if errors.Is(err, errForwardingBackendUnavailable) {
baseInfo.Reason = constant.FirewallBackendNotInstalled
return baseInfo, nil
}
return baseInfo, err
}
status, err := manager.Status()
if err != nil {
return baseInfo, err
}
baseInfo.IsExist = true
baseInfo.Name, baseInfo.Backend = forwardingDisplayName(status.Name), status.Name
baseInfo.Version = status.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)
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()
}
return dto.FirewallBackendFamilyStatus{Available: available, Initialized: initialized, Bound: bound}
}
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, []dto.ForwardRule, error) {
if request.Strategy != "" {
return 0, nil, nil
}
stored, err := s.rules.List(context.Background())
if err != nil {
return 0, nil, err
}
manager, err := s.managerFactory()
if err != nil {
return 0, nil, err
}
runtime, err := manager.List("", "")
if err != nil {
return 0, nil, err
}
inventory, err := mergeForwardingInventory(stored, runtime)
if err != nil {
return 0, nil, err
}
keyword := strings.ToLower(strings.TrimSpace(request.Info))
filtered := inventory[:0]
for _, item := range inventory {
if keyword == "" || forwardingRuleMatchesKeyword(item, keyword) {
filtered = append(filtered, item)
}
}
inventory = filtered
total := len(inventory)
start, end := (request.Page-1)*request.PageSize, request.Page*request.PageSize
if request.All {
start, end = 0, total
}
if start > total {
return int64(total), make([]dto.ForwardRule, 0), 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
}
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
}
}
return false
}
func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) {
labels := make([]string, len(request.Rules))
operation := task.TaskCreate
for i, rule := range request.Rules {
labels[i] = fmt.Sprintf("[%d/%d] %s %s %s %s -> %s:%s", i+1, len(request.Rules), rule.Operation, rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort)
if rule.Operation != "add" {
operation = task.TaskUpdate
}
}
if forwardingOperationsOnlyRemove(request.Rules) {
operation = task.TaskDelete
}
return queueFirewallRuleTask(firewallTaskForwarding, operation, labels, func(ctx context.Context) error {
return s.operateRules(ctx, request)
})
}
func (s *ForwardingService) operateRules(ctx context.Context, request dto.ForwardRuleOperate) error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
if err := ctx.Err(); err != nil {
return err
}
stored, err := s.rules.List(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 {
return err
}
if err := s.rules.ReplaceAll(ctx, forwardingRuleModels(desired)); 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
}
return err
}
recordForwardingSyncError(nil)
return nil
}
func (s *ForwardingService) Enable() error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
manager, err := s.managerFactory()
if err != nil {
recordForwardingSyncError(err)
return err
}
if err := s.persistForwardingEnabled(); err != nil {
recordForwardingSyncError(err)
return err
}
if err := s.activateManager(manager); err != nil {
recordForwardingSyncError(err)
return err
}
rules, err := s.rules.List(context.Background())
if err != nil {
recordForwardingSyncError(err)
return err
}
err = manager.Reconcile(forwardingRulesFromModels(rules))
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
}
taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskForwarding, ""), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0)
if err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("create forwarding initialization task: %w", err)
}
var manager *forwarding.Manager
var backend string
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallEnableForwardingStep"), func(t *task.Task) error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
var err error
manager, err = s.managerFactory()
if err != nil {
recordForwardingSyncError(err)
return err
}
backend = manager.Name()
t.Logf("backend=%s", backend)
if err := s.persistForwardingEnabled(); err != nil {
recordForwardingSyncError(err)
return err
}
if err := s.activateManager(manager); err != nil {
recordForwardingSyncError(err)
return err
}
return nil
}, nil)
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallRestoreForwardingRulesStep"), func(t *task.Task) error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
rules, err := s.rules.List(t.TaskCtx)
if err != nil {
recordForwardingSyncError(err)
return err
}
err = manager.Reconcile(forwardingRulesFromModels(rules))
recordForwardingSyncError(err)
return err
}, nil)
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
return dto.FilterChainOperationResponse{}, fmt.Errorf("save forwarding initialization task: %w", err)
}
go func() { _ = taskItem.Execute() }()
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
}
func (s *ForwardingService) Restore(ctx context.Context) error {
forwardingMutationMu.Lock()
defer forwardingMutationMu.Unlock()
enabled, err := s.forwardingEnabled()
if err != nil || !enabled {
if err != nil {
recordForwardingSyncError(err)
}
return err
}
manager, err := s.managerFactory()
if err != nil {
recordForwardingSyncError(err)
return err
}
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) reconcile(rules []forwarding.Rule) error {
manager, err := s.managerFactory()
if err != nil {
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)
}
}
}
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 newForwardingManager() (*forwarding.Manager, error) {
return newForwardingManagerFor(configuredForwardingBackend())
}
func configuredForwardingBackend() string {
selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
selected = strings.TrimSpace(selected)
if selected == "" {
return constant.FirewallProviderIptables
}
return selected
}
func newForwardingManagerFor(backend string) (*forwarding.Manager, error) {
client, err := lifecycle.NewClientFor(backend)
if err != nil {
return nil, fmt.Errorf(
"%w: selected forwarding backend %s: %w",
errForwardingBackendUnavailable, backend, err,
)
}
adapter, err := forwarding.New(client.Name())
if err != nil {
return nil, err
}
return forwarding.NewManager(adapter, client), nil
}
+235
View File
@@ -0,0 +1,235 @@
package service
import (
"sort"
"strconv"
"strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"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/firewall"
forwardClient "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
)
type IForwardingService interface {
LoadBaseInfo() (dto.FirewallBaseInfo, error)
SearchWithPage(search dto.ForwardRuleSearch) (int64, interface{}, error)
Operate(req dto.ForwardRuleOperate) error
Enable() error
Replay() error
}
type ForwardingService struct {
adapterFactory func() (forwardClient.Adapter, error)
filterFactory func() (firewall.FilterClient, error)
}
func NewIForwardingService() IForwardingService {
return &ForwardingService{
adapterFactory: newForwardingAdapter,
filterFactory: firewall.NewFirewallClient,
}
}
func newForwardingAdapter() (forwardClient.Adapter, error) {
client, err := firewall.NewFirewallClient()
if err != nil {
return nil, err
}
return forwardClient.NewAdapter(client.Name())
}
func (s *ForwardingService) LoadBaseInfo() (dto.FirewallBaseInfo, error) {
baseInfo := dto.FirewallBaseInfo{Version: "-", Name: "-"}
adapter, err := s.adapterFactory()
if err != nil {
global.LOG.Errorf("load forwarding failed, err: %v", err)
return baseInfo, nil
}
filter, err := s.filterFactory()
if err != nil {
global.LOG.Errorf("load firewall status failed, err: %v", err)
return baseInfo, nil
}
baseInfo.IsExist = true
baseInfo.Name = adapter.Name()
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
baseInfo.PingStatus = firewall.LoadPingStatus()
baseInfo.Version, _ = filter.Version()
}()
go func() {
defer wg.Done()
baseInfo.IsActive, _ = filter.Status()
baseInfo.IsInit, baseInfo.IsBind = adapter.InitStatus()
}()
wg.Wait()
return baseInfo, nil
}
func (s *ForwardingService) SearchWithPage(req dto.ForwardRuleSearch) (int64, interface{}, error) {
adapter, err := s.adapterFactory()
if err != nil {
return 0, nil, err
}
rules, err := adapter.List()
if err != nil {
return 0, nil, err
}
if req.Strategy != "" {
return 0, nil, nil
}
var filtered []forwardClient.Rule
for _, rule := range rules {
if req.Info != "" && !strings.Contains(rule.Port, req.Info) &&
!strings.Contains(rule.TargetPort, req.Info) && !strings.Contains(rule.TargetIP, req.Info) {
continue
}
filtered = append(filtered, rule)
}
total := len(filtered)
start, end := (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total {
return int64(total), make([]dto.ForwardRule, 0), nil
}
if end > total {
end = total
}
pageRules := filtered[start:end]
var items []dto.ForwardRule
if pageRules != nil {
items = make([]dto.ForwardRule, 0, len(pageRules))
}
for _, rule := range pageRules {
items = append(items, dto.ForwardRule{
Num: rule.Num,
Protocol: rule.Protocol,
Port: rule.Port,
TargetIP: rule.TargetIP,
TargetPort: rule.TargetPort,
Interface: rule.Interface,
})
}
return int64(total), items, nil
}
func (s *ForwardingService) Operate(req dto.ForwardRuleOperate) error {
adapter, err := s.adapterFactory()
if err != nil {
return err
}
rules, _ := adapter.List()
kept := rules[:0]
for _, rule := range rules {
shouldKeep := true
for i := range req.Rules {
reqRule := &req.Rules[i]
if reqRule.TargetIP == "" {
reqRule.TargetIP = "127.0.0.1"
}
if reqRule.Operation == "remove" && requestMatchesForwardRule(*reqRule, rule) {
shouldKeep = false
break
}
}
if shouldKeep {
kept = append(kept, rule)
}
}
for _, rule := range kept {
for _, reqRule := range req.Rules {
if reqRule.Operation != "remove" && requestMatchesForwardRule(reqRule, rule) {
return buserr.New("ErrRecordExist")
}
}
}
sort.SliceStable(req.Rules, func(i, j int) bool {
if req.Rules[i].Operation == "remove" && req.Rules[j].Operation != "remove" {
return true
}
if req.Rules[i].Operation != "remove" && req.Rules[j].Operation == "remove" {
return false
}
n1, _ := strconv.Atoi(req.Rules[i].Num)
n2, _ := strconv.Atoi(req.Rules[j].Num)
return n1 > n2
})
for _, rule := range req.Rules {
for _, protocol := range strings.Split(rule.Protocol, "/") {
targetIP := rule.TargetIP
if targetIP == "" {
targetIP = "127.0.0.1"
}
err := adapter.Operate(forwardClient.Rule{
Num: rule.Num,
Protocol: protocol,
Port: rule.Port,
TargetIP: targetIP,
TargetPort: rule.TargetPort,
Interface: rule.Interface,
}, rule.Operation)
if err == nil {
continue
}
if req.ForceDelete {
global.LOG.Error(err)
continue
}
return err
}
}
return nil
}
func requestMatchesForwardRule(req dto.ForwardRuleOperation, rule forwardClient.Rule) bool {
for _, protocol := range strings.Split(req.Protocol, "/") {
if req.Port == rule.Port && req.TargetPort == rule.TargetPort && req.TargetIP == rule.TargetIP &&
protocol == rule.Protocol && req.Interface == rule.Interface {
return true
}
}
return false
}
func (s *ForwardingService) Enable() error {
adapter, err := s.adapterFactory()
if err != nil {
return err
}
if err := adapter.Enable(); err != nil {
return err
}
if adapter.Name() != "firewalld" {
_ = settingRepo.Update("IptablesForwardStatus", constant.StatusEnable)
}
return nil
}
func (s *ForwardingService) Replay() error {
adapter, err := s.adapterFactory()
if err != nil {
return err
}
if err := adapter.Replay(); err != nil {
return err
}
if adapter.Name() == "firewalld" {
return nil
}
status, _ := settingRepo.GetValueByKey("IptablesForwardStatus")
if status == constant.StatusEnable {
return adapter.Enable()
}
return nil
}
@@ -0,0 +1,152 @@
package service
import (
"encoding/json"
"errors"
"reflect"
"testing"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
forwardClient "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
"github.com/go-playground/validator/v10"
)
type forwardingCall struct {
rule forwardClient.Rule
operation string
}
type fakeForwardingAdapter struct {
name string
rules []forwardClient.Rule
listErr error
operateErr error
calls []forwardingCall
}
func (f *fakeForwardingAdapter) Name() string { return f.name }
func (f *fakeForwardingAdapter) List() ([]forwardClient.Rule, error) {
return append([]forwardClient.Rule(nil), f.rules...), f.listErr
}
func (f *fakeForwardingAdapter) Operate(rule forwardClient.Rule, operation string) error {
f.calls = append(f.calls, forwardingCall{rule: rule, operation: operation})
return f.operateErr
}
func (f *fakeForwardingAdapter) Enable() error { return nil }
func (f *fakeForwardingAdapter) InitStatus() (bool, bool) { return true, true }
func (f *fakeForwardingAdapter) Replay() error { return nil }
func forwardingServiceWithAdapter(adapter forwardClient.Adapter) *ForwardingService {
return &ForwardingService{
adapterFactory: func() (forwardClient.Adapter, error) { return adapter, nil },
filterFactory: firewall.NewFirewallClient,
}
}
func TestForwardingAndFilterInterfacesAreSeparated(t *testing.T) {
filterType := reflect.TypeOf((*firewall.FilterClient)(nil)).Elem()
for _, method := range []string{"ListForward", "PortForward", "EnableForward"} {
if _, ok := filterType.MethodByName(method); ok {
t.Fatalf("filter interface still exposes %s", method)
}
}
firewallServiceType := reflect.TypeOf((*IFirewallService)(nil)).Elem()
if _, ok := firewallServiceType.MethodByName("OperateForwardRule"); ok {
t.Fatal("firewall service still owns forwarding writes")
}
forwardingServiceType := reflect.TypeOf((*IForwardingService)(nil)).Elem()
for _, method := range []string{"LoadBaseInfo", "SearchWithPage", "Operate", "Enable", "Replay"} {
if _, ok := forwardingServiceType.MethodByName(method); !ok {
t.Fatalf("forwarding service missing %s", method)
}
}
}
func TestForwardingInitRequestContract(t *testing.T) {
req := dto.IptablesOp{Name: "1PANEL_FORWARD", Operate: "init-forward"}
if err := validator.New().Struct(req); err != nil {
t.Fatalf("frontend forwarding initialization request must remain valid: %v", err)
}
}
func TestForwardingSearchPreservesAPIShapeAndPagination(t *testing.T) {
adapter := &fakeForwardingAdapter{name: "iptables", rules: []forwardClient.Rule{
{Num: "1", Protocol: "tcp", Port: "8080", TargetIP: "10.0.0.2", TargetPort: "80", Interface: "eth0"},
{Num: "2", Protocol: "udp", Port: "5353", TargetIP: "127.0.0.1", TargetPort: "53"},
}}
service := forwardingServiceWithAdapter(adapter)
total, value, err := service.SearchWithPage(dto.ForwardRuleSearch{PageInfo: dto.PageInfo{Page: 1, PageSize: 10}, Info: "10.0.0.2"})
if err != nil {
t.Fatal(err)
}
if total != 1 {
t.Fatalf("got total %d want 1", total)
}
items, ok := value.([]dto.ForwardRule)
if !ok || len(items) != 1 || items[0].Port != "8080" {
t.Fatalf("unexpected items: %#v", value)
}
data, err := json.Marshal(items[0])
if err != nil {
t.Fatal(err)
}
var fields map[string]interface{}
if err := json.Unmarshal(data, &fields); err != nil {
t.Fatal(err)
}
wantFields := []string{"id", "chain", "family", "address", "port", "protocol", "strategy", "num", "targetIP", "targetPort", "interface", "usedStatus", "description"}
for _, field := range wantFields {
if _, ok := fields[field]; !ok {
t.Fatalf("forward response dropped compatibility field %q: %s", field, data)
}
}
}
func TestForwardingOperatePreservesDuplicateAndOrderingContracts(t *testing.T) {
existing := &fakeForwardingAdapter{name: "ufw", rules: []forwardClient.Rule{
{Protocol: "tcp", Port: "8080", TargetIP: "127.0.0.1", TargetPort: "80"},
}}
service := forwardingServiceWithAdapter(existing)
err := service.Operate(dto.ForwardRuleOperate{Rules: []dto.ForwardRuleOperation{{
Operation: "add", Protocol: "tcp", Port: "8080", TargetPort: "80",
}}})
if err == nil {
t.Fatal("duplicate forwarding rule must be rejected")
}
if len(existing.calls) != 0 {
t.Fatalf("duplicate check wrote forwarding state: %#v", existing.calls)
}
adapter := &fakeForwardingAdapter{name: "iptables"}
service = forwardingServiceWithAdapter(adapter)
err = service.Operate(dto.ForwardRuleOperate{Rules: []dto.ForwardRuleOperation{
{Operation: "add", Protocol: "tcp/udp", Port: "9000", TargetIP: "10.0.0.2", TargetPort: "90"},
{Operation: "remove", Num: "1", Protocol: "tcp", Port: "8001", TargetIP: "10.0.0.2", TargetPort: "81"},
{Operation: "remove", Num: "3", Protocol: "tcp", Port: "8003", TargetIP: "10.0.0.2", TargetPort: "83"},
}})
if err != nil {
t.Fatal(err)
}
want := []forwardingCall{
{operation: "remove", rule: forwardClient.Rule{Num: "3", Protocol: "tcp", Port: "8003", TargetIP: "10.0.0.2", TargetPort: "83"}},
{operation: "remove", rule: forwardClient.Rule{Num: "1", Protocol: "tcp", Port: "8001", TargetIP: "10.0.0.2", TargetPort: "81"}},
{operation: "add", rule: forwardClient.Rule{Protocol: "tcp", Port: "9000", TargetIP: "10.0.0.2", TargetPort: "90"}},
{operation: "add", rule: forwardClient.Rule{Protocol: "udp", Port: "9000", TargetIP: "10.0.0.2", TargetPort: "90"}},
}
if !reflect.DeepEqual(adapter.calls, want) {
t.Fatalf("operation order changed\ngot %#v\nwant %#v", adapter.calls, want)
}
}
func TestForwardingSearchReturnsAdapterError(t *testing.T) {
wantErr := errors.New("list failed")
service := forwardingServiceWithAdapter(&fakeForwardingAdapter{name: "firewalld", listErr: wantErr})
_, _, err := service.SearchWithPage(dto.ForwardRuleSearch{PageInfo: dto.PageInfo{Page: 1, PageSize: 20}})
if !errors.Is(err, wantErr) {
t.Fatalf("got %v want %v", err, wantErr)
}
}
+7 -3
View File
@@ -21,7 +21,7 @@ type FtpService struct{}
type IFtpService interface { type IFtpService interface {
LoadBaseInfo() (dto.FtpBaseInfo, error) LoadBaseInfo() (dto.FtpBaseInfo, error)
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error) SearchWithPage(search dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error)
Operate(operation string) error Operate(operation string) error
Create(req dto.FtpCreate) (uint, error) Create(req dto.FtpCreate) (uint, error)
CreateWebsite(req dto.FtpCreate) (uint, error) CreateWebsite(req dto.FtpCreate) (uint, error)
@@ -74,7 +74,7 @@ func (u *FtpService) Operate(operation string) error {
return client.Operate(operation) return client.Operate(operation)
} }
func (f *FtpService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) { func (f *FtpService) SearchWithPage(req dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error) {
total, lists, err := ftpRepo.Page(req.Page, req.PageSize, ftpRepo.WithLikeUser(req.Info), repo.WithOrderDesc("created_at")) total, lists, err := ftpRepo.Page(req.Page, req.PageSize, ftpRepo.WithLikeUser(req.Info), repo.WithOrderDesc("created_at"))
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
@@ -85,7 +85,11 @@ func (f *FtpService) SearchWithPage(req dto.SearchWithPage) (int64, interface{},
if err := copier.Copy(&item, &user); err != nil { if err := copier.Copy(&item, &user); err != nil {
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
} }
item.Password, _ = encrypt.StringDecrypt(item.Password) if isDemoReadOnly(readOnly...) {
item.Password = ""
} else {
item.Password, _ = encrypt.StringDecrypt(item.Password)
}
users = append(users, item) users = append(users, item)
} }
return total, users, err return total, users, err
+7 -2
View File
@@ -21,7 +21,7 @@ type IHostService interface {
TestByInfo(req dto.HostConnTest) bool TestByInfo(req dto.HostConnTest) bool
GetHostByID(id uint) (*dto.HostInfo, error) GetHostByID(id uint) (*dto.HostInfo, error)
SearchForTree(search dto.SearchForTree) ([]dto.HostTree, error) SearchForTree(search dto.SearchForTree) ([]dto.HostTree, error)
SearchWithPage(search dto.SearchPageWithGroup) (int64, interface{}, error) SearchWithPage(search dto.SearchPageWithGroup, readOnly ...bool) (int64, interface{}, error)
Create(req dto.HostOperate) (*dto.HostInfo, error) Create(req dto.HostOperate) (*dto.HostInfo, error)
Update(id uint, upMap map[string]interface{}) (*dto.HostInfo, error) Update(id uint, upMap map[string]interface{}) (*dto.HostInfo, error)
Delete(id []uint) error Delete(id []uint) error
@@ -111,7 +111,7 @@ func (u *HostService) TestLocalConn(id uint) bool {
return true return true
} }
func (u *HostService) SearchWithPage(req dto.SearchPageWithGroup) (int64, interface{}, error) { func (u *HostService) SearchWithPage(req dto.SearchPageWithGroup, readOnly ...bool) (int64, interface{}, error) {
var options []repo.DBOption var options []repo.DBOption
if len(req.Info) != 0 { if len(req.Info) != 0 {
options = append(options, hostRepo.WithByInfo(req.Info)) options = append(options, hostRepo.WithByInfo(req.Info))
@@ -155,6 +155,11 @@ func (u *HostService) SearchWithPage(req dto.SearchPageWithGroup) (int64, interf
} }
} }
} }
if isDemoReadOnly(readOnly...) {
item.Password = ""
item.PrivateKey = ""
item.PassPhrase = ""
}
dtoHosts = append(dtoHosts, item) dtoHosts = append(dtoHosts, item)
} }
return total, dtoHosts, err return total, dtoHosts, err
+71 -23
View File
@@ -29,6 +29,7 @@ import (
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/homedir"
) )
type ImageService struct{} type ImageService struct{}
@@ -277,37 +278,38 @@ func (u *ImageService) ImagePull(req dto.ImagePull) error {
itemName := strings.ReplaceAll(path.Base(item), ":", "_") itemName := strings.ReplaceAll(path.Base(item), ":", "_")
taskItem.AddSubTask(i18n.GetWithName("ImagePull", itemName), func(t *task.Task) error { taskItem.AddSubTask(i18n.GetWithName("ImagePull", itemName), func(t *task.Task) error {
taskItem.Logf("----------------- %s -----------------", itemName) taskItem.Logf("----------------- %s -----------------", itemName)
if req.RepoID == 0 {
pullErr := pullImages(taskItem, client, item)
taskItem.LogWithStatus(i18n.GetMsgByKey("TaskPull"), pullErr)
return pullErr
}
options := image.PullOptions{} options := image.PullOptions{}
imageName := item imageName := item
repo, repoErr := imageRepoRepo.Get(repo.WithByID(req.RepoID)) if req.RepoID == 0 {
taskItem.LogWithStatus(i18n.GetMsgByKey("ImageRepoAuthFromDB"), repoErr) hasAuth, authStr := loadAuthInfo(item)
if repoErr != nil { if hasAuth {
return repoErr options.RegistryAuth = authStr
}
if repo.Auth {
authConfig := registry.AuthConfig{
Username: repo.Username,
Password: repo.Password,
} }
encodedJSON, err := json.Marshal(authConfig) } else {
repo, err := imageRepoRepo.Get(repo.WithByID(req.RepoID))
taskItem.LogWithStatus(i18n.GetMsgByKey("ImageRepoAuthFromDB"), err)
if err != nil { if err != nil {
return err return err
} }
authStr := base64.URLEncoding.EncodeToString(encodedJSON) if repo.Auth {
options.RegistryAuth = authStr 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
} }
imageName = repo.DownloadUrl + "/" + item
dockerCli := docker.NewClientWithExist(client) dockerCli := docker.NewClientWithExist(client)
pullErr := dockerCli.PullImageWithProcessAndOptions(taskItem, imageName, options) err = dockerCli.PullImageWithProcessAndOptions(taskItem, imageName, options)
taskItem.LogWithStatus(i18n.GetMsgByKey("TaskPull"), pullErr) taskItem.LogWithStatus(i18n.GetMsgByKey("TaskPull"), err)
if pullErr != nil { if err != nil {
return pullErr return err
} }
return nil return nil
}, nil) }, nil)
@@ -545,3 +547,49 @@ func checkUsed(imageID string, containers []container.Summary) bool {
} }
return false 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"`
}
+5 -2
View File
@@ -24,7 +24,7 @@ import (
type ImageRepoService struct{} type ImageRepoService struct{}
type IImageRepoService interface { type IImageRepoService interface {
Page(search dto.SearchWithPage) (int64, interface{}, error) Page(search dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error)
List() ([]dto.ImageRepoOption, error) List() ([]dto.ImageRepoOption, error)
Login(req dto.OperateByID) error Login(req dto.OperateByID) error
Create(req dto.ImageRepoCreate) error Create(req dto.ImageRepoCreate) error
@@ -36,10 +36,13 @@ func NewIImageRepoService() IImageRepoService {
return &ImageRepoService{} return &ImageRepoService{}
} }
func (u *ImageRepoService) Page(req dto.SearchWithPage) (int64, interface{}, error) { func (u *ImageRepoService) Page(req dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error) {
total, ops, err := imageRepoRepo.Page(req.Page, req.PageSize, repo.WithByLikeName(req.Info), repo.WithOrderDesc("created_at")) total, ops, err := imageRepoRepo.Page(req.Page, req.PageSize, repo.WithByLikeName(req.Info), repo.WithOrderDesc("created_at"))
var dtoOps []dto.ImageRepoInfo var dtoOps []dto.ImageRepoInfo
for _, op := range ops { for _, op := range ops {
if isDemoReadOnly(readOnly...) {
op.Password = ""
}
var item dto.ImageRepoInfo var item dto.ImageRepoInfo
if err := copier.Copy(&item, &op); err != nil { if err := copier.Copy(&item, &op); err != nil {
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)

Some files were not shown because too many files have changed in this diff Show More