mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-29 00:00:17 +00:00
Compare commits
134
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f91db55daa | ||
|
|
5346d5f58a | ||
|
|
ac09426f10 | ||
|
|
19707a68dd | ||
|
|
7c7fd492c4 | ||
|
|
fbf66766c2 | ||
|
|
d7355f3ccc | ||
|
|
93ca724985 | ||
|
|
9481a7535d | ||
|
|
14be5b7870 | ||
|
|
c4b76a8e89 | ||
|
|
9295f4ef8a | ||
|
|
c925184bf6 | ||
|
|
87828b03ae | ||
|
|
e7b4146e1b | ||
|
|
ec998552b9 | ||
|
|
1af237944d | ||
|
|
f9f9169a4d | ||
|
|
1e01761ba3 | ||
|
|
15283fde34 | ||
|
|
49f8e7f040 | ||
|
|
98b27af6a0 | ||
|
|
a393224729 | ||
|
|
b851c84290 | ||
|
|
372855157d | ||
|
|
577d771495 | ||
|
|
ca3e6511e5 | ||
|
|
8af14eae25 | ||
|
|
175fbaf3f2 | ||
|
|
13199efd62 | ||
|
|
04099ae2a2 | ||
|
|
c695a2a408 | ||
|
|
690cb48280 | ||
|
|
f226fed8cd | ||
|
|
110c292856 | ||
|
|
8e865c2c46 | ||
|
|
17b8f472e7 | ||
|
|
d9dc5d7aa9 | ||
|
|
547f9775b0 | ||
|
|
f37afb1a37 | ||
|
|
3b89d83554 | ||
|
|
12bfe6a1bd | ||
|
|
aaf70cfc41 | ||
|
|
1ecd4f7f0c | ||
|
|
a8ccdcade7 | ||
|
|
4a89c0c031 | ||
|
|
f21ebb1a36 | ||
|
|
3011383b07 | ||
|
|
fd12866897 | ||
|
|
bc6cf519ce | ||
|
|
f9c43989fe | ||
|
|
e90b201001 | ||
|
|
f7394938f7 | ||
|
|
dada7294dd | ||
|
|
afcb94f6ef | ||
|
|
5a2e430dff | ||
|
|
f2ca4f56e6 | ||
|
|
b6f609aaee | ||
|
|
c960c0137d | ||
|
|
09a901294e | ||
|
|
4d481a8f31 | ||
|
|
f1e0aa3391 | ||
|
|
f475500543 | ||
|
|
8415a1ce28 | ||
|
|
faf4933a02 | ||
|
|
c692a9f5e3 | ||
|
|
e7f0c21715 | ||
|
|
da7e8f1dbf | ||
|
|
486cd5d0ad | ||
|
|
00ce5a7b36 | ||
|
|
c17f055e06 | ||
|
|
f22df4b29c | ||
|
|
b9bab90a14 | ||
|
|
ce14a104cc | ||
|
|
2eaabccf94 | ||
|
|
e119239d3d | ||
|
|
14a57af4e9 | ||
|
|
2701af9054 | ||
|
|
4954067736 | ||
|
|
b962144e7d | ||
|
|
8f1be42b6b | ||
|
|
c6969f231b | ||
|
|
3571fb8d86 | ||
|
|
d21288788e | ||
|
|
f65e4b06ab | ||
|
|
d26bc9a3a4 | ||
|
|
7088a903fe | ||
|
|
864ebeed82 | ||
|
|
c1b2b92708 | ||
|
|
2189d9229a | ||
|
|
d0183f460e | ||
|
|
d27d6db1ea | ||
|
|
d2bb3813d9 | ||
|
|
cac73e6d45 | ||
|
|
4712ceaf2c | ||
|
|
65243c68c4 | ||
|
|
256e79ca81 | ||
|
|
4861eb69cb | ||
|
|
fb8cf15537 | ||
|
|
c4a6791271 | ||
|
|
f58e147636 | ||
|
|
387e9fbeed | ||
|
|
4eb627bc79 | ||
|
|
850c86229c | ||
|
|
f984917a66 | ||
|
|
8588217fbf | ||
|
|
a2307c5f64 | ||
|
|
5923290de8 | ||
|
|
1c994fba4a | ||
|
|
415ab96aab | ||
|
|
19bb823b05 | ||
|
|
3a5371652e | ||
|
|
a267b4148a | ||
|
|
36a01eb60d | ||
|
|
65f6fdd045 | ||
|
|
75b60b32e4 | ||
|
|
6cb65e2290 | ||
|
|
0bad1b471f | ||
|
|
3814525edd | ||
|
|
8162dd1856 | ||
|
|
e833787020 | ||
|
|
673ffac516 | ||
|
|
e864610015 | ||
|
|
78402e1b7d | ||
|
|
782bc1e67c | ||
|
|
86e4ed6f64 | ||
|
|
ee8bac39af | ||
|
|
fe742b9f41 | ||
|
|
9a5bd9bcba | ||
|
|
b9c8e39560 | ||
|
|
6b20ff0b13 | ||
|
|
89bd32b6d4 | ||
|
|
005f240fb7 | ||
|
|
75da53e374 |
+12
-16
@@ -72,22 +72,6 @@ func (b *BaseApi) CloseOllamaModel(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, nil)
|
helper.SuccessWithData(c, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags AI
|
|
||||||
// @Summary Sync Ollama model list
|
|
||||||
// @Success 200 {array} dto.OllamaModelDropList
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /ai/ollama/model/sync [post]
|
|
||||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Ollama 模型列表","formatEN":"sync Ollama model list"}
|
|
||||||
func (b *BaseApi) SyncOllamaModel(c *gin.Context) {
|
|
||||||
list, err := aiToolService.Sync()
|
|
||||||
if err != nil {
|
|
||||||
helper.BadRequest(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.SuccessWithData(c, list)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Page Ollama models
|
// @Summary Page Ollama models
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -114,6 +98,18 @@ func (b *BaseApi) SearchOllamaModel(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) SyncOllamaModelAccount(c *gin.Context) {
|
||||||
|
var req dto.OllamaModelAccountSync
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := aiToolService.SyncAccount(req); err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Page Ollama models
|
// @Summary Page Ollama models
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *BaseApi) AIBenchmarkSources(c *gin.Context) {
|
||||||
|
sources, err := service.AIBenchmarkSources(c.Query("kind"), c.Query("selected"))
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, sources)
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ import (
|
|||||||
"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/repo"
|
||||||
"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/i18n"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/webhook_sender"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -290,7 +293,7 @@ func (b *BaseApi) PageAlertConfig(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /alert/config/update [post]
|
// @Router /alert/config/update [post]
|
||||||
// @x-panel-log {"bodyKeys":["id","displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [id][displayName]","formatEN":"update alert config [id][displayName]"}
|
// @x-panel-log {"bodyKeys":["displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [displayName]","formatEN":"update alert config [displayName]"}
|
||||||
func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
||||||
var req dto.AlertConfigUpdate
|
var req dto.AlertConfigUpdate
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
@@ -318,13 +321,15 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /alert/config/status [post]
|
// @Router /alert/config/status [post]
|
||||||
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [id][status]","formatEN":"update alert config status [id][status]"}
|
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [name][status]","formatEN":"update alert config status [name][status]"}
|
||||||
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
|
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
|
||||||
var req dto.AlertConfigStatusUpdate
|
var req dto.AlertConfigStatusUpdate
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c)); err != nil {
|
name, err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c))
|
||||||
|
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -350,13 +355,14 @@ func loadAuditUser(c *gin.Context) string {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /alert/config/del [post]
|
// @Router /alert/config/del [post]
|
||||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [id]","formatEN":"delete alert config [id]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [name]","formatEN":"delete alert config [name]"}
|
||||||
func (b *BaseApi) DeleteAlertConfig(c *gin.Context) {
|
func (b *BaseApi) DeleteAlertConfig(c *gin.Context) {
|
||||||
var req dto.DeleteRequest
|
var req dto.DeleteRequest
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := alertService.DeleteAlertConfig(req.ID)
|
name, err := alertService.DeleteAlertConfig(req.ID)
|
||||||
|
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -378,10 +384,24 @@ func (b *BaseApi) TestAlertConfig(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.Type == constant.Custom {
|
if req.Type == constant.Custom {
|
||||||
result, err := alertService.TestCustomAlertConfig(req)
|
lang := strings.TrimSpace(c.GetHeader("Accept-Language"))
|
||||||
|
if lang == "" {
|
||||||
|
lang = i18n.GetLanguageFromDB()
|
||||||
|
}
|
||||||
|
result, err := alertService.TestCustomAlertConfig(req, lang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
if global.LOG != nil {
|
||||||
return
|
global.LOG.Warnf("webhook test preparation failed: configID=%d error=%q", req.ID,
|
||||||
|
webhook_sender.DiagnosticText(err.Error(), webhook_sender.Request{
|
||||||
|
Format: webhook_sender.BodyJSON, Body: []byte(req.Config),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
result = dto.AlertConfigTestResult{Success: false}
|
||||||
|
}
|
||||||
|
result.Response = ""
|
||||||
|
result.Message = ""
|
||||||
|
if !result.Success {
|
||||||
|
result.Message = i18n.GetMsgByKeyAndLang(lang, "ErrWebhookTestFailed")
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, result)
|
helper.SuccessWithData(c, result)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/appicon"
|
"github.com/1Panel-dev/1Panel/agent/utils/appicon"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -160,10 +161,25 @@ func (b *BaseApi) GetAppDetailByID(c *gin.Context) {
|
|||||||
// @Router /apps/install [post]
|
// @Router /apps/install [post]
|
||||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"安装应用 [name]","formatEN":"Install app [name]"}
|
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"安装应用 [name]","formatEN":"Install app [name]"}
|
||||||
func (b *BaseApi) InstallApp(c *gin.Context) {
|
func (b *BaseApi) InstallApp(c *gin.Context) {
|
||||||
|
mcpRequest, trustedMCP := mcpapi.Check(c.Request)
|
||||||
|
if mcpRequest && !trustedMCP {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
var req request.AppInstallCreate
|
var req request.AppInstallCreate
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if mcpRequest {
|
||||||
|
req.MCPPreserveWebsiteDir = true
|
||||||
|
if value, exists := req.Params["WEBSITE_DIR"]; exists {
|
||||||
|
directory, ok := value.(string)
|
||||||
|
if !ok || directory == "" || directory != settingService.GetWebsiteDir() {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
install, err := appService.Install(req, true)
|
install, err := appService.Install(req, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"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/service"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||||
|
|
||||||
"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"
|
||||||
@@ -336,11 +340,25 @@ func (b *BaseApi) UpdateAppConfig(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /apps/installed/info/:appInstallId [get]
|
// @Router /apps/installed/info/:appInstallId [get]
|
||||||
func (b *BaseApi) GetAppInstallInfo(c *gin.Context) {
|
func (b *BaseApi) GetAppInstallInfo(c *gin.Context) {
|
||||||
|
mcpRequest, trustedMCP := mcpapi.Check(c.Request)
|
||||||
|
if mcpRequest && !trustedMCP {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
appInstallId, err := helper.GetIntParamByKey(c, "appInstallId")
|
appInstallId, err := helper.GetIntParamByKey(c, "appInstallId")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if mcpRequest {
|
||||||
|
info, err := (&service.AppInstallService{}).GetAppInstallInfoForMCP(c.Request.Context(), appInstallId)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, info)
|
||||||
|
return
|
||||||
|
}
|
||||||
info, err := appInstallService.GetAppInstallInfo(appInstallId)
|
info, err := appInstallService.GetAppInstallInfo(appInstallId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func (b *BaseApi) SearchContainer(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, list, err := containerService.Page(req)
|
total, list, err := containerService.Page(c.Request.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -249,9 +249,10 @@ func (b *BaseApi) ListContainerByImage(c *gin.Context) {
|
|||||||
// @Success 200 {object} dto.ContainerStatus
|
// @Success 200 {object} dto.ContainerStatus
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
|
// @Param containersOnly query boolean false "Only count containers"
|
||||||
// @Router /containers/status [get]
|
// @Router /containers/status [get]
|
||||||
func (b *BaseApi) LoadContainerStatus(c *gin.Context) {
|
func (b *BaseApi) LoadContainerStatus(c *gin.Context) {
|
||||||
data, err := containerService.LoadStatus()
|
data, err := containerService.LoadStatus(c.Request.Context(), c.Query("containersOnly") == "true")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -415,9 +416,14 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) {
|
|||||||
// @Success 200 {array} dto.ContainerListStats
|
// @Success 200 {array} dto.ContainerListStats
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
|
// @Param ids query string false "Comma-separated container IDs; omitted selects all containers"
|
||||||
// @Router /containers/list/stats [get]
|
// @Router /containers/list/stats [get]
|
||||||
func (b *BaseApi) ContainerListStats(c *gin.Context) {
|
func (b *BaseApi) ContainerListStats(c *gin.Context) {
|
||||||
data, err := containerService.ContainerListStats()
|
var ids []string
|
||||||
|
if _, supplied := c.Request.URL.Query()["ids"]; supplied {
|
||||||
|
ids = strings.FieldsFunc(c.Query("ids"), func(r rune) bool { return r == ',' })
|
||||||
|
}
|
||||||
|
data, err := containerService.ContainerListStats(c.Request.Context(), ids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package v2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"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/service"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -98,11 +101,25 @@ func (b *BaseApi) SearchDatabase(c *gin.Context) {
|
|||||||
// @Param type path string true "type"
|
// @Param type path string true "type"
|
||||||
// @Router /databases/db/list/:type [get]
|
// @Router /databases/db/list/:type [get]
|
||||||
func (b *BaseApi) ListDatabase(c *gin.Context) {
|
func (b *BaseApi) ListDatabase(c *gin.Context) {
|
||||||
|
mcpRequest, trustedMCP := mcpapi.Check(c.Request)
|
||||||
|
if mcpRequest && !trustedMCP {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
dbType, err := helper.GetStrParamByKey(c, "type")
|
dbType, err := helper.GetStrParamByKey(c, "type")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if mcpRequest {
|
||||||
|
list, err := (&service.DatabaseService{}).ListForMCP(c.Request.Context(), dbType)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, list)
|
||||||
|
return
|
||||||
|
}
|
||||||
list, err := databaseService.List(dbType)
|
list, err := databaseService.List(dbType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ package v2
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"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/service"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,6 +22,11 @@ import (
|
|||||||
// @Router /databases [post]
|
// @Router /databases [post]
|
||||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 mysql 数据库 [name]","formatEN":"create mysql database [name]"}
|
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 mysql 数据库 [name]","formatEN":"create mysql database [name]"}
|
||||||
func (b *BaseApi) CreateMysql(c *gin.Context) {
|
func (b *BaseApi) CreateMysql(c *gin.Context) {
|
||||||
|
mcpRequest, trustedMCP := mcpapi.Check(c.Request)
|
||||||
|
if mcpRequest && !trustedMCP {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
var req dto.MysqlDBCreate
|
var req dto.MysqlDBCreate
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
@@ -32,6 +40,16 @@ func (b *BaseApi) CreateMysql(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
req.Password = string(password)
|
req.Password = string(password)
|
||||||
}
|
}
|
||||||
|
if mcpRequest {
|
||||||
|
created, err := (&service.MysqlService{}).CreateForMCP(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nodeID, _ := mcpapi.NodeID(c.Request)
|
||||||
|
helper.SuccessWithData(c, map[string]any{"id": created.ID, "database": created.MysqlName, "name": created.Name, "nodeId": nodeID})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := mysqlService.Create(context.Background(), req); err != nil {
|
if _, err := mysqlService.Create(context.Background(), req); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
@@ -513,11 +531,25 @@ func (b *BaseApi) LoadRemoteAccess(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /databases/status [post]
|
// @Router /databases/status [post]
|
||||||
func (b *BaseApi) LoadStatus(c *gin.Context) {
|
func (b *BaseApi) LoadStatus(c *gin.Context) {
|
||||||
|
mcpRequest, trustedMCP := mcpapi.Check(c.Request)
|
||||||
|
if mcpRequest && !trustedMCP {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
var req dto.OperationWithNameAndType
|
var req dto.OperationWithNameAndType
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if mcpRequest {
|
||||||
|
data, err := (&service.MysqlService{}).LoadStatusForMCP(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
return
|
||||||
|
}
|
||||||
data, err := mysqlService.LoadStatus(req)
|
data, err := mysqlService.LoadStatus(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
|
|||||||
@@ -86,17 +86,23 @@ func (b *BaseApi) CheckHasCli(c *gin.Context) {
|
|||||||
|
|
||||||
// @Tags Database Redis
|
// @Tags Database Redis
|
||||||
// @Summary Install redis-cli
|
// @Summary Install redis-cli
|
||||||
// @Success 200
|
// @Accept json
|
||||||
|
// @Param request body dto.RedisCliInstall true "request"
|
||||||
|
// @Success 200 {string} string
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /databases/redis/install/cli [post]
|
// @Router /databases/redis/install/cli [post]
|
||||||
func (b *BaseApi) InstallCli(c *gin.Context) {
|
func (b *BaseApi) InstallCli(c *gin.Context) {
|
||||||
if err := redisService.InstallCli(); err != nil {
|
var req dto.RedisCliInstall
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := redisService.InstallCli(req)
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
helper.Success(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Database Redis
|
// @Tags Database Redis
|
||||||
|
|||||||
+162
-166
@@ -2,14 +2,14 @@ package v2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"net/http"
|
"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/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -60,7 +60,7 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
|||||||
// @Summary Operate firewall
|
// @Summary Operate firewall
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallLifecycleOperation true "request"
|
// @Param request body dto.FirewallLifecycleOperation true "request"
|
||||||
// @Success 200
|
// @Success 200 {object} dto.FirewallLifecycleOperationResponse
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/operate [post]
|
// @Router /hosts/firewall/operate [post]
|
||||||
@@ -71,12 +71,13 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := firewallService.OperateFirewall(request); err != nil {
|
result, err := firewallService.QueueFirewallOperation(request)
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
helper.Success(c)
|
helper.SuccessWithData(c, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
@@ -87,7 +88,7 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/forward/base [post]
|
// @Router /hosts/firewall/forward/base [post]
|
||||||
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) {
|
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) {
|
||||||
data, err := forwardingService.LoadBaseInfo()
|
data, err := forwardingService.LoadBaseInfo(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -108,7 +109,7 @@ func (b *BaseApi) SearchForwardingRules(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
total, items, err := forwardingService.SearchRules(request)
|
total, items, err := forwardingService.SearchRules(c.Request.Context(), request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -259,28 +260,6 @@ func (b *BaseApi) LoadFirewallNativeDetail(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, info)
|
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
|
// @Tags Firewall
|
||||||
// @Summary Queue firewall rule creation
|
// @Summary Queue firewall rule creation
|
||||||
// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log.
|
// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log.
|
||||||
@@ -307,67 +286,8 @@ func (b *BaseApi) CreateFirewallRules(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Preview firewall rule synchronization
|
// @Summary Queue firewall rule deletion
|
||||||
// @Accept json
|
// @Description Deletes non-whitelist rules by scope and instance key. Returns a taskID immediately; results are written to the task log.
|
||||||
// @Param request body dto.FirewallRuleSyncRequest true "request"
|
|
||||||
// @Success 200 {object} dto.FirewallRuleSyncPreview
|
|
||||||
// @Failure 400 {object} dto.Response
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /hosts/firewall/rules/sync/preview [post]
|
|
||||||
func (b *BaseApi) PreviewFirewallRuleSync(c *gin.Context) {
|
|
||||||
var request dto.FirewallRuleSyncRequest
|
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result, err := firewallService.PreviewRuleSync(c.Request.Context(), c.ClientIP(), request)
|
|
||||||
if err != nil {
|
|
||||||
handleFirewallRuleError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.SuccessWithData(c, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags Firewall
|
|
||||||
// @Summary Load the currently executing firewall rule synchronization task
|
|
||||||
// @Success 200 {object} dto.FirewallRuleSyncTask
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /hosts/firewall/rules/sync/task [get]
|
|
||||||
func (b *BaseApi) LoadFirewallRuleSyncTask(c *gin.Context) {
|
|
||||||
result, err := firewallService.CurrentRuleSyncTask()
|
|
||||||
if err != nil {
|
|
||||||
helper.InternalServer(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.SuccessWithData(c, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags Firewall
|
|
||||||
// @Summary Synchronize firewall rules to a target backend
|
|
||||||
// @Accept json
|
|
||||||
// @Param request body dto.FirewallRuleSyncRequest true "request"
|
|
||||||
// @Success 200 {object} dto.FirewallRuleSyncResult
|
|
||||||
// @Failure 400 {object} dto.Response
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /hosts/firewall/rules/sync [post]
|
|
||||||
// @x-panel-log {"bodyKeys":["subsystem","sourceProvider","targetProvider"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 [subsystem] 防火墙规则到 [targetProvider]","formatEN":"sync [subsystem] firewall rules to [targetProvider]"}
|
|
||||||
func (b *BaseApi) SyncFirewallRules(c *gin.Context) {
|
|
||||||
var request dto.FirewallRuleSyncRequest
|
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result, err := firewallService.SyncRules(c.Request.Context(), c.ClientIP(), request)
|
|
||||||
if err != nil {
|
|
||||||
handleFirewallRuleError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
helper.SuccessWithData(c, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags Firewall
|
|
||||||
// @Summary Delete managed unified firewall v2 rules
|
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallRuleDelete true "request"
|
// @Param request body dto.FirewallRuleDelete true "request"
|
||||||
// @Success 200 {object} dto.FirewallRuleDeleteResponse
|
// @Success 200 {object} dto.FirewallRuleDeleteResponse
|
||||||
@@ -390,7 +310,7 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Update a managed unified firewall v2 rule
|
// @Summary Update a firewall rule
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallRuleUpdate true "request"
|
// @Param request body dto.FirewallRuleUpdate true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
@@ -398,16 +318,13 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/rules/update [post]
|
// @Router /hosts/firewall/rules/update [post]
|
||||||
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [uuid]","formatEN":"update firewall rule [uuid]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则","formatEN":"update firewall rule"}
|
||||||
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||||
var request dto.FirewallRuleUpdate
|
var request dto.FirewallRuleUpdate
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !normalizeFirewallRuleUUID(c, &request.UUID) {
|
if err := firewallService.Update(c.Request.Context(), request); err != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := firewallService.Update(c.Request.Context(), c.ClientIP(), request); err != nil {
|
|
||||||
handleFirewallRuleError(c, err)
|
handleFirewallRuleError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -415,7 +332,7 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Reorder a managed unified firewall v2 rule
|
// @Summary Reorder a firewall rule
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallRuleReorder true "request"
|
// @Param request body dto.FirewallRuleReorder true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
@@ -423,53 +340,41 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/rules/reorder [post]
|
// @Router /hosts/firewall/rules/reorder [post]
|
||||||
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [uuid]","formatEN":"reorder firewall rule [uuid]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序","formatEN":"reorder firewall rule"}
|
||||||
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
|
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
|
||||||
var request dto.FirewallRuleReorder
|
var request dto.FirewallRuleReorder
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !normalizeFirewallRuleUUID(c, &request.UUID) {
|
if err := firewallService.Reorder(c.Request.Context(), request); err != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := firewallService.Reorder(c.Request.Context(), c.ClientIP(), request); err != nil {
|
|
||||||
handleFirewallRuleError(c, err)
|
handleFirewallRuleError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.Success(c)
|
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) {
|
func handleFirewallRuleError(c *gin.Context, err error) {
|
||||||
|
var businessErr buserr.BusinessError
|
||||||
|
isBusinessError := errors.As(err, &businessErr)
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, filter.ErrProtectedRule):
|
case errors.Is(err, filter.ErrProtectedRule):
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
|
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
|
||||||
case errors.Is(err, filter.ErrRuleStale):
|
case errors.Is(err, filter.ErrRuleStale):
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err)
|
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),
|
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
|
||||||
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
|
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err)
|
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),
|
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation):
|
||||||
errors.Is(err, repo.ErrFirewallPersistenceInvalid):
|
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
|
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||||
case errors.Is(err, filter.ErrVerificationFailed):
|
case isBusinessError && businessErr.Msg == "ErrRecordExist":
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err)
|
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_DUPLICATE", Message: err.Error()})
|
||||||
|
c.Abort()
|
||||||
|
case isBusinessError && businessErr.Msg == "ErrFirewallRuleConflict":
|
||||||
|
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_CONFLICT", Message: err.Error()})
|
||||||
|
c.Abort()
|
||||||
|
case isBusinessError && businessErr.Msg == "ErrInvalidParams":
|
||||||
|
c.JSON(http.StatusOK, dto.Response{Code: http.StatusBadRequest, ErrorCode: "FW_RULE_UNSUPPORTED", Message: err.Error()})
|
||||||
|
c.Abort()
|
||||||
default:
|
default:
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err)
|
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err)
|
||||||
}
|
}
|
||||||
@@ -491,26 +396,69 @@ func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Queue firewall port whitelist update
|
// @Summary Create firewall port whitelist rules
|
||||||
// @Description Returns a taskID; configuration save and per-rule results are recorded in the task log.
|
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
|
// @Param request body dto.FirewallPortWhitelistCreate true "request"
|
||||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/settings/whitelist [post]
|
// @Router /hosts/firewall/settings/whitelist [post]
|
||||||
// @x-panel-log {"bodyKeys":["value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙端口白名单 [value]","formatEN":"update firewall port whitelist [value]"}
|
// @x-panel-log {"bodyKeys":["rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建防火墙端口白名单","formatEN":"create firewall port whitelist"}
|
||||||
|
func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) {
|
||||||
|
var request dto.FirewallPortWhitelistCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := firewallSettingService.CreatePortWhitelist(c.Request.Context(), request); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Update firewall port whitelist rules
|
||||||
|
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/settings/whitelist/update [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["oldRule","rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"编辑防火墙端口白名单","formatEN":"update firewall port whitelist"}
|
||||||
func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
|
func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
|
||||||
var request dto.FirewallPortWhitelistUpdate
|
var request dto.FirewallPortWhitelistUpdate
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result, err := firewallSettingService.QueuePortWhitelist(request.Value)
|
if err := firewallSettingService.UpdatePortWhitelist(c.Request.Context(), request); err != nil {
|
||||||
if err != nil {
|
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, result)
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Delete firewall port whitelist rules
|
||||||
|
// @Description Removes whitelist configuration; existing firewall rules are not removed.
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallPortWhitelistDelete true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/settings/whitelist/delete [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["rules"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除防火墙端口白名单","formatEN":"delete firewall port whitelist"}
|
||||||
|
func (b *BaseApi) DeleteFirewallPortWhitelist(c *gin.Context) {
|
||||||
|
var request dto.FirewallPortWhitelistDelete
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := firewallSettingService.DeletePortWhitelist(c.Request.Context(), request); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
@@ -528,14 +476,10 @@ func (b *BaseApi) OperateFirewallBackend(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := firewallSettingService.Operate(c.Request.Context(), request); err != nil {
|
if err := firewallSettingService.Operate(c.Request.Context(), request); err != nil {
|
||||||
if errors.Is(err, service.ErrFirewallBackendCleanupRequired) {
|
var businessErr buserr.BusinessError
|
||||||
helper.ErrorWithBusinessCode(
|
if errors.As(err, &businessErr) && businessErr.Msg == "ErrFirewallBackendCleanupRequired" {
|
||||||
c,
|
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_BACKEND_CLEANUP_REQUIRED", Message: err.Error()})
|
||||||
http.StatusConflict,
|
c.Abort()
|
||||||
"FW_BACKEND_CLEANUP_REQUIRED",
|
|
||||||
"ErrInvalidParams",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
@@ -574,21 +518,6 @@ func (b *BaseApi) ListDockerPublishedPorts(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, data)
|
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
|
// @Tags Firewall
|
||||||
// @Summary Operate Docker port guard
|
// @Summary Operate Docker port guard
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -627,7 +556,7 @@ func (b *BaseApi) OperateDockerPortGuard(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/docker/policies/delete/batch [post]
|
// @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]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 Docker 端口防护策略","formatEN":"delete Docker port guard policies"}
|
||||||
func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
|
func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
|
||||||
var request dto.DockerPortGuardPolicyBatchDelete
|
var request dto.DockerPortGuardPolicyBatchDelete
|
||||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
@@ -664,21 +593,88 @@ func (b *BaseApi) UpsertDockerPortGuardPolicies(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleDockerPortGuardError(c *gin.Context, err error) {
|
func handleDockerPortGuardError(c *gin.Context, err error) {
|
||||||
if errors.Is(err, service.ErrDockerIptablesChainUnavailable) {
|
var businessErr buserr.BusinessError
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE", "ErrDockerIptablesChainUnavailable", err)
|
if errors.As(err, &businessErr) {
|
||||||
|
code, errorCode := http.StatusInternalServerError, ""
|
||||||
|
switch businessErr.Msg {
|
||||||
|
case "ErrDockerIptablesChainUnavailable":
|
||||||
|
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE"
|
||||||
|
case "ErrDockerNftablesChainUnavailable":
|
||||||
|
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_NFTABLES_CHAIN_UNAVAILABLE"
|
||||||
|
case "ErrInvalidParams":
|
||||||
|
code, errorCode = http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID"
|
||||||
|
case "ErrDockerFailed":
|
||||||
|
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE"
|
||||||
|
}
|
||||||
|
if errorCode != "" {
|
||||||
|
c.JSON(http.StatusOK, dto.Response{Code: code, ErrorCode: errorCode, Message: err.Error()})
|
||||||
|
c.Abort()
|
||||||
return
|
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) {
|
if errors.Is(err, docker.ErrUnavailable) {
|
||||||
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)
|
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE", "ErrDockerFailed", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err)
|
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary List firewall rule backups
|
||||||
|
// @Param subsystem query string false "Firewall subsystem" Enums(system,forwarding,docker) default(system)
|
||||||
|
// @Success 200 {object} dto.FirewallRuleBackups
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/rules/backups [get]
|
||||||
|
func (b *BaseApi) ListFirewallRuleBackups(c *gin.Context) {
|
||||||
|
result, err := firewallService.ListRuleBackups(c.Request.Context(), c.DefaultQuery("subsystem", "system"))
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Initialize, repair or bind one firewall address family
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallFamilyOperation true "request"
|
||||||
|
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/family/operate [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["subsystem","family","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] [subsystem] [family] 防火墙链","formatEN":"[operation] [subsystem] [family] firewall chains"}
|
||||||
|
func (b *BaseApi) OperateFirewallFamily(c *gin.Context) {
|
||||||
|
var request dto.FirewallFamilyOperation
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := firewallSettingService.OperateFamily(request)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Update firewall IPv6 support
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallIPv6Operation true "request"
|
||||||
|
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/settings/ipv6 [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"设置防火墙 IPv6 支持为 [status]","formatEN":"Set firewall IPv6 support to [status]"}
|
||||||
|
func (b *BaseApi) OperateFirewallIPv6(c *gin.Context) {
|
||||||
|
var request dto.FirewallIPv6Operation
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := firewallSettingService.OperateIPv6(request)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package helper
|
|||||||
import (
|
import (
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
@@ -21,6 +23,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
||||||
|
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||||
res := dto.Response{
|
res := dto.Response{
|
||||||
Code: code,
|
Code: code,
|
||||||
Message: "",
|
Message: "",
|
||||||
@@ -31,6 +34,7 @@ func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey string, err error) {
|
func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey string, err error) {
|
||||||
|
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||||
res := dto.Response{
|
res := dto.Response{
|
||||||
Code: code,
|
Code: code,
|
||||||
ErrorCode: businessCode,
|
ErrorCode: businessCode,
|
||||||
@@ -41,6 +45,7 @@ func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
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{}) {
|
||||||
|
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||||
res := dto.Response{
|
res := dto.Response{
|
||||||
Code: code,
|
Code: code,
|
||||||
Data: data,
|
Data: data,
|
||||||
@@ -184,3 +189,23 @@ func GetParamInt32(paramName string, c *gin.Context) (int32, error) {
|
|||||||
intNum, _ := strconv.Atoi(idParam)
|
intNum, _ := strconv.Atoi(idParam)
|
||||||
return int32(intNum), nil
|
return int32(intNum), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetOperationLogValues supplies server-resolved values to Core's operation log.
|
||||||
|
// Call before writing the response. Only pass public audit fields, never credentials.
|
||||||
|
func SetOperationLogValues(c *gin.Context, values map[string]string) {
|
||||||
|
const header = "X-Op-Resolved"
|
||||||
|
resolved := make(map[string]string)
|
||||||
|
if data, err := base64.RawURLEncoding.DecodeString(c.Writer.Header().Get(header)); err == nil {
|
||||||
|
_ = json.Unmarshal(data, &resolved)
|
||||||
|
}
|
||||||
|
if resolved == nil {
|
||||||
|
resolved = make(map[string]string)
|
||||||
|
}
|
||||||
|
for key, value := range values {
|
||||||
|
resolved[key] = value
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(resolved)
|
||||||
|
if err == nil {
|
||||||
|
c.Header(header, base64.RawURLEncoding.EncodeToString(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+10
-23
@@ -1,13 +1,9 @@
|
|||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
|
||||||
|
|
||||||
"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/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/shirou/gopsutil/v4/disk"
|
|
||||||
"github.com/shirou/gopsutil/v4/net"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// @Tags Monitor
|
// @Tags Monitor
|
||||||
@@ -32,14 +28,19 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Monitor
|
// @Tags Monitor
|
||||||
// @Summary Clean monitor data
|
// @Summary Clean host or GPU monitor data
|
||||||
|
// @Param request body dto.MonitorClean true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/monitor/clean [post]
|
// @Router /hosts/monitor/clean [post]
|
||||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空监控数据","formatEN":"clean monitor datas"}
|
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 [type] 监控数据","formatEN":"clean [type] monitoring data"}
|
||||||
func (b *BaseApi) CleanMonitor(c *gin.Context) {
|
func (b *BaseApi) CleanMonitor(c *gin.Context) {
|
||||||
if err := monitorService.CleanData(); err != nil {
|
var req dto.MonitorClean
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := monitorService.CleanData(req.Type); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -91,14 +92,7 @@ func (b *BaseApi) UpdateMonitorSetting(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/monitor/netoptions [get]
|
// @Router /hosts/monitor/netoptions [get]
|
||||||
func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
||||||
netStat, _ := net.IOCounters(true)
|
helper.SuccessWithData(c, monitorService.LoadNetworkOptions())
|
||||||
var options []string
|
|
||||||
options = append(options, "all")
|
|
||||||
for _, net := range netStat {
|
|
||||||
options = append(options, net.Name)
|
|
||||||
}
|
|
||||||
sort.Strings(options)
|
|
||||||
helper.SuccessWithData(c, options)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Monitor
|
// @Tags Monitor
|
||||||
@@ -108,12 +102,5 @@ func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/monitor/iooptions [get]
|
// @Router /hosts/monitor/iooptions [get]
|
||||||
func (b *BaseApi) GetIOOptions(c *gin.Context) {
|
func (b *BaseApi) GetIOOptions(c *gin.Context) {
|
||||||
diskStat, _ := disk.IOCounters()
|
helper.SuccessWithData(c, monitorService.LoadIOOptions())
|
||||||
var options []string
|
|
||||||
options = append(options, "all")
|
|
||||||
for _, net := range diskStat {
|
|
||||||
options = append(options, net.Name)
|
|
||||||
}
|
|
||||||
sort.Strings(options)
|
|
||||||
helper.SuccessWithData(c, options)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *BaseApi) LoadVLLMMonitor(c *gin.Context) {
|
||||||
|
var req dto.MonitorVLLMSearch
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := monitorService.LoadVLLMMonitorData(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) LoadVLLMCurrent(c *gin.Context) {
|
||||||
|
var req dto.MonitorVLLMCurrent
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := monitorService.LoadVLLMCurrent(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) CleanVLLMMonitor(c *gin.Context) {
|
||||||
|
var req dto.MonitorVLLMClean
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := monitorService.CleanVLLMMonitor(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Tags Dashboard
|
||||||
|
// @Summary List quick directories
|
||||||
|
// @Success 200 {array} model.QuickDirectory
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /dashboard/quick/directory [get]
|
||||||
|
func (b *BaseApi) ListQuickDirectories(c *gin.Context) {
|
||||||
|
items, err := dashboardService.ListQuickDirectories()
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Dashboard
|
||||||
|
// @Summary Create quick directory
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.QuickDirectoryCreate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /dashboard/quick/directory [post]
|
||||||
|
func (b *BaseApi) CreateQuickDirectory(c *gin.Context) {
|
||||||
|
var req dto.QuickDirectoryCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := dashboardService.CreateQuickDirectory(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Dashboard
|
||||||
|
// @Summary Update quick directory
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.QuickDirectoryUpdate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /dashboard/quick/directory/update [post]
|
||||||
|
func (b *BaseApi) UpdateQuickDirectory(c *gin.Context) {
|
||||||
|
var req dto.QuickDirectoryUpdate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := dashboardService.UpdateQuickDirectory(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Dashboard
|
||||||
|
// @Summary Delete quick directory
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.QuickDirectoryDelete true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /dashboard/quick/directory/del [post]
|
||||||
|
func (b *BaseApi) DeleteQuickDirectory(c *gin.Context) {
|
||||||
|
var req dto.QuickDirectoryDelete
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := dashboardService.DeleteQuickDirectory(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
@@ -61,13 +61,14 @@ func (b *BaseApi) CreateRuntime(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /runtimes/del [post]
|
// @Router /runtimes/del [post]
|
||||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除运行环境 [id]","formatEN":"Delete runtime [id]"}
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除运行环境 [name]","formatEN":"Delete runtime [name]"}
|
||||||
func (b *BaseApi) DeleteRuntime(c *gin.Context) {
|
func (b *BaseApi) DeleteRuntime(c *gin.Context) {
|
||||||
var req request.RuntimeDelete
|
var req request.RuntimeDelete
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := runtimeService.Delete(req)
|
name, err := runtimeService.Delete(req)
|
||||||
|
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -169,7 +170,7 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /runtimes/operate [post]
|
// @Router /runtimes/operate [post]
|
||||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [id]","formatEN":"Operate runtime [id]"}
|
// @x-panel-log {"bodyKeys":["ID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ID","isList":false,"db":"runtimes","output_column":"name","output_value":"name"}],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"}
|
||||||
func (b *BaseApi) OperateRuntime(c *gin.Context) {
|
func (b *BaseApi) OperateRuntime(c *gin.Context) {
|
||||||
var req request.RuntimeOperate
|
var req request.RuntimeOperate
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ func (b *BaseApi) CreateRootCert(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err := sshService.CreateRootCert(req); err != nil {
|
if err := sshService.CreateRootCert(req); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
@@ -107,6 +108,7 @@ func (b *BaseApi) EditRootCert(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err := sshService.EditRootCert(req); err != nil {
|
if err := sshService.EditRootCert(req); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import (
|
|||||||
// @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 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
|
||||||
@@ -43,6 +44,7 @@ func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
|
|||||||
// @Param command query string false "command"
|
// @Param command query string false "command"
|
||||||
// @Param session query string false "session id to reattach"
|
// @Param session query string false "session id to reattach"
|
||||||
// @Param title query string false "session title shown in the session list"
|
// @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
|
||||||
@@ -149,6 +151,7 @@ func (b *BaseApi) runSSHSession(c *gin.Context, kind string, connect func() (*ss
|
|||||||
Identity: identity,
|
Identity: identity,
|
||||||
Kind: kind,
|
Kind: kind,
|
||||||
Title: sanitizeTerminalTitle(c.Query("title")),
|
Title: sanitizeTerminalTitle(c.Query("title")),
|
||||||
|
Persistent: c.Query("terminalPersistent") == "true",
|
||||||
HostID: uint(max(hostID, 0)),
|
HostID: uint(max(hostID, 0)),
|
||||||
Cols: cols,
|
Cols: cols,
|
||||||
Rows: rows,
|
Rows: rows,
|
||||||
@@ -240,6 +243,16 @@ func loadTerminalIdentity(c *gin.Context) (terminal.Identity, bool) {
|
|||||||
UserID: strings.TrimSpace(c.GetHeader(terminal.HeaderUserID)),
|
UserID: strings.TrimSpace(c.GetHeader(terminal.HeaderUserID)),
|
||||||
AuthSessionID: strings.TrimSpace(c.GetHeader(terminal.HeaderAuthSessionID)),
|
AuthSessionID: strings.TrimSpace(c.GetHeader(terminal.HeaderAuthSessionID)),
|
||||||
}
|
}
|
||||||
|
if value := c.GetHeader(terminal.HeaderAuthLeaseUntil); value != "" {
|
||||||
|
millis, err := strconv.ParseInt(value, 10, 64)
|
||||||
|
if err != nil || millis <= 0 {
|
||||||
|
return terminal.Identity{}, false
|
||||||
|
}
|
||||||
|
identity.AuthLeaseUntil = time.UnixMilli(millis)
|
||||||
|
if maximum := time.Now().Add(90 * time.Second); identity.AuthLeaseUntil.After(maximum) {
|
||||||
|
identity.AuthLeaseUntil = maximum
|
||||||
|
}
|
||||||
|
}
|
||||||
return identity, identity.Valid()
|
return identity, identity.Valid()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *BaseApi) TerminalCapabilities(c *gin.Context) {
|
||||||
|
helper.SuccessWithData(c, gin.H{"apiKeyLeaseVersion": 1})
|
||||||
|
}
|
||||||
@@ -402,7 +402,7 @@ func (b *BaseApi) ChangeDefaultServer(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /websites/php/version [post]
|
// @Router /websites/php/version [post]
|
||||||
// @x-panel-log {"bodyKeys":["websiteId"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteId","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"php 版本变更 [domain]","formatEN":"php version update [domain]"}
|
// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteID","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"php 版本变更 [domain]","formatEN":"php version update [domain]"}
|
||||||
func (b *BaseApi) ChangePHPVersion(c *gin.Context) {
|
func (b *BaseApi) ChangePHPVersion(c *gin.Context) {
|
||||||
var req request.WebsitePHPVersionReq
|
var req request.WebsitePHPVersionReq
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"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/dto/request"
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -176,3 +179,64 @@ func (b *BaseApi) DownloadCAFile(c *gin.Context) {
|
|||||||
c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name()))
|
c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name()))
|
||||||
http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file)
|
http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags Website CA
|
||||||
|
// @Summary Upload website CA archive
|
||||||
|
// @Accept multipart/form-data
|
||||||
|
// @Param file formData file true "ZIP containing ca.crt and ca.key"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/ca/upload [post]
|
||||||
|
func (b *BaseApi) UploadWebsiteCA(c *gin.Context) {
|
||||||
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 11<<20)
|
||||||
|
if err := c.Request.ParseMultipartForm(11 << 20); err != nil {
|
||||||
|
helper.InternalServer(c, buserr.New("ErrCAUpload"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.Request.MultipartForm != nil {
|
||||||
|
defer c.Request.MultipartForm.RemoveAll()
|
||||||
|
}
|
||||||
|
file, err := c.FormFile("file")
|
||||||
|
if err != nil || file.Size > 10<<20 || !strings.HasSuffix(strings.ToLower(file.Filename), ".zip") {
|
||||||
|
helper.InternalServer(c, buserr.New("ErrCAUpload"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stream, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer stream.Close()
|
||||||
|
data, err := io.ReadAll(io.LimitReader(stream, (10<<20)+1))
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = websiteCAService.Upload(file.Filename[:len(file.Filename)-len(".zip")], data); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website CA
|
||||||
|
// @Summary Import website CA from PEM or server files
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.WebsiteCAImport true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/ca/import [post]
|
||||||
|
func (b *BaseApi) ImportWebsiteCA(c *gin.Context) {
|
||||||
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 3<<20)
|
||||||
|
var req request.WebsiteCAImport
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := websiteCAService.Import(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|||||||
+13
-1
@@ -9,7 +9,8 @@ type AgentCreateReq struct {
|
|||||||
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
|
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
|
||||||
BridgePort int `json:"bridgePort"`
|
BridgePort int `json:"bridgePort"`
|
||||||
AllowedOrigins []string `json:"allowedOrigins"`
|
AllowedOrigins []string `json:"allowedOrigins"`
|
||||||
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
|
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent deepseek-harness octop"`
|
||||||
|
AccessHost string `json:"accessHost"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
AccountID uint `json:"accountId"`
|
AccountID uint `json:"accountId"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
@@ -110,6 +111,7 @@ type AgentBatchOperateResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentItem struct {
|
type AgentItem struct {
|
||||||
|
AccessHost string `json:"accessHost"`
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
@@ -166,12 +168,21 @@ type AgentModelConfigUpdateReq struct {
|
|||||||
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 {
|
||||||
@@ -375,6 +386,7 @@ type AgentAccountProviderCountReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountInfo struct {
|
type AgentAccountInfo struct {
|
||||||
|
AppInstallID uint `json:"appInstallId"`
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
MasterAccountID uint `json:"masterAccountId"`
|
MasterAccountID uint `json:"masterAccountId"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
|
|||||||
+6
-5
@@ -14,16 +14,17 @@ type OllamaModelInfo struct {
|
|||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OllamaModelDropList struct {
|
|
||||||
ID uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OllamaModelName struct {
|
type OllamaModelName struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OllamaModelAccountSync struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"required,oneof=container localhost systemIP custom"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
|
}
|
||||||
|
|
||||||
type OllamaBindDomain struct {
|
type OllamaBindDomain struct {
|
||||||
Domain string `json:"domain" validate:"required"`
|
Domain string `json:"domain" validate:"required"`
|
||||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type AIBenchmarkSource struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
BaseURL string `json:"baseUrl"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
Models []string `json:"models"`
|
||||||
|
Tokenizer string `json:"tokenizer"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ type AlertBase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PushAlert struct {
|
type PushAlert struct {
|
||||||
|
Result string `json:"result,omitempty"`
|
||||||
TaskName string `json:"taskName"`
|
TaskName string `json:"taskName"`
|
||||||
AlertType string `json:"alertType"`
|
AlertType string `json:"alertType"`
|
||||||
EntryID uint `json:"entryID"`
|
EntryID uint `json:"entryID"`
|
||||||
@@ -53,6 +54,7 @@ type AlertDTO struct {
|
|||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Project string `json:"project"`
|
Project string `json:"project"`
|
||||||
|
TaskName string `json:"taskName,omitempty"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
SendCount uint `json:"sendCount"`
|
SendCount uint `json:"sendCount"`
|
||||||
AdvancedParams string `json:"advancedParams"`
|
AdvancedParams string `json:"advancedParams"`
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ type ContainerItemStats struct {
|
|||||||
}
|
}
|
||||||
type ContainerListStats struct {
|
type ContainerListStats struct {
|
||||||
ContainerID string `json:"containerID"`
|
ContainerID string `json:"containerID"`
|
||||||
|
ContainerName string `json:"containerName"`
|
||||||
|
|
||||||
CPUTotalUsage uint64 `json:"cpuTotalUsage"`
|
CPUTotalUsage uint64 `json:"cpuTotalUsage"`
|
||||||
SystemUsage uint64 `json:"systemUsage"`
|
SystemUsage uint64 `json:"systemUsage"`
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ type CronjobOperate struct {
|
|||||||
AlertCount uint `json:"alertCount"`
|
AlertCount uint `json:"alertCount"`
|
||||||
AlertTitle string `json:"alertTitle"`
|
AlertTitle string `json:"alertTitle"`
|
||||||
AlertMethod string `json:"alertMethod"`
|
AlertMethod string `json:"alertMethod"`
|
||||||
|
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
|
||||||
|
|
||||||
CleanLogConfig
|
CleanLogConfig
|
||||||
}
|
}
|
||||||
@@ -127,6 +128,7 @@ type CronjobInfo struct {
|
|||||||
Args string `json:"args"`
|
Args string `json:"args"`
|
||||||
|
|
||||||
AlertCount uint `json:"alertCount"`
|
AlertCount uint `json:"alertCount"`
|
||||||
|
AlertTriggerMode string `json:"alertTriggerMode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CronjobImport struct {
|
type CronjobImport struct {
|
||||||
@@ -172,6 +174,7 @@ type CronjobTrans struct {
|
|||||||
AlertCount uint `json:"alertCount"`
|
AlertCount uint `json:"alertCount"`
|
||||||
AlertTitle string `json:"alertTitle"`
|
AlertTitle string `json:"alertTitle"`
|
||||||
AlertMethod string `json:"alertMethod"`
|
AlertMethod string `json:"alertMethod"`
|
||||||
|
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
|
||||||
}
|
}
|
||||||
type TransHelper struct {
|
type TransHelper struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|||||||
@@ -31,7 +31,27 @@ type DashboardBase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChangeQuicks struct {
|
type ChangeQuicks struct {
|
||||||
Quicks []QuickJump `json:"quicks"`
|
Quicks []QuickVisibility `json:"quicks" validate:"required,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuickVisibility struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
IsShow bool `json:"isShow"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuickDirectoryCreate struct {
|
||||||
|
Path string `json:"path" validate:"required"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuickDirectoryDelete struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
Legacy bool `json:"legacy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuickDirectoryUpdate struct {
|
||||||
|
QuickDirectoryDelete
|
||||||
|
QuickDirectoryCreate
|
||||||
}
|
}
|
||||||
|
|
||||||
type QuickJump struct {
|
type QuickJump struct {
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ type DBBaseInfo struct {
|
|||||||
Port int64 `json:"port"`
|
Port int64 `json:"port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RedisCliInstall struct {
|
||||||
|
TaskID string `json:"taskID" validate:"omitempty,uuid"`
|
||||||
|
}
|
||||||
|
|
||||||
// mysql
|
// mysql
|
||||||
type MysqlDBSearch struct {
|
type MysqlDBSearch struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
|
|||||||
+92
-100
@@ -1,11 +1,14 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||||
|
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||||
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FirewallSubsystemStatus struct {
|
type FirewallSubsystemStatus struct {
|
||||||
|
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Backend string `json:"backend"`
|
Backend string `json:"backend"`
|
||||||
ConflictBackend string `json:"conflictBackend,omitempty"`
|
ConflictBackend string `json:"conflictBackend,omitempty"`
|
||||||
@@ -17,16 +20,22 @@ type FirewallSubsystemStatus struct {
|
|||||||
PingStatus string `json:"pingStatus"`
|
PingStatus string `json:"pingStatus"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
Reason string `json:"reason,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
SyncError string `json:"syncError,omitempty"`
|
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
|
||||||
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
||||||
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallLifecycleOperation struct {
|
type FirewallLifecycleOperation struct {
|
||||||
|
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||||
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 {
|
||||||
|
TaskID string `json:"taskID,omitempty"`
|
||||||
|
Queued bool `json:"queued"`
|
||||||
|
}
|
||||||
|
|
||||||
type FirewallBackendOption struct {
|
type FirewallBackendOption struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Installed bool `json:"installed"`
|
Installed bool `json:"installed"`
|
||||||
@@ -42,10 +51,12 @@ type FirewallBackendOption struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FirewallBackendFamilyStatus struct {
|
type FirewallBackendFamilyStatus struct {
|
||||||
|
Partial bool `json:"partial"`
|
||||||
Available bool `json:"available"`
|
Available bool `json:"available"`
|
||||||
Initialized bool `json:"initialized"`
|
Initialized bool `json:"initialized"`
|
||||||
Bound bool `json:"bound"`
|
Bound bool `json:"bound"`
|
||||||
Reason string `json:"reason,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
|
RAInterfaces []string `json:"raInterfaces,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallBackendGroup struct {
|
type FirewallBackendGroup struct {
|
||||||
@@ -55,15 +66,27 @@ type FirewallBackendGroup struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FirewallSettings struct {
|
type FirewallSettings struct {
|
||||||
|
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||||
System FirewallBackendGroup `json:"system"`
|
System FirewallBackendGroup `json:"system"`
|
||||||
Forwarding FirewallBackendGroup `json:"forwarding"`
|
Forwarding FirewallBackendGroup `json:"forwarding"`
|
||||||
Docker FirewallBackendGroup `json:"docker"`
|
Docker FirewallBackendGroup `json:"docker"`
|
||||||
PingStatus string `json:"pingStatus"`
|
PingStatus string `json:"pingStatus"`
|
||||||
PortWhitelist string `json:"portWhiteList"`
|
PortWhitelist []filter.PortWhitelist `json:"portWhiteList"`
|
||||||
|
PanelPort string `json:"panelPort"`
|
||||||
|
SSHPort string `json:"sshPort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallPortWhitelistCreate struct {
|
||||||
|
Rule filter.PortWhitelist `json:"rule" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallPortWhitelistUpdate struct {
|
type FirewallPortWhitelistUpdate struct {
|
||||||
Value string `json:"value" validate:"required"`
|
OldRule filter.PortWhitelist `json:"oldRule" validate:"required"`
|
||||||
|
Rule filter.PortWhitelist `json:"rule" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallPortWhitelistDelete struct {
|
||||||
|
Rule *filter.PortWhitelist `json:"rule" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallBackendOperation struct {
|
type FirewallBackendOperation struct {
|
||||||
@@ -72,6 +95,17 @@ type FirewallBackendOperation struct {
|
|||||||
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"`
|
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FirewallIPv6Operation struct {
|
||||||
|
Status string `json:"status" validate:"required,oneof=Enable Disable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallFamilyOperation struct {
|
||||||
|
Subsystem string `json:"subsystem" validate:"required,oneof=system forwarding docker"`
|
||||||
|
Backend string `json:"backend" validate:"required,oneof=iptables nftables"`
|
||||||
|
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
|
||||||
|
Operation string `json:"operation" validate:"required,oneof=initialize repair bind"`
|
||||||
|
}
|
||||||
|
|
||||||
type FilterChainOperation struct {
|
type FilterChainOperation struct {
|
||||||
Name string `json:"name" validate:"required,eq=1PANEL_BASIC"`
|
Name string `json:"name" validate:"required,eq=1PANEL_BASIC"`
|
||||||
Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"`
|
Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"`
|
||||||
@@ -84,31 +118,42 @@ type FilterChainOperationResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FirewallInitializationTask struct {
|
type FirewallInitializationTask struct {
|
||||||
|
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallSystemPort struct {
|
type FirewallSystemPort = firewall.SystemPort
|
||||||
Family string
|
|
||||||
Port string
|
|
||||||
Protocol string
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleInventoryResponse struct {
|
type FirewallRuleInventoryResponse struct {
|
||||||
IPv4Range filter.PositionRange `json:"ipv4Range"`
|
IPv4Range filter.PositionRange `json:"ipv4Range"`
|
||||||
IPv6Range filter.PositionRange `json:"ipv6Range"`
|
IPv6Range filter.PositionRange `json:"ipv6Range"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
AllTotal int64 `json:"allTotal"`
|
AllTotal int64 `json:"allTotal"`
|
||||||
ManagedTotal int64 `json:"managedTotal"`
|
|
||||||
Items []filter.InventoryItem `json:"items"`
|
Items []filter.InventoryItem `json:"items"`
|
||||||
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FirewallRuleBackup struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Provider filter.Provider `json:"provider"`
|
||||||
|
RuleCount int `json:"ruleCount"`
|
||||||
|
ModifiedAt int64 `json:"modifiedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleBackups struct {
|
||||||
|
Directory string `json:"directory"`
|
||||||
|
Files []FirewallRuleBackup `json:"files"`
|
||||||
|
}
|
||||||
|
|
||||||
type FirewallRuleResetResponse struct {
|
type FirewallRuleResetResponse struct {
|
||||||
|
BackupPath string `json:"backupPath"`
|
||||||
Removed int `json:"removed"`
|
Removed int `json:"removed"`
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleReset struct {
|
type FirewallRuleReset struct {
|
||||||
|
Subsystem string `json:"subsystem,omitempty" validate:"omitempty,oneof=system forwarding docker"`
|
||||||
|
Backup *bool `json:"backup,omitempty" default:"true"`
|
||||||
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
||||||
WithDockerRestart bool `json:"withDockerRestart"`
|
WithDockerRestart bool `json:"withDockerRestart"`
|
||||||
}
|
}
|
||||||
@@ -121,7 +166,6 @@ type FirewallRuleInventory struct {
|
|||||||
Info string `json:"info"`
|
Info string `json:"info"`
|
||||||
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
|
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
|
||||||
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
|
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"`
|
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +177,7 @@ type FirewallNativeDetail struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DockerPortGuardBase struct {
|
type DockerPortGuardBase struct {
|
||||||
|
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
IsExist bool `json:"isExist"`
|
IsExist bool `json:"isExist"`
|
||||||
@@ -145,6 +190,7 @@ type DockerPortGuardBase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DockerPortGuardFamilyStatus struct {
|
type DockerPortGuardFamilyStatus struct {
|
||||||
|
Partial bool `json:"partial"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
Reason string `json:"reason,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
Initialized bool `json:"initialized"`
|
Initialized bool `json:"initialized"`
|
||||||
@@ -165,11 +211,8 @@ type DockerPortGuardEndpoint struct {
|
|||||||
Application string `json:"application,omitempty"`
|
Application string `json:"application,omitempty"`
|
||||||
PolicyUUID string `json:"policyUUID,omitempty"`
|
PolicyUUID string `json:"policyUUID,omitempty"`
|
||||||
Mode string `json:"mode,omitempty"`
|
Mode string `json:"mode,omitempty"`
|
||||||
NativeAction string `json:"nativeAction,omitempty"`
|
|
||||||
ReadOnly bool `json:"readOnly,omitempty"`
|
|
||||||
Sources []string `json:"sources"`
|
Sources []string `json:"sources"`
|
||||||
Effective bool `json:"effective"`
|
Effective bool `json:"effective"`
|
||||||
Description string `json:"description,omitempty"`
|
|
||||||
TrafficPath string `json:"trafficPath"`
|
TrafficPath string `json:"trafficPath"`
|
||||||
ManagementTarget string `json:"managementTarget"`
|
ManagementTarget string `json:"managementTarget"`
|
||||||
ManagementReason string `json:"managementReason,omitempty"`
|
ManagementReason string `json:"managementReason,omitempty"`
|
||||||
@@ -206,6 +249,7 @@ type DockerPortGuardEndpointIdentity struct {
|
|||||||
|
|
||||||
type DockerPortGuardPolicyBatch struct {
|
type DockerPortGuardPolicyBatch struct {
|
||||||
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
|
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
|
||||||
|
Import bool `json:"import"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DockerPortGuardPolicyBatchDelete struct {
|
type DockerPortGuardPolicyBatchDelete struct {
|
||||||
@@ -214,29 +258,27 @@ type DockerPortGuardPolicyBatchDelete struct {
|
|||||||
|
|
||||||
type DockerPortGuardPolicy struct {
|
type DockerPortGuardPolicy struct {
|
||||||
DockerPortGuardEndpointIdentity
|
DockerPortGuardEndpointIdentity
|
||||||
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all"`
|
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all accept_sources accept_all"`
|
||||||
Sources []string `json:"sources" validate:"dive,required,max=64"`
|
Sources []string `json:"sources" validate:"dive,required,max=64"`
|
||||||
Description string `json:"description" validate:"max=256"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DockerPortGuardOperation struct {
|
type DockerPortGuardOperation struct {
|
||||||
|
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||||
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
|
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
|
||||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
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 {
|
type FirewallRuleCreateItem struct {
|
||||||
|
Raw string `json:"raw,omitempty"`
|
||||||
|
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
|
||||||
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
||||||
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
|
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
|
||||||
SourceID string `json:"sourceID"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleCreate struct {
|
type FirewallRuleCreate struct {
|
||||||
Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,dive"`
|
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||||
|
Initialize bool `json:"initialize"`
|
||||||
|
Items []FirewallRuleCreateItem `json:"items" validate:"dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleCreateResponse struct {
|
type FirewallRuleCreateResponse struct {
|
||||||
@@ -255,68 +297,23 @@ type FirewallRuleCreateFailure struct {
|
|||||||
Error string `json:"error,omitempty"`
|
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 {
|
type FirewallRuleDelete struct {
|
||||||
UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"`
|
Targets []FirewallRuleDeleteItem `json:"targets" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleDeleteItem struct {
|
||||||
|
FirewallRuleDeleteTarget
|
||||||
|
Observed filter.ObservedRule `json:"observed" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleDeleteTarget struct {
|
||||||
|
Scope filter.Scope `json:"scope" validate:"required"`
|
||||||
|
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleDeleteResponse struct {
|
type FirewallRuleDeleteResponse struct {
|
||||||
|
TaskID string `json:"taskID,omitempty"`
|
||||||
|
Queued bool `json:"queued,omitempty"`
|
||||||
Succeeded int `json:"succeeded"`
|
Succeeded int `json:"succeeded"`
|
||||||
Failed int `json:"failed"`
|
Failed int `json:"failed"`
|
||||||
Errors []FirewallRuleDeleteFailure `json:"errors,omitempty"`
|
Errors []FirewallRuleDeleteFailure `json:"errors,omitempty"`
|
||||||
@@ -324,12 +321,12 @@ type FirewallRuleDeleteResponse struct {
|
|||||||
|
|
||||||
type FirewallRuleDeleteFailure struct {
|
type FirewallRuleDeleteFailure struct {
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
UUID string `json:"uuid"`
|
InstanceKey string `json:"instanceKey"`
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleUpdate struct {
|
type FirewallRuleUpdate struct {
|
||||||
UUID string `json:"uuid" validate:"required,max=64"`
|
FirewallRuleDeleteTarget
|
||||||
Rule *filter.FirewallRule `json:"rule,omitempty" validate:"required_without_all=Description OrderIndex Priority,excluded_with=Description OrderIndex Priority"`
|
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"`
|
Description *string `json:"description,omitempty" validate:"excluded_with=Rule"`
|
||||||
OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"`
|
OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"`
|
||||||
@@ -337,26 +334,21 @@ type FirewallRuleUpdate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FirewallRuleReorder struct {
|
type FirewallRuleReorder struct {
|
||||||
UUID string `json:"uuid" validate:"required,max=64"`
|
FirewallRuleDeleteTarget
|
||||||
TargetPosition *int64 `json:"targetPosition"`
|
TargetPosition *int64 `json:"targetPosition"`
|
||||||
Priority *int `json:"priority"`
|
Priority *int `json:"priority"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *FirewallRuleSyncPreview) Add(item FirewallRuleSyncItem) {
|
type FirewallRuleExportItem struct {
|
||||||
p.Items = append(p.Items, item)
|
filter.FirewallRule
|
||||||
switch item.Status {
|
Raw string `json:"raw,omitempty"`
|
||||||
case firewallsync.StatusReady:
|
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
|
||||||
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++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FirewallSubsystemBackup struct {
|
||||||
|
Families []string `json:"families,omitempty"`
|
||||||
|
Subsystem string `json:"subsystem"`
|
||||||
|
Provider filter.Provider `json:"provider"`
|
||||||
|
Forwarding []forwarding.Rule `json:"forwarding"`
|
||||||
|
Docker *dockerfirewall.PolicyInventory `json:"docker,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,14 +24,10 @@ 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"`
|
Import bool `json:"import"`
|
||||||
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
|
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+89
-13
@@ -1,6 +1,11 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
)
|
||||||
|
|
||||||
type MonitorSearch struct {
|
type MonitorSearch struct {
|
||||||
Param string `json:"param" validate:"required,oneof=all cpu memory load io network"`
|
Param string `json:"param" validate:"required,oneof=all cpu memory load io network"`
|
||||||
@@ -26,6 +31,12 @@ type Process struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MonitorSetting struct {
|
type MonitorSetting struct {
|
||||||
|
GPUMonitorStatus string `json:"gpuMonitorStatus"`
|
||||||
|
GPUMonitorStoreDays string `json:"gpuMonitorStoreDays"`
|
||||||
|
GPUMonitorInterval string `json:"gpuMonitorInterval"`
|
||||||
|
VLLMMonitorStatus string `json:"vllmMonitorStatus"`
|
||||||
|
VLLMMonitorStoreDays string `json:"vllmMonitorStoreDays"`
|
||||||
|
VLLMMonitorInterval string `json:"vllmMonitorInterval"`
|
||||||
MonitorStatus string `json:"monitorStatus"`
|
MonitorStatus string `json:"monitorStatus"`
|
||||||
MonitorStoreDays string `json:"monitorStoreDays"`
|
MonitorStoreDays string `json:"monitorStoreDays"`
|
||||||
MonitorInterval string `json:"monitorInterval"`
|
MonitorInterval string `json:"monitorInterval"`
|
||||||
@@ -34,16 +45,19 @@ type MonitorSetting struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MonitorSettingUpdate struct {
|
type MonitorSettingUpdate struct {
|
||||||
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval DefaultNetwork DefaultIO"`
|
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval GPUMonitorStatus GPUMonitorStoreDays GPUMonitorInterval VLLMMonitorStatus VLLMMonitorStoreDays VLLMMonitorInterval DefaultNetwork DefaultIO"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MonitorGPUOptions struct {
|
type MonitorGPUOptions struct {
|
||||||
|
Supported bool `json:"supported"`
|
||||||
GPUType string `json:"gpuType"`
|
GPUType string `json:"gpuType"`
|
||||||
ChartHide []GPUChartHide `json:"chartHide"`
|
ChartHide []GPUChartHide `json:"chartHide"`
|
||||||
Options []string `json:"options"`
|
Options []string `json:"options"`
|
||||||
}
|
}
|
||||||
type GPUChartHide struct {
|
type GPUChartHide struct {
|
||||||
|
DeviceID string `json:"deviceID"`
|
||||||
|
Legacy bool `json:"legacy"`
|
||||||
ProductName string `json:"productName"`
|
ProductName string `json:"productName"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Process bool `json:"process"`
|
Process bool `json:"process"`
|
||||||
@@ -55,23 +69,54 @@ type GPUChartHide struct {
|
|||||||
Speed bool `json:"speed"`
|
Speed bool `json:"speed"`
|
||||||
}
|
}
|
||||||
type MonitorGPUSearch struct {
|
type MonitorGPUSearch struct {
|
||||||
|
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
|
||||||
|
DeviceID string `json:"deviceID"`
|
||||||
|
Legacy bool `json:"legacy"`
|
||||||
ProductName string `json:"productName"`
|
ProductName string `json:"productName"`
|
||||||
StartTime time.Time `json:"startTime"`
|
StartTime time.Time `json:"startTime"`
|
||||||
EndTime time.Time `json:"endTime"`
|
EndTime time.Time `json:"endTime"`
|
||||||
}
|
}
|
||||||
type MonitorGPUData struct {
|
type MonitorGPUData struct {
|
||||||
Date []time.Time `json:"date"`
|
MemoryActivity []*float64 `json:"memoryActivity"`
|
||||||
GPUValue []float64 `json:"gpuValue"`
|
EncoderUtil []*float64 `json:"encoderUtil"`
|
||||||
TemperatureValue []float64 `json:"temperatureValue"`
|
DecoderUtil []*float64 `json:"decoderUtil"`
|
||||||
PowerTotal []float64 `json:"powerTotal"`
|
JPEGUtil []*float64 `json:"jpegUtil"`
|
||||||
PowerUsed []float64 `json:"powerUsed"`
|
OFAUtil []*float64 `json:"ofaUtil"`
|
||||||
PowerPercent []float64 `json:"powerPercent"`
|
MediaUtil []*float64 `json:"mediaUtil"`
|
||||||
MemoryTotal []float64 `json:"memoryTotal"`
|
ComputeUtil []*float64 `json:"computeUtil"`
|
||||||
MemoryUsed []float64 `json:"memoryUsed"`
|
CopyUtil []*float64 `json:"copyUtil"`
|
||||||
MemoryPercent []float64 `json:"memoryPercent"`
|
HotspotTemperature []*float64 `json:"hotspotTemperature"`
|
||||||
SpeedValue []int `json:"speedValue"`
|
FanRPM []*float64 `json:"fanRPM"`
|
||||||
|
AICPUUtil []*float64 `json:"aiCPUUtil"`
|
||||||
|
CtrlCPUUtil []*float64 `json:"ctrlCPUUtil"`
|
||||||
|
DDRUsed []*float64 `json:"ddrUsed"`
|
||||||
|
DDRTotal []*float64 `json:"ddrTotal"`
|
||||||
|
HBMUsed []*float64 `json:"hbmUsed"`
|
||||||
|
HBMTotal []*float64 `json:"hbmTotal"`
|
||||||
|
DDRBandwidth []*float64 `json:"ddrBandwidth"`
|
||||||
|
HBMBandwidth []*float64 `json:"hbmBandwidth"`
|
||||||
|
MemoryBandwidth []*float64 `json:"memoryBandwidth"`
|
||||||
|
MediaFrequency []*float64 `json:"mediaFrequency"`
|
||||||
|
HugepagesUsed []*float64 `json:"hugepagesUsed"`
|
||||||
|
HugepagesTotal []*float64 `json:"hugepagesTotal"`
|
||||||
|
|
||||||
ProcessCount []int `json:"processCount"`
|
BucketSeconds int64 `json:"bucketSeconds"`
|
||||||
|
SampleCount int64 `json:"sampleCount"`
|
||||||
|
MemoryTemperatureValue []*float64 `json:"memoryTemperatureValue"`
|
||||||
|
FrequencyValue []*float64 `json:"frequencyValue"`
|
||||||
|
MemoryFrequencyValue []*float64 `json:"memoryFrequencyValue"`
|
||||||
|
Date []time.Time `json:"date"`
|
||||||
|
GPUValue []*float64 `json:"gpuValue"`
|
||||||
|
TemperatureValue []*float64 `json:"temperatureValue"`
|
||||||
|
PowerTotal []*float64 `json:"powerTotal"`
|
||||||
|
PowerUsed []*float64 `json:"powerUsed"`
|
||||||
|
PowerPercent []*float64 `json:"powerPercent"`
|
||||||
|
MemoryTotal []*float64 `json:"memoryTotal"`
|
||||||
|
MemoryUsed []*float64 `json:"memoryUsed"`
|
||||||
|
MemoryPercent []*float64 `json:"memoryPercent"`
|
||||||
|
SpeedValue []*float64 `json:"speedValue"`
|
||||||
|
|
||||||
|
ProcessCount []*float64 `json:"processCount"`
|
||||||
GPUProcesses [][]GPUProcess `json:"gpuProcesses"`
|
GPUProcesses [][]GPUProcess `json:"gpuProcesses"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,3 +126,34 @@ type GPUProcess struct {
|
|||||||
ProcessName string `json:"processName"`
|
ProcessName string `json:"processName"`
|
||||||
UsedMemory string `json:"usedMemory"`
|
UsedMemory string `json:"usedMemory"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MonitorVLLMSearch struct {
|
||||||
|
Group string `json:"group" validate:"omitempty,oneof=overview latency requests"`
|
||||||
|
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||||
|
StartTime time.Time `json:"startTime" validate:"required"`
|
||||||
|
EndTime time.Time `json:"endTime" validate:"required"`
|
||||||
|
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorVLLMData struct {
|
||||||
|
SampleCount int64 `json:"sampleCount"`
|
||||||
|
BucketSeconds int64 `json:"bucketSeconds"`
|
||||||
|
Points []model.MonitorVLLM `json:"points"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorVLLMCurrent struct {
|
||||||
|
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorVLLMCurrentData struct {
|
||||||
|
model.MonitorVLLM
|
||||||
|
Metrics map[string]json.RawMessage `json:"metrics,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorVLLMClean struct {
|
||||||
|
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorClean struct {
|
||||||
|
Type string `json:"type" validate:"required,oneof=host gpu"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ type AppInstallCreate struct {
|
|||||||
Services map[string]string `json:"services"`
|
Services map[string]string `json:"services"`
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
|
|
||||||
|
SyncModelAccount *bool `json:"syncModelAccount"`
|
||||||
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"omitempty,oneof=container localhost systemIP custom"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
|
|
||||||
|
MCPPreserveWebsiteDir bool `json:"-"`
|
||||||
|
|
||||||
AppContainerConfig
|
AppContainerConfig
|
||||||
NodePushConfig
|
NodePushConfig
|
||||||
}
|
}
|
||||||
@@ -122,6 +128,9 @@ type AppInstallDelete struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppInstalledUpdate struct {
|
type AppInstalledUpdate struct {
|
||||||
|
SyncModelAccount *bool `json:"syncModelAccount"`
|
||||||
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"omitempty,oneof=container localhost systemIP custom"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
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:"-"`
|
TaskID string `json:"-"`
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ 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 {
|
||||||
|
|||||||
@@ -182,3 +182,12 @@ type WebsiteSSLFileUpload struct {
|
|||||||
PushNode bool `json:"pushNode"`
|
PushNode bool `json:"pushNode"`
|
||||||
Nodes string `json:"nodes"`
|
Nodes string `json:"nodes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WebsiteCAImport struct {
|
||||||
|
Name string `json:"name" validate:"required,max=128"`
|
||||||
|
Type string `json:"type" validate:"required,oneof=paste local"`
|
||||||
|
Certificate string `json:"certificate" validate:"required_if=Type paste"`
|
||||||
|
PrivateKey string `json:"privateKey" validate:"required_if=Type paste"`
|
||||||
|
CertificatePath string `json:"certificatePath" validate:"required_if=Type local"`
|
||||||
|
PrivateKeyPath string `json:"privateKeyPath" validate:"required_if=Type local"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ type TagDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppInstalledCheck struct {
|
type AppInstalledCheck struct {
|
||||||
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
IsExist bool `json:"isExist"`
|
IsExist bool `json:"isExist"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
App string `json:"app"`
|
App string `json:"app"`
|
||||||
@@ -184,6 +186,12 @@ type AppParam struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
|
AppKey string `json:"appKey"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
HttpPort int `json:"httpPort"`
|
||||||
|
SyncModelAccount bool `json:"syncModelAccount"`
|
||||||
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
Params []AppParam `json:"params"`
|
Params []AppParam `json:"params"`
|
||||||
RawCompose string `json:"rawCompose"`
|
RawCompose string `json:"rawCompose"`
|
||||||
request.AppContainerConfig
|
request.AppContainerConfig
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type RootCertOperate struct {
|
|||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
|
EncryptionMode string `json:"encryptionMode"`
|
||||||
PassPhrase string `json:"passPhrase"`
|
PassPhrase string `json:"passPhrase"`
|
||||||
PublicKey string `json:"publicKey"`
|
PublicKey string `json:"publicKey"`
|
||||||
PrivateKey string `json:"privateKey"`
|
PrivateKey string `json:"privateKey"`
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package model
|
|||||||
|
|
||||||
type AgentAccount struct {
|
type AgentAccount struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
|
AppInstallID uint `json:"appInstallId"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
APIKey string `json:"apiKey"`
|
APIKey string `json:"apiKey"`
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ type AppInstall struct {
|
|||||||
Favorite bool `json:"favorite"`
|
Favorite bool `json:"favorite"`
|
||||||
SortOrder int `json:"sortOrder" gorm:"default:0"`
|
SortOrder int `json:"sortOrder" gorm:"default:0"`
|
||||||
|
|
||||||
|
SyncModelAccount bool `json:"syncModelAccount"`
|
||||||
|
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||||
|
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||||
|
|
||||||
App App `json:"app" gorm:"-:migration"`
|
App App `json:"app" gorm:"-:migration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ type AppLauncher struct {
|
|||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QuickDirectory struct {
|
||||||
|
BaseModel
|
||||||
|
Path string `json:"path"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Legacy bool `json:"legacy" gorm:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
type QuickJump struct {
|
type QuickJump struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|||||||
@@ -1,222 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
|
||||||
)
|
|
||||||
|
|
||||||
const FirewallRuleSequenceStep int64 = 1 << 32
|
|
||||||
|
|
||||||
type DockerPortGuardPolicy struct {
|
|
||||||
BaseModel
|
|
||||||
|
|
||||||
UUID string `gorm:"size:64;not null;uniqueIndex" json:"uuid"`
|
|
||||||
ReadOnly bool `gorm:"not null;default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"`
|
|
||||||
Family string `gorm:"size:16;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"family"`
|
|
||||||
HostIP string `gorm:"size:64;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"`
|
|
||||||
HostPort uint16 `gorm:"not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"`
|
|
||||||
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"`
|
|
||||||
Mode string `gorm:"size:32;not null" json:"mode"`
|
|
||||||
Sources string `gorm:"type:text" json:"-"`
|
|
||||||
Description string `gorm:"type:text" json:"description"`
|
|
||||||
NativeAction string `gorm:"size:32;not null;default:''" json:"-"`
|
|
||||||
NativeRules string `gorm:"type:text" json:"-"`
|
|
||||||
Sequence int64 `gorm:"not null;default:0" json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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,5 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type MonitorBase struct {
|
type MonitorBase struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Cpu float64 `json:"cpu"`
|
Cpu float64 `json:"cpu"`
|
||||||
@@ -33,14 +35,88 @@ type MonitorNetwork struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MonitorGPU struct {
|
type MonitorGPU struct {
|
||||||
|
MemoryUtil *float64 `json:"memoryUtil"`
|
||||||
|
MemoryActivity *float64 `json:"memoryActivity"`
|
||||||
|
EncoderUtil *float64 `json:"encoderUtil"`
|
||||||
|
DecoderUtil *float64 `json:"decoderUtil"`
|
||||||
|
JPEGUtil *float64 `json:"jpegUtil"`
|
||||||
|
OFAUtil *float64 `json:"ofaUtil"`
|
||||||
|
MediaUtil *float64 `json:"mediaUtil"`
|
||||||
|
ComputeUtil *float64 `json:"computeUtil"`
|
||||||
|
CopyUtil *float64 `json:"copyUtil"`
|
||||||
|
HotspotTemperature *float64 `json:"hotspotTemperature"`
|
||||||
|
FanRPM *float64 `json:"fanRPM"`
|
||||||
|
AICPUUtil *float64 `json:"aiCPUUtil"`
|
||||||
|
CtrlCPUUtil *float64 `json:"ctrlCPUUtil"`
|
||||||
|
DDRUsed *float64 `json:"ddrUsed"`
|
||||||
|
DDRTotal *float64 `json:"ddrTotal"`
|
||||||
|
HBMUsed *float64 `json:"hbmUsed"`
|
||||||
|
HBMTotal *float64 `json:"hbmTotal"`
|
||||||
|
DDRBandwidth *float64 `json:"ddrBandwidth"`
|
||||||
|
HBMBandwidth *float64 `json:"hbmBandwidth"`
|
||||||
|
MemoryBandwidth *float64 `json:"memoryBandwidth"`
|
||||||
|
MediaFrequency *float64 `json:"mediaFrequency"`
|
||||||
|
HugepagesUsed *float64 `json:"hugepagesUsed"`
|
||||||
|
HugepagesTotal *float64 `json:"hugepagesTotal"`
|
||||||
|
|
||||||
|
MemoryTemperature *float64 `json:"memoryTemperature"`
|
||||||
|
DeviceID string `json:"deviceID"`
|
||||||
|
DeviceType string `json:"deviceType"`
|
||||||
|
ProcessStatus string `json:"processStatus"`
|
||||||
|
Frequency *float64 `json:"frequency"`
|
||||||
|
MemoryFrequency *float64 `json:"memoryFrequency"`
|
||||||
|
IntervalSeconds int `json:"intervalSeconds"`
|
||||||
BaseModel
|
BaseModel
|
||||||
ProductName string `json:"productName"`
|
ProductName string `json:"productName"`
|
||||||
GPUUtil float64 `json:"gpuUtil"`
|
GPUUtil *float64 `json:"gpuUtil"`
|
||||||
Temperature float64 `json:"temperature"`
|
Temperature *float64 `json:"temperature"`
|
||||||
PowerDraw float64 `json:"powerDraw"`
|
PowerDraw *float64 `json:"powerDraw"`
|
||||||
MaxPowerLimit float64 `json:"maxPowerLimit"`
|
MaxPowerLimit *float64 `json:"maxPowerLimit"`
|
||||||
MemUsed float64 `json:"memUsed"`
|
MemUsed *float64 `json:"memUsed"`
|
||||||
MemTotal float64 `json:"memTotal"`
|
MemTotal *float64 `json:"memTotal"`
|
||||||
FanSpeed int `json:"fanSpeed"`
|
FanSpeed *float64 `json:"fanSpeed"`
|
||||||
Processes string `json:"processes"`
|
Processes string `json:"processes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MonitorVLLM struct {
|
||||||
|
ID uint `json:"-" gorm:"primarykey;autoIncrement"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
AppInstallID uint `json:"appInstallID"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
RawMetrics string `json:"-"`
|
||||||
|
HistogramDeltas string `json:"-"`
|
||||||
|
IntervalSeconds float64 `json:"intervalSeconds" gorm:"-"`
|
||||||
|
SampleIntervalSeconds int `json:"-"`
|
||||||
|
RequestDeltas string `json:"-"`
|
||||||
|
CompletedRequests *float64 `json:"completedRequests" gorm:"-"`
|
||||||
|
QueueTime *float64 `json:"queueTime"`
|
||||||
|
AverageGenerationTokens *float64 `json:"averageGenerationTokens"`
|
||||||
|
PrefixCacheHitRate *float64 `json:"prefixCacheHitRate"`
|
||||||
|
FinishReasons map[string]float64 `json:"finishReasons" gorm:"-"`
|
||||||
|
PromptDistribution map[string]float64 `json:"promptDistribution" gorm:"-"`
|
||||||
|
GenerationDistribution map[string]float64 `json:"generationDistribution" gorm:"-"`
|
||||||
|
|
||||||
|
Running *float64 `json:"running"`
|
||||||
|
Waiting *float64 `json:"waiting"`
|
||||||
|
CacheUsage *float64 `json:"cacheUsage"`
|
||||||
|
PromptThroughput *float64 `json:"promptThroughput"`
|
||||||
|
GenerationThroughput *float64 `json:"generationThroughput"`
|
||||||
|
RequestThroughput *float64 `json:"requestThroughput"`
|
||||||
|
TimeToFirstToken *float64 `json:"timeToFirstToken"`
|
||||||
|
TimePerOutputToken *float64 `json:"timePerOutputToken"`
|
||||||
|
RequestLatency *float64 `json:"requestLatency"`
|
||||||
|
PrefillTime *float64 `json:"prefillTime"`
|
||||||
|
DecodeTime *float64 `json:"decodeTime"`
|
||||||
|
TimeToFirstTokenP50 *float64 `json:"timeToFirstTokenP50"`
|
||||||
|
TimeToFirstTokenP90 *float64 `json:"timeToFirstTokenP90"`
|
||||||
|
TimeToFirstTokenP95 *float64 `json:"timeToFirstTokenP95"`
|
||||||
|
TimeToFirstTokenP99 *float64 `json:"timeToFirstTokenP99"`
|
||||||
|
TimePerOutputTokenP50 *float64 `json:"timePerOutputTokenP50"`
|
||||||
|
TimePerOutputTokenP90 *float64 `json:"timePerOutputTokenP90"`
|
||||||
|
TimePerOutputTokenP95 *float64 `json:"timePerOutputTokenP95"`
|
||||||
|
TimePerOutputTokenP99 *float64 `json:"timePerOutputTokenP99"`
|
||||||
|
RequestLatencyP50 *float64 `json:"requestLatencyP50"`
|
||||||
|
RequestLatencyP90 *float64 `json:"requestLatencyP90"`
|
||||||
|
RequestLatencyP95 *float64 `json:"requestLatencyP95"`
|
||||||
|
RequestLatencyP99 *float64 `json:"requestLatencyP99"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ var catalog = map[string]Meta{
|
|||||||
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"},
|
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"},
|
||||||
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
|
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
|
||||||
},
|
},
|
||||||
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
Models: []Model{{ID: "deepseek-flash", Name: "deepseek-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
||||||
},
|
},
|
||||||
"bailian-coding-plan": {
|
"bailian-coding-plan": {
|
||||||
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
|
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
|
||||||
@@ -82,7 +82,6 @@ var catalog = map[string]Meta{
|
|||||||
{ID: "kimi-k2.5", Name: "Kimi K2.5"},
|
{ID: "kimi-k2.5", Name: "Kimi K2.5"},
|
||||||
{ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"},
|
{ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"},
|
||||||
{ID: "glm-5", Name: "GLM-5"},
|
{ID: "glm-5", Name: "GLM-5"},
|
||||||
{ID: "MiniMax-M2.5", Name: "MiniMax M2.5"},
|
|
||||||
{ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"},
|
{ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"},
|
||||||
{ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"},
|
{ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"},
|
||||||
},
|
},
|
||||||
@@ -114,7 +113,7 @@ var catalog = map[string]Meta{
|
|||||||
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
|
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
|
||||||
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"},
|
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"},
|
||||||
},
|
},
|
||||||
Models: []Model{{ID: "MiniMax-M3", Name: "MiniMax M3"}, {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}},
|
Models: []Model{{ID: "MiniMax-M3.1-Flash", Name: "MiniMax M3.1 Flash"}, {ID: "MiniMax-M3", Name: "MiniMax M3"}},
|
||||||
},
|
},
|
||||||
"xiaomi": {
|
"xiaomi": {
|
||||||
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
|
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMiniMaxCatalog(t *testing.T) {
|
||||||
|
meta, ok := Get("minimax")
|
||||||
|
if !ok || !reflect.DeepEqual(meta.Models, []Model{{ID: "MiniMax-M3.1-Flash", Name: "MiniMax M3.1 Flash"}, {ID: "MiniMax-M3", Name: "MiniMax M3"}}) {
|
||||||
|
t.Fatalf("unexpected MiniMax models: %+v", meta.Models)
|
||||||
|
}
|
||||||
|
if meta.DefaultAPIType != "anthropic-messages" {
|
||||||
|
t.Fatalf("changed default protocol: %s", meta.DefaultAPIType)
|
||||||
|
}
|
||||||
|
for _, provider := range All() {
|
||||||
|
for _, model := range provider.Models {
|
||||||
|
if strings.Contains(strings.ToLower(model.ID), "minimax") && model.ID != "MiniMax-M3.1-Flash" && model.ID != "MiniMax-M3" {
|
||||||
|
t.Errorf("legacy suggestion in %s: %s", provider.Key, model.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxVerificationRequests(t *testing.T) {
|
||||||
|
for _, region := range []string{"https://api.minimaxi.com", "https://api.minimax.io"} {
|
||||||
|
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||||
|
for _, auth := range []string{AuthModeXAPIKey, AuthModeBearer} {
|
||||||
|
for _, model := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed", "custom-model"} {
|
||||||
|
t.Run(region+"/"+protocol+"/"+auth+"/"+model, func(t *testing.T) {
|
||||||
|
base, endpoint := region+"/v1", "/chat/completions"
|
||||||
|
if protocol == "anthropic-messages" {
|
||||||
|
base, endpoint = region+"/anthropic", "/v1/messages"
|
||||||
|
}
|
||||||
|
built := BuildVerifyRequest("minimax", protocol, auth, base, "test-key", model)
|
||||||
|
if built.URL != base+endpoint {
|
||||||
|
t.Fatalf("URL = %s", built.URL)
|
||||||
|
}
|
||||||
|
var captured map[string]interface{}
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost || r.URL.Path != strings.TrimPrefix(base, region)+endpoint {
|
||||||
|
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
if protocol == "anthropic-messages" && auth == AuthModeXAPIKey {
|
||||||
|
if r.Header.Get("x-api-key") != "test-key" || r.Header.Get("Authorization") != "" {
|
||||||
|
t.Error("incorrect API key authentication")
|
||||||
|
}
|
||||||
|
} else if r.Header.Get("Authorization") != "Bearer test-key" {
|
||||||
|
t.Error("incorrect bearer authentication")
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
if err := VerifyAccount("minimax", protocol, auth, server.URL+strings.TrimPrefix(base, region), "test-key", model); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if captured["model"] != model || captured["stream"] != false || captured["max_tokens"] != float64(defaultVerifyMaxTokens) {
|
||||||
|
t.Fatalf("unexpected serialized body: %#v", captured)
|
||||||
|
}
|
||||||
|
if model == "MiniMax-M3.1-Flash" {
|
||||||
|
if protocol == "anthropic-messages" {
|
||||||
|
if !reflect.DeepEqual(captured["thinking"], map[string]interface{}{"type": "adaptive"}) || !reflect.DeepEqual(captured["output_config"], map[string]interface{}{"effort": "max"}) || captured["reasoning_effort"] != nil {
|
||||||
|
t.Fatalf("invalid Anthropic Flash reasoning: %#v", captured)
|
||||||
|
}
|
||||||
|
} else if captured["reasoning_effort"] != "max" || captured["thinking"] != nil || captured["output_config"] != nil {
|
||||||
|
t.Fatalf("invalid OpenAI Flash reasoning: %#v", captured)
|
||||||
|
}
|
||||||
|
} else if captured["reasoning_effort"] != nil || captured["thinking"] != nil || captured["output_config"] != nil {
|
||||||
|
t.Fatalf("changed legacy/custom reasoning: %#v", captured)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxExplicitGatewayAndCustomModels(t *testing.T) {
|
||||||
|
for _, provider := range []string{"bailian-coding-plan", "custom", "openrouter"} {
|
||||||
|
for _, protocol := range []string{"openai-completions", "anthropic-messages"} {
|
||||||
|
for _, model := range []string{"MiniMax-M2.5", "MiniMax-M3.1-Flash", "my-custom-model"} {
|
||||||
|
request := BuildVerifyRequest(provider, protocol, AuthModeBearer, "https://example.test/v1", "test-key", model)
|
||||||
|
var body map[string]interface{}
|
||||||
|
if err := json.Unmarshal(request.Body, &body); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if body["model"] != model || body["reasoning_effort"] != nil || body["output_config"] != nil || body["thinking"] != nil {
|
||||||
|
t.Fatalf("changed explicit %s request: %#v", provider, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -118,10 +118,15 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
|||||||
headers["x-api-key"] = apiKey
|
headers["x-api-key"] = apiKey
|
||||||
}
|
}
|
||||||
headers["anthropic-version"] = "2023-06-01"
|
headers["anthropic-version"] = "2023-06-01"
|
||||||
request.Body = mustJSON(map[string]interface{}{
|
body := map[string]interface{}{
|
||||||
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
"model": model, "max_tokens": defaultVerifyMaxTokens, "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"}}}},
|
||||||
})
|
}
|
||||||
|
if provider == "minimax" && model == "MiniMax-M3.1-Flash" {
|
||||||
|
body["thinking"] = map[string]string{"type": "adaptive"}
|
||||||
|
body["output_config"] = map[string]string{"effort": "max"}
|
||||||
|
}
|
||||||
|
request.Body = mustJSON(body)
|
||||||
case "openai-responses":
|
case "openai-responses":
|
||||||
request.URL = baseURL + "/responses"
|
request.URL = baseURL + "/responses"
|
||||||
headers["Authorization"] = "Bearer " + apiKey
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
@@ -131,9 +136,13 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
|||||||
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
|
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
|
||||||
headers["Authorization"] = "Bearer " + apiKey
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
}
|
}
|
||||||
request.Body = mustJSON(map[string]interface{}{
|
body := 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": defaultVerifyMaxTokens, "stream": false,
|
||||||
})
|
}
|
||||||
|
if provider == "minimax" && model == "MiniMax-M3.1-Flash" {
|
||||||
|
body["reasoning_effort"] = "max"
|
||||||
|
}
|
||||||
|
request.Body = mustJSON(body)
|
||||||
}
|
}
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ type IAgentAccountRepo interface {
|
|||||||
DeleteByID(id uint) error
|
DeleteByID(id uint) error
|
||||||
List(opts ...DBOption) ([]model.AgentAccount, error)
|
List(opts ...DBOption) ([]model.AgentAccount, error)
|
||||||
CountTextByProviders(providers []string) (map[string]int64, error)
|
CountTextByProviders(providers []string) (map[string]int64, error)
|
||||||
|
SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAgentAccountRepo() IAgentAccountRepo {
|
func NewIAgentAccountRepo() IAgentAccountRepo {
|
||||||
@@ -97,6 +99,25 @@ func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]i
|
|||||||
return counts, nil
|
return counts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AgentAccountRepo) SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error {
|
||||||
|
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Save(account).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Where("account_id = ?", account.ID).Delete(&model.AgentAccountModel{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(models) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i := range models {
|
||||||
|
models[i].AccountID = account.ID
|
||||||
|
models[i].SortOrder = i + 1
|
||||||
|
}
|
||||||
|
return tx.Create(&models).Error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeProviders(providers []string) []string {
|
func normalizeProviders(providers []string) []string {
|
||||||
seen := make(map[string]struct{}, len(providers))
|
seen := make(map[string]struct{}, len(providers))
|
||||||
result := make([]string, 0, len(providers))
|
result := make([]string, 0, len(providers))
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
)
|
)
|
||||||
@@ -14,6 +16,7 @@ type IAiRepo interface {
|
|||||||
Create(cronjob *model.OllamaModel) error
|
Create(cronjob *model.OllamaModel) error
|
||||||
Update(id uint, vars map[string]interface{}) error
|
Update(id uint, vars map[string]interface{}) error
|
||||||
Delete(opts ...DBOption) error
|
Delete(opts ...DBOption) error
|
||||||
|
DeleteAll(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAiRepo() IAiRepo {
|
func NewIAiRepo() IAiRepo {
|
||||||
@@ -67,3 +70,7 @@ func (u *AiRepo) Delete(opts ...DBOption) error {
|
|||||||
}
|
}
|
||||||
return db.Delete(&model.OllamaModel{}).Error
|
return db.Delete(&model.OllamaModel{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *AiRepo) DeleteAll(ctx context.Context) error {
|
||||||
|
return getTx(ctx).Where("1 = 1").Delete(&model.OllamaModel{}).Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package repo
|
|||||||
import (
|
import (
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LauncherRepo struct{}
|
type LauncherRepo struct{}
|
||||||
@@ -17,6 +18,11 @@ type ILauncherRepo interface {
|
|||||||
GetQuickJump(opts ...DBOption) (model.QuickJump, error)
|
GetQuickJump(opts ...DBOption) (model.QuickJump, error)
|
||||||
ListQuickJump(withAll bool) []model.QuickJump
|
ListQuickJump(withAll bool) []model.QuickJump
|
||||||
UpdateQuicks(quicks []model.QuickJump) error
|
UpdateQuicks(quicks []model.QuickJump) error
|
||||||
|
ListQuickDirectories() ([]model.QuickDirectory, error)
|
||||||
|
CountQuickDirectories() (int64, error)
|
||||||
|
CreateQuickDirectory(item *model.QuickDirectory) error
|
||||||
|
UpdateQuickDirectory(item *model.QuickDirectory) error
|
||||||
|
DeleteQuickDirectory(id uint, legacy bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewILauncherRepo() ILauncherRepo {
|
func NewILauncherRepo() ILauncherRepo {
|
||||||
@@ -95,13 +101,73 @@ func (u *LauncherRepo) UpdateQuicks(quicks []model.QuickJump) error {
|
|||||||
for _, item := range quicks {
|
for _, item := range quicks {
|
||||||
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
||||||
"is_show": item.IsShow,
|
"is_show": item.IsShow,
|
||||||
"detail": item.Detail,
|
|
||||||
"alias": item.Alias,
|
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tx.Commit()
|
return tx.Commit().Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy entries remain in the overview configuration until explicitly deleted.
|
||||||
|
func legacyQuickDirectories(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Model(&model.QuickJump{}).Where("name = ? AND is_show = ? AND detail <> ?", "File", true, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *LauncherRepo) ListQuickDirectories() ([]model.QuickDirectory, error) {
|
||||||
|
items := make([]model.QuickDirectory, 0)
|
||||||
|
if err := global.DB.Order("id ASC").Find(&items).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var quicks []model.QuickJump
|
||||||
|
if err := legacyQuickDirectories(global.DB).Order("id ASC").Find(&quicks).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, quick := range quicks {
|
||||||
|
items = append(items, model.QuickDirectory{BaseModel: quick.BaseModel, Path: quick.Detail, Alias: quick.Alias, Legacy: true})
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
func (u *LauncherRepo) CountQuickDirectories() (int64, error) {
|
||||||
|
var count, legacyCount int64
|
||||||
|
if err := global.DB.Model(&model.QuickDirectory{}).Count(&count).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if err := legacyQuickDirectories(global.DB).Count(&legacyCount).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return count + legacyCount, nil
|
||||||
|
}
|
||||||
|
func (u *LauncherRepo) CreateQuickDirectory(item *model.QuickDirectory) error {
|
||||||
|
return global.DB.Create(item).Error
|
||||||
|
}
|
||||||
|
func (u *LauncherRepo) UpdateQuickDirectory(item *model.QuickDirectory) error {
|
||||||
|
var result *gorm.DB
|
||||||
|
if item.Legacy {
|
||||||
|
result = legacyQuickDirectories(global.DB).Where("id = ?", item.ID).Updates(map[string]interface{}{"detail": item.Path, "alias": item.Alias})
|
||||||
|
} else {
|
||||||
|
result = global.DB.Model(&model.QuickDirectory{}).Where("id = ?", item.ID).Updates(map[string]interface{}{"path": item.Path, "alias": item.Alias})
|
||||||
|
}
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (u *LauncherRepo) DeleteQuickDirectory(id uint, legacy bool) error {
|
||||||
|
var result *gorm.DB
|
||||||
|
if legacy {
|
||||||
|
result = legacyQuickDirectories(global.DB).Where("id = ?", id).Updates(map[string]interface{}{"detail": nil, "alias": nil})
|
||||||
|
} else {
|
||||||
|
result = global.DB.Delete(&model.QuickDirectory{}, id)
|
||||||
|
}
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrFirewallRuleRevisionConflict = errors.New("firewall rule revision conflict")
|
|
||||||
ErrFirewallPersistenceInvalid = errors.New("invalid firewall persistence record")
|
|
||||||
)
|
|
||||||
|
|
||||||
type IFirewallRuleRepo interface {
|
|
||||||
Create(context.Context, *model.FirewallRule) error
|
|
||||||
GetByUUID(context.Context, string) (model.FirewallRule, error)
|
|
||||||
List(context.Context, ...DBOption) ([]model.FirewallRule, error)
|
|
||||||
UpdateWithRevision(context.Context, string, uint, map[string]interface{}) error
|
|
||||||
DeleteWithRevision(context.Context, string, uint) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallRuleRepo struct {
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithFirewallRuleSource(kind, id string) DBOption {
|
|
||||||
return func(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.Where("owner = ?", model.FirewallRuleOwner(kind, id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewIFirewallRuleRepo() IFirewallRuleRepo {
|
|
||||||
return &FirewallRuleRepo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFirewallRuleRepo(db *gorm.DB) *FirewallRuleRepo {
|
|
||||||
return &FirewallRuleRepo{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) Create(ctx context.Context, rule *model.FirewallRule) error {
|
|
||||||
if err := prepareFirewallRule(rule); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return r.dbFor(ctx).Create(rule).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) GetByUUID(ctx context.Context, ruleUUID string) (model.FirewallRule, error) {
|
|
||||||
var rule model.FirewallRule
|
|
||||||
err := r.dbFor(ctx).Where("uuid = ?", ruleUUID).First(&rule).Error
|
|
||||||
return rule, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) List(ctx context.Context, opts ...DBOption) ([]model.FirewallRule, error) {
|
|
||||||
var rules []model.FirewallRule
|
|
||||||
db := r.dbFor(ctx).Model(&model.FirewallRule{})
|
|
||||||
for _, opt := range opts {
|
|
||||||
db = opt(db)
|
|
||||||
}
|
|
||||||
return rules, db.Find(&rules).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) UpdateWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint, updates map[string]interface{}) error {
|
|
||||||
updates = sanitizeRuleUpdates(updates)
|
|
||||||
updates["revision"] = gorm.Expr("revision + 1")
|
|
||||||
result := r.dbFor(ctx).Model(&model.FirewallRule{}).
|
|
||||||
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
|
|
||||||
Updates(updates)
|
|
||||||
if result.Error != nil {
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
return ErrFirewallRuleRevisionConflict
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) DeleteWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint) error {
|
|
||||||
result := r.dbFor(ctx).
|
|
||||||
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
|
|
||||||
Delete(&model.FirewallRule{})
|
|
||||||
if result.Error != nil {
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
return ErrFirewallRuleRevisionConflict
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FirewallRuleRepo) dbFor(ctx context.Context) *gorm.DB {
|
|
||||||
return firewallDB(ctx, r.db)
|
|
||||||
}
|
|
||||||
|
|
||||||
func firewallDB(ctx context.Context, fallback *gorm.DB) *gorm.DB {
|
|
||||||
if ctx == nil {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
if tx, ok := ctx.Value(constant.DB).(*gorm.DB); ok && tx != nil {
|
|
||||||
return tx.WithContext(ctx)
|
|
||||||
}
|
|
||||||
if fallback == nil {
|
|
||||||
fallback = global.DB
|
|
||||||
}
|
|
||||||
return fallback.WithContext(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepareFirewallRule(rule *model.FirewallRule) error {
|
|
||||||
if rule == nil {
|
|
||||||
return fmt.Errorf("%w: rule is nil", ErrFirewallPersistenceInvalid)
|
|
||||||
}
|
|
||||||
if rule.Family == "" || rule.Protocol == "" || rule.Action == "" {
|
|
||||||
return fmt.Errorf("%w: atomic rule identity fields are required", ErrFirewallPersistenceInvalid)
|
|
||||||
}
|
|
||||||
if rule.UUID == "" {
|
|
||||||
rule.UUID = uuid.NewString()
|
|
||||||
}
|
|
||||||
if rule.Revision == 0 {
|
|
||||||
rule.Revision = 1
|
|
||||||
}
|
|
||||||
if rule.Origin == "" {
|
|
||||||
rule.Origin = constant.FirewallRuleOriginCreated
|
|
||||||
}
|
|
||||||
if rule.Owner == "" {
|
|
||||||
rule.Owner = constant.FirewallRuleSourceUser
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizeRuleUpdates(updates map[string]interface{}) map[string]interface{} {
|
|
||||||
result := cloneUpdates(updates)
|
|
||||||
delete(result, "id")
|
|
||||||
delete(result, "uuid")
|
|
||||||
delete(result, "revision")
|
|
||||||
delete(result, "created_at")
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneUpdates(updates map[string]interface{}) map[string]interface{} {
|
|
||||||
result := make(map[string]interface{}, len(updates)+1)
|
|
||||||
for key, value := range updates {
|
|
||||||
result[key] = value
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IForwardingRuleRepo interface {
|
|
||||||
List(context.Context) ([]model.ForwardingRule, error)
|
|
||||||
ReplaceAll(context.Context, []model.ForwardingRule) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type ForwardingRuleRepo struct{}
|
|
||||||
|
|
||||||
func NewIForwardingRuleRepo() IForwardingRuleRepo { return &ForwardingRuleRepo{} }
|
|
||||||
|
|
||||||
func (r *ForwardingRuleRepo) List(ctx context.Context) ([]model.ForwardingRule, error) {
|
|
||||||
var rules []model.ForwardingRule
|
|
||||||
err := global.DB.WithContext(ctx).Order("id ASC").Find(&rules).Error
|
|
||||||
return rules, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ForwardingRuleRepo) ReplaceAll(ctx context.Context, rules []model.ForwardingRule) error {
|
|
||||||
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
||||||
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.ForwardingRule{}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(rules) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return tx.Create(&rules).Error
|
|
||||||
})
|
|
||||||
}
|
|
||||||
+102
-12
@@ -1,6 +1,8 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
@@ -10,11 +12,25 @@ import (
|
|||||||
|
|
||||||
type MonitorRepo struct{}
|
type MonitorRepo struct{}
|
||||||
|
|
||||||
|
type GPUHistoryPoint struct {
|
||||||
|
model.MonitorGPU
|
||||||
|
Bucket int64
|
||||||
|
PowerPercent *float64
|
||||||
|
MemoryPercent *float64
|
||||||
|
ProcessCount *float64
|
||||||
|
}
|
||||||
|
|
||||||
type IMonitorRepo interface {
|
type IMonitorRepo interface {
|
||||||
|
CleanHost() error
|
||||||
|
CleanGPU() error
|
||||||
GetBase(opts ...DBOption) ([]model.MonitorBase, error)
|
GetBase(opts ...DBOption) ([]model.MonitorBase, error)
|
||||||
GetGPU(opts ...DBOption) ([]model.MonitorGPU, error)
|
CountGPU(opts ...DBOption) (int64, error)
|
||||||
|
GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error)
|
||||||
|
GetGPUDevices() ([]model.MonitorGPU, error)
|
||||||
GetIO(opts ...DBOption) ([]model.MonitorIO, error)
|
GetIO(opts ...DBOption) ([]model.MonitorIO, error)
|
||||||
GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, error)
|
GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, error)
|
||||||
|
GetIONames() ([]string, error)
|
||||||
|
GetNetworkNames() ([]string, error)
|
||||||
|
|
||||||
CreateMonitorBase(model model.MonitorBase) error
|
CreateMonitorBase(model model.MonitorBase) error
|
||||||
BatchCreateMonitorGPU(list []model.MonitorGPU) error
|
BatchCreateMonitorGPU(list []model.MonitorGPU) error
|
||||||
@@ -25,13 +41,26 @@ type IMonitorRepo interface {
|
|||||||
DelMonitorIO(timeForDelete time.Time) error
|
DelMonitorIO(timeForDelete time.Time) error
|
||||||
DelMonitorNet(timeForDelete time.Time) error
|
DelMonitorNet(timeForDelete time.Time) error
|
||||||
|
|
||||||
WithByProductName(name string) DBOption
|
WithByGPUDevice(deviceID, name string, legacy bool) DBOption
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIMonitorRepo() IMonitorRepo {
|
func NewIMonitorRepo() IMonitorRepo {
|
||||||
return &MonitorRepo{}
|
return &MonitorRepo{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *MonitorRepo) CleanHost() error {
|
||||||
|
for _, item := range []interface{}{&model.MonitorBase{}, &model.MonitorIO{}, &model.MonitorNetwork{}} {
|
||||||
|
if err := global.MonitorDB.Where("1 = 1").Delete(item).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonitorRepo) CleanGPU() error {
|
||||||
|
return global.GPUMonitorDB.Where("1 = 1").Delete(&model.MonitorGPU{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (u *MonitorRepo) GetBase(opts ...DBOption) ([]model.MonitorBase, error) {
|
func (u *MonitorRepo) GetBase(opts ...DBOption) ([]model.MonitorBase, error) {
|
||||||
var data []model.MonitorBase
|
var data []model.MonitorBase
|
||||||
db := global.MonitorDB
|
db := global.MonitorDB
|
||||||
@@ -59,14 +88,16 @@ func (u *MonitorRepo) GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, erro
|
|||||||
err := db.Find(&data).Error
|
err := db.Find(&data).Error
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
func (u *MonitorRepo) GetGPU(opts ...DBOption) ([]model.MonitorGPU, error) {
|
func (u *MonitorRepo) GetIONames() ([]string, error) {
|
||||||
var data []model.MonitorGPU
|
var names []string
|
||||||
db := global.GPUMonitorDB
|
err := global.MonitorDB.Model(&model.MonitorIO{}).Distinct().Pluck("name", &names).Error
|
||||||
for _, opt := range opts {
|
return names, err
|
||||||
db = opt(db)
|
|
||||||
}
|
}
|
||||||
err := db.Find(&data).Error
|
|
||||||
return data, err
|
func (u *MonitorRepo) GetNetworkNames() ([]string, error) {
|
||||||
|
var names []string
|
||||||
|
err := global.MonitorDB.Model(&model.MonitorNetwork{}).Distinct().Pluck("name", &names).Error
|
||||||
|
return names, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
|
func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
|
||||||
@@ -97,8 +128,67 @@ func (s *MonitorRepo) DelMonitorGPU(timeForDelete time.Time) error {
|
|||||||
return global.GPUMonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorGPU{}).Error
|
return global.GPUMonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorGPU{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitorRepo) WithByProductName(name string) DBOption {
|
func (u *MonitorRepo) GetGPUDevices() ([]model.MonitorGPU, error) {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
var data []model.MonitorGPU
|
||||||
return g.Where("product_name = ?", name)
|
err := global.GPUMonitorDB.Model(&model.MonitorGPU{}).Select("device_id, product_name, device_type").Group("device_id, product_name, device_type").Order("product_name, device_id").Find(&data).Error
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MonitorRepo) WithByGPUDevice(deviceID, name string, legacy bool) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
if deviceID != "" {
|
||||||
|
return db.Where("device_id = ?", deviceID)
|
||||||
|
}
|
||||||
|
db = db.Where("product_name = ?", name)
|
||||||
|
if legacy {
|
||||||
|
db = db.Where("device_id IS NULL OR device_id = ''")
|
||||||
|
}
|
||||||
|
return db
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *MonitorRepo) CountGPU(opts ...DBOption) (int64, error) {
|
||||||
|
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
err := db.Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MonitorRepo) GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error) {
|
||||||
|
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
expressions := []string{
|
||||||
|
"CASE WHEN max_power_limit > 0 THEN 100.0 * power_draw / max_power_limit END",
|
||||||
|
"CASE WHEN mem_total > 0 AND mem_used IS NOT NULL THEN 100.0 * mem_used / mem_total ELSE memory_util END",
|
||||||
|
"CASE WHEN (process_status = 'ok' OR process_status IS NULL OR process_status = '') AND json_valid(processes) THEN CASE WHEN json_type(processes) = 'array' THEN json_array_length(processes) END END",
|
||||||
|
}
|
||||||
|
aliases := []string{"power_percent", "memory_percent", "process_count"}
|
||||||
|
columns := []string{"*"}
|
||||||
|
if bucketSeconds > 0 {
|
||||||
|
operation := "AVG"
|
||||||
|
if aggregation == "max" {
|
||||||
|
operation = "MAX"
|
||||||
|
}
|
||||||
|
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), bucketSeconds)}
|
||||||
|
for _, column := range []string{"memory_activity", "encoder_util", "decoder_util", "jpeg_util", "ofa_util", "media_util", "compute_util", "copy_util", "hotspot_temperature", "fan_rpm", "ai_cpu_util", "ctrl_cpu_util", "ddr_used", "ddr_total", "hbm_used", "hbm_total", "ddr_bandwidth", "hbm_bandwidth", "memory_bandwidth", "media_frequency", "hugepages_used", "hugepages_total", "gpu_util", "temperature", "memory_temperature", "power_draw", "max_power_limit", "mem_used", "mem_total", "frequency", "memory_frequency", "fan_speed"} {
|
||||||
|
columns = append(columns, operation+"("+column+") AS "+column)
|
||||||
|
}
|
||||||
|
for i := range expressions {
|
||||||
|
expressions[i] = operation + "(" + expressions[i] + ")"
|
||||||
|
}
|
||||||
|
db = db.Group("bucket").Order("bucket ASC")
|
||||||
|
} else {
|
||||||
|
db = db.Order("created_at ASC, id ASC")
|
||||||
|
}
|
||||||
|
for i, expression := range expressions {
|
||||||
|
columns = append(columns, expression+" AS "+aliases[i])
|
||||||
|
}
|
||||||
|
var data []GPUHistoryPoint
|
||||||
|
err := db.Select(strings.Join(columns, ", ")).Scan(&data).Error
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VLLMMonitorRepo struct{}
|
||||||
|
|
||||||
|
type VLLMHistoryPoint struct {
|
||||||
|
model.MonitorVLLM
|
||||||
|
Bucket int64
|
||||||
|
CompletedRequests *float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
|
||||||
|
return global.VLLMMonitorDB.Create(point).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) Latest(id uint) (model.MonitorVLLM, error) {
|
||||||
|
var point model.MonitorVLLM
|
||||||
|
db := global.VLLMMonitorDB.Where("app_install_id = ?", id)
|
||||||
|
err := db.Order("created_at DESC, id DESC").First(&point).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return point, nil
|
||||||
|
}
|
||||||
|
return point, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) CleanTarget(id uint) error {
|
||||||
|
return global.VLLMMonitorDB.Where("app_install_id = ?", id).Delete(&model.MonitorVLLM{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) DeleteBefore(before time.Time) error {
|
||||||
|
return global.VLLMMonitorDB.Where("created_at < ?", before).Delete(&model.MonitorVLLM{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) Count(ctx context.Context, id uint, start, end time.Time) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
db := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
|
||||||
|
err := db.Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) History(ctx context.Context, id uint, start, end time.Time, seconds int64, aggregation, group string) ([]VLLMHistoryPoint, error) {
|
||||||
|
db := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
|
||||||
|
var metrics []string
|
||||||
|
if group == "" || group == "overview" {
|
||||||
|
metrics = append(metrics, "running", "waiting", "cache_usage", "prompt_throughput", "generation_throughput", "request_throughput")
|
||||||
|
if seconds <= 0 || aggregation == "max" {
|
||||||
|
metrics = append(metrics, "prefix_cache_hit_rate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if seconds <= 0 || aggregation == "max" {
|
||||||
|
if group == "" || group == "latency" {
|
||||||
|
metrics = append(metrics, "queue_time", "time_to_first_token", "time_per_output_token", "request_latency", "prefill_time", "decode_time", "time_to_first_token_p50", "time_to_first_token_p90", "time_to_first_token_p95", "time_to_first_token_p99", "time_per_output_token_p50", "time_per_output_token_p90", "time_per_output_token_p95", "time_per_output_token_p99", "request_latency_p50", "request_latency_p90", "request_latency_p95", "request_latency_p99")
|
||||||
|
}
|
||||||
|
if group == "" || group == "requests" {
|
||||||
|
metrics = append(metrics, "average_generation_tokens")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var columns []string
|
||||||
|
if seconds > 0 {
|
||||||
|
operation := "AVG"
|
||||||
|
if aggregation == "max" {
|
||||||
|
operation = "MAX"
|
||||||
|
}
|
||||||
|
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), seconds)}
|
||||||
|
for _, column := range metrics {
|
||||||
|
columns = append(columns, operation+"("+column+") AS "+column)
|
||||||
|
}
|
||||||
|
db = db.Group("bucket").Order("bucket ASC")
|
||||||
|
} else {
|
||||||
|
columns = append([]string{"id", "created_at", "app_install_id", "status", "sample_interval_seconds"}, metrics...)
|
||||||
|
if group == "" || group == "requests" {
|
||||||
|
columns = append(columns, "histogram_deltas", "request_deltas")
|
||||||
|
}
|
||||||
|
db = db.Order("created_at ASC, id ASC")
|
||||||
|
}
|
||||||
|
if group == "" || group == "overview" {
|
||||||
|
count := "CASE WHEN json_valid(request_deltas) THEN CASE WHEN json_extract(request_deltas, '$.seconds') > 0 THEN ROUND(request_throughput * json_extract(request_deltas, '$.seconds')) END END"
|
||||||
|
if seconds > 0 {
|
||||||
|
count = "SUM(" + count + ")"
|
||||||
|
}
|
||||||
|
columns = append(columns, count+" AS completed_requests")
|
||||||
|
}
|
||||||
|
var points []VLLMHistoryPoint
|
||||||
|
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
|
||||||
|
return points, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VLLMMonitorRepo) WalkDeltas(ctx context.Context, id uint, start, end time.Time, seconds int64, aggregation, group string, visit func(int64, string, string) error) error {
|
||||||
|
histograms, requests := "''", "''"
|
||||||
|
if group == "" || group == "requests" || (group == "latency" && aggregation != "max") {
|
||||||
|
histograms = "COALESCE(histogram_deltas, '')"
|
||||||
|
}
|
||||||
|
if group == "" || group == "requests" || (group == "overview" && aggregation != "max") {
|
||||||
|
requests = "COALESCE(request_deltas, '')"
|
||||||
|
}
|
||||||
|
if histograms == "''" && requests == "''" {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
bucket := fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d", start.Unix(), seconds)
|
||||||
|
rows, err := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).
|
||||||
|
Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end).
|
||||||
|
Select(bucket + ", " + histograms + ", " + requests).Rows()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var index int64
|
||||||
|
var histograms, requests string
|
||||||
|
if err := rows.Scan(&index, &histograms, &requests); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := visit(index, histograms, requests); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows.Err()
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SettingRepo struct{}
|
type SettingRepo struct{}
|
||||||
@@ -14,17 +16,22 @@ type ISettingRepo interface {
|
|||||||
GetList(opts ...DBOption) ([]model.Setting, error)
|
GetList(opts ...DBOption) ([]model.Setting, error)
|
||||||
Get(opts ...DBOption) (model.Setting, error)
|
Get(opts ...DBOption) (model.Setting, error)
|
||||||
GetValueByKey(key string) (string, error)
|
GetValueByKey(key string) (string, error)
|
||||||
|
GetCoreValueByKey(key string) (string, error)
|
||||||
Create(key, value string) error
|
Create(key, value string) error
|
||||||
Update(key, value string) error
|
Update(key, value string) error
|
||||||
WithByKey(key string) DBOption
|
WithByKey(key string) DBOption
|
||||||
|
|
||||||
UpdateOrCreate(key, value string) error
|
UpdateOrCreate(key, value string) error
|
||||||
|
UpdateValues(map[string]string) error
|
||||||
|
|
||||||
GetDescription(opts ...DBOption) (model.CommonDescription, error)
|
GetDescription(opts ...DBOption) (model.CommonDescription, error)
|
||||||
GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error)
|
GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error)
|
||||||
CreateDescription(data *model.CommonDescription) error
|
CreateDescription(data *model.CommonDescription) error
|
||||||
|
SaveDescriptions(context.Context, []model.CommonDescription) error
|
||||||
UpdateDescription(id string, val map[string]interface{}) error
|
UpdateDescription(id string, val map[string]interface{}) error
|
||||||
DelDescription(id string) error
|
DelDescription(id string) error
|
||||||
|
DeleteDescriptions(context.Context, string, []string, bool) (int64, error)
|
||||||
|
WithDescriptionIDs(ids []string) DBOption
|
||||||
WithByDescriptionID(id string) DBOption
|
WithByDescriptionID(id string) DBOption
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +75,17 @@ func (s *SettingRepo) GetValueByKey(key string) (string, error) {
|
|||||||
return setting.Value, nil
|
return setting.Value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) GetCoreValueByKey(key string) (string, error) {
|
||||||
|
if global.CoreDB == nil {
|
||||||
|
return "", errors.New("core database is unavailable")
|
||||||
|
}
|
||||||
|
var setting model.Setting
|
||||||
|
if err := global.CoreDB.Where("key = ?", key).First(&setting).Error; err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return setting.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingRepo) WithByKey(key string) DBOption {
|
func (s *SettingRepo) WithByKey(key string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
return g.Where("key = ?", key)
|
return g.Where("key = ?", key)
|
||||||
@@ -90,6 +108,25 @@ func (s *SettingRepo) UpdateOrCreate(key, value string) error {
|
|||||||
return global.DB.Model(&setting).UpdateColumn("value", value).Error
|
return global.DB.Model(&setting).UpdateColumn("value", value).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) UpdateValues(values map[string]string) error {
|
||||||
|
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
for key, value := range values {
|
||||||
|
var setting model.Setting
|
||||||
|
err := tx.Where("key = ?", key).First(&setting).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
if err := tx.Create(&model.Setting{Key: key, Value: value}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
} else if err := tx.Model(&setting).UpdateColumn("value", value).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingRepo) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) {
|
func (s *SettingRepo) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) {
|
||||||
var lists []model.CommonDescription
|
var lists []model.CommonDescription
|
||||||
db := global.DB.Model(&model.CommonDescription{})
|
db := global.DB.Model(&model.CommonDescription{})
|
||||||
@@ -111,14 +148,42 @@ func (s *SettingRepo) GetDescription(opts ...DBOption) (model.CommonDescription,
|
|||||||
func (s *SettingRepo) CreateDescription(data *model.CommonDescription) error {
|
func (s *SettingRepo) CreateDescription(data *model.CommonDescription) error {
|
||||||
return global.DB.Create(data).Error
|
return global.DB.Create(data).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) SaveDescriptions(ctx context.Context, descriptions []model.CommonDescription) error {
|
||||||
|
return global.DB.WithContext(ctx).Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "id"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{"description"}),
|
||||||
|
}).CreateInBatches(&descriptions, 100).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingRepo) UpdateDescription(id string, val map[string]interface{}) error {
|
func (s *SettingRepo) UpdateDescription(id string, val map[string]interface{}) error {
|
||||||
return global.DB.Model(&model.CommonDescription{}).Where("id = ?", id).Updates(val).Error
|
return global.DB.Model(&model.CommonDescription{}).Where("id = ?", id).Updates(val).Error
|
||||||
}
|
}
|
||||||
func (s *SettingRepo) DelDescription(id string) error {
|
func (s *SettingRepo) DelDescription(id string) error {
|
||||||
return global.DB.Where("id = ?", id).Delete(&model.CommonDescription{}).Error
|
return global.DB.Where("id = ?", id).Delete(&model.CommonDescription{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) DeleteDescriptions(ctx context.Context, kind string, ids []string, emptyOnly bool) (int64, error) {
|
||||||
|
var deleted int64
|
||||||
|
for start := 0; start < len(ids); start += 500 {
|
||||||
|
query := global.DB.WithContext(ctx).Where("type = ? AND id IN ?", kind, ids[start:min(start+500, len(ids))])
|
||||||
|
if emptyOnly {
|
||||||
|
query = query.Where("description = ? AND is_pinned = ?", "", false)
|
||||||
|
}
|
||||||
|
result := query.Delete(&model.CommonDescription{})
|
||||||
|
deleted += result.RowsAffected
|
||||||
|
if result.Error != nil {
|
||||||
|
return deleted, result.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deleted, nil
|
||||||
|
}
|
||||||
func (s *SettingRepo) WithByDescriptionID(id string) DBOption {
|
func (s *SettingRepo) WithByDescriptionID(id string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
return g.Where("id = ?", id)
|
return g.Where("id = ?", id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingRepo) WithDescriptionIDs(ids []string) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB { return db.Where("id IN ?", ids) }
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package repo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"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"
|
||||||
@@ -110,7 +111,9 @@ func (t TaskRepo) Update(ctx context.Context, task *model.Task) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t TaskRepo) UpdateRunningTaskToFailed() error {
|
func (t TaskRepo) UpdateRunningTaskToFailed() error {
|
||||||
return getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Updates(map[string]interface{}{"status": constant.StatusFailed, "error_msg": "1Panel restart causes failure"}).Error
|
return getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Updates(map[string]interface{}{
|
||||||
|
"status": constant.StatusFailed, "error_msg": constant.InterruptedMsg, "end_at": time.Now(),
|
||||||
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t TaskRepo) CountExecutingTask() (int64, error) {
|
func (t TaskRepo) CountExecutingTask() (int64, error) {
|
||||||
|
|||||||
@@ -142,6 +142,9 @@ const (
|
|||||||
|
|
||||||
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||||
agentType := req.AgentType
|
agentType := req.AgentType
|
||||||
|
if err := validateNativeAgentCreate(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if err := checkPortExist(req.WebUIPort); err != nil {
|
if err := checkPortExist(req.WebUIPort); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -237,6 +240,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
}
|
}
|
||||||
} else if agentType == constant.AppCopaw {
|
} else if agentType == constant.AppCopaw {
|
||||||
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||||
|
} else if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOctop {
|
||||||
|
dashboardAuth = agentDashboardAuth{Username: req.DashboardUsername, Password: req.DashboardPassword}
|
||||||
}
|
}
|
||||||
|
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
@@ -245,6 +250,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
constant.HostIP: "",
|
constant.HostIP: "",
|
||||||
}
|
}
|
||||||
setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort)
|
setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort)
|
||||||
|
if agentType == constant.AppDeepSeekHarness {
|
||||||
|
params["HTTPS_ACCESS_HOST"] = req.AccessHost
|
||||||
|
}
|
||||||
|
if agentType == constant.AppOctop {
|
||||||
|
params["OCTOP_LOG_LEVEL"] = "info"
|
||||||
|
}
|
||||||
if agentType == constant.AppOpenclaw {
|
if agentType == constant.AppOpenclaw {
|
||||||
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
|
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
|
||||||
params["ALLOWED_ORIGIN"] = allowedOrigin
|
params["ALLOWED_ORIGIN"] = allowedOrigin
|
||||||
@@ -738,7 +749,7 @@ func requiresAgentAccount(agentType string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
|
func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
|
||||||
if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
|
if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
|
||||||
params["PANEL_APP_PORT_HTTPS"] = webUIPort
|
params["PANEL_APP_PORT_HTTPS"] = webUIPort
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -936,6 +947,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -967,7 +979,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); err != nil {
|
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks, req.Metadata); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1043,7 +1055,7 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1096,12 +1108,15 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
if strings.TrimSpace(requestedVerifyModel) == "" {
|
if strings.TrimSpace(requestedVerifyModel) == "" {
|
||||||
requestedVerifyModel = account.VerifyModel
|
requestedVerifyModel = account.VerifyModel
|
||||||
}
|
}
|
||||||
verifyModel, err := resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
verifyModel := ""
|
||||||
|
if account.AppInstallID == 0 || len(models) > 0 {
|
||||||
|
verifyModel, err = resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability, account.AppInstallID != 0 && len(models) == 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1115,7 +1130,7 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
account.Remark = req.Remark
|
account.Remark = req.Remark
|
||||||
account.Verified = true
|
account.Verified = true
|
||||||
|
|
||||||
if err := global.DB.Save(account).Error; err != nil {
|
if err := agentAccountRepo.Save(account); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
terminalai.InvalidateTerminalRuntimeCache()
|
terminalai.InvalidateTerminalRuntimeCache()
|
||||||
@@ -1146,13 +1161,34 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
installIDs := make([]uint, 0, len(list))
|
||||||
|
for _, account := range list {
|
||||||
|
if account.AppInstallID != 0 {
|
||||||
|
installIDs = append(installIDs, account.AppInstallID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
localInstalls := make(map[uint]bool, len(installIDs))
|
||||||
|
if len(installIDs) > 0 {
|
||||||
|
installs, err := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(installIDs))
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
for _, install := range installs {
|
||||||
|
localInstalls[install.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
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 {
|
||||||
apiKey = item.APIKey
|
apiKey = item.APIKey
|
||||||
}
|
}
|
||||||
|
appInstallID := uint(0)
|
||||||
|
if localInstalls[item.AppInstallID] {
|
||||||
|
appInstallID = item.AppInstallID
|
||||||
|
}
|
||||||
items = append(items, dto.AgentAccountInfo{
|
items = append(items, dto.AgentAccountInfo{
|
||||||
|
AppInstallID: appInstallID,
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
MasterAccountID: item.MasterAccountID,
|
MasterAccountID: item.MasterAccountID,
|
||||||
Provider: item.Provider,
|
Provider: item.Provider,
|
||||||
@@ -1350,13 +1386,27 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
||||||
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true)
|
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true, false)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
||||||
agentAccountMutationMu.Lock()
|
agentAccountMutationMu.Lock()
|
||||||
defer agentAccountMutationMu.Unlock()
|
defer agentAccountMutationMu.Unlock()
|
||||||
|
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if account.AppInstallID != 0 && account.Provider != "ollama" {
|
||||||
|
_, err := appInstallRepo.GetFirst(repo.WithByID(account.AppInstallID))
|
||||||
|
if err == nil {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "Delete the local inference engine instead", nil)
|
||||||
|
}
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
var agentCount int64
|
var agentCount int64
|
||||||
if err := tx.Model(&model.Agent{}).Where("account_id = ?", req.ID).Count(&agentCount).Error; err != nil {
|
if err := tx.Model(&model.Agent{}).Where("account_id = ?", req.ID).Count(&agentCount).Error; err != nil {
|
||||||
@@ -1684,7 +1734,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); err != nil {
|
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case constant.AppHermesAgent:
|
case constant.AppHermesAgent:
|
||||||
|
|||||||
@@ -1354,6 +1354,10 @@ func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID
|
|||||||
} else if slices.Contains(options, "--dangerously-force-unsafe-install") {
|
} else if slices.Contains(options, "--dangerously-force-unsafe-install") {
|
||||||
args = append(args, "--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...)
|
return mgr.Run("docker", args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const hermesWeixinLoginScript = `import asyncio
|
const hermesWeixinLoginScript = `import asyncio
|
||||||
|
try:
|
||||||
|
from plugins.platforms.weixin.adapter import qr_login
|
||||||
|
except ModuleNotFoundError as exc:
|
||||||
|
if exc.name not in ('plugins', 'plugins.platforms', 'plugins.platforms.weixin', 'plugins.platforms.weixin.adapter'):
|
||||||
|
raise
|
||||||
from gateway.platforms.weixin import qr_login
|
from gateway.platforms.weixin import qr_login
|
||||||
from hermes_cli.config import save_env_value
|
from hermes_cli.config import save_env_value
|
||||||
|
|
||||||
creds = asyncio.run(qr_login('/opt/data'))
|
creds = asyncio.run(qr_login('/opt/data'))
|
||||||
print('RESULT =', creds)
|
|
||||||
|
|
||||||
if not creds:
|
if not creds:
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL string, models []dto.AgentAccountModel, updateBaseURL, createIfMissing bool) (*model.AgentAccount, error) {
|
||||||
|
if install.ID == 0 || (install.App.Key != "ollama" && install.App.Key != "vllm") {
|
||||||
|
return nil, fmt.Errorf("invalid local model installation")
|
||||||
|
}
|
||||||
|
agentAccountMutationMu.Lock()
|
||||||
|
defer agentAccountMutationMu.Unlock()
|
||||||
|
account, err := agentAccountRepo.GetFirst(repo.WithByAppInstallID(install.ID))
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) && accountID != 0 {
|
||||||
|
account, err = agentAccountRepo.GetFirst(repo.WithByID(accountID), repo.WithByProvider(install.App.Key))
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err == nil && account.AppInstallID != 0 && account.AppInstallID != install.ID {
|
||||||
|
return nil, fmt.Errorf("model account belongs to another installation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
if !createIfMissing {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
name := install.Name
|
||||||
|
for suffix := 1; ; suffix++ {
|
||||||
|
existing, err := agentAccountRepo.List(repo.WithByProvider(install.App.Key), repo.WithByName(name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(existing) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
name = fmt.Sprintf("%s-%d", install.Name, suffix)
|
||||||
|
}
|
||||||
|
apiType, apiKey := "openai-completions", "vllm-local"
|
||||||
|
if install.App.Key == "ollama" {
|
||||||
|
apiType, apiKey = "openai-responses", "ollama-local"
|
||||||
|
}
|
||||||
|
account = &model.AgentAccount{Name: name, Provider: install.App.Key, BaseURL: baseURL, APIType: apiType, APIKey: apiKey, RememberAPIKey: true, Verified: true}
|
||||||
|
}
|
||||||
|
accountChanged := account.ID == 0 || account.AppInstallID != install.ID || (updateBaseURL && account.BaseURL != baseURL)
|
||||||
|
account.AppInstallID = install.ID
|
||||||
|
if updateBaseURL {
|
||||||
|
account.BaseURL = baseURL
|
||||||
|
}
|
||||||
|
rows := make([]model.AgentAccountModel, 0, len(models))
|
||||||
|
verifyModelExists := account.VerifyModel == ""
|
||||||
|
for _, item := range models {
|
||||||
|
rows = append(rows, model.AgentAccountModel{Model: item.ID, Name: item.Name})
|
||||||
|
if item.ID == account.VerifyModel {
|
||||||
|
verifyModelExists = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !verifyModelExists {
|
||||||
|
account.VerifyModel = ""
|
||||||
|
accountChanged = true
|
||||||
|
}
|
||||||
|
var previous []model.AgentAccountModel
|
||||||
|
if account.ID != 0 {
|
||||||
|
previous, err = agentAccountModelRepo.List(repo.WithByAccountID(account.ID), repo.WithOrderAsc("sort_order"), repo.WithOrderAsc("id"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modelsChanged := len(previous) != len(rows)
|
||||||
|
if !modelsChanged {
|
||||||
|
for i := range rows {
|
||||||
|
if previous[i].Model != rows[i].Model || previous[i].Name != rows[i].Name {
|
||||||
|
modelsChanged = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if modelsChanged {
|
||||||
|
err = agentAccountRepo.SaveLocal(account, rows)
|
||||||
|
} else if accountChanged {
|
||||||
|
err = agentAccountRepo.Save(account)
|
||||||
|
} else {
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
terminalai.InvalidateTerminalRuntimeCache()
|
||||||
|
terminalai.InvalidateFileAIRuntimeCache()
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SyncOllamaModelAccount(createIfMissing bool) error {
|
||||||
|
app, err := appRepo.GetFirst(appRepo.WithKey("ollama"))
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
install, err := appInstallRepo.GetFirst(appInstallRepo.WithAppId(app.ID))
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !install.SyncModelAccount {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return syncOllamaModelAccount(install, fmt.Sprintf("http://%s:11434/v1", install.ContainerName), false, createIfMissing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncOllamaModelAccount(install model.AppInstall, baseURL string, updateBaseURL, createIfMissing bool) error {
|
||||||
|
models, err := aiRepo.List(repo.WithByStatus(constant.StatusSuccess), repo.WithOrderAsc("name"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
items := make([]dto.AgentAccountModel, 0, len(models))
|
||||||
|
for _, item := range models {
|
||||||
|
items = append(items, dto.AgentAccountModel{ID: item.Name, Name: item.Name})
|
||||||
|
}
|
||||||
|
_, err = SyncLocalModelAccount(install, 0, baseURL, items, updateBaseURL, createIfMissing)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadOllamaModelAccountAddress(install model.AppInstall) (string, string, error) {
|
||||||
|
addressType := install.ModelAccountBaseURLType
|
||||||
|
if addressType == "" {
|
||||||
|
addressType = "container"
|
||||||
|
}
|
||||||
|
account, err := agentAccountRepo.GetFirst(repo.WithByAppInstallID(install.ID), repo.WithByProvider("ollama"))
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return addressType, install.ModelAccountBaseURL, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if addressType != "custom" {
|
||||||
|
expectedURL, err := ResolveLocalModelAccountBaseURL(addressType, "", install.ContainerName, "11434", install.HttpPort)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if strings.TrimRight(account.BaseURL, "/") != expectedURL {
|
||||||
|
addressType = "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addressType, account.BaseURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CleanupLocalModelAccount(installID uint) {
|
||||||
|
if installID == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
accounts, err := agentAccountRepo.List(repo.WithByAppInstallID(installID))
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("load local model accounts: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, account := range accounts {
|
||||||
|
if err := (AgentService{}).DeleteAccount(dto.AgentAccountDeleteReq{ID: account.ID}); err != nil {
|
||||||
|
global.LOG.Warnf("keep local model account %d after uninstall: %v", account.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveLocalModelAccountBaseURL(addressType, customURL, containerName, containerPort string, hostPort int) (string, error) {
|
||||||
|
switch addressType {
|
||||||
|
case "custom":
|
||||||
|
baseURL := strings.TrimRight(strings.TrimSpace(customURL), "/")
|
||||||
|
if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
|
||||||
|
return "", fmt.Errorf("model account base URL must start with http:// or https://")
|
||||||
|
}
|
||||||
|
return baseURL, nil
|
||||||
|
case "localhost":
|
||||||
|
return fmt.Sprintf("http://127.0.0.1:%d/v1", hostPort), nil
|
||||||
|
case "systemIP":
|
||||||
|
systemIP, err := settingRepo.GetValueByKey("SystemIP")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
systemIP = strings.TrimSpace(systemIP)
|
||||||
|
if systemIP == "" {
|
||||||
|
systemIP = "127.0.0.1"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("http://%s:%d/v1", systemIP, hostPort), nil
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("http://%s:%s/v1", containerName, containerPort), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMiniMaxOpenclawModelMetadata(t *testing.T) {
|
||||||
|
for _, provider := range []string{"minimax", "custom", "bailian-coding-plan"} {
|
||||||
|
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||||
|
for _, id := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5", "saved-custom"} {
|
||||||
|
t.Run(provider+"/"+protocol+"/"+id, func(t *testing.T) {
|
||||||
|
account := &model.AgentAccount{Provider: provider, APIType: protocol, BaseURL: "https://example.test/v1", APIKey: "test-key"}
|
||||||
|
_, entry, _, _, err := buildOpenclawAccountModelConfig(account, dto.AgentAccountModel{ID: id, Name: "Saved name"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
payload, err := json.Marshal(entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var actual modelEntry
|
||||||
|
if err := json.Unmarshal(payload, &actual); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if actual.ID != id || actual.Name != "Saved name" {
|
||||||
|
t.Fatalf("changed saved identity: %s", payload)
|
||||||
|
}
|
||||||
|
if provider == "minimax" && id == "MiniMax-M3.1-Flash" {
|
||||||
|
if actual.ContextWindow != 1000000 || actual.MaxTokens != 524288 || !reflect.DeepEqual(actual.Input, []string{"text", "image"}) {
|
||||||
|
t.Fatalf("incorrect direct Flash metadata: %s", payload)
|
||||||
|
}
|
||||||
|
} else if actual.ContextWindow != 0 || actual.MaxTokens != 0 {
|
||||||
|
t.Fatalf("changed existing/gateway limits: %s", payload)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxPreservesSavedOpenclawMetadata(t *testing.T) {
|
||||||
|
current := &modelsConfig{Mode: "merge", Providers: map[string]modelProvider{
|
||||||
|
"minimax": {Models: []modelEntry{{ID: "MiniMax-M3.1-Flash", Input: []string{"text"}, ContextWindow: 32000, MaxTokens: 4096}}},
|
||||||
|
}}
|
||||||
|
conf := map[string]interface{}{}
|
||||||
|
if err := applyOpenclawModelsConfig(conf, current); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
next := &modelsConfig{Mode: "merge", Providers: map[string]modelProvider{
|
||||||
|
"minimax": {Models: []modelEntry{{ID: "MiniMax-M3.1-Flash", Input: []string{"text", "image"}, ContextWindow: 1000000, MaxTokens: 524288}}},
|
||||||
|
}}
|
||||||
|
preserveOpenclawModelMetadata(conf, next)
|
||||||
|
if !reflect.DeepEqual(next.Providers["minimax"].Models, current.Providers["minimax"].Models) {
|
||||||
|
t.Fatalf("saved metadata changed: %+v", next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxDefaultAccountRequest(t *testing.T) {
|
||||||
|
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||||
|
account := &model.AgentAccount{Provider: "minimax", APIType: protocol}
|
||||||
|
models, err := buildInitialAgentAccountModels(account, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
selected, err := resolveAgentAccountVerifyModel("minimax", "", models)
|
||||||
|
if err != nil || selected != "MiniMax-M3.1-Flash" || len(models) != 2 || models[1].ID != "MiniMax-M3" {
|
||||||
|
t.Fatalf("unexpected default: %s, %+v, %v", selected, models, err)
|
||||||
|
}
|
||||||
|
request := providercatalog.BuildVerifyRequest("minimax", protocol, "", "https://example.test", "test-key", selected)
|
||||||
|
var body map[string]interface{}
|
||||||
|
if err := json.Unmarshal(request.Body, &body); err != nil || body["model"] != selected {
|
||||||
|
t.Fatalf("default request: %s, %v", request.Body, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxHermesConfiguration(t *testing.T) {
|
||||||
|
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||||
|
for _, id := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "my-custom-model"} {
|
||||||
|
dir := t.TempDir()
|
||||||
|
account := &model.AgentAccount{Provider: "minimax", APIType: protocol, BaseURL: "https://api.minimaxi.com/anthropic", APIKey: "test-key"}
|
||||||
|
if err := writeHermesConfig(dir, account, id, ""); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
conf, err := readHermesConfig(filepath.Join(dir, "config.yaml"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if conf.Model.Default != id || conf.Model.Provider != "minimax-cn" || conf.Model.BaseURL != account.BaseURL {
|
||||||
|
t.Fatalf("changed Hermes selection: %+v", conf.Model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiniMaxExplicitAccountModels(t *testing.T) {
|
||||||
|
for _, provider := range []string{"minimax", "bailian-coding-plan", "custom"} {
|
||||||
|
account := &model.AgentAccount{Provider: provider, APIType: "openai-completions"}
|
||||||
|
requested := []dto.AgentAccountModel{{ID: "MiniMax-M2.5", Name: "Saved legacy"}, {ID: "my-custom-model", Name: "Saved custom"}}
|
||||||
|
actual, err := buildInitialAgentAccountModels(account, requested)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(actual, requested) {
|
||||||
|
t.Fatalf("changed explicit %s models: %+v", provider, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
var deepSeekUsernamePattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
|
||||||
|
var deepSeekHostLabelPattern = regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$`)
|
||||||
|
|
||||||
|
func isDeepSeekAccessHost(value string) bool {
|
||||||
|
if ip := net.ParseIP(value); ip != nil {
|
||||||
|
return ip.To4() != nil && !strings.Contains(value, ":")
|
||||||
|
}
|
||||||
|
if value == "" || len(value) > 253 || strings.Trim(value, "0123456789.") == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, label := range strings.Split(value, ".") {
|
||||||
|
if !deepSeekHostLabelPattern.MatchString(label) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateNativeAgentCreate(req dto.AgentCreateReq) error {
|
||||||
|
if req.AgentType != constant.AppDeepSeekHarness && req.AgentType != constant.AppOctop {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.DashboardUsername) == "" || strings.ContainsAny(req.DashboardUsername, "\r\n\x00") {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardUsername", nil)
|
||||||
|
}
|
||||||
|
if strings.ContainsAny(req.DashboardPassword, "\r\n\x00") || req.DashboardPassword != strings.TrimSpace(req.DashboardPassword) {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||||
|
}
|
||||||
|
if req.AgentType == constant.AppDeepSeekHarness {
|
||||||
|
if !isDeepSeekAccessHost(req.AccessHost) {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "accessHost", nil)
|
||||||
|
}
|
||||||
|
if !deepSeekUsernamePattern.MatchString(req.DashboardUsername) {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardUsername", nil)
|
||||||
|
}
|
||||||
|
if utf8.RuneCountInString(req.DashboardPassword) < 12 {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
hasLetter, hasDigit := false, false
|
||||||
|
for _, char := range req.DashboardPassword {
|
||||||
|
hasLetter = hasLetter || unicode.IsLetter(char)
|
||||||
|
hasDigit = hasDigit || unicode.IsDigit(char)
|
||||||
|
}
|
||||||
|
if utf8.RuneCountInString(req.DashboardPassword) < 8 || !hasLetter || !hasDigit {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||||
|
}
|
||||||
|
switch strings.ToLower(req.DashboardPassword) {
|
||||||
|
case "password", "password1", "password12", "password123", "12345678", "123456789", "qwerty123",
|
||||||
|
"admin123", "welcome1", "letmein1", "changeme1", "octop123", "abc12345", "iloveyou1":
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -97,7 +98,7 @@ func ensureContainerRunning(containerName string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability bool) (resolvedAgentAccountInput, error) {
|
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability, allowEmptyModel bool) (resolvedAgentAccountInput, error) {
|
||||||
resolvedAPIKey := strings.TrimSpace(apiKey)
|
resolvedAPIKey := strings.TrimSpace(apiKey)
|
||||||
resolvedAPIType := strings.TrimSpace(apiType)
|
resolvedAPIType := strings.TrimSpace(apiType)
|
||||||
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||||
@@ -112,7 +113,7 @@ func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, mode
|
|||||||
return resolvedAgentAccountInput{}, err
|
return resolvedAgentAccountInput{}, err
|
||||||
}
|
}
|
||||||
modelID = strings.TrimSpace(modelID)
|
modelID = strings.TrimSpace(modelID)
|
||||||
if modelID == "" {
|
if modelID == "" && !allowEmptyModel {
|
||||||
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
||||||
}
|
}
|
||||||
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
|
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
|
||||||
@@ -414,6 +415,15 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
|||||||
} else {
|
} else {
|
||||||
item.WebUIPort = appInstall.HttpPort
|
item.WebUIPort = appInstall.HttpPort
|
||||||
}
|
}
|
||||||
|
if agentType == constant.AppDeepSeekHarness {
|
||||||
|
item.WebUIPort = appInstall.HttpsPort
|
||||||
|
if values, err := readAgentEnvMap(appInstall.GetEnvPath()); err == nil {
|
||||||
|
accessHost := values["HTTPS_ACCESS_HOST"]
|
||||||
|
if isDeepSeekAccessHost(accessHost) {
|
||||||
|
item.AccessHost = accessHost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
item.Path = appInstall.GetPath()
|
item.Path = appInstall.GetPath()
|
||||||
item.Status = appInstall.Status
|
item.Status = appInstall.Status
|
||||||
item.Message = appInstall.Message
|
item.Message = appInstall.Message
|
||||||
@@ -441,7 +451,8 @@ func localizedAgentProviderName(provider string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isAgentAppKey(appKey string) bool {
|
func isAgentAppKey(appKey string) bool {
|
||||||
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent ||
|
||||||
|
appKey == constant.AppDeepSeekHarness || appKey == constant.AppOctop
|
||||||
}
|
}
|
||||||
|
|
||||||
func isOpenclawLegacyHTTPVersion(version string) bool {
|
func isOpenclawLegacyHTTPVersion(version string) bool {
|
||||||
@@ -651,7 +662,7 @@ func checkAgentUpgradable(install model.AppInstall) bool {
|
|||||||
if len(ignores) > 0 {
|
if len(ignores) > 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if common.IsCrossVersion(install.Version, item.Version) && !install.App.CrossVersionUpdate {
|
if isCrossAppVersion(install.App.Key, install.Version, item.Version) && !install.App.CrossVersionUpdate {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
versions = append(versions, item.Version)
|
versions = append(versions, item.Version)
|
||||||
@@ -659,12 +670,12 @@ func checkAgentUpgradable(install model.AppInstall) bool {
|
|||||||
if len(versions) == 0 {
|
if len(versions) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
versions = common.GetSortedVersions(versions)
|
versions = sortAppVersions(install.App.Key, versions)
|
||||||
lastVersion := versions[0]
|
lastVersion := versions[0]
|
||||||
if common.IsCrossVersion(install.Version, lastVersion) {
|
if isCrossAppVersion(install.App.Key, install.Version, lastVersion) {
|
||||||
return install.App.CrossVersionUpdate
|
return install.App.CrossVersionUpdate
|
||||||
}
|
}
|
||||||
return common.CompareVersion(lastVersion, install.Version)
|
return isNewerAppVersion(install.App.Key, lastVersion, install.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
type openclawConfig struct {
|
type openclawConfig struct {
|
||||||
@@ -740,6 +751,8 @@ 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 {
|
||||||
@@ -767,7 +780,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) error {
|
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string, metadata []dto.AgentModelMetadata) error {
|
||||||
if strings.TrimSpace(confDir) == "" {
|
if strings.TrimSpace(confDir) == "" {
|
||||||
return fmt.Errorf("config dir is required")
|
return fmt.Errorf("config dir is required")
|
||||||
}
|
}
|
||||||
@@ -852,6 +865,7 @@ 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
|
||||||
}
|
}
|
||||||
@@ -906,6 +920,9 @@ 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
|
||||||
}
|
}
|
||||||
@@ -920,6 +937,144 @@ 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 {
|
||||||
@@ -1041,7 +1196,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); err != nil {
|
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dataDir := path.Join(appInstall.GetPath(), "data")
|
dataDir := path.Join(appInstall.GetPath(), "data")
|
||||||
@@ -1108,7 +1263,12 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", modelEntry{}, "", modelProvider{}, err
|
return "", modelEntry{}, "", modelProvider{}, err
|
||||||
}
|
}
|
||||||
return providerPatch.PrimaryModel, buildOpenclawModelEntry(providerPatch.ModelID, model), providerPatch.ProviderKey, modelProvider{
|
entry := buildOpenclawModelEntry(providerPatch.ModelID, model)
|
||||||
|
if account.Provider == "minimax" && providerPatch.ModelID == "MiniMax-M3.1-Flash" {
|
||||||
|
entry.ContextWindow = 1000000
|
||||||
|
entry.MaxTokens = 524288
|
||||||
|
}
|
||||||
|
return providerPatch.PrimaryModel, entry, providerPatch.ProviderKey, modelProvider{
|
||||||
ApiKey: providerPatch.APIKey,
|
ApiKey: providerPatch.APIKey,
|
||||||
BaseUrl: providerPatch.BaseURL,
|
BaseUrl: providerPatch.BaseURL,
|
||||||
Api: providerPatch.APIType,
|
Api: providerPatch.APIType,
|
||||||
@@ -1496,6 +1656,10 @@ func agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
|
|||||||
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
|
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
|
||||||
case constant.AppCopaw:
|
case constant.AppCopaw:
|
||||||
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
|
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
|
||||||
|
case constant.AppDeepSeekHarness:
|
||||||
|
return "DSH_AUTH_USERNAME", "DSH_AUTH_PASSWORD", true
|
||||||
|
case constant.AppOctop:
|
||||||
|
return "OCTOP_ADMIN_USERNAME", "OCTOP_DEFAULT_PASSWORD", true
|
||||||
default:
|
default:
|
||||||
return "", "", false
|
return "", "", false
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-47
@@ -8,6 +8,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -28,13 +29,15 @@ import (
|
|||||||
|
|
||||||
type AIToolService struct{}
|
type AIToolService struct{}
|
||||||
|
|
||||||
|
var ollamaModelMu sync.Mutex
|
||||||
|
|
||||||
type IAIToolService interface {
|
type IAIToolService interface {
|
||||||
Search(search dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error)
|
Search(search dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error)
|
||||||
Create(req dto.OllamaModelName) error
|
Create(req dto.OllamaModelName) error
|
||||||
Close(name string) error
|
Close(name string) error
|
||||||
Recreate(req dto.OllamaModelName) error
|
Recreate(req dto.OllamaModelName) error
|
||||||
Delete(req dto.ForceDelete) error
|
Delete(req dto.ForceDelete) error
|
||||||
Sync() ([]dto.OllamaModelDropList, error)
|
SyncAccount(req dto.OllamaModelAccountSync) error
|
||||||
LoadDetail(name string) (string, error)
|
LoadDetail(name string) (string, error)
|
||||||
BindDomain(req dto.OllamaBindDomain) error
|
BindDomain(req dto.OllamaBindDomain) error
|
||||||
GetBindDomain(req dto.OllamaBindDomainReq) (*dto.OllamaBindDomainRes, error)
|
GetBindDomain(req dto.OllamaBindDomainReq) (*dto.OllamaBindDomainRes, error)
|
||||||
@@ -46,6 +49,7 @@ func NewIAIToolService() IAIToolService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) Search(req dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error) {
|
func (u *AIToolService) Search(req dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error) {
|
||||||
|
syncErr := syncOllamaModels()
|
||||||
var options []repo.DBOption
|
var options []repo.DBOption
|
||||||
if len(req.Info) != 0 {
|
if len(req.Info) != 0 {
|
||||||
options = append(options, repo.WithByLikeName(req.Info))
|
options = append(options, repo.WithByLikeName(req.Info))
|
||||||
@@ -60,6 +64,11 @@ func (u *AIToolService) Search(req dto.SearchWithPage) (int64, []dto.OllamaModel
|
|||||||
if err := copier.Copy(&item, &itemModel); err != nil {
|
if err := copier.Copy(&item, &itemModel); err != nil {
|
||||||
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
}
|
}
|
||||||
|
if syncErr != nil && item.Status != constant.StatusWaiting {
|
||||||
|
item.Status = constant.StatusUnknown
|
||||||
|
item.Message = syncErr.Error()
|
||||||
|
item.Size = ""
|
||||||
|
}
|
||||||
taskModel, _ := taskRepo.GetFirst(taskRepo.WithResourceID(item.ID), repo.WithByType(task.TaskScopeAI))
|
taskModel, _ := taskRepo.GetFirst(taskRepo.WithResourceID(item.ID), repo.WithByType(task.TaskScopeAI))
|
||||||
if len(taskModel.ID) != 0 {
|
if len(taskModel.ID) != 0 {
|
||||||
item.LogFileExist = true
|
item.LogFileExist = true
|
||||||
@@ -85,6 +94,8 @@ func (u *AIToolService) LoadDetail(name string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
||||||
|
ollamaModelMu.Lock()
|
||||||
|
defer ollamaModelMu.Unlock()
|
||||||
if cmd.CheckIllegal(req.Name) {
|
if cmd.CheckIllegal(req.Name) {
|
||||||
return buserr.New("ErrCmdIllegal")
|
return buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
@@ -125,6 +136,9 @@ func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
|||||||
if err := taskItem.Execute(); err != nil {
|
if err := taskItem.Execute(); err != nil {
|
||||||
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||||
}
|
}
|
||||||
|
if err := SyncOllamaModelAccount(false); err != nil {
|
||||||
|
global.LOG.Errorf("sync Ollama model account after pull: %v", err)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -144,6 +158,8 @@ func (u *AIToolService) Close(name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
||||||
|
ollamaModelMu.Lock()
|
||||||
|
defer ollamaModelMu.Unlock()
|
||||||
if cmd.CheckIllegal(req.Name) {
|
if cmd.CheckIllegal(req.Name) {
|
||||||
return buserr.New("ErrCmdIllegal")
|
return buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
@@ -179,11 +195,16 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
|||||||
if err := taskItem.Execute(); err != nil {
|
if err := taskItem.Execute(); err != nil {
|
||||||
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||||
}
|
}
|
||||||
|
if err := SyncOllamaModelAccount(false); err != nil {
|
||||||
|
global.LOG.Errorf("sync Ollama model account after pull: %v", err)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) Delete(req dto.ForceDelete) error {
|
func (u *AIToolService) Delete(req dto.ForceDelete) error {
|
||||||
|
ollamaModelMu.Lock()
|
||||||
|
defer ollamaModelMu.Unlock()
|
||||||
ollamaList, _ := aiRepo.List(repo.WithByIDs(req.IDs))
|
ollamaList, _ := aiRepo.List(repo.WithByIDs(req.IDs))
|
||||||
if len(ollamaList) == 0 {
|
if len(ollamaList) == 0 {
|
||||||
return buserr.New("ErrRecordNotFound")
|
return buserr.New("ErrRecordNotFound")
|
||||||
@@ -202,55 +223,28 @@ func (u *AIToolService) Delete(req dto.ForceDelete) error {
|
|||||||
logItem := path.Join(global.Dir.DataDir, "log", "AITools", item.Name)
|
logItem := path.Join(global.Dir.DataDir, "log", "AITools", item.Name)
|
||||||
_ = os.Remove(logItem)
|
_ = os.Remove(logItem)
|
||||||
}
|
}
|
||||||
return nil
|
return SyncOllamaModelAccount(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) Sync() ([]dto.OllamaModelDropList, error) {
|
func (u *AIToolService) SyncAccount(req dto.OllamaModelAccountSync) error {
|
||||||
containerName, err := LoadContainerName()
|
install, err := appInstallRepo.GetFirst(repo.WithByID(req.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
stdout, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "ollama", "list")
|
if install.App.Key != "ollama" {
|
||||||
|
return buserr.New("ErrInvalidParams")
|
||||||
|
}
|
||||||
|
baseURL, err := ResolveLocalModelAccountBaseURL(req.ModelAccountBaseURLType, req.ModelAccountBaseURL, install.ContainerName, "11434", install.HttpPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
var list []model.OllamaModel
|
if err := syncOllamaModelAccount(install, baseURL, true, true); err != nil {
|
||||||
lines := strings.Split(stdout, "\n")
|
return err
|
||||||
for _, line := range lines {
|
|
||||||
parts := strings.Fields(line)
|
|
||||||
if len(parts) < 5 {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if parts[0] == "NAME" {
|
return appInstallRepo.BatchUpdateBy(map[string]interface{}{
|
||||||
continue
|
"model_account_base_url_type": req.ModelAccountBaseURLType,
|
||||||
}
|
"model_account_base_url": baseURL,
|
||||||
list = append(list, model.OllamaModel{Name: parts[0], Size: parts[2] + " " + parts[3]})
|
}, repo.WithByID(install.ID))
|
||||||
}
|
|
||||||
listInDB, _ := aiRepo.List()
|
|
||||||
var dropList []dto.OllamaModelDropList
|
|
||||||
for _, itemModel := range listInDB {
|
|
||||||
isExit := false
|
|
||||||
for i := 0; i < len(list); i++ {
|
|
||||||
if list[i].Name == itemModel.Name {
|
|
||||||
_ = aiRepo.Update(itemModel.ID, map[string]interface{}{"status": constant.StatusSuccess, "message": "", "size": list[i].Size})
|
|
||||||
list = append(list[:i], list[(i+1):]...)
|
|
||||||
isExit = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !isExit && itemModel.Status != constant.StatusWaiting {
|
|
||||||
_ = aiRepo.Update(itemModel.ID, map[string]interface{}{"status": constant.StatusDeleted, "message": "not exist", "size": ""})
|
|
||||||
dropList = append(dropList, dto.OllamaModelDropList{ID: itemModel.ID, Name: itemModel.Name})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, item := range list {
|
|
||||||
item.Status = constant.StatusSuccess
|
|
||||||
item.From = "remote"
|
|
||||||
_ = aiRepo.Create(&item)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dropList, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *AIToolService) BindDomain(req dto.OllamaBindDomain) error {
|
func (u *AIToolService) BindDomain(req dto.OllamaBindDomain) error {
|
||||||
@@ -370,6 +364,72 @@ func (u *AIToolService) UpdateBindDomain(req dto.OllamaBindDomain) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func syncOllamaModels() error {
|
||||||
|
ollamaModelMu.Lock()
|
||||||
|
defer ollamaModelMu.Unlock()
|
||||||
|
containerName, err := LoadContainerName()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stdout, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "ollama", "list")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
models := make(map[string]string)
|
||||||
|
lines := strings.Split(strings.TrimSpace(stdout), "\n")
|
||||||
|
if len(lines) == 0 || !strings.HasPrefix(strings.TrimSpace(lines[0]), "NAME") {
|
||||||
|
return fmt.Errorf("invalid ollama list output: %s", stdout)
|
||||||
|
}
|
||||||
|
for _, line := range lines[1:] {
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) < 5 {
|
||||||
|
return fmt.Errorf("invalid ollama model entry: %s", line)
|
||||||
|
}
|
||||||
|
models[parts[0]] = parts[2] + " " + parts[3]
|
||||||
|
}
|
||||||
|
list, err := aiRepo.List()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
known := make(map[string]bool, len(list))
|
||||||
|
for _, item := range list {
|
||||||
|
name := item.Name
|
||||||
|
if !strings.Contains(path.Base(name), ":") {
|
||||||
|
name += ":latest"
|
||||||
|
}
|
||||||
|
size, exists := models[name]
|
||||||
|
known[name] = true
|
||||||
|
if item.Status == constant.StatusWaiting {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
status, message := constant.StatusSuccess, ""
|
||||||
|
if !exists {
|
||||||
|
if item.Status == constant.StatusFailed || item.Status == constant.StatusCanceled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
status, message = constant.StatusDeleted, "not exist"
|
||||||
|
}
|
||||||
|
if item.Status != status || item.Size != size || item.Message != message {
|
||||||
|
if err := aiRepo.Update(item.ID, map[string]interface{}{"status": status, "size": size, "message": message}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, size := range models {
|
||||||
|
if known[name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item := model.OllamaModel{Name: name, Size: size, From: "remote", Status: constant.StatusSuccess}
|
||||||
|
if err := aiRepo.Create(&item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func LoadContainerName() (string, error) {
|
func LoadContainerName() (string, error) {
|
||||||
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
|
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -387,12 +447,12 @@ func loadModelSize(name string, containerName string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
lines := strings.Split(stdout, "\n")
|
lines := strings.Split(stdout, "\n")
|
||||||
for _, line := range lines {
|
if !strings.Contains(path.Base(name), ":") {
|
||||||
if !strings.Contains(line, name) {
|
name += ":latest"
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
for _, line := range lines {
|
||||||
parts := strings.Fields(line)
|
parts := strings.Fields(line)
|
||||||
if len(parts) < 5 {
|
if len(parts) < 5 || parts[0] != name {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return parts[2] + " " + parts[3], nil
|
return parts[2] + " " + parts[3], nil
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
|
"github.com/mattn/go-shellwords"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AIBenchmarkSources(kind, selected string) ([]dto.AIBenchmarkSource, error) {
|
||||||
|
if kind != "vllm" && kind != "ollama" {
|
||||||
|
return nil, errors.New("unsupported benchmark source")
|
||||||
|
}
|
||||||
|
selectedID := strings.SplitN(selected, ":", 2)[0]
|
||||||
|
type instance struct {
|
||||||
|
id uint
|
||||||
|
name, container string
|
||||||
|
port int
|
||||||
|
}
|
||||||
|
instances := []instance{}
|
||||||
|
apps, err := appRepo.GetBy(appRepo.WithKey(kind))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, app := range apps {
|
||||||
|
installs, err := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppId(app.ID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, install := range installs {
|
||||||
|
if selected != "" && strconv.FormatUint(uint64(install.ID), 10) != selectedID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
instances = append(instances, instance{id: install.ID, name: install.Name, container: install.ContainerName, port: install.HttpPort})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results := []dto.AIBenchmarkSource{}
|
||||||
|
if len(instances) == 0 {
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
if !strings.HasPrefix(client.DaemonHost(), "unix://") {
|
||||||
|
return nil, errors.New("benchmark shortcuts require a Docker daemon on the main node; enter the remote service address manually")
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
for _, instance := range instances {
|
||||||
|
base := dto.AIBenchmarkSource{Kind: kind, Name: instance.name, Endpoint: "/v1/chat/completions", Models: []string{}, Status: "unknown"}
|
||||||
|
inspected, err := client.ContainerInspect(ctx, instance.container)
|
||||||
|
if err != nil {
|
||||||
|
base.ID = strconv.FormatUint(uint64(instance.id), 10)
|
||||||
|
base.Message = "container inspection unavailable"
|
||||||
|
results = append(results, base)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inspected.State != nil {
|
||||||
|
base.Status = inspected.State.Status
|
||||||
|
}
|
||||||
|
args := []string{}
|
||||||
|
if inspected.Config != nil {
|
||||||
|
args = append(args, inspected.Config.Entrypoint...)
|
||||||
|
args = append(args, inspected.Config.Cmd...)
|
||||||
|
}
|
||||||
|
if len(args) >= 3 && (args[0] == "sh" || args[0] == "bash" || strings.HasSuffix(args[0], "/sh") || strings.HasSuffix(args[0], "/bash")) && args[1] == "-c" {
|
||||||
|
parser := shellwords.NewParser()
|
||||||
|
parser.ParseEnv = false
|
||||||
|
parser.ParseBacktick = false
|
||||||
|
if parsed, err := parser.Parse(args[2]); err == nil {
|
||||||
|
args = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flags := map[string]string{}
|
||||||
|
for i, arg := range args {
|
||||||
|
if key, value, ok := strings.Cut(arg, "="); ok && strings.HasPrefix(key, "--") {
|
||||||
|
flags[key] = value
|
||||||
|
} else if strings.HasPrefix(arg, "--") && i+1 < len(args) && !strings.HasPrefix(args[i+1], "--") {
|
||||||
|
flags[arg] = args[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name := flags["--served-model-name"]; name != "" {
|
||||||
|
base.Models = []string{name}
|
||||||
|
}
|
||||||
|
tokenizer := flags["--tokenizer"]
|
||||||
|
if tokenizer == "" {
|
||||||
|
tokenizer = flags["--model"]
|
||||||
|
for i, arg := range args {
|
||||||
|
if tokenizer == "" && arg == "serve" && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
||||||
|
tokenizer = args[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if kind != "ollama" && tokenizer != "" {
|
||||||
|
if !filepath.IsAbs(tokenizer) {
|
||||||
|
base.Tokenizer = tokenizer
|
||||||
|
} else {
|
||||||
|
longest := 0
|
||||||
|
for _, mount := range inspected.Mounts {
|
||||||
|
relative, err := filepath.Rel(mount.Destination, tokenizer)
|
||||||
|
if err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) && len(mount.Destination) > longest {
|
||||||
|
candidate := filepath.Join(mount.Source, relative)
|
||||||
|
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
|
||||||
|
base.Tokenizer = candidate
|
||||||
|
longest = len(mount.Destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ports := map[int]string{}
|
||||||
|
if instance.port > 0 {
|
||||||
|
ports[instance.port] = "127.0.0.1"
|
||||||
|
}
|
||||||
|
if inspected.NetworkSettings != nil {
|
||||||
|
for _, bindings := range inspected.NetworkSettings.Ports {
|
||||||
|
for _, binding := range bindings {
|
||||||
|
port, _ := strconv.Atoi(binding.HostPort)
|
||||||
|
if port > 0 {
|
||||||
|
host := binding.HostIP
|
||||||
|
if host == "" || host == "0.0.0.0" || host == "::" {
|
||||||
|
host = "127.0.0.1"
|
||||||
|
}
|
||||||
|
ports[port] = host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(ports) == 0 && inspected.HostConfig != nil && inspected.HostConfig.NetworkMode == "host" {
|
||||||
|
port, _ := strconv.Atoi(flags["--port"])
|
||||||
|
if port > 0 {
|
||||||
|
ports[port] = "127.0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ordered := []int{}
|
||||||
|
for port := range ports {
|
||||||
|
ordered = append(ordered, port)
|
||||||
|
}
|
||||||
|
sort.Ints(ordered)
|
||||||
|
if len(ordered) == 0 {
|
||||||
|
ordered = append(ordered, 0)
|
||||||
|
}
|
||||||
|
for _, port := range ordered {
|
||||||
|
source := base
|
||||||
|
source.ID = fmt.Sprintf("%d:%d", instance.id, port)
|
||||||
|
if selected != "" && source.ID != selected {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if port > 0 {
|
||||||
|
source.BaseURL = "http://" + net.JoinHostPort(ports[port], strconv.Itoa(port))
|
||||||
|
}
|
||||||
|
if selected == source.ID && source.BaseURL != "" && source.Status == "running" {
|
||||||
|
endpoint := "/v1/models"
|
||||||
|
if kind == "ollama" {
|
||||||
|
endpoint = "/api/tags"
|
||||||
|
}
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, source.BaseURL+endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
response, err := (&http.Client{Timeout: 3 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}).Do(request)
|
||||||
|
if err != nil {
|
||||||
|
source.Message = "model list unavailable; enter the served model name"
|
||||||
|
} else {
|
||||||
|
raw, readErr := io.ReadAll(io.LimitReader(response.Body, 2*1024*1024))
|
||||||
|
response.Body.Close()
|
||||||
|
if response.StatusCode == http.StatusOK && readErr == nil {
|
||||||
|
var data struct {
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"data"`
|
||||||
|
Models []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"models"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &data); err == nil {
|
||||||
|
source.Models = []string{}
|
||||||
|
for _, model := range data.Data {
|
||||||
|
source.Models = append(source.Models, model.ID)
|
||||||
|
}
|
||||||
|
for _, model := range data.Models {
|
||||||
|
source.Models = append(source.Models, model.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
source.Message = fmt.Sprintf("model list HTTP %d; provide model name and credentials if required", response.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results = append(results, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareAIGatewayAppRecover(install *model.AppInstall, recoverFile, secret string) (string, func(), error) {
|
||||||
|
noop := func() {}
|
||||||
|
if !isAIGatewayApp(install.App.Key) {
|
||||||
|
return "", noop, nil
|
||||||
|
}
|
||||||
|
tmpDir, err := os.MkdirTemp(global.Dir.TmpDir, "ai-gateway-recover-")
|
||||||
|
if err != nil {
|
||||||
|
return "", noop, err
|
||||||
|
}
|
||||||
|
cleanup := func() { _ = os.RemoveAll(tmpDir) }
|
||||||
|
fail := func(err error) (string, func(), error) {
|
||||||
|
cleanup()
|
||||||
|
return "", noop, err
|
||||||
|
}
|
||||||
|
if err := files.NewFileOp().TarGzExtractPro(recoverFile, tmpDir, secret); err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
backupDir := filepath.Join(tmpDir, strings.ReplaceAll(filepath.Base(recoverFile), ".tar.gz", ""))
|
||||||
|
for _, name := range []string{"app.json", "app.tar.gz"} {
|
||||||
|
info, err := os.Lstat(filepath.Join(backupDir, name))
|
||||||
|
if err != nil || !info.Mode().IsRegular() {
|
||||||
|
return fail(buserr.New("AppBackupFileIncomplete"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metadata, err := os.ReadFile(filepath.Join(backupDir, "app.json"))
|
||||||
|
if err != nil {
|
||||||
|
return fail(err)
|
||||||
|
}
|
||||||
|
var backup model.AppInstall
|
||||||
|
if err := json.Unmarshal(metadata, &backup); err != nil {
|
||||||
|
return fail(fmt.Errorf("unmarshal app.json failed: %w", err))
|
||||||
|
}
|
||||||
|
if backup.App.Key != install.App.Key || backup.Name != install.Name {
|
||||||
|
return fail(buserr.New("AppAttributesNotMatch"))
|
||||||
|
}
|
||||||
|
// Historical backups already include Version. Unsuffixed releases are
|
||||||
|
// SQLite; no new metadata field is required. Missing versions are invalid,
|
||||||
|
// not evidence that an archive contains SQLite data.
|
||||||
|
if strings.TrimSpace(backup.Version) == "" || strings.TrimSpace(install.Version) == "" {
|
||||||
|
return fail(buserr.New("AppBackupFileIncomplete"))
|
||||||
|
}
|
||||||
|
// Deliberately do not compare version order: restoring an older version of
|
||||||
|
// the same database type is required for failed-upgrade rollbacks.
|
||||||
|
if !isAIGatewayUpgradeVersionAllowed(install.App.Key, install.Version, backup.Version) {
|
||||||
|
return fail(buserr.New("ErrAIGatewayRecoverDatabaseTypeMismatch"))
|
||||||
|
}
|
||||||
|
return backupDir, cleanup, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const aiGatewayAppKey = "ai-gateway"
|
||||||
|
|
||||||
|
func isAIGatewayApp(appKey string) bool {
|
||||||
|
return appKey == aiGatewayAppKey || appKey == "local"+aiGatewayAppKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAIGatewayPostgresVersion(version string) bool {
|
||||||
|
return strings.HasSuffix(version, "-pg") || strings.HasSuffix(version, "-postgres")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database variants are deployment choices, not interchangeable upgrades.
|
||||||
|
func isAIGatewayUpgradeVersionAllowed(appKey, currentVersion, targetVersion string) bool {
|
||||||
|
return !isAIGatewayApp(appKey) ||
|
||||||
|
isAIGatewayPostgresVersion(currentVersion) == isAIGatewayPostgresVersion(targetVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortAIGatewayVersions(versions []string) {
|
||||||
|
sort.SliceStable(versions, func(i, j int) bool {
|
||||||
|
if common.CompareVersion(versions[i], versions[j]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if common.CompareVersion(versions[j], versions[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Prefer SQLite only when the numeric versions are equal.
|
||||||
|
return !isAIGatewayPostgresVersion(versions[i]) && isAIGatewayPostgresVersion(versions[j])
|
||||||
|
})
|
||||||
|
}
|
||||||
+135
-135
@@ -4,11 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime"
|
"mime"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"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/model"
|
||||||
@@ -17,14 +14,13 @@ 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"
|
||||||
|
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||||
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
|
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
|
||||||
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
|
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/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/1Panel-dev/1Panel/agent/utils/xpack/providers"
|
||||||
"github.com/shirou/gopsutil/v4/disk"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AlertService struct{}
|
type AlertService struct{}
|
||||||
@@ -78,10 +74,10 @@ type IAlertService interface {
|
|||||||
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) (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
|
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) (string, error)
|
||||||
DeleteAlertConfig(id uint) error
|
DeleteAlertConfig(id uint) (string, error)
|
||||||
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
||||||
TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error)
|
TestCustomAlertConfig(req dto.AlertConfigTest, lang string) (dto.AlertConfigTestResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAlertService() IAlertService {
|
func NewIAlertService() IAlertService {
|
||||||
@@ -109,7 +105,38 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
|
|||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cronjobProjects := make(map[string]uint)
|
||||||
|
var cronjobIDs []uint
|
||||||
for _, item := range alerts {
|
for _, item := range alerts {
|
||||||
|
if alertUtil.GetCronJobType(item.Type) != "cronJob" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := cronjobProjects[item.Project]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id, parseErr := strconv.ParseUint(item.Project, 10, strconv.IntSize)
|
||||||
|
if parseErr != nil || id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cronjobProjects[item.Project] = uint(id)
|
||||||
|
cronjobIDs = append(cronjobIDs, uint(id))
|
||||||
|
}
|
||||||
|
cronjobsByID := make(map[uint]model.Cronjob)
|
||||||
|
if len(cronjobIDs) > 0 {
|
||||||
|
cronjobs, err := cronjobRepo.List(repo.WithByIDs(cronjobIDs))
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
for _, cronjob := range cronjobs {
|
||||||
|
cronjobsByID[cronjob.ID] = cronjob
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range alerts {
|
||||||
|
var taskName string
|
||||||
|
if cronjob, exists := cronjobsByID[cronjobProjects[item.Project]]; exists && cronjob.Type == item.Type {
|
||||||
|
taskName = cronjob.Name
|
||||||
|
}
|
||||||
|
|
||||||
result = append(result, dto.AlertDTO{
|
result = append(result, dto.AlertDTO{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
@@ -119,6 +146,7 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
|
|||||||
Method: item.Method,
|
Method: item.Method,
|
||||||
Title: item.Title,
|
Title: item.Title,
|
||||||
Project: item.Project,
|
Project: item.Project,
|
||||||
|
TaskName: taskName,
|
||||||
Status: item.Status,
|
Status: item.Status,
|
||||||
SendCount: item.SendCount,
|
SendCount: item.SendCount,
|
||||||
AdvancedParams: item.AdvancedParams,
|
AdvancedParams: item.AdvancedParams,
|
||||||
@@ -190,6 +218,16 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
advanced, err := prepareCronJobAlertParams(create.Type, "", create.AdvancedParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
create.AdvancedParams = advanced
|
||||||
|
if create.Status != constant.AlertDisable {
|
||||||
|
if err := a.validateCronJobAlertChannels(create.Type, advanced, create.Method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
alertInfo.Status = constant.AlertEnable
|
alertInfo.Status = constant.AlertEnable
|
||||||
if err := copier.Copy(&alertInfo, &create); err != nil {
|
if err := copier.Copy(&alertInfo, &create); err != nil {
|
||||||
return buserr.WithErr("ErrStructTransform", err)
|
return buserr.WithErr("ErrStructTransform", err)
|
||||||
@@ -207,11 +245,24 @@ 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 {
|
||||||
|
if alertUtil.GetCronJobType(req.Type) == "cronJob" {
|
||||||
|
previous, err := alertRepo.Get(repo.WithByID(req.ID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.AdvancedParams, err = prepareCronJobAlertParams(req.Type, previous.AdvancedParams, req.AdvancedParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
methodTypes, err := a.validateAlertMethodReferences(req.Method)
|
methodTypes, err := a.validateAlertMethodReferences(req.Method)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if req.Status != constant.AlertDisable {
|
if req.Status != constant.AlertDisable {
|
||||||
|
if err := a.validateCronJobAlertChannels(req.Type, req.AdvancedParams, req.Method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -278,6 +329,9 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if status == constant.AlertEnable {
|
if status == constant.AlertEnable {
|
||||||
|
if err := a.validateCronJobAlertChannels(alertInfo.Type, alertInfo.AdvancedParams, alertInfo.Method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -299,122 +353,14 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) GetDisks() ([]dto.DiskDTO, error) {
|
func (a AlertService) GetDisks() ([]dto.DiskDTO, error) {
|
||||||
var disks []dto.DiskDTO
|
infos := loadDiskInfo(true)
|
||||||
excludes := map[string]struct{}{
|
disks := make([]dto.DiskDTO, 0, len(infos))
|
||||||
"/mnt/cdrom": {}, "/boot": {}, "/boot/efi": {}, "/dev": {}, "/dev/shm": {},
|
for _, item := range infos {
|
||||||
"/run/lock": {}, "/run": {}, "/run/shm": {}, "/run/user": {},
|
disks = append(disks, dto.DiskDTO(item))
|
||||||
}
|
}
|
||||||
stdout, err := executeDiskCommand()
|
|
||||||
if err != nil {
|
|
||||||
return disks, nil
|
return disks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := strings.Split(stdout, "\n")
|
|
||||||
var mounts []dto.AlertDiskInfo
|
|
||||||
|
|
||||||
for _, line := range lines {
|
|
||||||
fields := strings.Fields(line)
|
|
||||||
if len(fields) < 7 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mountPoint := strings.Join(fields[6:], " ")
|
|
||||||
if shouldExclude(fields, mountPoint, excludes) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mounts = append(mounts, dto.AlertDiskInfo{Type: fields[1], Device: fields[0], Mount: mountPoint})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
wg sync.WaitGroup
|
|
||||||
mu sync.Mutex
|
|
||||||
)
|
|
||||||
wg.Add(len(mounts))
|
|
||||||
for i := 0; i < len(mounts); i++ {
|
|
||||||
go func(timeoutCh <-chan time.Time, mount dto.AlertDiskInfo) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
var itemData dto.DiskDTO
|
|
||||||
itemData.Path = mount.Mount
|
|
||||||
itemData.Type = mount.Type
|
|
||||||
itemData.Device = mount.Device
|
|
||||||
select {
|
|
||||||
case <-timeoutCh:
|
|
||||||
mu.Lock()
|
|
||||||
disks = append(disks, itemData)
|
|
||||||
mu.Unlock()
|
|
||||||
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
|
|
||||||
default:
|
|
||||||
state, err := disk.Usage(mount.Mount)
|
|
||||||
if err != nil {
|
|
||||||
mu.Lock()
|
|
||||||
disks = append(disks, itemData)
|
|
||||||
mu.Unlock()
|
|
||||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
itemData.Total = state.Total
|
|
||||||
itemData.Free = state.Free
|
|
||||||
itemData.Used = state.Used
|
|
||||||
itemData.UsedPercent = state.UsedPercent
|
|
||||||
itemData.InodesTotal = state.InodesTotal
|
|
||||||
itemData.InodesUsed = state.InodesUsed
|
|
||||||
itemData.InodesFree = state.InodesFree
|
|
||||||
itemData.InodesUsedPercent = state.InodesUsedPercent
|
|
||||||
mu.Lock()
|
|
||||||
disks = append(disks, itemData)
|
|
||||||
mu.Unlock()
|
|
||||||
}
|
|
||||||
}(time.After(5*time.Second), mounts[i])
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
sort.Slice(disks, func(i, j int) bool {
|
|
||||||
return disks[i].Path < disks[j].Path
|
|
||||||
})
|
|
||||||
return disks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func executeDiskCommand() (string, error) {
|
|
||||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
|
||||||
stdout, err := cmdMgr.RunWithStdout("df", "-hT", "-P")
|
|
||||||
if err != nil {
|
|
||||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
|
||||||
stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return stdout, err
|
|
||||||
}
|
|
||||||
var lines []string
|
|
||||||
for _, line := range strings.Split(stdout, "\n") {
|
|
||||||
if !strings.Contains(line, "/") || strings.Contains(line, "tmpfs") || strings.Contains(line, "snap/core") || strings.Contains(line, "udev") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
lines = append(lines, line)
|
|
||||||
}
|
|
||||||
if len(lines) == 0 {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
return strings.Join(lines, "\n"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldExclude(fields []string, mountPoint string, excludes map[string]struct{}) bool {
|
|
||||||
if strings.HasPrefix(mountPoint, "/snap") || len(strings.Split(mountPoint, "/")) > 10 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(fields[1]) == "tmpfs" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.Contains(fields[2], "K") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.Contains(mountPoint, "docker") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
_, excluded := excludes[mountPoint]
|
|
||||||
return excluded
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) {
|
func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) {
|
||||||
var (
|
var (
|
||||||
opts []repo.DBOption
|
opts []repo.DBOption
|
||||||
@@ -677,25 +623,26 @@ func (a AlertService) updateCustomAlertConfig(req dto.AlertConfigUpdate, operato
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error {
|
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) (string, error) {
|
||||||
if err := validateAlertConfigStatus(req.Status); err != nil {
|
if err := validateAlertConfigStatus(req.Status); err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
config, err := alertRepo.GetConfigById(req.ID)
|
config, err := alertRepo.GetConfigById(req.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
name := alertConfigLogName(config.Config, config.Title)
|
||||||
if req.Status == constant.AlertEnable {
|
if req.Status == constant.AlertEnable {
|
||||||
if err := a.validateCommunityAlertConfigType(config.Type); err != nil {
|
if err := a.validateCommunityAlertConfigType(config.Type); err != nil {
|
||||||
return err
|
return name, err
|
||||||
}
|
}
|
||||||
if config.Type == constant.Custom {
|
if config.Type == constant.Custom {
|
||||||
if _, err := alertwebhook.Resolve(config); err != nil {
|
if _, err := alertwebhook.Resolve(config); err != nil {
|
||||||
return err
|
return name, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return alertRepo.UpdateAlertConfig(map[string]interface{}{
|
return name, alertRepo.UpdateAlertConfig(map[string]interface{}{
|
||||||
"status": req.Status,
|
"status": req.Status,
|
||||||
"update_user": operator,
|
"update_user": operator,
|
||||||
}, repo.WithByID(req.ID))
|
}, repo.WithByID(req.ID))
|
||||||
@@ -867,19 +814,20 @@ func alertConfigSMSPhone(configData string) string {
|
|||||||
return strings.TrimSpace(cfg.Phone)
|
return strings.TrimSpace(cfg.Phone)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) DeleteAlertConfig(id uint) error {
|
func (a AlertService) DeleteAlertConfig(id uint) (string, error) {
|
||||||
_, err := alertRepo.GetConfigById(id)
|
config, err := alertRepo.GetConfigById(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
name := alertConfigLogName(config.Config, config.Title)
|
||||||
usedAlerts, err := alertRepo.List(alertRepo.WithByAlertMethodContainsConfigID(id))
|
usedAlerts, err := alertRepo.List(alertRepo.WithByAlertMethodContainsConfigID(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return name, err
|
||||||
}
|
}
|
||||||
if len(usedAlerts) > 0 {
|
if len(usedAlerts) > 0 {
|
||||||
return buserr.New("ErrAlertConfigInUse")
|
return name, buserr.New("ErrAlertConfigInUse")
|
||||||
}
|
}
|
||||||
return alertRepo.DeleteAlertConfig(repo.WithByID(id))
|
return name, alertRepo.DeleteAlertConfig(repo.WithByID(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
||||||
@@ -905,7 +853,7 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
|||||||
|
|
||||||
msg := email.EmailMessage{
|
msg := email.EmailMessage{
|
||||||
Subject: i18n.GetMsgByKey("TestAlertTitle"),
|
Subject: i18n.GetMsgByKey("TestAlertTitle"),
|
||||||
Body: i18n.GetMsgByKey("TestAlert"),
|
Body: i18n.GetMsgByKey("TestAlertMessage"),
|
||||||
IsHTML: false,
|
IsHTML: false,
|
||||||
}
|
}
|
||||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||||
@@ -953,7 +901,7 @@ func resolveEmailTestConfig(req dto.AlertConfigTest) (dto.AlertEmailConfig, erro
|
|||||||
return emailConfig, nil
|
return emailConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error) {
|
func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest, lang string) (dto.AlertConfigTestResult, error) {
|
||||||
if req.Type != constant.Custom {
|
if req.Type != constant.Custom {
|
||||||
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config test type must be custom")
|
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config test type must be custom")
|
||||||
}
|
}
|
||||||
@@ -982,12 +930,15 @@ func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertC
|
|||||||
}
|
}
|
||||||
tester, ok := xpack.AlertProvider.(providers.CustomWebhookTester)
|
tester, ok := xpack.AlertProvider.(providers.CustomWebhookTester)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
if global.LOG != nil {
|
||||||
|
global.LOG.Warnf("webhook test failed: %v", providers.ErrCustomWebhookUnsupported)
|
||||||
|
}
|
||||||
return dto.AlertConfigTestResult{
|
return dto.AlertConfigTestResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Message: providers.ErrCustomWebhookUnsupported.Error(),
|
Message: providers.ErrCustomWebhookUnsupported.Error(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return tester.TestCustomWebhook(resolved)
|
return tester.TestCustomWebhook(resolved, lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
|
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
|
||||||
@@ -1021,6 +972,23 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
|||||||
alertRepo.WithByType(updateAlert.Type),
|
alertRepo.WithByType(updateAlert.Type),
|
||||||
alertRepo.WithByProject(updateAlert.Project),
|
alertRepo.WithByProject(updateAlert.Project),
|
||||||
)
|
)
|
||||||
|
advanced, err := prepareCronJobAlertParams(updateAlert.Type, alertInfo.AdvancedParams, updateAlert.AdvancedParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updateAlert.AdvancedParams = advanced
|
||||||
|
if alertUtil.GetCronJobType(updateAlert.Type) == "cronJob" {
|
||||||
|
upMap["advanced_params"] = advanced
|
||||||
|
}
|
||||||
|
if newStatus == constant.AlertEnable {
|
||||||
|
method := updateAlert.Method
|
||||||
|
if method == "" {
|
||||||
|
method = alertInfo.Method
|
||||||
|
}
|
||||||
|
if err := a.validateCronJobAlertChannels(updateAlert.Type, advanced, method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if alertInfo.ID > 0 {
|
if alertInfo.ID > 0 {
|
||||||
shouldUpdate := false
|
shouldUpdate := false
|
||||||
@@ -1034,6 +1002,9 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
|||||||
if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method {
|
if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method {
|
||||||
shouldUpdate = true
|
shouldUpdate = true
|
||||||
}
|
}
|
||||||
|
if val, ok := upMap["advanced_params"]; ok && val != alertInfo.AdvancedParams {
|
||||||
|
shouldUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
if shouldUpdate {
|
if shouldUpdate {
|
||||||
if err := alertRepo.Update(
|
if err := alertRepo.Update(
|
||||||
@@ -1055,3 +1026,32 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prepareCronJobAlertParams(alertType, previous, incoming string) (string, error) {
|
||||||
|
if alertUtil.GetCronJobType(alertType) != "cronJob" {
|
||||||
|
return incoming, nil
|
||||||
|
}
|
||||||
|
return alertUtil.MergeCronJobAlertParams(previous, incoming)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AlertService) validateCronJobAlertChannels(alertType, advanced, method string) error {
|
||||||
|
if alertUtil.GetCronJobType(alertType) != "cronJob" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mode, err := alertUtil.CronJobAlertTriggerMode(advanced)
|
||||||
|
if err != nil || mode != alertUtil.CronJobAlertSuccess {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = a.validateAlertMethodReferences(method)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func alertConfigLogName(config, title string) string {
|
||||||
|
var value struct {
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal([]byte(config), &value) == nil && strings.TrimSpace(value.DisplayName) != "" {
|
||||||
|
return strings.TrimSpace(value.DisplayName)
|
||||||
|
}
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const (
|
|||||||
CheckIntervalSec = 3
|
CheckIntervalSec = 3
|
||||||
LoadCheckIntervalMin = 5
|
LoadCheckIntervalMin = 5
|
||||||
sshIPLoginWindow = 30 * time.Minute
|
sshIPLoginWindow = 30 * time.Minute
|
||||||
|
sslAutoRenewAlertSkipDays = 31
|
||||||
)
|
)
|
||||||
|
|
||||||
type AlertTaskHelper struct {
|
type AlertTaskHelper struct {
|
||||||
@@ -529,11 +530,15 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
global.LOG.Errorf("Failed to load ssh login logs: %v", err)
|
global.LOG.Errorf("Failed to load ssh login logs: %v", err)
|
||||||
}
|
}
|
||||||
|
interfaceAddrs, err := net.InterfaceAddrs()
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Warnf("Failed to load local IP addresses for ssh login alert: %v", err)
|
||||||
|
}
|
||||||
count, records := summarizeSSHLoginHistories(
|
count, records := summarizeSSHLoginHistories(
|
||||||
histories,
|
histories,
|
||||||
now,
|
now,
|
||||||
failedWindow,
|
failedWindow,
|
||||||
strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n"),
|
sshSuccessLoginWhitelist(alert.AdvancedParams, interfaceAddrs),
|
||||||
)
|
)
|
||||||
isAlert := count >= int(alert.Count)
|
isAlert := count >= int(alert.Count)
|
||||||
if isAlert {
|
if isAlert {
|
||||||
@@ -569,6 +574,19 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sshSuccessLoginWhitelist(configured string, interfaceAddrs []net.Addr) []string {
|
||||||
|
whitelist := strings.Split(strings.TrimSpace(configured), "\n")
|
||||||
|
whitelist = append(whitelist, "127.0.0.0/8", "::1")
|
||||||
|
for _, addr := range interfaceAddrs {
|
||||||
|
ipNet, ok := addr.(*net.IPNet)
|
||||||
|
if !ok || ipNet.IP == nil || ipNet.IP.IsUnspecified() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
whitelist = append(whitelist, ipNet.IP.String())
|
||||||
|
}
|
||||||
|
return whitelist
|
||||||
|
}
|
||||||
|
|
||||||
func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) []model.LoginLog {
|
func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) []model.LoginLog {
|
||||||
filtered := make([]model.LoginLog, 0, len(records))
|
filtered := make([]model.LoginLog, 0, len(records))
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
@@ -864,7 +882,7 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
|
|||||||
daysDiff := int(math.Ceil(
|
daysDiff := int(math.Ceil(
|
||||||
ssl.ExpireDate.Sub(currentDate).Hours() / 24,
|
ssl.ExpireDate.Sub(currentDate).Hours() / 24,
|
||||||
))
|
))
|
||||||
if daysDiff > 0 && int(cycle) >= daysDiff {
|
if daysDiff > 0 && int(cycle) >= daysDiff && !shouldSuppressSSLExpiryAlert(ssl, daysDiff) {
|
||||||
daysDiffMap[daysDiff] = append(daysDiffMap[daysDiff], ssl.PrimaryDomain)
|
daysDiffMap[daysDiff] = append(daysDiffMap[daysDiff], ssl.PrimaryDomain)
|
||||||
projectMap[ssl.ID] = append(projectMap[ssl.ID], ssl.ExpireDate)
|
projectMap[ssl.ID] = append(projectMap[ssl.ID], ssl.ExpireDate)
|
||||||
}
|
}
|
||||||
@@ -872,6 +890,10 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
|
|||||||
return daysDiffMap, projectMap
|
return daysDiffMap, projectMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldSuppressSSLExpiryAlert(ssl model.WebsiteSSL, remainingDays int) bool {
|
||||||
|
return ssl.AutoRenew && remainingDays < sslAutoRenewAlertSkipDays
|
||||||
|
}
|
||||||
|
|
||||||
func calculateWebsiteExpiryDays(websites []model.Website, cycle uint) (map[int][]string, map[uint][]time.Time) {
|
func calculateWebsiteExpiryDays(websites []model.Website, cycle uint) (map[int][]string, map[uint][]time.Time) {
|
||||||
currentDate := time.Now()
|
currentDate := time.Now()
|
||||||
daysDiffMap := make(map[int][]string)
|
daysDiffMap := make(map[int][]string)
|
||||||
@@ -1044,50 +1066,37 @@ func processAllDisks(alert dto.AlertDTO) error {
|
|||||||
global.LOG.Errorf("error getting disk list, err: %v", err)
|
global.LOG.Errorf("error getting disk list, err: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var errMsgs []string
|
|
||||||
for _, item := range diskList {
|
for _, item := range diskList {
|
||||||
err := checkAndCreateDiskAlert(alert, item.Path)
|
if item.Total == 0 {
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Sprintf("disk path %s process failed: %v", item.Path, err)
|
|
||||||
errMsgs = append(errMsgs, errMsg)
|
|
||||||
global.LOG.Errorf("%s", errMsg)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
checkAndCreateDiskAlert(alert, item.Path, &disk.UsageStat{Used: item.Used, UsedPercent: item.UsedPercent})
|
||||||
if len(errMsgs) > 0 {
|
|
||||||
return fmt.Errorf("batch process disks failed, error count: %d, details: %s", len(errMsgs), strings.Join(errMsgs, "; "))
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func processSingleDisk(alert dto.AlertDTO) error {
|
func processSingleDisk(alert dto.AlertDTO) error {
|
||||||
err := checkAndCreateDiskAlert(alert, alert.Project)
|
usageStat, err := loadDiskUsageWithTimeout(alert.Project, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.LOG.Errorf("%s", err.Error())
|
global.LOG.Errorf("error getting disk usage for %s, err: %v", alert.Project, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
checkAndCreateDiskAlert(alert, alert.Project, usageStat)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string) error {
|
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string, usageStat *disk.UsageStat) {
|
||||||
usageStat, err := psutil.DISK.GetUsage(path, false)
|
|
||||||
if err != nil {
|
|
||||||
global.LOG.Errorf("error getting disk usage for %s, err: %v", path, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
usedTotal, usedStr := calculateUsedTotal(alert.Cycle, usageStat)
|
usedTotal, usedStr := calculateUsedTotal(alert.Cycle, usageStat)
|
||||||
commonTotal := float64(alert.Count)
|
commonTotal := float64(alert.Count)
|
||||||
if alert.Cycle == 1 {
|
if alert.Cycle == 1 {
|
||||||
commonTotal *= 1024 * 1024 * 1024
|
commonTotal *= 1024 * 1024 * 1024
|
||||||
}
|
}
|
||||||
if usedTotal < commonTotal {
|
if usedTotal < commonTotal {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
params := createAlertDiskParams(path, usedStr)
|
params := createAlertDiskParams(path, usedStr)
|
||||||
sender := NewAlertSender(alert, alert.Project)
|
sender := NewAlertSender(alert, alert.Project)
|
||||||
sender.ResourceSend(path, params)
|
sender.ResourceSend(path, params)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateUsedTotal(cycle uint, usageStat *disk.UsageStat) (float64, string) {
|
func calculateUsedTotal(cycle uint, usageStat *disk.UsageStat) (float64, string) {
|
||||||
|
|||||||
@@ -258,13 +258,22 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
|||||||
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 {
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
if err == nil && app.Resource == constant.AppResourceRemote {
|
||||||
|
detail.Update = false
|
||||||
|
appDetailDTO.Update = false
|
||||||
}
|
}
|
||||||
switch app.Type {
|
switch app.Type {
|
||||||
case constant.RuntimePHP:
|
case constant.RuntimePHP:
|
||||||
|
paramMap := make(map[string]interface{})
|
||||||
|
if app.Resource == constant.AppResourceRemote {
|
||||||
|
if err = json.Unmarshal([]byte(detail.Params), ¶mMap); err != nil {
|
||||||
|
return appDetailDTO, err
|
||||||
|
}
|
||||||
|
appDetailDTO.Params = paramMap
|
||||||
|
} else {
|
||||||
paramsPath := filepath.Join(versionPath, "data.yml")
|
paramsPath := filepath.Join(versionPath, "data.yml")
|
||||||
if !fileOp.Stat(paramsPath) {
|
if !fileOp.Stat(paramsPath) {
|
||||||
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
|
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
|
||||||
@@ -273,11 +282,11 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return appDetailDTO, err
|
return appDetailDTO, err
|
||||||
}
|
}
|
||||||
paramMap := make(map[string]interface{})
|
|
||||||
if err = yaml.Unmarshal(param, ¶mMap); err != nil {
|
if err = yaml.Unmarshal(param, ¶mMap); err != nil {
|
||||||
return appDetailDTO, err
|
return appDetailDTO, err
|
||||||
}
|
}
|
||||||
appDetailDTO.Params = paramMap["additionalProperties"]
|
appDetailDTO.Params = paramMap["additionalProperties"]
|
||||||
|
}
|
||||||
composePath := filepath.Join(versionPath, "docker-compose.yml")
|
composePath := filepath.Join(versionPath, "docker-compose.yml")
|
||||||
if !fileOp.Stat(composePath) {
|
if !fileOp.Stat(composePath) {
|
||||||
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", composePath, nil)
|
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", composePath, nil)
|
||||||
@@ -308,6 +317,12 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
|||||||
if appDetailDTO.DockerCompose == "" {
|
if appDetailDTO.DockerCompose == "" {
|
||||||
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")
|
||||||
|
if app.Resource == constant.AppResourceRemote {
|
||||||
|
dockerComposeUrl, err = currentAppDownloadURL(dockerComposeUrl)
|
||||||
|
if err != nil {
|
||||||
|
return appDetailDTO, err
|
||||||
|
}
|
||||||
|
}
|
||||||
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||||
if statusCode == http.StatusNotFound {
|
if statusCode == http.StatusNotFound {
|
||||||
return appDetailDTO, buserr.New("ErrAppVersionUnavailable")
|
return appDetailDTO, buserr.New("ErrAppVersionUnavailable")
|
||||||
@@ -434,6 +449,12 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
} else {
|
} else {
|
||||||
if appDetail.DockerCompose == "" {
|
if appDetail.DockerCompose == "" {
|
||||||
dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version)
|
dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version)
|
||||||
|
if app.Resource == constant.AppResourceRemote {
|
||||||
|
dockerComposeUrl, err = currentAppDownloadURL(dockerComposeUrl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
var statusCode int
|
var statusCode int
|
||||||
statusCode, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
statusCode, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||||
if statusCode == http.StatusNotFound {
|
if statusCode == http.StatusNotFound {
|
||||||
@@ -478,6 +499,22 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
req.Params[constant.ContainerName] = containerName
|
req.Params[constant.ContainerName] = containerName
|
||||||
appInstall.ContainerName = containerName
|
appInstall.ContainerName = containerName
|
||||||
|
|
||||||
|
modelAccountBaseURL := ""
|
||||||
|
if app.Key == "ollama" {
|
||||||
|
appInstall.SyncModelAccount = req.SyncModelAccount == nil || *req.SyncModelAccount
|
||||||
|
appInstall.ModelAccountBaseURLType = req.ModelAccountBaseURLType
|
||||||
|
if appInstall.ModelAccountBaseURLType == "" {
|
||||||
|
appInstall.ModelAccountBaseURLType = "container"
|
||||||
|
}
|
||||||
|
appInstall.ModelAccountBaseURL = req.ModelAccountBaseURL
|
||||||
|
}
|
||||||
|
if app.Key == "ollama" && appInstall.SyncModelAccount {
|
||||||
|
modelAccountBaseURL, err = ResolveLocalModelAccountBaseURL(req.ModelAccountBaseURLType, req.ModelAccountBaseURL, containerName, "11434", httpPort)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
index := 0
|
index := 0
|
||||||
serviceName := ""
|
serviceName := ""
|
||||||
for k := range servicesMap {
|
for k := range servicesMap {
|
||||||
@@ -550,6 +587,11 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
|
|
||||||
installTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskInstall, task.TaskScopeApp, req.TaskID, appInstall.ID)
|
installTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskInstall, task.TaskScopeApp, req.TaskID, appInstall.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
appInstall.Status = constant.StatusInstallErr
|
||||||
|
appInstall.Message = err.Error()
|
||||||
|
if saveErr := appInstallRepo.Save(context.Background(), appInstall); saveErr != nil {
|
||||||
|
err = fmt.Errorf("%w; save failed install status: %v", err, saveErr)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,7 +599,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
installApp := func(t *task.Task) error {
|
installApp := func(t *task.Task) (err error) {
|
||||||
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
|
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -583,6 +625,10 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
updateToolApp(appInstall)
|
updateToolApp(appInstall)
|
||||||
|
if app.Key == "ollama" && appInstall.SyncModelAccount {
|
||||||
|
_, err := SyncLocalModelAccount(*appInstall, 0, modelAccountBaseURL, nil, false, true)
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,14 @@ func (a *AppInstallService) CheckExist(req request.AppInstalledInfo) (*response.
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.ContainerName = appInstall.ContainerName
|
res.ContainerName = appInstall.ContainerName
|
||||||
|
res.ModelAccountBaseURLType = appInstall.ModelAccountBaseURLType
|
||||||
|
res.ModelAccountBaseURL = appInstall.ModelAccountBaseURL
|
||||||
|
if app.Key == "ollama" {
|
||||||
|
res.ModelAccountBaseURLType, res.ModelAccountBaseURL, err = loadOllamaModelAccountAddress(appInstall)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
res.Name = appInstall.Name
|
res.Name = appInstall.Name
|
||||||
res.Version = appInstall.Version
|
res.Version = appInstall.Version
|
||||||
res.CreatedAt = appInstall.CreatedAt
|
res.CreatedAt = appInstall.CreatedAt
|
||||||
@@ -339,6 +347,11 @@ func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppIns
|
|||||||
|
|
||||||
operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID)
|
operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
install.Status = constant.StatusUpErr
|
||||||
|
install.Message = err.Error()
|
||||||
|
if saveErr := appInstallRepo.Save(context.Background(), &install); saveErr != nil {
|
||||||
|
return fmt.Errorf("%w; save failed operation status: %v", err, saveErr)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
operation := string(req.Operate)
|
operation := string(req.Operate)
|
||||||
@@ -433,6 +446,32 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modelAccountBaseURL := ""
|
||||||
|
if installed.App.Key == "ollama" {
|
||||||
|
if req.SyncModelAccount != nil {
|
||||||
|
installed.SyncModelAccount = *req.SyncModelAccount
|
||||||
|
}
|
||||||
|
if req.ModelAccountBaseURLType != "" {
|
||||||
|
installed.ModelAccountBaseURLType = req.ModelAccountBaseURLType
|
||||||
|
installed.ModelAccountBaseURL = req.ModelAccountBaseURL
|
||||||
|
} else if installed.SyncModelAccount {
|
||||||
|
installed.ModelAccountBaseURLType, installed.ModelAccountBaseURL, err = loadOllamaModelAccountAddress(oldInstalled)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if installed.SyncModelAccount {
|
||||||
|
containerName := installed.ContainerName
|
||||||
|
if req.Advanced && req.ContainerName != "" {
|
||||||
|
containerName = req.ContainerName
|
||||||
|
}
|
||||||
|
modelAccountBaseURL, err = ResolveLocalModelAccountBaseURL(installed.ModelAccountBaseURLType, installed.ModelAccountBaseURL, containerName, "11434", installed.HttpPort)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
backupDockerCompose := installed.DockerCompose
|
backupDockerCompose := installed.DockerCompose
|
||||||
if req.Advanced {
|
if req.Advanced {
|
||||||
composeMap := make(map[string]interface{})
|
composeMap := make(map[string]interface{})
|
||||||
@@ -558,6 +597,9 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
|||||||
if proxyChanged && website.ID != 0 && currentProxyErr != nil {
|
if proxyChanged && website.ID != 0 && currentProxyErr != nil {
|
||||||
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", currentProxyErr).Error())
|
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", currentProxyErr).Error())
|
||||||
}
|
}
|
||||||
|
if installed.App.Key == "ollama" && installed.SyncModelAccount {
|
||||||
|
return syncOllamaModelAccount(installed, modelAccountBaseURL, true, true)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,11 +609,15 @@ func (a *AppInstallService) SyncAll(systemInit bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, i := range allList {
|
for _, i := range allList {
|
||||||
if i.Status == constant.StatusInstalling || i.Status == constant.StatusUpgrading || i.Status == constant.StatusRebuilding || i.Status == constant.StatusUninstalling {
|
if appInstallOperationInterruptedOnRestart(i.Status) {
|
||||||
if systemInit {
|
if systemInit {
|
||||||
i.Status = constant.StatusError
|
i.Status = appInstallOperationFailureStatus(i.Status)
|
||||||
i.Message = "1Panel restart causes the task to terminate"
|
i.Message = constant.InterruptedMsg
|
||||||
_ = appInstallRepo.Save(context.Background(), &i)
|
if err := appInstallRepo.Save(context.Background(), &i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err := syncAppInstallStatus(&i, false); err != nil {
|
||||||
|
global.LOG.Errorf("sync install app[%s] error,mgs: %s", i.Name, err.Error())
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -678,14 +724,17 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
|||||||
if !canAccessVllmVersion(app.Key, detail.Version) {
|
if !canAccessVllmVersion(app.Key, detail.Version) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !isAIGatewayUpgradeVersionAllowed(app.Key, install.Version, 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
|
||||||
}
|
}
|
||||||
if common.IsCrossVersion(install.Version, detail.Version) && !app.CrossVersionUpdate {
|
if isCrossAppVersion(app.Key, install.Version, detail.Version) && !app.CrossVersionUpdate {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
canUpgrade := common.CompareVersion(detail.Version, install.Version)
|
canUpgrade := isNewerAppVersion(app.Key, detail.Version, install.Version)
|
||||||
if app.Key == vllmAppKeyForUpgrade {
|
if app.Key == vllmAppKeyForUpgrade {
|
||||||
currentImage := loadVllmImageFromEnv(install.Env)
|
currentImage := loadVllmImageFromEnv(install.Env)
|
||||||
canUpgrade = isVllmUpgradeCandidate(install.Version, detail.Version, currentImage)
|
canUpgrade = isVllmUpgradeCandidate(install.Version, detail.Version, currentImage)
|
||||||
@@ -695,6 +744,12 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
|||||||
if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() {
|
if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() {
|
||||||
filename := filepath.Base(detail.DownloadUrl)
|
filename := filepath.Base(detail.DownloadUrl)
|
||||||
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml")
|
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml")
|
||||||
|
if app.Resource == constant.AppResourceRemote {
|
||||||
|
dockerComposeUrl, err = currentAppDownloadURL(dockerComposeUrl)
|
||||||
|
if err != nil {
|
||||||
|
return versions, err
|
||||||
|
}
|
||||||
|
}
|
||||||
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||||
if statusCode == http.StatusNotFound {
|
if statusCode == http.StatusNotFound {
|
||||||
return versions, buserr.New("ErrAppVersionUnavailable")
|
return versions, buserr.New("ErrAppVersionUnavailable")
|
||||||
@@ -726,7 +781,7 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort.Slice(versions, func(i, j int) bool {
|
sort.Slice(versions, func(i, j int) bool {
|
||||||
return common.CompareVersion(versions[i].Version, versions[j].Version)
|
return appVersionSortBefore(app.Key, versions[i].Version, versions[j].Version)
|
||||||
})
|
})
|
||||||
return versions, nil
|
return versions, nil
|
||||||
}
|
}
|
||||||
@@ -916,6 +971,18 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
|||||||
config := getAppCommonConfig(envs)
|
config := getAppCommonConfig(envs)
|
||||||
config.DockerCompose = install.DockerCompose
|
config.DockerCompose = install.DockerCompose
|
||||||
res.Params = params
|
res.Params = params
|
||||||
|
res.AppKey = install.App.Key
|
||||||
|
res.Name = install.Name
|
||||||
|
res.HttpPort = install.HttpPort
|
||||||
|
res.SyncModelAccount = install.SyncModelAccount
|
||||||
|
res.ModelAccountBaseURLType = install.ModelAccountBaseURLType
|
||||||
|
res.ModelAccountBaseURL = install.ModelAccountBaseURL
|
||||||
|
if install.App.Key == "ollama" {
|
||||||
|
res.ModelAccountBaseURLType, res.ModelAccountBaseURL, err = loadOllamaModelAccountAddress(install)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if config.ContainerName == "" {
|
if config.ContainerName == "" {
|
||||||
config.ContainerName = install.ContainerName
|
config.ContainerName = install.ContainerName
|
||||||
}
|
}
|
||||||
@@ -931,10 +998,34 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
||||||
|
operation := ""
|
||||||
|
switch appInstall.Status {
|
||||||
|
case constant.StatusInstalling:
|
||||||
|
operation = task.TaskInstall
|
||||||
|
case constant.StatusUpgrading:
|
||||||
|
operation = task.TaskUpgrade
|
||||||
|
case constant.StatusRebuilding:
|
||||||
|
operation = task.TaskBuild
|
||||||
|
case constant.StatusUninstalling:
|
||||||
|
operation = task.TaskUninstall
|
||||||
|
case constant.StatusStarting, constant.StatusWaiting:
|
||||||
|
operation = task.TaskUpdate
|
||||||
|
case constant.StatusRestarting:
|
||||||
|
operation = task.TaskRestart
|
||||||
|
}
|
||||||
|
if operation != "" {
|
||||||
|
if err := reconcileAppInstallTaskFailure(appInstall, operation, global.DB, global.TaskDB); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
switch appInstall.Status {
|
switch appInstall.Status {
|
||||||
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
||||||
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
|
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
|
||||||
return nil
|
return nil
|
||||||
|
case constant.StatusInstallErr, constant.StatusUpgradeErr, constant.StatusUpErr:
|
||||||
|
if !force {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cli, err := docker.NewClient()
|
cli, err := docker.NewClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareMCPWebsiteDirectory(req request.AppInstallCreate) error {
|
||||||
|
changed := errors.New("MCP website directory changed; inspect the installation before retrying")
|
||||||
|
directory, ok := req.Params["WEBSITE_DIR"].(string)
|
||||||
|
if !ok || directory == "" || global.DB == nil {
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
var setting model.Setting
|
||||||
|
err := tx.Where("key = ?", "WEBSITE_DIR").First(&setting).Error
|
||||||
|
missing := errors.Is(err, gorm.ErrRecordNotFound)
|
||||||
|
if err != nil && !missing {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
current := setting.Value
|
||||||
|
if current == "" {
|
||||||
|
current = path.Join(global.Dir.BaseDir, "1panel", "www")
|
||||||
|
}
|
||||||
|
if directory != current {
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
if missing {
|
||||||
|
if err := tx.Create(&model.Setting{Key: "WEBSITE_DIR", Value: directory}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if setting.Value == "" {
|
||||||
|
result := tx.Model(&model.Setting{}).Where("id = ? AND value = ?", setting.ID, "").Update("value", directory)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected != 1 {
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(path.Join(directory, "conf.d"), constant.DirPerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func appInstallOperationFailureStatus(status string) string {
|
||||||
|
switch status {
|
||||||
|
case constant.StatusInstalling:
|
||||||
|
return constant.StatusInstallErr
|
||||||
|
case constant.StatusUpgrading:
|
||||||
|
return constant.StatusUpgradeErr
|
||||||
|
case constant.StatusUninstalling:
|
||||||
|
return constant.StatusError
|
||||||
|
default:
|
||||||
|
return constant.StatusUpErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appInstallOperationPending(status string) bool {
|
||||||
|
switch status {
|
||||||
|
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
||||||
|
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appInstallOperationInterruptedOnRestart(status string) bool {
|
||||||
|
// ReStarting can also describe a container's Docker restart policy.
|
||||||
|
return appInstallOperationPending(status) && status != constant.StatusRestarting
|
||||||
|
}
|
||||||
|
|
||||||
|
func reconcileAppInstallTaskFailure(install *model.AppInstall, operation string, appDB, taskDB *gorm.DB) error {
|
||||||
|
if !appInstallOperationPending(install.Status) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var operationTask model.Task
|
||||||
|
err := taskDB.Where("type = ? AND resource_id = ? AND operate = ?", "App", install.ID, operation).
|
||||||
|
Order("created_at DESC").First(&operationTask).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if operationTask.Status != constant.StatusFailed && operationTask.Status != constant.StatusCanceled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// A previous operation must not turn a newly queued retry into a failure.
|
||||||
|
if operationTask.CreatedAt.Before(install.UpdatedAt) &&
|
||||||
|
(operationTask.EndAt.IsZero() || operationTask.EndAt.Before(install.UpdatedAt)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
message := operationTask.ErrorMsg
|
||||||
|
if message == "" {
|
||||||
|
message = "the application operation task failed"
|
||||||
|
}
|
||||||
|
status := appInstallOperationFailureStatus(install.Status)
|
||||||
|
result := appDB.Model(&model.AppInstall{}).
|
||||||
|
Where("id = ? AND status = ? AND updated_at = ?", install.ID, install.Status, install.UpdatedAt).
|
||||||
|
Updates(map[string]interface{}{"status": status, "message": message})
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected > 0 {
|
||||||
|
install.Status = status
|
||||||
|
install.Message = message
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func operationStatusTestDB(t *testing.T) (*gorm.DB, *gorm.DB, model.AppInstall) {
|
||||||
|
t.Helper()
|
||||||
|
open := func(name string, schema interface{}) *gorm.DB {
|
||||||
|
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), name)), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(schema); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
sqlDB, _ := db.DB()
|
||||||
|
if sqlDB != nil {
|
||||||
|
_ = sqlDB.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
appDB := open("apps.db", &model.AppInstall{})
|
||||||
|
taskDB := open("tasks.db", &model.Task{})
|
||||||
|
install := model.AppInstall{
|
||||||
|
Name: "gb10-ds-vision", AppId: 1, AppDetailId: 1,
|
||||||
|
Version: "nvidia-gb10-dspark-0.1.1", ContainerName: "vllm-dspark-1", ServiceName: "vllm-dspark",
|
||||||
|
Status: constant.StatusStarting, Env: `{"MODEL_DIR":"/models/vision"}`,
|
||||||
|
}
|
||||||
|
if err := appDB.Omit(clause.Associations).Create(&install).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return appDB, taskDB, install
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppInstallFailedTaskRecoversPendingState(t *testing.T) {
|
||||||
|
for _, state := range []string{constant.StatusStarting, constant.StatusWaiting, constant.StatusRestarting, constant.StatusInstalling} {
|
||||||
|
t.Run(state, func(t *testing.T) {
|
||||||
|
appDB, taskDB, install := operationStatusTestDB(t)
|
||||||
|
install.Status = state
|
||||||
|
if err := appDB.Omit(clause.Associations).Save(&install).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
task := model.Task{ID: "failed", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
|
||||||
|
Status: constant.StatusFailed, ErrorMsg: "model startup script failed", CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
|
||||||
|
if err := taskDB.Create(&task).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := constant.StatusUpErr
|
||||||
|
if state == constant.StatusInstalling {
|
||||||
|
want = constant.StatusInstallErr
|
||||||
|
}
|
||||||
|
var stored model.AppInstall
|
||||||
|
if err := appDB.First(&stored, install.ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if install.Status != want || stored.Status != want || stored.Message != task.ErrorMsg || stored.Env != install.Env {
|
||||||
|
t.Fatalf("recovered state = %s/%s, message %q, env %q", install.Status, stored.Status, stored.Message, stored.Env)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppInstallPendingStateKeepsActiveAndUnrelatedTasks(t *testing.T) {
|
||||||
|
for _, taskStatus := range []string{constant.StatusExecuting, constant.StatusSuccess, "missing", "previous-failure", "other-resource"} {
|
||||||
|
t.Run(taskStatus, func(t *testing.T) {
|
||||||
|
appDB, taskDB, install := operationStatusTestDB(t)
|
||||||
|
task := model.Task{ID: "task", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
|
||||||
|
Status: taskStatus, CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
|
||||||
|
if taskStatus == "previous-failure" {
|
||||||
|
task.Status = constant.StatusFailed
|
||||||
|
task.CreatedAt = install.UpdatedAt.Add(-2 * time.Hour)
|
||||||
|
task.EndAt = install.UpdatedAt.Add(-time.Hour)
|
||||||
|
}
|
||||||
|
if taskStatus == "other-resource" {
|
||||||
|
task.Status = constant.StatusFailed
|
||||||
|
task.ResourceID++
|
||||||
|
}
|
||||||
|
if taskStatus != "missing" {
|
||||||
|
if err := taskDB.Create(&task).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if install.Status != constant.StatusStarting {
|
||||||
|
t.Fatalf("unrelated or active task changed pending state to %s", install.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppInstallRecoveryDoesNotOverwriteConcurrentRetry(t *testing.T) {
|
||||||
|
appDB, taskDB, install := operationStatusTestDB(t)
|
||||||
|
task := model.Task{ID: "failed", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
|
||||||
|
Status: constant.StatusCanceled, ErrorMsg: "interrupted", CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
|
||||||
|
if err := taskDB.Create(&task).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := appDB.Callback().Update().Before("gorm:update").Register("new-operation", func(db *gorm.DB) {
|
||||||
|
if err := db.Exec("UPDATE app_installs SET updated_at = ?, message = ? WHERE id = ?", install.UpdatedAt.Add(time.Hour), "new attempt", install.ID).Error; err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var stored model.AppInstall
|
||||||
|
if err := appDB.First(&stored, install.ID).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if stored.Status != constant.StatusStarting || stored.Message != "new attempt" {
|
||||||
|
t.Fatalf("recovery overwrote a newer operation: %s, %s", stored.Status, stored.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppInstallRestartRecoveryCoversLifecycleStates(t *testing.T) {
|
||||||
|
for _, state := range []string{constant.StatusStarting, constant.StatusWaiting, constant.StatusInstalling} {
|
||||||
|
if !appInstallOperationInterruptedOnRestart(state) {
|
||||||
|
t.Errorf("restart recovery omitted %s", state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, state := range []string{constant.StatusRunning, constant.StatusStopped, constant.StatusRestarting} {
|
||||||
|
if appInstallOperationInterruptedOnRestart(state) {
|
||||||
|
t.Errorf("restart recovery changes a Docker runtime state: %s", state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -191,7 +191,11 @@ func (c *appSyncContext) processOneApp(item appWorkItem) appWorkResult {
|
|||||||
|
|
||||||
if _, ok := InitTypes[app.Type]; ok {
|
if _, ok := InitTypes[app.Type]; ok {
|
||||||
dockerComposeUrl := fmt.Sprintf("%s/%s", versionUrl, "docker-compose.yml")
|
dockerComposeUrl := fmt.Sprintf("%s/%s", versionUrl, "docker-compose.yml")
|
||||||
_, composeRes, err := req_helper.HandleRequestWithClient(&c.httpClient, dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
dockerComposeUrl, err := appDownloadURL(dockerComposeUrl, strconv.Itoa(c.list.LastModified))
|
||||||
|
var composeRes []byte
|
||||||
|
if err == nil {
|
||||||
|
_, composeRes, err = req_helper.HandleRequestWithClient(&c.httpClient, dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||||
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
detail.DockerCompose = string(composeRes)
|
detail.DockerCompose = string(composeRes)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -40,14 +40,13 @@ const (
|
|||||||
appUpgradeDown
|
appUpgradeDown
|
||||||
appUpgradeMutated
|
appUpgradeMutated
|
||||||
appUpgradeStarted
|
appUpgradeStarted
|
||||||
appUpgradeReady
|
|
||||||
appUpgradeCommitted
|
appUpgradeCommitted
|
||||||
)
|
)
|
||||||
|
|
||||||
const composeServiceLabel = "com.docker.compose.service"
|
|
||||||
|
|
||||||
var appUpgradeLocks sync.Map
|
var appUpgradeLocks sync.Map
|
||||||
|
|
||||||
|
const composeServiceLabel = "com.docker.compose.service"
|
||||||
|
|
||||||
type appUpgradeSnapshot interface {
|
type appUpgradeSnapshot interface {
|
||||||
Restore() error
|
Restore() error
|
||||||
Cleanup()
|
Cleanup()
|
||||||
@@ -107,6 +106,14 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !isAIGatewayUpgradeVersionAllowed(install.App.Key, install.Version, detail.Version) {
|
||||||
|
return buserr.New("ErrAIGatewayDatabaseTypeMismatch")
|
||||||
|
}
|
||||||
|
if isHermesApp(install.App.Key) &&
|
||||||
|
(!isNewerAppVersion(install.App.Key, detail.Version, install.Version) ||
|
||||||
|
(isCrossAppVersion(install.App.Key, install.Version, detail.Version) && !install.App.CrossVersionUpdate)) {
|
||||||
|
return buserr.New("ErrHermesUpgradeVersion")
|
||||||
|
}
|
||||||
if err = checkVllmVersionAccess(install.App.Key, detail.Version); err != nil {
|
if err = checkVllmVersionAccess(install.App.Key, detail.Version); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -439,15 +446,14 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
|
|||||||
t.LogSuccess(logStr)
|
t.LogSuccess(logStr)
|
||||||
u.phase = appUpgradeStarted
|
u.phase = appUpgradeStarted
|
||||||
|
|
||||||
t.LogStart(i18n.GetMsgByKey("UpgradeWaitReady"))
|
containerNames, discoverErr := discoverUpgradeContainerNames(u.candidate, u.envContent)
|
||||||
containerNames, err := waitAppContainersReady(context.Background(), u.candidate)
|
if discoverErr != nil {
|
||||||
if err != nil {
|
t.Logf("WARNING: discover upgraded application containers failed: %v", discoverErr)
|
||||||
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeWaitReady"), err)
|
} else if len(containerNames) > 0 {
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.LogSuccess(i18n.GetMsgByKey("UpgradeWaitReady"))
|
|
||||||
u.phase = appUpgradeReady
|
|
||||||
u.candidate.ContainerName = strings.Join(containerNames, ",")
|
u.candidate.ContainerName = strings.Join(containerNames, ",")
|
||||||
|
} else {
|
||||||
|
t.Log("WARNING: no containers found for the upgraded application")
|
||||||
|
}
|
||||||
u.candidate.Status = constant.StatusRunning
|
u.candidate.Status = constant.StatusRunning
|
||||||
u.candidate.Message = ""
|
u.candidate.Message = ""
|
||||||
|
|
||||||
@@ -472,6 +478,11 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
|
|||||||
} 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
|
||||||
}
|
}
|
||||||
|
if discoverErr == nil && len(containerNames) > 0 {
|
||||||
|
if syncErr := syncAppInstallStatus(&u.candidate, true); syncErr != nil {
|
||||||
|
t.Logf("WARNING: sync upgraded application status failed: %v", syncErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
u.phase = appUpgradeCommitted
|
u.phase = appUpgradeCommitted
|
||||||
u.deleteOldImages(t)
|
u.deleteOldImages(t)
|
||||||
return nil
|
return nil
|
||||||
@@ -591,9 +602,6 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *appUpgradeContext) finishRollback() error {
|
func (u *appUpgradeContext) finishRollback() error {
|
||||||
if _, err := waitAppContainersReady(context.Background(), u.original); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
restored := u.original
|
restored := u.original
|
||||||
if err := appInstallRepo.Save(context.Background(), &restored); err != nil {
|
if err := appInstallRepo.Save(context.Background(), &restored); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -881,28 +889,7 @@ func (s *upgradeFileSnapshot) Cleanup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type appContainerReadinessClient interface {
|
func discoverUpgradeContainerNames(install model.AppInstall, envContent []byte) ([]string, error) {
|
||||||
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
|
|
||||||
ContainerInspect(context.Context, string) (container.InspectResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitAppContainersReady(ctx context.Context, install model.AppInstall) ([]string, error) {
|
|
||||||
client, err := docker.NewDockerClient()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
return waitAppContainersReadyWithClient(ctx, client, install)
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitAppContainersReadyWithClient(ctx context.Context, client appContainerReadinessClient, install model.AppInstall) ([]string, error) {
|
|
||||||
envContent, err := os.ReadFile(install.GetEnvPath())
|
|
||||||
if err != nil {
|
|
||||||
envContent, err = renderUpgradeEnv(&install, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), envContent, false)
|
project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), envContent, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -916,36 +903,24 @@ func waitAppContainersReadyWithClient(ctx context.Context, client appContainerRe
|
|||||||
if len(expectedServices) == 0 {
|
if len(expectedServices) == 0 {
|
||||||
return strings.Split(install.ContainerName, ","), nil
|
return strings.Split(install.ContainerName, ","), nil
|
||||||
}
|
}
|
||||||
options := container.ListOptions{
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
containers, err := client.ContainerList(context.Background(), container.ListOptions{
|
||||||
All: true,
|
All: true,
|
||||||
Filters: filters.NewArgs(
|
Filters: filters.NewArgs(filters.Arg("label", composeWorkdirLabel+"="+install.GetPath())),
|
||||||
filters.Arg("label", composeWorkdirLabel+"="+install.GetPath()),
|
})
|
||||||
),
|
|
||||||
}
|
|
||||||
containers, err := client.ContainerList(ctx, options)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
foundServices := make(map[string]bool, len(expectedServices))
|
|
||||||
containerNames := make([]string, 0, len(containers))
|
containerNames := make([]string, 0, len(containers))
|
||||||
for _, item := range containers {
|
for _, item := range containers {
|
||||||
serviceName := item.Labels[composeServiceLabel]
|
if _, ok := expectedServices[item.Labels[composeServiceLabel]]; ok && len(item.Names) > 0 {
|
||||||
if _, ok := expectedServices[serviceName]; !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err = waitContainerReady(ctx, client, item.ID); err != nil {
|
|
||||||
return nil, fmt.Errorf("container %s is not ready: %w", serviceName, err)
|
|
||||||
}
|
|
||||||
foundServices[serviceName] = true
|
|
||||||
if len(item.Names) > 0 {
|
|
||||||
containerNames = append(containerNames, strings.TrimPrefix(item.Names[0], "/"))
|
containerNames = append(containerNames, strings.TrimPrefix(item.Names[0], "/"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for serviceName := range expectedServices {
|
|
||||||
if !foundServices[serviceName] {
|
|
||||||
return nil, fmt.Errorf("container for service %s was not created", serviceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(containerNames)
|
sort.Strings(containerNames)
|
||||||
return containerNames, nil
|
return containerNames, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+109
-30
@@ -8,6 +8,7 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -352,22 +353,34 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
|||||||
if dir != nil {
|
if dir != nil {
|
||||||
logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("App")
|
logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("App")
|
||||||
t.Log(logStr)
|
t.Log(logStr)
|
||||||
|
cleanupFailed := false
|
||||||
|
|
||||||
if deleteReq.UseLifecycleScripts {
|
if deleteReq.UseLifecycleScripts {
|
||||||
if err = runScript(t, &install, "uninstall"); err != nil {
|
if scriptErr := runScript(t, &install, "uninstall"); scriptErr != nil {
|
||||||
return err
|
cleanupFailed = true
|
||||||
|
if !deleteReq.ForceDelete {
|
||||||
|
return scriptErr
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
out, err := compose.Down(install.GetComposePath())
|
out, downErr := compose.Down(install.GetComposePath())
|
||||||
if err != nil && !deleteReq.ForceDelete {
|
if downErr != nil {
|
||||||
return handleErr(install, err, out)
|
cleanupFailed = true
|
||||||
|
if !deleteReq.ForceDelete {
|
||||||
|
return handleErr(install, downErr, out)
|
||||||
}
|
}
|
||||||
if err = runScript(t, &install, "uninstall"); err != nil {
|
}
|
||||||
|
if scriptErr := runScript(t, &install, "uninstall"); scriptErr != nil {
|
||||||
|
cleanupFailed = true
|
||||||
|
if !deleteReq.ForceDelete {
|
||||||
_, _ = compose.Up(install.GetComposePath())
|
_, _ = compose.Up(install.GetComposePath())
|
||||||
return err
|
return scriptErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if !cleanupFailed {
|
||||||
t.LogSuccess(logStr)
|
t.LogSuccess(logStr)
|
||||||
|
}
|
||||||
if deleteReq.DeleteImage {
|
if deleteReq.DeleteImage {
|
||||||
content, err := op.GetContent(install.GetEnvPath())
|
content, err := op.GetContent(install.GetEnvPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -378,12 +391,27 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if install.App.Key == "ollama" {
|
||||||
|
ollamaModelMu.Lock()
|
||||||
|
defer ollamaModelMu.Unlock()
|
||||||
|
}
|
||||||
tx, ctx := helper.GetTxAndContext()
|
tx, ctx := helper.GetTxAndContext()
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
if err = appInstallRepo.Delete(ctx, install); err != nil {
|
if err = appInstallRepo.Delete(ctx, install); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
appKey := install.App.Key
|
appKey := install.App.Key
|
||||||
|
if appKey == "ollama" {
|
||||||
|
remaining, listErr := appInstallRepo.ListBy(ctx, appInstallRepo.WithAppId(install.AppId))
|
||||||
|
if listErr != nil {
|
||||||
|
return listErr
|
||||||
|
}
|
||||||
|
if len(remaining) == 0 {
|
||||||
|
if err = aiRepo.DeleteAll(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if isAgentAppKey(appKey) {
|
if isAgentAppKey(appKey) {
|
||||||
_ = agentRepo.DeleteByAppInstallIDWithCtx(ctx, install.ID)
|
_ = agentRepo.DeleteByAppInstallIDWithCtx(ctx, install.ID)
|
||||||
}
|
}
|
||||||
@@ -456,7 +484,12 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
|||||||
if err == nil && len(entries) == 0 {
|
if err == nil && len(entries) == 0 {
|
||||||
_ = op.DeleteDir(parentDir)
|
_ = op.DeleteDir(parentDir)
|
||||||
}
|
}
|
||||||
tx.Commit()
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if install.App.Key == "ollama" || install.App.Key == "vllm" {
|
||||||
|
CleanupLocalModelAccount(install.ID)
|
||||||
|
}
|
||||||
|
|
||||||
existApps, _ := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppId(install.AppId))
|
existApps, _ := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppId(install.AppId))
|
||||||
if len(existApps) == 0 {
|
if len(existApps) == 0 {
|
||||||
@@ -466,8 +499,9 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
|||||||
}
|
}
|
||||||
uninstallTask.AddSubTask(task.GetTaskName(install.Name, task.TaskUninstall, task.TaskScopeApp), uninstall, nil)
|
uninstallTask.AddSubTask(task.GetTaskName(install.Name, task.TaskUninstall, task.TaskScopeApp), uninstall, nil)
|
||||||
go func() {
|
go func() {
|
||||||
if err := uninstallTask.Execute(); err != nil && !deleteReq.ForceDelete {
|
if err := uninstallTask.Execute(); err != nil {
|
||||||
install.Status = constant.StatusError
|
install.Status = constant.StatusError
|
||||||
|
install.Message = err.Error()
|
||||||
_ = appInstallRepo.Save(context.Background(), &install)
|
_ = appInstallRepo.Save(context.Background(), &install)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -878,16 +912,52 @@ func handleMap(params map[string]interface{}, envParams map[string]string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appDownloadURL(rawURL, version string) (string, error) {
|
||||||
|
parsedURL, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
query := parsedURL.Query()
|
||||||
|
query.Set("v", version)
|
||||||
|
parsedURL.RawQuery = query.Encode()
|
||||||
|
return parsedURL.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentAppDownloadURL(rawURL string) (string, error) {
|
||||||
|
storeVersion, err := settingRepo.GetValueByKey("AppStoreLastModified")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return appDownloadURL(rawURL, storeVersion)
|
||||||
|
}
|
||||||
|
|
||||||
func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.AppInstall, logger *logrus.Logger) (err error) {
|
func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.AppInstall, logger *logrus.Logger) (err error) {
|
||||||
if app.IsLocalApp() || app.IsCustomApp() {
|
if app.IsLocalApp() || app.IsCustomApp() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
if err != nil && appInstall != nil {
|
||||||
|
appInstall.Status = constant.StatusDownloadErr
|
||||||
|
appInstall.Message = err.Error()
|
||||||
|
}
|
||||||
|
}()
|
||||||
appResourceDir := path.Join(global.Dir.AppResourceDir, app.Resource)
|
appResourceDir := path.Join(global.Dir.AppResourceDir, app.Resource)
|
||||||
appDownloadDir := app.GetAppResourcePath()
|
appDownloadDir := app.GetAppResourcePath()
|
||||||
appVersionDir := path.Join(appDownloadDir, appDetail.Version)
|
appVersionDir := path.Join(appDownloadDir, appDetail.Version)
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
|
storeVersion, err := settingRepo.GetValueByKey("AppStoreLastModified")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cacheVersionPath := path.Join(appVersionDir, ".1panel-appstore-version")
|
||||||
if !appDetail.Update && fileOp.Stat(appVersionDir) {
|
if !appDetail.Update && fileOp.Stat(appVersionDir) {
|
||||||
return
|
if cachedVersion, readErr := os.ReadFile(cacheVersionPath); readErr == nil && string(cachedVersion) == storeVersion {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
downloadURL, err := appDownloadURL(appDetail.DownloadUrl, storeVersion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
if !fileOp.Stat(appDownloadDir) {
|
if !fileOp.Stat(appDownloadDir) {
|
||||||
_ = fileOp.CreateDir(appDownloadDir, constant.DirPerm)
|
_ = fileOp.CreateDir(appDownloadDir, constant.DirPerm)
|
||||||
@@ -896,23 +966,14 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
|
|||||||
_ = fileOp.CreateDir(appVersionDir, constant.DirPerm)
|
_ = fileOp.CreateDir(appVersionDir, constant.DirPerm)
|
||||||
}
|
}
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
global.LOG.Infof("download app [%s] from %s", app.Name, appDetail.DownloadUrl)
|
global.LOG.Infof("download app [%s] from %s", app.Name, downloadURL)
|
||||||
} else {
|
} else {
|
||||||
logger.Printf("download app [%s] from %s", app.Name, appDetail.DownloadUrl)
|
logger.Printf("download app [%s] from %s", app.Name, downloadURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
filePath := path.Join(appVersionDir, app.Key+"-"+appDetail.Version+".tar.gz")
|
filePath := path.Join(appVersionDir, app.Key+"-"+appDetail.Version+".tar.gz")
|
||||||
|
|
||||||
defer func() {
|
if err = files.DownloadFileWithProxy(downloadURL, filePath); err != nil {
|
||||||
if err != nil {
|
|
||||||
if appInstall != nil {
|
|
||||||
appInstall.Status = constant.StatusDownloadErr
|
|
||||||
appInstall.Message = err.Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err = files.DownloadFileWithProxy(appDetail.DownloadUrl, filePath); err != nil {
|
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
global.LOG.Errorf("download app [%s] error %v", app.Name, err)
|
global.LOG.Errorf("download app [%s] error %v", app.Name, err)
|
||||||
} else {
|
} else {
|
||||||
@@ -929,6 +990,9 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = fileOp.DeleteFile(filePath)
|
_ = fileOp.DeleteFile(filePath)
|
||||||
|
if err = os.WriteFile(cacheVersionPath, []byte(storeVersion), constant.FilePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
appDetail.Update = false
|
appDetail.Update = false
|
||||||
_ = appDetailRepo.Update(context.Background(), appDetail)
|
_ = appDetailRepo.Update(context.Background(), appDetail)
|
||||||
return
|
return
|
||||||
@@ -1393,9 +1457,8 @@ 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 appInstallOperationPending(installed.Status) || installed.Status == constant.StatusSyncing ||
|
||||||
installed.Status == constant.StatusSyncing || installed.Status == constant.StatusUninstalling || installed.Status == constant.StatusInstallErr ||
|
installed.Status == constant.StatusInstallErr || installed.Status == constant.StatusUpgradeErr || installed.Status == constant.StatusUpErr
|
||||||
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) {
|
||||||
@@ -1489,6 +1552,11 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, installed := range appInstallList {
|
for _, installed := range appInstallList {
|
||||||
|
if sync && appInstallOperationPending(installed.Status) {
|
||||||
|
if err := syncAppInstallStatus(&installed, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if updated && ignoreUpdate(installed) {
|
if updated && ignoreUpdate(installed) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1554,11 +1622,14 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
|
|
||||||
var versions []string
|
var versions []string
|
||||||
for _, appDetail := range details {
|
for _, appDetail := range details {
|
||||||
|
if !isAIGatewayUpgradeVersionAllowed(installed.App.Key, installed.Version, appDetail.Version) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(appDetail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(appDetail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
||||||
if len(ignores) > 0 {
|
if len(ignores) > 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if common.IsCrossVersion(installed.Version, appDetail.Version) && !installed.App.CrossVersionUpdate {
|
if isCrossAppVersion(installed.App.Key, installed.Version, appDetail.Version) && !installed.App.CrossVersionUpdate {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
versions = append(versions, appDetail.Version)
|
versions = append(versions, appDetail.Version)
|
||||||
@@ -1573,7 +1644,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
versions = common.GetSortedVersions(versions)
|
versions = sortAppVersions(installed.App.Key, versions)
|
||||||
lastVersion := versions[0]
|
lastVersion := versions[0]
|
||||||
|
|
||||||
if installed.App.Key == constant.AppMysql || installed.App.Key == constant.AppMysqlCluster {
|
if installed.App.Key == constant.AppMysql || installed.App.Key == constant.AppMysqlCluster {
|
||||||
@@ -1588,10 +1659,10 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if common.IsCrossVersion(installed.Version, lastVersion) {
|
if isCrossAppVersion(installed.App.Key, installed.Version, lastVersion) {
|
||||||
installDTO.CanUpdate = installed.App.CrossVersionUpdate
|
installDTO.CanUpdate = installed.App.CrossVersionUpdate
|
||||||
} else {
|
} else {
|
||||||
installDTO.CanUpdate = common.CompareVersion(lastVersion, installed.Version)
|
installDTO.CanUpdate = isNewerAppVersion(installed.App.Key, lastVersion, installed.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
if updated {
|
if updated {
|
||||||
@@ -1964,6 +2035,9 @@ func getAppTags(appID uint, lang string) ([]response.TagDTO, error) {
|
|||||||
|
|
||||||
func handleSiteDir(app model.App, appDetail model.AppDetail, req request.AppInstallCreate, t *task.Task) error {
|
func handleSiteDir(app model.App, appDetail model.AppDetail, req request.AppInstallCreate, t *task.Task) error {
|
||||||
if app.Key == "openresty" && (app.Resource == "remote" || app.Resource == "custom") && common.CompareVersion(appDetail.Version, "1.27") {
|
if app.Key == "openresty" && (app.Resource == "remote" || app.Resource == "custom") && common.CompareVersion(appDetail.Version, "1.27") {
|
||||||
|
if req.MCPPreserveWebsiteDir {
|
||||||
|
return prepareMCPWebsiteDirectory(req)
|
||||||
|
}
|
||||||
if dir, ok := req.Params["WEBSITE_DIR"]; ok {
|
if dir, ok := req.Params["WEBSITE_DIR"]; ok {
|
||||||
siteDir := dir.(string)
|
siteDir := dir.(string)
|
||||||
if siteDir == "" || !strings.HasPrefix(siteDir, "/") {
|
if siteDir == "" || !strings.HasPrefix(siteDir, "/") {
|
||||||
@@ -2262,7 +2336,12 @@ func getAppVersions(key string, details []model.AppDetail) []string {
|
|||||||
}
|
}
|
||||||
versionsRaw = append(versionsRaw, detail.Version)
|
versionsRaw = append(versionsRaw, detail.Version)
|
||||||
}
|
}
|
||||||
versions = common.GetSortedVersions(versionsRaw)
|
if isAIGatewayApp(key) {
|
||||||
|
sortAIGatewayVersions(versionsRaw)
|
||||||
|
versions = versionsRaw
|
||||||
|
} else {
|
||||||
|
versions = sortAppVersions(key, versionsRaw)
|
||||||
|
}
|
||||||
if hasLatest {
|
if hasLatest {
|
||||||
versions = append([]string{latestVersion}, versions...)
|
versions = append([]string{latestVersion}, versions...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,11 +132,9 @@ func (u *BackupService) AppRecover(req dto.CommonRecover) error {
|
|||||||
if !fileOp.Stat(req.File) {
|
if !fileOp.Stat(req.File) {
|
||||||
return buserr.WithName("ErrFileNotFound", req.File)
|
return buserr.WithName("ErrFileNotFound", req.File)
|
||||||
}
|
}
|
||||||
if _, err := compose.Down(install.GetComposePath()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := handleAppRecover(&install, nil, req.File, false, req.Secret, req.TaskID); err != nil {
|
if err := handleAppRecover(&install, nil, req.File, false, req.Secret, req.TaskID); err != nil {
|
||||||
global.LOG.Errorf("recover app %s failed, err: %v", req.DetailName, err)
|
global.LOG.Errorf("recover app %s failed, err: %v", req.DetailName, err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -200,8 +198,20 @@ func handleAppBackup(install *model.AppInstall, parentTask *task.Task, recordID
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverFile string, isRollback bool, secret, taskID string) error {
|
func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverFile string, isRollback bool, secret, taskID string) error {
|
||||||
|
// Validate AI Gateway backups before stopping the app, creating rollback
|
||||||
|
// backups, or touching its files/database. Keep the validated extraction for
|
||||||
|
// the restore so uploaded/encrypted archives are not unpacked twice.
|
||||||
|
preparedPath, cleanup, err := prepareAIGatewayAppRecover(install, recoverFile, secret)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if parentTask == nil && !isRollback {
|
||||||
|
if _, err := compose.Down(install.GetComposePath()); err != nil {
|
||||||
|
cleanup()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
var (
|
var (
|
||||||
err error
|
|
||||||
recoverTask *task.Task
|
recoverTask *task.Task
|
||||||
isOk = false
|
isOk = false
|
||||||
rollbackFile string
|
rollbackFile string
|
||||||
@@ -210,11 +220,13 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
|||||||
if parentTask == nil {
|
if parentTask == nil {
|
||||||
recoverTask, err = task.NewTaskWithOps(install.Name, task.TaskRecover, task.TaskScopeBackup, taskID, install.ID)
|
recoverTask, err = task.NewTaskWithOps(install.Name, task.TaskRecover, task.TaskScopeBackup, taskID, install.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
cleanup()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recoverApp := func(t *task.Task) error {
|
recoverApp := func(t *task.Task) error {
|
||||||
|
defer cleanup()
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
if !isRollback {
|
if !isRollback {
|
||||||
rollbackFile = path.Join(global.Dir.TmpDir, fmt.Sprintf("app/%s_%s.tar.gz", install.Name, time.Now().Format(constant.DateTimeSlimLayout)))
|
rollbackFile = path.Join(global.Dir.TmpDir, fmt.Sprintf("app/%s_%s.tar.gz", install.Name, time.Now().Format(constant.DateTimeSlimLayout)))
|
||||||
@@ -223,10 +235,13 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tmpPath := preparedPath
|
||||||
|
if tmpPath == "" {
|
||||||
if err := fileOp.TarGzExtractPro(recoverFile, path.Dir(recoverFile), secret); err != nil {
|
if err := fileOp.TarGzExtractPro(recoverFile, path.Dir(recoverFile), secret); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tmpPath := strings.ReplaceAll(recoverFile, ".tar.gz", "")
|
tmpPath = strings.ReplaceAll(recoverFile, ".tar.gz", "")
|
||||||
|
}
|
||||||
if err := fileOp.TarGzExtractPro(tmpPath+"/app.tar.gz", tmpPath, ""); err != nil {
|
if err := fileOp.TarGzExtractPro(tmpPath+"/app.tar.gz", tmpPath, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -236,7 +251,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
|||||||
} else {
|
} else {
|
||||||
_, _ = compose.Up(install.GetComposePath())
|
_, _ = compose.Up(install.GetComposePath())
|
||||||
}
|
}
|
||||||
_ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", ""))
|
_ = os.RemoveAll(tmpPath)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if !fileOp.Stat(tmpPath+"/app.json") || !fileOp.Stat(tmpPath+"/app.tar.gz") {
|
if !fileOp.Stat(tmpPath+"/app.json") || !fileOp.Stat(tmpPath+"/app.tar.gz") {
|
||||||
@@ -397,6 +412,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
|||||||
}
|
}
|
||||||
recoverTask.AddSubTask(task.GetTaskName(install.Name, task.TaskRecover, task.TaskScopeBackup), recoverApp, rollBackApp)
|
recoverTask.AddSubTask(task.GetTaskName(install.Name, task.TaskRecover, task.TaskScopeBackup), recoverApp, rollBackApp)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer cleanup()
|
||||||
_ = recoverTask.Execute()
|
_ = recoverTask.Execute()
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -371,43 +371,39 @@ func loadComposePathAndFiles(composeName string, dockerClient *client.Client) (s
|
|||||||
}
|
}
|
||||||
config := targetContainer.Labels[composeConfigLabel]
|
config := targetContainer.Labels[composeConfigLabel]
|
||||||
workdir := targetContainer.Labels[composeWorkdirLabel]
|
workdir := targetContainer.Labels[composeWorkdirLabel]
|
||||||
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
|
files, err := resolveComposeFiles(workdir, config)
|
||||||
composePath = config
|
if err != nil {
|
||||||
} else {
|
return "", nil, err
|
||||||
composePath = workdir
|
|
||||||
}
|
}
|
||||||
|
composePath = strings.Join(files, ",")
|
||||||
}
|
}
|
||||||
composeFiles := normalizeComposeFiles(composePath)
|
composeFiles, err := normalizeComposeFiles(composePath)
|
||||||
if len(composeFiles) == 0 {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("compose file not found for %s", composeName)
|
return "", nil, err
|
||||||
}
|
}
|
||||||
return composePath, composeFiles, nil
|
return strings.Join(composeFiles, ","), composeFiles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeComposeFiles(composePath string) []string {
|
func normalizeComposeFiles(composePath string) ([]string, error) {
|
||||||
items := strings.Split(composePath, ",")
|
files, err := resolveComposeFiles("", composePath)
|
||||||
result := make([]string, 0)
|
if err != nil {
|
||||||
seen := make(map[string]struct{})
|
return nil, err
|
||||||
for _, item := range items {
|
|
||||||
item = strings.TrimSpace(item)
|
|
||||||
if item == "" {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
stat, err := os.Stat(item)
|
for i, file := range files {
|
||||||
if err == nil && stat.IsDir() {
|
info, err := os.Stat(file)
|
||||||
item = path.Join(item, "docker-compose.yml")
|
if err == nil && info.IsDir() {
|
||||||
|
file = filepath.Join(file, "docker-compose.yml")
|
||||||
|
info, err = os.Stat(file)
|
||||||
|
files[i] = file
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(item); err != nil {
|
if err != nil {
|
||||||
continue
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, ok := seen[item]; ok {
|
if !info.Mode().IsRegular() {
|
||||||
continue
|
return nil, fmt.Errorf("compose path %s is not a regular file", file)
|
||||||
}
|
}
|
||||||
seen[item] = struct{}{}
|
|
||||||
result = append(result, item)
|
|
||||||
}
|
}
|
||||||
sort.Strings(result)
|
return files, nil
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *composeBackupContext) close() {
|
func (c *composeBackupContext) close() {
|
||||||
@@ -621,20 +617,46 @@ func safeJoinWithinBase(baseDir, name string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
||||||
if recoverCtx.targetDir != "" {
|
composeFiles := recoverCtx.meta.Files
|
||||||
_ = os.RemoveAll(recoverCtx.targetDir)
|
if strings.TrimSpace(recoverCtx.meta.ComposePath) != "" {
|
||||||
|
filesByPath := make(map[string]composeBackupFile, len(composeFiles))
|
||||||
|
for _, item := range composeFiles {
|
||||||
|
filesByPath[filepath.Clean(item.OriginalPath)] = item
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(recoverCtx.targetDir, os.ModePerm); err != nil {
|
orderedFiles := make([]composeBackupFile, 0, len(composeFiles))
|
||||||
return err
|
for _, file := range strings.Split(recoverCtx.meta.ComposePath, ",") {
|
||||||
|
file = strings.TrimSpace(file)
|
||||||
|
if file == "" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
restored := make([]string, 0, len(recoverCtx.meta.Files))
|
file = filepath.Clean(file)
|
||||||
for _, item := range recoverCtx.meta.Files {
|
item, ok := filesByPath[file]
|
||||||
|
if !ok {
|
||||||
|
if len(composeFiles) != 1 || file != filepath.Dir(composeFiles[0].OriginalPath) {
|
||||||
|
return fmt.Errorf("compose file %s is missing from backup metadata", file)
|
||||||
|
}
|
||||||
|
item = composeFiles[0]
|
||||||
|
}
|
||||||
|
orderedFiles = append(orderedFiles, item)
|
||||||
|
}
|
||||||
|
composeFiles = orderedFiles
|
||||||
|
}
|
||||||
|
if len(composeFiles) == 0 {
|
||||||
|
return fmt.Errorf("compose files are missing from backup metadata")
|
||||||
|
}
|
||||||
|
restored := make([]string, 0, len(composeFiles))
|
||||||
|
sourceByTarget := make(map[string]string, len(composeFiles))
|
||||||
|
for _, item := range composeFiles {
|
||||||
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
|
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
|
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
|
||||||
}
|
}
|
||||||
if !recoverCtx.fileOp.Stat(backupPath) {
|
info, err := os.Stat(backupPath)
|
||||||
continue
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
return fmt.Errorf("compose backup path %s is not a regular file", backupPath)
|
||||||
}
|
}
|
||||||
targetName := item.FileName
|
targetName := item.FileName
|
||||||
if item.RelativePath != "" {
|
if item.RelativePath != "" {
|
||||||
@@ -650,6 +672,29 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid compose target path %q, err: %v", targetName, err)
|
return fmt.Errorf("invalid compose target path %q, err: %v", targetName, err)
|
||||||
}
|
}
|
||||||
|
sourcePath := item.OriginalPath
|
||||||
|
if sourcePath == "" {
|
||||||
|
sourcePath = backupPath
|
||||||
|
}
|
||||||
|
sourcePath = filepath.Clean(sourcePath)
|
||||||
|
if previous, ok := sourceByTarget[targetPath]; ok && previous != sourcePath {
|
||||||
|
return fmt.Errorf("compose files %q and %q resolve to the same restore path %q", previous, sourcePath, targetPath)
|
||||||
|
}
|
||||||
|
sourceByTarget[targetPath] = sourcePath
|
||||||
|
restored = append(restored, targetPath)
|
||||||
|
}
|
||||||
|
if recoverCtx.targetDir != "" {
|
||||||
|
_ = os.RemoveAll(recoverCtx.targetDir)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(recoverCtx.targetDir, os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, item := range composeFiles {
|
||||||
|
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
|
||||||
|
}
|
||||||
|
targetPath := restored[i]
|
||||||
if err := os.MkdirAll(path.Dir(targetPath), os.ModePerm); err != nil {
|
if err := os.MkdirAll(path.Dir(targetPath), os.ModePerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -660,7 +705,6 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
|||||||
if err := recoverCtx.fileOp.SaveFile(targetPath, string(content), fs.ModePerm); err != nil {
|
if err := recoverCtx.fileOp.SaveFile(targetPath, string(content), fs.ModePerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
restored = append(restored, targetPath)
|
|
||||||
}
|
}
|
||||||
envPath := path.Join(recoverCtx.tmpDir, "compose_files", ".env")
|
envPath := path.Join(recoverCtx.tmpDir, "compose_files", ".env")
|
||||||
if recoverCtx.fileOp.Stat(envPath) {
|
if recoverCtx.fileOp.Stat(envPath) {
|
||||||
@@ -672,14 +716,6 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(restored) == 0 {
|
|
||||||
defaultPath := path.Join(recoverCtx.targetDir, "docker-compose.yml")
|
|
||||||
if !recoverCtx.fileOp.Stat(defaultPath) {
|
|
||||||
return fmt.Errorf("compose file not found in backup data")
|
|
||||||
}
|
|
||||||
restored = append(restored, defaultPath)
|
|
||||||
}
|
|
||||||
sort.Strings(restored)
|
|
||||||
recoverCtx.composePath = strings.Join(restored, ",")
|
recoverCtx.composePath = strings.Join(restored, ",")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+113
-78
@@ -58,10 +58,10 @@ type ContainerService struct{}
|
|||||||
var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>")
|
var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>")
|
||||||
|
|
||||||
type IContainerService interface {
|
type IContainerService interface {
|
||||||
Page(req dto.PageContainer) (int64, interface{}, error)
|
Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error)
|
||||||
List() []dto.ContainerOptions
|
List() []dto.ContainerOptions
|
||||||
ListByImage(imageName string) []dto.ContainerOptions
|
ListByImage(imageName string) []dto.ContainerOptions
|
||||||
LoadStatus() (dto.ContainerStatus, error)
|
LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error)
|
||||||
PageNetwork(req dto.SearchWithPage) (int64, interface{}, error)
|
PageNetwork(req dto.SearchWithPage) (int64, interface{}, error)
|
||||||
ListNetwork() ([]dto.Options, error)
|
ListNetwork() ([]dto.Options, error)
|
||||||
PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
|
PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
|
||||||
@@ -80,7 +80,7 @@ type IContainerService interface {
|
|||||||
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) (*dto.ContainerOperate, error)
|
||||||
ContainerListStats() ([]dto.ContainerListStats, error)
|
ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error)
|
||||||
ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error)
|
ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error)
|
||||||
LoadResourceLimit() (*dto.ResourceLimit, error)
|
LoadResourceLimit() (*dto.ResourceLimit, error)
|
||||||
ContainerRename(req dto.ContainerRename) error
|
ContainerRename(req dto.ContainerRename) error
|
||||||
@@ -112,7 +112,7 @@ func NewIContainerService() IContainerService {
|
|||||||
return &ContainerService{}
|
return &ContainerService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) {
|
func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error) {
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
@@ -123,7 +123,7 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro
|
|||||||
options.Filters = filters.NewArgs()
|
options.Filters = filters.NewArgs()
|
||||||
options.Filters.Add("label", req.Filters)
|
options.Filters.Add("label", req.Filters)
|
||||||
}
|
}
|
||||||
containers, err := client.ContainerList(context.Background(), options)
|
containers, err := client.ContainerList(ctx, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
@@ -206,27 +206,30 @@ func (u *ContainerService) ListByImage(imageName string) []dto.ContainerOptions
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
|
func (u *ContainerService) LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error) {
|
||||||
var data dto.ContainerStatus
|
var data dto.ContainerStatus
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
c := context.Background()
|
if !containersOnly {
|
||||||
|
images, _ := client.ImageList(ctx, image.ListOptions{All: true})
|
||||||
images, _ := client.ImageList(c, image.ListOptions{All: true})
|
|
||||||
data.ImageCount = len(images)
|
data.ImageCount = len(images)
|
||||||
repo, _ := imageRepoRepo.List()
|
repo, _ := imageRepoRepo.List()
|
||||||
data.RepoCount = len(repo)
|
data.RepoCount = len(repo)
|
||||||
templates, _ := composeRepo.List()
|
templates, _ := composeRepo.List()
|
||||||
data.ComposeTemplateCount = len(templates)
|
data.ComposeTemplateCount = len(templates)
|
||||||
networks, _ := client.NetworkList(c, network.ListOptions{})
|
networks, _ := client.NetworkList(ctx, network.ListOptions{})
|
||||||
data.NetworkCount = len(networks)
|
data.NetworkCount = len(networks)
|
||||||
volumes, _ := client.VolumeList(c, volume.ListOptions{})
|
volumes, _ := client.VolumeList(ctx, volume.ListOptions{})
|
||||||
data.VolumeCount = len(volumes.Volumes)
|
data.VolumeCount = len(volumes.Volumes)
|
||||||
data.ComposeCount = loadComposeCount(client)
|
data.ComposeCount = loadComposeCount(client)
|
||||||
containers, _ := client.ContainerList(c, container.ListOptions{All: true})
|
}
|
||||||
|
containers, err := client.ContainerList(ctx, container.ListOptions{All: true})
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
data.ContainerCount = len(containers)
|
data.ContainerCount = len(containers)
|
||||||
for _, item := range containers {
|
for _, item := range containers {
|
||||||
switch item.State {
|
switch item.State {
|
||||||
@@ -292,25 +295,48 @@ func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.Opera
|
|||||||
}
|
}
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error) {
|
func (u *ContainerService) ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error) {
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
list, err := client.ContainerList(context.Background(), container.ListOptions{All: true})
|
options := container.ListOptions{All: true}
|
||||||
|
if ids != nil {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return []dto.ContainerListStats{}, nil
|
||||||
|
}
|
||||||
|
options.Filters = filters.NewArgs()
|
||||||
|
for _, id := range ids {
|
||||||
|
options.Filters.Add("id", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list, err := client.ContainerList(ctx, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
datas := make([]dto.ContainerListStats, len(list))
|
datas := make([]dto.ContainerListStats, len(list))
|
||||||
|
jobs := make(chan int)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(len(list))
|
for worker := 0; worker < min(8, len(list)); worker++ {
|
||||||
for i := 0; i < len(list); i++ {
|
wg.Add(1)
|
||||||
go func(index int, item container.Summary) {
|
go func() {
|
||||||
datas[index] = loadCpuAndMem(client, item.ID)
|
defer wg.Done()
|
||||||
wg.Done()
|
for index := range jobs {
|
||||||
}(i, list[i])
|
datas[index].ContainerID = list[index].ID
|
||||||
|
if ctx.Err() == nil && list[index].State == "running" {
|
||||||
|
datas[index] = loadCpuAndMem(ctx, client, list[index].ID)
|
||||||
}
|
}
|
||||||
|
if len(list[index].Names) > 0 {
|
||||||
|
datas[index].ContainerName = strings.TrimPrefix(list[index].Names[0], "/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
for index := range list {
|
||||||
|
jobs <- index
|
||||||
|
}
|
||||||
|
close(jobs)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
return datas, nil
|
return datas, nil
|
||||||
}
|
}
|
||||||
@@ -326,40 +352,47 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
|
|||||||
case "container":
|
case "container":
|
||||||
inspectInfo, err = client.ContainerInspect(context.Background(), req.ID)
|
inspectInfo, err = client.ContainerInspect(context.Background(), req.ID)
|
||||||
case "compose":
|
case "compose":
|
||||||
filePath := ""
|
config, workdir := "", ""
|
||||||
cli, err := docker.NewDockerClient()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer cli.Close()
|
|
||||||
options := container.ListOptions{All: true}
|
options := container.ListOptions{All: true}
|
||||||
options.Filters = filters.NewArgs()
|
options.Filters = filters.NewArgs()
|
||||||
options.Filters.Add("label", fmt.Sprintf("%s=%s", composeProjectLabel, req.ID))
|
options.Filters.Add("label", fmt.Sprintf("%s=%s", composeProjectLabel, req.ID))
|
||||||
containers, err := cli.ContainerList(context.Background(), options)
|
containers, err := client.ContainerList(context.Background(), options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
for _, container := range containers {
|
|
||||||
config := container.Labels[composeConfigLabel]
|
|
||||||
if len(req.Detail) != 0 && strings.Contains(config, req.Detail) {
|
|
||||||
config = req.Detail
|
|
||||||
}
|
|
||||||
workdir := container.Labels[composeWorkdirLabel]
|
|
||||||
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
|
|
||||||
filePath = config
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
filePath = workdir
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(containers) == 0 {
|
if len(containers) == 0 {
|
||||||
composeItem, _ := composeRepo.GetRecord(repo.WithByName(req.ID))
|
composeItem, err := composeRepo.GetRecord(repo.WithByName(req.ID))
|
||||||
filePath = composeItem.Path
|
if err != nil {
|
||||||
}
|
|
||||||
if _, err := os.Stat(filePath); err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
config = composeItem.Path
|
||||||
|
workdir = filepath.Dir(primaryComposePath(config))
|
||||||
|
} else {
|
||||||
|
config = containers[0].Labels[composeConfigLabel]
|
||||||
|
workdir = containers[0].Labels[composeWorkdirLabel]
|
||||||
|
}
|
||||||
|
composeFiles, err := resolveComposeFiles(workdir, config)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
filePath := composeFiles[0]
|
||||||
|
if strings.TrimSpace(req.Detail) != "" {
|
||||||
|
selectedFiles, err := resolveComposeFiles(workdir, req.Detail)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(selectedFiles) != 1 || !slices.Contains(composeFiles, selectedFiles[0]) {
|
||||||
|
return "", fmt.Errorf("compose file %s does not belong to project %s", req.Detail, req.ID)
|
||||||
|
}
|
||||||
|
filePath = selectedFiles[0]
|
||||||
|
}
|
||||||
|
info, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
return "", fmt.Errorf("compose path %s is not a regular file", filePath)
|
||||||
|
}
|
||||||
content, err := os.ReadFile(filePath)
|
content, err := os.ReadFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -383,11 +416,6 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
||||||
client, err := docker.NewDockerClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
name := ""
|
name := ""
|
||||||
switch req.PruneType {
|
switch req.PruneType {
|
||||||
case "container":
|
case "container":
|
||||||
@@ -408,6 +436,14 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
|
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
|
||||||
|
if err := t.TaskCtx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
pruneFilters := filters.NewArgs()
|
pruneFilters := filters.NewArgs()
|
||||||
if req.WithTagAll {
|
if req.WithTagAll {
|
||||||
pruneFilters.Add("dangling", "false")
|
pruneFilters.Add("dangling", "false")
|
||||||
@@ -431,10 +467,7 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
|||||||
}
|
}
|
||||||
SpaceReclaimed = int(rep.SpaceReclaimed)
|
SpaceReclaimed = int(rep.SpaceReclaimed)
|
||||||
case "network":
|
case "network":
|
||||||
_, err := client.NetworksPrune(context.Background(), pruneFilters)
|
return cleanUnusedNetworks(t, client)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case "volume":
|
case "volume":
|
||||||
versions, err := client.ServerVersion(context.Background())
|
versions, err := client.ServerVersion(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -803,33 +836,33 @@ func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
|
|||||||
func (u *ContainerService) StreamLogs(ctx *gin.Context, params dto.StreamLog) {
|
func (u *ContainerService) StreamLogs(ctx *gin.Context, params dto.StreamLog) {
|
||||||
messageChan := make(chan string, 1024)
|
messageChan := make(chan string, 1024)
|
||||||
errorChan := make(chan error, 1)
|
errorChan := make(chan error, 1)
|
||||||
doneChan := make(chan struct{})
|
logCtx, cancel := context.WithCancel(ctx.Request.Context())
|
||||||
|
defer cancel()
|
||||||
go func() {
|
doneChan := logCtx.Done()
|
||||||
<-ctx.Request.Context().Done()
|
|
||||||
close(doneChan)
|
|
||||||
}()
|
|
||||||
go collectLogs(doneChan, params, messageChan, errorChan)
|
go collectLogs(doneChan, params, messageChan, errorChan)
|
||||||
ctx.Stream(func(w io.Writer) bool {
|
ctx.Stream(func(w io.Writer) bool {
|
||||||
select {
|
return writeContainerLogMessage(w, doneChan, messageChan, errorChan)
|
||||||
case msg, ok := <-messageChan:
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, err := fmt.Fprintf(w, "data: %s\n\n", msg)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
case err := <-errorChan:
|
|
||||||
if err != nil {
|
|
||||||
_, _ = fmt.Fprintf(w, "event: error\ndata: %v\n\n", err.Error())
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeContainerLogMessage(w io.Writer, done <-chan struct{}, messages <-chan string, errors <-chan error) bool {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return false
|
||||||
|
case message, ok := <-messages:
|
||||||
|
if ok {
|
||||||
|
_, err := fmt.Fprintf(w, "data: %s\n\n", message)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
// The collector closes both channels when it finishes. Drain all buffered
|
||||||
|
// logs before handling its result, or a ready error channel can drop the tail.
|
||||||
|
if err := <-errors; err != nil {
|
||||||
|
_, _ = fmt.Fprintf(w, "event: error\ndata: %v\n\n", err)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func collectLogs(done <-chan struct{}, params dto.StreamLog, messageChan chan<- string, errorChan chan<- error) {
|
func collectLogs(done <-chan struct{}, params dto.StreamLog, messageChan chan<- string, errorChan chan<- error) {
|
||||||
defer close(messageChan)
|
defer close(messageChan)
|
||||||
defer close(errorChan)
|
defer close(errorChan)
|
||||||
@@ -1715,11 +1748,13 @@ func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo
|
|||||||
return selected
|
return selected
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerListStats {
|
func loadCpuAndMem(ctx context.Context, client *client.Client, containerItem string) dto.ContainerListStats {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
data := dto.ContainerListStats{
|
data := dto.ContainerListStats{
|
||||||
ContainerID: containerItem,
|
ContainerID: containerItem,
|
||||||
}
|
}
|
||||||
res, err := client.ContainerStats(context.Background(), containerItem, false)
|
res, err := client.ContainerStats(ctx, containerItem, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,10 +71,9 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
ContainerCount: 0,
|
ContainerCount: 0,
|
||||||
CreatedAt: localItem.CreatedAt.Format(constant.DateTimeLayout),
|
CreatedAt: localItem.CreatedAt.Format(constant.DateTimeLayout),
|
||||||
ConfigFile: localItem.Path,
|
ConfigFile: localItem.Path,
|
||||||
Workdir: strings.TrimSuffix(localItem.Path, "/docker-compose.yml"),
|
Workdir: filepath.Dir(primaryComposePath(localItem.Path)),
|
||||||
}
|
}
|
||||||
composeItemLocal.CreatedBy = "1Panel"
|
composeItemLocal.CreatedBy = "1Panel"
|
||||||
composeItemLocal.Path = localItem.Path
|
|
||||||
composeLocalMap[localItem.Name] = composeItemLocal
|
composeLocalMap[localItem.Name] = composeItemLocal
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,11 +111,6 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
if ok {
|
if ok {
|
||||||
composeItem.CreatedBy = createdBy
|
composeItem.CreatedBy = createdBy
|
||||||
}
|
}
|
||||||
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
|
|
||||||
composeItem.Path = config
|
|
||||||
} else {
|
|
||||||
composeItem.Path = workdir
|
|
||||||
}
|
|
||||||
for i := 0; i < len(composeCreatedByLocal); i++ {
|
for i := 0; i < len(composeCreatedByLocal); i++ {
|
||||||
if composeCreatedByLocal[i].Name == name {
|
if composeCreatedByLocal[i].Name == name {
|
||||||
composeItem.CreatedBy = "1Panel"
|
composeItem.CreatedBy = "1Panel"
|
||||||
@@ -147,7 +141,9 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
|
|
||||||
for key, value := range mergedMap {
|
for key, value := range mergedMap {
|
||||||
value.Name = key
|
value.Name = key
|
||||||
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
|
composeFiles, err := resolveComposeFiles(value.Workdir, value.ConfigFile)
|
||||||
|
value.Path = strings.Join(composeFiles, ",")
|
||||||
|
value.ComposeFileExists = err == nil && composeFileExists(composeFiles)
|
||||||
value.IsPinned = pinnedByName[key]
|
value.IsPinned = pinnedByName[key]
|
||||||
records = append(records, value)
|
records = append(records, value)
|
||||||
}
|
}
|
||||||
@@ -192,29 +188,6 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
return int64(total), listItem, nil
|
return int64(total), listItem, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func composeFileExists(workdir, configFile string) bool {
|
|
||||||
workdir = strings.TrimSpace(workdir)
|
|
||||||
configFile = strings.TrimSpace(configFile)
|
|
||||||
if configFile == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, item := range strings.Split(configFile, ",") {
|
|
||||||
file := strings.TrimSpace(item)
|
|
||||||
if file == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !filepath.IsAbs(file) && workdir != "" {
|
|
||||||
file = filepath.Join(workdir, file)
|
|
||||||
}
|
|
||||||
file = filepath.Clean(file)
|
|
||||||
info, err := os.Stat(file)
|
|
||||||
if err == nil && !info.IsDir() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
||||||
if err := validateComposeCreateName(req); err != nil {
|
if err := validateComposeCreateName(req); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -522,10 +495,6 @@ func isComposeProjectNameEmptyError(err error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
||||||
if len(req.Path) == 0 && req.Operation == "delete" {
|
|
||||||
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if cmd.CheckIllegal(req.Path, req.Operation) {
|
if cmd.CheckIllegal(req.Path, req.Operation) {
|
||||||
return buserr.New("ErrCmdIllegal")
|
return buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
@@ -535,13 +504,19 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
|||||||
}
|
}
|
||||||
if req.WithFile {
|
if req.WithFile {
|
||||||
for _, item := range strings.Split(req.Path, ",") {
|
for _, item := range strings.Split(req.Path, ",") {
|
||||||
if len(item) != 0 {
|
item = strings.TrimSpace(item)
|
||||||
_ = os.RemoveAll(path.Dir(item))
|
if !filepath.IsAbs(item) || filepath.Dir(item) == "/" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info, err := os.Stat(item)
|
||||||
|
if err == nil && info.Mode().IsRegular() {
|
||||||
|
if err := os.RemoveAll(filepath.Dir(item)); err != nil && !req.Force {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
}
|
||||||
return nil
|
return composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
||||||
}
|
}
|
||||||
if req.Operation == "up" {
|
if req.Operation == "up" {
|
||||||
if stdout, err := compose.Up(req.Path, req.Name); err != nil {
|
if stdout, err := compose.Up(req.Path, req.Name); err != nil {
|
||||||
@@ -584,13 +559,13 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
|||||||
write.Flush()
|
write.Flush()
|
||||||
|
|
||||||
global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.DetailPath)
|
global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.DetailPath)
|
||||||
if err := newComposeEnv(req.DetailPath, req.Env); err != nil {
|
if err := newComposeEnv(primaryComposePath(req.Path), req.Env); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name); err != nil {
|
if err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name); err != nil {
|
||||||
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
|
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
|
||||||
if err := recreateCompose(string(oldFile), req.Path, req.Name); err != nil {
|
if err := recreateCompose(string(oldFile), req.Path, req.DetailPath, req.Name); err != nil {
|
||||||
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
|
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("update failed when handle compose up, err: %s", err)
|
return fmt.Errorf("update failed when handle compose up, err: %s", err)
|
||||||
@@ -702,9 +677,18 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func removeContainerForCompose(composeName, composePath string) error {
|
func removeContainerForCompose(composeName, composePath string) error {
|
||||||
if _, err := os.Stat(composePath); err == nil {
|
composeFiles, err := resolveComposeFiles("", composePath)
|
||||||
if stdout, err := compose.Operate(composePath, "down", composeName); err != nil {
|
filesAvailable := err == nil
|
||||||
return errors.New(stdout)
|
for _, file := range composeFiles {
|
||||||
|
info, err := os.Stat(file)
|
||||||
|
if err != nil || !info.Mode().IsRegular() {
|
||||||
|
filesAvailable = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if filesAvailable {
|
||||||
|
if stdout, err := compose.Operate(strings.Join(composeFiles, ","), "down", composeName); err != nil {
|
||||||
|
return fmt.Errorf("docker-compose down failed, std: %s, err: %w", stdout, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -722,14 +706,17 @@ func removeContainerForCompose(composeName, composePath string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
var removeErrors []error
|
||||||
for _, c := range containers {
|
for _, c := range containers {
|
||||||
_ = client.ContainerRemove(ctx, c.ID, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
if err := client.ContainerRemove(ctx, c.ID, container.RemoveOptions{RemoveVolumes: true, Force: true}); err != nil {
|
||||||
|
removeErrors = append(removeErrors, fmt.Errorf("remove container %s failed: %w", c.ID, err))
|
||||||
}
|
}
|
||||||
return nil
|
}
|
||||||
|
return errors.Join(removeErrors...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func recreateCompose(content, path, projectName string) error {
|
func recreateCompose(content, composePath, detailPath, projectName string) error {
|
||||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
|
file, err := os.OpenFile(detailPath, os.O_WRONLY|os.O_TRUNC, 0640)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -738,7 +725,7 @@ func recreateCompose(content, path, projectName string) error {
|
|||||||
_, _ = write.WriteString(content)
|
_, _ = write.WriteString(content)
|
||||||
write.Flush()
|
write.Flush()
|
||||||
|
|
||||||
if stdout, err := compose.Up(path, projectName); err != nil {
|
if stdout, err := compose.Up(composePath, projectName); err != nil {
|
||||||
return errors.New(string(stdout))
|
return errors.New(string(stdout))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -746,6 +733,9 @@ func recreateCompose(content, path, projectName string) error {
|
|||||||
|
|
||||||
func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo {
|
func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo {
|
||||||
for i := 0; i < len(list); i++ {
|
for i := 0; i < len(list); i++ {
|
||||||
|
if list[i].Path == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
tmpPath := list[i].Path
|
tmpPath := list[i].Path
|
||||||
if strings.Contains(list[i].Path, ",") {
|
if strings.Contains(list[i].Path, ",") {
|
||||||
tmpPath = strings.Split(list[i].Path, ",")[0]
|
tmpPath = strings.Split(list[i].Path, ",")[0]
|
||||||
@@ -774,3 +764,35 @@ func newComposeEnv(pathItem string, env string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveComposeFiles(workdir, configFile string) ([]string, error) {
|
||||||
|
workdir = strings.TrimSpace(workdir)
|
||||||
|
var files []string
|
||||||
|
for _, item := range strings.Split(configFile, ",") {
|
||||||
|
file := strings.TrimSpace(item)
|
||||||
|
if file == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(file) {
|
||||||
|
if !filepath.IsAbs(workdir) {
|
||||||
|
return nil, fmt.Errorf("cannot resolve compose file %s without an absolute working directory", file)
|
||||||
|
}
|
||||||
|
file = filepath.Join(workdir, file)
|
||||||
|
}
|
||||||
|
files = append(files, filepath.Clean(file))
|
||||||
|
}
|
||||||
|
if len(files) == 0 {
|
||||||
|
return nil, fmt.Errorf("compose config file is not specified")
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func composeFileExists(files []string) bool {
|
||||||
|
for _, file := range files {
|
||||||
|
info, err := os.Stat(file)
|
||||||
|
if err == nil && info.Mode().IsRegular() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,16 +2,24 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var networkCleanupSlot = make(chan struct{}, 1)
|
||||||
|
|
||||||
func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) {
|
func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) {
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -172,3 +180,114 @@ func (u *ContainerService) CreateNetwork(req dto.NetworkCreate) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cleanUnusedNetworks(t *task.Task, cli *client.Client) error {
|
||||||
|
ctx := t.TaskCtx
|
||||||
|
select {
|
||||||
|
case networkCleanupSlot <- struct{}{}:
|
||||||
|
defer func() { <-networkCleanupSlot }()
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
networks, err := cli.NetworkList(ctx, network.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
deleted, skipped, failed := 0, 0, 0
|
||||||
|
defer func() {
|
||||||
|
t.Log(i18n.GetMsgWithMap("NetworkCleanupSummary", map[string]interface{}{
|
||||||
|
"deleted": deleted, "skipped": skipped, "failed": failed,
|
||||||
|
}))
|
||||||
|
}()
|
||||||
|
var used map[string]bool
|
||||||
|
sort.Slice(networks, func(i, j int) bool { return networks[i].Name < networks[j].Name })
|
||||||
|
for _, n := range networks {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch n.Name {
|
||||||
|
case "none", "host", "bridge", "1panel-network":
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if n.Scope != "local" || n.Ingress || n.ConfigOnly {
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values := map[string]interface{}{"name": n.Name, "id": n.ID}
|
||||||
|
if used == nil {
|
||||||
|
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
used = make(map[string]bool)
|
||||||
|
for _, c := range containers {
|
||||||
|
if c.NetworkSettings == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for name, endpoint := range c.NetworkSettings.Networks {
|
||||||
|
used[name] = true
|
||||||
|
if endpoint != nil {
|
||||||
|
used[endpoint.NetworkID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if used[n.Name] || used[n.ID] {
|
||||||
|
skipped++
|
||||||
|
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
inspected, err := cli.NetworkInspect(ctx, n.ID, network.InspectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
if errdefs.IsNotFound(err) {
|
||||||
|
skipped++
|
||||||
|
} else {
|
||||||
|
failed++
|
||||||
|
t.Logf("Failed to inspect network [%s] (%s): %v", n.Name, n.ID, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(inspected.Containers) > 0 {
|
||||||
|
skipped++
|
||||||
|
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := cli.NetworkRemove(ctx, n.ID); err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case errdefs.IsNotFound(err):
|
||||||
|
skipped++
|
||||||
|
case errdefs.IsConflict(err):
|
||||||
|
skipped++
|
||||||
|
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
|
||||||
|
default:
|
||||||
|
failed++
|
||||||
|
t.Logf("Failed to remove network [%s] (%s): %v", n.Name, n.ID, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
deleted++
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if failed > 0 {
|
||||||
|
return errors.New(i18n.GetMsgByKey("NetworkCleanupPartialFailure"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ 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"
|
||||||
|
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
"github.com/jinzhu/copier"
|
"github.com/jinzhu/copier"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -75,6 +76,7 @@ func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interfac
|
|||||||
EntryID: cronjob.ID,
|
EntryID: cronjob.ID,
|
||||||
}
|
}
|
||||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable))
|
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable))
|
||||||
|
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
|
||||||
if alertInfo.SendCount != 0 {
|
if alertInfo.SendCount != 0 {
|
||||||
item.AlertCount = alertInfo.SendCount
|
item.AlertCount = alertInfo.SendCount
|
||||||
} else {
|
} else {
|
||||||
@@ -98,9 +100,11 @@ func (u *CronjobService) LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, err
|
|||||||
AlertType: cronjob.Type,
|
AlertType: cronjob.Type,
|
||||||
EntryID: cronjob.ID,
|
EntryID: cronjob.ID,
|
||||||
}
|
}
|
||||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable))
|
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))))
|
||||||
item.AlertMethod = alertInfo.Method
|
item.AlertMethod = alertInfo.Method
|
||||||
if alertInfo.SendCount != 0 {
|
item.AlertTitle = alertInfo.Title
|
||||||
|
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
|
||||||
|
if alertInfo.Status == constant.AlertEnable {
|
||||||
item.AlertCount = alertInfo.SendCount
|
item.AlertCount = alertInfo.SendCount
|
||||||
} else {
|
} else {
|
||||||
item.AlertCount = 0
|
item.AlertCount = 0
|
||||||
@@ -195,11 +199,12 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
item.SourceAccounts, item.DownloadAccount, _ = loadBackupNamesByID(cronjob.SourceAccountIDs, cronjob.DownloadAccountID)
|
item.SourceAccounts, item.DownloadAccount, _ = loadBackupNamesByID(cronjob.SourceAccountIDs, cronjob.DownloadAccountID)
|
||||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))), repo.WithByStatus(constant.AlertEnable))
|
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))))
|
||||||
if alertInfo.SendCount != 0 {
|
|
||||||
item.AlertCount = alertInfo.SendCount
|
|
||||||
item.AlertTitle = alertInfo.Title
|
item.AlertTitle = alertInfo.Title
|
||||||
item.AlertMethod = alertInfo.Method
|
item.AlertMethod = alertInfo.Method
|
||||||
|
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
|
||||||
|
if alertInfo.Status == constant.AlertEnable {
|
||||||
|
item.AlertCount = alertInfo.SendCount
|
||||||
} else {
|
} else {
|
||||||
item.AlertCount = 0
|
item.AlertCount = 0
|
||||||
}
|
}
|
||||||
@@ -213,6 +218,17 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
|
func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
|
||||||
|
for _, item := range req {
|
||||||
|
advanced, err := cronJobAlertAdvancedParams(item.AlertTriggerMode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if item.AlertCount != 0 {
|
||||||
|
if err := (AlertService{}).validateCronJobAlertChannels(item.Type, advanced, item.AlertMethod); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, item := range req {
|
for _, item := range req {
|
||||||
cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name))
|
cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name))
|
||||||
if cronjobItem.ID != 0 {
|
if cronjobItem.ID != 0 {
|
||||||
@@ -395,17 +411,27 @@ func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
|
|||||||
} else {
|
} else {
|
||||||
cronjob.Status = constant.StatusDisable
|
cronjob.Status = constant.StatusDisable
|
||||||
}
|
}
|
||||||
_ = cronjobRepo.Create(&cronjob)
|
if err := cronjobRepo.Create(&cronjob); err != nil {
|
||||||
if item.AlertCount != 0 && item.AlertTitle != "" && item.AlertMethod != "" {
|
return err
|
||||||
|
}
|
||||||
|
if item.AlertTitle != "" && item.AlertMethod != "" {
|
||||||
|
advanced, _ := cronJobAlertAdvancedParams(item.AlertTriggerMode)
|
||||||
|
status := constant.AlertEnable
|
||||||
|
if item.AlertCount == 0 {
|
||||||
|
status = constant.AlertDisable
|
||||||
|
}
|
||||||
createAlert := dto.AlertCreate{
|
createAlert := dto.AlertCreate{
|
||||||
Title: item.AlertTitle,
|
Title: item.AlertTitle,
|
||||||
SendCount: item.AlertCount,
|
SendCount: item.AlertCount,
|
||||||
Method: item.AlertMethod,
|
Method: item.AlertMethod,
|
||||||
Type: cronjob.Type,
|
Type: cronjob.Type,
|
||||||
Project: strconv.Itoa(int(cronjob.ID)),
|
Project: strconv.Itoa(int(cronjob.ID)),
|
||||||
Status: constant.AlertEnable,
|
Status: status,
|
||||||
|
AdvancedParams: advanced,
|
||||||
|
}
|
||||||
|
if err := NewIAlertService().CreateAlert(createAlert, operator); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
_ = NewIAlertService().CreateAlert(createAlert, operator)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -562,6 +588,15 @@ func (u *CronjobService) HandleOnce(id uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
|
func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
|
||||||
|
advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if req.AlertCount != 0 {
|
||||||
|
if err := (AlertService{}).validateCronJobAlertChannels(req.Type, advanced, req.AlertMethod); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
cronjob, _ := cronjobRepo.Get(repo.WithByName(req.Name))
|
cronjob, _ := cronjobRepo.Get(repo.WithByName(req.Name))
|
||||||
if cronjob.ID != 0 {
|
if cronjob.ID != 0 {
|
||||||
return buserr.New("ErrRecordExist")
|
return buserr.New("ErrRecordExist")
|
||||||
@@ -609,6 +644,7 @@ func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
|
|||||||
Type: cronjob.Type,
|
Type: cronjob.Type,
|
||||||
Project: strconv.Itoa(int(cronjob.ID)),
|
Project: strconv.Itoa(int(cronjob.ID)),
|
||||||
Status: constant.AlertEnable,
|
Status: constant.AlertEnable,
|
||||||
|
AdvancedParams: advanced,
|
||||||
}
|
}
|
||||||
err := NewIAlertService().CreateAlert(createAlert, operator)
|
err := NewIAlertService().CreateAlert(createAlert, operator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -682,6 +718,10 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string) error {
|
func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string) error {
|
||||||
|
advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
var cronjob model.Cronjob
|
var cronjob model.Cronjob
|
||||||
if err := copier.Copy(&cronjob, &req); err != nil {
|
if err := copier.Copy(&cronjob, &req); err != nil {
|
||||||
return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
@@ -697,6 +737,20 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return buserr.New("ErrRecordNotFound")
|
return buserr.New("ErrRecordNotFound")
|
||||||
}
|
}
|
||||||
|
if req.AlertCount != 0 {
|
||||||
|
previous, _ := alertRepo.Get(alertRepo.WithByType(cronModel.Type), alertRepo.WithByProject(strconv.Itoa(int(id))))
|
||||||
|
merged, err := prepareCronJobAlertParams(cronModel.Type, previous.AdvancedParams, advanced)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
method := req.AlertMethod
|
||||||
|
if method == "" {
|
||||||
|
method = previous.Method
|
||||||
|
}
|
||||||
|
if err := (AlertService{}).validateCronJobAlertChannels(cronModel.Type, merged, method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
upMap := make(map[string]interface{})
|
upMap := make(map[string]interface{})
|
||||||
cronjob.EntryIDs = cronModel.EntryIDs
|
cronjob.EntryIDs = cronModel.EntryIDs
|
||||||
cronjob.Type = cronModel.Type
|
cronjob.Type = cronModel.Type
|
||||||
@@ -758,6 +812,7 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
|
|||||||
Method: req.AlertMethod,
|
Method: req.AlertMethod,
|
||||||
Type: cronjob.Type,
|
Type: cronjob.Type,
|
||||||
Project: strconv.Itoa(int(cronModel.ID)),
|
Project: strconv.Itoa(int(cronModel.ID)),
|
||||||
|
AdvancedParams: advanced,
|
||||||
}
|
}
|
||||||
err = NewIAlertService().ExternalUpdateAlert(updateAlert, operator)
|
err = NewIAlertService().ExternalUpdateAlert(updateAlert, operator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -766,6 +821,17 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cronJobAlertAdvancedParams(mode string) (string, error) {
|
||||||
|
if mode == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(map[string]string{"alertTriggerMode": mode})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return alertUtil.MergeCronJobAlertParams("", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
func (u *CronjobService) UpdateStatus(id uint, status string) error {
|
func (u *CronjobService) UpdateStatus(id uint, status string) error {
|
||||||
cronjob, _ := cronjobRepo.Get(repo.WithByID(id))
|
cronjob, _ := cronjobRepo.Get(repo.WithByID(id))
|
||||||
if cronjob.ID == 0 {
|
if cronjob.ID == 0 {
|
||||||
|
|||||||
@@ -412,6 +412,7 @@ func addSkipTask(source string, taskItem *task.Task) {
|
|||||||
taskItem.Log(i18n.GetMsgByKey("NoSuchResource"))
|
taskItem.Log(i18n.GetMsgByKey("NoSuchResource"))
|
||||||
return nil
|
return nil
|
||||||
}, nil)
|
}, nil)
|
||||||
|
taskItem.SubTasks[len(taskItem.SubTasks)-1].StepAlias = cronJobSkippedStep
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
|
func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -23,6 +24,7 @@ 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"
|
||||||
|
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/ntp"
|
"github.com/1Panel-dev/1Panel/agent/utils/ntp"
|
||||||
@@ -56,10 +58,11 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
|
|||||||
_ = taskRepo.Save(context.Background(), taskItem.Task)
|
_ = taskRepo.Save(context.Background(), taskItem.Task)
|
||||||
}
|
}
|
||||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||||
handleCronJobAlert(cronjob)
|
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
|
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
|
||||||
|
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil))
|
||||||
}()
|
}()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -70,19 +73,20 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
|
|||||||
record.TaskID = ""
|
record.TaskID = ""
|
||||||
}
|
}
|
||||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||||
handleCronJobAlert(cronjob)
|
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
if err := taskItem.Execute(); err != nil {
|
if err := taskItem.Execute(); err != nil {
|
||||||
taskItem, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
|
storedTask, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
|
||||||
if len(taskItem.ID) == 0 {
|
if len(storedTask.ID) == 0 {
|
||||||
record.TaskID = ""
|
record.TaskID = ""
|
||||||
}
|
}
|
||||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||||
handleCronJobAlert(cronjob)
|
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
|
||||||
} else {
|
} else {
|
||||||
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
|
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
|
||||||
|
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -482,8 +486,33 @@ func hasBackup(cronjobType string) bool {
|
|||||||
return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log" || cronjobType == "cutWebsiteLog"
|
return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log" || cronjobType == "cutWebsiteLog"
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCronJobAlert(cronjob *model.Cronjob) {
|
const cronJobSkippedStep = "cronjob-skipped"
|
||||||
|
|
||||||
|
func cronJobAlertResult(taskItem *task.Task, err error) string {
|
||||||
|
if errors.Is(err, context.Canceled) || taskItem.Task.Status == constant.StatusCanceled ||
|
||||||
|
(taskItem.TaskCtx != nil && taskItem.TaskCtx.Err() != nil) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return alertUtil.CronJobAlertFailed
|
||||||
|
}
|
||||||
|
if taskItem.Task.Status != constant.StatusSuccess {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, subTask := range taskItem.SubTasks {
|
||||||
|
if subTask.StepAlias != cronJobSkippedStep {
|
||||||
|
return alertUtil.CronJobAlertSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCronJobAlert(cronjob *model.Cronjob, result string) {
|
||||||
|
if result == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
pushAlert := dto.PushAlert{
|
pushAlert := dto.PushAlert{
|
||||||
|
Result: result,
|
||||||
TaskName: cronjob.Name,
|
TaskName: cronjob.Name,
|
||||||
AlertType: cronjob.Type,
|
AlertType: cronjob.Type,
|
||||||
EntryID: cronjob.ID,
|
EntryID: cronjob.ID,
|
||||||
|
|||||||
+102
-58
@@ -43,6 +43,10 @@ type IDashboardService interface {
|
|||||||
|
|
||||||
LoadQuickOptions() []dto.QuickJump
|
LoadQuickOptions() []dto.QuickJump
|
||||||
ChangeQuick(req dto.ChangeQuicks) error
|
ChangeQuick(req dto.ChangeQuicks) error
|
||||||
|
ListQuickDirectories() ([]model.QuickDirectory, error)
|
||||||
|
CreateQuickDirectory(req dto.QuickDirectoryCreate) (*model.QuickDirectory, error)
|
||||||
|
UpdateQuickDirectory(req dto.QuickDirectoryUpdate) (*model.QuickDirectory, error)
|
||||||
|
DeleteQuickDirectory(req dto.QuickDirectoryDelete) error
|
||||||
|
|
||||||
LoadAppLauncher(ctx *gin.Context) ([]dto.AppLauncher, error)
|
LoadAppLauncher(ctx *gin.Context) ([]dto.AppLauncher, error)
|
||||||
ChangeShow(req dto.SettingUpdate) error
|
ChangeShow(req dto.SettingUpdate) error
|
||||||
@@ -242,7 +246,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
|
|||||||
currentInfo.SwapMemoryUsed = swapInfo.Used
|
currentInfo.SwapMemoryUsed = swapInfo.Used
|
||||||
currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent
|
currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent
|
||||||
|
|
||||||
currentInfo.DiskData = loadDiskInfo()
|
currentInfo.DiskData = loadDiskInfo(false)
|
||||||
currentInfo.GPUData, currentInfo.NPUData, currentInfo.XPUData = loadAcceleratorInfo()
|
currentInfo.GPUData, currentInfo.NPUData, currentInfo.XPUData = loadAcceleratorInfo()
|
||||||
|
|
||||||
if ioOption == "all" {
|
if ioOption == "all" {
|
||||||
@@ -397,16 +401,15 @@ func (u *DashboardService) LoadQuickOptions() []dto.QuickJump {
|
|||||||
}
|
}
|
||||||
func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
|
func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
|
||||||
showCount := 0
|
showCount := 0
|
||||||
var quicks []model.QuickJump
|
quicks := make([]model.QuickJump, 0, len(req.Quicks))
|
||||||
for _, item := range req.Quicks {
|
for _, item := range req.Quicks {
|
||||||
var quick model.QuickJump
|
|
||||||
if item.IsShow {
|
if item.IsShow {
|
||||||
showCount++
|
showCount++
|
||||||
}
|
}
|
||||||
if err := copier.Copy(&quick, &item); err != nil {
|
quicks = append(quicks, model.QuickJump{
|
||||||
return err
|
BaseModel: model.BaseModel{ID: item.ID},
|
||||||
}
|
IsShow: item.IsShow,
|
||||||
quicks = append(quicks, quick)
|
})
|
||||||
}
|
}
|
||||||
if showCount == 0 {
|
if showCount == 0 {
|
||||||
return buserr.New("ErrMinQuickJump")
|
return buserr.New("ErrMinQuickJump")
|
||||||
@@ -455,24 +458,9 @@ type diskInfo struct {
|
|||||||
Device string
|
Device string
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadDiskInfo() []dto.DiskInfo {
|
func loadDiskInfo(forceRefresh bool) []dto.DiskInfo {
|
||||||
var datas []dto.DiskInfo
|
var datas []dto.DiskInfo
|
||||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
stdout := loadDiskMounts()
|
||||||
format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
|
|
||||||
stdout, err := cmdMgr.RunPipe(
|
|
||||||
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
|
|
||||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
|
||||||
stdout, err = cmdMgr2.RunPipe(
|
|
||||||
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
|
||||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return datas
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lines := strings.Split(stdout, "\n")
|
lines := strings.Split(stdout, "\n")
|
||||||
|
|
||||||
var mounts []diskInfo
|
var mounts []diskInfo
|
||||||
@@ -520,43 +508,22 @@ func loadDiskInfo() []dto.DiskInfo {
|
|||||||
itemData.Type = mount.Type
|
itemData.Type = mount.Type
|
||||||
itemData.Device = mount.Device
|
itemData.Device = mount.Device
|
||||||
|
|
||||||
type diskResult struct {
|
state, err := loadDiskUsageWithTimeout(mount.Mount, forceRefresh)
|
||||||
state *disk.UsageStat
|
if err != nil {
|
||||||
err error
|
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
|
||||||
|
} else {
|
||||||
|
itemData.Total = state.Total
|
||||||
|
itemData.Free = state.Free
|
||||||
|
itemData.Used = state.Used
|
||||||
|
itemData.UsedPercent = state.UsedPercent
|
||||||
|
itemData.InodesTotal = state.InodesTotal
|
||||||
|
itemData.InodesUsed = state.InodesUsed
|
||||||
|
itemData.InodesFree = state.InodesFree
|
||||||
|
itemData.InodesUsedPercent = state.InodesUsedPercent
|
||||||
}
|
}
|
||||||
resultCh := make(chan diskResult, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
state, err := psutil.DISK.GetUsage(mount.Mount, false)
|
|
||||||
resultCh <- diskResult{state: state, err: err}
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
datas = append(datas, itemData)
|
datas = append(datas, itemData)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
|
|
||||||
case result := <-resultCh:
|
|
||||||
if result.err != nil {
|
|
||||||
mu.Lock()
|
|
||||||
datas = append(datas, itemData)
|
|
||||||
mu.Unlock()
|
|
||||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, result.err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
itemData.Total = result.state.Total
|
|
||||||
itemData.Free = result.state.Free
|
|
||||||
itemData.Used = result.state.Used
|
|
||||||
itemData.UsedPercent = result.state.UsedPercent
|
|
||||||
itemData.InodesTotal = result.state.InodesTotal
|
|
||||||
itemData.InodesUsed = result.state.InodesUsed
|
|
||||||
itemData.InodesFree = result.state.InodesFree
|
|
||||||
itemData.InodesUsedPercent = result.state.InodesUsedPercent
|
|
||||||
mu.Lock()
|
|
||||||
datas = append(datas, itemData)
|
|
||||||
mu.Unlock()
|
|
||||||
}
|
|
||||||
}(mounts[i])
|
}(mounts[i])
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -567,6 +534,69 @@ func loadDiskInfo() []dto.DiskInfo {
|
|||||||
return datas
|
return datas
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var diskMountsMu sync.Mutex
|
||||||
|
|
||||||
|
func loadDiskMounts() string {
|
||||||
|
if !diskMountsMu.TryLock() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
resultCh := make(chan string, 1)
|
||||||
|
go func() {
|
||||||
|
var stdout string
|
||||||
|
defer func() {
|
||||||
|
diskMountsMu.Unlock()
|
||||||
|
resultCh <- stdout
|
||||||
|
}()
|
||||||
|
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||||
|
format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
|
||||||
|
output, err := cmdMgr.RunPipe(
|
||||||
|
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
|
||||||
|
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("load disk info with df -hT -P failed, err: %v", err)
|
||||||
|
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||||
|
output, err = cmdMgr2.RunPipe(
|
||||||
|
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
||||||
|
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("load disk info with df -lhT -P failed, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout = output
|
||||||
|
}()
|
||||||
|
timer := time.NewTimer(3 * time.Second)
|
||||||
|
defer timer.Stop()
|
||||||
|
select {
|
||||||
|
case stdout := <-resultCh:
|
||||||
|
return stdout
|
||||||
|
case <-timer.C:
|
||||||
|
global.LOG.Error("load disk mounts timed out; df collection is still running")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadDiskUsageWithTimeout(path string, forceRefresh bool) (*disk.UsageStat, error) {
|
||||||
|
type diskResult struct {
|
||||||
|
state *disk.UsageStat
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
resultCh := make(chan diskResult, 1)
|
||||||
|
go func() {
|
||||||
|
state, err := psutil.DISK.GetUsage(path, forceRefresh)
|
||||||
|
resultCh <- diskResult{state: state, err: err}
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
return nil, fmt.Errorf("load disk usage from %s: timeout", path)
|
||||||
|
case result := <-resultCh:
|
||||||
|
return result.state, result.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
||||||
ok, client := accelerator.New()
|
ok, client := accelerator.New()
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -586,6 +616,9 @@ func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
|||||||
xpuData []dto.XPUInfo
|
xpuData []dto.XPUInfo
|
||||||
)
|
)
|
||||||
for _, device := range snapshot.Devices {
|
for _, device := range snapshot.Devices {
|
||||||
|
if device.ParentID != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
switch device.Kind {
|
switch device.Kind {
|
||||||
case accelerator.KindGPU:
|
case accelerator.KindGPU:
|
||||||
if device.GPU == nil {
|
if device.GPU == nil {
|
||||||
@@ -595,7 +628,11 @@ func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
|||||||
if err := copier.Copy(&dataItem, device.GPU); err != nil {
|
if err := copier.Copy(&dataItem, device.GPU); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dataItem.PowerUsage = dataItem.PowerDraw + " / " + dataItem.MaxPowerLimit
|
dataItem.MaxPowerLimit = device.GPU.PowerLimit
|
||||||
|
dataItem.PowerUsage = dataItem.PowerDraw
|
||||||
|
if dataItem.MaxPowerLimit != "" {
|
||||||
|
dataItem.PowerUsage += " / " + dataItem.MaxPowerLimit
|
||||||
|
}
|
||||||
dataItem.MemoryUsage = dataItem.MemUsed + " / " + dataItem.MemTotal
|
dataItem.MemoryUsage = dataItem.MemUsed + " / " + dataItem.MemTotal
|
||||||
gpuData = append(gpuData, dataItem)
|
gpuData = append(gpuData, dataItem)
|
||||||
case accelerator.KindNPU:
|
case accelerator.KindNPU:
|
||||||
@@ -672,6 +709,13 @@ func loadQuickJump(base *dto.DashboardBase) {
|
|||||||
quicks := launcherRepo.ListQuickJump(false)
|
quicks := launcherRepo.ListQuickJump(false)
|
||||||
for i := 0; i < len(quicks); i++ {
|
for i := 0; i < len(quicks); i++ {
|
||||||
switch quicks[i].Name {
|
switch quicks[i].Name {
|
||||||
|
case "File":
|
||||||
|
count, err := launcherRepo.CountQuickDirectories()
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("load quick directory count: %v", err)
|
||||||
|
}
|
||||||
|
quicks[i].Detail = fmt.Sprintf("%d", count)
|
||||||
|
quicks[i].Alias = ""
|
||||||
case "Agent":
|
case "Agent":
|
||||||
quicks[i].Detail = fmt.Sprintf("%d", base.AgentNumber)
|
quicks[i].Detail = fmt.Sprintf("%d", base.AgentNumber)
|
||||||
case "Website":
|
case "Website":
|
||||||
|
|||||||
@@ -433,6 +433,10 @@ func (u *MysqlService) ListDBOption() ([]dto.MysqlOption, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) {
|
func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) {
|
||||||
|
return u.create(ctx, req, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *MysqlService) create(ctx context.Context, req dto.MysqlDBCreate, mcpBounded bool) (*model.DatabaseMysql, error) {
|
||||||
if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format, req.Collation, req.Permission) {
|
if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format, req.Collation, req.Permission) {
|
||||||
return nil, buserr.New("ErrCmdIllegal")
|
return nil, buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
@@ -470,13 +474,13 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
|
|||||||
return nil, errors.New("cannot set root as user name")
|
return nil, errors.New("cannot set root as user name")
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, version, err := LoadMysqlClientByFrom(req.Database)
|
cli, version, err := loadMysqlClientByFrom(req.Database, mcpBounded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
createItem.MysqlName = req.Database
|
createItem.MysqlName = req.Database
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
if err := cli.Create(client.CreateInfo{
|
createInfo := client.CreateInfo{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Format: req.Format,
|
Format: req.Format,
|
||||||
Collation: req.Collation,
|
Collation: req.Collation,
|
||||||
@@ -485,9 +489,31 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
|
|||||||
Permission: req.Permission,
|
Permission: req.Permission,
|
||||||
Version: version,
|
Version: version,
|
||||||
Timeout: 300,
|
Timeout: 300,
|
||||||
}); err != nil {
|
}
|
||||||
|
if mcpBounded {
|
||||||
|
local, ok := cli.(*client.Local)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("an installed local MySQL instance is required")
|
||||||
|
}
|
||||||
|
deadline := time.Now().Add(300 * time.Second)
|
||||||
|
if writeDeadline, ok := ctx.Deadline(); ok && writeDeadline.Add(-10*time.Second).Before(deadline) {
|
||||||
|
deadline = writeDeadline.Add(-10 * time.Second)
|
||||||
|
}
|
||||||
|
sqlContext, cancel := context.WithDeadline(ctx, deadline)
|
||||||
|
err = local.CreateWithContext(sqlContext, createInfo)
|
||||||
|
cancel()
|
||||||
|
} else {
|
||||||
|
err = cli.Create(createInfo)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if mcpBounded {
|
||||||
|
if err := persistMCPMysqlMetadata(ctx, req, permissionHosts, &createItem); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &createItem, nil
|
||||||
|
}
|
||||||
if len(req.Username) != 0 {
|
if len(req.Username) != 0 {
|
||||||
if err := saveDatabaseUserCredentials(dbType, req.Database, req.Username, req.Permission, req.Password, req.Description); err != nil {
|
if err := saveDatabaseUserCredentials(dbType, req.Database, req.Username, req.Permission, req.Password, req.Description); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -506,6 +532,45 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
|
|||||||
return &createItem, nil
|
return &createItem, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func persistMCPMysqlMetadata(ctx context.Context, req dto.MysqlDBCreate, hosts []string, item *model.DatabaseMysql) error {
|
||||||
|
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if req.Username != "" {
|
||||||
|
key := global.CONF.Base.EncryptKey
|
||||||
|
if key == "" {
|
||||||
|
var setting model.Setting
|
||||||
|
if err := tx.Where("key = ?", "EncryptKey").First(&setting).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key = setting.Value
|
||||||
|
}
|
||||||
|
password, err := encrypt.StringEncryptWithKey(req.Password, key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, host := range hosts {
|
||||||
|
var user model.DatabaseUser
|
||||||
|
err := tx.Where("type = ? AND database = ? AND username = ? AND host = ?", "mysql", req.Database, req.Username, host).First(&user).Error
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.Type, user.Database, user.Username, user.Host = "mysql", req.Database, req.Username, host
|
||||||
|
user.Password, user.IsDelete = password, false
|
||||||
|
if req.Description != "" {
|
||||||
|
user.Description = req.Description
|
||||||
|
}
|
||||||
|
if err := tx.Save(&user).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
grant := model.DatabaseUserGrant{Type: "mysql", Database: req.Database, DBName: req.Name, Username: req.Username, Host: host}
|
||||||
|
if err := tx.Where("type = ? AND database = ? AND db_name = ? AND username = ? AND host = ?", grant.Type, grant.Database, grant.DBName, grant.Username, grant.Host).FirstOrCreate(&grant).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tx.Create(item).Error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *MysqlService) ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, error) {
|
func (u *MysqlService) ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, error) {
|
||||||
dbType, err := resolveDatabaseUserType(req.Database)
|
dbType, err := resolveDatabaseUserType(req.Database)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1398,7 +1463,14 @@ func (u *MysqlService) LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlS
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
statusMap, err := executeSqlForMaps(app.ContainerName, app.Key, app.Password, "show global status;")
|
return loadMysqlStatus(req, app, executeSqlForMaps, executeSqlForRows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMysqlStatus(req dto.OperationWithNameAndType, app *repo.RootInfo,
|
||||||
|
maps func(string, string, string, string) (map[string]string, error),
|
||||||
|
rowsQuery func(string, string, string, string) ([]string, error),
|
||||||
|
) (*dto.MysqlStatus, error) {
|
||||||
|
statusMap, err := maps(app.ContainerName, app.Key, app.Password, "show global status;")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1426,7 +1498,7 @@ func (u *MysqlService) LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlS
|
|||||||
if common.CompareAppVersion(app.Version, "8.4.0") && (req.Type == constant.AppMysql || req.Type == constant.AppMysqlCluster) {
|
if common.CompareAppVersion(app.Version, "8.4.0") && (req.Type == constant.AppMysql || req.Type == constant.AppMysqlCluster) {
|
||||||
masterStatus = "show binary log status;"
|
masterStatus = "show binary log status;"
|
||||||
}
|
}
|
||||||
rows, err := executeSqlForRows(app.ContainerName, app.Key, app.Password, masterStatus)
|
rows, err := rowsQuery(app.ContainerName, app.Key, app.Password, masterStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1531,6 +1603,10 @@ func updateMyCnf(oldFiles []string, group string, param string, value interface{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LoadMysqlClientByFrom(database string) (mysql.MysqlClient, string, error) {
|
func LoadMysqlClientByFrom(database string) (mysql.MysqlClient, string, error) {
|
||||||
|
return loadMysqlClientByFrom(database, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMysqlClientByFrom(database string, mcpLocalOnly bool) (mysql.MysqlClient, string, error) {
|
||||||
var (
|
var (
|
||||||
dbInfo client.DBInfo
|
dbInfo client.DBInfo
|
||||||
version string
|
version string
|
||||||
@@ -1542,6 +1618,9 @@ func LoadMysqlClientByFrom(database string) (mysql.MysqlClient, string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
if mcpLocalOnly && (databaseItem.ID == 0 || databaseItem.From != "local" || databaseItem.Type != "mysql" || databaseItem.AppInstallID == 0) {
|
||||||
|
return nil, "", errors.New("an installed local MySQL instance is required")
|
||||||
|
}
|
||||||
dbInfo.Type = databaseItem.Type
|
dbInfo.Type = databaseItem.Type
|
||||||
dbInfo.From = databaseItem.From
|
dbInfo.From = databaseItem.From
|
||||||
dbInfo.Database = database
|
dbInfo.Database = database
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (u *MysqlService) CreateForMCP(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) {
|
||||||
|
if req.From != "local" || ctx.Err() != nil {
|
||||||
|
return nil, errors.New("invalid MCP database target or request")
|
||||||
|
}
|
||||||
|
writeContext, cancel := context.WithTimeout(context.WithoutCancel(ctx), 310*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
created, err := u.create(writeContext, req, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, mcpapi.SanitizeBusinessError(err, "database creation failed; inspect the invocation before retrying")
|
||||||
|
}
|
||||||
|
return created, nil
|
||||||
|
}
|
||||||
@@ -8,6 +8,12 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
@@ -17,12 +23,15 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||||
"github.com/docker/docker/api/types/container"
|
dockerclient "github.com/docker/docker/client"
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RedisService struct{}
|
type RedisService struct{}
|
||||||
|
|
||||||
|
// The CLI container is shared by all remote Redis databases on this node.
|
||||||
|
var redisCliInstallMutex sync.Mutex
|
||||||
|
|
||||||
type IRedisService interface {
|
type IRedisService interface {
|
||||||
UpdateConf(req dto.RedisConfUpdate) error
|
UpdateConf(req dto.RedisConfUpdate) error
|
||||||
UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
|
UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
|
||||||
@@ -33,7 +42,7 @@ type IRedisService interface {
|
|||||||
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
|
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
|
||||||
|
|
||||||
CheckHasCli() bool
|
CheckHasCli() bool
|
||||||
InstallCli() error
|
InstallCli(req dto.RedisCliInstall) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIRedisService() IRedisService {
|
func NewIRedisService() IRedisService {
|
||||||
@@ -61,30 +70,65 @@ func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *RedisService) CheckHasCli() bool {
|
func (u *RedisService) CheckHasCli() bool {
|
||||||
client, err := docker.NewDockerClient()
|
installed, _ := u.checkCliInstalled()
|
||||||
if err != nil {
|
return installed
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
containerLists, err := client.ContainerList(context.Background(), container.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, item := range containerLists {
|
|
||||||
if strings.ReplaceAll(item.Names[0], "/", "") == "1Panel-redis-cli-tools" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *RedisService) InstallCli() error {
|
func (u *RedisService) checkCliInstalled() (bool, error) {
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
info, err := client.ContainerInspect(context.Background(), "1Panel-redis-cli-tools")
|
||||||
|
if dockerclient.IsErrNotFound(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return info.ContainerJSONBase != nil && info.State != nil && info.State.Running, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *RedisService) InstallCli(req dto.RedisCliInstall) (string, error) {
|
||||||
|
if !redisCliInstallMutex.TryLock() {
|
||||||
|
return "", buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
defer redisCliInstallMutex.Unlock()
|
||||||
item := dto.ContainerOperate{
|
item := dto.ContainerOperate{
|
||||||
|
TaskID: req.TaskID,
|
||||||
Name: "1Panel-redis-cli-tools",
|
Name: "1Panel-redis-cli-tools",
|
||||||
Image: "redis:7.4.4",
|
Image: "redis:7.4.4",
|
||||||
Networks: []dto.ContainerNetwork{{Network: "1panel-network"}},
|
Networks: []dto.ContainerNetwork{{Network: "1panel-network"}},
|
||||||
}
|
}
|
||||||
return NewIContainerService().ContainerCreate(item, false)
|
running, err := taskRepo.GetFirst(
|
||||||
|
repo.WithByName(task.GetTaskName(item.Name, task.TaskCreate, task.TaskScopeContainer)),
|
||||||
|
repo.WithByType(task.TaskScopeContainer),
|
||||||
|
taskRepo.WithByStatus(constant.StatusExecuting),
|
||||||
|
)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if running.ID != "" {
|
||||||
|
return running.ID, nil
|
||||||
|
}
|
||||||
|
installed, err := u.checkCliInstalled()
|
||||||
|
if err != nil || installed {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if item.TaskID == "" {
|
||||||
|
item.TaskID = uuid.NewString()
|
||||||
|
}
|
||||||
|
if _, err := taskRepo.GetFirst(taskRepo.WithByID(item.TaskID)); !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "", buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
if err := NewIContainerService().ContainerCreate(item, true); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return item.TaskID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
|
func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func (u *DeviceService) LoadBaseInfo() (dto.DeviceBaseInfo, error) {
|
|||||||
if baseInfo.SwapMemoryTotal != 0 {
|
if baseInfo.SwapMemoryTotal != 0 {
|
||||||
baseInfo.SwapDetails = loadSwap()
|
baseInfo.SwapDetails = loadSwap()
|
||||||
}
|
}
|
||||||
disks := loadDiskInfo()
|
disks := loadDiskInfo(false)
|
||||||
for _, item := range disks {
|
for _, item := range disks {
|
||||||
baseInfo.MaxSize += item.Free
|
baseInfo.MaxSize += item.Free
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import (
|
|||||||
"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"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
|
|
||||||
|
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||||
)
|
)
|
||||||
|
|
||||||
const dockerNftablesMinVersion = "29.0.0"
|
const dockerNftablesMinVersion = "29.0.0"
|
||||||
@@ -82,6 +84,11 @@ func (u *DockerService) UpdateFirewallBackend(backend string) error {
|
|||||||
if backend == constant.FirewallProviderNftables && !dockerNftablesSupported(version) {
|
if backend == constant.FirewallProviderNftables && !dockerNftablesSupported(version) {
|
||||||
return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion)
|
return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion)
|
||||||
}
|
}
|
||||||
|
if backend == constant.FirewallProviderNftables {
|
||||||
|
if err := dockerfirewall.CheckIPv4Forwarding(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
original, readErr := os.ReadFile(constant.DaemonJsonPath)
|
original, readErr := os.ReadFile(constant.DaemonJsonPath)
|
||||||
existed := readErr == nil
|
existed := readErr == nil
|
||||||
@@ -276,7 +283,8 @@ 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 {
|
backend, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||||
|
if !strings.EqualFold(strings.TrimSpace(backend), constant.FirewallProviderNftables) {
|
||||||
delete(daemonMap, "experimental")
|
delete(daemonMap, "experimental")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -479,6 +487,9 @@ func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
|
|||||||
if err := controller.Handle(req.Operation, service); err != nil {
|
if err := controller.Handle(req.Operation, service); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if req.Operation == "start" || req.Operation == "restart" {
|
||||||
|
return RestoreDockerPortGuard(context.Background())
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ var (
|
|||||||
ftpRepo = repo.NewIFtpRepo()
|
ftpRepo = repo.NewIFtpRepo()
|
||||||
clamRepo = repo.NewIClamRepo()
|
clamRepo = repo.NewIClamRepo()
|
||||||
monitorRepo = repo.NewIMonitorRepo()
|
monitorRepo = repo.NewIMonitorRepo()
|
||||||
|
vllmMonitorRepo = &repo.VLLMMonitorRepo{}
|
||||||
|
|
||||||
settingRepo = repo.NewISettingRepo()
|
settingRepo = repo.NewISettingRepo()
|
||||||
backupRepo = repo.NewIBackupRepo()
|
backupRepo = repo.NewIBackupRepo()
|
||||||
|
|||||||
+40
-10
@@ -439,13 +439,15 @@ func (f *FileService) Compress(c request.FileCompress) error {
|
|||||||
if err := preflightCompressTool(files.CompressType(c.Type)); err != nil {
|
if err := preflightCompressTool(files.CompressType(c.Type)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
taskItem, err := task.NewTask(c.Name, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
taskName := i18n.GetMsgWithMap("FileTaskCompress", map[string]interface{}{"dst": strconv.Quote(filepath.Join(c.Dst, c.Name))})
|
||||||
|
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
taskItem.AddSubTask(c.Name, func(t *task.Task) error {
|
taskItem.AddSubTask(taskName, func(t *task.Task) error {
|
||||||
t.LogStart(c.Name)
|
logFileTaskSources(t, c.Files)
|
||||||
|
t.Log(i18n.GetMsgWithMap("FileTaskFormat", map[string]interface{}{"format": strconv.Quote(c.Type)}))
|
||||||
compressType := files.CompressType(c.Type)
|
compressType := files.CompressType(c.Type)
|
||||||
dstFile := filepath.Join(c.Dst, c.Name)
|
dstFile := filepath.Join(c.Dst, c.Name)
|
||||||
success := false
|
success := false
|
||||||
@@ -516,13 +518,15 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
|
|||||||
if err := preflightDecompressTool(files.CompressType(c.Type)); err != nil {
|
if err := preflightDecompressTool(files.CompressType(c.Type)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
taskItem, err := task.NewTask(c.Path, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
taskName := i18n.GetMsgWithMap("FileTaskDecompress", map[string]interface{}{"dst": strconv.Quote(c.Dst)})
|
||||||
|
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
taskItem.AddSubTask(c.Path, func(t *task.Task) error {
|
taskItem.AddSubTask(taskName, func(t *task.Task) error {
|
||||||
t.LogStart(c.Path)
|
logFileTaskSources(t, []string{c.Path})
|
||||||
|
t.Log(i18n.GetMsgWithMap("FileTaskFormat", map[string]interface{}{"format": strconv.Quote(c.Type)}))
|
||||||
dstExisted := fo.Stat(c.Dst)
|
dstExisted := fo.Stat(c.Dst)
|
||||||
parentDir := filepath.Dir(c.Dst)
|
parentDir := filepath.Dir(c.Dst)
|
||||||
if !fo.Stat(parentDir) {
|
if !fo.Stat(parentDir) {
|
||||||
@@ -878,6 +882,16 @@ func (f *FileService) ChangeName(req request.FileRename) error {
|
|||||||
if hasInvalidFileName(req.NewName) {
|
if hasInvalidFileName(req.NewName) {
|
||||||
return buserr.New("ErrInvalidChar")
|
return buserr.New("ErrInvalidChar")
|
||||||
}
|
}
|
||||||
|
if req.OldName == req.NewName {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// os.Rename silently replaces an existing target on Linux, so reject the
|
||||||
|
// rename here to avoid losing the target file without confirmation.
|
||||||
|
if _, err := os.Lstat(req.NewName); err == nil {
|
||||||
|
return buserr.New("ErrFileIsExist")
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
fo := files.NewFileOp()
|
fo := files.NewFileOp()
|
||||||
content, mode, shouldRecordHistory := readEditableFileHistoryContent(req.OldName)
|
content, mode, shouldRecordHistory := readEditableFileHistoryContent(req.OldName)
|
||||||
if err := fo.Rename(req.OldName, req.NewName); err != nil {
|
if err := fo.Rename(req.OldName, req.NewName); err != nil {
|
||||||
@@ -896,6 +910,7 @@ 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()
|
||||||
@@ -913,6 +928,12 @@ func (f *FileService) Wget(w request.FileWget) (string, error) {
|
|||||||
return key, fo.DownloadFileWithProcess(w.Url, filepath.Join(w.Path, w.Name), key, options)
|
return key, fo.DownloadFileWithProcess(w.Url, filepath.Join(w.Path, w.Name), key, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func logFileTaskSources(t *task.Task, sources []string) {
|
||||||
|
for _, source := range sources {
|
||||||
|
t.Log(i18n.GetMsgWithMap("FileTaskSource", map[string]interface{}{"path": strconv.Quote(source)}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (f *FileService) MvFile(m request.FileMove) error {
|
func (f *FileService) MvFile(m request.FileMove) error {
|
||||||
fo := files.NewFileOp()
|
fo := files.NewFileOp()
|
||||||
if err := validateFileMove(fo, m); err != nil {
|
if err := validateFileMove(fo, m); err != nil {
|
||||||
@@ -924,15 +945,24 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
|||||||
if !fileTransferLocks.Acquire(m.TaskID, getFileTransferPaths(m)) {
|
if !fileTransferLocks.Acquire(m.TaskID, getFileTransferPaths(m)) {
|
||||||
return buserr.New("TaskIsExecuting")
|
return buserr.New("TaskIsExecuting")
|
||||||
}
|
}
|
||||||
taskItem, err := task.NewTask(m.NewPath, task.TaskExec, task.TaskScopeTask, m.TaskID, 1)
|
nameKey := "FileTaskCopy"
|
||||||
|
if m.Type == "cut" {
|
||||||
|
nameKey = "FileTaskMove"
|
||||||
|
}
|
||||||
|
taskName := i18n.GetMsgWithMap(nameKey, map[string]interface{}{"dst": strconv.Quote(m.NewPath)})
|
||||||
|
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, m.TaskID, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fileTransferLocks.Release(m.TaskID)
|
fileTransferLocks.Release(m.TaskID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
defer fileTransferLocks.Release(m.TaskID)
|
defer fileTransferLocks.Release(m.TaskID)
|
||||||
taskItem.AddSubTaskWithOps(m.NewPath, func(t *task.Task) error {
|
taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error {
|
||||||
t.LogStart(m.NewPath)
|
logFileTaskSources(t, m.OldPaths)
|
||||||
|
logFileTaskSources(t, m.CoverPaths)
|
||||||
|
if m.Name != "" {
|
||||||
|
t.Log(i18n.GetMsgWithMap("FileTaskRename", map[string]interface{}{"name": strconv.Quote(m.Name)}))
|
||||||
|
}
|
||||||
err := f.moveFileWithContext(t.TaskCtx, m)
|
err := f.moveFileWithContext(t.TaskCtx, m)
|
||||||
if err != nil && t.TaskCtx.Err() != nil {
|
if err != nil && t.TaskCtx.Err() != nil {
|
||||||
return t.TaskCtx.Err()
|
return t.TaskCtx.Err()
|
||||||
@@ -1290,7 +1320,7 @@ func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.Exi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileService) GetHostMount() []dto.DiskInfo {
|
func (f *FileService) GetHostMount() []dto.DiskInfo {
|
||||||
return loadDiskInfo()
|
return loadDiskInfo(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileService) GetUsersAndGroups() (*response.UserGroupResponse, error) {
|
func (f *FileService) GetUsersAndGroups() (*response.UserGroupResponse, error) {
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestChangeNameRejectsExistingTarget(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
oldPath := filepath.Join(dir, "old.txt")
|
||||||
|
targetPath := filepath.Join(dir, "target.txt")
|
||||||
|
// Binary source content keeps this test independent of the file-history DB.
|
||||||
|
if err := os.WriteFile(oldPath, []byte("old\x00"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(targetPath, []byte("target"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := (&FileService{}).ChangeName(request.FileRename{OldName: oldPath, NewName: targetPath})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error when the target name already exists")
|
||||||
|
}
|
||||||
|
var bizErr buserr.BusinessError
|
||||||
|
if !errors.As(err, &bizErr) || bizErr.Msg != "ErrFileIsExist" {
|
||||||
|
t.Fatalf("expected ErrFileIsExist, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(targetPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(content) != "target" {
|
||||||
|
t.Fatalf("target file was overwritten, content = %q", content)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(oldPath); err != nil {
|
||||||
|
t.Fatalf("source file should still exist after a rejected rename: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChangeNameRenamesToNewName(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
oldPath := filepath.Join(dir, "old")
|
||||||
|
newPath := filepath.Join(dir, "new")
|
||||||
|
if err := os.Mkdir(oldPath, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := (&FileService{}).ChangeName(request.FileRename{OldName: oldPath, NewName: newPath}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(newPath); err != nil {
|
||||||
|
t.Fatalf("renamed path does not exist: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(oldPath); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("old path should be gone, stat err = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+948
-2581
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,144 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
|
||||||
)
|
|
||||||
|
|
||||||
type panelPortWhitelistKey struct{}
|
|
||||||
|
|
||||||
func (s *FirewallService) UpdatePanelPort(ctx context.Context, oldPort, port uint) error {
|
|
||||||
if oldPort == 0 || oldPort > 65535 || port == 0 || port > 65535 {
|
|
||||||
return fmt.Errorf("invalid panel port transition %d -> %d", oldPort, port)
|
|
||||||
}
|
|
||||||
if LoadPanelPort() != strconv.Itoa(int(oldPort)) {
|
|
||||||
return fmt.Errorf("panel port changed before firewall update")
|
|
||||||
}
|
|
||||||
if oldPort == port {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
client, err := s.baseClient()
|
|
||||||
if err != nil {
|
|
||||||
if configuredSystemFirewallBackend() == "" && len(lifecycle.InstalledProviders()) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
provider := client.Name()
|
|
||||||
active, err := client.Status()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !supportsManagedFilterChains(provider) && !active {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
required, err := loadRequiredFirewallPorts(strconv.Itoa(int(port)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
managedChains := supportsManagedFilterChains(provider)
|
|
||||||
if managedChains {
|
|
||||||
firewallRuleMutationMu.Lock()
|
|
||||||
defer firewallRuleMutationMu.Unlock()
|
|
||||||
}
|
|
||||||
configured, err := loadConfiguredFirewallPortWhiteList()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
protected := firewall.NormalizePortWhitelist(append(configured, required...))
|
|
||||||
if managedChains {
|
|
||||||
prepared := append([]firewall.PortWhitelist{{Port: strconv.Itoa(int(oldPort)), Protocol: "tcp"}}, required...)
|
|
||||||
if err := syncPanelRequiredPorts(provider, prepared); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := syncPanelRequiredPorts(provider, required); err != nil {
|
|
||||||
warnPanelPortCleanupFailure(oldPort, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
warnPanelPortCleanupFailure(oldPort, s.cleanupPanelPortLocked(ctx, provider, oldPort, protected))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ports := systemPorts([]firewall.PortWhitelist{{Port: strconv.Itoa(int(port)), Protocol: "tcp"}})
|
|
||||||
for _, port := range ports {
|
|
||||||
if err := s.ensureSystemPort(ctx, port); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
firewallRuleMutationMu.Lock()
|
|
||||||
defer firewallRuleMutationMu.Unlock()
|
|
||||||
warnPanelPortCleanupFailure(oldPort, s.cleanupPanelPortLocked(ctx, provider, oldPort, protected))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanupPanelPortLocked removes the old system-owned policy as well as any
|
|
||||||
// remaining managed runtime rule. The caller must hold firewallRuleMutationMu.
|
|
||||||
func (s *FirewallService) cleanupPanelPortLocked(ctx context.Context, provider string, oldPort uint, protected []firewall.PortWhitelist) error {
|
|
||||||
ctx = context.WithValue(ctx, panelPortWhitelistKey{}, protected)
|
|
||||||
ports := systemPorts([]firewall.PortWhitelist{{Port: strconv.Itoa(int(oldPort)), Protocol: "tcp"}})
|
|
||||||
if provider == constant.FirewallProviderUFW {
|
|
||||||
for _, port := range ports {
|
|
||||||
port.Protocol = "all"
|
|
||||||
ports = append(ports, port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Include family-neutral records left by firewalld or older versions, even
|
|
||||||
// when the selected backend has since changed.
|
|
||||||
ports = append(ports, dto.FirewallSystemPort{Port: strconv.Itoa(int(oldPort)), Protocol: "tcp"})
|
|
||||||
var cleanupErrors []error
|
|
||||||
for _, port := range ports {
|
|
||||||
if panelPortStillRequired(port, protected) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
records, err := s.systemPortRecords(ctx, port)
|
|
||||||
if err != nil {
|
|
||||||
cleanupErrors = append(cleanupErrors, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, record := range records {
|
|
||||||
if err := s.deleteRule(ctx, record.UUID, true); err != nil && !errors.Is(err, filter.ErrProtectedRule) {
|
|
||||||
cleanupErrors = append(cleanupErrors, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors.Join(cleanupErrors...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func warnPanelPortCleanupFailure(port uint, err error) {
|
|
||||||
if err != nil && global.LOG != nil {
|
|
||||||
global.LOG.Warnf("clean up old panel firewall port %d failed: %v", port, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func panelPortStillRequired(port dto.FirewallSystemPort, protected []firewall.PortWhitelist) bool {
|
|
||||||
rule := systemPortRule(filter.ProviderUFW, port)
|
|
||||||
for _, required := range protected {
|
|
||||||
if port.Family != "" && required.Family != "" && port.Family != required.Family {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
other := rule
|
|
||||||
other.Protocol, other.DestinationPort = required.Protocol, required.Port
|
|
||||||
if filter.RulesOverlap(rule, other) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func panelRuleStillRequired(rule filter.FirewallRule, protected []firewall.PortWhitelist) bool {
|
|
||||||
family := string(rule.Scope.Family)
|
|
||||||
if rule.Scope.Family == filter.FamilyInet {
|
|
||||||
family = ""
|
|
||||||
}
|
|
||||||
return panelPortStillRequired(dto.FirewallSystemPort{
|
|
||||||
Family: family, Port: rule.DestinationPort, Protocol: rule.Protocol,
|
|
||||||
}, protected)
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -5,240 +5,150 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"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/task"
|
"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/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/utils/cmd"
|
"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"
|
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IFirewallSettingService interface {
|
type IFirewallSettingService interface {
|
||||||
QueuePortWhitelist(value string) (dto.FilterChainOperationResponse, error)
|
CreatePortWhitelist(context.Context, dto.FirewallPortWhitelistCreate) error
|
||||||
|
UpdatePortWhitelist(context.Context, dto.FirewallPortWhitelistUpdate) error
|
||||||
|
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error
|
||||||
Load(context.Context) (dto.FirewallSettings, error)
|
Load(context.Context) (dto.FirewallSettings, error)
|
||||||
Operate(context.Context, dto.FirewallBackendOperation) error
|
Operate(context.Context, dto.FirewallBackendOperation) error
|
||||||
|
OperateFamily(dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error)
|
||||||
|
OperateIPv6(dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type FirewallSettingService struct{}
|
type FirewallSettingService struct{}
|
||||||
|
|
||||||
var firewallWhitelistTaskMu sync.Mutex
|
var firewallWhitelistMu sync.Mutex
|
||||||
|
|
||||||
var ErrFirewallBackendCleanupRequired = errors.New("firewall backend cleanup required")
|
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) (result error) {
|
||||||
|
firewallWhitelistMu.Lock()
|
||||||
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) QueuePortWhitelist(value string) (dto.FilterChainOperationResponse, error) {
|
|
||||||
return s.queuePortWhitelist(value, newFirewallService())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *FirewallSettingService) queuePortWhitelist(value string, firewallService *FirewallService) (dto.FilterChainOperationResponse, error) {
|
|
||||||
firewallWhitelistTaskMu.Lock()
|
|
||||||
defer firewallWhitelistTaskMu.Unlock()
|
|
||||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
|
||||||
return dto.FilterChainOperationResponse{}, err
|
|
||||||
}
|
|
||||||
taskItem, err := task.NewTask(i18n.GetMsgByKey("FirewallWhitelistTask"), task.TaskUpdate, task.TaskScopeFirewall, "", 0)
|
|
||||||
if err != nil {
|
|
||||||
return dto.FilterChainOperationResponse{}, err
|
|
||||||
}
|
|
||||||
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
|
|
||||||
succeeded, failed := 0, 0
|
|
||||||
err := s.applyPortWhitelist(t.TaskCtx, value, firewallService, func(status, label string, err error) {
|
|
||||||
switch status {
|
|
||||||
case "applied":
|
|
||||||
succeeded++
|
|
||||||
t.LogSuccess(label)
|
|
||||||
case "failed":
|
|
||||||
failed++
|
|
||||||
t.LogFailedWithErr(label, err)
|
|
||||||
default:
|
|
||||||
t.Log(i18n.GetWithName(status, 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 {
|
|
||||||
closeUnstartedFirewallTask(taskItem)
|
|
||||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall whitelist task: %w", err)
|
|
||||||
}
|
|
||||||
go func() { _ = taskItem.Execute() }()
|
|
||||||
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type whitelistReporter func(status, label string, err error)
|
|
||||||
|
|
||||||
func (s *FirewallSettingService) applyPortWhitelist(ctx context.Context, value string, firewallService *FirewallService, report whitelistReporter) error {
|
|
||||||
firewallRuleMutationMu.Lock()
|
firewallRuleMutationMu.Lock()
|
||||||
defer firewallRuleMutationMu.Unlock()
|
defer func() {
|
||||||
if err := ctx.Err(); err != nil {
|
firewallRuleMutationMu.Unlock()
|
||||||
return err
|
firewallWhitelistMu.Unlock()
|
||||||
|
if result == nil {
|
||||||
|
result = newFirewallService().SyncPortWhitelist(ctx)
|
||||||
}
|
}
|
||||||
ports, err := firewall.ParsePortWhitelist(value)
|
}()
|
||||||
|
current, err := loadFirewallPortWhiteList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
required, err := firewallService.requiredPorts()
|
current = append(current, request.Rule)
|
||||||
|
current, err = firewall.ValidatePortWhitelist(current)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
removed, err := s.savePortWhitelist(ctx, ports, required, firewallService)
|
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
value, err := json.Marshal(current)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
report("FirewallWhitelistSaved", "", nil)
|
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||||
for _, port := range systemPorts(removed) {
|
|
||||||
report("FirewallWhitelistReleased", whitelistPortLabel(port), nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = context.WithValue(ctx, panelPortWhitelistKey{}, required)
|
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) (result error) {
|
||||||
provider, providerErr := firewallService.selectedProvider(ctx)
|
firewallWhitelistMu.Lock()
|
||||||
ready := s.portWhitelistReadiness(provider, providerErr, firewallService)
|
firewallRuleMutationMu.Lock()
|
||||||
return syncPortWhitelist(ctx, ports, required, ready, firewallService.ensureSystemPortLocked, report)
|
defer func() {
|
||||||
|
firewallRuleMutationMu.Unlock()
|
||||||
|
firewallWhitelistMu.Unlock()
|
||||||
|
if result == nil {
|
||||||
|
result = newFirewallService().SyncPortWhitelist(ctx)
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
func (s *FirewallSettingService) savePortWhitelist(ctx context.Context, ports, required []firewall.PortWhitelist, firewallService *FirewallService) ([]firewall.PortWhitelist, error) {
|
current, err := loadFirewallPortWhiteList()
|
||||||
var removed []firewall.PortWhitelist
|
|
||||||
err := global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
||||||
var setting model.Setting
|
|
||||||
err := tx.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
setting.Value = constant.FirewallPortWhiteListValue
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
previous, err := firewall.ParsePortWhitelist(setting.Value)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
removed = excludeFirewallPorts(excludeFirewallPorts(previous, ports), required)
|
index, err := findPortWhitelistRule(current, request.OldRule)
|
||||||
txCtx := context.WithValue(ctx, constant.DB, tx)
|
|
||||||
if err := firewallService.releaseSystemPorts(txCtx, systemPorts(removed)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
value, err := json.Marshal(ports)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return tx.Where("key = ?", constant.FirewallPortWhiteList).
|
current[index] = request.Rule
|
||||||
Assign(map[string]interface{}{"value": string(value)}).
|
current, err = firewall.ValidatePortWhitelist(current)
|
||||||
FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
|
|
||||||
})
|
|
||||||
return removed, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *FirewallSettingService) portWhitelistReadiness(provider filter.Provider, providerErr error, firewallService *FirewallService) func(dto.FirewallSystemPort) (bool, error) {
|
|
||||||
type state struct {
|
|
||||||
ready bool
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
states := make(map[string]state)
|
|
||||||
return func(port dto.FirewallSystemPort) (bool, error) {
|
|
||||||
if providerErr != nil {
|
|
||||||
return false, providerErr
|
|
||||||
}
|
|
||||||
key := "service"
|
|
||||||
if isDirectFirewallProvider(provider) {
|
|
||||||
key = port.Family
|
|
||||||
}
|
|
||||||
if cached, ok := states[key]; ok {
|
|
||||||
return cached.ready, cached.err
|
|
||||||
}
|
|
||||||
var result state
|
|
||||||
if isDirectFirewallProvider(provider) {
|
|
||||||
initialized, bound, err := loadSystemFirewallFamilyStatus(string(provider), port.Family)
|
|
||||||
result = state{ready: initialized && bound, err: err}
|
|
||||||
} else {
|
|
||||||
client, err := firewallService.baseClient()
|
|
||||||
result.err = err
|
|
||||||
if err == nil {
|
|
||||||
result.ready, result.err = client.Status()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
states[key] = result
|
|
||||||
return result.ready, result.err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncPortWhitelist(
|
|
||||||
ctx context.Context,
|
|
||||||
ports, required []firewall.PortWhitelist,
|
|
||||||
ready func(dto.FirewallSystemPort) (bool, error),
|
|
||||||
ensure func(context.Context, dto.FirewallSystemPort) error,
|
|
||||||
report whitelistReporter,
|
|
||||||
) error {
|
|
||||||
var failures []error
|
|
||||||
for _, port := range systemPorts(ports) {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return errors.Join(append(failures, err)...)
|
|
||||||
}
|
|
||||||
label := whitelistPortLabel(port)
|
|
||||||
if containsFirewallPort(required, firewall.PortWhitelist{Family: port.Family, Port: port.Port, Protocol: port.Protocol}) {
|
|
||||||
report("FirewallWhitelistRequired", label, nil)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
active, err := ready(port)
|
|
||||||
if err == nil && !active {
|
|
||||||
report("FirewallWhitelistDeferred", label, nil)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
err = ensure(ctx, port)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
report("failed", label, err)
|
return err
|
||||||
failures = append(failures, fmt.Errorf("%s: %w", label, err))
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
report("applied", label, nil)
|
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return errors.Join(failures...)
|
value, err := json.Marshal(current)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
func whitelistPortLabel(port dto.FirewallSystemPort) string {
|
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error {
|
||||||
return fmt.Sprintf("%s %s/%s", port.Family, port.Port, port.Protocol)
|
firewallWhitelistMu.Lock()
|
||||||
|
firewallRuleMutationMu.Lock()
|
||||||
|
defer func() {
|
||||||
|
firewallRuleMutationMu.Unlock()
|
||||||
|
firewallWhitelistMu.Unlock()
|
||||||
|
}()
|
||||||
|
current, err := loadFirewallPortWhiteList()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if request.Rule == nil {
|
||||||
|
return filter.ErrInvalidRule
|
||||||
|
}
|
||||||
|
index, err := findPortWhitelistRule(current, *request.Rule)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
current = slices.Delete(current, index, index+1)
|
||||||
|
current, err = firewall.ValidatePortWhitelist(current)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
value, err := json.Marshal(current)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {
|
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {
|
||||||
result := dto.FirewallSettings{PingStatus: ping.LoadStatus()}
|
families, err := loadFirewallFamilies()
|
||||||
if ports, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList); err == nil {
|
if err != nil {
|
||||||
result.PortWhitelist = ports
|
return dto.FirewallSettings{}, err
|
||||||
} else {
|
|
||||||
result.PortWhitelist = constant.FirewallPortWhiteListValue
|
|
||||||
}
|
}
|
||||||
|
result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus(), IPv6Enabled: slices.Contains(families, constant.FirewallFamilyIPv6)}
|
||||||
|
|
||||||
installed := make(map[string]bool)
|
installed := make(map[string]bool)
|
||||||
for _, name := range lifecycle.InstalledProviders() {
|
for _, name := range lifecycle.InstalledProviders() {
|
||||||
installed[name] = true
|
installed[name] = true
|
||||||
}
|
}
|
||||||
result.System.Selected = configuredSystemFirewallBackend()
|
systemBackend, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
||||||
|
result.System.Selected = strings.TrimSpace(systemBackend)
|
||||||
if result.System.Selected == "" {
|
if result.System.Selected == "" {
|
||||||
if client, err := lifecycle.NewClient(); err == nil {
|
if client, err := lifecycle.NewClient(""); err == nil {
|
||||||
result.System.Selected = client.Name()
|
result.System.Selected = client.Name()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,16 +161,16 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
|||||||
} {
|
} {
|
||||||
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
|
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
|
||||||
if option.Installed && name == result.System.Selected {
|
if option.Installed && name == result.System.Selected {
|
||||||
client, err := lifecycle.NewClientFor(name)
|
client, err := lifecycle.NewClient(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
option.Message = err.Error()
|
option.Message = err.Error()
|
||||||
} else if supportsManagedFilterChains(name) {
|
} else if name == constant.FirewallProviderIptables || name == constant.FirewallProviderNftables {
|
||||||
option.Initialized, option.Bound, err = loadFirewallInitStatus(name, "base")
|
overview, err := loadSystemFirewallOverview(name, "base", families)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
option.Message = err.Error()
|
option.Message = err.Error()
|
||||||
}
|
}
|
||||||
option.IPv4 = loadSystemFirewallFamilyInfo(name, constant.FirewallFamilyIPv4)
|
option.Initialized, option.Bound = overview.IsInit, overview.IsBind
|
||||||
option.IPv6 = loadSystemFirewallFamilyInfo(name, constant.FirewallFamilyIPv6)
|
option.IPv4, option.IPv6 = overview.IPv4, overview.IPv6
|
||||||
} else if option.Active, err = client.Status(); err != nil {
|
} else if option.Active, err = client.Status(); err != nil {
|
||||||
option.Message = err.Error()
|
option.Message = err.Error()
|
||||||
}
|
}
|
||||||
@@ -273,32 +183,29 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
|||||||
result.System.Options = append(result.System.Options, option)
|
result.System.Options = append(result.System.Options, option)
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Forwarding.Selected = configuredForwardingBackend()
|
forwardingBackend, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||||
|
result.Forwarding.Selected = strings.TrimSpace(forwardingBackend)
|
||||||
|
if result.Forwarding.Selected == "" {
|
||||||
|
result.Forwarding.Selected = constant.FirewallProviderIptables
|
||||||
|
}
|
||||||
result.Forwarding.Current = result.Forwarding.Selected
|
result.Forwarding.Current = result.Forwarding.Selected
|
||||||
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
|
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
|
||||||
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
|
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
|
||||||
if option.Installed && name == result.Forwarding.Selected {
|
if option.Installed && name == result.Forwarding.Selected {
|
||||||
manager, err := newForwardingManagerFor(name)
|
manager, err := newForwardingAdapterFor(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
option.Message = err.Error()
|
option.Message = err.Error()
|
||||||
} else if status, err := manager.Status(); err != nil {
|
} else {
|
||||||
option.Message = err.Error()
|
status, statusErr := loadForwardingFirewallOverview(manager, families)
|
||||||
|
option.IPv4, option.IPv6 = status.IPv4, status.IPv6
|
||||||
|
if statusErr != nil {
|
||||||
|
option.Message = statusErr.Error()
|
||||||
} else {
|
} else {
|
||||||
option.Initialized, option.Bound = status.IsInit, status.IsBind
|
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 result.IPv6Enabled && name == constant.FirewallProviderIptables && !option.IPv6.Available {
|
||||||
|
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil && !commands.IPv6Available() {
|
||||||
|
option.IPv6.Reason = dockerfirewall.ReasonCommandMissing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,7 +223,11 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
|||||||
if dockerInstalled {
|
if dockerInstalled {
|
||||||
dockerVersion = loadDockerEngineVersion(ctx)
|
dockerVersion = loadDockerEngineVersion(ctx)
|
||||||
}
|
}
|
||||||
result.Docker.Selected = configuredDockerFirewallBackend()
|
dockerBackend, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||||
|
dockerBackend = strings.ToLower(strings.TrimSpace(dockerBackend))
|
||||||
|
if dockerBackend == constant.FirewallProviderIptables || dockerBackend == constant.FirewallProviderNftables {
|
||||||
|
result.Docker.Selected = dockerBackend
|
||||||
|
}
|
||||||
result.Docker.Current = result.Docker.Selected
|
result.Docker.Current = result.Docker.Selected
|
||||||
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
|
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
|
||||||
option := dto.FirewallBackendOption{
|
option := dto.FirewallBackendOption{
|
||||||
@@ -329,53 +240,51 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
|||||||
option.Active = false
|
option.Active = false
|
||||||
}
|
}
|
||||||
if option.Active {
|
if option.Active {
|
||||||
guard := docker_guard.NewRuntime(name)
|
guard := newDockerFirewallRuntime(ctx, name)
|
||||||
ipv4, ipv6 := guard.Status(docker_guard.FamilyIPv4), guard.Status(docker_guard.FamilyIPv6)
|
for _, family := range families {
|
||||||
option.Initialized = ipv4.Initialized || ipv6.Initialized
|
status := guard.Status(family)
|
||||||
option.Bound = ipv4.Bound || ipv6.Bound
|
option.Initialized = option.Initialized || status.Initialized
|
||||||
option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound
|
option.Bound = option.Bound || status.Bound
|
||||||
option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound
|
info := dto.FirewallBackendFamilyStatus{
|
||||||
option.IPv4.Available = ipv4.Reason != docker_guard.ReasonCommandMissing
|
Available: status.Reason != dockerfirewall.ReasonCommandMissing,
|
||||||
option.IPv6.Available = ipv6.Reason != docker_guard.ReasonCommandMissing
|
Initialized: status.Initialized, Bound: status.Bound, Reason: status.Reason,
|
||||||
option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason
|
}
|
||||||
|
if family == constant.FirewallFamilyIPv4 {
|
||||||
|
option.IPv4 = info
|
||||||
|
} else {
|
||||||
|
option.IPv6 = info
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result.Docker.Options = append(result.Docker.Options, option)
|
result.Docker.Options = append(result.Docker.Options, option)
|
||||||
}
|
}
|
||||||
|
result.PortWhitelist, err = loadPortWhitelistSetting()
|
||||||
return result, nil
|
if err != nil {
|
||||||
|
return result, err
|
||||||
}
|
}
|
||||||
|
result.PanelPort = LoadPanelPort()
|
||||||
func loadSystemFirewallFamilyStatus(provider, family string) (bool, bool, error) {
|
sshPort, sshErr := loadSSHWhitelistPortFrom(sshPath)
|
||||||
switch provider {
|
if sshErr != nil {
|
||||||
case constant.FirewallProviderIptables:
|
global.LOG.Warnf("load SSH port for firewall settings: %v", sshErr)
|
||||||
return iptables_helper.LoadFamilyInitStatus(family, "base")
|
} else {
|
||||||
case constant.FirewallProviderNftables:
|
result.SSHPort = sshPort
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
|
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||||
|
if request.Operation == "cleanup" {
|
||||||
|
_, err := newFirewallService().Reset(ctx, dto.FirewallRuleReset{Subsystem: request.Subsystem, Provider: filter.Provider(request.Backend)})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer firewallLifecycleTaskMu.Unlock()
|
||||||
if request.Subsystem != "system" && request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
|
if request.Subsystem != "system" && request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
|
||||||
return fmt.Errorf("%s only supports iptables or nftables", request.Subsystem)
|
return fmt.Errorf("%s only supports iptables or nftables", request.Subsystem)
|
||||||
}
|
}
|
||||||
if request.Subsystem == "system" && !supportsManagedFilterChains(request.Backend) && request.Operation != "select" {
|
if request.Subsystem == "system" && (request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables) && request.Operation != "select" {
|
||||||
return fmt.Errorf("%s does not support initialization or cleanup", request.Backend)
|
return fmt.Errorf("%s does not support initialization or cleanup", request.Backend)
|
||||||
}
|
}
|
||||||
switch request.Subsystem {
|
switch request.Subsystem {
|
||||||
@@ -385,14 +294,12 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
|
|||||||
}
|
}
|
||||||
if request.Operation == "initialize" {
|
if request.Operation == "initialize" {
|
||||||
service := newFirewallService()
|
service := newFirewallService()
|
||||||
if err := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend)); err != nil {
|
whitelistErr := service.SyncPortWhitelist(ctx)
|
||||||
return err
|
return whitelistErr
|
||||||
}
|
|
||||||
return service.syncConfiguredFirewallPorts(ctx)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
case "forwarding":
|
case "forwarding":
|
||||||
return s.operateForwarding(request)
|
return s.operateForwarding(ctx, request)
|
||||||
case "docker":
|
case "docker":
|
||||||
return s.operateDocker(ctx, request)
|
return s.operateDocker(ctx, request)
|
||||||
default:
|
default:
|
||||||
@@ -400,72 +307,224 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error {
|
func (s *FirewallSettingService) OperateFamily(request dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error) {
|
||||||
guard := docker_guard.NewRuntime(request.Backend)
|
if request.Family != constant.FirewallFamilyIPv4 && request.Family != constant.FirewallFamilyIPv6 {
|
||||||
if request.Operation == "cleanup" {
|
return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope
|
||||||
if err := guard.Cleanup(); err != nil {
|
}
|
||||||
|
if request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
|
||||||
|
return dto.FilterChainOperationResponse{}, filter.ErrUnsupportedScope
|
||||||
|
}
|
||||||
|
if request.Operation != "initialize" && request.Operation != "repair" && request.Operation != "bind" {
|
||||||
|
return dto.FilterChainOperationResponse{}, filter.ErrRuleOperation
|
||||||
|
}
|
||||||
|
subsystem := ""
|
||||||
|
switch request.Subsystem {
|
||||||
|
case "system":
|
||||||
|
subsystem = firewallTaskHost
|
||||||
|
case "forwarding":
|
||||||
|
subsystem = firewallTaskForwarding
|
||||||
|
case "docker":
|
||||||
|
subsystem = firewallTaskDocker
|
||||||
|
default:
|
||||||
|
return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope
|
||||||
|
}
|
||||||
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||||
|
return dto.FilterChainOperationResponse{}, err
|
||||||
|
}
|
||||||
|
return queueFirewallRuleTask(subsystem, task.TaskExec, "", nil, func(t *task.Task) error {
|
||||||
|
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
|
defer firewallLifecycleTaskMu.Unlock()
|
||||||
}
|
t.Logf("backend=%s family=%s operation=%s", request.Backend, request.Family, request.Operation)
|
||||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
families, err := loadFirewallFamilies()
|
||||||
if request.Operation == "select" {
|
|
||||||
current := previous
|
|
||||||
if current == "" {
|
|
||||||
current = alternateDirectBackend(request.Backend)
|
|
||||||
}
|
|
||||||
initialized, err := dockerGuardBackendInitialized(current)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if current != request.Backend && initialized {
|
if !slices.Contains(families, request.Family) {
|
||||||
return firewallBackendCleanupRequired(current, request.Backend)
|
return fmt.Errorf("IPv6 firewall support is disabled")
|
||||||
}
|
}
|
||||||
}
|
initialize := request.Operation != "bind"
|
||||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, request.Backend); err != nil {
|
switch request.Subsystem {
|
||||||
|
case "system":
|
||||||
|
firewallWhitelistMu.Lock()
|
||||||
|
defer firewallWhitelistMu.Unlock()
|
||||||
|
if err := newFirewallService().checkSelectedProvider(t.TaskCtx, filter.Provider(request.Backend)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if request.Operation == "select" {
|
ports, err := loadFirewallPortWhiteList()
|
||||||
if err := (&DockerService{}).UpdateFirewallBackend(request.Backend); err != nil {
|
if err != nil {
|
||||||
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
|
return err
|
||||||
|
}
|
||||||
|
required, err := firewall.RequiredPortWhitelist(ports)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
firewallRuleMutationMu.Lock()
|
||||||
|
if request.Backend == constant.FirewallProviderIptables {
|
||||||
|
err = iptables_helper.OperateFamily(request.Family, initialize, required)
|
||||||
|
} else {
|
||||||
|
err = nftables_helper.OperateFamily(filter.Family(request.Family), initialize, required)
|
||||||
|
}
|
||||||
|
firewallRuleMutationMu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if initialize {
|
||||||
|
if err := newFirewallService().applyPortWhitelist(t.TaskCtx, ports, nil, filter.Family(request.Family)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if request.Operation == "initialize" {
|
return settingRepo.UpdateOrCreate("IptablesStatus", constant.StatusEnable)
|
||||||
if err := newDockerPortGuardService().Operate(ctx, dto.DockerPortGuardOperation{Operation: "initialize"}); err != nil {
|
case "forwarding":
|
||||||
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
|
forwardingMutationMu.Lock()
|
||||||
|
defer forwardingMutationMu.Unlock()
|
||||||
|
manager, err := newForwardingAdapter(t.TaskCtx)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if manager.Name() != request.Backend {
|
||||||
|
return filter.ErrProviderUnavailable
|
||||||
}
|
}
|
||||||
return nil
|
if err := manager.OperateFamily(request.Family, initialize); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rules, err := manager.List()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := persistForwardingRules(manager, rules); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
|
||||||
|
default:
|
||||||
|
dockerPortGuardServiceMu.Lock()
|
||||||
|
defer dockerPortGuardServiceMu.Unlock()
|
||||||
|
runtime, backend, err := newDockerPortGuardService().runtimeForDocker(t.TaskCtx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if backend != request.Backend {
|
||||||
|
return filter.ErrProviderUnavailable
|
||||||
|
}
|
||||||
|
if err := runtime.OperateFamily(request.Family, initialize); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
inventory, err := runtime.ListPolicies()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := persistDockerRules(backend, inventory); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func dockerGuardBackendInitialized(backend string) (bool, error) {
|
func (s *FirewallSettingService) OperateIPv6(request dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error) {
|
||||||
guard := docker_guard.NewRuntime(backend)
|
if request.Status != constant.StatusEnable && request.Status != constant.StatusDisable {
|
||||||
for _, family := range []string{docker_guard.FamilyIPv4, docker_guard.FamilyIPv6} {
|
return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule
|
||||||
initialized, err := guard.Initialized(family)
|
}
|
||||||
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||||
|
return dto.FilterChainOperationResponse{}, err
|
||||||
|
}
|
||||||
|
return queueFirewallRuleTask(firewallTaskHost, task.TaskExec, "", nil, func(t *task.Task) error {
|
||||||
|
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer firewallLifecycleTaskMu.Unlock()
|
||||||
|
firewallWhitelistMu.Lock()
|
||||||
|
defer firewallWhitelistMu.Unlock()
|
||||||
|
firewallRuleMutationMu.Lock()
|
||||||
|
defer firewallRuleMutationMu.Unlock()
|
||||||
|
forwardingMutationMu.Lock()
|
||||||
|
defer forwardingMutationMu.Unlock()
|
||||||
|
dockerPortGuardServiceMu.Lock()
|
||||||
|
defer dockerPortGuardServiceMu.Unlock()
|
||||||
|
if request.Status == constant.StatusEnable {
|
||||||
|
return settingRepo.UpdateOrCreate(constant.FirewallIPv6SupportKey, request.Status)
|
||||||
|
}
|
||||||
|
ports, err := loadFirewallPortWhiteList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return err
|
||||||
}
|
}
|
||||||
if initialized {
|
value, err := json.Marshal(ipv4PortWhitelist(ports))
|
||||||
return true, nil
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
installed := lifecycle.InstalledProviders()
|
||||||
|
for _, selection := range []struct{ subsystem, key string }{
|
||||||
|
{"system", constant.FirewallSystemBackendKey},
|
||||||
|
{"forwarding", constant.FirewallForwardingBackendKey},
|
||||||
|
{"docker", constant.FirewallDockerBackendKey},
|
||||||
|
} {
|
||||||
|
backend, err := settingRepo.GetValueByKey(selection.key)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
backend = strings.ToLower(strings.TrimSpace(backend))
|
||||||
|
if backend == "" {
|
||||||
|
if selection.subsystem == "system" {
|
||||||
|
if len(installed) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
client, err := NewSelectedSystemFirewallClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
backend = client.Name()
|
||||||
|
} else {
|
||||||
|
backend = constant.FirewallProviderIptables
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, nil
|
if !slices.Contains(installed, backend) || (backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Logf("disable IPv6 firewall bindings: subsystem=%s backend=%s", selection.subsystem, backend)
|
||||||
|
switch selection.subsystem {
|
||||||
|
case "system":
|
||||||
|
if backend == constant.FirewallProviderIptables {
|
||||||
|
err = iptables_helper.UnbindIPv6BaseChains()
|
||||||
|
} else {
|
||||||
|
err = nftables_helper.SetTableDormant(t.TaskCtx, "ip6", nftables_helper.TableName)
|
||||||
|
if err == nil {
|
||||||
|
err = nftables_helper.PersistRuleset(t.TaskCtx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "forwarding":
|
||||||
|
var manager forwarding.Adapter
|
||||||
|
manager, err = newForwardingAdapterFor(t.TaskCtx, backend)
|
||||||
|
if err == nil {
|
||||||
|
err = manager.UnbindFamily(constant.FirewallFamilyIPv6)
|
||||||
|
}
|
||||||
|
case "docker":
|
||||||
|
err = newDockerFirewallRuntime(t.TaskCtx, backend).Unbind(constant.FirewallFamilyIPv6)
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := t.TaskCtx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return settingRepo.UpdateValues(map[string]string{constant.FirewallIPv6SupportKey: request.Status, constant.FirewallPortWhiteList: string(value)})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIFirewallSettingService() IFirewallSettingService {
|
||||||
|
return &FirewallSettingService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperation) error {
|
func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperation) error {
|
||||||
firewallRuleMutationMu.Lock()
|
firewallRuleMutationMu.Lock()
|
||||||
defer firewallRuleMutationMu.Unlock()
|
defer firewallRuleMutationMu.Unlock()
|
||||||
if _, err := lifecycle.NewClientFor(request.Backend); err != nil {
|
if _, err := lifecycle.NewClient(request.Backend); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if request.Operation == "cleanup" {
|
|
||||||
return cleanupSystemBackend(request.Backend)
|
|
||||||
}
|
|
||||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
||||||
if previous == "" {
|
if previous == "" {
|
||||||
if client, err := lifecycle.NewClient(); err == nil {
|
if client, err := lifecycle.NewClient(""); err == nil {
|
||||||
previous = client.Name()
|
previous = client.Name()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -475,7 +534,7 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if initialized {
|
if initialized {
|
||||||
return firewallBackendCleanupRequired(previous, request.Backend)
|
return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": previous, "target": request.Backend}, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, request.Backend); err != nil {
|
if err := settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, request.Backend); err != nil {
|
||||||
@@ -501,21 +560,14 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
|
|||||||
}
|
}
|
||||||
|
|
||||||
func systemFirewallBackendInitialized(backend string) (bool, error) {
|
func systemFirewallBackendInitialized(backend string) (bool, error) {
|
||||||
return systemFirewallBackendInitializedWithClientFactory(backend, lifecycle.NewClientFor)
|
client, err := lifecycle.NewClient(backend)
|
||||||
}
|
|
||||||
|
|
||||||
func systemFirewallBackendInitializedWithClientFactory(
|
|
||||||
backend string,
|
|
||||||
newClient func(string) (lifecycle.Client, error),
|
|
||||||
) (bool, error) {
|
|
||||||
client, err := newClient(backend)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, lifecycle.ErrNotInstalled) {
|
if errors.Is(err, lifecycle.ErrNotInstalled) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if supportsManagedFilterChains(backend) {
|
if backend == constant.FirewallProviderIptables || backend == constant.FirewallProviderNftables {
|
||||||
for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} {
|
for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} {
|
||||||
initialized, _, err := loadSystemFirewallFamilyStatus(backend, family)
|
initialized, _, err := loadSystemFirewallFamilyStatus(backend, family)
|
||||||
if family == constant.FirewallFamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
|
if family == constant.FirewallFamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||||
@@ -533,73 +585,39 @@ func systemFirewallBackendInitializedWithClientFactory(
|
|||||||
return client.Status()
|
return client.Status()
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanupSystemBackend(backend string) error {
|
func (s *FirewallSettingService) operateForwarding(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||||
switch backend {
|
if _, err := newForwardingAdapterFor(ctx, request.Backend); err != nil {
|
||||||
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
|
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)
|
previous, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||||
if request.Operation == "select" {
|
if request.Operation == "select" {
|
||||||
current := previous
|
current := previous
|
||||||
if current == "" {
|
if current == "" {
|
||||||
detected, err := newForwardingManager()
|
detected, err := newForwardingAdapter(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
current = detected.Name()
|
current = detected.Name()
|
||||||
}
|
}
|
||||||
initialized, err := forwardingBackendInitialized(current)
|
initialized, err := forwardingBackendInitialized(ctx, current)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if current != request.Backend && initialized {
|
if current != request.Backend && initialized {
|
||||||
return firewallBackendCleanupRequired(current, request.Backend)
|
return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": current, "target": request.Backend}, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, request.Backend); err != nil {
|
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, request.Backend); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if request.Operation == "initialize" {
|
if request.Operation == "initialize" {
|
||||||
return newForwardingService().Enable()
|
return newForwardingService().Enable(ctx)
|
||||||
}
|
}
|
||||||
recordForwardingSyncError(nil)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func forwardingBackendInitialized(backend string) (bool, error) {
|
func forwardingBackendInitialized(ctx context.Context, backend string) (bool, error) {
|
||||||
manager, err := newForwardingManagerFor(backend)
|
manager, err := newForwardingAdapterFor(ctx, backend)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, lifecycle.ErrNotInstalled) {
|
if errors.Is(err, lifecycle.ErrNotInstalled) {
|
||||||
return false, nil
|
return false, nil
|
||||||
@@ -618,9 +636,53 @@ func forwardingBackendInitialized(backend string) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func alternateDirectBackend(backend string) string {
|
func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||||
if backend == constant.FirewallProviderNftables {
|
|
||||||
return constant.FirewallProviderIptables
|
previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||||
|
if request.Operation == "select" {
|
||||||
|
current := previous
|
||||||
|
if current == "" {
|
||||||
|
current = constant.FirewallProviderNftables
|
||||||
|
if request.Backend == constant.FirewallProviderNftables {
|
||||||
|
current = constant.FirewallProviderIptables
|
||||||
}
|
}
|
||||||
return constant.FirewallProviderNftables
|
}
|
||||||
|
initialized, err := dockerGuardBackendInitialized(ctx, current)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if current != request.Backend && initialized {
|
||||||
|
return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": current, "target": request.Backend}, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, request.Backend); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if request.Operation == "select" {
|
||||||
|
if err := (&DockerService{}).UpdateFirewallBackend(request.Backend); err != nil {
|
||||||
|
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if request.Operation == "initialize" {
|
||||||
|
if err := newDockerPortGuardService().Operate(ctx, dto.DockerPortGuardOperation{Operation: "initialize"}); err != nil {
|
||||||
|
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dockerGuardBackendInitialized(ctx context.Context, backend string) (bool, error) {
|
||||||
|
guard := newDockerFirewallRuntime(ctx, backend)
|
||||||
|
for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} {
|
||||||
|
initialized, err := guard.Initialized(family)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if initialized {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user