mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-28 16:00:18 +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)
|
||||
}
|
||||
|
||||
// @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
|
||||
// @Summary Page Ollama models
|
||||
// @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
|
||||
// @Summary Page Ollama models
|
||||
// @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/repo"
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -290,7 +293,7 @@ func (b *BaseApi) PageAlertConfig(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @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) {
|
||||
var req dto.AlertConfigUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
@@ -318,13 +321,15 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /alert/config/status [post]
|
||||
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [id][status]","formatEN":"update alert config status [id][status]"}
|
||||
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [name][status]","formatEN":"update alert config status [name][status]"}
|
||||
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
|
||||
var req dto.AlertConfigStatusUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c)); err != nil {
|
||||
name, err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c))
|
||||
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
@@ -350,13 +355,14 @@ func loadAuditUser(c *gin.Context) string {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @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) {
|
||||
var req dto.DeleteRequest
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
err := alertService.DeleteAlertConfig(req.ID)
|
||||
name, err := alertService.DeleteAlertConfig(req.ID)
|
||||
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -378,10 +384,24 @@ func (b *BaseApi) TestAlertConfig(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
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 {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
if global.LOG != nil {
|
||||
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)
|
||||
return
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/appicon"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -160,10 +161,25 @@ func (b *BaseApi) GetAppDetailByID(c *gin.Context) {
|
||||
// @Router /apps/install [post]
|
||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"安装应用 [name]","formatEN":"Install app [name]"}
|
||||
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
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"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/dto"
|
||||
@@ -336,11 +340,25 @@ func (b *BaseApi) UpdateAppConfig(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /apps/installed/info/:appInstallId [get]
|
||||
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")
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
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)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
@@ -28,7 +28,7 @@ func (b *BaseApi) SearchContainer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
total, list, err := containerService.Page(req)
|
||||
total, list, err := containerService.Page(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -249,9 +249,10 @@ func (b *BaseApi) ListContainerByImage(c *gin.Context) {
|
||||
// @Success 200 {object} dto.ContainerStatus
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Param containersOnly query boolean false "Only count containers"
|
||||
// @Router /containers/status [get]
|
||||
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 {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -415,9 +416,14 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) {
|
||||
// @Success 200 {array} dto.ContainerListStats
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Param ids query string false "Comma-separated container IDs; omitted selects all containers"
|
||||
// @Router /containers/list/stats [get]
|
||||
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 {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
|
||||
@@ -2,9 +2,12 @@ package v2
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"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/service"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -98,11 +101,25 @@ func (b *BaseApi) SearchDatabase(c *gin.Context) {
|
||||
// @Param type path string true "type"
|
||||
// @Router /databases/db/list/:type [get]
|
||||
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")
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
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)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
@@ -3,9 +3,12 @@ package v2
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"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/service"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -19,6 +22,11 @@ import (
|
||||
// @Router /databases [post]
|
||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 mysql 数据库 [name]","formatEN":"create mysql database [name]"}
|
||||
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
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
@@ -32,6 +40,16 @@ func (b *BaseApi) CreateMysql(c *gin.Context) {
|
||||
}
|
||||
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 {
|
||||
helper.InternalServer(c, err)
|
||||
@@ -513,11 +531,25 @@ func (b *BaseApi) LoadRemoteAccess(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /databases/status [post]
|
||||
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
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
@@ -86,17 +86,23 @@ func (b *BaseApi) CheckHasCli(c *gin.Context) {
|
||||
|
||||
// @Tags Database Redis
|
||||
// @Summary Install redis-cli
|
||||
// @Success 200
|
||||
// @Accept json
|
||||
// @Param request body dto.RedisCliInstall true "request"
|
||||
// @Success 200 {string} string
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/redis/install/cli [post]
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags Database Redis
|
||||
|
||||
+164
-168
@@ -2,14 +2,14 @@ package v2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -60,7 +60,7 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
||||
// @Summary Operate firewall
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallLifecycleOperation true "request"
|
||||
// @Success 200
|
||||
// @Success 200 {object} dto.FirewallLifecycleOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/operate [post]
|
||||
@@ -71,12 +71,13 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := firewallService.OperateFirewall(request); err != nil {
|
||||
result, err := firewallService.QueueFirewallOperation(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
@@ -87,7 +88,7 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/forward/base [post]
|
||||
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) {
|
||||
data, err := forwardingService.LoadBaseInfo()
|
||||
data, err := forwardingService.LoadBaseInfo(c.Request.Context())
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -108,7 +109,7 @@ func (b *BaseApi) SearchForwardingRules(c *gin.Context) {
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
total, items, err := forwardingService.SearchRules(request)
|
||||
total, items, err := forwardingService.SearchRules(c.Request.Context(), request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -259,28 +260,6 @@ func (b *BaseApi) LoadFirewallNativeDetail(c *gin.Context) {
|
||||
helper.SuccessWithData(c, info)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Adopt an external firewall rule
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleAdopt true "request"
|
||||
// @Success 200
|
||||
// @Failure 400 {object} dto.Response
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/adopt [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"纳管防火墙规则","formatEN":"adopt firewall rule"}
|
||||
func (b *BaseApi) AdoptFirewallRule(c *gin.Context) {
|
||||
var request dto.FirewallRuleAdopt
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallService.Adopt(c.Request.Context(), request); err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Queue firewall rule creation
|
||||
// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log.
|
||||
@@ -307,67 +286,8 @@ func (b *BaseApi) CreateFirewallRules(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Preview firewall rule synchronization
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleSyncRequest true "request"
|
||||
// @Success 200 {object} dto.FirewallRuleSyncPreview
|
||||
// @Failure 400 {object} dto.Response
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/sync/preview [post]
|
||||
func (b *BaseApi) PreviewFirewallRuleSync(c *gin.Context) {
|
||||
var request dto.FirewallRuleSyncRequest
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallService.PreviewRuleSync(c.Request.Context(), c.ClientIP(), request)
|
||||
if err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Load the currently executing firewall rule synchronization task
|
||||
// @Success 200 {object} dto.FirewallRuleSyncTask
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/sync/task [get]
|
||||
func (b *BaseApi) LoadFirewallRuleSyncTask(c *gin.Context) {
|
||||
result, err := firewallService.CurrentRuleSyncTask()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Synchronize firewall rules to a target backend
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleSyncRequest true "request"
|
||||
// @Success 200 {object} dto.FirewallRuleSyncResult
|
||||
// @Failure 400 {object} dto.Response
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/sync [post]
|
||||
// @x-panel-log {"bodyKeys":["subsystem","sourceProvider","targetProvider"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 [subsystem] 防火墙规则到 [targetProvider]","formatEN":"sync [subsystem] firewall rules to [targetProvider]"}
|
||||
func (b *BaseApi) SyncFirewallRules(c *gin.Context) {
|
||||
var request dto.FirewallRuleSyncRequest
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallService.SyncRules(c.Request.Context(), c.ClientIP(), request)
|
||||
if err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Delete managed unified firewall v2 rules
|
||||
// @Summary Queue firewall rule deletion
|
||||
// @Description Deletes non-whitelist rules by scope and instance key. Returns a taskID immediately; results are written to the task log.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleDelete true "request"
|
||||
// @Success 200 {object} dto.FirewallRuleDeleteResponse
|
||||
@@ -390,7 +310,7 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Update a managed unified firewall v2 rule
|
||||
// @Summary Update a firewall rule
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleUpdate true "request"
|
||||
// @Success 200
|
||||
@@ -398,16 +318,13 @@ func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/update [post]
|
||||
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [uuid]","formatEN":"update firewall rule [uuid]"}
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则","formatEN":"update firewall rule"}
|
||||
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||
var request dto.FirewallRuleUpdate
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if !normalizeFirewallRuleUUID(c, &request.UUID) {
|
||||
return
|
||||
}
|
||||
if err := firewallService.Update(c.Request.Context(), c.ClientIP(), request); err != nil {
|
||||
if err := firewallService.Update(c.Request.Context(), request); err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
@@ -415,7 +332,7 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Reorder a managed unified firewall v2 rule
|
||||
// @Summary Reorder a firewall rule
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleReorder true "request"
|
||||
// @Success 200
|
||||
@@ -423,53 +340,41 @@ func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/reorder [post]
|
||||
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [uuid]","formatEN":"reorder firewall rule [uuid]"}
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序","formatEN":"reorder firewall rule"}
|
||||
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
|
||||
var request dto.FirewallRuleReorder
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if !normalizeFirewallRuleUUID(c, &request.UUID) {
|
||||
return
|
||||
}
|
||||
if err := firewallService.Reorder(c.Request.Context(), c.ClientIP(), request); err != nil {
|
||||
if err := firewallService.Reorder(c.Request.Context(), request); err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
func normalizeFirewallRuleUUID(c *gin.Context, value *string) bool {
|
||||
if value == nil {
|
||||
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
|
||||
return false
|
||||
}
|
||||
*value = strings.TrimSpace(*value)
|
||||
if *value == "" {
|
||||
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func handleFirewallRuleError(c *gin.Context, err error) {
|
||||
var businessErr buserr.BusinessError
|
||||
isBusinessError := errors.As(err, &businessErr)
|
||||
switch {
|
||||
case errors.Is(err, filter.ErrProtectedRule):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
|
||||
case errors.Is(err, filter.ErrRuleStale):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err)
|
||||
case errors.Is(err, repo.ErrFirewallRuleRevisionConflict):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_REVISION_CONFLICT", "ErrInvalidParams", err)
|
||||
case errors.Is(err, filter.ErrManagedScopeChange):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrFirewallRuleScopeChange", err)
|
||||
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
|
||||
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation), errors.Is(err, filter.ErrRuleConflict),
|
||||
errors.Is(err, repo.ErrFirewallPersistenceInvalid):
|
||||
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||
case errors.Is(err, filter.ErrVerificationFailed):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err)
|
||||
case isBusinessError && businessErr.Msg == "ErrRecordExist":
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_DUPLICATE", Message: err.Error()})
|
||||
c.Abort()
|
||||
case isBusinessError && businessErr.Msg == "ErrFirewallRuleConflict":
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_CONFLICT", Message: err.Error()})
|
||||
c.Abort()
|
||||
case isBusinessError && businessErr.Msg == "ErrInvalidParams":
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusBadRequest, ErrorCode: "FW_RULE_UNSUPPORTED", Message: err.Error()})
|
||||
c.Abort()
|
||||
default:
|
||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err)
|
||||
}
|
||||
@@ -491,26 +396,69 @@ func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Queue firewall port whitelist update
|
||||
// @Description Returns a taskID; configuration save and per-rule results are recorded in the task log.
|
||||
// @Summary Create firewall port whitelist rules
|
||||
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Param request body dto.FirewallPortWhitelistCreate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @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) {
|
||||
var request dto.FirewallPortWhitelistUpdate
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallSettingService.QueuePortWhitelist(request.Value)
|
||||
if err != nil {
|
||||
if err := firewallSettingService.UpdatePortWhitelist(c.Request.Context(), request); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
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
|
||||
@@ -528,14 +476,10 @@ func (b *BaseApi) OperateFirewallBackend(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if err := firewallSettingService.Operate(c.Request.Context(), request); err != nil {
|
||||
if errors.Is(err, service.ErrFirewallBackendCleanupRequired) {
|
||||
helper.ErrorWithBusinessCode(
|
||||
c,
|
||||
http.StatusConflict,
|
||||
"FW_BACKEND_CLEANUP_REQUIRED",
|
||||
"ErrInvalidParams",
|
||||
err,
|
||||
)
|
||||
var businessErr buserr.BusinessError
|
||||
if errors.As(err, &businessErr) && businessErr.Msg == "ErrFirewallBackendCleanupRequired" {
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_BACKEND_CLEANUP_REQUIRED", Message: err.Error()})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
helper.InternalServer(c, err)
|
||||
@@ -574,21 +518,6 @@ func (b *BaseApi) ListDockerPublishedPorts(c *gin.Context) {
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Sync Docker port guard rules
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/docker/sync [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Docker 端口防护规则","formatEN":"sync Docker port guard rules"}
|
||||
func (b *BaseApi) SyncDockerPortGuard(c *gin.Context) {
|
||||
if err := dockerPortGuardService.Reconcile(c.Request.Context()); err != nil {
|
||||
handleDockerPortGuardError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Operate Docker port guard
|
||||
// @Accept json
|
||||
@@ -627,7 +556,7 @@ func (b *BaseApi) OperateDockerPortGuard(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/docker/policies/delete/batch [post]
|
||||
// @x-panel-log {"bodyKeys":["uuids"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 Docker 端口防护策略 [uuids]","formatEN":"delete Docker port guard policies [uuids]"}
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 Docker 端口防护策略","formatEN":"delete Docker port guard policies"}
|
||||
func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
|
||||
var request dto.DockerPortGuardPolicyBatchDelete
|
||||
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) {
|
||||
if errors.Is(err, service.ErrDockerIptablesChainUnavailable) {
|
||||
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE", "ErrDockerIptablesChainUnavailable", err)
|
||||
return
|
||||
var businessErr buserr.BusinessError
|
||||
if errors.As(err, &businessErr) {
|
||||
code, errorCode := http.StatusInternalServerError, ""
|
||||
switch businessErr.Msg {
|
||||
case "ErrDockerIptablesChainUnavailable":
|
||||
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE"
|
||||
case "ErrDockerNftablesChainUnavailable":
|
||||
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_NFTABLES_CHAIN_UNAVAILABLE"
|
||||
case "ErrInvalidParams":
|
||||
code, errorCode = http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID"
|
||||
case "ErrDockerFailed":
|
||||
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE"
|
||||
}
|
||||
if errorCode != "" {
|
||||
c.JSON(http.StatusOK, dto.Response{Code: code, ErrorCode: errorCode, Message: err.Error()})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
if errors.Is(err, service.ErrDockerNftablesChainUnavailable) {
|
||||
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_NFTABLES_CHAIN_UNAVAILABLE", "ErrDockerNftablesChainUnavailable", err)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrDockerGuardInvalid) {
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID", "ErrInvalidParams", err)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrDockerUnavailable) {
|
||||
if errors.Is(err, docker.ErrUnavailable) {
|
||||
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE", "ErrDockerFailed", err)
|
||||
return
|
||||
}
|
||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary List firewall rule backups
|
||||
// @Param subsystem query string false "Firewall subsystem" Enums(system,forwarding,docker) default(system)
|
||||
// @Success 200 {object} dto.FirewallRuleBackups
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/backups [get]
|
||||
func (b *BaseApi) ListFirewallRuleBackups(c *gin.Context) {
|
||||
result, err := firewallService.ListRuleBackups(c.Request.Context(), c.DefaultQuery("subsystem", "system"))
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Initialize, repair or bind one firewall address family
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallFamilyOperation true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/family/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["subsystem","family","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] [subsystem] [family] 防火墙链","formatEN":"[operation] [subsystem] [family] firewall chains"}
|
||||
func (b *BaseApi) OperateFirewallFamily(c *gin.Context) {
|
||||
var request dto.FirewallFamilyOperation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallSettingService.OperateFamily(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Update firewall IPv6 support
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallIPv6Operation true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/settings/ipv6 [post]
|
||||
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"设置防火墙 IPv6 支持为 [status]","formatEN":"Set firewall IPv6 support to [status]"}
|
||||
func (b *BaseApi) OperateFirewallIPv6(c *gin.Context) {
|
||||
var request dto.FirewallIPv6Operation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallSettingService.OperateIPv6(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package helper
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
@@ -21,6 +23,7 @@ import (
|
||||
)
|
||||
|
||||
func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
||||
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||
res := dto.Response{
|
||||
Code: code,
|
||||
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) {
|
||||
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||
res := dto.Response{
|
||||
Code: code,
|
||||
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{}) {
|
||||
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||
res := dto.Response{
|
||||
Code: code,
|
||||
Data: data,
|
||||
@@ -184,3 +189,23 @@ func GetParamInt32(paramName string, c *gin.Context) (int32, error) {
|
||||
intNum, _ := strconv.Atoi(idParam)
|
||||
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
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
// @Tags Monitor
|
||||
@@ -32,14 +28,19 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags Monitor
|
||||
// @Summary Clean monitor data
|
||||
// @Summary Clean host or GPU monitor data
|
||||
// @Param request body dto.MonitorClean true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/monitor/clean [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空监控数据","formatEN":"clean monitor datas"}
|
||||
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 [type] 监控数据","formatEN":"clean [type] monitoring data"}
|
||||
func (b *BaseApi) CleanMonitor(c *gin.Context) {
|
||||
if err := monitorService.CleanData(); err != nil {
|
||||
var req dto.MonitorClean
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := monitorService.CleanData(req.Type); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
@@ -91,14 +92,7 @@ func (b *BaseApi) UpdateMonitorSetting(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/monitor/netoptions [get]
|
||||
func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
||||
netStat, _ := net.IOCounters(true)
|
||||
var options []string
|
||||
options = append(options, "all")
|
||||
for _, net := range netStat {
|
||||
options = append(options, net.Name)
|
||||
}
|
||||
sort.Strings(options)
|
||||
helper.SuccessWithData(c, options)
|
||||
helper.SuccessWithData(c, monitorService.LoadNetworkOptions())
|
||||
}
|
||||
|
||||
// @Tags Monitor
|
||||
@@ -108,12 +102,5 @@ func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/monitor/iooptions [get]
|
||||
func (b *BaseApi) GetIOOptions(c *gin.Context) {
|
||||
diskStat, _ := disk.IOCounters()
|
||||
var options []string
|
||||
options = append(options, "all")
|
||||
for _, net := range diskStat {
|
||||
options = append(options, net.Name)
|
||||
}
|
||||
sort.Strings(options)
|
||||
helper.SuccessWithData(c, options)
|
||||
helper.SuccessWithData(c, monitorService.LoadIOOptions())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (b *BaseApi) LoadVLLMMonitor(c *gin.Context) {
|
||||
var req dto.MonitorVLLMSearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := monitorService.LoadVLLMMonitorData(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 Timestamp
|
||||
// @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) {
|
||||
var req request.RuntimeDelete
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
err := runtimeService.Delete(req)
|
||||
name, err := runtimeService.Delete(req)
|
||||
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -169,7 +170,7 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /runtimes/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [id]","formatEN":"Operate runtime [id]"}
|
||||
// @x-panel-log {"bodyKeys":["ID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ID","isList":false,"db":"runtimes","output_column":"name","output_value":"name"}],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"}
|
||||
func (b *BaseApi) OperateRuntime(c *gin.Context) {
|
||||
var req request.RuntimeOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
|
||||
@@ -83,6 +83,7 @@ func (b *BaseApi) CreateRootCert(c *gin.Context) {
|
||||
}
|
||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if err := sshService.CreateRootCert(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
@@ -107,6 +108,7 @@ func (b *BaseApi) EditRootCert(c *gin.Context) {
|
||||
}
|
||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if err := sshService.EditRootCert(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
// @Summary Ws local terminal
|
||||
// @Param command query string false "command"
|
||||
// @Param session query string false "session id to reattach"
|
||||
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
@@ -43,6 +44,7 @@ func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
|
||||
// @Param command query string false "command"
|
||||
// @Param session query string false "session id to reattach"
|
||||
// @Param title query string false "session title shown in the session list"
|
||||
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
@@ -146,13 +148,14 @@ func (b *BaseApi) runSSHSession(c *gin.Context, kind string, connect func() (*ss
|
||||
hostID, _ = strconv.Atoi(c.DefaultQuery("id", "0"))
|
||||
}
|
||||
opts := terminal.SessionOptions{
|
||||
Identity: identity,
|
||||
Kind: kind,
|
||||
Title: sanitizeTerminalTitle(c.Query("title")),
|
||||
HostID: uint(max(hostID, 0)),
|
||||
Cols: cols,
|
||||
Rows: rows,
|
||||
InitCmd: command,
|
||||
Identity: identity,
|
||||
Kind: kind,
|
||||
Title: sanitizeTerminalTitle(c.Query("title")),
|
||||
Persistent: c.Query("terminalPersistent") == "true",
|
||||
HostID: uint(max(hostID, 0)),
|
||||
Cols: cols,
|
||||
Rows: rows,
|
||||
InitCmd: command,
|
||||
}
|
||||
err := terminal.Serve(wsConn, strings.TrimSpace(c.Query("session")), opts, func() (*gossh.Client, error) {
|
||||
client, err := connect()
|
||||
@@ -240,6 +243,16 @@ func loadTerminalIdentity(c *gin.Context) (terminal.Identity, bool) {
|
||||
UserID: strings.TrimSpace(c.GetHeader(terminal.HeaderUserID)),
|
||||
AuthSessionID: strings.TrimSpace(c.GetHeader(terminal.HeaderAuthSessionID)),
|
||||
}
|
||||
if value := c.GetHeader(terminal.HeaderAuthLeaseUntil); value != "" {
|
||||
millis, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || millis <= 0 {
|
||||
return terminal.Identity{}, false
|
||||
}
|
||||
identity.AuthLeaseUntil = time.UnixMilli(millis)
|
||||
if maximum := time.Now().Add(90 * time.Second); identity.AuthLeaseUntil.After(maximum) {
|
||||
identity.AuthLeaseUntil = maximum
|
||||
}
|
||||
}
|
||||
return identity, identity.Valid()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 Timestamp
|
||||
// @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) {
|
||||
var req request.WebsitePHPVersionReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"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()))
|
||||
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)
|
||||
}
|
||||
|
||||
+20
-8
@@ -9,7 +9,8 @@ type AgentCreateReq struct {
|
||||
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
|
||||
BridgePort int `json:"bridgePort"`
|
||||
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"`
|
||||
AccountID uint `json:"accountId"`
|
||||
Token string `json:"token"`
|
||||
@@ -110,6 +111,7 @@ type AgentBatchOperateResult struct {
|
||||
}
|
||||
|
||||
type AgentItem struct {
|
||||
AccessHost string `json:"accessHost"`
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Remark string `json:"remark"`
|
||||
@@ -162,16 +164,25 @@ type AgentWebsiteBindReq struct {
|
||||
}
|
||||
|
||||
type AgentModelConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
Model string `json:"model" validate:"required"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
Model string `json:"model" validate:"required"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
Metadata []AgentModelMetadata `json:"metadata" validate:"dive"`
|
||||
}
|
||||
|
||||
type AgentModelConfig struct {
|
||||
AccountID uint `json:"accountId"`
|
||||
Model string `json:"model"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
AccountID uint `json:"accountId"`
|
||||
Model string `json:"model"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
Metadata []AgentModelMetadata `json:"metadata"`
|
||||
}
|
||||
|
||||
type AgentModelMetadata struct {
|
||||
Model string `json:"model" validate:"required"`
|
||||
InputMode string `json:"inputMode" validate:"required,oneof=auto text image"`
|
||||
ContextWindow int `json:"contextWindow" validate:"min=0"`
|
||||
MaxTokens int `json:"maxTokens" validate:"min=0"`
|
||||
}
|
||||
|
||||
type AgentHermesChatSessionItem struct {
|
||||
@@ -375,6 +386,7 @@ type AgentAccountProviderCountReq struct {
|
||||
}
|
||||
|
||||
type AgentAccountInfo struct {
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
ID uint `json:"id"`
|
||||
MasterAccountID uint `json:"masterAccountId"`
|
||||
Provider string `json:"provider"`
|
||||
|
||||
+6
-5
@@ -14,16 +14,17 @@ type OllamaModelInfo struct {
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type OllamaModelDropList struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type OllamaModelName struct {
|
||||
Name string `json:"name"`
|
||||
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 {
|
||||
Domain string `json:"domain" 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 {
|
||||
Result string `json:"result,omitempty"`
|
||||
TaskName string `json:"taskName"`
|
||||
AlertType string `json:"alertType"`
|
||||
EntryID uint `json:"entryID"`
|
||||
@@ -53,6 +54,7 @@ type AlertDTO struct {
|
||||
Method string `json:"method"`
|
||||
Title string `json:"title"`
|
||||
Project string `json:"project"`
|
||||
TaskName string `json:"taskName,omitempty"`
|
||||
Status string `json:"status"`
|
||||
SendCount uint `json:"sendCount"`
|
||||
AdvancedParams string `json:"advancedParams"`
|
||||
|
||||
@@ -171,7 +171,8 @@ type ContainerItemStats struct {
|
||||
BuildCacheReclaimable int64 `json:"buildCacheReclaimable"`
|
||||
}
|
||||
type ContainerListStats struct {
|
||||
ContainerID string `json:"containerID"`
|
||||
ContainerID string `json:"containerID"`
|
||||
ContainerName string `json:"containerName"`
|
||||
|
||||
CPUTotalUsage uint64 `json:"cpuTotalUsage"`
|
||||
SystemUsage uint64 `json:"systemUsage"`
|
||||
|
||||
@@ -51,9 +51,10 @@ type CronjobOperate struct {
|
||||
Secret string `json:"secret"`
|
||||
Args string `json:"args"`
|
||||
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertMethod string `json:"alertMethod"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertMethod string `json:"alertMethod"`
|
||||
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
|
||||
|
||||
CleanLogConfig
|
||||
}
|
||||
@@ -126,7 +127,8 @@ type CronjobInfo struct {
|
||||
Secret string `json:"secret"`
|
||||
Args string `json:"args"`
|
||||
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTriggerMode string `json:"alertTriggerMode"`
|
||||
}
|
||||
|
||||
type CronjobImport struct {
|
||||
@@ -169,9 +171,10 @@ type CronjobTrans struct {
|
||||
SourceAccounts []string `json:"sourceAccounts"`
|
||||
DownloadAccount string `json:"downloadAccount"`
|
||||
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertMethod string `json:"alertMethod"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertMethod string `json:"alertMethod"`
|
||||
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
|
||||
}
|
||||
type TransHelper struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -31,7 +31,27 @@ type DashboardBase 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 {
|
||||
|
||||
@@ -22,6 +22,10 @@ type DBBaseInfo struct {
|
||||
Port int64 `json:"port"`
|
||||
}
|
||||
|
||||
type RedisCliInstall struct {
|
||||
TaskID string `json:"taskID" validate:"omitempty,uuid"`
|
||||
}
|
||||
|
||||
// mysql
|
||||
type MysqlDBSearch struct {
|
||||
PageInfo
|
||||
|
||||
+124
-132
@@ -1,11 +1,14 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||
)
|
||||
|
||||
type FirewallSubsystemStatus struct {
|
||||
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||
Name string `json:"name"`
|
||||
Backend string `json:"backend"`
|
||||
ConflictBackend string `json:"conflictBackend,omitempty"`
|
||||
@@ -17,16 +20,22 @@ type FirewallSubsystemStatus struct {
|
||||
PingStatus string `json:"pingStatus"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
SyncError string `json:"syncError,omitempty"`
|
||||
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
|
||||
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
||||
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
||||
}
|
||||
|
||||
type FirewallLifecycleOperation struct {
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
|
||||
WithDockerRestart bool `json:"withDockerRestart"`
|
||||
}
|
||||
|
||||
type FirewallLifecycleOperationResponse struct {
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Queued bool `json:"queued"`
|
||||
}
|
||||
|
||||
type FirewallBackendOption struct {
|
||||
Name string `json:"name"`
|
||||
Installed bool `json:"installed"`
|
||||
@@ -42,10 +51,12 @@ type FirewallBackendOption struct {
|
||||
}
|
||||
|
||||
type FirewallBackendFamilyStatus struct {
|
||||
Available bool `json:"available"`
|
||||
Initialized bool `json:"initialized"`
|
||||
Bound bool `json:"bound"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Partial bool `json:"partial"`
|
||||
Available bool `json:"available"`
|
||||
Initialized bool `json:"initialized"`
|
||||
Bound bool `json:"bound"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
RAInterfaces []string `json:"raInterfaces,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallBackendGroup struct {
|
||||
@@ -55,15 +66,27 @@ type FirewallBackendGroup struct {
|
||||
}
|
||||
|
||||
type FirewallSettings struct {
|
||||
System FirewallBackendGroup `json:"system"`
|
||||
Forwarding FirewallBackendGroup `json:"forwarding"`
|
||||
Docker FirewallBackendGroup `json:"docker"`
|
||||
PingStatus string `json:"pingStatus"`
|
||||
PortWhitelist string `json:"portWhiteList"`
|
||||
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||
System FirewallBackendGroup `json:"system"`
|
||||
Forwarding FirewallBackendGroup `json:"forwarding"`
|
||||
Docker FirewallBackendGroup `json:"docker"`
|
||||
PingStatus string `json:"pingStatus"`
|
||||
PortWhitelist []filter.PortWhitelist `json:"portWhiteList"`
|
||||
PanelPort string `json:"panelPort"`
|
||||
SSHPort string `json:"sshPort"`
|
||||
}
|
||||
|
||||
type FirewallPortWhitelistCreate struct {
|
||||
Rule filter.PortWhitelist `json:"rule" validate:"required"`
|
||||
}
|
||||
|
||||
type FirewallPortWhitelistUpdate struct {
|
||||
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 {
|
||||
@@ -72,6 +95,17 @@ type FirewallBackendOperation struct {
|
||||
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"`
|
||||
}
|
||||
|
||||
type FirewallIPv6Operation struct {
|
||||
Status string `json:"status" validate:"required,oneof=Enable Disable"`
|
||||
}
|
||||
|
||||
type FirewallFamilyOperation struct {
|
||||
Subsystem string `json:"subsystem" validate:"required,oneof=system forwarding docker"`
|
||||
Backend string `json:"backend" validate:"required,oneof=iptables nftables"`
|
||||
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
|
||||
Operation string `json:"operation" validate:"required,oneof=initialize repair bind"`
|
||||
}
|
||||
|
||||
type FilterChainOperation struct {
|
||||
Name string `json:"name" validate:"required,eq=1PANEL_BASIC"`
|
||||
Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"`
|
||||
@@ -84,45 +118,55 @@ type FilterChainOperationResponse struct {
|
||||
}
|
||||
|
||||
type FirewallInitializationTask struct {
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FirewallSystemPort struct {
|
||||
Family string
|
||||
Port string
|
||||
Protocol string
|
||||
}
|
||||
type FirewallSystemPort = firewall.SystemPort
|
||||
|
||||
type FirewallRuleInventoryResponse struct {
|
||||
IPv4Range filter.PositionRange `json:"ipv4Range"`
|
||||
IPv6Range filter.PositionRange `json:"ipv6Range"`
|
||||
Total int64 `json:"total"`
|
||||
AllTotal int64 `json:"allTotal"`
|
||||
ManagedTotal int64 `json:"managedTotal"`
|
||||
Items []filter.InventoryItem `json:"items"`
|
||||
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
||||
IPv4Range filter.PositionRange `json:"ipv4Range"`
|
||||
IPv6Range filter.PositionRange `json:"ipv6Range"`
|
||||
Total int64 `json:"total"`
|
||||
AllTotal int64 `json:"allTotal"`
|
||||
Items []filter.InventoryItem `json:"items"`
|
||||
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleBackup struct {
|
||||
Name string `json:"name"`
|
||||
Provider filter.Provider `json:"provider"`
|
||||
RuleCount int `json:"ruleCount"`
|
||||
ModifiedAt int64 `json:"modifiedAt"`
|
||||
}
|
||||
|
||||
type FirewallRuleBackups struct {
|
||||
Directory string `json:"directory"`
|
||||
Files []FirewallRuleBackup `json:"files"`
|
||||
}
|
||||
|
||||
type FirewallRuleResetResponse struct {
|
||||
Removed int `json:"removed"`
|
||||
Disabled bool `json:"disabled"`
|
||||
BackupPath string `json:"backupPath"`
|
||||
Removed int `json:"removed"`
|
||||
Disabled bool `json:"disabled"`
|
||||
}
|
||||
|
||||
type FirewallRuleReset struct {
|
||||
Subsystem string `json:"subsystem,omitempty" validate:"omitempty,oneof=system forwarding docker"`
|
||||
Backup *bool `json:"backup,omitempty" default:"true"`
|
||||
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
||||
WithDockerRestart bool `json:"withDockerRestart"`
|
||||
}
|
||||
|
||||
type FirewallRuleInventory struct {
|
||||
PageInfo
|
||||
Scope filter.Scope `json:"scope,omitempty"`
|
||||
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
|
||||
All bool `json:"all,omitempty"`
|
||||
Info string `json:"info"`
|
||||
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
|
||||
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
|
||||
States []filter.InventoryState `json:"states,omitempty" validate:"omitempty,dive,oneof=managed adopted external drifted protected"`
|
||||
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
|
||||
Scope filter.Scope `json:"scope,omitempty"`
|
||||
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
|
||||
All bool `json:"all,omitempty"`
|
||||
Info string `json:"info"`
|
||||
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
|
||||
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
|
||||
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
|
||||
}
|
||||
|
||||
type FirewallNativeDetail struct {
|
||||
@@ -133,6 +177,7 @@ type FirewallNativeDetail struct {
|
||||
}
|
||||
|
||||
type DockerPortGuardBase struct {
|
||||
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
IsExist bool `json:"isExist"`
|
||||
@@ -145,6 +190,7 @@ type DockerPortGuardBase struct {
|
||||
}
|
||||
|
||||
type DockerPortGuardFamilyStatus struct {
|
||||
Partial bool `json:"partial"`
|
||||
State string `json:"state"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Initialized bool `json:"initialized"`
|
||||
@@ -165,11 +211,8 @@ type DockerPortGuardEndpoint struct {
|
||||
Application string `json:"application,omitempty"`
|
||||
PolicyUUID string `json:"policyUUID,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
NativeAction string `json:"nativeAction,omitempty"`
|
||||
ReadOnly bool `json:"readOnly,omitempty"`
|
||||
Sources []string `json:"sources"`
|
||||
Effective bool `json:"effective"`
|
||||
Description string `json:"description,omitempty"`
|
||||
TrafficPath string `json:"trafficPath"`
|
||||
ManagementTarget string `json:"managementTarget"`
|
||||
ManagementReason string `json:"managementReason,omitempty"`
|
||||
@@ -206,6 +249,7 @@ type DockerPortGuardEndpointIdentity struct {
|
||||
|
||||
type DockerPortGuardPolicyBatch struct {
|
||||
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
|
||||
Import bool `json:"import"`
|
||||
}
|
||||
|
||||
type DockerPortGuardPolicyBatchDelete struct {
|
||||
@@ -214,29 +258,27 @@ type DockerPortGuardPolicyBatchDelete struct {
|
||||
|
||||
type DockerPortGuardPolicy struct {
|
||||
DockerPortGuardEndpointIdentity
|
||||
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all"`
|
||||
Sources []string `json:"sources" validate:"dive,required,max=64"`
|
||||
Description string `json:"description" validate:"max=256"`
|
||||
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all accept_sources accept_all"`
|
||||
Sources []string `json:"sources" validate:"dive,required,max=64"`
|
||||
}
|
||||
|
||||
type DockerPortGuardOperation struct {
|
||||
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
|
||||
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"`
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FirewallRuleCreateItem struct {
|
||||
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
||||
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
|
||||
SourceID string `json:"sourceID"`
|
||||
Raw string `json:"raw,omitempty"`
|
||||
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
|
||||
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
||||
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
|
||||
}
|
||||
|
||||
type FirewallRuleCreate struct {
|
||||
Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,dive"`
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
Initialize bool `json:"initialize"`
|
||||
Items []FirewallRuleCreateItem `json:"items" validate:"dive"`
|
||||
}
|
||||
|
||||
type FirewallRuleCreateResponse struct {
|
||||
@@ -255,81 +297,36 @@ type FirewallRuleCreateFailure struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncRequest struct {
|
||||
Subsystem string `json:"subsystem" validate:"omitempty,oneof=system forwarding docker"`
|
||||
SourceProvider filter.Provider `json:"sourceProvider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
||||
TargetProvider filter.Provider `json:"targetProvider" validate:"required,oneof=firewalld ufw iptables nftables"`
|
||||
ResetSource bool `json:"resetSource"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncItem struct {
|
||||
SourceUUID string `json:"sourceUUID"`
|
||||
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
||||
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
|
||||
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
|
||||
Status firewallsync.Status `json:"status"`
|
||||
ReasonCode firewallsync.ReasonCode `json:"reasonCode,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncPreview struct {
|
||||
Subsystem string `json:"subsystem"`
|
||||
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
|
||||
TargetProvider filter.Provider `json:"targetProvider"`
|
||||
Total int `json:"total"`
|
||||
Ready int `json:"ready"`
|
||||
Existing int `json:"existing"`
|
||||
Removed int `json:"removed"`
|
||||
Blocked int `json:"blocked"`
|
||||
Items []FirewallRuleSyncItem `json:"items"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncResult struct {
|
||||
Subsystem string `json:"subsystem"`
|
||||
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
|
||||
TargetProvider filter.Provider `json:"targetProvider"`
|
||||
Total int `json:"total"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Skipped int `json:"skipped"`
|
||||
Removed int `json:"removed"`
|
||||
Failed int `json:"failed"`
|
||||
Errors []FirewallRuleSyncFailure `json:"errors,omitempty"`
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Queued bool `json:"queued,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncTask struct {
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Executing bool `json:"executing"`
|
||||
}
|
||||
|
||||
type FirewallRuleSyncFailure struct {
|
||||
SourceUUID string `json:"sourceUUID"`
|
||||
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
||||
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
|
||||
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type FirewallRuleDelete struct {
|
||||
UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"`
|
||||
Targets []FirewallRuleDeleteItem `json:"targets" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteItem struct {
|
||||
FirewallRuleDeleteTarget
|
||||
Observed filter.ObservedRule `json:"observed" validate:"required"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteTarget struct {
|
||||
Scope filter.Scope `json:"scope" validate:"required"`
|
||||
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteResponse struct {
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Queued bool `json:"queued,omitempty"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
Errors []FirewallRuleDeleteFailure `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteFailure struct {
|
||||
Index int `json:"index"`
|
||||
UUID string `json:"uuid"`
|
||||
Error string `json:"error"`
|
||||
Index int `json:"index"`
|
||||
InstanceKey string `json:"instanceKey"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type FirewallRuleUpdate struct {
|
||||
UUID string `json:"uuid" validate:"required,max=64"`
|
||||
FirewallRuleDeleteTarget
|
||||
Rule *filter.FirewallRule `json:"rule,omitempty" validate:"required_without_all=Description OrderIndex Priority,excluded_with=Description OrderIndex Priority"`
|
||||
Description *string `json:"description,omitempty" validate:"excluded_with=Rule"`
|
||||
OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"`
|
||||
@@ -337,26 +334,21 @@ type FirewallRuleUpdate struct {
|
||||
}
|
||||
|
||||
type FirewallRuleReorder struct {
|
||||
UUID string `json:"uuid" validate:"required,max=64"`
|
||||
FirewallRuleDeleteTarget
|
||||
TargetPosition *int64 `json:"targetPosition"`
|
||||
Priority *int `json:"priority"`
|
||||
}
|
||||
|
||||
func (p *FirewallRuleSyncPreview) Add(item FirewallRuleSyncItem) {
|
||||
p.Items = append(p.Items, item)
|
||||
switch item.Status {
|
||||
case firewallsync.StatusReady:
|
||||
p.Ready++
|
||||
p.Total++
|
||||
case firewallsync.StatusExisting:
|
||||
p.Existing++
|
||||
p.Total++
|
||||
case firewallsync.StatusRemove:
|
||||
p.Removed++
|
||||
case firewallsync.StatusBlocked:
|
||||
p.Blocked++
|
||||
if item.ReasonCode != firewallsync.ReasonReadOnlyRule {
|
||||
p.Total++
|
||||
}
|
||||
}
|
||||
type FirewallRuleExportItem struct {
|
||||
filter.FirewallRule
|
||||
Raw string `json:"raw,omitempty"`
|
||||
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallSubsystemBackup struct {
|
||||
Families []string `json:"families,omitempty"`
|
||||
Subsystem string `json:"subsystem"`
|
||||
Provider filter.Provider `json:"provider"`
|
||||
Forwarding []forwarding.Rule `json:"forwarding"`
|
||||
Docker *dockerfirewall.PolicyInventory `json:"docker,omitempty"`
|
||||
}
|
||||
|
||||
@@ -24,15 +24,11 @@ type ForwardRule struct {
|
||||
|
||||
UsedStatus string `json:"usedStatus"`
|
||||
Description string `json:"description"`
|
||||
|
||||
IsDesired bool `json:"isDesired"`
|
||||
IsRuntime bool `json:"isRuntime"`
|
||||
SyncStatus string `json:"syncStatus"`
|
||||
}
|
||||
|
||||
type ForwardRuleOperate struct {
|
||||
ForceDelete bool `json:"forceDelete"`
|
||||
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
|
||||
Import bool `json:"import"`
|
||||
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type ForwardRuleOperation struct {
|
||||
|
||||
+94
-18
@@ -1,6 +1,11 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
)
|
||||
|
||||
type MonitorSearch struct {
|
||||
Param string `json:"param" validate:"required,oneof=all cpu memory load io network"`
|
||||
@@ -26,24 +31,33 @@ type Process struct {
|
||||
}
|
||||
|
||||
type MonitorSetting struct {
|
||||
MonitorStatus string `json:"monitorStatus"`
|
||||
MonitorStoreDays string `json:"monitorStoreDays"`
|
||||
MonitorInterval string `json:"monitorInterval"`
|
||||
DefaultNetwork string `json:"defaultNetwork"`
|
||||
DefaultIO string `json:"defaultIO"`
|
||||
GPUMonitorStatus string `json:"gpuMonitorStatus"`
|
||||
GPUMonitorStoreDays string `json:"gpuMonitorStoreDays"`
|
||||
GPUMonitorInterval string `json:"gpuMonitorInterval"`
|
||||
VLLMMonitorStatus string `json:"vllmMonitorStatus"`
|
||||
VLLMMonitorStoreDays string `json:"vllmMonitorStoreDays"`
|
||||
VLLMMonitorInterval string `json:"vllmMonitorInterval"`
|
||||
MonitorStatus string `json:"monitorStatus"`
|
||||
MonitorStoreDays string `json:"monitorStoreDays"`
|
||||
MonitorInterval string `json:"monitorInterval"`
|
||||
DefaultNetwork string `json:"defaultNetwork"`
|
||||
DefaultIO string `json:"defaultIO"`
|
||||
}
|
||||
|
||||
type MonitorSettingUpdate struct {
|
||||
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval DefaultNetwork DefaultIO"`
|
||||
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval GPUMonitorStatus GPUMonitorStoreDays GPUMonitorInterval VLLMMonitorStatus VLLMMonitorStoreDays VLLMMonitorInterval DefaultNetwork DefaultIO"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type MonitorGPUOptions struct {
|
||||
Supported bool `json:"supported"`
|
||||
GPUType string `json:"gpuType"`
|
||||
ChartHide []GPUChartHide `json:"chartHide"`
|
||||
Options []string `json:"options"`
|
||||
}
|
||||
type GPUChartHide struct {
|
||||
DeviceID string `json:"deviceID"`
|
||||
Legacy bool `json:"legacy"`
|
||||
ProductName string `json:"productName"`
|
||||
Type string `json:"type"`
|
||||
Process bool `json:"process"`
|
||||
@@ -55,23 +69,54 @@ type GPUChartHide struct {
|
||||
Speed bool `json:"speed"`
|
||||
}
|
||||
type MonitorGPUSearch struct {
|
||||
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
|
||||
DeviceID string `json:"deviceID"`
|
||||
Legacy bool `json:"legacy"`
|
||||
ProductName string `json:"productName"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
}
|
||||
type MonitorGPUData struct {
|
||||
Date []time.Time `json:"date"`
|
||||
GPUValue []float64 `json:"gpuValue"`
|
||||
TemperatureValue []float64 `json:"temperatureValue"`
|
||||
PowerTotal []float64 `json:"powerTotal"`
|
||||
PowerUsed []float64 `json:"powerUsed"`
|
||||
PowerPercent []float64 `json:"powerPercent"`
|
||||
MemoryTotal []float64 `json:"memoryTotal"`
|
||||
MemoryUsed []float64 `json:"memoryUsed"`
|
||||
MemoryPercent []float64 `json:"memoryPercent"`
|
||||
SpeedValue []int `json:"speedValue"`
|
||||
MemoryActivity []*float64 `json:"memoryActivity"`
|
||||
EncoderUtil []*float64 `json:"encoderUtil"`
|
||||
DecoderUtil []*float64 `json:"decoderUtil"`
|
||||
JPEGUtil []*float64 `json:"jpegUtil"`
|
||||
OFAUtil []*float64 `json:"ofaUtil"`
|
||||
MediaUtil []*float64 `json:"mediaUtil"`
|
||||
ComputeUtil []*float64 `json:"computeUtil"`
|
||||
CopyUtil []*float64 `json:"copyUtil"`
|
||||
HotspotTemperature []*float64 `json:"hotspotTemperature"`
|
||||
FanRPM []*float64 `json:"fanRPM"`
|
||||
AICPUUtil []*float64 `json:"aiCPUUtil"`
|
||||
CtrlCPUUtil []*float64 `json:"ctrlCPUUtil"`
|
||||
DDRUsed []*float64 `json:"ddrUsed"`
|
||||
DDRTotal []*float64 `json:"ddrTotal"`
|
||||
HBMUsed []*float64 `json:"hbmUsed"`
|
||||
HBMTotal []*float64 `json:"hbmTotal"`
|
||||
DDRBandwidth []*float64 `json:"ddrBandwidth"`
|
||||
HBMBandwidth []*float64 `json:"hbmBandwidth"`
|
||||
MemoryBandwidth []*float64 `json:"memoryBandwidth"`
|
||||
MediaFrequency []*float64 `json:"mediaFrequency"`
|
||||
HugepagesUsed []*float64 `json:"hugepagesUsed"`
|
||||
HugepagesTotal []*float64 `json:"hugepagesTotal"`
|
||||
|
||||
ProcessCount []int `json:"processCount"`
|
||||
BucketSeconds int64 `json:"bucketSeconds"`
|
||||
SampleCount int64 `json:"sampleCount"`
|
||||
MemoryTemperatureValue []*float64 `json:"memoryTemperatureValue"`
|
||||
FrequencyValue []*float64 `json:"frequencyValue"`
|
||||
MemoryFrequencyValue []*float64 `json:"memoryFrequencyValue"`
|
||||
Date []time.Time `json:"date"`
|
||||
GPUValue []*float64 `json:"gpuValue"`
|
||||
TemperatureValue []*float64 `json:"temperatureValue"`
|
||||
PowerTotal []*float64 `json:"powerTotal"`
|
||||
PowerUsed []*float64 `json:"powerUsed"`
|
||||
PowerPercent []*float64 `json:"powerPercent"`
|
||||
MemoryTotal []*float64 `json:"memoryTotal"`
|
||||
MemoryUsed []*float64 `json:"memoryUsed"`
|
||||
MemoryPercent []*float64 `json:"memoryPercent"`
|
||||
SpeedValue []*float64 `json:"speedValue"`
|
||||
|
||||
ProcessCount []*float64 `json:"processCount"`
|
||||
GPUProcesses [][]GPUProcess `json:"gpuProcesses"`
|
||||
}
|
||||
|
||||
@@ -81,3 +126,34 @@ type GPUProcess struct {
|
||||
ProcessName string `json:"processName"`
|
||||
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"`
|
||||
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
|
||||
NodePushConfig
|
||||
}
|
||||
@@ -122,9 +128,12 @@ type AppInstallDelete struct {
|
||||
}
|
||||
|
||||
type AppInstalledUpdate struct {
|
||||
InstallId uint `json:"installId" validate:"required"`
|
||||
Params map[string]interface{} `json:"params" validate:"required"`
|
||||
TaskID string `json:"-"`
|
||||
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"`
|
||||
Params map[string]interface{} `json:"params" validate:"required"`
|
||||
TaskID string `json:"-"`
|
||||
AppContainerConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ type FileWget struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
IgnoreCertificate bool `json:"ignoreCertificate"`
|
||||
UseProxy bool `json:"useProxy"`
|
||||
UseServerFilename bool `json:"useServerFilename"`
|
||||
}
|
||||
|
||||
type FileMove struct {
|
||||
|
||||
@@ -182,3 +182,12 @@ type WebsiteSSLFileUpload struct {
|
||||
PushNode bool `json:"pushNode"`
|
||||
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,19 +50,21 @@ type TagDTO struct {
|
||||
}
|
||||
|
||||
type AppInstalledCheck struct {
|
||||
IsExist bool `json:"isExist"`
|
||||
Name string `json:"name"`
|
||||
App string `json:"app"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
LastBackupAt string `json:"lastBackupAt"`
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
ContainerName string `json:"containerName"`
|
||||
InstallPath string `json:"installPath"`
|
||||
HttpPort int `json:"httpPort"`
|
||||
HttpsPort int `json:"httpsPort"`
|
||||
WebsiteDir string `json:"websiteDir"`
|
||||
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||
IsExist bool `json:"isExist"`
|
||||
Name string `json:"name"`
|
||||
App string `json:"app"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
LastBackupAt string `json:"lastBackupAt"`
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
ContainerName string `json:"containerName"`
|
||||
InstallPath string `json:"installPath"`
|
||||
HttpPort int `json:"httpPort"`
|
||||
HttpsPort int `json:"httpsPort"`
|
||||
WebsiteDir string `json:"websiteDir"`
|
||||
}
|
||||
|
||||
type AppDetailDTO struct {
|
||||
@@ -184,7 +186,13 @@ type AppParam struct {
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Params []AppParam `json:"params"`
|
||||
RawCompose string `json:"rawCompose"`
|
||||
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"`
|
||||
RawCompose string `json:"rawCompose"`
|
||||
request.AppContainerConfig
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ type RootCertOperate struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Mode string `json:"mode"`
|
||||
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
|
||||
EncryptionMode string `json:"encryptionMode"`
|
||||
PassPhrase string `json:"passPhrase"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
|
||||
@@ -2,6 +2,7 @@ package model
|
||||
|
||||
type AgentAccount struct {
|
||||
BaseModel
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
Provider string `json:"provider"`
|
||||
Name string `json:"name"`
|
||||
APIKey string `json:"apiKey"`
|
||||
|
||||
@@ -28,6 +28,10 @@ type AppInstall struct {
|
||||
Favorite bool `json:"favorite"`
|
||||
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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,13 @@ type AppLauncher struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type QuickDirectory struct {
|
||||
BaseModel
|
||||
Path string `json:"path"`
|
||||
Alias string `json:"alias"`
|
||||
Legacy bool `json:"legacy" gorm:"-"`
|
||||
}
|
||||
|
||||
type QuickJump struct {
|
||||
BaseModel
|
||||
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
|
||||
|
||||
import "time"
|
||||
|
||||
type MonitorBase struct {
|
||||
BaseModel
|
||||
Cpu float64 `json:"cpu"`
|
||||
@@ -33,14 +35,88 @@ type MonitorNetwork struct {
|
||||
}
|
||||
|
||||
type MonitorGPU struct {
|
||||
MemoryUtil *float64 `json:"memoryUtil"`
|
||||
MemoryActivity *float64 `json:"memoryActivity"`
|
||||
EncoderUtil *float64 `json:"encoderUtil"`
|
||||
DecoderUtil *float64 `json:"decoderUtil"`
|
||||
JPEGUtil *float64 `json:"jpegUtil"`
|
||||
OFAUtil *float64 `json:"ofaUtil"`
|
||||
MediaUtil *float64 `json:"mediaUtil"`
|
||||
ComputeUtil *float64 `json:"computeUtil"`
|
||||
CopyUtil *float64 `json:"copyUtil"`
|
||||
HotspotTemperature *float64 `json:"hotspotTemperature"`
|
||||
FanRPM *float64 `json:"fanRPM"`
|
||||
AICPUUtil *float64 `json:"aiCPUUtil"`
|
||||
CtrlCPUUtil *float64 `json:"ctrlCPUUtil"`
|
||||
DDRUsed *float64 `json:"ddrUsed"`
|
||||
DDRTotal *float64 `json:"ddrTotal"`
|
||||
HBMUsed *float64 `json:"hbmUsed"`
|
||||
HBMTotal *float64 `json:"hbmTotal"`
|
||||
DDRBandwidth *float64 `json:"ddrBandwidth"`
|
||||
HBMBandwidth *float64 `json:"hbmBandwidth"`
|
||||
MemoryBandwidth *float64 `json:"memoryBandwidth"`
|
||||
MediaFrequency *float64 `json:"mediaFrequency"`
|
||||
HugepagesUsed *float64 `json:"hugepagesUsed"`
|
||||
HugepagesTotal *float64 `json:"hugepagesTotal"`
|
||||
|
||||
MemoryTemperature *float64 `json:"memoryTemperature"`
|
||||
DeviceID string `json:"deviceID"`
|
||||
DeviceType string `json:"deviceType"`
|
||||
ProcessStatus string `json:"processStatus"`
|
||||
Frequency *float64 `json:"frequency"`
|
||||
MemoryFrequency *float64 `json:"memoryFrequency"`
|
||||
IntervalSeconds int `json:"intervalSeconds"`
|
||||
BaseModel
|
||||
ProductName string `json:"productName"`
|
||||
GPUUtil float64 `json:"gpuUtil"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
PowerDraw float64 `json:"powerDraw"`
|
||||
MaxPowerLimit float64 `json:"maxPowerLimit"`
|
||||
MemUsed float64 `json:"memUsed"`
|
||||
MemTotal float64 `json:"memTotal"`
|
||||
FanSpeed int `json:"fanSpeed"`
|
||||
Processes string `json:"processes"`
|
||||
ProductName string `json:"productName"`
|
||||
GPUUtil *float64 `json:"gpuUtil"`
|
||||
Temperature *float64 `json:"temperature"`
|
||||
PowerDraw *float64 `json:"powerDraw"`
|
||||
MaxPowerLimit *float64 `json:"maxPowerLimit"`
|
||||
MemUsed *float64 `json:"memUsed"`
|
||||
MemTotal *float64 `json:"memTotal"`
|
||||
FanSpeed *float64 `json:"fanSpeed"`
|
||||
Processes string `json:"processes"`
|
||||
}
|
||||
|
||||
type MonitorVLLM struct {
|
||||
ID uint `json:"-" gorm:"primarykey;autoIncrement"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
AppInstallID uint `json:"appInstallID"`
|
||||
Status string `json:"status"`
|
||||
RawMetrics string `json:"-"`
|
||||
HistogramDeltas string `json:"-"`
|
||||
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"},
|
||||
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": {
|
||||
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: "qwen3.5-plus", Name: "Qwen3.5-Plus"},
|
||||
{ID: "glm-5", Name: "GLM-5"},
|
||||
{ID: "MiniMax-M2.5", Name: "MiniMax M2.5"},
|
||||
{ID: "qwen3.6-plus", Name: "Qwen3.6-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: "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": {
|
||||
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["anthropic-version"] = "2023-06-01"
|
||||
request.Body = mustJSON(map[string]interface{}{
|
||||
body := map[string]interface{}{
|
||||
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
||||
"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":
|
||||
request.URL = baseURL + "/responses"
|
||||
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) != "" {
|
||||
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,
|
||||
})
|
||||
}
|
||||
if provider == "minimax" && model == "MiniMax-M3.1-Flash" {
|
||||
body["reasoning_effort"] = "max"
|
||||
}
|
||||
request.Body = mustJSON(body)
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -18,6 +19,7 @@ type IAgentAccountRepo interface {
|
||||
DeleteByID(id uint) error
|
||||
List(opts ...DBOption) ([]model.AgentAccount, error)
|
||||
CountTextByProviders(providers []string) (map[string]int64, error)
|
||||
SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error
|
||||
}
|
||||
|
||||
func NewIAgentAccountRepo() IAgentAccountRepo {
|
||||
@@ -97,6 +99,25 @@ func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]i
|
||||
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 {
|
||||
seen := make(map[string]struct{}, len(providers))
|
||||
result := make([]string, 0, len(providers))
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
)
|
||||
@@ -14,6 +16,7 @@ type IAiRepo interface {
|
||||
Create(cronjob *model.OllamaModel) error
|
||||
Update(id uint, vars map[string]interface{}) error
|
||||
Delete(opts ...DBOption) error
|
||||
DeleteAll(ctx context.Context) error
|
||||
}
|
||||
|
||||
func NewIAiRepo() IAiRepo {
|
||||
@@ -67,3 +70,7 @@ func (u *AiRepo) Delete(opts ...DBOption) 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 (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type LauncherRepo struct{}
|
||||
@@ -17,6 +18,11 @@ type ILauncherRepo interface {
|
||||
GetQuickJump(opts ...DBOption) (model.QuickJump, error)
|
||||
ListQuickJump(withAll bool) []model.QuickJump
|
||||
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 {
|
||||
@@ -95,13 +101,73 @@ func (u *LauncherRepo) UpdateQuicks(quicks []model.QuickJump) error {
|
||||
for _, item := range quicks {
|
||||
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
||||
"is_show": item.IsShow,
|
||||
"detail": item.Detail,
|
||||
"alias": item.Alias,
|
||||
}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
+103
-13
@@ -1,6 +1,8 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
@@ -10,11 +12,25 @@ import (
|
||||
|
||||
type MonitorRepo struct{}
|
||||
|
||||
type GPUHistoryPoint struct {
|
||||
model.MonitorGPU
|
||||
Bucket int64
|
||||
PowerPercent *float64
|
||||
MemoryPercent *float64
|
||||
ProcessCount *float64
|
||||
}
|
||||
|
||||
type IMonitorRepo interface {
|
||||
CleanHost() error
|
||||
CleanGPU() error
|
||||
GetBase(opts ...DBOption) ([]model.MonitorBase, error)
|
||||
GetGPU(opts ...DBOption) ([]model.MonitorGPU, error)
|
||||
CountGPU(opts ...DBOption) (int64, error)
|
||||
GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error)
|
||||
GetGPUDevices() ([]model.MonitorGPU, error)
|
||||
GetIO(opts ...DBOption) ([]model.MonitorIO, error)
|
||||
GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, error)
|
||||
GetIONames() ([]string, error)
|
||||
GetNetworkNames() ([]string, error)
|
||||
|
||||
CreateMonitorBase(model model.MonitorBase) error
|
||||
BatchCreateMonitorGPU(list []model.MonitorGPU) error
|
||||
@@ -25,13 +41,26 @@ type IMonitorRepo interface {
|
||||
DelMonitorIO(timeForDelete time.Time) error
|
||||
DelMonitorNet(timeForDelete time.Time) error
|
||||
|
||||
WithByProductName(name string) DBOption
|
||||
WithByGPUDevice(deviceID, name string, legacy bool) DBOption
|
||||
}
|
||||
|
||||
func NewIMonitorRepo() IMonitorRepo {
|
||||
return &MonitorRepo{}
|
||||
}
|
||||
|
||||
func (s *MonitorRepo) CleanHost() error {
|
||||
for _, item := range []interface{}{&model.MonitorBase{}, &model.MonitorIO{}, &model.MonitorNetwork{}} {
|
||||
if err := global.MonitorDB.Where("1 = 1").Delete(item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MonitorRepo) CleanGPU() error {
|
||||
return global.GPUMonitorDB.Where("1 = 1").Delete(&model.MonitorGPU{}).Error
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) GetBase(opts ...DBOption) ([]model.MonitorBase, error) {
|
||||
var data []model.MonitorBase
|
||||
db := global.MonitorDB
|
||||
@@ -59,14 +88,16 @@ func (u *MonitorRepo) GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, erro
|
||||
err := db.Find(&data).Error
|
||||
return data, err
|
||||
}
|
||||
func (u *MonitorRepo) GetGPU(opts ...DBOption) ([]model.MonitorGPU, error) {
|
||||
var data []model.MonitorGPU
|
||||
db := global.GPUMonitorDB
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
err := db.Find(&data).Error
|
||||
return data, err
|
||||
func (u *MonitorRepo) GetIONames() ([]string, error) {
|
||||
var names []string
|
||||
err := global.MonitorDB.Model(&model.MonitorIO{}).Distinct().Pluck("name", &names).Error
|
||||
return names, err
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) GetNetworkNames() ([]string, error) {
|
||||
var names []string
|
||||
err := global.MonitorDB.Model(&model.MonitorNetwork{}).Distinct().Pluck("name", &names).Error
|
||||
return names, err
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
|
||||
@@ -97,8 +128,67 @@ func (s *MonitorRepo) DelMonitorGPU(timeForDelete time.Time) error {
|
||||
return global.GPUMonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorGPU{}).Error
|
||||
}
|
||||
|
||||
func (s *MonitorRepo) WithByProductName(name string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("product_name = ?", name)
|
||||
func (u *MonitorRepo) GetGPUDevices() ([]model.MonitorGPU, error) {
|
||||
var data []model.MonitorGPU
|
||||
err := global.GPUMonitorDB.Model(&model.MonitorGPU{}).Select("device_id, product_name, device_type").Group("device_id, product_name, device_type").Order("product_name, device_id").Find(&data).Error
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) WithByGPUDevice(deviceID, name string, legacy bool) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
if deviceID != "" {
|
||||
return db.Where("device_id = ?", deviceID)
|
||||
}
|
||||
db = db.Where("product_name = ?", name)
|
||||
if legacy {
|
||||
db = db.Where("device_id IS NULL OR device_id = ''")
|
||||
}
|
||||
return db
|
||||
}
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) CountGPU(opts ...DBOption) (int64, error) {
|
||||
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
var count int64
|
||||
err := db.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error) {
|
||||
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
expressions := []string{
|
||||
"CASE WHEN max_power_limit > 0 THEN 100.0 * power_draw / max_power_limit END",
|
||||
"CASE WHEN mem_total > 0 AND mem_used IS NOT NULL THEN 100.0 * mem_used / mem_total ELSE memory_util END",
|
||||
"CASE WHEN (process_status = 'ok' OR process_status IS NULL OR process_status = '') AND json_valid(processes) THEN CASE WHEN json_type(processes) = 'array' THEN json_array_length(processes) END END",
|
||||
}
|
||||
aliases := []string{"power_percent", "memory_percent", "process_count"}
|
||||
columns := []string{"*"}
|
||||
if bucketSeconds > 0 {
|
||||
operation := "AVG"
|
||||
if aggregation == "max" {
|
||||
operation = "MAX"
|
||||
}
|
||||
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), bucketSeconds)}
|
||||
for _, column := range []string{"memory_activity", "encoder_util", "decoder_util", "jpeg_util", "ofa_util", "media_util", "compute_util", "copy_util", "hotspot_temperature", "fan_rpm", "ai_cpu_util", "ctrl_cpu_util", "ddr_used", "ddr_total", "hbm_used", "hbm_total", "ddr_bandwidth", "hbm_bandwidth", "memory_bandwidth", "media_frequency", "hugepages_used", "hugepages_total", "gpu_util", "temperature", "memory_temperature", "power_draw", "max_power_limit", "mem_used", "mem_total", "frequency", "memory_frequency", "fan_speed"} {
|
||||
columns = append(columns, operation+"("+column+") AS "+column)
|
||||
}
|
||||
for i := range expressions {
|
||||
expressions[i] = operation + "(" + expressions[i] + ")"
|
||||
}
|
||||
db = db.Group("bucket").Order("bucket ASC")
|
||||
} else {
|
||||
db = db.Order("created_at ASC, id ASC")
|
||||
}
|
||||
for i, expression := range expressions {
|
||||
columns = append(columns, expression+" AS "+aliases[i])
|
||||
}
|
||||
var data []GPUHistoryPoint
|
||||
err := db.Select(strings.Join(columns, ", ")).Scan(&data).Error
|
||||
return data, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type SettingRepo struct{}
|
||||
@@ -14,17 +16,22 @@ type ISettingRepo interface {
|
||||
GetList(opts ...DBOption) ([]model.Setting, error)
|
||||
Get(opts ...DBOption) (model.Setting, error)
|
||||
GetValueByKey(key string) (string, error)
|
||||
GetCoreValueByKey(key string) (string, error)
|
||||
Create(key, value string) error
|
||||
Update(key, value string) error
|
||||
WithByKey(key string) DBOption
|
||||
|
||||
UpdateOrCreate(key, value string) error
|
||||
UpdateValues(map[string]string) error
|
||||
|
||||
GetDescription(opts ...DBOption) (model.CommonDescription, error)
|
||||
GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error)
|
||||
CreateDescription(data *model.CommonDescription) error
|
||||
SaveDescriptions(context.Context, []model.CommonDescription) error
|
||||
UpdateDescription(id string, val map[string]interface{}) error
|
||||
DelDescription(id string) error
|
||||
DeleteDescriptions(context.Context, string, []string, bool) (int64, error)
|
||||
WithDescriptionIDs(ids []string) DBOption
|
||||
WithByDescriptionID(id string) DBOption
|
||||
}
|
||||
|
||||
@@ -68,6 +75,17 @@ func (s *SettingRepo) GetValueByKey(key string) (string, error) {
|
||||
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 {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
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
|
||||
}
|
||||
|
||||
func (s *SettingRepo) UpdateValues(values map[string]string) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
for key, value := range values {
|
||||
var setting model.Setting
|
||||
err := tx.Where("key = ?", key).First(&setting).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err := tx.Create(&model.Setting{Key: key, Value: value}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else if err := tx.Model(&setting).UpdateColumn("value", value).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SettingRepo) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) {
|
||||
var lists []model.CommonDescription
|
||||
db := global.DB.Model(&model.CommonDescription{})
|
||||
@@ -111,14 +148,42 @@ func (s *SettingRepo) GetDescription(opts ...DBOption) (model.CommonDescription,
|
||||
func (s *SettingRepo) CreateDescription(data *model.CommonDescription) error {
|
||||
return global.DB.Create(data).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) SaveDescriptions(ctx context.Context, descriptions []model.CommonDescription) error {
|
||||
return global.DB.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"description"}),
|
||||
}).CreateInBatches(&descriptions, 100).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) UpdateDescription(id string, val map[string]interface{}) error {
|
||||
return global.DB.Model(&model.CommonDescription{}).Where("id = ?", id).Updates(val).Error
|
||||
}
|
||||
func (s *SettingRepo) DelDescription(id string) error {
|
||||
return global.DB.Where("id = ?", id).Delete(&model.CommonDescription{}).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) DeleteDescriptions(ctx context.Context, kind string, ids []string, emptyOnly bool) (int64, error) {
|
||||
var deleted int64
|
||||
for start := 0; start < len(ids); start += 500 {
|
||||
query := global.DB.WithContext(ctx).Where("type = ? AND id IN ?", kind, ids[start:min(start+500, len(ids))])
|
||||
if emptyOnly {
|
||||
query = query.Where("description = ? AND is_pinned = ?", "", false)
|
||||
}
|
||||
result := query.Delete(&model.CommonDescription{})
|
||||
deleted += result.RowsAffected
|
||||
if result.Error != nil {
|
||||
return deleted, result.Error
|
||||
}
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
func (s *SettingRepo) WithByDescriptionID(id string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("id = ?", id)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SettingRepo) WithDescriptionIDs(ids []string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB { return db.Where("id IN ?", ids) }
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"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 {
|
||||
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) {
|
||||
|
||||
+60
-10
@@ -142,6 +142,9 @@ const (
|
||||
|
||||
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
agentType := req.AgentType
|
||||
if err := validateNativeAgentCreate(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := checkPortExist(req.WebUIPort); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -237,6 +240,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
}
|
||||
} else if agentType == constant.AppCopaw {
|
||||
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{}{
|
||||
@@ -245,6 +250,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
constant.HostIP: "",
|
||||
}
|
||||
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 allowedOrigin := firstAllowedOrigin(allowedOrigins); 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) {
|
||||
if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
|
||||
if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
|
||||
params["PANEL_APP_PORT_HTTPS"] = webUIPort
|
||||
return
|
||||
}
|
||||
@@ -936,6 +947,7 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig,
|
||||
AccountID: agent.AccountID,
|
||||
Model: model,
|
||||
Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
|
||||
Metadata: extractOpenclawModelMetadata(conf, account, models),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -967,7 +979,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil {
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks, req.Metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1043,7 +1055,7 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -1096,12 +1108,15 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||
if strings.TrimSpace(requestedVerifyModel) == "" {
|
||||
requestedVerifyModel = account.VerifyModel
|
||||
}
|
||||
verifyModel, err := resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
||||
if err != nil {
|
||||
return err
|
||||
verifyModel := ""
|
||||
if account.AppInstallID == 0 || len(models) > 0 {
|
||||
verifyModel, err = resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -1115,7 +1130,7 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||
account.Remark = req.Remark
|
||||
account.Verified = true
|
||||
|
||||
if err := global.DB.Save(account).Error; err != nil {
|
||||
if err := agentAccountRepo.Save(account); err != nil {
|
||||
return err
|
||||
}
|
||||
terminalai.InvalidateTerminalRuntimeCache()
|
||||
@@ -1146,13 +1161,34 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
||||
if err != nil {
|
||||
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))
|
||||
for _, item := range list {
|
||||
apiKey := ""
|
||||
if item.RememberAPIKey {
|
||||
apiKey = item.APIKey
|
||||
}
|
||||
appInstallID := uint(0)
|
||||
if localInstalls[item.AppInstallID] {
|
||||
appInstallID = item.AppInstallID
|
||||
}
|
||||
items = append(items, dto.AgentAccountInfo{
|
||||
AppInstallID: appInstallID,
|
||||
ID: item.ID,
|
||||
MasterAccountID: item.MasterAccountID,
|
||||
Provider: item.Provider,
|
||||
@@ -1350,13 +1386,27 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) 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
|
||||
}
|
||||
|
||||
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
||||
agentAccountMutationMu.Lock()
|
||||
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 {
|
||||
var agentCount int64
|
||||
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
|
||||
}
|
||||
fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil {
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
case constant.AppHermesAgent:
|
||||
|
||||
@@ -1354,6 +1354,10 @@ func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID
|
||||
} else if slices.Contains(options, "--dangerously-force-unsafe-install") {
|
||||
args = append(args, "--dangerously-force-unsafe-install")
|
||||
}
|
||||
// Source confirmation does not grant the selected channel plugin's capabilities.
|
||||
if slices.Contains(options, "--accept-capabilities") {
|
||||
args = append(args, "--accept-capabilities")
|
||||
}
|
||||
return mgr.Run("docker", args...)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,15 @@ import (
|
||||
)
|
||||
|
||||
const hermesWeixinLoginScript = `import asyncio
|
||||
from gateway.platforms.weixin import qr_login
|
||||
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 hermes_cli.config import save_env_value
|
||||
|
||||
creds = asyncio.run(qr_login('/opt/data'))
|
||||
print('RESULT =', creds)
|
||||
|
||||
if not creds:
|
||||
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"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -97,7 +98,7 @@ func ensureContainerRunning(containerName string) error {
|
||||
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)
|
||||
resolvedAPIType := strings.TrimSpace(apiType)
|
||||
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||
@@ -112,7 +113,7 @@ func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, mode
|
||||
return resolvedAgentAccountInput{}, err
|
||||
}
|
||||
modelID = strings.TrimSpace(modelID)
|
||||
if modelID == "" {
|
||||
if modelID == "" && !allowEmptyModel {
|
||||
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
||||
}
|
||||
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
|
||||
@@ -414,6 +415,15 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
||||
} else {
|
||||
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.Status = appInstall.Status
|
||||
item.Message = appInstall.Message
|
||||
@@ -441,7 +451,8 @@ func localizedAgentProviderName(provider string) string {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -651,7 +662,7 @@ func checkAgentUpgradable(install model.AppInstall) bool {
|
||||
if len(ignores) > 0 {
|
||||
continue
|
||||
}
|
||||
if common.IsCrossVersion(install.Version, item.Version) && !install.App.CrossVersionUpdate {
|
||||
if isCrossAppVersion(install.App.Key, install.Version, item.Version) && !install.App.CrossVersionUpdate {
|
||||
continue
|
||||
}
|
||||
versions = append(versions, item.Version)
|
||||
@@ -659,12 +670,12 @@ func checkAgentUpgradable(install model.AppInstall) bool {
|
||||
if len(versions) == 0 {
|
||||
return false
|
||||
}
|
||||
versions = common.GetSortedVersions(versions)
|
||||
versions = sortAppVersions(install.App.Key, versions)
|
||||
lastVersion := versions[0]
|
||||
if common.IsCrossVersion(install.Version, lastVersion) {
|
||||
if isCrossAppVersion(install.App.Key, install.Version, lastVersion) {
|
||||
return install.App.CrossVersionUpdate
|
||||
}
|
||||
return common.CompareVersion(lastVersion, install.Version)
|
||||
return isNewerAppVersion(install.App.Key, lastVersion, install.Version)
|
||||
}
|
||||
|
||||
type openclawConfig struct {
|
||||
@@ -737,9 +748,11 @@ type modelProvider struct {
|
||||
}
|
||||
|
||||
type modelEntry struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Input []string `json:"input,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Input []string `json:"input,omitempty"`
|
||||
ContextWindow int `json:"contextWindow,omitempty"`
|
||||
MaxTokens int `json:"maxTokens,omitempty"`
|
||||
}
|
||||
|
||||
func requiresOpenclawProviderModels(provider string) bool {
|
||||
@@ -767,7 +780,7 @@ type browserConfig struct {
|
||||
DefaultProfile string `json:"defaultProfile"`
|
||||
}
|
||||
|
||||
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string) error {
|
||||
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string, metadata []dto.AgentModelMetadata) error {
|
||||
if strings.TrimSpace(confDir) == "" {
|
||||
return fmt.Errorf("config dir is required")
|
||||
}
|
||||
@@ -852,6 +865,7 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
}
|
||||
conf = initial
|
||||
} else {
|
||||
preserveOpenclawModelMetadata(conf, cfg.Models)
|
||||
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -906,6 +920,9 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
if allowedOrigins != nil {
|
||||
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
|
||||
}
|
||||
if err := applyOpenclawModelMetadata(conf, account, metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -920,6 +937,144 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order)
|
||||
}
|
||||
|
||||
func readOpenclawModelsConfig(conf map[string]interface{}) *modelsConfig {
|
||||
raw, ok := conf["models"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
payload, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var models modelsConfig
|
||||
if err := json.Unmarshal(payload, &models); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &models
|
||||
}
|
||||
|
||||
func preserveOpenclawModelMetadata(conf map[string]interface{}, next *modelsConfig) {
|
||||
current := readOpenclawModelsConfig(conf)
|
||||
if current == nil || next == nil {
|
||||
return
|
||||
}
|
||||
for providerID, nextProvider := range next.Providers {
|
||||
currentProvider, ok := current.Providers[providerID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
byID := make(map[string]modelEntry, len(currentProvider.Models))
|
||||
for _, entry := range currentProvider.Models {
|
||||
byID[entry.ID] = entry
|
||||
}
|
||||
for index := range nextProvider.Models {
|
||||
currentEntry, ok := byID[nextProvider.Models[index].ID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
nextProvider.Models[index].Input = currentEntry.Input
|
||||
nextProvider.Models[index].ContextWindow = currentEntry.ContextWindow
|
||||
nextProvider.Models[index].MaxTokens = currentEntry.MaxTokens
|
||||
}
|
||||
next.Providers[providerID] = nextProvider
|
||||
}
|
||||
}
|
||||
|
||||
func extractOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, accountModels []dto.AgentAccountModel) []dto.AgentModelMetadata {
|
||||
result := make([]dto.AgentModelMetadata, 0, len(accountModels))
|
||||
configured := readOpenclawModelsConfig(conf)
|
||||
for _, item := range accountModels {
|
||||
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
metadata := dto.AgentModelMetadata{Model: item.ID, InputMode: "auto"}
|
||||
if configured != nil {
|
||||
for _, entry := range configured.Providers[providerID].Models {
|
||||
if entry.ID != inferred.ID {
|
||||
continue
|
||||
}
|
||||
metadata.ContextWindow = entry.ContextWindow
|
||||
metadata.MaxTokens = entry.MaxTokens
|
||||
if len(entry.Input) > 0 && !slices.Equal(entry.Input, inferred.Input) {
|
||||
if slices.Contains(entry.Input, "image") {
|
||||
metadata.InputMode = "image"
|
||||
} else {
|
||||
metadata.InputMode = "text"
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
result = append(result, metadata)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func applyOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, requested []dto.AgentModelMetadata) error {
|
||||
if len(requested) == 0 {
|
||||
return nil
|
||||
}
|
||||
configured := readOpenclawModelsConfig(conf)
|
||||
if configured == nil {
|
||||
return fmt.Errorf("model metadata is not supported for provider %s", account.Provider)
|
||||
}
|
||||
accountModels, err := loadAgentAccountModels(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
available := make(map[string]dto.AgentAccountModel, len(accountModels))
|
||||
for _, item := range accountModels {
|
||||
available[item.ID] = item
|
||||
}
|
||||
seen := make(map[string]struct{}, len(requested))
|
||||
for _, metadata := range requested {
|
||||
item, ok := available[metadata.Model]
|
||||
if !ok {
|
||||
return buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
if _, ok := seen[metadata.Model]; ok {
|
||||
return fmt.Errorf("duplicate model metadata: %s", metadata.Model)
|
||||
}
|
||||
seen[metadata.Model] = struct{}{}
|
||||
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provider := configured.Providers[providerID]
|
||||
found := false
|
||||
for index := range provider.Models {
|
||||
if provider.Models[index].ID != inferred.ID {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
provider.Models[index].ContextWindow = metadata.ContextWindow
|
||||
provider.Models[index].MaxTokens = metadata.MaxTokens
|
||||
switch metadata.InputMode {
|
||||
case "auto":
|
||||
provider.Models[index].Input = inferred.Input
|
||||
case "text":
|
||||
provider.Models[index].Input = []string{"text"}
|
||||
case "image":
|
||||
provider.Models[index].Input = []string{"text", "image"}
|
||||
default:
|
||||
return fmt.Errorf("unsupported model input mode: %s", metadata.InputMode)
|
||||
}
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
return buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
configured.Providers[providerID] = provider
|
||||
}
|
||||
modelsMap, err := structToMap(configured)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conf["models"] = modelsMap
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
|
||||
accountModels, err := loadAgentAccountModels(account)
|
||||
if err != nil {
|
||||
@@ -1041,7 +1196,7 @@ func prepareOpenclawInstallFiles(appInstall *model.AppInstall, account *model.Ag
|
||||
return fmt.Errorf("app install is required")
|
||||
}
|
||||
confDir := path.Join(appInstall.GetPath(), "data", "conf")
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil); err != nil {
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
dataDir := path.Join(appInstall.GetPath(), "data")
|
||||
@@ -1108,7 +1263,12 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen
|
||||
if err != nil {
|
||||
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,
|
||||
BaseUrl: providerPatch.BaseURL,
|
||||
Api: providerPatch.APIType,
|
||||
@@ -1496,6 +1656,10 @@ func agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
|
||||
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
|
||||
case constant.AppCopaw:
|
||||
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:
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
+107
-47
@@ -8,6 +8,7 @@ import (
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -28,13 +29,15 @@ import (
|
||||
|
||||
type AIToolService struct{}
|
||||
|
||||
var ollamaModelMu sync.Mutex
|
||||
|
||||
type IAIToolService interface {
|
||||
Search(search dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error)
|
||||
Create(req dto.OllamaModelName) error
|
||||
Close(name string) error
|
||||
Recreate(req dto.OllamaModelName) error
|
||||
Delete(req dto.ForceDelete) error
|
||||
Sync() ([]dto.OllamaModelDropList, error)
|
||||
SyncAccount(req dto.OllamaModelAccountSync) error
|
||||
LoadDetail(name string) (string, error)
|
||||
BindDomain(req dto.OllamaBindDomain) 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) {
|
||||
syncErr := syncOllamaModels()
|
||||
var options []repo.DBOption
|
||||
if len(req.Info) != 0 {
|
||||
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 {
|
||||
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))
|
||||
if len(taskModel.ID) != 0 {
|
||||
item.LogFileExist = true
|
||||
@@ -85,6 +94,8 @@ func (u *AIToolService) LoadDetail(name string) (string, error) {
|
||||
}
|
||||
|
||||
func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
||||
ollamaModelMu.Lock()
|
||||
defer ollamaModelMu.Unlock()
|
||||
if cmd.CheckIllegal(req.Name) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
@@ -125,6 +136,9 @@ func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
_ = 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
|
||||
}
|
||||
@@ -144,6 +158,8 @@ func (u *AIToolService) Close(name string) error {
|
||||
}
|
||||
|
||||
func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
||||
ollamaModelMu.Lock()
|
||||
defer ollamaModelMu.Unlock()
|
||||
if cmd.CheckIllegal(req.Name) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
@@ -179,11 +195,16 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
_ = 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
|
||||
}
|
||||
|
||||
func (u *AIToolService) Delete(req dto.ForceDelete) error {
|
||||
ollamaModelMu.Lock()
|
||||
defer ollamaModelMu.Unlock()
|
||||
ollamaList, _ := aiRepo.List(repo.WithByIDs(req.IDs))
|
||||
if len(ollamaList) == 0 {
|
||||
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)
|
||||
_ = os.Remove(logItem)
|
||||
}
|
||||
return nil
|
||||
return SyncOllamaModelAccount(false)
|
||||
}
|
||||
|
||||
func (u *AIToolService) Sync() ([]dto.OllamaModelDropList, error) {
|
||||
containerName, err := LoadContainerName()
|
||||
func (u *AIToolService) SyncAccount(req dto.OllamaModelAccountSync) error {
|
||||
install, err := appInstallRepo.GetFirst(repo.WithByID(req.ID))
|
||||
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 {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
var list []model.OllamaModel
|
||||
lines := strings.Split(stdout, "\n")
|
||||
for _, line := range lines {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 5 {
|
||||
continue
|
||||
}
|
||||
if parts[0] == "NAME" {
|
||||
continue
|
||||
}
|
||||
list = append(list, model.OllamaModel{Name: parts[0], Size: parts[2] + " " + parts[3]})
|
||||
if err := syncOllamaModelAccount(install, baseURL, true, true); err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
return appInstallRepo.BatchUpdateBy(map[string]interface{}{
|
||||
"model_account_base_url_type": req.ModelAccountBaseURLType,
|
||||
"model_account_base_url": baseURL,
|
||||
}, repo.WithByID(install.ID))
|
||||
}
|
||||
|
||||
func (u *AIToolService) BindDomain(req dto.OllamaBindDomain) error {
|
||||
@@ -370,6 +364,72 @@ func (u *AIToolService) UpdateBindDomain(req dto.OllamaBindDomain) error {
|
||||
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) {
|
||||
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
|
||||
if err != nil {
|
||||
@@ -387,12 +447,12 @@ func loadModelSize(name string, containerName string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
lines := strings.Split(stdout, "\n")
|
||||
if !strings.Contains(path.Base(name), ":") {
|
||||
name += ":latest"
|
||||
}
|
||||
for _, line := range lines {
|
||||
if !strings.Contains(line, name) {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 5 {
|
||||
if len(parts) < 5 || parts[0] != name {
|
||||
continue
|
||||
}
|
||||
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"
|
||||
"fmt"
|
||||
"mime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"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/global"
|
||||
"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"
|
||||
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/email"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
)
|
||||
|
||||
type AlertService struct{}
|
||||
@@ -78,10 +74,10 @@ type IAlertService interface {
|
||||
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
|
||||
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
|
||||
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
|
||||
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
|
||||
DeleteAlertConfig(id uint) error
|
||||
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) (string, error)
|
||||
DeleteAlertConfig(id uint) (string, 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 {
|
||||
@@ -109,7 +105,38 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
cronjobProjects := make(map[string]uint)
|
||||
var cronjobIDs []uint
|
||||
for _, item := range alerts {
|
||||
if alertUtil.GetCronJobType(item.Type) != "cronJob" {
|
||||
continue
|
||||
}
|
||||
if _, exists := cronjobProjects[item.Project]; exists {
|
||||
continue
|
||||
}
|
||||
id, parseErr := strconv.ParseUint(item.Project, 10, strconv.IntSize)
|
||||
if parseErr != nil || id == 0 {
|
||||
continue
|
||||
}
|
||||
cronjobProjects[item.Project] = uint(id)
|
||||
cronjobIDs = append(cronjobIDs, uint(id))
|
||||
}
|
||||
cronjobsByID := make(map[uint]model.Cronjob)
|
||||
if len(cronjobIDs) > 0 {
|
||||
cronjobs, err := cronjobRepo.List(repo.WithByIDs(cronjobIDs))
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
for _, cronjob := range cronjobs {
|
||||
cronjobsByID[cronjob.ID] = cronjob
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range alerts {
|
||||
var taskName string
|
||||
if cronjob, exists := cronjobsByID[cronjobProjects[item.Project]]; exists && cronjob.Type == item.Type {
|
||||
taskName = cronjob.Name
|
||||
}
|
||||
|
||||
result = append(result, dto.AlertDTO{
|
||||
ID: item.ID,
|
||||
@@ -119,6 +146,7 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
|
||||
Method: item.Method,
|
||||
Title: item.Title,
|
||||
Project: item.Project,
|
||||
TaskName: taskName,
|
||||
Status: item.Status,
|
||||
SendCount: item.SendCount,
|
||||
AdvancedParams: item.AdvancedParams,
|
||||
@@ -190,6 +218,16 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
advanced, err := prepareCronJobAlertParams(create.Type, "", create.AdvancedParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
create.AdvancedParams = advanced
|
||||
if create.Status != constant.AlertDisable {
|
||||
if err := a.validateCronJobAlertChannels(create.Type, advanced, create.Method); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
alertInfo.Status = constant.AlertEnable
|
||||
if err := copier.Copy(&alertInfo, &create); err != nil {
|
||||
return buserr.WithErr("ErrStructTransform", err)
|
||||
@@ -207,11 +245,24 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, 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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Status != constant.AlertDisable {
|
||||
if err := a.validateCronJobAlertChannels(req.Type, req.AdvancedParams, req.Method); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -278,6 +329,9 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
|
||||
return err
|
||||
}
|
||||
if status == constant.AlertEnable {
|
||||
if err := a.validateCronJobAlertChannels(alertInfo.Type, alertInfo.AdvancedParams, alertInfo.Method); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -299,122 +353,14 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
|
||||
}
|
||||
|
||||
func (a AlertService) GetDisks() ([]dto.DiskDTO, error) {
|
||||
var disks []dto.DiskDTO
|
||||
excludes := map[string]struct{}{
|
||||
"/mnt/cdrom": {}, "/boot": {}, "/boot/efi": {}, "/dev": {}, "/dev/shm": {},
|
||||
"/run/lock": {}, "/run": {}, "/run/shm": {}, "/run/user": {},
|
||||
infos := loadDiskInfo(true)
|
||||
disks := make([]dto.DiskDTO, 0, len(infos))
|
||||
for _, item := range infos {
|
||||
disks = append(disks, dto.DiskDTO(item))
|
||||
}
|
||||
stdout, err := executeDiskCommand()
|
||||
if err != nil {
|
||||
return disks, nil
|
||||
}
|
||||
|
||||
lines := strings.Split(stdout, "\n")
|
||||
var mounts []dto.AlertDiskInfo
|
||||
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 7 {
|
||||
continue
|
||||
}
|
||||
mountPoint := strings.Join(fields[6:], " ")
|
||||
if shouldExclude(fields, mountPoint, excludes) {
|
||||
continue
|
||||
}
|
||||
mounts = append(mounts, dto.AlertDiskInfo{Type: fields[1], Device: fields[0], Mount: mountPoint})
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
)
|
||||
wg.Add(len(mounts))
|
||||
for i := 0; i < len(mounts); i++ {
|
||||
go func(timeoutCh <-chan time.Time, mount dto.AlertDiskInfo) {
|
||||
defer wg.Done()
|
||||
|
||||
var itemData dto.DiskDTO
|
||||
itemData.Path = mount.Mount
|
||||
itemData.Type = mount.Type
|
||||
itemData.Device = mount.Device
|
||||
select {
|
||||
case <-timeoutCh:
|
||||
mu.Lock()
|
||||
disks = append(disks, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
|
||||
default:
|
||||
state, err := disk.Usage(mount.Mount)
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
disks = append(disks, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
|
||||
return
|
||||
}
|
||||
itemData.Total = state.Total
|
||||
itemData.Free = state.Free
|
||||
itemData.Used = state.Used
|
||||
itemData.UsedPercent = state.UsedPercent
|
||||
itemData.InodesTotal = state.InodesTotal
|
||||
itemData.InodesUsed = state.InodesUsed
|
||||
itemData.InodesFree = state.InodesFree
|
||||
itemData.InodesUsedPercent = state.InodesUsedPercent
|
||||
mu.Lock()
|
||||
disks = append(disks, itemData)
|
||||
mu.Unlock()
|
||||
}
|
||||
}(time.After(5*time.Second), mounts[i])
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
sort.Slice(disks, func(i, j int) bool {
|
||||
return disks[i].Path < disks[j].Path
|
||||
})
|
||||
return disks, nil
|
||||
}
|
||||
|
||||
func executeDiskCommand() (string, error) {
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||
stdout, err := cmdMgr.RunWithStdout("df", "-hT", "-P")
|
||||
if err != nil {
|
||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||
stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P")
|
||||
}
|
||||
if err != nil {
|
||||
return stdout, err
|
||||
}
|
||||
var lines []string
|
||||
for _, line := range strings.Split(stdout, "\n") {
|
||||
if !strings.Contains(line, "/") || strings.Contains(line, "tmpfs") || strings.Contains(line, "snap/core") || strings.Contains(line, "udev") {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
if len(lines) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
func shouldExclude(fields []string, mountPoint string, excludes map[string]struct{}) bool {
|
||||
if strings.HasPrefix(mountPoint, "/snap") || len(strings.Split(mountPoint, "/")) > 10 {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(fields[1]) == "tmpfs" {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(fields[2], "K") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(mountPoint, "docker") {
|
||||
return true
|
||||
}
|
||||
_, excluded := excludes[mountPoint]
|
||||
return excluded
|
||||
}
|
||||
|
||||
func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) {
|
||||
var (
|
||||
opts []repo.DBOption
|
||||
@@ -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 {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
config, err := alertRepo.GetConfigById(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
name := alertConfigLogName(config.Config, config.Title)
|
||||
if req.Status == constant.AlertEnable {
|
||||
if err := a.validateCommunityAlertConfigType(config.Type); err != nil {
|
||||
return err
|
||||
return name, err
|
||||
}
|
||||
if config.Type == constant.Custom {
|
||||
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,
|
||||
"update_user": operator,
|
||||
}, repo.WithByID(req.ID))
|
||||
@@ -867,19 +814,20 @@ func alertConfigSMSPhone(configData string) string {
|
||||
return strings.TrimSpace(cfg.Phone)
|
||||
}
|
||||
|
||||
func (a AlertService) DeleteAlertConfig(id uint) error {
|
||||
_, err := alertRepo.GetConfigById(id)
|
||||
func (a AlertService) DeleteAlertConfig(id uint) (string, error) {
|
||||
config, err := alertRepo.GetConfigById(id)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
name := alertConfigLogName(config.Config, config.Title)
|
||||
usedAlerts, err := alertRepo.List(alertRepo.WithByAlertMethodContainsConfigID(id))
|
||||
if err != nil {
|
||||
return err
|
||||
return name, err
|
||||
}
|
||||
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) {
|
||||
@@ -905,7 +853,7 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
||||
|
||||
msg := email.EmailMessage{
|
||||
Subject: i18n.GetMsgByKey("TestAlertTitle"),
|
||||
Body: i18n.GetMsgByKey("TestAlert"),
|
||||
Body: i18n.GetMsgByKey("TestAlertMessage"),
|
||||
IsHTML: false,
|
||||
}
|
||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||
@@ -953,7 +901,7 @@ func resolveEmailTestConfig(req dto.AlertConfigTest) (dto.AlertEmailConfig, erro
|
||||
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 {
|
||||
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)
|
||||
if !ok {
|
||||
if global.LOG != nil {
|
||||
global.LOG.Warnf("webhook test failed: %v", providers.ErrCustomWebhookUnsupported)
|
||||
}
|
||||
return dto.AlertConfigTestResult{
|
||||
Success: false,
|
||||
Message: providers.ErrCustomWebhookUnsupported.Error(),
|
||||
}, nil
|
||||
}
|
||||
return tester.TestCustomWebhook(resolved)
|
||||
return tester.TestCustomWebhook(resolved, lang)
|
||||
}
|
||||
|
||||
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.WithByProject(updateAlert.Project),
|
||||
)
|
||||
advanced, err := prepareCronJobAlertParams(updateAlert.Type, alertInfo.AdvancedParams, updateAlert.AdvancedParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateAlert.AdvancedParams = advanced
|
||||
if alertUtil.GetCronJobType(updateAlert.Type) == "cronJob" {
|
||||
upMap["advanced_params"] = advanced
|
||||
}
|
||||
if newStatus == constant.AlertEnable {
|
||||
method := updateAlert.Method
|
||||
if method == "" {
|
||||
method = alertInfo.Method
|
||||
}
|
||||
if err := a.validateCronJobAlertChannels(updateAlert.Type, advanced, method); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if alertInfo.ID > 0 {
|
||||
shouldUpdate := false
|
||||
@@ -1034,6 +1002,9 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
||||
if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method {
|
||||
shouldUpdate = true
|
||||
}
|
||||
if val, ok := upMap["advanced_params"]; ok && val != alertInfo.AdvancedParams {
|
||||
shouldUpdate = true
|
||||
}
|
||||
|
||||
if shouldUpdate {
|
||||
if err := alertRepo.Update(
|
||||
@@ -1055,3 +1026,32 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareCronJobAlertParams(alertType, previous, incoming string) (string, error) {
|
||||
if alertUtil.GetCronJobType(alertType) != "cronJob" {
|
||||
return incoming, nil
|
||||
}
|
||||
return alertUtil.MergeCronJobAlertParams(previous, incoming)
|
||||
}
|
||||
|
||||
func (a AlertService) validateCronJobAlertChannels(alertType, advanced, method string) error {
|
||||
if alertUtil.GetCronJobType(alertType) != "cronJob" {
|
||||
return nil
|
||||
}
|
||||
mode, err := alertUtil.CronJobAlertTriggerMode(advanced)
|
||||
if err != nil || mode != alertUtil.CronJobAlertSuccess {
|
||||
return err
|
||||
}
|
||||
_, err = a.validateAlertMethodReferences(method)
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -30,10 +30,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ResourceAlertInterval = 30
|
||||
CheckIntervalSec = 3
|
||||
LoadCheckIntervalMin = 5
|
||||
sshIPLoginWindow = 30 * time.Minute
|
||||
ResourceAlertInterval = 30
|
||||
CheckIntervalSec = 3
|
||||
LoadCheckIntervalMin = 5
|
||||
sshIPLoginWindow = 30 * time.Minute
|
||||
sslAutoRenewAlertSkipDays = 31
|
||||
)
|
||||
|
||||
type AlertTaskHelper struct {
|
||||
@@ -529,11 +530,15 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Failed to load ssh login logs: %v", err)
|
||||
}
|
||||
interfaceAddrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
global.LOG.Warnf("Failed to load local IP addresses for ssh login alert: %v", err)
|
||||
}
|
||||
count, records := summarizeSSHLoginHistories(
|
||||
histories,
|
||||
now,
|
||||
failedWindow,
|
||||
strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n"),
|
||||
sshSuccessLoginWhitelist(alert.AdvancedParams, interfaceAddrs),
|
||||
)
|
||||
isAlert := count >= int(alert.Count)
|
||||
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 {
|
||||
filtered := make([]model.LoginLog, 0, len(records))
|
||||
for _, record := range records {
|
||||
@@ -864,7 +882,7 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
|
||||
daysDiff := int(math.Ceil(
|
||||
ssl.ExpireDate.Sub(currentDate).Hours() / 24,
|
||||
))
|
||||
if daysDiff > 0 && int(cycle) >= daysDiff {
|
||||
if daysDiff > 0 && int(cycle) >= daysDiff && !shouldSuppressSSLExpiryAlert(ssl, daysDiff) {
|
||||
daysDiffMap[daysDiff] = append(daysDiffMap[daysDiff], ssl.PrimaryDomain)
|
||||
projectMap[ssl.ID] = append(projectMap[ssl.ID], ssl.ExpireDate)
|
||||
}
|
||||
@@ -872,6 +890,10 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
|
||||
return daysDiffMap, projectMap
|
||||
}
|
||||
|
||||
func shouldSuppressSSLExpiryAlert(ssl model.WebsiteSSL, remainingDays int) bool {
|
||||
return ssl.AutoRenew && remainingDays < sslAutoRenewAlertSkipDays
|
||||
}
|
||||
|
||||
func calculateWebsiteExpiryDays(websites []model.Website, cycle uint) (map[int][]string, map[uint][]time.Time) {
|
||||
currentDate := time.Now()
|
||||
daysDiffMap := make(map[int][]string)
|
||||
@@ -1044,50 +1066,37 @@ func processAllDisks(alert dto.AlertDTO) error {
|
||||
global.LOG.Errorf("error getting disk list, err: %v", err)
|
||||
return err
|
||||
}
|
||||
var errMsgs []string
|
||||
for _, item := range diskList {
|
||||
err := checkAndCreateDiskAlert(alert, item.Path)
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("disk path %s process failed: %v", item.Path, err)
|
||||
errMsgs = append(errMsgs, errMsg)
|
||||
global.LOG.Errorf("%s", errMsg)
|
||||
if item.Total == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(errMsgs) > 0 {
|
||||
return fmt.Errorf("batch process disks failed, error count: %d, details: %s", len(errMsgs), strings.Join(errMsgs, "; "))
|
||||
checkAndCreateDiskAlert(alert, item.Path, &disk.UsageStat{Used: item.Used, UsedPercent: item.UsedPercent})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func processSingleDisk(alert dto.AlertDTO) error {
|
||||
err := checkAndCreateDiskAlert(alert, alert.Project)
|
||||
usageStat, err := loadDiskUsageWithTimeout(alert.Project, true)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("%s", err.Error())
|
||||
global.LOG.Errorf("error getting disk usage for %s, err: %v", alert.Project, err)
|
||||
return err
|
||||
}
|
||||
checkAndCreateDiskAlert(alert, alert.Project, usageStat)
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string) error {
|
||||
usageStat, err := psutil.DISK.GetUsage(path, false)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("error getting disk usage for %s, err: %v", path, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string, usageStat *disk.UsageStat) {
|
||||
usedTotal, usedStr := calculateUsedTotal(alert.Cycle, usageStat)
|
||||
commonTotal := float64(alert.Count)
|
||||
if alert.Cycle == 1 {
|
||||
commonTotal *= 1024 * 1024 * 1024
|
||||
}
|
||||
if usedTotal < commonTotal {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
params := createAlertDiskParams(path, usedStr)
|
||||
sender := NewAlertSender(alert, alert.Project)
|
||||
sender.ResourceSend(path, params)
|
||||
return nil
|
||||
}
|
||||
|
||||
func calculateUsedTotal(cycle uint, usageStat *disk.UsageStat) (float64, string) {
|
||||
|
||||
+62
-16
@@ -258,26 +258,35 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
||||
fileOp := files.NewFileOp()
|
||||
|
||||
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) {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
if err = downloadApp(app, detail, nil, nil); err != nil && !fileOp.Stat(versionPath) {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
if err == nil && app.Resource == constant.AppResourceRemote {
|
||||
detail.Update = false
|
||||
appDetailDTO.Update = false
|
||||
}
|
||||
switch app.Type {
|
||||
case constant.RuntimePHP:
|
||||
paramsPath := filepath.Join(versionPath, "data.yml")
|
||||
if !fileOp.Stat(paramsPath) {
|
||||
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
|
||||
}
|
||||
param, err := fileOp.GetContent(paramsPath)
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
paramMap := make(map[string]interface{})
|
||||
if err = yaml.Unmarshal(param, ¶mMap); err != nil {
|
||||
return appDetailDTO, err
|
||||
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")
|
||||
if !fileOp.Stat(paramsPath) {
|
||||
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
|
||||
}
|
||||
param, err := fileOp.GetContent(paramsPath)
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
if err = yaml.Unmarshal(param, ¶mMap); err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
appDetailDTO.Params = paramMap["additionalProperties"]
|
||||
}
|
||||
appDetailDTO.Params = paramMap["additionalProperties"]
|
||||
composePath := filepath.Join(versionPath, "docker-compose.yml")
|
||||
if !fileOp.Stat(composePath) {
|
||||
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", composePath, nil)
|
||||
@@ -308,6 +317,12 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
||||
if appDetailDTO.DockerCompose == "" {
|
||||
filename := filepath.Base(appDetailDTO.DownloadUrl)
|
||||
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)
|
||||
if statusCode == http.StatusNotFound {
|
||||
return appDetailDTO, buserr.New("ErrAppVersionUnavailable")
|
||||
@@ -434,6 +449,12 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
} else {
|
||||
if appDetail.DockerCompose == "" {
|
||||
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
|
||||
statusCode, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||
if statusCode == http.StatusNotFound {
|
||||
@@ -478,6 +499,22 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
req.Params[constant.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
|
||||
serviceName := ""
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -557,7 +599,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
return
|
||||
}
|
||||
|
||||
installApp := func(t *task.Task) error {
|
||||
installApp := func(t *task.Task) (err error) {
|
||||
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -583,6 +625,10 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
return err
|
||||
}
|
||||
updateToolApp(appInstall)
|
||||
if app.Key == "ollama" && appInstall.SyncModelAccount {
|
||||
_, err := SyncLocalModelAccount(*appInstall, 0, modelAccountBaseURL, nil, false, true)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -159,6 +159,14 @@ func (a *AppInstallService) CheckExist(req request.AppInstalledInfo) (*response.
|
||||
}
|
||||
|
||||
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.Version = appInstall.Version
|
||||
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)
|
||||
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
|
||||
}
|
||||
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
|
||||
if req.Advanced {
|
||||
composeMap := make(map[string]interface{})
|
||||
@@ -558,6 +597,9 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
||||
if proxyChanged && website.ID != 0 && currentProxyErr != nil {
|
||||
global.LOG.Error(buserr.WithErr("ErrUpdateBuWebsite", currentProxyErr).Error())
|
||||
}
|
||||
if installed.App.Key == "ollama" && installed.SyncModelAccount {
|
||||
return syncOllamaModelAccount(installed, modelAccountBaseURL, true, true)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -567,11 +609,15 @@ func (a *AppInstallService) SyncAll(systemInit bool) error {
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
i.Status = constant.StatusError
|
||||
i.Message = "1Panel restart causes the task to terminate"
|
||||
_ = appInstallRepo.Save(context.Background(), &i)
|
||||
i.Status = appInstallOperationFailureStatus(i.Status)
|
||||
i.Message = constant.InterruptedMsg
|
||||
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
|
||||
}
|
||||
@@ -678,14 +724,17 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
||||
if !canAccessVllmVersion(app.Key, detail.Version) {
|
||||
continue
|
||||
}
|
||||
if !isAIGatewayUpgradeVersionAllowed(app.Key, install.Version, detail.Version) {
|
||||
continue
|
||||
}
|
||||
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
||||
if len(ignores) > 0 {
|
||||
continue
|
||||
}
|
||||
if common.IsCrossVersion(install.Version, detail.Version) && !app.CrossVersionUpdate {
|
||||
if isCrossAppVersion(app.Key, install.Version, detail.Version) && !app.CrossVersionUpdate {
|
||||
continue
|
||||
}
|
||||
canUpgrade := common.CompareVersion(detail.Version, install.Version)
|
||||
canUpgrade := isNewerAppVersion(app.Key, detail.Version, install.Version)
|
||||
if app.Key == vllmAppKeyForUpgrade {
|
||||
currentImage := loadVllmImageFromEnv(install.Env)
|
||||
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() {
|
||||
filename := filepath.Base(detail.DownloadUrl)
|
||||
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)
|
||||
if statusCode == http.StatusNotFound {
|
||||
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 {
|
||||
return common.CompareVersion(versions[i].Version, versions[j].Version)
|
||||
return appVersionSortBefore(app.Key, versions[i].Version, versions[j].Version)
|
||||
})
|
||||
return versions, nil
|
||||
}
|
||||
@@ -916,6 +971,18 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
||||
config := getAppCommonConfig(envs)
|
||||
config.DockerCompose = install.DockerCompose
|
||||
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 == "" {
|
||||
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 {
|
||||
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 {
|
||||
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
||||
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
|
||||
return nil
|
||||
case constant.StatusInstallErr, constant.StatusUpgradeErr, constant.StatusUpErr:
|
||||
if !force {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
cli, err := docker.NewClient()
|
||||
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 {
|
||||
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 {
|
||||
detail.DockerCompose = string(composeRes)
|
||||
} else {
|
||||
|
||||
@@ -40,14 +40,13 @@ const (
|
||||
appUpgradeDown
|
||||
appUpgradeMutated
|
||||
appUpgradeStarted
|
||||
appUpgradeReady
|
||||
appUpgradeCommitted
|
||||
)
|
||||
|
||||
const composeServiceLabel = "com.docker.compose.service"
|
||||
|
||||
var appUpgradeLocks sync.Map
|
||||
|
||||
const composeServiceLabel = "com.docker.compose.service"
|
||||
|
||||
type appUpgradeSnapshot interface {
|
||||
Restore() error
|
||||
Cleanup()
|
||||
@@ -107,6 +106,14 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -439,15 +446,14 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
|
||||
t.LogSuccess(logStr)
|
||||
u.phase = appUpgradeStarted
|
||||
|
||||
t.LogStart(i18n.GetMsgByKey("UpgradeWaitReady"))
|
||||
containerNames, err := waitAppContainersReady(context.Background(), u.candidate)
|
||||
if err != nil {
|
||||
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeWaitReady"), err)
|
||||
return err
|
||||
containerNames, discoverErr := discoverUpgradeContainerNames(u.candidate, u.envContent)
|
||||
if discoverErr != nil {
|
||||
t.Logf("WARNING: discover upgraded application containers failed: %v", discoverErr)
|
||||
} else if len(containerNames) > 0 {
|
||||
u.candidate.ContainerName = strings.Join(containerNames, ",")
|
||||
} else {
|
||||
t.Log("WARNING: no containers found for the upgraded application")
|
||||
}
|
||||
t.LogSuccess(i18n.GetMsgByKey("UpgradeWaitReady"))
|
||||
u.phase = appUpgradeReady
|
||||
u.candidate.ContainerName = strings.Join(containerNames, ",")
|
||||
u.candidate.Status = constant.StatusRunning
|
||||
u.candidate.Message = ""
|
||||
|
||||
@@ -472,6 +478,11 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
|
||||
} else if err = appInstallRepo.Save(context.Background(), &u.candidate); err != nil {
|
||||
return err
|
||||
}
|
||||
if discoverErr == nil && len(containerNames) > 0 {
|
||||
if syncErr := syncAppInstallStatus(&u.candidate, true); syncErr != nil {
|
||||
t.Logf("WARNING: sync upgraded application status failed: %v", syncErr)
|
||||
}
|
||||
}
|
||||
u.phase = appUpgradeCommitted
|
||||
u.deleteOldImages(t)
|
||||
return nil
|
||||
@@ -591,9 +602,6 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
|
||||
}
|
||||
|
||||
func (u *appUpgradeContext) finishRollback() error {
|
||||
if _, err := waitAppContainersReady(context.Background(), u.original); err != nil {
|
||||
return err
|
||||
}
|
||||
restored := u.original
|
||||
if err := appInstallRepo.Save(context.Background(), &restored); err != nil {
|
||||
return err
|
||||
@@ -881,28 +889,7 @@ func (s *upgradeFileSnapshot) Cleanup() {
|
||||
}
|
||||
}
|
||||
|
||||
type appContainerReadinessClient interface {
|
||||
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
|
||||
ContainerInspect(context.Context, string) (container.InspectResponse, error)
|
||||
}
|
||||
|
||||
func waitAppContainersReady(ctx context.Context, install model.AppInstall) ([]string, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
return waitAppContainersReadyWithClient(ctx, client, install)
|
||||
}
|
||||
|
||||
func waitAppContainersReadyWithClient(ctx context.Context, client appContainerReadinessClient, install model.AppInstall) ([]string, error) {
|
||||
envContent, err := os.ReadFile(install.GetEnvPath())
|
||||
if err != nil {
|
||||
envContent, err = renderUpgradeEnv(&install, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
func discoverUpgradeContainerNames(install model.AppInstall, envContent []byte) ([]string, error) {
|
||||
project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), envContent, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -916,36 +903,24 @@ func waitAppContainersReadyWithClient(ctx context.Context, client appContainerRe
|
||||
if len(expectedServices) == 0 {
|
||||
return strings.Split(install.ContainerName, ","), nil
|
||||
}
|
||||
options := container.ListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(
|
||||
filters.Arg("label", composeWorkdirLabel+"="+install.GetPath()),
|
||||
),
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
containers, err := client.ContainerList(ctx, options)
|
||||
defer client.Close()
|
||||
containers, err := client.ContainerList(context.Background(), container.ListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(filters.Arg("label", composeWorkdirLabel+"="+install.GetPath())),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
foundServices := make(map[string]bool, len(expectedServices))
|
||||
containerNames := make([]string, 0, len(containers))
|
||||
for _, item := range containers {
|
||||
serviceName := item.Labels[composeServiceLabel]
|
||||
if _, ok := expectedServices[serviceName]; !ok {
|
||||
continue
|
||||
}
|
||||
if err = waitContainerReady(ctx, client, item.ID); err != nil {
|
||||
return nil, fmt.Errorf("container %s is not ready: %w", serviceName, err)
|
||||
}
|
||||
foundServices[serviceName] = true
|
||||
if len(item.Names) > 0 {
|
||||
if _, ok := expectedServices[item.Labels[composeServiceLabel]]; ok && len(item.Names) > 0 {
|
||||
containerNames = append(containerNames, strings.TrimPrefix(item.Names[0], "/"))
|
||||
}
|
||||
}
|
||||
for serviceName := range expectedServices {
|
||||
if !foundServices[serviceName] {
|
||||
return nil, fmt.Errorf("container for service %s was not created", serviceName)
|
||||
}
|
||||
}
|
||||
sort.Strings(containerNames)
|
||||
return containerNames, nil
|
||||
}
|
||||
|
||||
+111
-32
@@ -8,6 +8,7 @@ import (
|
||||
"maps"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -352,22 +353,34 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
if dir != nil {
|
||||
logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("App")
|
||||
t.Log(logStr)
|
||||
cleanupFailed := false
|
||||
|
||||
if deleteReq.UseLifecycleScripts {
|
||||
if err = runScript(t, &install, "uninstall"); err != nil {
|
||||
return err
|
||||
if scriptErr := runScript(t, &install, "uninstall"); scriptErr != nil {
|
||||
cleanupFailed = true
|
||||
if !deleteReq.ForceDelete {
|
||||
return scriptErr
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out, err := compose.Down(install.GetComposePath())
|
||||
if err != nil && !deleteReq.ForceDelete {
|
||||
return handleErr(install, err, out)
|
||||
out, downErr := compose.Down(install.GetComposePath())
|
||||
if downErr != nil {
|
||||
cleanupFailed = true
|
||||
if !deleteReq.ForceDelete {
|
||||
return handleErr(install, downErr, out)
|
||||
}
|
||||
}
|
||||
if err = runScript(t, &install, "uninstall"); err != nil {
|
||||
_, _ = compose.Up(install.GetComposePath())
|
||||
return err
|
||||
if scriptErr := runScript(t, &install, "uninstall"); scriptErr != nil {
|
||||
cleanupFailed = true
|
||||
if !deleteReq.ForceDelete {
|
||||
_, _ = compose.Up(install.GetComposePath())
|
||||
return scriptErr
|
||||
}
|
||||
}
|
||||
}
|
||||
t.LogSuccess(logStr)
|
||||
if !cleanupFailed {
|
||||
t.LogSuccess(logStr)
|
||||
}
|
||||
if deleteReq.DeleteImage {
|
||||
content, err := op.GetContent(install.GetEnvPath())
|
||||
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()
|
||||
defer tx.Rollback()
|
||||
if err = appInstallRepo.Delete(ctx, install); err != nil {
|
||||
return err
|
||||
}
|
||||
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) {
|
||||
_ = agentRepo.DeleteByAppInstallIDWithCtx(ctx, install.ID)
|
||||
}
|
||||
@@ -456,7 +484,12 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
if err == nil && len(entries) == 0 {
|
||||
_ = 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))
|
||||
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)
|
||||
go func() {
|
||||
if err := uninstallTask.Execute(); err != nil && !deleteReq.ForceDelete {
|
||||
if err := uninstallTask.Execute(); err != nil {
|
||||
install.Status = constant.StatusError
|
||||
install.Message = err.Error()
|
||||
_ = appInstallRepo.Save(context.Background(), &install)
|
||||
}
|
||||
}()
|
||||
@@ -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) {
|
||||
if app.IsLocalApp() || app.IsCustomApp() {
|
||||
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)
|
||||
appDownloadDir := app.GetAppResourcePath()
|
||||
appVersionDir := path.Join(appDownloadDir, appDetail.Version)
|
||||
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) {
|
||||
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) {
|
||||
_ = 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)
|
||||
}
|
||||
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 {
|
||||
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")
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if appInstall != nil {
|
||||
appInstall.Status = constant.StatusDownloadErr
|
||||
appInstall.Message = err.Error()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = files.DownloadFileWithProxy(appDetail.DownloadUrl, filePath); err != nil {
|
||||
if err = files.DownloadFileWithProxy(downloadURL, filePath); err != nil {
|
||||
if logger == nil {
|
||||
global.LOG.Errorf("download app [%s] error %v", app.Name, err)
|
||||
} else {
|
||||
@@ -929,6 +990,9 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
|
||||
return
|
||||
}
|
||||
_ = fileOp.DeleteFile(filePath)
|
||||
if err = os.WriteFile(cacheVersionPath, []byte(storeVersion), constant.FilePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
appDetail.Update = false
|
||||
_ = appDetailRepo.Update(context.Background(), appDetail)
|
||||
return
|
||||
@@ -1393,9 +1457,8 @@ func handleErr(install model.AppInstall, err error, out string) error {
|
||||
}
|
||||
|
||||
func doNotNeedSync(installed model.AppInstall) bool {
|
||||
return installed.Status == constant.StatusInstalling || installed.Status == constant.StatusRebuilding || installed.Status == constant.StatusUpgrading ||
|
||||
installed.Status == constant.StatusSyncing || installed.Status == constant.StatusUninstalling || installed.Status == constant.StatusInstallErr ||
|
||||
installed.Status == constant.StatusStarting || installed.Status == constant.StatusRestarting || installed.Status == constant.StatusWaiting
|
||||
return appInstallOperationPending(installed.Status) || installed.Status == constant.StatusSyncing ||
|
||||
installed.Status == constant.StatusInstallErr || installed.Status == constant.StatusUpgradeErr || installed.Status == constant.StatusUpErr
|
||||
}
|
||||
|
||||
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 {
|
||||
if sync && appInstallOperationPending(installed.Status) {
|
||||
if err := syncAppInstallStatus(&installed, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if updated && ignoreUpdate(installed) {
|
||||
continue
|
||||
}
|
||||
@@ -1554,11 +1622,14 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
||||
|
||||
var versions []string
|
||||
for _, appDetail := range details {
|
||||
if !isAIGatewayUpgradeVersionAllowed(installed.App.Key, installed.Version, appDetail.Version) {
|
||||
continue
|
||||
}
|
||||
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(appDetail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
||||
if len(ignores) > 0 {
|
||||
continue
|
||||
}
|
||||
if common.IsCrossVersion(installed.Version, appDetail.Version) && !installed.App.CrossVersionUpdate {
|
||||
if isCrossAppVersion(installed.App.Key, installed.Version, appDetail.Version) && !installed.App.CrossVersionUpdate {
|
||||
continue
|
||||
}
|
||||
versions = append(versions, appDetail.Version)
|
||||
@@ -1573,7 +1644,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
||||
continue
|
||||
}
|
||||
|
||||
versions = common.GetSortedVersions(versions)
|
||||
versions = sortAppVersions(installed.App.Key, versions)
|
||||
lastVersion := versions[0]
|
||||
|
||||
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
|
||||
} else {
|
||||
installDTO.CanUpdate = common.CompareVersion(lastVersion, installed.Version)
|
||||
installDTO.CanUpdate = isNewerAppVersion(installed.App.Key, lastVersion, installed.Version)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
siteDir := dir.(string)
|
||||
if siteDir == "" || !strings.HasPrefix(siteDir, "/") {
|
||||
@@ -2262,7 +2336,12 @@ func getAppVersions(key string, details []model.AppDetail) []string {
|
||||
}
|
||||
versionsRaw = append(versionsRaw, detail.Version)
|
||||
}
|
||||
versions = common.GetSortedVersions(versionsRaw)
|
||||
if isAIGatewayApp(key) {
|
||||
sortAIGatewayVersions(versionsRaw)
|
||||
versions = versionsRaw
|
||||
} else {
|
||||
versions = sortAppVersions(key, versionsRaw)
|
||||
}
|
||||
if hasLatest {
|
||||
versions = append([]string{latestVersion}, versions...)
|
||||
}
|
||||
|
||||
@@ -132,11 +132,9 @@ func (u *BackupService) AppRecover(req dto.CommonRecover) error {
|
||||
if !fileOp.Stat(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 {
|
||||
global.LOG.Errorf("recover app %s failed, err: %v", req.DetailName, err)
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
// 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 (
|
||||
err error
|
||||
recoverTask *task.Task
|
||||
isOk = false
|
||||
rollbackFile string
|
||||
@@ -210,11 +220,13 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
||||
if parentTask == nil {
|
||||
recoverTask, err = task.NewTaskWithOps(install.Name, task.TaskRecover, task.TaskScopeBackup, taskID, install.ID)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
recoverApp := func(t *task.Task) error {
|
||||
defer cleanup()
|
||||
fileOp := files.NewFileOp()
|
||||
if !isRollback {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if err := fileOp.TarGzExtractPro(recoverFile, path.Dir(recoverFile), secret); err != nil {
|
||||
return err
|
||||
tmpPath := preparedPath
|
||||
if tmpPath == "" {
|
||||
if err := fileOp.TarGzExtractPro(recoverFile, path.Dir(recoverFile), secret); err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -236,7 +251,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
||||
} else {
|
||||
_, _ = 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") {
|
||||
@@ -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)
|
||||
go func() {
|
||||
defer cleanup()
|
||||
_ = recoverTask.Execute()
|
||||
}()
|
||||
return nil
|
||||
|
||||
@@ -371,43 +371,39 @@ func loadComposePathAndFiles(composeName string, dockerClient *client.Client) (s
|
||||
}
|
||||
config := targetContainer.Labels[composeConfigLabel]
|
||||
workdir := targetContainer.Labels[composeWorkdirLabel]
|
||||
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
|
||||
composePath = config
|
||||
} else {
|
||||
composePath = workdir
|
||||
files, err := resolveComposeFiles(workdir, config)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
composePath = strings.Join(files, ",")
|
||||
}
|
||||
composeFiles := normalizeComposeFiles(composePath)
|
||||
if len(composeFiles) == 0 {
|
||||
return "", nil, fmt.Errorf("compose file not found for %s", composeName)
|
||||
composeFiles, err := normalizeComposeFiles(composePath)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return composePath, composeFiles, nil
|
||||
return strings.Join(composeFiles, ","), composeFiles, nil
|
||||
}
|
||||
|
||||
func normalizeComposeFiles(composePath string) []string {
|
||||
items := strings.Split(composePath, ",")
|
||||
result := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
for _, item := range items {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
stat, err := os.Stat(item)
|
||||
if err == nil && stat.IsDir() {
|
||||
item = path.Join(item, "docker-compose.yml")
|
||||
}
|
||||
if _, err := os.Stat(item); err != nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[item]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
func normalizeComposeFiles(composePath string) ([]string, error) {
|
||||
files, err := resolveComposeFiles("", composePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
for i, file := range files {
|
||||
info, err := os.Stat(file)
|
||||
if err == nil && info.IsDir() {
|
||||
file = filepath.Join(file, "docker-compose.yml")
|
||||
info, err = os.Stat(file)
|
||||
files[i] = file
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("compose path %s is not a regular file", file)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (c *composeBackupContext) close() {
|
||||
@@ -621,20 +617,46 @@ func safeJoinWithinBase(baseDir, name string) (string, error) {
|
||||
}
|
||||
|
||||
func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
||||
if recoverCtx.targetDir != "" {
|
||||
_ = os.RemoveAll(recoverCtx.targetDir)
|
||||
composeFiles := recoverCtx.meta.Files
|
||||
if strings.TrimSpace(recoverCtx.meta.ComposePath) != "" {
|
||||
filesByPath := make(map[string]composeBackupFile, len(composeFiles))
|
||||
for _, item := range composeFiles {
|
||||
filesByPath[filepath.Clean(item.OriginalPath)] = item
|
||||
}
|
||||
orderedFiles := make([]composeBackupFile, 0, len(composeFiles))
|
||||
for _, file := range strings.Split(recoverCtx.meta.ComposePath, ",") {
|
||||
file = strings.TrimSpace(file)
|
||||
if file == "" {
|
||||
continue
|
||||
}
|
||||
file = filepath.Clean(file)
|
||||
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 err := os.MkdirAll(recoverCtx.targetDir, os.ModePerm); err != nil {
|
||||
return err
|
||||
if len(composeFiles) == 0 {
|
||||
return fmt.Errorf("compose files are missing from backup metadata")
|
||||
}
|
||||
restored := make([]string, 0, len(recoverCtx.meta.Files))
|
||||
for _, item := range recoverCtx.meta.Files {
|
||||
restored := make([]string, 0, len(composeFiles))
|
||||
sourceByTarget := make(map[string]string, len(composeFiles))
|
||||
for _, 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)
|
||||
}
|
||||
if !recoverCtx.fileOp.Stat(backupPath) {
|
||||
continue
|
||||
info, err := os.Stat(backupPath)
|
||||
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
|
||||
if item.RelativePath != "" {
|
||||
@@ -650,6 +672,29 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -660,7 +705,6 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
||||
if err := recoverCtx.fileOp.SaveFile(targetPath, string(content), fs.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
restored = append(restored, targetPath)
|
||||
}
|
||||
envPath := path.Join(recoverCtx.tmpDir, "compose_files", ".env")
|
||||
if recoverCtx.fileOp.Stat(envPath) {
|
||||
@@ -672,14 +716,6 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
||||
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, ",")
|
||||
return nil
|
||||
}
|
||||
|
||||
+120
-85
@@ -58,10 +58,10 @@ type ContainerService struct{}
|
||||
var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>")
|
||||
|
||||
type IContainerService interface {
|
||||
Page(req dto.PageContainer) (int64, interface{}, error)
|
||||
Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error)
|
||||
List() []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)
|
||||
ListNetwork() ([]dto.Options, error)
|
||||
PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
|
||||
@@ -80,7 +80,7 @@ type IContainerService interface {
|
||||
ContainerUpdate(req dto.ContainerOperate) error
|
||||
ContainerUpgrade(req dto.ContainerUpgrade) 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)
|
||||
LoadResourceLimit() (*dto.ResourceLimit, error)
|
||||
ContainerRename(req dto.ContainerRename) error
|
||||
@@ -112,7 +112,7 @@ func NewIContainerService() IContainerService {
|
||||
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()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
@@ -123,7 +123,7 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro
|
||||
options.Filters = filters.NewArgs()
|
||||
options.Filters.Add("label", req.Filters)
|
||||
}
|
||||
containers, err := client.ContainerList(context.Background(), options)
|
||||
containers, err := client.ContainerList(ctx, options)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
@@ -206,27 +206,30 @@ func (u *ContainerService) ListByImage(imageName string) []dto.ContainerOptions
|
||||
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
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
defer client.Close()
|
||||
c := context.Background()
|
||||
|
||||
images, _ := client.ImageList(c, image.ListOptions{All: true})
|
||||
data.ImageCount = len(images)
|
||||
repo, _ := imageRepoRepo.List()
|
||||
data.RepoCount = len(repo)
|
||||
templates, _ := composeRepo.List()
|
||||
data.ComposeTemplateCount = len(templates)
|
||||
networks, _ := client.NetworkList(c, network.ListOptions{})
|
||||
data.NetworkCount = len(networks)
|
||||
volumes, _ := client.VolumeList(c, volume.ListOptions{})
|
||||
data.VolumeCount = len(volumes.Volumes)
|
||||
data.ComposeCount = loadComposeCount(client)
|
||||
containers, _ := client.ContainerList(c, container.ListOptions{All: true})
|
||||
if !containersOnly {
|
||||
images, _ := client.ImageList(ctx, image.ListOptions{All: true})
|
||||
data.ImageCount = len(images)
|
||||
repo, _ := imageRepoRepo.List()
|
||||
data.RepoCount = len(repo)
|
||||
templates, _ := composeRepo.List()
|
||||
data.ComposeTemplateCount = len(templates)
|
||||
networks, _ := client.NetworkList(ctx, network.ListOptions{})
|
||||
data.NetworkCount = len(networks)
|
||||
volumes, _ := client.VolumeList(ctx, volume.ListOptions{})
|
||||
data.VolumeCount = len(volumes.Volumes)
|
||||
data.ComposeCount = loadComposeCount(client)
|
||||
}
|
||||
containers, err := client.ContainerList(ctx, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
data.ContainerCount = len(containers)
|
||||
for _, item := range containers {
|
||||
switch item.State {
|
||||
@@ -292,25 +295,48 @@ func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.Opera
|
||||
}
|
||||
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()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
datas := make([]dto.ContainerListStats, len(list))
|
||||
jobs := make(chan int)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(list))
|
||||
for i := 0; i < len(list); i++ {
|
||||
go func(index int, item container.Summary) {
|
||||
datas[index] = loadCpuAndMem(client, item.ID)
|
||||
wg.Done()
|
||||
}(i, list[i])
|
||||
for worker := 0; worker < min(8, len(list)); worker++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for index := range jobs {
|
||||
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()
|
||||
return datas, nil
|
||||
}
|
||||
@@ -326,40 +352,47 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
|
||||
case "container":
|
||||
inspectInfo, err = client.ContainerInspect(context.Background(), req.ID)
|
||||
case "compose":
|
||||
filePath := ""
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cli.Close()
|
||||
config, workdir := "", ""
|
||||
options := container.ListOptions{All: true}
|
||||
options.Filters = filters.NewArgs()
|
||||
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 {
|
||||
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 {
|
||||
composeItem, _ := composeRepo.GetRecord(repo.WithByName(req.ID))
|
||||
filePath = composeItem.Path
|
||||
composeItem, err := composeRepo.GetRecord(repo.WithByName(req.ID))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
config = composeItem.Path
|
||||
workdir = filepath.Dir(primaryComposePath(config))
|
||||
} else {
|
||||
config = containers[0].Labels[composeConfigLabel]
|
||||
workdir = containers[0].Labels[composeWorkdirLabel]
|
||||
}
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -383,11 +416,6 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
|
||||
}
|
||||
|
||||
func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
name := ""
|
||||
switch req.PruneType {
|
||||
case "container":
|
||||
@@ -408,6 +436,14 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) 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()
|
||||
if req.WithTagAll {
|
||||
pruneFilters.Add("dangling", "false")
|
||||
@@ -431,10 +467,7 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
||||
}
|
||||
SpaceReclaimed = int(rep.SpaceReclaimed)
|
||||
case "network":
|
||||
_, err := client.NetworksPrune(context.Background(), pruneFilters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cleanUnusedNetworks(t, client)
|
||||
case "volume":
|
||||
versions, err := client.ServerVersion(context.Background())
|
||||
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) {
|
||||
messageChan := make(chan string, 1024)
|
||||
errorChan := make(chan error, 1)
|
||||
doneChan := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
<-ctx.Request.Context().Done()
|
||||
close(doneChan)
|
||||
}()
|
||||
logCtx, cancel := context.WithCancel(ctx.Request.Context())
|
||||
defer cancel()
|
||||
doneChan := logCtx.Done()
|
||||
go collectLogs(doneChan, params, messageChan, errorChan)
|
||||
ctx.Stream(func(w io.Writer) bool {
|
||||
select {
|
||||
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
|
||||
}
|
||||
return writeContainerLogMessage(w, doneChan, messageChan, errorChan)
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
defer close(messageChan)
|
||||
defer close(errorChan)
|
||||
@@ -1715,11 +1748,13 @@ func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo
|
||||
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{
|
||||
ContainerID: containerItem,
|
||||
}
|
||||
res, err := client.ContainerStats(context.Background(), containerItem, false)
|
||||
res, err := client.ContainerStats(ctx, containerItem, false)
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -71,10 +71,9 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
ContainerCount: 0,
|
||||
CreatedAt: localItem.CreatedAt.Format(constant.DateTimeLayout),
|
||||
ConfigFile: localItem.Path,
|
||||
Workdir: strings.TrimSuffix(localItem.Path, "/docker-compose.yml"),
|
||||
Workdir: filepath.Dir(primaryComposePath(localItem.Path)),
|
||||
}
|
||||
composeItemLocal.CreatedBy = "1Panel"
|
||||
composeItemLocal.Path = localItem.Path
|
||||
composeLocalMap[localItem.Name] = composeItemLocal
|
||||
}
|
||||
|
||||
@@ -112,11 +111,6 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
if ok {
|
||||
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++ {
|
||||
if composeCreatedByLocal[i].Name == name {
|
||||
composeItem.CreatedBy = "1Panel"
|
||||
@@ -147,7 +141,9 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
|
||||
for key, value := range mergedMap {
|
||||
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]
|
||||
records = append(records, value)
|
||||
}
|
||||
@@ -192,29 +188,6 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
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) {
|
||||
if err := validateComposeCreateName(req); err != nil {
|
||||
return false, err
|
||||
@@ -522,10 +495,6 @@ func isComposeProjectNameEmptyError(err error) bool {
|
||||
}
|
||||
|
||||
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) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
@@ -535,13 +504,19 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
||||
}
|
||||
if req.WithFile {
|
||||
for _, item := range strings.Split(req.Path, ",") {
|
||||
if len(item) != 0 {
|
||||
_ = os.RemoveAll(path.Dir(item))
|
||||
item = strings.TrimSpace(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 stdout, err := compose.Up(req.Path, req.Name); err != nil {
|
||||
@@ -584,13 +559,13 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
||||
write.Flush()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
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 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 {
|
||||
if _, err := os.Stat(composePath); err == nil {
|
||||
if stdout, err := compose.Operate(composePath, "down", composeName); err != nil {
|
||||
return errors.New(stdout)
|
||||
composeFiles, err := resolveComposeFiles("", composePath)
|
||||
filesAvailable := err == nil
|
||||
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
|
||||
}
|
||||
@@ -722,14 +706,17 @@ func removeContainerForCompose(composeName, composePath string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var removeErrors []error
|
||||
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 {
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
|
||||
func recreateCompose(content, composePath, detailPath, projectName string) error {
|
||||
file, err := os.OpenFile(detailPath, os.O_WRONLY|os.O_TRUNC, 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -738,7 +725,7 @@ func recreateCompose(content, path, projectName string) error {
|
||||
_, _ = write.WriteString(content)
|
||||
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 nil
|
||||
@@ -746,6 +733,9 @@ func recreateCompose(content, path, projectName string) error {
|
||||
|
||||
func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo {
|
||||
for i := 0; i < len(list); i++ {
|
||||
if list[i].Path == "" {
|
||||
continue
|
||||
}
|
||||
tmpPath := list[i].Path
|
||||
if strings.Contains(list[i].Path, ",") {
|
||||
tmpPath = strings.Split(list[i].Path, ",")[0]
|
||||
@@ -774,3 +764,35 @@ func newComposeEnv(pathItem string, env string) error {
|
||||
}
|
||||
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 (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"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/i18n"
|
||||
"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/client"
|
||||
"github.com/docker/docker/errdefs"
|
||||
)
|
||||
|
||||
var networkCleanupSlot = make(chan struct{}, 1)
|
||||
|
||||
func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
@@ -172,3 +180,114 @@ func (u *ContainerService) CreateNetwork(req dto.NetworkCreate) error {
|
||||
}
|
||||
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/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/pkg/errors"
|
||||
@@ -75,6 +76,7 @@ func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interfac
|
||||
EntryID: cronjob.ID,
|
||||
}
|
||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable))
|
||||
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
|
||||
if alertInfo.SendCount != 0 {
|
||||
item.AlertCount = alertInfo.SendCount
|
||||
} else {
|
||||
@@ -98,9 +100,11 @@ func (u *CronjobService) LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, err
|
||||
AlertType: cronjob.Type,
|
||||
EntryID: cronjob.ID,
|
||||
}
|
||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable))
|
||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))))
|
||||
item.AlertMethod = alertInfo.Method
|
||||
if alertInfo.SendCount != 0 {
|
||||
item.AlertTitle = alertInfo.Title
|
||||
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
|
||||
if alertInfo.Status == constant.AlertEnable {
|
||||
item.AlertCount = alertInfo.SendCount
|
||||
} else {
|
||||
item.AlertCount = 0
|
||||
@@ -195,11 +199,12 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
|
||||
}
|
||||
}
|
||||
item.SourceAccounts, item.DownloadAccount, _ = loadBackupNamesByID(cronjob.SourceAccountIDs, cronjob.DownloadAccountID)
|
||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))), repo.WithByStatus(constant.AlertEnable))
|
||||
if alertInfo.SendCount != 0 {
|
||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))))
|
||||
item.AlertTitle = alertInfo.Title
|
||||
item.AlertMethod = alertInfo.Method
|
||||
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
|
||||
if alertInfo.Status == constant.AlertEnable {
|
||||
item.AlertCount = alertInfo.SendCount
|
||||
item.AlertTitle = alertInfo.Title
|
||||
item.AlertMethod = alertInfo.Method
|
||||
} else {
|
||||
item.AlertCount = 0
|
||||
}
|
||||
@@ -213,6 +218,17 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
|
||||
}
|
||||
|
||||
func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
|
||||
for _, item := range req {
|
||||
advanced, err := cronJobAlertAdvancedParams(item.AlertTriggerMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if item.AlertCount != 0 {
|
||||
if err := (AlertService{}).validateCronJobAlertChannels(item.Type, advanced, item.AlertMethod); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, item := range req {
|
||||
cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name))
|
||||
if cronjobItem.ID != 0 {
|
||||
@@ -395,17 +411,27 @@ func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
|
||||
} else {
|
||||
cronjob.Status = constant.StatusDisable
|
||||
}
|
||||
_ = cronjobRepo.Create(&cronjob)
|
||||
if item.AlertCount != 0 && item.AlertTitle != "" && item.AlertMethod != "" {
|
||||
if err := cronjobRepo.Create(&cronjob); err != nil {
|
||||
return err
|
||||
}
|
||||
if item.AlertTitle != "" && item.AlertMethod != "" {
|
||||
advanced, _ := cronJobAlertAdvancedParams(item.AlertTriggerMode)
|
||||
status := constant.AlertEnable
|
||||
if item.AlertCount == 0 {
|
||||
status = constant.AlertDisable
|
||||
}
|
||||
createAlert := dto.AlertCreate{
|
||||
Title: item.AlertTitle,
|
||||
SendCount: item.AlertCount,
|
||||
Method: item.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronjob.ID)),
|
||||
Status: constant.AlertEnable,
|
||||
Title: item.AlertTitle,
|
||||
SendCount: item.AlertCount,
|
||||
Method: item.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronjob.ID)),
|
||||
Status: status,
|
||||
AdvancedParams: advanced,
|
||||
}
|
||||
if err := NewIAlertService().CreateAlert(createAlert, operator); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = NewIAlertService().CreateAlert(createAlert, operator)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -562,6 +588,15 @@ func (u *CronjobService) HandleOnce(id uint) error {
|
||||
}
|
||||
|
||||
func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
|
||||
advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.AlertCount != 0 {
|
||||
if err := (AlertService{}).validateCronJobAlertChannels(req.Type, advanced, req.AlertMethod); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
cronjob, _ := cronjobRepo.Get(repo.WithByName(req.Name))
|
||||
if cronjob.ID != 0 {
|
||||
return buserr.New("ErrRecordExist")
|
||||
@@ -603,12 +638,13 @@ func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
|
||||
}
|
||||
if req.AlertCount != 0 && req.AlertTitle != "" && req.AlertMethod != "" {
|
||||
createAlert := dto.AlertCreate{
|
||||
Title: req.AlertTitle,
|
||||
SendCount: req.AlertCount,
|
||||
Method: req.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronjob.ID)),
|
||||
Status: constant.AlertEnable,
|
||||
Title: req.AlertTitle,
|
||||
SendCount: req.AlertCount,
|
||||
Method: req.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronjob.ID)),
|
||||
Status: constant.AlertEnable,
|
||||
AdvancedParams: advanced,
|
||||
}
|
||||
err := NewIAlertService().CreateAlert(createAlert, operator)
|
||||
if err != nil {
|
||||
@@ -682,6 +718,10 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
|
||||
}
|
||||
|
||||
func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string) error {
|
||||
advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var cronjob model.Cronjob
|
||||
if err := copier.Copy(&cronjob, &req); err != nil {
|
||||
return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||
@@ -697,6 +737,20 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
|
||||
if err != nil {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
if req.AlertCount != 0 {
|
||||
previous, _ := alertRepo.Get(alertRepo.WithByType(cronModel.Type), alertRepo.WithByProject(strconv.Itoa(int(id))))
|
||||
merged, err := prepareCronJobAlertParams(cronModel.Type, previous.AdvancedParams, advanced)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
method := req.AlertMethod
|
||||
if method == "" {
|
||||
method = previous.Method
|
||||
}
|
||||
if err := (AlertService{}).validateCronJobAlertChannels(cronModel.Type, merged, method); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
upMap := make(map[string]interface{})
|
||||
cronjob.EntryIDs = cronModel.EntryIDs
|
||||
cronjob.Type = cronModel.Type
|
||||
@@ -753,11 +807,12 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
|
||||
return err
|
||||
}
|
||||
updateAlert := dto.AlertCreate{
|
||||
Title: req.AlertTitle,
|
||||
SendCount: req.AlertCount,
|
||||
Method: req.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronModel.ID)),
|
||||
Title: req.AlertTitle,
|
||||
SendCount: req.AlertCount,
|
||||
Method: req.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronModel.ID)),
|
||||
AdvancedParams: advanced,
|
||||
}
|
||||
err = NewIAlertService().ExternalUpdateAlert(updateAlert, operator)
|
||||
if err != nil {
|
||||
@@ -766,6 +821,17 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
|
||||
return nil
|
||||
}
|
||||
|
||||
func cronJobAlertAdvancedParams(mode string) (string, error) {
|
||||
if mode == "" {
|
||||
return "", nil
|
||||
}
|
||||
data, err := json.Marshal(map[string]string{"alertTriggerMode": mode})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return alertUtil.MergeCronJobAlertParams("", string(data))
|
||||
}
|
||||
|
||||
func (u *CronjobService) UpdateStatus(id uint, status string) error {
|
||||
cronjob, _ := cronjobRepo.Get(repo.WithByID(id))
|
||||
if cronjob.ID == 0 {
|
||||
|
||||
@@ -412,6 +412,7 @@ func addSkipTask(source string, taskItem *task.Task) {
|
||||
taskItem.Log(i18n.GetMsgByKey("NoSuchResource"))
|
||||
return nil
|
||||
}, nil)
|
||||
taskItem.SubTasks[len(taskItem.SubTasks)-1].StepAlias = cronJobSkippedStep
|
||||
}
|
||||
|
||||
func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ntp"
|
||||
@@ -56,10 +58,11 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
|
||||
_ = taskRepo.Save(context.Background(), taskItem.Task)
|
||||
}
|
||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||
handleCronJobAlert(cronjob)
|
||||
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
|
||||
return
|
||||
}
|
||||
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
|
||||
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil))
|
||||
}()
|
||||
return
|
||||
}
|
||||
@@ -70,19 +73,20 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
|
||||
record.TaskID = ""
|
||||
}
|
||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||
handleCronJobAlert(cronjob)
|
||||
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
taskItem, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
|
||||
if len(taskItem.ID) == 0 {
|
||||
storedTask, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
|
||||
if len(storedTask.ID) == 0 {
|
||||
record.TaskID = ""
|
||||
}
|
||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||
handleCronJobAlert(cronjob)
|
||||
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
|
||||
} else {
|
||||
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
|
||||
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil))
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -482,8 +486,33 @@ func hasBackup(cronjobType string) bool {
|
||||
return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log" || cronjobType == "cutWebsiteLog"
|
||||
}
|
||||
|
||||
func handleCronJobAlert(cronjob *model.Cronjob) {
|
||||
const cronJobSkippedStep = "cronjob-skipped"
|
||||
|
||||
func cronJobAlertResult(taskItem *task.Task, err error) string {
|
||||
if errors.Is(err, context.Canceled) || taskItem.Task.Status == constant.StatusCanceled ||
|
||||
(taskItem.TaskCtx != nil && taskItem.TaskCtx.Err() != nil) {
|
||||
return ""
|
||||
}
|
||||
if err != nil {
|
||||
return alertUtil.CronJobAlertFailed
|
||||
}
|
||||
if taskItem.Task.Status != constant.StatusSuccess {
|
||||
return ""
|
||||
}
|
||||
for _, subTask := range taskItem.SubTasks {
|
||||
if subTask.StepAlias != cronJobSkippedStep {
|
||||
return alertUtil.CronJobAlertSuccess
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func handleCronJobAlert(cronjob *model.Cronjob, result string) {
|
||||
if result == "" {
|
||||
return
|
||||
}
|
||||
pushAlert := dto.PushAlert{
|
||||
Result: result,
|
||||
TaskName: cronjob.Name,
|
||||
AlertType: cronjob.Type,
|
||||
EntryID: cronjob.ID,
|
||||
|
||||
+105
-61
@@ -43,6 +43,10 @@ type IDashboardService interface {
|
||||
|
||||
LoadQuickOptions() []dto.QuickJump
|
||||
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)
|
||||
ChangeShow(req dto.SettingUpdate) error
|
||||
@@ -242,7 +246,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
|
||||
currentInfo.SwapMemoryUsed = swapInfo.Used
|
||||
currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent
|
||||
|
||||
currentInfo.DiskData = loadDiskInfo()
|
||||
currentInfo.DiskData = loadDiskInfo(false)
|
||||
currentInfo.GPUData, currentInfo.NPUData, currentInfo.XPUData = loadAcceleratorInfo()
|
||||
|
||||
if ioOption == "all" {
|
||||
@@ -397,16 +401,15 @@ func (u *DashboardService) LoadQuickOptions() []dto.QuickJump {
|
||||
}
|
||||
func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
|
||||
showCount := 0
|
||||
var quicks []model.QuickJump
|
||||
quicks := make([]model.QuickJump, 0, len(req.Quicks))
|
||||
for _, item := range req.Quicks {
|
||||
var quick model.QuickJump
|
||||
if item.IsShow {
|
||||
showCount++
|
||||
}
|
||||
if err := copier.Copy(&quick, &item); err != nil {
|
||||
return err
|
||||
}
|
||||
quicks = append(quicks, quick)
|
||||
quicks = append(quicks, model.QuickJump{
|
||||
BaseModel: model.BaseModel{ID: item.ID},
|
||||
IsShow: item.IsShow,
|
||||
})
|
||||
}
|
||||
if showCount == 0 {
|
||||
return buserr.New("ErrMinQuickJump")
|
||||
@@ -455,24 +458,9 @@ type diskInfo struct {
|
||||
Device string
|
||||
}
|
||||
|
||||
func loadDiskInfo() []dto.DiskInfo {
|
||||
func loadDiskInfo(forceRefresh bool) []dto.DiskInfo {
|
||||
var datas []dto.DiskInfo
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||
format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
|
||||
stdout, err := cmdMgr.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||
stdout, err = cmdMgr2.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
return datas
|
||||
}
|
||||
}
|
||||
stdout := loadDiskMounts()
|
||||
lines := strings.Split(stdout, "\n")
|
||||
|
||||
var mounts []diskInfo
|
||||
@@ -520,43 +508,22 @@ func loadDiskInfo() []dto.DiskInfo {
|
||||
itemData.Type = mount.Type
|
||||
itemData.Device = mount.Device
|
||||
|
||||
type diskResult struct {
|
||||
state *disk.UsageStat
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan diskResult, 1)
|
||||
|
||||
go func() {
|
||||
state, err := psutil.DISK.GetUsage(mount.Mount, false)
|
||||
resultCh <- diskResult{state: state, err: err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
|
||||
case result := <-resultCh:
|
||||
if result.err != nil {
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, result.err)
|
||||
return
|
||||
}
|
||||
itemData.Total = result.state.Total
|
||||
itemData.Free = result.state.Free
|
||||
itemData.Used = result.state.Used
|
||||
itemData.UsedPercent = result.state.UsedPercent
|
||||
itemData.InodesTotal = result.state.InodesTotal
|
||||
itemData.InodesUsed = result.state.InodesUsed
|
||||
itemData.InodesFree = result.state.InodesFree
|
||||
itemData.InodesUsedPercent = result.state.InodesUsedPercent
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
state, err := loadDiskUsageWithTimeout(mount.Mount, forceRefresh)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
|
||||
} else {
|
||||
itemData.Total = state.Total
|
||||
itemData.Free = state.Free
|
||||
itemData.Used = state.Used
|
||||
itemData.UsedPercent = state.UsedPercent
|
||||
itemData.InodesTotal = state.InodesTotal
|
||||
itemData.InodesUsed = state.InodesUsed
|
||||
itemData.InodesFree = state.InodesFree
|
||||
itemData.InodesUsedPercent = state.InodesUsedPercent
|
||||
}
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
}(mounts[i])
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -567,6 +534,69 @@ func loadDiskInfo() []dto.DiskInfo {
|
||||
return datas
|
||||
}
|
||||
|
||||
var diskMountsMu sync.Mutex
|
||||
|
||||
func loadDiskMounts() string {
|
||||
if !diskMountsMu.TryLock() {
|
||||
return ""
|
||||
}
|
||||
resultCh := make(chan string, 1)
|
||||
go func() {
|
||||
var stdout string
|
||||
defer func() {
|
||||
diskMountsMu.Unlock()
|
||||
resultCh <- stdout
|
||||
}()
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||
format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
|
||||
output, err := cmdMgr.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info with df -hT -P failed, err: %v", err)
|
||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||
output, err = cmdMgr2.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info with df -lhT -P failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stdout = output
|
||||
}()
|
||||
timer := time.NewTimer(3 * time.Second)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case stdout := <-resultCh:
|
||||
return stdout
|
||||
case <-timer.C:
|
||||
global.LOG.Error("load disk mounts timed out; df collection is still running")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func loadDiskUsageWithTimeout(path string, forceRefresh bool) (*disk.UsageStat, error) {
|
||||
type diskResult struct {
|
||||
state *disk.UsageStat
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan diskResult, 1)
|
||||
go func() {
|
||||
state, err := psutil.DISK.GetUsage(path, forceRefresh)
|
||||
resultCh <- diskResult{state: state, err: err}
|
||||
}()
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
return nil, fmt.Errorf("load disk usage from %s: timeout", path)
|
||||
case result := <-resultCh:
|
||||
return result.state, result.err
|
||||
}
|
||||
}
|
||||
|
||||
func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
||||
ok, client := accelerator.New()
|
||||
if !ok {
|
||||
@@ -586,6 +616,9 @@ func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
||||
xpuData []dto.XPUInfo
|
||||
)
|
||||
for _, device := range snapshot.Devices {
|
||||
if device.ParentID != "" {
|
||||
continue
|
||||
}
|
||||
switch device.Kind {
|
||||
case accelerator.KindGPU:
|
||||
if device.GPU == nil {
|
||||
@@ -595,7 +628,11 @@ func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
||||
if err := copier.Copy(&dataItem, device.GPU); err != nil {
|
||||
continue
|
||||
}
|
||||
dataItem.PowerUsage = dataItem.PowerDraw + " / " + dataItem.MaxPowerLimit
|
||||
dataItem.MaxPowerLimit = device.GPU.PowerLimit
|
||||
dataItem.PowerUsage = dataItem.PowerDraw
|
||||
if dataItem.MaxPowerLimit != "" {
|
||||
dataItem.PowerUsage += " / " + dataItem.MaxPowerLimit
|
||||
}
|
||||
dataItem.MemoryUsage = dataItem.MemUsed + " / " + dataItem.MemTotal
|
||||
gpuData = append(gpuData, dataItem)
|
||||
case accelerator.KindNPU:
|
||||
@@ -672,6 +709,13 @@ func loadQuickJump(base *dto.DashboardBase) {
|
||||
quicks := launcherRepo.ListQuickJump(false)
|
||||
for i := 0; i < len(quicks); i++ {
|
||||
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":
|
||||
quicks[i].Detail = fmt.Sprintf("%d", base.AgentNumber)
|
||||
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) {
|
||||
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) {
|
||||
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")
|
||||
}
|
||||
|
||||
cli, version, err := LoadMysqlClientByFrom(req.Database)
|
||||
cli, version, err := loadMysqlClientByFrom(req.Database, mcpBounded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
createItem.MysqlName = req.Database
|
||||
defer cli.Close()
|
||||
if err := cli.Create(client.CreateInfo{
|
||||
createInfo := client.CreateInfo{
|
||||
Name: req.Name,
|
||||
Format: req.Format,
|
||||
Collation: req.Collation,
|
||||
@@ -485,9 +489,31 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
|
||||
Permission: req.Permission,
|
||||
Version: version,
|
||||
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
|
||||
}
|
||||
if mcpBounded {
|
||||
if err := persistMCPMysqlMetadata(ctx, req, permissionHosts, &createItem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &createItem, nil
|
||||
}
|
||||
if len(req.Username) != 0 {
|
||||
if err := saveDatabaseUserCredentials(dbType, req.Database, req.Username, req.Permission, req.Password, req.Description); err != nil {
|
||||
return nil, err
|
||||
@@ -506,6 +532,45 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
|
||||
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) {
|
||||
dbType, err := resolveDatabaseUserType(req.Database)
|
||||
if err != nil {
|
||||
@@ -1398,7 +1463,14 @@ func (u *MysqlService) LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlS
|
||||
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 {
|
||||
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) {
|
||||
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 {
|
||||
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) {
|
||||
return loadMysqlClientByFrom(database, false)
|
||||
}
|
||||
|
||||
func loadMysqlClientByFrom(database string, mcpLocalOnly bool) (mysql.MysqlClient, string, error) {
|
||||
var (
|
||||
dbInfo client.DBInfo
|
||||
version string
|
||||
@@ -1542,6 +1618,9 @@ func LoadMysqlClientByFrom(database string) (mysql.MysqlClient, string, error) {
|
||||
if err != nil {
|
||||
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.From = databaseItem.From
|
||||
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/exec"
|
||||
"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/global"
|
||||
@@ -17,12 +23,15 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"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"
|
||||
)
|
||||
|
||||
type RedisService struct{}
|
||||
|
||||
// The CLI container is shared by all remote Redis databases on this node.
|
||||
var redisCliInstallMutex sync.Mutex
|
||||
|
||||
type IRedisService interface {
|
||||
UpdateConf(req dto.RedisConfUpdate) error
|
||||
UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
|
||||
@@ -33,7 +42,7 @@ type IRedisService interface {
|
||||
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
|
||||
|
||||
CheckHasCli() bool
|
||||
InstallCli() error
|
||||
InstallCli(req dto.RedisCliInstall) (string, error)
|
||||
}
|
||||
|
||||
func NewIRedisService() IRedisService {
|
||||
@@ -61,30 +70,65 @@ func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error {
|
||||
}
|
||||
|
||||
func (u *RedisService) CheckHasCli() bool {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
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
|
||||
installed, _ := u.checkCliInstalled()
|
||||
return installed
|
||||
}
|
||||
|
||||
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{
|
||||
TaskID: req.TaskID,
|
||||
Name: "1Panel-redis-cli-tools",
|
||||
Image: "redis:7.4.4",
|
||||
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 {
|
||||
|
||||
@@ -73,7 +73,7 @@ func (u *DeviceService) LoadBaseInfo() (dto.DeviceBaseInfo, error) {
|
||||
if baseInfo.SwapMemoryTotal != 0 {
|
||||
baseInfo.SwapDetails = loadSwap()
|
||||
}
|
||||
disks := loadDiskInfo()
|
||||
disks := loadDiskInfo(false)
|
||||
for _, item := range disks {
|
||||
baseInfo.MaxSize += item.Free
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
)
|
||||
|
||||
const dockerNftablesMinVersion = "29.0.0"
|
||||
@@ -82,6 +84,11 @@ func (u *DockerService) UpdateFirewallBackend(backend string) error {
|
||||
if backend == constant.FirewallProviderNftables && !dockerNftablesSupported(version) {
|
||||
return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion)
|
||||
}
|
||||
if backend == constant.FirewallProviderNftables {
|
||||
if err := dockerfirewall.CheckIPv4Forwarding(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
original, readErr := os.ReadFile(constant.DaemonJsonPath)
|
||||
existed := readErr == nil
|
||||
@@ -276,7 +283,8 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate, withRestart bool) erro
|
||||
delete(daemonMap, "ipv6")
|
||||
delete(daemonMap, "fixed-cidr-v6")
|
||||
delete(daemonMap, "ip6tables")
|
||||
if configuredDockerFirewallBackend() != constant.FirewallProviderNftables {
|
||||
backend, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||
if !strings.EqualFold(strings.TrimSpace(backend), constant.FirewallProviderNftables) {
|
||||
delete(daemonMap, "experimental")
|
||||
}
|
||||
}
|
||||
@@ -479,6 +487,9 @@ func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
|
||||
if err := controller.Handle(req.Operation, service); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Operation == "start" || req.Operation == "restart" {
|
||||
return RestoreDockerPortGuard(context.Background())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -32,10 +32,11 @@ var (
|
||||
scriptRepo = repo.NewIScriptRepo()
|
||||
cronjobRepo = repo.NewICronjobRepo()
|
||||
|
||||
hostRepo = repo.NewIHostRepo()
|
||||
ftpRepo = repo.NewIFtpRepo()
|
||||
clamRepo = repo.NewIClamRepo()
|
||||
monitorRepo = repo.NewIMonitorRepo()
|
||||
hostRepo = repo.NewIHostRepo()
|
||||
ftpRepo = repo.NewIFtpRepo()
|
||||
clamRepo = repo.NewIClamRepo()
|
||||
monitorRepo = repo.NewIMonitorRepo()
|
||||
vllmMonitorRepo = &repo.VLLMMonitorRepo{}
|
||||
|
||||
settingRepo = repo.NewISettingRepo()
|
||||
backupRepo = repo.NewIBackupRepo()
|
||||
|
||||
+40
-10
@@ -439,13 +439,15 @@ func (f *FileService) Compress(c request.FileCompress) error {
|
||||
if err := preflightCompressTool(files.CompressType(c.Type)); err != nil {
|
||||
return err
|
||||
}
|
||||
taskItem, err := task.NewTask(c.Name, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||
taskName := i18n.GetMsgWithMap("FileTaskCompress", map[string]interface{}{"dst": strconv.Quote(filepath.Join(c.Dst, c.Name))})
|
||||
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
taskItem.AddSubTask(c.Name, func(t *task.Task) error {
|
||||
t.LogStart(c.Name)
|
||||
taskItem.AddSubTask(taskName, func(t *task.Task) error {
|
||||
logFileTaskSources(t, c.Files)
|
||||
t.Log(i18n.GetMsgWithMap("FileTaskFormat", map[string]interface{}{"format": strconv.Quote(c.Type)}))
|
||||
compressType := files.CompressType(c.Type)
|
||||
dstFile := filepath.Join(c.Dst, c.Name)
|
||||
success := false
|
||||
@@ -516,13 +518,15 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
|
||||
if err := preflightDecompressTool(files.CompressType(c.Type)); err != nil {
|
||||
return err
|
||||
}
|
||||
taskItem, err := task.NewTask(c.Path, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||
taskName := i18n.GetMsgWithMap("FileTaskDecompress", map[string]interface{}{"dst": strconv.Quote(c.Dst)})
|
||||
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
taskItem.AddSubTask(c.Path, func(t *task.Task) error {
|
||||
t.LogStart(c.Path)
|
||||
taskItem.AddSubTask(taskName, func(t *task.Task) error {
|
||||
logFileTaskSources(t, []string{c.Path})
|
||||
t.Log(i18n.GetMsgWithMap("FileTaskFormat", map[string]interface{}{"format": strconv.Quote(c.Type)}))
|
||||
dstExisted := fo.Stat(c.Dst)
|
||||
parentDir := filepath.Dir(c.Dst)
|
||||
if !fo.Stat(parentDir) {
|
||||
@@ -878,6 +882,16 @@ func (f *FileService) ChangeName(req request.FileRename) error {
|
||||
if hasInvalidFileName(req.NewName) {
|
||||
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()
|
||||
content, mode, shouldRecordHistory := readEditableFileHistoryContent(req.OldName)
|
||||
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()
|
||||
options := files.DownloadOptions{
|
||||
IgnoreCertificate: w.IgnoreCertificate,
|
||||
UseServerFilename: w.UseServerFilename,
|
||||
}
|
||||
if w.UseProxy {
|
||||
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)
|
||||
}
|
||||
|
||||
func logFileTaskSources(t *task.Task, sources []string) {
|
||||
for _, source := range sources {
|
||||
t.Log(i18n.GetMsgWithMap("FileTaskSource", map[string]interface{}{"path": strconv.Quote(source)}))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileService) MvFile(m request.FileMove) error {
|
||||
fo := files.NewFileOp()
|
||||
if err := validateFileMove(fo, m); err != nil {
|
||||
@@ -924,15 +945,24 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
||||
if !fileTransferLocks.Acquire(m.TaskID, getFileTransferPaths(m)) {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
taskItem, err := task.NewTask(m.NewPath, task.TaskExec, task.TaskScopeTask, m.TaskID, 1)
|
||||
nameKey := "FileTaskCopy"
|
||||
if m.Type == "cut" {
|
||||
nameKey = "FileTaskMove"
|
||||
}
|
||||
taskName := i18n.GetMsgWithMap(nameKey, map[string]interface{}{"dst": strconv.Quote(m.NewPath)})
|
||||
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, m.TaskID, 1)
|
||||
if err != nil {
|
||||
fileTransferLocks.Release(m.TaskID)
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
defer fileTransferLocks.Release(m.TaskID)
|
||||
taskItem.AddSubTaskWithOps(m.NewPath, func(t *task.Task) error {
|
||||
t.LogStart(m.NewPath)
|
||||
taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error {
|
||||
logFileTaskSources(t, m.OldPaths)
|
||||
logFileTaskSources(t, m.CoverPaths)
|
||||
if m.Name != "" {
|
||||
t.Log(i18n.GetMsgWithMap("FileTaskRename", map[string]interface{}{"name": strconv.Quote(m.Name)}))
|
||||
}
|
||||
err := f.moveFileWithContext(t.TaskCtx, m)
|
||||
if err != nil && t.TaskCtx.Err() != nil {
|
||||
return t.TaskCtx.Err()
|
||||
@@ -1290,7 +1320,7 @@ func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.Exi
|
||||
}
|
||||
|
||||
func (f *FileService) GetHostMount() []dto.DiskInfo {
|
||||
return loadDiskInfo()
|
||||
return loadDiskInfo(false)
|
||||
}
|
||||
|
||||
func (f *FileService) GetUsersAndGroups() (*response.UserGroupResponse, error) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+974
-2607
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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/ping"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type IFirewallSettingService interface {
|
||||
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)
|
||||
Operate(context.Context, dto.FirewallBackendOperation) error
|
||||
OperateFamily(dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error)
|
||||
OperateIPv6(dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error)
|
||||
}
|
||||
|
||||
type FirewallSettingService struct{}
|
||||
|
||||
var firewallWhitelistTaskMu sync.Mutex
|
||||
var firewallWhitelistMu sync.Mutex
|
||||
|
||||
var ErrFirewallBackendCleanupRequired = errors.New("firewall backend cleanup required")
|
||||
|
||||
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 {
|
||||
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) (result error) {
|
||||
firewallWhitelistMu.Lock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
ports, err := firewall.ParsePortWhitelist(value)
|
||||
defer func() {
|
||||
firewallRuleMutationMu.Unlock()
|
||||
firewallWhitelistMu.Unlock()
|
||||
if result == nil {
|
||||
result = newFirewallService().SyncPortWhitelist(ctx)
|
||||
}
|
||||
}()
|
||||
current, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
required, err := firewallService.requiredPorts()
|
||||
current = append(current, request.Rule)
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
report("FirewallWhitelistSaved", "", nil)
|
||||
for _, port := range systemPorts(removed) {
|
||||
report("FirewallWhitelistReleased", whitelistPortLabel(port), nil)
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, panelPortWhitelistKey{}, required)
|
||||
provider, providerErr := firewallService.selectedProvider(ctx)
|
||||
ready := s.portWhitelistReadiness(provider, providerErr, firewallService)
|
||||
return syncPortWhitelist(ctx, ports, required, ready, firewallService.ensureSystemPortLocked, report)
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) savePortWhitelist(ctx context.Context, ports, required []firewall.PortWhitelist, firewallService *FirewallService) ([]firewall.PortWhitelist, error) {
|
||||
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
|
||||
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) (result error) {
|
||||
firewallWhitelistMu.Lock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer func() {
|
||||
firewallRuleMutationMu.Unlock()
|
||||
firewallWhitelistMu.Unlock()
|
||||
if result == nil {
|
||||
result = newFirewallService().SyncPortWhitelist(ctx)
|
||||
}
|
||||
previous, err := firewall.ParsePortWhitelist(setting.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
removed = excludeFirewallPorts(excludeFirewallPorts(previous, ports), required)
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
return tx.Where("key = ?", constant.FirewallPortWhiteList).
|
||||
Assign(map[string]interface{}{"value": string(value)}).
|
||||
FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
|
||||
})
|
||||
return removed, err
|
||||
}()
|
||||
current, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index, err := findPortWhitelistRule(current, request.OldRule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current[index] = request.Rule
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) portWhitelistReadiness(provider filter.Provider, providerErr error, firewallService *FirewallService) func(dto.FirewallSystemPort) (bool, error) {
|
||||
type state struct {
|
||||
ready bool
|
||||
err error
|
||||
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error {
|
||||
firewallWhitelistMu.Lock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer func() {
|
||||
firewallRuleMutationMu.Unlock()
|
||||
firewallWhitelistMu.Unlock()
|
||||
}()
|
||||
current, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
if request.Rule == nil {
|
||||
return filter.ErrInvalidRule
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
report("failed", label, err)
|
||||
failures = append(failures, fmt.Errorf("%s: %w", label, err))
|
||||
continue
|
||||
}
|
||||
report("applied", label, nil)
|
||||
index, err := findPortWhitelistRule(current, *request.Rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.Join(failures...)
|
||||
}
|
||||
|
||||
func whitelistPortLabel(port dto.FirewallSystemPort) string {
|
||||
return fmt.Sprintf("%s %s/%s", port.Family, port.Port, port.Protocol)
|
||||
current = slices.Delete(current, index, index+1)
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {
|
||||
result := dto.FirewallSettings{PingStatus: ping.LoadStatus()}
|
||||
if ports, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList); err == nil {
|
||||
result.PortWhitelist = ports
|
||||
} else {
|
||||
result.PortWhitelist = constant.FirewallPortWhiteListValue
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return dto.FirewallSettings{}, err
|
||||
}
|
||||
result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus(), IPv6Enabled: slices.Contains(families, constant.FirewallFamilyIPv6)}
|
||||
|
||||
installed := make(map[string]bool)
|
||||
for _, name := range lifecycle.InstalledProviders() {
|
||||
installed[name] = true
|
||||
}
|
||||
result.System.Selected = configuredSystemFirewallBackend()
|
||||
systemBackend, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
||||
result.System.Selected = strings.TrimSpace(systemBackend)
|
||||
if result.System.Selected == "" {
|
||||
if client, err := lifecycle.NewClient(); err == nil {
|
||||
if client, err := lifecycle.NewClient(""); err == nil {
|
||||
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}
|
||||
if option.Installed && name == result.System.Selected {
|
||||
client, err := lifecycle.NewClientFor(name)
|
||||
client, err := lifecycle.NewClient(name)
|
||||
if err != nil {
|
||||
option.Message = err.Error()
|
||||
} else if supportsManagedFilterChains(name) {
|
||||
option.Initialized, option.Bound, err = loadFirewallInitStatus(name, "base")
|
||||
} else if name == constant.FirewallProviderIptables || name == constant.FirewallProviderNftables {
|
||||
overview, err := loadSystemFirewallOverview(name, "base", families)
|
||||
if err != nil {
|
||||
option.Message = err.Error()
|
||||
}
|
||||
option.IPv4 = loadSystemFirewallFamilyInfo(name, constant.FirewallFamilyIPv4)
|
||||
option.IPv6 = loadSystemFirewallFamilyInfo(name, constant.FirewallFamilyIPv6)
|
||||
option.Initialized, option.Bound = overview.IsInit, overview.IsBind
|
||||
option.IPv4, option.IPv6 = overview.IPv4, overview.IPv6
|
||||
} else if option.Active, err = client.Status(); err != nil {
|
||||
option.Message = err.Error()
|
||||
}
|
||||
@@ -273,32 +183,29 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
||||
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
|
||||
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
|
||||
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
|
||||
if option.Installed && name == result.Forwarding.Selected {
|
||||
manager, err := newForwardingManagerFor(name)
|
||||
manager, err := newForwardingAdapterFor(ctx, name)
|
||||
if err != nil {
|
||||
option.Message = err.Error()
|
||||
} else if status, err := manager.Status(); err != nil {
|
||||
option.Message = err.Error()
|
||||
} else {
|
||||
option.Initialized, option.Bound = status.IsInit, status.IsBind
|
||||
ipv4Init, ipv4Bound, ipv4Err := manager.FamilyStatus(constant.FirewallFamilyIPv4)
|
||||
ipv6Init, ipv6Bound, ipv6Err := manager.FamilyStatus(constant.FirewallFamilyIPv6)
|
||||
option.IPv4 = dto.FirewallBackendFamilyStatus{
|
||||
Available: ipv4Err == nil, Initialized: ipv4Init, Bound: ipv4Bound,
|
||||
status, statusErr := loadForwardingFirewallOverview(manager, families)
|
||||
option.IPv4, option.IPv6 = status.IPv4, status.IPv6
|
||||
if statusErr != nil {
|
||||
option.Message = statusErr.Error()
|
||||
} else {
|
||||
option.Initialized, option.Bound = status.IsInit, status.IsBind
|
||||
}
|
||||
option.IPv6 = dto.FirewallBackendFamilyStatus{
|
||||
Available: ipv6Err == nil, Initialized: ipv6Init, Bound: ipv6Bound,
|
||||
}
|
||||
if name == constant.FirewallProviderIptables {
|
||||
if commands, commandErr := lifecycle.ResolveIptablesCommands(); commandErr == nil {
|
||||
option.IPv6.Available = option.IPv6.Available && commands.IPv6Available()
|
||||
if !commands.IPv6Available() {
|
||||
option.IPv6.Reason = docker_guard.ReasonCommandMissing
|
||||
}
|
||||
if result.IPv6Enabled && name == constant.FirewallProviderIptables && !option.IPv6.Available {
|
||||
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil && !commands.IPv6Available() {
|
||||
option.IPv6.Reason = dockerfirewall.ReasonCommandMissing
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,7 +223,11 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
||||
if dockerInstalled {
|
||||
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
|
||||
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
|
||||
option := dto.FirewallBackendOption{
|
||||
@@ -329,53 +240,51 @@ func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings
|
||||
option.Active = false
|
||||
}
|
||||
if option.Active {
|
||||
guard := docker_guard.NewRuntime(name)
|
||||
ipv4, ipv6 := guard.Status(docker_guard.FamilyIPv4), guard.Status(docker_guard.FamilyIPv6)
|
||||
option.Initialized = ipv4.Initialized || ipv6.Initialized
|
||||
option.Bound = ipv4.Bound || ipv6.Bound
|
||||
option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound
|
||||
option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound
|
||||
option.IPv4.Available = ipv4.Reason != docker_guard.ReasonCommandMissing
|
||||
option.IPv6.Available = ipv6.Reason != docker_guard.ReasonCommandMissing
|
||||
option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason
|
||||
guard := newDockerFirewallRuntime(ctx, name)
|
||||
for _, family := range families {
|
||||
status := guard.Status(family)
|
||||
option.Initialized = option.Initialized || status.Initialized
|
||||
option.Bound = option.Bound || status.Bound
|
||||
info := dto.FirewallBackendFamilyStatus{
|
||||
Available: status.Reason != dockerfirewall.ReasonCommandMissing,
|
||||
Initialized: status.Initialized, Bound: status.Bound, Reason: status.Reason,
|
||||
}
|
||||
if family == constant.FirewallFamilyIPv4 {
|
||||
option.IPv4 = info
|
||||
} else {
|
||||
option.IPv6 = info
|
||||
}
|
||||
}
|
||||
}
|
||||
result.Docker.Options = append(result.Docker.Options, option)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func loadSystemFirewallFamilyStatus(provider, family string) (bool, bool, error) {
|
||||
switch provider {
|
||||
case constant.FirewallProviderIptables:
|
||||
return iptables_helper.LoadFamilyInitStatus(family, "base")
|
||||
case constant.FirewallProviderNftables:
|
||||
return nftables_helper.LoadFamilyInitStatus(filter.Family(family), "base")
|
||||
default:
|
||||
return false, false, fmt.Errorf("unsupported firewall provider %q", provider)
|
||||
result.PortWhitelist, err = loadPortWhitelistSetting()
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
|
||||
func loadSystemFirewallFamilyInfo(provider, family string) dto.FirewallBackendFamilyStatus {
|
||||
if provider == constant.FirewallProviderIptables && family == constant.FirewallFamilyIPv6 {
|
||||
commands, err := lifecycle.ResolveIptablesCommands()
|
||||
if err != nil || !commands.IPv6Available() {
|
||||
return dto.FirewallBackendFamilyStatus{Reason: docker_guard.ReasonCommandMissing}
|
||||
}
|
||||
}
|
||||
initialized, bound, err := loadSystemFirewallFamilyStatus(provider, family)
|
||||
return dto.FirewallBackendFamilyStatus{
|
||||
Available: err == nil,
|
||||
Initialized: initialized,
|
||||
Bound: bound,
|
||||
result.PanelPort = LoadPanelPort()
|
||||
sshPort, sshErr := loadSSHWhitelistPortFrom(sshPath)
|
||||
if sshErr != nil {
|
||||
global.LOG.Warnf("load SSH port for firewall settings: %v", sshErr)
|
||||
} else {
|
||||
result.SSHPort = sshPort
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||
if request.Operation == "cleanup" {
|
||||
_, err := newFirewallService().Reset(ctx, dto.FirewallRuleReset{Subsystem: request.Subsystem, Provider: filter.Provider(request.Backend)})
|
||||
return err
|
||||
}
|
||||
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer firewallLifecycleTaskMu.Unlock()
|
||||
if request.Subsystem != "system" && request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
|
||||
return fmt.Errorf("%s only supports iptables or nftables", request.Subsystem)
|
||||
}
|
||||
if request.Subsystem == "system" && !supportsManagedFilterChains(request.Backend) && request.Operation != "select" {
|
||||
if request.Subsystem == "system" && (request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables) && request.Operation != "select" {
|
||||
return fmt.Errorf("%s does not support initialization or cleanup", request.Backend)
|
||||
}
|
||||
switch request.Subsystem {
|
||||
@@ -385,14 +294,12 @@ func (s *FirewallSettingService) Operate(ctx context.Context, request dto.Firewa
|
||||
}
|
||||
if request.Operation == "initialize" {
|
||||
service := newFirewallService()
|
||||
if err := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend)); err != nil {
|
||||
return err
|
||||
}
|
||||
return service.syncConfiguredFirewallPorts(ctx)
|
||||
whitelistErr := service.SyncPortWhitelist(ctx)
|
||||
return whitelistErr
|
||||
}
|
||||
return nil
|
||||
case "forwarding":
|
||||
return s.operateForwarding(request)
|
||||
return s.operateForwarding(ctx, request)
|
||||
case "docker":
|
||||
return s.operateDocker(ctx, request)
|
||||
default:
|
||||
@@ -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 {
|
||||
guard := docker_guard.NewRuntime(request.Backend)
|
||||
if request.Operation == "cleanup" {
|
||||
if err := guard.Cleanup(); err != nil {
|
||||
func (s *FirewallSettingService) OperateFamily(request dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error) {
|
||||
if request.Family != constant.FirewallFamilyIPv4 && request.Family != constant.FirewallFamilyIPv6 {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope
|
||||
}
|
||||
if request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrUnsupportedScope
|
||||
}
|
||||
if request.Operation != "initialize" && request.Operation != "repair" && request.Operation != "bind" {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrRuleOperation
|
||||
}
|
||||
subsystem := ""
|
||||
switch request.Subsystem {
|
||||
case "system":
|
||||
subsystem = firewallTaskHost
|
||||
case "forwarding":
|
||||
subsystem = firewallTaskForwarding
|
||||
case "docker":
|
||||
subsystem = firewallTaskDocker
|
||||
default:
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope
|
||||
}
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
return queueFirewallRuleTask(subsystem, task.TaskExec, "", nil, func(t *task.Task) error {
|
||||
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
|
||||
}
|
||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||
if request.Operation == "select" {
|
||||
current := previous
|
||||
if current == "" {
|
||||
current = alternateDirectBackend(request.Backend)
|
||||
}
|
||||
initialized, err := dockerGuardBackendInitialized(current)
|
||||
defer firewallLifecycleTaskMu.Unlock()
|
||||
t.Logf("backend=%s family=%s operation=%s", request.Backend, request.Family, request.Operation)
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current != request.Backend && initialized {
|
||||
return firewallBackendCleanupRequired(current, request.Backend)
|
||||
if !slices.Contains(families, request.Family) {
|
||||
return fmt.Errorf("IPv6 firewall support is disabled")
|
||||
}
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "select" {
|
||||
if err := (&DockerService{}).UpdateFirewallBackend(request.Backend); err != nil {
|
||||
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
|
||||
return err
|
||||
initialize := request.Operation != "bind"
|
||||
switch request.Subsystem {
|
||||
case "system":
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
if err := newFirewallService().checkSelectedProvider(t.TaskCtx, filter.Provider(request.Backend)); err != nil {
|
||||
return err
|
||||
}
|
||||
ports, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
required, err := firewall.RequiredPortWhitelist(ports)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
firewallRuleMutationMu.Lock()
|
||||
if request.Backend == constant.FirewallProviderIptables {
|
||||
err = iptables_helper.OperateFamily(request.Family, initialize, required)
|
||||
} else {
|
||||
err = nftables_helper.OperateFamily(filter.Family(request.Family), initialize, required)
|
||||
}
|
||||
firewallRuleMutationMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if initialize {
|
||||
if err := newFirewallService().applyPortWhitelist(t.TaskCtx, ports, nil, filter.Family(request.Family)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return settingRepo.UpdateOrCreate("IptablesStatus", constant.StatusEnable)
|
||||
case "forwarding":
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
manager, err := newForwardingAdapter(t.TaskCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if manager.Name() != request.Backend {
|
||||
return filter.ErrProviderUnavailable
|
||||
}
|
||||
if err := manager.OperateFamily(request.Family, initialize); err != nil {
|
||||
return err
|
||||
}
|
||||
rules, err := manager.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := persistForwardingRules(manager, rules); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
|
||||
default:
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
runtime, backend, err := newDockerPortGuardService().runtimeForDocker(t.TaskCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if backend != request.Backend {
|
||||
return filter.ErrProviderUnavailable
|
||||
}
|
||||
if err := runtime.OperateFamily(request.Family, initialize); err != nil {
|
||||
return err
|
||||
}
|
||||
inventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := persistDockerRules(backend, inventory); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
|
||||
}
|
||||
}
|
||||
if request.Operation == "initialize" {
|
||||
if err := newDockerPortGuardService().Operate(ctx, dto.DockerPortGuardOperation{Operation: "initialize"}); err != nil {
|
||||
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func dockerGuardBackendInitialized(backend string) (bool, error) {
|
||||
guard := docker_guard.NewRuntime(backend)
|
||||
for _, family := range []string{docker_guard.FamilyIPv4, docker_guard.FamilyIPv6} {
|
||||
initialized, err := guard.Initialized(family)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if initialized {
|
||||
return true, nil
|
||||
}
|
||||
func (s *FirewallSettingService) OperateIPv6(request dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error) {
|
||||
if request.Status != constant.StatusEnable && request.Status != constant.StatusDisable {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule
|
||||
}
|
||||
return false, nil
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
return queueFirewallRuleTask(firewallTaskHost, task.TaskExec, "", nil, func(t *task.Task) error {
|
||||
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer firewallLifecycleTaskMu.Unlock()
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
if request.Status == constant.StatusEnable {
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallIPv6SupportKey, request.Status)
|
||||
}
|
||||
ports, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(ipv4PortWhitelist(ports))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
installed := lifecycle.InstalledProviders()
|
||||
for _, selection := range []struct{ subsystem, key string }{
|
||||
{"system", constant.FirewallSystemBackendKey},
|
||||
{"forwarding", constant.FirewallForwardingBackendKey},
|
||||
{"docker", constant.FirewallDockerBackendKey},
|
||||
} {
|
||||
backend, err := settingRepo.GetValueByKey(selection.key)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
backend = strings.ToLower(strings.TrimSpace(backend))
|
||||
if backend == "" {
|
||||
if selection.subsystem == "system" {
|
||||
if len(installed) == 0 {
|
||||
continue
|
||||
}
|
||||
client, err := NewSelectedSystemFirewallClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backend = client.Name()
|
||||
} else {
|
||||
backend = constant.FirewallProviderIptables
|
||||
}
|
||||
}
|
||||
if !slices.Contains(installed, backend) || (backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables) {
|
||||
continue
|
||||
}
|
||||
t.Logf("disable IPv6 firewall bindings: subsystem=%s backend=%s", selection.subsystem, backend)
|
||||
switch selection.subsystem {
|
||||
case "system":
|
||||
if backend == constant.FirewallProviderIptables {
|
||||
err = iptables_helper.UnbindIPv6BaseChains()
|
||||
} else {
|
||||
err = nftables_helper.SetTableDormant(t.TaskCtx, "ip6", nftables_helper.TableName)
|
||||
if err == nil {
|
||||
err = nftables_helper.PersistRuleset(t.TaskCtx)
|
||||
}
|
||||
}
|
||||
case "forwarding":
|
||||
var manager forwarding.Adapter
|
||||
manager, err = newForwardingAdapterFor(t.TaskCtx, backend)
|
||||
if err == nil {
|
||||
err = manager.UnbindFamily(constant.FirewallFamilyIPv6)
|
||||
}
|
||||
case "docker":
|
||||
err = newDockerFirewallRuntime(t.TaskCtx, backend).Unbind(constant.FirewallFamilyIPv6)
|
||||
}
|
||||
if err != nil && !errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := t.TaskCtx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateValues(map[string]string{constant.FirewallIPv6SupportKey: request.Status, constant.FirewallPortWhiteList: string(value)})
|
||||
})
|
||||
}
|
||||
|
||||
func NewIFirewallSettingService() IFirewallSettingService {
|
||||
return &FirewallSettingService{}
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperation) error {
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
if _, err := lifecycle.NewClientFor(request.Backend); err != nil {
|
||||
if _, err := lifecycle.NewClient(request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "cleanup" {
|
||||
return cleanupSystemBackend(request.Backend)
|
||||
}
|
||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
||||
if previous == "" {
|
||||
if client, err := lifecycle.NewClient(); err == nil {
|
||||
if client, err := lifecycle.NewClient(""); err == nil {
|
||||
previous = client.Name()
|
||||
}
|
||||
}
|
||||
@@ -475,7 +534,7 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
@@ -501,21 +560,14 @@ func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperat
|
||||
}
|
||||
|
||||
func systemFirewallBackendInitialized(backend string) (bool, error) {
|
||||
return systemFirewallBackendInitializedWithClientFactory(backend, lifecycle.NewClientFor)
|
||||
}
|
||||
|
||||
func systemFirewallBackendInitializedWithClientFactory(
|
||||
backend string,
|
||||
newClient func(string) (lifecycle.Client, error),
|
||||
) (bool, error) {
|
||||
client, err := newClient(backend)
|
||||
client, err := lifecycle.NewClient(backend)
|
||||
if err != nil {
|
||||
if errors.Is(err, lifecycle.ErrNotInstalled) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if supportsManagedFilterChains(backend) {
|
||||
if backend == constant.FirewallProviderIptables || backend == constant.FirewallProviderNftables {
|
||||
for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} {
|
||||
initialized, _, err := loadSystemFirewallFamilyStatus(backend, family)
|
||||
if family == constant.FirewallFamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||
@@ -533,73 +585,39 @@ func systemFirewallBackendInitializedWithClientFactory(
|
||||
return client.Status()
|
||||
}
|
||||
|
||||
func cleanupSystemBackend(backend string) error {
|
||||
switch backend {
|
||||
case constant.FirewallProviderIptables:
|
||||
return newIptablesHelperManager().Cleanup()
|
||||
case constant.FirewallProviderNftables:
|
||||
return newNftablesHelperManager().Cleanup()
|
||||
default:
|
||||
return fmt.Errorf("cleanup is only available for 1Panel-owned iptables and nftables resources")
|
||||
}
|
||||
}
|
||||
|
||||
func cleanupInactiveSystemBackend(backend string) error {
|
||||
switch backend {
|
||||
case constant.FirewallProviderIptables:
|
||||
return (&iptables_helper.Manager{}).Cleanup()
|
||||
case constant.FirewallProviderNftables:
|
||||
return (&nftables_helper.Manager{}).Cleanup()
|
||||
default:
|
||||
return fmt.Errorf("cleanup is only available for 1Panel-owned iptables and nftables resources")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOperation) error {
|
||||
manager, err := newForwardingManagerFor(request.Backend)
|
||||
if err != nil {
|
||||
func (s *FirewallSettingService) operateForwarding(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||
if _, err := newForwardingAdapterFor(ctx, request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "cleanup" {
|
||||
if err := manager.Cleanup(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusDisable); err != nil {
|
||||
return err
|
||||
}
|
||||
recordForwardingSyncError(nil)
|
||||
return nil
|
||||
}
|
||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||
if request.Operation == "select" {
|
||||
current := previous
|
||||
if current == "" {
|
||||
detected, err := newForwardingManager()
|
||||
detected, err := newForwardingAdapter(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current = detected.Name()
|
||||
}
|
||||
initialized, err := forwardingBackendInitialized(current)
|
||||
initialized, err := forwardingBackendInitialized(ctx, current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current != request.Backend && initialized {
|
||||
return 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 {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "initialize" {
|
||||
return newForwardingService().Enable()
|
||||
return newForwardingService().Enable(ctx)
|
||||
}
|
||||
recordForwardingSyncError(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func forwardingBackendInitialized(backend string) (bool, error) {
|
||||
manager, err := newForwardingManagerFor(backend)
|
||||
func forwardingBackendInitialized(ctx context.Context, backend string) (bool, error) {
|
||||
manager, err := newForwardingAdapterFor(ctx, backend)
|
||||
if err != nil {
|
||||
if errors.Is(err, lifecycle.ErrNotInstalled) {
|
||||
return false, nil
|
||||
@@ -618,9 +636,53 @@ func forwardingBackendInitialized(backend string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func alternateDirectBackend(backend string) string {
|
||||
if backend == constant.FirewallProviderNftables {
|
||||
return constant.FirewallProviderIptables
|
||||
func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||
|
||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||
if request.Operation == "select" {
|
||||
current := previous
|
||||
if current == "" {
|
||||
current = constant.FirewallProviderNftables
|
||||
if request.Backend == constant.FirewallProviderNftables {
|
||||
current = constant.FirewallProviderIptables
|
||||
}
|
||||
}
|
||||
initialized, err := dockerGuardBackendInitialized(ctx, current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current != request.Backend && initialized {
|
||||
return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": current, "target": request.Backend}, nil)
|
||||
}
|
||||
}
|
||||
return constant.FirewallProviderNftables
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "select" {
|
||||
if err := (&DockerService{}).UpdateFirewallBackend(request.Backend); err != nil {
|
||||
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if request.Operation == "initialize" {
|
||||
if err := newDockerPortGuardService().Operate(ctx, dto.DockerPortGuardOperation{Operation: "initialize"}); err != nil {
|
||||
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dockerGuardBackendInitialized(ctx context.Context, backend string) (bool, error) {
|
||||
guard := newDockerFirewallRuntime(ctx, backend)
|
||||
for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} {
|
||||
initialized, err := guard.Initialized(family)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if initialized {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user