Compare commits

..
Author SHA1 Message Date
wanghe-fit2cloud 3a888d289f fix app image pull check 2026-06-12 14:10:20 +08:00
421 changed files with 67316 additions and 48866 deletions
+25
View File
@@ -0,0 +1,25 @@
name: LLM Code Review
permissions:
contents: read
pull-requests: write
on:
pull_request:
types: [opened, reopened, synchronize]
jobs:
llm-code-review:
runs-on: ubuntu-latest
steps:
- uses: fit2cloud/LLM-CodeReview-Action@main
env:
GITHUB_TOKEN: ${{ secrets.FIT2CLOUDRD_LLM_CODE_REVIEW_TOKEN }}
OPENAI_API_KEY: ${{ secrets.ALIYUN_LLM_API_KEY }}
LANGUAGE: English
OPENAI_API_ENDPOINT: https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL: qwen2.5-coder-3b-instruct
PROMPT: "Please check the following code differences for any irregularities, potential issues, or optimization suggestions, and provide your answers in English."
top_p: 1
temperature: 1
# max_tokens: 10000
MAX_PATCH_LENGTH: 10000
IGNORE_PATTERNS: "/node_modules,*.md,/dist,/.github"
FILE_PATTERNS: "*.java,*.go,*.py,*.vue,*.ts,*.js,*.css,*.scss,*.html"
+21
View File
@@ -0,0 +1,21 @@
name: SonarCloud Scan
on:
push:
branches:
- dev
pull_request:
types: [opened, synchronize, reopened]
jobs:
sonarcloud:
name: SonarCloud
if: github.repository == '1Panel-dev/1Panel'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUBTOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Mirror the Github organization repos to Gitee.
uses: Yikun/hub-mirror-action@ba51c01b28a6c9f95a25d4f1bcf6af2a147c0e18 # master
uses: Yikun/hub-mirror-action@master
with:
src: 'github/1Panel-dev'
dst: 'gitee/fit2cloud-feizhiyun'
+11
View File
@@ -0,0 +1,11 @@
name: Typos Check
on: pull_request
jobs:
run:
name: Spell Check with Typos
runs-on: ubuntu-latest
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v2
- name: Check spelling
uses: crate-ci/typos@master
+1 -3
View File
@@ -25,7 +25,7 @@
<a href="/docs/README.pt-br.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-d9d9d9"></a>
<a href="/docs/README.ar.md"><img alt="العربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
<a href="/docs/README.de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-d9d9d9"></a>
<a href="/docs/README.es-es.md"><img alt="Español" src="https://img.shields.io/badge/Español-d9d9d9"></a>
<a href="/docs/README.es.md"><img alt="Español" src="https://img.shields.io/badge/Español-d9d9d9"></a>
<a href="/docs/README.fr.md"><img alt="français" src="https://img.shields.io/badge/français-d9d9d9"></a>
<a href="/docs/README.ko.md"><img alt="한국어" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
<a href="/docs/README.id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-d9d9d9"></a>
@@ -33,8 +33,6 @@
<a href="/docs/README.tr.md"><img alt="Türkçe" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
<a href="/docs/README.ru.md"><img alt="Русский" src="https://img.shields.io/badge/Русский-d9d9d9"></a>
<a href="/docs/README.ms.md"><img alt="Bahasa Melayu" src="https://img.shields.io/badge/Bahasa Melayu-d9d9d9"></a>
<a href="/docs/README.fa.md"><img alt="Persian" src="https://img.shields.io/badge/%D9%81%D8%A7%D8%B1%D8%B3%DB%8C-d9d9d9"></a>
<a href="/docs/README.lo.md"><img alt="ພາສາລາວ" src="https://img.shields.io/badge/%E0%BA%9E%E0%BA%B2%E0%BA%AA%E0%BA%B2%E0%BA%A5%E0%BA%B2%E0%BA%A7-d9d9d9"></a>
</p>
---
-21
View File
@@ -500,27 +500,6 @@ func (b *BaseApi) GetAgentAccountModels(c *gin.Context) {
helper.SuccessWithData(c, list)
}
// @Tags AI
// @Summary Discover custom provider models
// @Accept json
// @Param request body dto.AgentAccountModelDiscoverReq true "request"
// @Success 200 {array} dto.AgentAccountModel
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/accounts/models/discover [post]
func (b *BaseApi) DiscoverAgentAccountModels(c *gin.Context) {
var req dto.AgentAccountModelDiscoverReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
list, err := agentService.DiscoverAccountModels(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, list)
}
// @Tags AI
// @Summary Create model account model
// @Accept json
+1 -6
View File
@@ -413,15 +413,10 @@ func (b *BaseApi) Backup(c *gin.Context) {
switch req.Type {
case "app":
record, err := backupService.AppBackup(req)
if err != nil {
if _, err := backupService.AppBackup(req); err != nil {
helper.InternalServer(c, err)
return
}
if req.IsImmediate {
helper.SuccessWithData(c, record)
return
}
case "mysql", "mariadb", constant.AppMysqlCluster:
if err := backupService.MysqlBackup(req); err != nil {
helper.InternalServer(c, err)
+12 -215
View File
@@ -41,40 +41,20 @@ func (b *BaseApi) CreateMysql(c *gin.Context) {
}
// @Tags Database Mysql
// @Summary List mysql users
// @Summary Bind user of mysql database
// @Accept json
// @Param request body dto.MysqlUserSearch true "request"
// @Success 200 {array} dto.MysqlUser
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/search [post]
func (b *BaseApi) ListMysqlUsers(c *gin.Context) {
var req dto.MysqlUserSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := mysqlService.ListUsers(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Database Mysql
// @Summary Create mysql user
// @Accept json
// @Param request body dto.MysqlUserCreate true "request"
// @Param request body dto.BindUser true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users [post]
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 mysql 数据库 [database] 用户 [username]@[host]","formatEN":"create mysql database [database] user [username]@[host]"}
func (b *BaseApi) CreateMysqlUser(c *gin.Context) {
var req dto.MysqlUserCreate
// @Router /databases/bind [post]
// @x-panel-log {"bodyKeys":["database", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"绑定 mysql 数据库名 [database] [username]","formatEN":"bind mysql database [database] [username]"}
func (b *BaseApi) BindUser(c *gin.Context) {
var req dto.BindUser
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
@@ -83,191 +63,8 @@ func (b *BaseApi) CreateMysqlUser(c *gin.Context) {
}
req.Password = string(password)
}
if err := mysqlService.CreateUser(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Delete mysql user
// @Accept json
// @Param request body dto.MysqlUserDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/del [post]
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 mysql 数据库 [database] 用户 [username]@[host]","formatEN":"delete mysql database [database] user [username]@[host]"}
func (b *BaseApi) DeleteMysqlUser(c *gin.Context) {
var req dto.MysqlUserDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := mysqlService.DeleteUser(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Update mysql user
// @Accept json
// @Param request body dto.MysqlUserUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/update [post]
// @x-panel-log {"bodyKeys":["database","username","host","newHost","description"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]","formatEN":"update mysql database [database] user [username] access [host] => [newHost] description [description]"}
func (b *BaseApi) UpdateMysqlUser(c *gin.Context) {
var req dto.MysqlUserUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := mysqlService.UpdateUser(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Change mysql user password
// @Accept json
// @Param request body dto.MysqlUserPassword true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/password [post]
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mysql 数据库 [database] 用户 [username]@[host] 密码","formatEN":"update mysql database [database] user [username]@[host] password"}
func (b *BaseApi) ChangeMysqlUserPassword(c *gin.Context) {
var req dto.MysqlUserPassword
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
helper.BadRequest(c, err)
return
}
req.Password = string(password)
}
if err := mysqlService.ChangeUserPassword(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Save mysql user password locally
// @Accept json
// @Param request body dto.MysqlUserPassword true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/password/save [post]
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"补充 mysql 数据库 [database] 用户 [username]@[host] 密码","formatEN":"save mysql database [database] user [username]@[host] password locally"}
func (b *BaseApi) SaveMysqlUserPassword(c *gin.Context) {
var req dto.MysqlUserPassword
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
helper.BadRequest(c, err)
return
}
req.Password = string(password)
}
if err := mysqlService.SaveUserPassword(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary List mysql grants
// @Accept json
// @Param request body dto.MysqlUserSearch true "request"
// @Success 200 {array} dto.MysqlGrant
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/grants/search [post]
func (b *BaseApi) ListMysqlGrants(c *gin.Context) {
var req dto.MysqlUserSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := mysqlService.ListGrants(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Database Mysql
// @Summary List mysql grant summary
// @Accept json
// @Param request body dto.MysqlGrantSummarySearch true "request"
// @Success 200 {object} map[string][]dto.MysqlUser
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/grants/summary [post]
func (b *BaseApi) ListMysqlGrantSummary(c *gin.Context) {
var req dto.MysqlGrantSummarySearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := mysqlService.ListGrantSummary(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Database Mysql
// @Summary Grant mysql user
// @Accept json
// @Param request body dto.MysqlGrantCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/grants [post]
// @x-panel-log {"bodyKeys":["database","db","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"授权 mysql 数据库 [database] 用户 [username]@[host] 访问 [db]","formatEN":"grant mysql database [database] user [username]@[host] access to [db]"}
func (b *BaseApi) GrantMysqlUser(c *gin.Context) {
var req dto.MysqlGrantCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := mysqlService.GrantUser(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Revoke mysql grant
// @Accept json
// @Param request body dto.MysqlGrantDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/grants/del [post]
// @x-panel-log {"bodyKeys":["database","db","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"取消 mysql 数据库 [database] 用户 [username]@[host] 对 [db] 的授权","formatEN":"revoke mysql database [database] user [username]@[host] access to [db]"}
func (b *BaseApi) RevokeMysqlGrant(c *gin.Context) {
var req dto.MysqlGrantDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := mysqlService.RevokeGrant(req); err != nil {
if err := mysqlService.BindUser(req); err != nil {
helper.InternalServer(c, err)
return
}
@@ -297,14 +94,14 @@ func (b *BaseApi) UpdateMysqlDescription(c *gin.Context) {
}
// @Tags Database Mysql
// @Summary Change mysql root password
// @Summary Change mysql password
// @Accept json
// @Param request body dto.ChangeDBInfo true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/change/password [post]
// @x-panel-log {"bodyKeys":["database"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新数据库 [database] root 密码","formatEN":"Update database [database] root password"}
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"更新数据库 [name] 密码","formatEN":"Update database [name] password"}
func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) {
var req dto.ChangeDBInfo
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -328,14 +125,14 @@ func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) {
}
// @Tags Database Mysql
// @Summary Change mysql root access
// @Summary Change mysql access
// @Accept json
// @Param request body dto.ChangeDBInfo true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/change/access [post]
// @x-panel-log {"bodyKeys":["database"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新数据库 [database] root 访问权限","formatEN":"Update database [database] root access"}
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"更新数据库 [name] 访问权限","formatEN":"Update database [name] access"}
func (b *BaseApi) ChangeMysqlAccess(c *gin.Context) {
var req dto.ChangeDBInfo
if err := helper.CheckBindAndValidate(&req, c); err != nil {
+8 -9
View File
@@ -37,15 +37,14 @@ var (
cronjobService = service.NewICronjobService()
fileService = service.NewIFileService()
fileHistoryService = service.NewIFileHistoryService()
fileShareService = service.NewIFileShareService()
sshService = service.NewISSHService()
firewallService = service.NewIFirewallService()
iptablesService = service.NewIIptablesService()
monitorService = service.NewIMonitorService()
systemService = service.NewISystemService()
runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService()
fileService = service.NewIFileService()
fileHistoryService = service.NewIFileHistoryService()
fileShareService = service.NewIFileShareService()
sshService = service.NewISSHService()
firewallService = service.NewIFirewallService()
iptablesService = service.NewIIptablesService()
monitorService = service.NewIMonitorService()
systemService = service.NewISystemService()
deviceService = service.NewIDeviceService()
fail2banService = service.NewIFail2BanService()
+2 -69
View File
@@ -12,9 +12,7 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -30,11 +28,6 @@ import (
qrcode "github.com/skip2/go-qrcode"
)
var cancelledChunkUploads = struct {
sync.RWMutex
ids map[string]struct{}
}{ids: make(map[string]struct{})}
// @Tags File
// @Summary List files
// @Accept json
@@ -880,24 +873,7 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
}
}
filename := c.PostForm("filename")
if filename == "" || filepath.Base(filename) != filename || strings.ContainsAny(filename, `/\\`) {
helper.BadRequest(c, errors.New("invalid filename"))
return
}
uploadID := strings.TrimSpace(c.PostForm("uploadID"))
cancellable := uploadID != ""
if cancellable && (filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`)) {
helper.BadRequest(c, errors.New("invalid upload ID"))
return
}
if !cancellable {
uploadID = filename
}
fileDir := filepath.Join(tmpDir, uploadID)
if cancellable && chunkUploadCancelled(uploadID) {
helper.BadRequest(c, errors.New("upload cancelled"))
return
}
fileDir := filepath.Join(tmpDir, filename)
if chunkIndex == 0 {
if fileOp.Stat(fileDir) {
_ = fileOp.DeleteDir(fileDir)
@@ -908,7 +884,7 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
defer func() {
if err != nil {
_ = os.RemoveAll(fileDir)
_ = os.Remove(fileDir)
}
}()
var (
@@ -928,11 +904,6 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
return
}
if cancellable && chunkUploadCancelled(uploadID) {
err = errors.New("upload cancelled")
helper.BadRequest(c, err)
return
}
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
err = os.WriteFile(chunkPath, chunkData, constant.DirPerm)
@@ -951,50 +922,12 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
return
}
if cancellable {
cancelledChunkUploads.Lock()
delete(cancelledChunkUploads.ids, uploadID)
cancelledChunkUploads.Unlock()
}
helper.SuccessWithData(c, true)
} else {
return
}
}
// StopChunkUpload removes temporary chunks left by a cancelled upload.
func (b *BaseApi) StopChunkUpload(c *gin.Context) {
var req request.FileProcessReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
uploadID := strings.TrimSpace(req.Key)
if uploadID == "" || filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`) {
helper.BadRequest(c, errors.New("invalid upload ID"))
return
}
cancelledChunkUploads.Lock()
cancelledChunkUploads.ids[uploadID] = struct{}{}
cancelledChunkUploads.Unlock()
time.AfterFunc(10*time.Minute, func() {
cancelledChunkUploads.Lock()
delete(cancelledChunkUploads.ids, uploadID)
cancelledChunkUploads.Unlock()
})
if err := os.RemoveAll(filepath.Join(global.Dir.TmpDir, "upload", uploadID)); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
func chunkUploadCancelled(uploadID string) bool {
cancelledChunkUploads.RLock()
_, ok := cancelledChunkUploads.ids[uploadID]
cancelledChunkUploads.RUnlock()
return ok
}
var wsUpgrade = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
-55
View File
@@ -2,7 +2,6 @@ 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"
)
@@ -21,57 +20,3 @@ func (b *BaseApi) GetSystemFiles(c *gin.Context) {
helper.SuccessWithData(c, data)
}
// @Tags Logs
// @Summary Get host system log status
// @Produce json
// @Success 200 {object} dto.SystemLogStatus
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /logs/system/status [get]
func (b *BaseApi) GetSystemLogStatus(c *gin.Context) {
data, err := logService.GetSystemLogStatus()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Logs
// @Summary Read host logs
// @Accept json
// @Param request body dto.SystemLogReq true "request"
// @Produce json
// @Success 200 {object} dto.SystemLogRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /logs/system/read [post]
func (b *BaseApi) ReadSystemLog(c *gin.Context) {
var req dto.SystemLogReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := logService.ReadSystemLog(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Logs
// @Summary List running host services
// @Produce json
// @Success 200 {array} string
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /logs/system/services [get]
func (b *BaseApi) ListRunningServices(c *gin.Context) {
data, err := logService.ListRunningServices()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
-63
View File
@@ -23,27 +23,6 @@ func (b *BaseApi) PageMcpServers(c *gin.Context) {
helper.SuccessWithData(c, list)
}
// @Tags McpServer
// @Summary Load mcp server detail
// @Accept json
// @Param request body request.McpServerDetail true "request"
// @Success 200 {object} response.McpServerDTO
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/mcp/server/detail [post]
func (b *BaseApi) LoadMcpServerDetail(c *gin.Context) {
var req request.McpServerDetail
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := mcpServerService.Detail(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags McpServer
// @Summary Create mcp server
// @Accept json
@@ -128,48 +107,6 @@ func (b *BaseApi) OperateMcpServer(c *gin.Context) {
helper.Success(c)
}
// @Tags McpServer
// @Summary Sync mcp server status
// @Accept json
// @Param request body request.McpServerStatusSync true "request"
// @Success 200 {array} response.McpServerStatusDTO
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/mcp/server/status/sync [post]
func (b *BaseApi) SyncMcpServerStatus(c *gin.Context) {
var req request.McpServerStatusSync
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := mcpServerService.SyncStatus(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags McpServer
// @Summary Test mcp server connection
// @Accept json
// @Param request body request.McpServerConnectionTest true "request"
// @Success 200 {object} response.McpServerConnectionTestRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/mcp/server/connection/test [post]
func (b *BaseApi) TestMcpServerConnection(c *gin.Context) {
var req request.McpServerConnectionTest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := mcpServerService.TestConnection(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags McpServer
// @Summary Bind Domain for mcp server
// @Accept json
-63
View File
@@ -1,63 +0,0 @@
package v2
import (
"os"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)
// @Tags RuntimeDiagnostics
// @Summary Load runtime diagnostics summary
// @Success 200 {object} dto.RuntimeDiagnosticsSummary
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/diagnostics/summary [get]
func (b *BaseApi) LoadRuntimeDiagnosticsSummary(c *gin.Context) {
data, err := runtimeDiagnosticsService.Summary()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags RuntimeDiagnostics
// @Summary Load grouped goroutine snapshot
// @Success 200 {object} dto.RuntimeGoroutineSnapshot
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/diagnostics/goroutines [get]
func (b *BaseApi) LoadRuntimeGoroutines(c *gin.Context) {
data, err := runtimeDiagnosticsService.Goroutines()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithDataGzipped(c, data)
}
// @Tags RuntimeDiagnostics
// @Summary Capture runtime profile
// @Param request body dto.RuntimeProfileCreate true "request"
// @Success 200 {file} file
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/diagnostics/profiles [post]
func (b *BaseApi) CreateRuntimeProfile(c *gin.Context) {
var req dto.RuntimeProfileCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
profile, err := runtimeDiagnosticsService.CreateProfile(req)
if err != nil {
helper.BadRequest(c, err)
return
}
defer os.Remove(profile.Path)
c.Header("Content-Disposition", `attachment; filename="`+profile.Name+`"`)
c.Header("Content-Type", "application/octet-stream")
c.File(profile.Path)
c.Abort()
}
-15
View File
@@ -224,21 +224,6 @@ func (b *BaseApi) ExportSSHLogs(c *gin.Context) {
helper.SuccessWithData(c, tmpFile)
}
// @Tags SSH
// @Summary Clean host SSH logs
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/ssh/log/clean [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 SSH 登录日志","formatEN":"clean SSH login logs"}
func (b *BaseApi) CleanSSHLogs(c *gin.Context) {
if err := sshService.CleanLog(); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags SSH
// @Summary Load host SSH conf
// @Accept json
-25
View File
@@ -210,27 +210,6 @@ func (b *BaseApi) UpdateWebsiteSSL(c *gin.Context) {
helper.Success(c)
}
// @Tags Website SSL
// @Summary Push ssl to nodes
// @Accept json
// @Param request body request.WebsiteSSLPush true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/ssl/push [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"推送证书到节点 [domain]","formatEN":"Push ssl to nodes [domain]"}
func (b *BaseApi) PushWebsiteSSLToNode(c *gin.Context) {
var req request.WebsiteSSLPush
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteSSLService.PushToNode(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website SSL
// @Summary Upload ssl
// @Accept json
@@ -269,8 +248,6 @@ func (b *BaseApi) UploadSSLFile(c *gin.Context) {
var req request.WebsiteSSLFileUpload
req.Description = c.PostForm("description")
req.Nodes = c.PostForm("nodes")
req.PushNode, _ = strconv.ParseBool(c.PostForm("pushNode"))
sslID := c.PostForm("sslID")
if sslID != "" {
req.SSLID, _ = strconv.ParseUint(sslID, 10, 64)
@@ -306,8 +283,6 @@ func (b *BaseApi) UploadSSLFile(c *gin.Context) {
Certificate: string(certificateContent),
Description: req.Description,
SSLID: uint(req.SSLID),
PushNode: req.PushNode,
Nodes: req.Nodes,
}
if err := websiteSSLService.Upload(uploadReq); err != nil {
+54 -75
View File
@@ -3,30 +3,28 @@ package dto
import "time"
type AgentCreateReq struct {
Name string `json:"name" validate:"required"`
Remark string `json:"remark"`
AppVersion string `json:"appVersion" validate:"required"`
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"`
Model string `json:"model"`
AccountID uint `json:"accountId"`
Token string `json:"token"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
TaskID string `json:"taskID"`
Advanced bool `json:"advanced"`
ContainerName string `json:"containerName"`
AllowPort bool `json:"allowPort"`
SpecifyIP string `json:"specifyIP"`
RestartPolicy string `json:"restartPolicy"`
CpuQuota float64 `json:"cpuQuota"`
MemoryLimit float64 `json:"memoryLimit"`
MemoryUnit string `json:"memoryUnit"`
PullImage bool `json:"pullImage"`
EditCompose bool `json:"editCompose"`
DockerCompose string `json:"dockerCompose"`
Name string `json:"name" validate:"required"`
Remark string `json:"remark"`
AppVersion string `json:"appVersion" validate:"required"`
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"`
Model string `json:"model"`
AccountID uint `json:"accountId"`
Token string `json:"token"`
TaskID string `json:"taskID"`
Advanced bool `json:"advanced"`
ContainerName string `json:"containerName"`
AllowPort bool `json:"allowPort"`
SpecifyIP string `json:"specifyIP"`
RestartPolicy string `json:"restartPolicy"`
CpuQuota float64 `json:"cpuQuota"`
MemoryLimit float64 `json:"memoryLimit"`
MemoryUnit string `json:"memoryUnit"`
PullImage bool `json:"pullImage"`
EditCompose bool `json:"editCompose"`
DockerCompose string `json:"dockerCompose"`
}
type AgentBatchInstallReq struct {
@@ -40,8 +38,6 @@ type AgentBatchInstallReq struct {
Model string `json:"model"`
AccountID uint `json:"accountId"`
Token string `json:"token"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
TaskID string `json:"taskID"`
Advanced bool `json:"advanced"`
ContainerName string `json:"containerName"`
@@ -118,11 +114,11 @@ type AgentItem struct {
ProviderName string `json:"providerName"`
Model string `json:"model"`
APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
BaseURL string `json:"baseUrl"`
APIKey string `json:"apiKey"`
Token string `json:"token"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
Status string `json:"status"`
Message string `json:"message"`
AppInstallID uint `json:"appInstallId"`
@@ -290,22 +286,19 @@ type AgentOverviewSnapshot struct {
}
type AgentAccountModel struct {
RecordID uint `json:"recordId"`
ID string `json:"id"`
Name string `json:"name"`
RecordID uint `json:"recordId"`
ID string `json:"id"`
Name string `json:"name"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"`
Input []string `json:"input"`
}
type AgentAccountModelReq struct {
AccountID uint `json:"accountId" validate:"required"`
}
type AgentAccountModelDiscoverReq struct {
Provider string `json:"provider" validate:"required"`
BaseURL string `json:"baseURL" validate:"required"`
APIKey string `json:"apiKey" validate:"required"`
APIType string `json:"apiType" validate:"required"`
}
type AgentAccountModelCreateReq struct {
AccountID uint `json:"accountId" validate:"required"`
Model AgentAccountModel `json:"model" validate:"required"`
@@ -329,8 +322,6 @@ type AgentAccountCreateReq struct {
BaseURL string `json:"baseURL"`
Models []AgentAccountModel `json:"models"`
APIType string `json:"apiType" validate:"required"`
AuthMode string `json:"authMode"`
VerifyModel string `json:"verifyModel"`
Remark string `json:"remark"`
}
@@ -341,8 +332,6 @@ type AgentAccountUpdateReq struct {
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseURL"`
APIType string `json:"apiType" validate:"required"`
AuthMode string `json:"authMode"`
VerifyModel string `json:"verifyModel"`
Remark string `json:"remark"`
SyncAgents bool `json:"syncAgents"`
}
@@ -351,9 +340,6 @@ type AgentAccountVerifyReq struct {
Provider string `json:"provider" validate:"required"`
APIKey string `json:"apiKey" validate:"required"`
BaseURL string `json:"baseURL"`
APIType string `json:"apiType" validate:"required"`
AuthMode string `json:"authMode"`
Model string `json:"model"`
}
type AgentAccountDeleteReq struct {
@@ -381,34 +367,26 @@ type AgentAccountInfo struct {
BaseURL string `json:"baseUrl"`
Models []AgentAccountModel `json:"models"`
APIType string `json:"apiType"`
AuthMode string `json:"authMode"`
VerifyModel string `json:"verifyModel"`
Verified bool `json:"verified"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
}
type ProviderModelInfo struct {
ID string `json:"id"`
Name string `json:"name"`
}
type ProviderAPIInfo struct {
APIType string `json:"apiType"`
BaseURL string `json:"baseUrl"`
EditableBaseURL bool `json:"editableBaseUrl"`
DefaultAuthMode string `json:"defaultAuthMode"`
AuthModes []string `json:"authModes"`
ID string `json:"id"`
Name string `json:"name"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"`
Input []string `json:"input"`
}
type ProviderInfo struct {
Sort uint `json:"-"`
Provider string `json:"provider"`
DisplayName string `json:"displayName"`
BaseURL string `json:"baseUrl"`
DefaultAPIType string `json:"defaultApiType"`
APITypes []ProviderAPIInfo `json:"apiTypes"`
Models []ProviderModelInfo `json:"models"`
Sort uint `json:"-"`
Provider string `json:"provider"`
DisplayName string `json:"displayName"`
BaseURL string `json:"baseUrl"`
Models []ProviderModelInfo `json:"models"`
}
type AgentFeishuConfigReq struct {
@@ -429,6 +407,11 @@ type AgentFeishuConfigUpdateReq struct {
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"`
}
type AgentFeishuPairingApproveReq struct {
AgentID uint `json:"agentId" validate:"required"`
PairingCode string `json:"pairingCode" validate:"required"`
}
type AgentFeishuConfig struct {
Enabled bool `json:"enabled"`
ThreadSession bool `json:"threadSession"`
@@ -677,20 +660,16 @@ type AgentSecurityConfig struct {
}
type AgentOtherConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
UserTimezone string `json:"userTimezone" validate:"required"`
BrowserEnabled bool `json:"browserEnabled"`
NPMRegistry string `json:"npmRegistry" validate:"required"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
AgentID uint `json:"agentId" validate:"required"`
UserTimezone string `json:"userTimezone" validate:"required"`
BrowserEnabled bool `json:"browserEnabled"`
NPMRegistry string `json:"npmRegistry" validate:"required"`
}
type AgentOtherConfig struct {
UserTimezone string `json:"userTimezone"`
BrowserEnabled bool `json:"browserEnabled"`
NPMRegistry string `json:"npmRegistry"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
UserTimezone string `json:"userTimezone"`
BrowserEnabled bool `json:"browserEnabled"`
NPMRegistry string `json:"npmRegistry"`
}
type AgentConfigFileReq struct {
-3
View File
@@ -2,7 +2,6 @@ package dto
import (
"context"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
)
@@ -133,8 +132,6 @@ type Locale struct {
Ko string `json:"ko"`
Tr string `json:"tr"`
Es string `json:"es-es" yaml:"es-es"`
Fa string `json:"fa"`
Lo string `json:"lo"`
}
type AppForm struct {
+8 -9
View File
@@ -65,15 +65,14 @@ type UploadForRecover struct {
}
type CommonBackup struct {
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"`
Name string `json:"name"`
DetailName string `json:"detailName"`
Secret string `json:"secret"`
IsImmediate bool `json:"isImmediate"`
StopBefore bool `json:"stopBefore"`
TaskID string `json:"taskID"`
FileName string `json:"fileName"`
Args []string `json:"args"`
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"`
Name string `json:"name"`
DetailName string `json:"detailName"`
Secret string `json:"secret"`
StopBefore bool `json:"stopBefore"`
TaskID string `json:"taskID"`
FileName string `json:"fileName"`
Args []string `json:"args"`
Description string `json:"description"`
}
+5 -10
View File
@@ -134,15 +134,10 @@ type ExtraHost struct {
IP string `json:"ip"`
}
type ContainerNetwork struct {
Network string `json:"network"`
Ipv4 string `json:"ipv4"`
Ipv6 string `json:"ipv6"`
MacAddr string `json:"macAddr"`
Links []string `json:"links"`
Aliases []string `json:"aliases"`
DriverOpts map[string]string `json:"driverOpts"`
GwPriority int `json:"gwPriority"`
LinkLocalIPs []string `json:"linkLocalIPs"`
Network string `json:"network"`
Ipv4 string `json:"ipv4"`
Ipv6 string `json:"ipv6"`
MacAddr string `json:"macAddr"`
}
type ContainerCreateByCommand struct {
@@ -324,7 +319,7 @@ type ComposeCreate struct {
type ComposeOperation struct {
Name string `json:"name" validate:"required"`
Path string `json:"path"`
Operation string `json:"operation" validate:"required,oneof=up start restart stop down delete rebuild"`
Operation string `json:"operation" validate:"required,oneof=up start restart stop down delete"`
WithFile bool `json:"withFile"`
Force bool `json:"force"`
}
+2 -10
View File
@@ -79,9 +79,8 @@ type NodeCurrent struct {
}
type DashboardCurrent struct {
Uptime uint64 `json:"uptime"`
TimeSinceUptime string `json:"timeSinceUptime"`
RunningTime RunningTime `json:"runningTime"`
Uptime uint64 `json:"uptime"`
TimeSinceUptime string `json:"timeSinceUptime"`
Procs uint64 `json:"procs"`
@@ -129,13 +128,6 @@ type DashboardCurrent struct {
ShotTime time.Time `json:"shotTime"`
}
type RunningTime struct {
Days uint64 `json:"days"`
Hours uint64 `json:"hours"`
Minutes uint64 `json:"minutes"`
Seconds uint64 `json:"seconds"`
}
type AppLauncherSync struct {
Keys []string `json:"keys"`
}
+10 -69
View File
@@ -60,84 +60,25 @@ type MysqlDBCreate struct {
Database string `json:"database" validate:"required"`
Format string `json:"format" validate:"required"`
Collation string `json:"collation"`
Username string `json:"username"`
Password string `json:"password"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Permission string `json:"permission" validate:"required"`
Description string `json:"description"`
}
type MysqlUser struct {
Username string `json:"username"`
Host string `json:"host"`
Password string `json:"password"`
Description string `json:"description"`
IsDelete bool `json:"isDelete"`
}
type MysqlGrant struct {
Database string `json:"database"`
Username string `json:"username"`
Host string `json:"host"`
}
type MysqlGrantSummarySearch struct {
Database string `json:"database" validate:"required"`
DBs []string `json:"dbs" validate:"required"`
}
type MysqlUserSearch struct {
Database string `json:"database" validate:"required"`
}
type MysqlUserCreate struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Host string `json:"host" validate:"required"`
Description string `json:"description"`
DBs []string `json:"dbs"`
}
type MysqlUserDelete struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
}
type MysqlUserUpdate struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
NewHost string `json:"newHost" validate:"required"`
Description string `json:"description"`
}
type MysqlUserPassword struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
Password string `json:"password" validate:"required"`
}
type MysqlGrantCreate struct {
Database string `json:"database" validate:"required"`
DB string `json:"db" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
}
type MysqlGrantDelete struct {
Database string `json:"database" validate:"required"`
DB string `json:"db" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
}
type MysqlFormatCollationOption struct {
Format string `json:"format"`
Collations []string `json:"collations"`
}
type BindUser struct {
Database string `json:"database" validate:"required"`
DB string `json:"db" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Permission string `json:"permission" validate:"required"`
}
type MysqlLoadDB struct {
From string `json:"from" validate:"required,oneof=local remote"`
Type string `json:"type" validate:"required,oneof=mysql mariadb mysql-cluster"`
-36
View File
@@ -1,8 +1,6 @@
package dto
import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
)
@@ -16,37 +14,3 @@ type SearchTaskLogReq struct {
type TaskDTO struct {
model.Task
}
type SystemLogReq struct {
PageSize int `json:"pageSize" validate:"omitempty,min=1,max=500"`
Cursor string `json:"cursor"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
Keyword string `json:"keyword"`
Priority string `json:"priority"`
Service string `json:"service"`
}
type SystemLogRes struct {
Source string `json:"source"`
Items []SystemLogItem `json:"items"`
HasMore bool `json:"hasMore"`
NextCursor string `json:"nextCursor"`
}
type SystemLogStatus struct {
Source string `json:"source"`
Version string `json:"version"`
KeywordFilterSupported bool `json:"keywordFilterSupported"`
Message string `json:"message"`
}
type SystemLogItem struct {
Timestamp int64 `json:"-"`
Cursor string `json:"-"`
Time string `json:"time"`
Priority string `json:"priority"`
Service string `json:"service"`
Message string `json:"message"`
Raw string `json:"raw"`
}
+5 -39
View File
@@ -1,8 +1,6 @@
package dto
import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
)
@@ -89,41 +87,9 @@ var LBAlgorithms = map[string]struct{}{"ip_hash": {}, "least_conn": {}}
var RealIPKeys = map[string]struct{}{"X-Forwarded-For": {}, "X-Real-IP": {}, "CF-Connecting-IP": {}}
type NginxModule struct {
Name string `json:"name"`
Custom bool `json:"custom,omitempty"`
Script string `json:"script"`
Packages []string `json:"packages"`
Params string `json:"params"`
Enable bool `json:"enable"`
BuildMode string `json:"buildMode,omitempty"`
Provider string `json:"provider,omitempty"`
LoadOrder int `json:"loadOrder,omitempty"`
Builds []NginxModuleBuild `json:"builds,omitempty"`
LastError string `json:"lastError,omitempty"`
}
type NginxModuleBuild struct {
Provider string `json:"provider"`
BuildMode string `json:"buildMode"`
Status string `json:"status"`
Hash string `json:"hash"`
Target NginxModuleTarget `json:"target"`
Artifacts []NginxModuleArtifact `json:"artifacts,omitempty"`
Error string `json:"error,omitempty"`
BuiltAt time.Time `json:"builtAt,omitempty"`
}
type NginxModuleTarget struct {
Key string `json:"key"`
OpenRestyVersion string `json:"openrestyVersion"`
Architecture string `json:"architecture"`
Image string `json:"image,omitempty"`
ImageDigest string `json:"imageDigest,omitempty"`
BuilderDigest string `json:"builderDigest,omitempty"`
}
type NginxModuleArtifact struct {
Name string `json:"name"`
Path string `json:"path"`
Checksum string `json:"checksum"`
Name string `json:"name"`
Script string `json:"script"`
Packages []string `json:"packages"`
Params string `json:"params"`
Enable bool `json:"enable"`
}
-1
View File
@@ -99,7 +99,6 @@ type AppInstallUpgrade struct {
DetailID uint `json:"detailId"`
Backup bool `json:"backup"`
PullImage bool `json:"pullImage"`
DeleteImage bool `json:"deleteImage"`
DockerCompose string `json:"dockerCompose"`
TaskID string `json:"taskID"`
}
-16
View File
@@ -21,10 +21,6 @@ type McpServerCreate struct {
StreamableHttpPath string `json:"streamableHttpPath"`
OutputTransport string `json:"outputTransport" validate:"required"`
Type string `json:"type" validate:"required"`
GatewayImage string `json:"gatewayImage"`
ProtocolVersion string `json:"protocolVersion"`
GatewayArgs string `json:"gatewayArgs" validate:"max=4096"`
TaskID string `json:"taskID"`
}
type McpServerUpdate struct {
@@ -36,23 +32,11 @@ type McpServerDelete struct {
ID uint `json:"id" validate:"required"`
}
type McpServerDetail struct {
ID uint `json:"id" validate:"required"`
}
type McpServerStatusSync struct {
IDs []uint `json:"ids"`
}
type McpServerOperate struct {
ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"`
}
type McpServerConnectionTest struct {
ID uint `json:"id" validate:"required"`
}
type McpBindDomain struct {
Domain string `json:"domain" validate:"required"`
SSLID uint `json:"sslID"`
+8 -13
View File
@@ -114,22 +114,17 @@ type NginxRedirectUpdate struct {
}
type NginxBuildReq struct {
TaskID string `json:"taskID" validate:"required"`
Mirror string `json:"mirror" validate:"required"`
Modules []string `json:"modules"`
Force bool `json:"force"`
TaskID string `json:"taskID" validate:"required"`
Mirror string `json:"mirror" validate:"required"`
}
type NginxModuleUpdate struct {
Operate string `json:"operate" validate:"required,oneof=create delete update"`
Name string `json:"name" validate:"required"`
Script string `json:"script"`
Packages string `json:"packages"`
Enable bool `json:"enable"`
Params string `json:"params"`
BuildMode string `json:"buildMode" validate:"omitempty,oneof=dynamic static"`
Provider string `json:"provider" validate:"omitempty,oneof=local prebuilt"`
LoadOrder int `json:"loadOrder" validate:"omitempty,min=0,max=9999"`
Operate string `json:"operate" validate:"required,oneof=create delete update"`
Name string `json:"name" validate:"required"`
Script string `json:"script"`
Packages string `json:"packages"`
Enable bool `json:"enable"`
Params string `json:"params"`
}
type NginxOperateReq struct {
+3 -8
View File
@@ -21,14 +21,13 @@ type RuntimeCreate struct {
Source string `json:"source"`
CodeDir string `json:"codeDir"`
Remark string `json:"remark"`
TaskID string `json:"taskID"`
Params map[string]interface{} `json:"params"`
NodeConfig
}
type NodeConfig struct {
Install *bool `json:"install"`
Install bool `json:"install"`
Clean bool `json:"clean"`
ExposedPorts []ExposedPort `json:"exposedPorts"`
Environments []Environment `json:"environments"`
@@ -43,14 +42,12 @@ type Environment struct {
type Volume struct {
Source string `json:"source"`
Target string `json:"target"`
Mode string `json:"mode"`
}
type ExposedPort struct {
HostPort int `json:"hostPort"`
ContainerPort int `json:"containerPort"`
HostIP string `json:"hostIP"`
Protocol string `json:"protocol"`
}
type ExtraHost struct {
@@ -59,10 +56,8 @@ type ExtraHost struct {
}
type RuntimeDelete struct {
ID uint `json:"id"`
ForceDelete bool `json:"forceDelete"`
DeleteImage bool `json:"deleteImage"`
TaskID string `json:"taskID"`
ID uint `json:"id"`
ForceDelete bool `json:"forceDelete"`
}
type RuntimeUpdate struct {
-14
View File
@@ -126,16 +126,6 @@ type WebsiteSSLUpload struct {
Type string `json:"type" validate:"required,oneof=paste local"`
SSLID uint `json:"sslID"`
Description string `json:"description"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
}
type WebsiteSSLPush struct {
ID uint `json:"id" validate:"required"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
TaskID string `json:"taskID" validate:"required"`
Sync bool `json:"sync"`
}
type WebsiteCASearch struct {
@@ -167,8 +157,6 @@ type WebsiteCAObtain struct {
Description string `json:"description"`
ExecShell bool `json:"execShell"`
Shell string `json:"shell"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
}
type WebsiteCARenew struct {
@@ -179,6 +167,4 @@ type WebsiteSSLFileUpload struct {
Type string `json:"type"`
Description string `json:"description"`
SSLID uint64 `json:"sslID"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
}
+28 -30
View File
@@ -104,36 +104,34 @@ type AppDetail struct {
}
type AppInstallDTO struct {
ID uint `json:"id"`
Name string `json:"name"`
AppID uint `json:"appID"`
AppDetailID uint `json:"appDetailID"`
Version string `json:"version"`
Status string `json:"status"`
Message string `json:"message"`
HttpPort int `json:"httpPort"`
HttpsPort int `json:"httpsPort"`
Path string `json:"path"`
CanUpdate bool `json:"canUpdate"`
Icon string `json:"icon"`
AppName string `json:"appName"`
Ready int `json:"ready"`
Total int `json:"total"`
AppKey string `json:"appKey"`
AppType string `json:"appType"`
AppStatus string `json:"appStatus"`
DockerCompose string `json:"dockerCompose"`
WebUI string `json:"webUI"`
CreatedAt time.Time `json:"createdAt"`
Favorite bool `json:"favorite"`
SortOrder int `json:"sortOrder"`
App AppDetail `json:"app"`
Container string `json:"container"`
IsEdit bool `json:"isEdit"`
LinkDB bool `json:"linkDB"`
ResourceKeys []string `json:"resourceKeys"`
ServiceName string `json:"serviceName"`
Env map[string]interface{} `json:"env"`
ID uint `json:"id"`
Name string `json:"name"`
AppID uint `json:"appID"`
AppDetailID uint `json:"appDetailID"`
Version string `json:"version"`
Status string `json:"status"`
Message string `json:"message"`
HttpPort int `json:"httpPort"`
HttpsPort int `json:"httpsPort"`
Path string `json:"path"`
CanUpdate bool `json:"canUpdate"`
Icon string `json:"icon"`
AppName string `json:"appName"`
Ready int `json:"ready"`
Total int `json:"total"`
AppKey string `json:"appKey"`
AppType string `json:"appType"`
AppStatus string `json:"appStatus"`
DockerCompose string `json:"dockerCompose"`
WebUI string `json:"webUI"`
CreatedAt time.Time `json:"createdAt"`
Favorite bool `json:"favorite"`
SortOrder int `json:"sortOrder"`
App AppDetail `json:"app"`
Container string `json:"container"`
IsEdit bool `json:"isEdit"`
LinkDB bool `json:"linkDB"`
ServiceName string `json:"serviceName"`
}
type AppInstallInfo struct {
-14
View File
@@ -16,12 +16,6 @@ type McpServerDTO struct {
Volumes []request.Volume `json:"volumes"`
}
type McpServerStatusDTO struct {
ID uint `json:"id"`
Status string `json:"status"`
Message string `json:"message"`
}
type McpBindDomainRes struct {
Domain string `json:"domain"`
SSLID uint `json:"sslID"`
@@ -30,11 +24,3 @@ type McpBindDomainRes struct {
WebsiteID uint `json:"websiteID"`
ConnUrl string `json:"connUrl"`
}
type McpServerConnectionTestRes struct {
Success bool `json:"success"`
Endpoint string `json:"endpoint"`
OutputTransport string `json:"outputTransport"`
ProtocolVersion string `json:"protocolVersion,omitempty"`
Message string `json:"message"`
}
+7 -16
View File
@@ -69,25 +69,16 @@ type NginxProxyCache struct {
}
type NginxModule struct {
Name string `json:"name"`
Custom bool `json:"custom"`
Script string `json:"script"`
Packages string `json:"packages"`
Params string `json:"params"`
Enable bool `json:"enable"`
BuildMode string `json:"buildMode"`
Provider string `json:"provider"`
LoadOrder int `json:"loadOrder"`
BuildStatus string `json:"buildStatus"`
LoadStatus string `json:"loadStatus"`
Artifacts []dto.NginxModuleArtifact `json:"artifacts"`
LastError string `json:"lastError"`
Name string `json:"name"`
Script string `json:"script"`
Packages string `json:"packages"`
Params string `json:"params"`
Enable bool `json:"enable"`
}
type NginxBuildConfig struct {
Mirror string `json:"mirror"`
DynamicSupported bool `json:"dynamicSupported"`
Modules []NginxModule `json:"modules"`
Mirror string `json:"mirror"`
Modules []NginxModule `json:"modules"`
}
type NginxConfigRes struct {
-30
View File
@@ -1,30 +0,0 @@
package dto
import "time"
type RuntimeDiagnosticsSummary struct {
RSS uint64 `json:"rss"`
HeapAlloc uint64 `json:"heapAlloc"`
HeapObjects uint64 `json:"heapObjects"`
Goroutines int `json:"goroutines"`
}
type RuntimeGoroutineGroup struct {
State string `json:"state"`
Top string `json:"top"`
Count int `json:"count"`
Stack []string `json:"stack"`
}
type RuntimeGoroutineSnapshot struct {
Total int `json:"total"`
GroupCount int `json:"groupCount"`
Truncated bool `json:"truncated"`
CapturedAt time.Time `json:"capturedAt"`
Goroutines []RuntimeGoroutineGroup `json:"goroutines"`
}
type RuntimeProfileCreate struct {
Type string `json:"type" validate:"required,oneof=cpu heap goroutine mutex block"`
Duration int `json:"duration" validate:"omitempty,min=5,max=30"`
}
-2
View File
@@ -7,8 +7,6 @@ type AgentAccount struct {
APIKey string `json:"apiKey"`
BaseURL string `json:"baseUrl"`
APIType string `json:"apiType"`
AuthMode string `json:"authMode"`
VerifyModel string `json:"verifyModel"`
RememberAPIKey bool `json:"rememberApiKey"`
Verified bool `json:"verified"`
Remark string `json:"remark"`
+8 -4
View File
@@ -2,10 +2,14 @@ package model
type AgentAccountModel struct {
BaseModel
AccountID uint `json:"accountId" gorm:"index"`
Model string `json:"model" gorm:"index"`
Name string `json:"name"`
SortOrder int `json:"sortOrder" gorm:"index"`
AccountID uint `json:"accountId" gorm:"index"`
Model string `json:"model" gorm:"index"`
Name string `json:"name"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"`
Input string `json:"input" gorm:"type:text"`
SortOrder int `json:"sortOrder" gorm:"index"`
}
func (AgentAccountModel) TableName() string {
-21
View File
@@ -1,21 +0,0 @@
package model
type DatabaseUser struct {
BaseModel
Type string `json:"type" gorm:"not null;uniqueIndex:idx_database_user"`
Database string `json:"database" gorm:"not null;uniqueIndex:idx_database_user"`
Username string `json:"username" gorm:"not null;uniqueIndex:idx_database_user"`
Host string `json:"host" gorm:"uniqueIndex:idx_database_user"`
Password string `json:"password"`
Description string `json:"description"`
IsDelete bool `json:"isDelete"`
}
type DatabaseUserGrant struct {
BaseModel
Type string `json:"type" gorm:"not null;uniqueIndex:idx_database_user_grant"`
Database string `json:"database" gorm:"not null;uniqueIndex:idx_database_user_grant"`
DBName string `json:"dbName" gorm:"not null;uniqueIndex:idx_database_user_grant"`
Username string `json:"username" gorm:"not null;uniqueIndex:idx_database_user_grant"`
Host string `json:"host" gorm:"not null;uniqueIndex:idx_database_user_grant"`
}
-3
View File
@@ -18,7 +18,4 @@ type McpServer struct {
StreamableHttpPath string `json:"streamableHttpPath"`
OutputTransport string `json:"outputTransport"`
Type string `json:"type"`
GatewayImage string `json:"gatewayImage"`
ProtocolVersion string `json:"protocolVersion"`
GatewayArgs string `json:"gatewayArgs"`
}
+333 -245
View File
@@ -1,181 +1,321 @@
package provider
import (
"fmt"
"net/url"
"strings"
)
type APIConfig struct {
APIType string
BaseURL string
EditableBaseURL bool
DefaultAuthMode string
AuthModes []string
type Model struct {
ID string
Name string
ContextWindow int
MaxTokens int
Reasoning bool
Input []string
}
const (
AuthModeBearer = "bearer"
AuthModeXAPIKey = "x-api-key"
)
type Model struct {
ID string
Name string
type RuntimeDefault struct {
APIType string
ContextWindow int
MaxTokens int
Input []string
}
type Meta struct {
Key string
DisplayName string
Sort uint
DefaultAPIType string
APIConfigs []APIConfig
DefaultBaseURL string
EnvKey string
Default RuntimeDefault
Models []Model
}
var catalog = map[string]Meta{
"custom": {
Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY",
APIConfigs: editableAPIConfigs("openai-completions", "openai-responses", "anthropic-messages"),
Key: "custom",
DisplayName: "Custom",
Sort: 10,
DefaultBaseURL: "",
EnvKey: "CUSTOM_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{},
},
"ollama": {
Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses",
APIConfigs: editableAPIConfigs("openai-responses", "openai-completions"),
Key: "ollama",
DisplayName: "Ollama",
Sort: 15,
Default: RuntimeDefault{
APIType: "openai-responses",
ContextWindow: 160000,
MaxTokens: 8192,
Input: []string{"text"},
},
},
"vllm": {
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY",
APIConfigs: editableAPIConfigs("openai-completions", "openai-responses", "anthropic-messages"),
Key: "vllm",
DisplayName: "vLLM",
Sort: 20,
DefaultBaseURL: "",
EnvKey: "VLLM_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{},
},
"deepseek": {
Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY",
APIConfigs: []APIConfig{
{APIType: "openai-completions", BaseURL: "https://api.deepseek.com/v1"},
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"}},
},
"bailian-coding-plan": {
Key: "bailian-coding-plan", DisplayName: "阿里云百炼 Coding Plan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
APIConfigs: []APIConfig{
{APIType: "openai-completions", BaseURL: "https://coding.dashscope.aliyuncs.com/v1"},
anthropicAPIConfig("https://coding.dashscope.aliyuncs.com/apps/anthropic", AuthModeBearer),
Key: "deepseek",
DisplayName: "DeepSeek",
Sort: 25,
DefaultBaseURL: "https://api.deepseek.com/v1",
EnvKey: "DEEPSEEK_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 131072,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "qwen3-coder-plus", Name: "Qwen3-Coder-Plus"},
{ID: "qwen3-max-2026-01-23", Name: "Qwen3-Max-2026-01-23"},
{ID: "qwen3-coder-next", Name: "Qwen3-Coder-Next"},
{ID: "glm-4.7", Name: "GLM-4.7"},
{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"},
{ID: "deepseek/deepseek-v4-flash", Name: "deepseek-v4-flash"},
{ID: "deepseek/deepseek-v4-pro", Name: "deepseek-v4-pro"},
},
},
"bailian-coding-plan": {
Key: "bailian-coding-plan",
DisplayName: "阿里云百炼 Coding Plan",
Sort: 30,
DefaultBaseURL: "https://coding.dashscope.aliyuncs.com/v1",
EnvKey: "QWEN_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "bailian-coding-plan/qwen3.5-plus", Name: "Qwen3.5-Plus", Reasoning: true},
{ID: "bailian-coding-plan/qwen3-max", Name: "Qwen3-Max", Reasoning: true},
{ID: "bailian-coding-plan/qwen3-coder-next", Name: "Qwen3-Coder-Next", Reasoning: true},
{ID: "bailian-coding-plan/qwen3-coder-plus", Name: "Qwen3-Coder-Plus", Reasoning: true},
{ID: "bailian-coding-plan/minimax-m2.5", Name: "MiniMax M2.5", Reasoning: true},
{ID: "bailian-coding-plan/glm-5", Name: "GLM-5", Reasoning: true},
{ID: "bailian-coding-plan/kimi-k2.5", Name: "Kimi-k2.5", Reasoning: true},
{ID: "bailian-coding-plan/glm-4.7", Name: "GLM-4.7", Reasoning: true},
},
},
"ark-coding-plan": {
Key: "ark-coding-plan", DisplayName: "方舟 Coding Plan", Sort: 35, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY",
APIConfigs: []APIConfig{
{APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3"},
anthropicAPIConfig("https://ark.cn-beijing.volces.com/api/coding", AuthModeBearer),
Key: "ark-coding-plan",
DisplayName: "方舟 Coding Plan",
Sort: 35,
DefaultBaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3",
EnvKey: "ARK_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 4096,
Input: []string{"text"},
},
Models: []Model{
{ID: "ark-code-latest", Name: "Ark Coding Plan"}, {ID: "doubao-seed-code", Name: "Doubao Seed Code"},
{ID: "glm-4.7", Name: "GLM 4.7 Coding"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"},
{ID: "kimi-k2.5", Name: "Kimi K2.5 Coding"}, {ID: "doubao-seed-code-preview-251028", Name: "Doubao Seed Code Preview"},
{ID: "ark-coding-plan/ark-code-latest", Name: "Ark Coding Plan", ContextWindow: 256000, MaxTokens: 4096},
{ID: "ark-coding-plan/doubao-seed-code", Name: "Doubao Seed Code", ContextWindow: 256000, MaxTokens: 4096},
{ID: "ark-coding-plan/glm-4.7", Name: "GLM 4.7 Coding", ContextWindow: 200000, MaxTokens: 4096},
{ID: "ark-coding-plan/kimi-k2-thinking", Name: "Kimi K2 Thinking", ContextWindow: 256000, MaxTokens: 4096},
{ID: "ark-coding-plan/kimi-k2.5", Name: "Kimi K2.5 Coding", ContextWindow: 256000, MaxTokens: 4096},
{ID: "ark-coding-plan/doubao-seed-code-preview-251028", Name: "Doubao Seed Code Preview", ContextWindow: 256000, MaxTokens: 4096},
},
},
"zai": {
Key: "zai", DisplayName: "Z.ai", Sort: 40, DefaultAPIType: "openai-completions", EnvKey: "ZAI_API_KEY",
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true}},
Models: []Model{{ID: "glm-5", Name: "GLM-5"}, {ID: "glm-4.7", Name: "GLM-4.7"}, {ID: "glm-4.7-flash", Name: "GLM-4.7-Flash"}, {ID: "glm-4.7-flashx", Name: "GLM-4.7-FlashX"}},
Key: "zai",
DisplayName: "Z.ai",
Sort: 40,
DefaultBaseURL: "https://open.bigmodel.cn/api/paas/v4",
EnvKey: "ZAI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 204800,
MaxTokens: 131072,
Input: []string{"text"},
},
Models: []Model{
{ID: "zai/glm-5", Name: "GLM-5", Reasoning: true},
{ID: "zai/glm-4.7", Name: "GLM-4.7", Reasoning: true},
{ID: "zai/glm-4.7-flash", Name: "GLM-4.7-Flash", Reasoning: true},
{ID: "zai/glm-4.7-flashx", Name: "GLM-4.7-FlashX", Reasoning: true},
},
},
"minimax": {
Key: "minimax", DisplayName: "MiniMax (CN)", Sort: 45, DefaultAPIType: "anthropic-messages", EnvKey: "MINIMAX_API_KEY",
APIConfigs: []APIConfig{
anthropicAPIConfig("https://api.minimaxi.com/anthropic", AuthModeXAPIKey, AuthModeBearer),
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
Key: "minimax",
DisplayName: "MiniMax (CN)",
Sort: 45,
DefaultBaseURL: "https://api.minimaxi.com/anthropic",
EnvKey: "MINIMAX_API_KEY",
Default: RuntimeDefault{
APIType: "anthropic-messages",
ContextWindow: 200000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "minimax/MiniMax-M2.7", Name: "MiniMax M2.7"},
{ID: "minimax/MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"},
{ID: "minimax/MiniMax-M2.5", Name: "MiniMax M2.5", Reasoning: true},
{ID: "minimax/MiniMax-M2.5-highspeed", Name: "MiniMax M2.5 highspeed"},
},
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"}},
},
"xiaomi": {
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
APIConfigs: []APIConfig{
{APIType: "openai-completions", BaseURL: "https://api.xiaomimimo.com/v1"},
anthropicAPIConfig("https://api.xiaomimimo.com/anthropic", AuthModeBearer),
Key: "xiaomi",
DisplayName: "Xiaomi",
Sort: 46,
DefaultBaseURL: "https://api.xiaomimimo.com/v1",
EnvKey: "XIAOMI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 262144,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "xiaomi/mimo-v2-flash", Name: "Xiaomi MiMo V2 Flash", ContextWindow: 262144, MaxTokens: 8192, Input: []string{"text"}},
{ID: "xiaomi/mimo-v2-pro", Name: "Xiaomi MiMo V2 Pro", ContextWindow: 1048576, MaxTokens: 32000, Reasoning: true, Input: []string{"text"}},
{ID: "xiaomi/mimo-v2-omni", Name: "Xiaomi MiMo V2 Omni", ContextWindow: 262144, MaxTokens: 32000, Reasoning: true, Input: []string{"text", "image"}},
},
Models: []Model{{ID: "mimo-v2-flash", Name: "Xiaomi MiMo V2 Flash"}, {ID: "mimo-v2-pro", Name: "Xiaomi MiMo V2 Pro"}, {ID: "mimo-v2-omni", Name: "Xiaomi MiMo V2 Omni"}},
},
"kimi": {
Key: "kimi", DisplayName: "Kimi (CN)", Sort: 50, DefaultAPIType: "openai-completions", EnvKey: "KIMI_API_KEY",
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.cn/v1"}},
Models: []Model{{ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}},
Key: "kimi",
DisplayName: "Kimi (CN)",
Sort: 50,
DefaultBaseURL: "https://api.moonshot.cn/v1",
EnvKey: "KIMI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "kimi/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true},
{ID: "kimi/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
{ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true},
},
},
"kimi-coding": {
Key: "kimi-coding", DisplayName: "Kimi Coding", Sort: 51, DefaultAPIType: "anthropic-messages", EnvKey: "KIMI_API_KEY",
APIConfigs: []APIConfig{anthropicAPIConfig("https://api.kimi.com/coding/", AuthModeXAPIKey)},
Models: []Model{{ID: "kimi-code", Name: "Kimi Code"}, {ID: "k2p5", Name: "Kimi K2.5"}},
Key: "kimi-coding",
DisplayName: "Kimi Coding",
Sort: 51,
DefaultBaseURL: "https://api.kimi.com/coding/",
EnvKey: "KIMI_API_KEY",
Default: RuntimeDefault{
APIType: "anthropic-messages",
ContextWindow: 262144,
MaxTokens: 32768,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "kimi-coding/kimi-code", Name: "Kimi Code", ContextWindow: 262144, MaxTokens: 32768, Reasoning: true, Input: []string{"text", "image"}},
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5", ContextWindow: 262144, MaxTokens: 32768, Reasoning: true, Input: []string{"text", "image"}},
},
},
"openai": {
Key: "openai", DisplayName: "OpenAI", Sort: 55, DefaultAPIType: "openai-responses", EnvKey: "OPENAI_API_KEY",
APIConfigs: []APIConfig{
{APIType: "openai-responses", BaseURL: "https://api.openai.com/v1"},
{APIType: "openai-completions", BaseURL: "https://api.openai.com/v1"},
Key: "openai",
DisplayName: "OpenAI",
Sort: 55,
DefaultBaseURL: "https://api.openai.com/v1",
EnvKey: "OPENAI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-responses",
ContextWindow: 272000,
MaxTokens: 128000,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "openai/gpt-5.4", Name: "gpt-5.4", ContextWindow: 272000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
{ID: "openai/gpt-5.4-pro", Name: "gpt-5.4-pro", ContextWindow: 1050000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
{ID: "openai/gpt-5.4-mini", Name: "gpt-5.4-mini", ContextWindow: 400000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
{ID: "openai/gpt-5.4-nano", Name: "gpt-5.4-nano", ContextWindow: 400000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
},
Models: []Model{{ID: "gpt-5.4", Name: "gpt-5.4"}, {ID: "gpt-5.4-pro", Name: "gpt-5.4-pro"}, {ID: "gpt-5.4-mini", Name: "gpt-5.4-mini"}, {ID: "gpt-5.4-nano", Name: "gpt-5.4-nano"}},
},
"openrouter": {
Key: "openrouter", DisplayName: "OpenRouter", Sort: 56, DefaultAPIType: "openai-completions", EnvKey: "OPENROUTER_API_KEY",
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://openrouter.ai/api/v1"}},
Models: []Model{{ID: "openrouter/free", Name: "openrouter/free"}, {ID: "openrouter/auto", Name: "openrouter/auto"}},
Key: "openrouter",
DisplayName: "OpenRouter",
Sort: 56,
DefaultBaseURL: "https://openrouter.ai/api/v1",
EnvKey: "OPENROUTER_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "openrouter/free", Name: "openrouter/free"},
{ID: "openrouter/auto", Name: "openrouter/auto"},
},
},
"anthropic": {
Key: "anthropic", DisplayName: "Anthropic", Sort: 60, DefaultAPIType: "anthropic-messages", EnvKey: "ANTHROPIC_API_KEY",
APIConfigs: []APIConfig{anthropicAPIConfig("https://api.anthropic.com", AuthModeXAPIKey)},
Models: []Model{{ID: "claude-sonnet-4-6", Name: "Claude Sonnet 4.6"}, {ID: "claude-opus-4-6", Name: "Claude Opus 4.6"}, {ID: "claude-opus-4-5", Name: "Claude Opus 4.5"}, {ID: "claude-sonnet-4-5", Name: "Claude Sonnet 4.5"}, {ID: "claude-haiku-4-5", Name: "Claude Haiku 4.5"}},
Key: "anthropic",
DisplayName: "Anthropic",
Sort: 60,
DefaultBaseURL: "https://api.anthropic.com",
EnvKey: "ANTHROPIC_API_KEY",
Default: RuntimeDefault{
APIType: "anthropic-messages",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "anthropic/claude-sonnet-4-6", Name: "Claude Sonnet 4.6", Reasoning: true},
{ID: "anthropic/claude-opus-4-6", Name: "Claude Opus 4.6", Reasoning: true},
{ID: "anthropic/claude-opus-4-5", Name: "Claude Opus 4.5"},
{ID: "anthropic/claude-sonnet-4-5", Name: "Claude Sonnet 4.5"},
{ID: "anthropic/claude-haiku-4-5", Name: "Claude Haiku 4.5"},
},
},
"gemini": {
Key: "gemini", DisplayName: "Gemini", Sort: 65, DefaultAPIType: "gemini-generate-content", EnvKey: "GEMINI_API_KEY",
APIConfigs: []APIConfig{{APIType: "gemini-generate-content", BaseURL: "https://generativelanguage.googleapis.com"}},
Models: []Model{{ID: "gemini-3-flash-preview", Name: "Gemini 3 Flash Preview"}, {ID: "gemini-flash-latest", Name: "Gemini Flash Latest"}, {ID: "gemini-3-pro-preview", Name: "Gemini 3 Pro Preview"}},
Key: "gemini",
DisplayName: "Gemini",
Sort: 65,
DefaultBaseURL: "https://generativelanguage.googleapis.com",
EnvKey: "GEMINI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview", Reasoning: true},
{ID: "google/gemini-flash-latest", Name: "Gemini Flash Latest"},
{ID: "google/gemini-3-pro-preview", Name: "Gemini 3 Pro Preview", Reasoning: true},
},
},
"moonshot": {
Key: "moonshot", DisplayName: "Moonshot (Global)", Sort: 70, DefaultAPIType: "openai-completions", EnvKey: "MOONSHOT_API_KEY",
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.ai/v1"}},
Models: []Model{{ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}},
Key: "moonshot",
DisplayName: "Moonshot (Global)",
Sort: 70,
DefaultBaseURL: "https://api.moonshot.ai/v1",
EnvKey: "MOONSHOT_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true},
{ID: "moonshot/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
{ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true},
},
},
}
func editableAPIConfigs(apiTypes ...string) []APIConfig {
configs := make([]APIConfig, 0, len(apiTypes))
for _, apiType := range apiTypes {
if apiType == "anthropic-messages" {
config := anthropicAPIConfig("", AuthModeXAPIKey, AuthModeBearer)
config.EditableBaseURL = true
configs = append(configs, config)
continue
}
configs = append(configs, APIConfig{APIType: apiType, EditableBaseURL: true})
}
return configs
}
func anthropicAPIConfig(baseURL, defaultAuthMode string, additionalAuthModes ...string) APIConfig {
authModes := []string{defaultAuthMode}
for _, authMode := range additionalAuthModes {
if authMode != defaultAuthMode {
authModes = append(authModes, authMode)
}
}
return APIConfig{
APIType: "anthropic-messages",
BaseURL: baseURL,
DefaultAuthMode: defaultAuthMode,
AuthModes: authModes,
}
}
func Get(key string) (Meta, bool) {
meta, ok := catalog[key]
if !ok {
@@ -192,101 +332,12 @@ func All() map[string]Meta {
return result
}
func FindAPIConfig(key, apiType string) (APIConfig, bool) {
meta, ok := catalog[key]
if !ok {
return APIConfig{}, false
}
target := strings.TrimSpace(apiType)
if target == "" {
target = meta.DefaultAPIType
}
for _, config := range meta.APIConfigs {
if config.APIType == target {
return config, true
}
}
return APIConfig{}, false
}
func ResolveAuthMode(provider, apiType, requested string) (string, error) {
config, ok := FindAPIConfig(provider, apiType)
if !ok {
return "", fmt.Errorf("provider %s does not support api type %s", provider, apiType)
}
if config.APIType != "anthropic-messages" {
return "", nil
}
authMode := strings.TrimSpace(requested)
if authMode == "" {
authMode = config.DefaultAuthMode
}
for _, allowed := range config.AuthModes {
if authMode == allowed {
return authMode, nil
}
}
return "", fmt.Errorf("provider %s does not support auth mode %s", provider, authMode)
}
func DefaultBaseURL(key string) (string, bool) {
config, ok := FindAPIConfig(key, "")
if !ok || strings.TrimSpace(config.BaseURL) == "" {
meta, ok := catalog[key]
if !ok || strings.TrimSpace(meta.DefaultBaseURL) == "" {
return "", false
}
return config.BaseURL, true
}
func DefaultAPIType(key string) string {
meta, ok := catalog[key]
if !ok || strings.TrimSpace(meta.DefaultAPIType) == "" {
return "openai-completions"
}
return meta.DefaultAPIType
}
func ResolveBaseURL(key, apiType, requested string) (string, error) {
config, ok := FindAPIConfig(key, apiType)
if !ok {
return "", fmt.Errorf("provider %s does not support api type %s", key, apiType)
}
if !config.EditableBaseURL {
return strings.TrimRight(config.BaseURL, "/"), nil
}
baseURL := strings.TrimSpace(requested)
if baseURL == "" {
baseURL = config.BaseURL
}
if baseURL == "" {
return "", fmt.Errorf("base url is required")
}
parsed, err := url.Parse(baseURL)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return "", fmt.Errorf("invalid base url")
}
parsed.Path = normalizeEndpointPath(config.APIType, parsed.Path)
parsed.RawQuery = ""
parsed.Fragment = ""
return strings.TrimRight(parsed.String(), "/"), nil
}
func normalizeEndpointPath(apiType, value string) string {
path := strings.TrimRight(value, "/")
suffixes := []string{}
switch apiType {
case "openai-completions":
suffixes = []string{"/chat/completions"}
case "openai-responses":
suffixes = []string{"/responses"}
case "anthropic-messages":
suffixes = []string{"/v1/messages", "/messages"}
}
for _, suffix := range suffixes {
if strings.HasSuffix(strings.ToLower(path), suffix) {
return path[:len(path)-len(suffix)]
}
}
return path
return meta.DefaultBaseURL, true
}
func EnvKey(key string) string {
@@ -299,52 +350,89 @@ func EnvKey(key string) string {
func DisplayName(key string) string {
meta, ok := catalog[key]
if !ok || strings.TrimSpace(meta.DisplayName) == "" {
if !ok {
return key
}
if strings.TrimSpace(meta.DisplayName) == "" {
return key
}
return meta.DisplayName
}
func NormalizeModelID(provider, modelID string) string {
target := strings.TrimLeft(strings.TrimSpace(modelID), "/")
for _, prefix := range legacyModelPrefixes[provider] {
legacyPrefix := prefix + "/"
if !strings.HasPrefix(target, legacyPrefix) {
continue
}
candidate := strings.TrimSpace(strings.TrimPrefix(target, legacyPrefix))
if candidate != "" {
return candidate
func FindModel(key, modelID string) (Model, bool) {
meta, ok := Get(key)
if !ok {
return Model{}, false
}
for _, item := range meta.Models {
if item.ID == modelID {
return item, true
}
}
return target
}
var legacyModelPrefixes = map[string][]string{
"custom": {"custom"},
"vllm": {"custom"},
"ollama": {"ollama"},
"deepseek": {"deepseek"},
"bailian-coding-plan": {"bailian-coding-plan"},
"ark-coding-plan": {"ark-coding-plan"},
"zai": {"zai"},
"minimax": {"minimax"},
"xiaomi": {"xiaomi"},
"kimi": {"kimi", "moonshot"},
"kimi-coding": {"kimi-coding"},
"openai": {"openai"},
"anthropic": {"anthropic"},
"gemini": {"google", "gemini"},
"moonshot": {"moonshot"},
return Model{}, false
}
func cloneMeta(meta Meta) Meta {
clone := meta
clone.APIConfigs = make([]APIConfig, len(meta.APIConfigs))
for index, config := range meta.APIConfigs {
clone.APIConfigs[index] = config
clone.APIConfigs[index].AuthModes = append([]string(nil), config.AuthModes...)
if len(meta.Default.Input) > 0 {
clone.Default.Input = make([]string, len(meta.Default.Input))
copy(clone.Default.Input, meta.Default.Input)
}
if len(meta.Models) > 0 {
clone.Models = make([]Model, len(meta.Models))
for i, item := range meta.Models {
clone.Models[i] = normalizeModel(meta, item)
}
}
clone.Models = append([]Model(nil), meta.Models...)
return clone
}
func normalizeModel(meta Meta, model Model) Model {
clone := model
clone.ID = strings.TrimSpace(clone.ID)
clone.Name = strings.TrimSpace(clone.Name)
if clone.Name == "" {
clone.Name = clone.ID
}
if clone.MaxTokens <= 0 {
clone.MaxTokens = meta.Default.MaxTokens
}
if clone.ContextWindow <= 0 {
clone.ContextWindow = meta.Default.ContextWindow
}
if len(clone.Input) == 0 && len(meta.Default.Input) > 0 {
clone.Input = make([]string, len(meta.Default.Input))
copy(clone.Input, meta.Default.Input)
}
return clone
}
func ResolveRuntimeParams(provider, apiType string, maxTokens, contextWindow int) (string, int, int) {
defaultAPIType := "openai-completions"
defaultMaxTokens := 8192
defaultContextWindow := 256000
if meta, ok := Get(provider); ok {
if meta.Default.APIType != "" {
defaultAPIType = meta.Default.APIType
}
if meta.Default.MaxTokens > 0 {
defaultMaxTokens = meta.Default.MaxTokens
}
if meta.Default.ContextWindow > 0 {
defaultContextWindow = meta.Default.ContextWindow
}
}
resolvedAPI := apiType
if strings.TrimSpace(apiType) == "" {
resolvedAPI = defaultAPIType
}
resolvedMaxTokens := defaultMaxTokens
resolvedContextWindow := defaultContextWindow
if maxTokens > 0 {
resolvedMaxTokens = maxTokens
}
if contextWindow > 0 {
resolvedContextWindow = contextWindow
}
return resolvedAPI, resolvedMaxTokens, resolvedContextWindow
}
-64
View File
@@ -1,64 +0,0 @@
package provider
import (
"encoding/json"
"fmt"
"net/http"
"strings"
)
func DiscoverModels(baseURL, apiKey string) ([]string, error) {
req, err := http.NewRequest(http.MethodGet, buildModelDiscoveryURL(baseURL), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := (&http.Client{Timeout: defaultVerifyTimeout}).Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
return nil, fmt.Errorf("request failed: %s", resp.Status)
}
var payload struct {
Data []struct {
ID string `json:"id"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, err
}
models := make([]string, 0, len(payload.Data))
seen := make(map[string]struct{}, len(payload.Data))
for _, item := range payload.Data {
id := strings.TrimSpace(item.ID)
if id == "" {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
models = append(models, id)
}
if len(models) == 0 {
return nil, fmt.Errorf("no models found")
}
return models, nil
}
func buildModelDiscoveryURL(baseURL string) string {
base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
for _, apiType := range []string{"openai-completions", "openai-responses", "anthropic-messages"} {
base = normalizeEndpointPath(apiType, base)
}
switch {
case strings.HasSuffix(base, "/v1/models"):
return base
case strings.HasSuffix(base, "/v1"):
return base + "/models"
default:
return base + "/v1/models"
}
}
+52 -29
View File
@@ -15,49 +15,72 @@ type OpenClawProviderPatch struct {
AuthHeader bool
}
func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL, apiKey string) (*OpenClawProviderPatch, error) {
func BuildOpenClawProviderPatch(provider, modelName, apiType, baseURL, apiKey string) (*OpenClawProviderPatch, error) {
if modelName == "" {
return nil, fmt.Errorf("model is required")
}
resolvedAPIType := apiType
if _, ok := FindAPIConfig(provider, resolvedAPIType); !ok {
resolvedAPIType = DefaultAPIType(provider)
}
resolvedAuthMode, err := ResolveAuthMode(provider, resolvedAPIType, authMode)
if err != nil {
return nil, err
}
usesBearer := resolvedAuthMode == AuthModeBearer
modelID := NormalizeModelID(provider, modelName)
providerKey := provider
preserveQualifiedModel := false
resolvedAPIType, _, _ := ResolveRuntimeParams(provider, apiType, 0, 0)
modelID := resolveOpenClawModelID(provider, modelName)
switch provider {
case "deepseek":
return newOpenClawProviderPatch(modelName, "deepseek", modelID, apiKey, baseURL, "openai-completions", false), nil
case "gemini":
providerKey = "google"
resolvedAPIType = "google-generative-ai"
usesBearer = false
return newOpenClawProviderPatch("google/"+modelID, "google", modelID, apiKey, baseURL, resolvedAPIType, false), nil
case "moonshot", "kimi":
providerKey = "moonshot"
resolvedAPIType = "openai-completions"
usesBearer = false
return buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey), nil
case "bailian-coding-plan":
return newOpenClawProviderPatch("bailian-coding-plan/"+modelID, "bailian-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil
case "ark-coding-plan":
return newOpenClawProviderPatch("ark-coding-plan/"+modelID, "ark-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil
case "minimax":
return newOpenClawProviderPatch("minimax/"+modelID, "minimax", modelID, apiKey, baseURL, "anthropic-messages", true), nil
case "xiaomi":
return newOpenClawProviderPatch("xiaomi/"+modelID, "xiaomi", modelID, apiKey, baseURL, "openai-completions", false), nil
case "custom", "vllm":
return newOpenClawProviderPatch(provider+"/"+modelID, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil
case "ollama":
apiKey = "ollama"
usesBearer = false
case "openai", "openrouter", "anthropic":
preserveQualifiedModel = strings.Contains(modelName, "/")
return newOpenClawProviderPatch(modelName, "ollama", modelID, "ollama", baseURL, resolvedAPIType, false), nil
case "kimi-coding":
return newOpenClawProviderPatch(modelName, "kimi-coding", modelID, apiKey, baseURL, "anthropic-messages", false), nil
case "zai":
return newOpenClawProviderPatch("zai/"+modelID, "zai", modelID, apiKey, baseURL, "openai-completions", false), nil
default:
return newOpenClawProviderPatch(modelName, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil
}
}
primaryModel := providerKey + "/" + modelID
if preserveQualifiedModel {
primaryModel = modelName
func buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey string) *OpenClawProviderPatch {
providerKey := provider
primaryModel := modelName
if provider == "kimi" {
providerKey = "moonshot"
primaryModel = "moonshot/" + modelID
}
return newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, "openai-completions", false)
}
func newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, apiType string, authHeader bool) *OpenClawProviderPatch {
return &OpenClawProviderPatch{
PrimaryModel: primaryModel,
ProviderKey: providerKey,
ModelID: modelID,
APIKey: apiKey,
BaseURL: baseURL,
APIType: resolvedAPIType,
AuthHeader: usesBearer,
}, nil
APIType: apiType,
AuthHeader: authHeader,
}
}
func resolveOpenClawModelID(provider, modelName string) string {
if provider == "custom" || provider == "vllm" {
target := strings.TrimLeft(modelName, "/")
if parts := strings.SplitN(target, "/", 2); len(parts) == 2 && parts[0] == "custom" {
return strings.TrimLeft(parts[1], "/")
}
return target
}
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
return parts[1]
}
return modelName
}
+127 -77
View File
@@ -3,9 +3,7 @@ package provider
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
@@ -20,106 +18,158 @@ type VerifyRequest struct {
Body []byte
}
type verifyErrorResponse struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
Message string `json:"message"`
}
const (
defaultVerifyTimeout = 30 * time.Second
)
const defaultVerifyTimeout = 30 * time.Second
func SkipVerification(provider string) bool {
switch provider {
case "vllm", "ollama", "kimi-coding":
func SkipVerification(key string) bool {
switch key {
case "custom", "vllm", "ollama", "kimi-coding":
return true
default:
return false
}
}
func VerifyAccount(provider, apiType, authMode, baseURL, apiKey, model string) error {
req := BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model)
httpReq, err := http.NewRequest(req.Method, req.URL, bytes.NewReader(req.Body))
func VerifyAccount(provider, baseURL, apiKey string) error {
req := BuildVerifyRequest(provider, baseURL, apiKey)
var body *bytes.Buffer
if len(req.Body) > 0 {
body = bytes.NewBuffer(req.Body)
} else {
body = bytes.NewBuffer(nil)
}
httpReq, err := http.NewRequest(req.Method, req.URL, body)
if err != nil {
return err
}
for key, value := range req.Headers {
httpReq.Header.Set(key, value)
}
httpReq.Header.Set("Accept", "application/json")
resp, err := (&http.Client{Timeout: defaultVerifyTimeout}).Do(httpReq)
resp, err := (&http.Client{Timeout: verifyTimeout()}).Do(httpReq)
if err != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", err)
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
if readErr != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", readErr)
}
return buserr.WithErr("ErrAgentAccountUnavailable", errors.New(verifyHTTPError(resp.StatusCode, body)))
if resp.StatusCode >= 400 {
return buserr.WithErr("ErrAgentAccountUnavailable", fmt.Errorf("verify failed: %s", resp.Status))
}
return nil
}
func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model string) VerifyRequest {
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
headers := map[string]string{"Content-Type": "application/json"}
request := VerifyRequest{Method: http.MethodPost, Headers: headers}
if provider == "gemini" {
request.URL = baseURL + "/v1beta/models/" + strings.TrimSpace(model) + ":generateContent"
headers["x-goog-api-key"] = apiKey
request.Body = mustJSON(map[string]interface{}{
"contents": []map[string]interface{}{{"parts": []map[string]string{{"text": "test"}}}},
})
return request
}
switch apiType {
case "anthropic-messages":
request.URL = baseURL + "/v1/messages"
if authMode == AuthModeBearer {
headers["Authorization"] = "Bearer " + apiKey
} else {
headers["x-api-key"] = apiKey
}
headers["anthropic-version"] = "2023-06-01"
request.Body = mustJSON(map[string]interface{}{
"model": model, "max_tokens": 1, "stream": false,
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
})
case "openai-responses":
request.URL = baseURL + "/responses"
headers["Authorization"] = "Bearer " + apiKey
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": 1, "stream": false})
default:
request.URL = baseURL + "/chat/completions"
if provider != "ollama" || strings.TrimSpace(apiKey) != "" {
headers["Authorization"] = "Bearer " + apiKey
}
request.Body = mustJSON(map[string]interface{}{
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": 1, "stream": false,
})
}
return request
func verifyTimeout() time.Duration {
return defaultVerifyTimeout
}
func verifyHTTPError(statusCode int, body []byte) string {
message := strings.TrimSpace(string(body))
var payload verifyErrorResponse
if err := json.Unmarshal(body, &payload); err == nil {
if value := strings.TrimSpace(payload.Error.Message); value != "" {
message = value
} else if value := strings.TrimSpace(payload.Message); value != "" {
message = value
func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest {
base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
headers := map[string]string{}
request := VerifyRequest{Method: http.MethodGet, Headers: headers}
switch provider {
case "anthropic", "kimi-coding":
headers["x-api-key"] = apiKey
headers["anthropic-version"] = "2023-06-01"
if strings.Contains(base, "/v1") {
request.URL = base + "/models"
} else {
request.URL = base + "/v1/models"
}
case "gemini":
request.Method = http.MethodPost
if strings.Contains(base, "/v1beta") {
request.URL = base + "/models/gemini-3-flash-preview:generateContent"
} else {
request.URL = base + "/v1beta/models/gemini-3-flash-preview:generateContent"
}
headers["x-goog-api-key"] = apiKey
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{
"contents": []map[string]interface{}{{
"parts": []map[string]string{{
"text": "Explain how AI works in a few words",
}},
}},
})
case "zai":
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
request.URL = base + "/models"
case "bailian-coding-plan":
request.Method = http.MethodPost
if !strings.Contains(base, "/v1") {
base = base + "/v1"
}
request.URL = base + "/chat/completions"
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{
"model": "qwen3.5-plus",
"messages": []map[string]string{{"role": "user", "content": "test"}},
"max_tokens": 1,
})
case "ark-coding-plan":
request.Method = http.MethodPost
if !strings.Contains(base, "/api/coding/v3") {
base = "https://ark.cn-beijing.volces.com/api/coding/v3"
}
request.URL = base + "/chat/completions"
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{
"model": "ark-code-latest",
"messages": []map[string]string{{"role": "user", "content": "test"}},
"max_tokens": 1,
})
case "minimax":
request.Method = http.MethodPost
headers["x-api-key"] = apiKey
headers["anthropic-version"] = "2023-06-01"
headers["Content-Type"] = "application/json"
if strings.Contains(base, "/v1") {
request.URL = base + "/messages"
} else {
request.URL = base + "/v1/messages"
}
request.Body = mustJSON(map[string]interface{}{
"model": "MiniMax-M2.5",
"max_tokens": 1,
"messages": []map[string]interface{}{{
"role": "user",
"content": []map[string]string{{
"type": "text",
"text": "test",
}},
}},
})
case "xiaomi":
request.Method = http.MethodPost
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Content-Type"] = "application/json"
if !strings.Contains(base, "/v1") {
base = base + "/v1"
}
request.URL = base + "/chat/completions"
request.Body = mustJSON(map[string]interface{}{
"model": "mimo-v2-flash",
"max_tokens": 1,
"messages": []map[string]string{{"role": "user", "content": "test"}},
})
case "openrouter":
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
if strings.Contains(base, "/v1") {
request.URL = base + "/key"
} else {
request.URL = base + "/v1/key"
}
default:
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
if strings.Contains(base, "/v1") {
request.URL = base + "/models"
} else {
request.URL = base + "/v1/models"
}
}
if message == "" {
return fmt.Sprintf("validation request returned status %d", statusCode)
}
return message
return request
}
func mustJSON(value interface{}) []byte {
-7
View File
@@ -15,7 +15,6 @@ type IAppInstallResourceRpo interface {
WithAppInstallId(appInstallId uint) DBOption
WithLinkId(linkId uint) DBOption
WithResourceId(resourceId uint) DBOption
WithResourceIds(resourceIds []uint) DBOption
GetBy(opts ...DBOption) ([]model.AppInstallResource, error)
GetFirst(opts ...DBOption) (model.AppInstallResource, error)
Create(ctx context.Context, resource *model.AppInstallResource) error
@@ -45,12 +44,6 @@ func (a AppInstallResourceRpo) WithResourceId(resourceId uint) DBOption {
}
}
func (a AppInstallResourceRpo) WithResourceIds(resourceIds []uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("resource_id IN ?", resourceIds)
}
}
func (a AppInstallResourceRpo) GetBy(opts ...DBOption) ([]model.AppInstallResource, error) {
db := global.DB.Model(&model.AppInstallResource{})
var resources []model.AppInstallResource
-121
View File
@@ -1,121 +0,0 @@
package repo
import (
"context"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"gorm.io/gorm"
)
type DatabaseUserRepo struct{}
type IDatabaseUserRepo interface {
Get(opts ...DBOption) (model.DatabaseUser, error)
List(opts ...DBOption) ([]model.DatabaseUser, error)
Save(user *model.DatabaseUser) error
Delete(opts ...DBOption) error
DeleteBy(ctx context.Context, opts ...DBOption) error
Update(vars map[string]interface{}, opts ...DBOption) error
WithByDatabase(database string) DBOption
WithByUser(username, host string) DBOption
WithByUserList(users [][2]string) DBOption
}
func NewIDatabaseUserRepo() IDatabaseUserRepo {
return &DatabaseUserRepo{}
}
func (u *DatabaseUserRepo) Get(opts ...DBOption) (model.DatabaseUser, error) {
var user model.DatabaseUser
db := global.DB.Model(&model.DatabaseUser{})
for _, opt := range opts {
db = opt(db)
}
if err := db.First(&user).Error; err != nil {
return user, err
}
password, err := encrypt.StringDecrypt(user.Password)
if err != nil {
global.LOG.Errorf("decrypt database user %s password failed, err: %v", user.Username, err)
}
user.Password = password
return user, nil
}
func (u *DatabaseUserRepo) List(opts ...DBOption) ([]model.DatabaseUser, error) {
var users []model.DatabaseUser
db := global.DB.Model(&model.DatabaseUser{})
for _, opt := range opts {
db = opt(db)
}
if err := db.Find(&users).Error; err != nil {
return users, err
}
for i := 0; i < len(users); i++ {
password, err := encrypt.StringDecrypt(users[i].Password)
if err != nil {
global.LOG.Errorf("decrypt database user %s password failed, err: %v", users[i].Username, err)
}
users[i].Password = password
}
return users, nil
}
func (u *DatabaseUserRepo) Save(user *model.DatabaseUser) error {
if len(user.Password) != 0 {
password, err := encrypt.StringEncrypt(user.Password)
if err != nil {
return fmt.Errorf("encrypt database user %s password failed, err: %v", user.Username, err)
}
user.Password = password
}
return global.DB.Save(user).Error
}
func (u *DatabaseUserRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.DatabaseUser{}).Error
}
func (u *DatabaseUserRepo) DeleteBy(ctx context.Context, opts ...DBOption) error {
return getTx(ctx, opts...).Delete(&model.DatabaseUser{}).Error
}
func (u *DatabaseUserRepo) Update(vars map[string]interface{}, opts ...DBOption) error {
db := global.DB.Model(&model.DatabaseUser{})
for _, opt := range opts {
db = opt(db)
}
return db.Updates(vars).Error
}
func (u *DatabaseUserRepo) WithByDatabase(database string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("database = ?", database)
}
}
func (u *DatabaseUserRepo) WithByUser(username, host string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("username = ? AND host = ?", username, host)
}
}
func (u *DatabaseUserRepo) WithByUserList(users [][2]string) DBOption {
return func(g *gorm.DB) *gorm.DB {
if len(users) == 0 {
return g.Where("1 = 0")
}
values := make([][]interface{}, 0, len(users))
for _, user := range users {
values = append(values, []interface{}{user[0], user[1]})
}
return g.Where("(username, host) IN ?", values)
}
}
-109
View File
@@ -1,109 +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 DatabaseUserGrantRepo struct{}
type IDatabaseUserGrantRepo interface {
Get(opts ...DBOption) (model.DatabaseUserGrant, error)
List(opts ...DBOption) ([]model.DatabaseUserGrant, error)
Save(grant *model.DatabaseUserGrant) error
Replace(dbType, database string, grants []model.DatabaseUserGrant) error
Delete(opts ...DBOption) error
DeleteBy(ctx context.Context, opts ...DBOption) error
Update(vars map[string]interface{}, opts ...DBOption) error
WithByDatabase(database string) DBOption
WithByDBName(dbName string) DBOption
WithByDBNames(dbNames []string) DBOption
WithByUser(username, host string) DBOption
}
func (u *DatabaseUserGrantRepo) Get(opts ...DBOption) (model.DatabaseUserGrant, error) {
var grant model.DatabaseUserGrant
db := global.DB.Model(&model.DatabaseUserGrant{})
for _, opt := range opts {
db = opt(db)
}
err := db.First(&grant).Error
return grant, err
}
func NewIDatabaseUserGrantRepo() IDatabaseUserGrantRepo {
return &DatabaseUserGrantRepo{}
}
func (u *DatabaseUserGrantRepo) List(opts ...DBOption) ([]model.DatabaseUserGrant, error) {
var grants []model.DatabaseUserGrant
db := global.DB.Model(&model.DatabaseUserGrant{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&grants).Error
return grants, err
}
func (u *DatabaseUserGrantRepo) Save(grant *model.DatabaseUserGrant) error {
return global.DB.Save(grant).Error
}
func (u *DatabaseUserGrantRepo) Replace(dbType, database string, grants []model.DatabaseUserGrant) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("`type` = ? AND database = ?", dbType, database).Delete(&model.DatabaseUserGrant{}).Error; err != nil {
return err
}
if len(grants) != 0 {
return tx.Create(&grants).Error
}
return nil
})
}
func (u *DatabaseUserGrantRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.DatabaseUserGrant{}).Error
}
func (u *DatabaseUserGrantRepo) DeleteBy(ctx context.Context, opts ...DBOption) error {
return getTx(ctx, opts...).Delete(&model.DatabaseUserGrant{}).Error
}
func (u *DatabaseUserGrantRepo) Update(vars map[string]interface{}, opts ...DBOption) error {
db := global.DB.Model(&model.DatabaseUserGrant{})
for _, opt := range opts {
db = opt(db)
}
return db.Updates(vars).Error
}
func (u *DatabaseUserGrantRepo) WithByDatabase(database string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("database = ?", database)
}
}
func (u *DatabaseUserGrantRepo) WithByDBName(dbName string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("db_name = ?", dbName)
}
}
func (u *DatabaseUserGrantRepo) WithByDBNames(dbNames []string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("db_name IN ?", dbNames)
}
}
func (u *DatabaseUserGrantRepo) WithByUser(username, host string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("username = ? AND host = ?", username, host)
}
}
-21
View File
@@ -17,12 +17,10 @@ type IWebsiteRepo interface {
WithGroupID(groupId uint) DBOption
WithDefaultServer() DBOption
WithDomainLike(domain string) DBOption
WithSearchKeyword(keyword string, ids []uint) DBOption
WithRuntimeID(runtimeID uint) DBOption
WithParentID(websiteID uint) DBOption
WithType(websiteType string) DBOption
WithDBType(dbType string) DBOption
WithDBTypes(dbTypes []string) DBOption
WithDBID(dbID uint) DBOption
Page(page, size int, opts ...DBOption) (int64, []model.Website, error)
@@ -78,19 +76,6 @@ func (w *WebsiteRepo) WithDomainLike(domain string) DBOption {
}
}
func (w *WebsiteRepo) WithSearchKeyword(keyword string, ids []uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
if keyword == "" {
return db
}
keyword = "%" + keyword + "%"
if len(ids) == 0 {
return db.Where("(primary_domain like ? OR alias like ?)", keyword, keyword)
}
return db.Where("(primary_domain like ? OR alias like ? OR id in (?))", keyword, keyword, ids)
}
}
func (w *WebsiteRepo) WithAlias(alias string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("alias = ?", alias)
@@ -133,12 +118,6 @@ func (w *WebsiteRepo) WithDBType(dbType string) DBOption {
}
}
func (w *WebsiteRepo) WithDBTypes(dbTypes []string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("db_type IN ?", dbTypes)
}
}
func (w *WebsiteRepo) WithDBID(dbID uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("db_id = ?", dbID)
+139 -232
View File
@@ -8,8 +8,8 @@ import (
"os"
"path"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -30,8 +30,6 @@ import (
"gorm.io/gorm"
)
var agentAccountMutationMu sync.Mutex
type IAgentService interface {
Create(req dto.AgentCreateReq) (*dto.AgentItem, error)
BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error)
@@ -79,7 +77,6 @@ type IAgentService interface {
PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error)
CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error)
GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error)
CreateAccountModel(req dto.AgentAccountModelCreateReq) error
UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error
DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error
@@ -147,7 +144,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 {
return nil, buserr.New("ErrNameIsExist")
}
if !global.CONF.Base.IsEnterprise && !xpack.MultiNodeProvider.IsXpack() {
if !xpack.MultiNodeProvider.IsXpack() {
count, _, err := agentRepo.Page(1, 1)
if err != nil {
return nil, err
@@ -168,6 +165,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
provider := ""
baseURL := ""
apiType := ""
maxTokens := 0
contextWindow := 0
apiKey := ""
runtimeModel := ""
accountID := uint(0)
@@ -177,7 +176,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
var allowedOrigins []string
var account *model.AgentAccount
var installHooks *appInstallHooks
var hermesAuth hermesDashboardAuth
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
if req.AccountID == 0 {
@@ -198,6 +196,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
}
storedModel = resolvedRuntime.StoredModel
apiType = resolvedRuntime.APIType
maxTokens = resolvedRuntime.MaxTokens
contextWindow = resolvedRuntime.ContextWindow
runtimeModel = resolvedRuntime.PrimaryModel
apiKey = account.APIKey
accountID = account.ID
@@ -222,13 +222,9 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
},
}
} else if agentType == constant.AppHermesAgent {
hermesAuth = normalizeHermesDashboardAuth(req.DashboardUsername, req.DashboardPassword)
installHooks = &appInstallHooks{
AfterCopyData: func(appInstall *model.AppInstall) error {
if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil {
return err
}
return writeHermesDashboardAuthEnv(path.Join(appInstall.GetPath(), ".env"), hermesAuth, false)
return prepareHermesInstallFiles(appInstall, account, storedModel)
},
}
}
@@ -246,14 +242,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
params["PROVIDER"] = provider
params["MODEL"] = runtimeModel
params["API_TYPE"] = apiType
params["MAX_TOKENS"] = maxTokens
params["CONTEXT_WINDOW"] = contextWindow
params["BASE_URL"] = baseURL
params["API_KEY"] = apiKey
params["OPENCLAW_GATEWAY_TOKEN"] = token
}
if agentType == constant.AppHermesAgent {
params[hermesDashboardUsernameEnvKey] = hermesAuth.Username
params[hermesDashboardPasswordEnvKey] = hermesAuth.Password
}
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
return nil, buserr.New("ErrAgentComposeRequired")
@@ -288,20 +282,22 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
configPath = path.Join(appInstall.GetPath(), "data", "config.yaml")
}
agent := &model.Agent{
Name: req.Name,
Remark: req.Remark,
AgentType: agentType,
Provider: provider,
Model: storedModel,
APIType: apiType,
BaseURL: baseURL,
APIKey: apiKey,
Token: token,
Status: appInstall.Status,
Message: appInstall.Message,
AppInstallID: appInstall.ID,
AccountID: accountID,
ConfigPath: configPath,
Name: req.Name,
Remark: req.Remark,
AgentType: agentType,
Provider: provider,
Model: storedModel,
APIType: apiType,
MaxTokens: maxTokens,
ContextWindow: contextWindow,
BaseURL: baseURL,
APIKey: apiKey,
Token: token,
Status: appInstall.Status,
Message: appInstall.Message,
AppInstallID: appInstall.ID,
AccountID: accountID,
ConfigPath: configPath,
}
if err := agentRepo.Create(agent); err != nil {
return nil, err
@@ -399,7 +395,7 @@ func (a AgentService) BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dt
results = append(results, result)
continue
}
installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchTaskID(req.TaskID, "batch-skill-install", agent.ID), agent.ID)
installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchSkillInstallTaskID(req.TaskID, agent.ID), agent.ID)
if err != nil {
result.Message = err.Error()
results = append(results, result)
@@ -441,7 +437,7 @@ func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBat
if operate == constant.Delete {
if err := a.Delete(dto.AgentDeleteReq{
ID: agent.ID,
TaskID: buildBatchTaskID(req.TaskID, "batch-operate", agent.ID),
TaskID: buildBatchOperateTaskID(req.TaskID, agent.ID),
ForceDelete: req.ForceDelete,
}); err != nil {
result.Message = err.Error()
@@ -469,7 +465,7 @@ func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBat
if err := NewIAppInstalledService().Operate(request.AppInstalledOperate{
InstallId: install.ID,
Operate: operate,
TaskID: buildBatchTaskID(req.TaskID, "batch-operate", agent.ID),
TaskID: buildBatchOperateTaskID(req.TaskID, agent.ID),
}); err != nil {
result.Message = err.Error()
} else {
@@ -553,19 +549,35 @@ func buildBatchUpgradePlans(req dto.AgentBatchUpgradeReq) ([]batchUpgradePlan, [
DetailID: detail.ID,
Backup: req.Backup,
PullImage: req.PullImage,
TaskID: buildBatchTaskID(req.TaskID, "batch-upgrade", install.ID),
TaskID: buildBatchUpgradeTaskID(req.TaskID, install.ID),
},
})
}
return plans, results, nil
}
func buildBatchTaskID(taskID, prefix string, id uint) string {
func buildBatchUpgradeTaskID(taskID string, appInstallID uint) string {
taskID = strings.TrimSpace(taskID)
if taskID == "" {
taskID = fmt.Sprintf("%s-%d-%d", prefix, id, time.Now().UnixNano())
taskID = fmt.Sprintf("batch-upgrade-%d-%d", appInstallID, time.Now().UnixNano())
}
return fmt.Sprintf("%s-%d", taskID, id)
return fmt.Sprintf("%s-%d", taskID, appInstallID)
}
func buildBatchSkillInstallTaskID(taskID string, agentID uint) string {
taskID = strings.TrimSpace(taskID)
if taskID == "" {
taskID = fmt.Sprintf("batch-skill-install-%d-%d", agentID, time.Now().UnixNano())
}
return fmt.Sprintf("%s-%d", taskID, agentID)
}
func buildBatchOperateTaskID(taskID string, agentID uint) string {
taskID = strings.TrimSpace(taskID)
if taskID == "" {
taskID = fmt.Sprintf("batch-operate-%d-%d", agentID, time.Now().UnixNano())
}
return fmt.Sprintf("%s-%d", taskID, agentID)
}
func batchOperateSkipMessage(operate constant.AppOperate, status string) string {
@@ -592,30 +604,28 @@ func batchOperateSkipMessage(operate constant.AppOperate, status string) string
func buildCreateReqFromBatchInstallReq(req dto.AgentBatchInstallReq) dto.AgentCreateReq {
return dto.AgentCreateReq{
Name: req.Name,
Remark: req.Remark,
AppVersion: req.AppVersion,
WebUIPort: req.WebUIPort,
BridgePort: req.BridgePort,
AllowedOrigins: req.AllowedOrigins,
AgentType: req.AgentType,
Model: req.Model,
AccountID: req.AccountID,
Token: req.Token,
DashboardUsername: req.DashboardUsername,
DashboardPassword: req.DashboardPassword,
TaskID: req.TaskID,
Advanced: req.Advanced,
ContainerName: req.ContainerName,
AllowPort: req.AllowPort,
SpecifyIP: req.SpecifyIP,
RestartPolicy: req.RestartPolicy,
CpuQuota: req.CpuQuota,
MemoryLimit: req.MemoryLimit,
MemoryUnit: req.MemoryUnit,
PullImage: req.PullImage,
EditCompose: req.EditCompose,
DockerCompose: req.DockerCompose,
Name: req.Name,
Remark: req.Remark,
AppVersion: req.AppVersion,
WebUIPort: req.WebUIPort,
BridgePort: req.BridgePort,
AllowedOrigins: req.AllowedOrigins,
AgentType: req.AgentType,
Model: req.Model,
AccountID: req.AccountID,
Token: req.Token,
TaskID: req.TaskID,
Advanced: req.Advanced,
ContainerName: req.ContainerName,
AllowPort: req.AllowPort,
SpecifyIP: req.SpecifyIP,
RestartPolicy: req.RestartPolicy,
CpuQuota: req.CpuQuota,
MemoryLimit: req.MemoryLimit,
MemoryUnit: req.MemoryUnit,
PullImage: req.PullImage,
EditCompose: req.EditCompose,
DockerCompose: req.DockerCompose,
}
}
@@ -694,7 +704,6 @@ func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (u
account.RememberAPIKey = snapshot.RememberAPIKey
account.BaseURL = snapshot.BaseURL
account.APIType = snapshot.APIType
account.AuthMode = snapshot.AuthMode
account.Remark = snapshot.Remark
account.Verified = true
@@ -702,11 +711,6 @@ func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (u
if err != nil {
return 0, err
}
verifyModel, err := resolveAgentAccountVerifyModel(account.Provider, snapshot.VerifyModel, initialModels)
if err != nil {
return 0, err
}
account.VerifyModel = verifyModel
if err := global.DB.Transaction(func(tx *gorm.DB) error {
if account.ID == 0 {
if err := tx.Create(account).Error; err != nil {
@@ -944,7 +948,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
return err
}
modelName := resolvedRuntime.StoredModel
apiType := resolvedRuntime.APIType
apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
confDir := path.Dir(agent.ConfigPath)
if agent.AgentType == constant.AppHermesAgent {
cfg, err := readHermesConfig(agent.ConfigPath)
@@ -965,6 +969,8 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
agent.Provider = account.Provider
agent.Model = modelName
agent.APIType = apiType
agent.MaxTokens = maxTokens
agent.ContextWindow = contextWindow
agent.BaseURL = account.BaseURL
agent.APIKey = account.APIKey
agent.AccountID = account.ID
@@ -978,29 +984,20 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
models := make([]dto.ProviderModelInfo, 0, len(def.Models))
for _, item := range def.Models {
models = append(models, dto.ProviderModelInfo{
ID: item.ID,
Name: item.Name,
ID: item.ID,
Name: item.Name,
ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: append([]string(nil), item.Input...),
})
}
apiTypes := make([]dto.ProviderAPIInfo, 0, len(def.APIConfigs))
for _, item := range def.APIConfigs {
apiTypes = append(apiTypes, dto.ProviderAPIInfo{
APIType: item.APIType,
BaseURL: item.BaseURL,
EditableBaseURL: item.EditableBaseURL,
DefaultAuthMode: item.DefaultAuthMode,
AuthModes: item.AuthModes,
})
}
baseURL, _ := providercatalog.DefaultBaseURL(key)
providers = append(providers, dto.ProviderInfo{
Sort: def.Sort,
Provider: key,
DisplayName: def.DisplayName,
BaseURL: baseURL,
DefaultAPIType: def.DefaultAPIType,
APITypes: apiTypes,
Models: models,
Sort: def.Sort,
Provider: key,
DisplayName: def.DisplayName,
BaseURL: def.DefaultBaseURL,
Models: models,
})
}
sort.Slice(providers, func(i, j int) bool {
@@ -1010,21 +1007,11 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
}
func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
agentAccountMutationMu.Lock()
defer agentAccountMutationMu.Unlock()
provider := req.Provider
if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil {
return err
if exist, _ := agentAccountRepo.GetFirst(repo.WithByProvider(provider), repo.WithByName(req.Name)); exist != nil && exist.ID > 0 {
return buserr.New("ErrRecordExist")
}
initialModels, err := buildInitialAgentAccountModels(&model.AgentAccount{Provider: provider}, req.Models)
if err != nil {
return err
}
verifyModel, err := resolveAgentAccountVerifyModel(provider, req.VerifyModel, initialModels)
if err != nil {
return err
}
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel)
resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL)
if err != nil {
return err
}
@@ -1034,12 +1021,14 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
APIKey: resolvedInput.APIKey,
RememberAPIKey: req.RememberAPIKey,
BaseURL: resolvedInput.BaseURL,
APIType: resolvedInput.APIType,
AuthMode: resolvedInput.AuthMode,
VerifyModel: verifyModel,
APIType: req.APIType,
Verified: true,
Remark: req.Remark,
}
initialModels, err := buildInitialAgentAccountModels(account, req.Models)
if err != nil {
return err
}
if err := global.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(account).Error; err != nil {
return err
@@ -1056,29 +1045,12 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
}
func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
agentAccountMutationMu.Lock()
defer agentAccountMutationMu.Unlock()
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return err
}
provider := account.Provider
if err := ensureAgentAccountNameAvailable(provider, req.Name, account.ID); err != nil {
return err
}
models, err := loadAgentAccountModels(account)
if err != nil {
return err
}
requestedVerifyModel := req.VerifyModel
if strings.TrimSpace(requestedVerifyModel) == "" {
requestedVerifyModel = account.VerifyModel
}
verifyModel, err := resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
if err != nil {
return err
}
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel)
resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL)
if err != nil {
return err
}
@@ -1086,9 +1058,7 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
account.APIKey = resolvedInput.APIKey
account.RememberAPIKey = req.RememberAPIKey
account.BaseURL = resolvedInput.BaseURL
account.APIType = resolvedInput.APIType
account.AuthMode = resolvedInput.AuthMode
account.VerifyModel = verifyModel
account.APIType = req.APIType
account.Remark = req.Remark
account.Verified = true
@@ -1134,33 +1104,17 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
BaseURL: item.BaseURL,
Models: nil,
APIType: item.APIType,
AuthMode: item.AuthMode,
VerifyModel: item.VerifyModel,
Verified: item.Verified,
Remark: item.Remark,
CreatedAt: item.CreatedAt,
})
}
if len(list) > 0 {
accountIDs := make([]uint, 0, len(list))
for _, account := range list {
accountIDs = append(accountIDs, account.ID)
}
var rows []model.AgentAccountModel
if err := global.DB.Where("account_id IN ?", accountIDs).Order("account_id ASC, sort_order ASC, id ASC").Find(&rows).Error; err != nil {
for i := range items {
models, err := loadAgentAccountModels(&list[i])
if err != nil {
return 0, nil, err
}
modelsByAccount := make(map[uint][]dto.AgentAccountModel, len(list))
for _, row := range rows {
modelsByAccount[row.AccountID] = append(modelsByAccount[row.AccountID], dto.AgentAccountModel{
RecordID: row.ID,
ID: strings.TrimSpace(row.Model),
Name: strings.TrimSpace(row.Name),
})
}
for index, account := range list {
items[index].Models = modelsByAccount[account.ID]
}
items[i].Models = models
}
return count, items, nil
}
@@ -1177,21 +1131,6 @@ func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.Agen
return loadAgentAccountModels(account)
}
func (a AgentService) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) {
if req.APIType != "openai-completions" && req.APIType != "openai-responses" {
return nil, buserr.New("ErrAgentAccountModelsRequired")
}
baseURL, err := providercatalog.ResolveBaseURL(req.Provider, req.APIType, req.BaseURL)
if err != nil {
return nil, buserr.WithErr("ErrAgentAccountUnavailable", err)
}
models, err := providercatalog.DiscoverModels(baseURL, req.APIKey)
if err != nil {
return nil, buserr.WithErr("ErrAgentAccountUnavailable", err)
}
return buildDiscoveredAgentAccountModels(models), nil
}
func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) error {
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
if err != nil {
@@ -1201,19 +1140,24 @@ func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) err
if err != nil {
return err
}
nextModel, err := normalizeAgentAccountModel(account, req.Model)
if err != nil {
return err
}
nextModel := cloneAgentAccountModel(req.Model)
if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok {
return buserr.New("ErrRecordExist")
}
inputPayload, err := json.Marshal(nextModel.Input)
if err != nil {
return err
}
sortOrder := len(models) + 1
record := &model.AgentAccountModel{
AccountID: account.ID,
Model: nextModel.ID,
Name: nextModel.Name,
SortOrder: sortOrder,
AccountID: account.ID,
Model: nextModel.ID,
Name: nextModel.Name,
ContextWindow: nextModel.ContextWindow,
MaxTokens: nextModel.MaxTokens,
Reasoning: nextModel.Reasoning,
Input: string(inputPayload),
SortOrder: sortOrder,
}
if err := agentAccountModelRepo.Create(record); err != nil {
return err
@@ -1234,11 +1178,7 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
if err != nil {
return err
}
nextModel, err := normalizeAgentAccountModel(account, req.Model)
if err != nil {
return err
}
nextModel.RecordID = req.Model.RecordID
nextModel := cloneAgentAccountModel(req.Model)
for _, item := range models {
if item.RecordID == req.Model.RecordID {
continue
@@ -1258,17 +1198,18 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
return err
}
previousModelID := record.Model
record.Model = nextModel.ID
record.Name = nextModel.Name
if err := agentAccountModelRepo.Save(record); err != nil {
inputPayload, err := json.Marshal(nextModel.Input)
if err != nil {
return err
}
if sameProviderModelID(account.Provider, account.VerifyModel, previousModelID) {
account.VerifyModel = nextModel.ID
if err := agentAccountRepo.Save(account); err != nil {
return err
}
record.Model = nextModel.ID
record.Name = nextModel.Name
record.ContextWindow = nextModel.ContextWindow
record.MaxTokens = nextModel.MaxTokens
record.Reasoning = nextModel.Reasoning
record.Input = string(inputPayload)
if err := agentAccountModelRepo.Save(record); err != nil {
return err
}
terminalai.InvalidateTerminalRuntimeCache()
terminalai.InvalidateFileAIRuntimeCache()
@@ -1280,13 +1221,9 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err
if err != nil {
return err
}
record, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID))
if err != nil {
if _, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID)); err != nil {
return err
}
if sameProviderModelID(account.Provider, account.VerifyModel, record.Model) {
return buserr.New("ErrAgentVerifyModelInUse")
}
models, err := loadAgentAccountModels(account)
if err != nil {
return err
@@ -1320,45 +1257,25 @@ 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)
_, err := resolveAgentAccountInput(req.Provider, req.APIKey, req.BaseURL)
return err
}
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
agentAccountMutationMu.Lock()
defer agentAccountMutationMu.Unlock()
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 {
return err
}
if agentCount > 0 {
return buserr.New("ErrAgentAccountBound")
}
used, err := agentAccountUsedBySetting(tx, req.ID, "AIStatus", "AIAccountID")
if err != nil {
return err
}
if used {
if exists, _ := agentRepo.GetFirst(repo.WithByAccountID(req.ID)); exists != nil && exists.ID > 0 {
return buserr.New("ErrAgentAccountBound")
}
if aiStatus, _ := settingRepo.GetValueByKey("AIStatus"); strings.EqualFold(strings.TrimSpace(aiStatus), constant.StatusEnable) {
if aiAccountID, _ := settingRepo.GetValueByKey("AIAccountID"); strings.TrimSpace(aiAccountID) == strconv.FormatUint(uint64(req.ID), 10) {
return buserr.New("ErrTerminalAIAccountInUse")
}
used, err = agentAccountUsedBySetting(tx, req.ID, "FileAIStatus", "FileAIAccountID")
if err != nil {
return err
}
if used {
return buserr.New("ErrFileAIAccountInUse")
}
if err := tx.Where("account_id = ?", req.ID).Delete(&model.AgentAccountModel{}).Error; err != nil {
return err
}
return tx.Delete(&model.AgentAccount{}, req.ID).Error
}); err != nil {
}
if err := agentAccountModelRepo.Delete(repo.WithByAccountID(req.ID)); err != nil {
return err
}
terminalai.InvalidateTerminalRuntimeCache()
terminalai.InvalidateFileAIRuntimeCache()
return nil
return agentAccountRepo.DeleteByID(req.ID)
}
func (a AgentService) GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error) {
@@ -1410,13 +1327,10 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
if err != nil {
return nil, err
}
auth := readHermesDashboardAuthFromInstall(install)
return &dto.AgentOtherConfig{
UserTimezone: cfg.Timezone,
BrowserEnabled: true,
NPMRegistry: "https://registry.npmjs.org/",
DashboardUsername: auth.Username,
DashboardPassword: auth.Password,
UserTimezone: cfg.Timezone,
BrowserEnabled: true,
NPMRegistry: "https://registry.npmjs.org/",
}, nil
}
conf, err := readOpenclawConfig(agent.ConfigPath)
@@ -1441,21 +1355,12 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
if err != nil {
return err
}
previousAuth := readHermesDashboardAuthFromInstall(install)
nextAuth := normalizeHermesDashboardAuth(req.DashboardUsername, req.DashboardPassword)
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
return err
}
if err := writeHermesDashboardAuthEnv(path.Join(install.GetPath(), ".env"), nextAuth, true); err != nil {
return err
}
operate := constant.Restart
if previousAuth.Username != nextAuth.Username || previousAuth.Password != nextAuth.Password {
operate = constant.Rebuild
}
return NewIAppInstalledService().Operate(request.AppInstalledOperate{
InstallId: install.ID,
Operate: operate,
Operate: constant.Restart,
})
}
if err := ensureContainerRunning(install.ContainerName); err != nil {
@@ -1629,7 +1534,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
return err
}
modelName := resolvedRuntime.StoredModel
apiType := resolvedRuntime.APIType
apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
confDir := path.Dir(agent.ConfigPath)
switch agent.AgentType {
case constant.AppOpenclaw:
@@ -1657,6 +1562,8 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
agent.Provider = account.Provider
agent.Model = modelName
agent.APIType = apiType
agent.MaxTokens = maxTokens
agent.ContextWindow = contextWindow
_ = agentRepo.Save(&agent)
}
return nil
+6 -27
View File
@@ -811,7 +811,9 @@ func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig
result.RequireMention = result.GroupPolicy == "allowlist"
result.GroupAllowFrom = extractStringList(telegram["groupAllowFrom"])
result.Proxy = extractStringValue(telegram["proxy"])
result.Streaming = normalizeTelegramStreamingMode(telegram["streaming"], result.Streaming)
if streaming := extractStringValue(telegram["streaming"]); streaming != "" {
result.Streaming = streaming
}
accounts := childMap(telegram, "accounts")
if len(accounts) == 0 {
botToken := extractStringValue(telegram["botToken"])
@@ -837,7 +839,7 @@ func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig
BotToken: extractStringValue(account["botToken"]),
DmPolicy: extractStringValue(account["dmPolicy"]),
GroupPolicy: extractStringValue(account["groupPolicy"]),
Streaming: normalizeTelegramStreamingMode(account["streaming"], result.Streaming),
Streaming: extractStringValue(account["streaming"]),
})
}
result.DefaultAccount = normalizeDefaultAccount(extractStringValue(telegram["defaultAccount"]), getTelegramBotAccountIDs(bots))
@@ -877,7 +879,7 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
} else {
delete(telegram, "proxy")
}
telegram["streaming"] = buildTelegramStreamingConfig(config.Streaming)
telegram["streaming"] = config.Streaming
accounts := make(map[string]interface{}, len(config.Bots))
for _, bot := range config.Bots {
account := map[string]interface{}{
@@ -886,7 +888,7 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
"botToken": bot.BotToken,
"dmPolicy": bot.DmPolicy,
"groupPolicy": bot.GroupPolicy,
"streaming": buildTelegramStreamingConfig(bot.Streaming),
"streaming": bot.Streaming,
}
if bot.DmPolicy == "open" {
account["allowFrom"] = []string{"*"}
@@ -897,29 +899,6 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
delete(telegram, "botToken")
}
func normalizeTelegramStreamingMode(value interface{}, defaultMode string) string {
mode := defaultMode
switch typed := value.(type) {
case string:
mode = typed
case map[string]interface{}:
mode = extractStringValue(typed["mode"])
}
mode = strings.ToLower(strings.TrimSpace(mode))
switch mode {
case "off", "partial", "block", "progress":
return mode
default:
return "partial"
}
}
func buildTelegramStreamingConfig(mode string) map[string]interface{} {
return map[string]interface{}{
"mode": normalizeTelegramStreamingMode(mode, "partial"),
}
}
func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig {
result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", AllowFrom: []string{}, RequireMention: false, GroupPolicy: "open"}
discord := getChannelConfig(conf, "discord")
+68 -84
View File
@@ -13,19 +13,14 @@ import (
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/common"
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/joho/godotenv"
"gopkg.in/yaml.v3"
)
const hermesWorkspaceDir = "/opt/data/workspace"
const hermesExecutablePath = "/opt/hermes/.venv/bin/hermes"
const hermesDashboardUsernameEnvKey = "HERMES_DASHBOARD_USERNAME"
const hermesDashboardPasswordEnvKey = "HERMES_DASHBOARD_PASSWORD"
type hermesDashboardAuth struct {
Username string
Password string
}
type hermesConfig struct {
Model hermesModelConfig `yaml:"model"`
@@ -76,7 +71,7 @@ func writeHermesConfig(confDir string, account *model.AgentAccount, modelName st
}
}
provider := resolveHermesProvider(account.Provider, account.APIType)
provider := resolveHermesProvider(account.Provider)
configPath := path.Join(confDir, "config.yaml")
cfg, err := readHermesConfigMap(configPath)
if err != nil {
@@ -117,52 +112,6 @@ func prepareHermesInstallFiles(appInstall *model.AppInstall, account *model.Agen
return files.NewFileOp().ChownR(dataDir, "1000", "1000", true)
}
func normalizeHermesDashboardAuth(username, password string) hermesDashboardAuth {
auth := hermesDashboardAuth{
Username: strings.TrimSpace(username),
Password: strings.TrimSpace(password),
}
if auth.Username == "" {
auth.Username = "admin"
}
if auth.Password == "" {
auth.Password = common.RandStr(8)
}
return auth
}
func writeHermesDashboardAuthEnv(envPath string, auth hermesDashboardAuth, overwrite bool) error {
return upsertAgentEnv(envPath, map[string]string{
hermesDashboardUsernameEnvKey: auth.Username,
hermesDashboardPasswordEnvKey: auth.Password,
}, []string{
hermesDashboardUsernameEnvKey,
hermesDashboardPasswordEnvKey,
}, overwrite)
}
func readHermesDashboardAuthEnv(envPath string) (hermesDashboardAuth, error) {
envMap, err := readAgentEnvMap(envPath)
if err != nil {
return hermesDashboardAuth{}, err
}
return hermesDashboardAuth{
Username: strings.TrimSpace(envMap[hermesDashboardUsernameEnvKey]),
Password: strings.TrimSpace(envMap[hermesDashboardPasswordEnvKey]),
}, nil
}
func readHermesDashboardAuthFromInstall(appInstall *model.AppInstall) hermesDashboardAuth {
if appInstall == nil || appInstall.ID == 0 {
return hermesDashboardAuth{}
}
auth, err := readHermesDashboardAuthEnv(path.Join(appInstall.GetPath(), ".env"))
if err != nil {
return hermesDashboardAuth{}
}
return auth
}
func readHermesConfig(configPath string) (*hermesConfig, error) {
content, err := files.NewFileOp().GetContent(configPath)
if err != nil {
@@ -203,7 +152,7 @@ func writeHermesConfigMap(configPath string, cfg map[string]interface{}) error {
}
func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil {
return nil, err
}
@@ -260,7 +209,7 @@ func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig,
func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramConfig) error {
envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath)
envMap, err := readHermesEnvMap(envPath)
if err != nil {
return err
}
@@ -280,7 +229,7 @@ func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramCo
envMap["TELEGRAM_ALLOWED_USERS"] = allow
}
}
if err := writeAgentEnvMap(envPath, envMap, []string{
if err := writeHermesEnvMap(envPath, envMap, []string{
"TELEGRAM_BOT_TOKEN",
"TELEGRAM_ALLOWED_USERS",
"TELEGRAM_ALLOW_ALL_USERS",
@@ -299,7 +248,7 @@ func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramCo
}
func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil {
return nil, err
}
@@ -351,7 +300,7 @@ func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, er
func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConfig) error {
envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath)
envMap, err := readHermesEnvMap(envPath)
if err != nil {
return err
}
@@ -371,7 +320,7 @@ func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConf
envMap["DISCORD_ALLOWED_USERS"] = allow
}
}
if err := writeAgentEnvMap(envPath, envMap, []string{
if err := writeHermesEnvMap(envPath, envMap, []string{
"DISCORD_BOT_TOKEN",
"DISCORD_ALLOWED_USERS",
"DISCORD_ALLOW_ALL_USERS",
@@ -391,14 +340,14 @@ func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConf
func deleteHermesEnvKeys(confDir string, keys ...string) error {
envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath)
envMap, err := readHermesEnvMap(envPath)
if err != nil {
return err
}
for _, key := range keys {
delete(envMap, key)
}
return writeAgentEnvMap(envPath, envMap, keys)
return writeHermesEnvMap(envPath, envMap, keys)
}
func deleteHermesConfigSections(confDir string, topLevelKeys []string, platformKeys []string) error {
@@ -458,18 +407,11 @@ func normalizeHermesTimezone(timezone string) string {
return timezone
}
func resolveHermesProvider(provider, apiType string) string {
if apiType == "anthropic-messages" && (provider == "deepseek" || provider == "bailian-coding-plan" || provider == "ark-coding-plan" || provider == "xiaomi") {
return "anthropic"
}
func resolveHermesProvider(provider string) string {
switch provider {
case "":
return "custom"
case "moonshot":
return "kimi-coding"
case "kimi":
return "kimi-coding-cn"
case "openrouter", "anthropic", "deepseek", "gemini", "zai", "kimi-coding", "xiaomi":
case "openrouter", "anthropic", "gemini", "zai", "kimi-coding", "xiaomi":
return provider
case "minimax":
return "minimax-cn"
@@ -486,7 +428,17 @@ func resolveHermesModel(sourceProvider, targetProvider, modelName string) string
if targetProvider != "custom" {
return target
}
return providercatalog.NormalizeModelID(sourceProvider, target)
if sourceProvider == "custom" || sourceProvider == "vllm" {
return normalizeCustomModel(target)
}
if strings.Contains(target, "/") {
parts := strings.SplitN(target, "/", 2)
model := strings.TrimSpace(parts[1])
if model != "" {
return model
}
}
return target
}
func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) (string, error) {
@@ -497,7 +449,7 @@ func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels [
if configuredModel == "" {
return "", buserr.New("ErrAgentModelNotInAccount")
}
provider := resolveHermesProvider(account.Provider, account.APIType)
provider := resolveHermesProvider(account.Provider)
for _, item := range accountModels {
if resolveHermesModel(account.Provider, provider, item.ID) == configuredModel {
return item.ID, nil
@@ -510,8 +462,7 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
if account == nil {
return nil
}
resolvedProvider := resolveHermesProvider(account.Provider, account.APIType)
if resolvedProvider == "custom" {
if resolveHermesProvider(account.Provider) == "custom" {
if account.APIKey == "" {
return nil
}
@@ -529,11 +480,6 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
}
entries = append(entries, hermesEnvEntry{Key: key, Value: value})
}
if resolvedProvider == "anthropic" {
appendEntry("ANTHROPIC_API_KEY", apiKey)
appendEntry("ANTHROPIC_BASE_URL", baseURL)
return entries
}
switch account.Provider {
case "openrouter":
@@ -541,7 +487,6 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
appendEntry("OPENROUTER_BASE_URL", baseURL)
case "anthropic":
appendEntry("ANTHROPIC_API_KEY", apiKey)
appendEntry("ANTHROPIC_BASE_URL", baseURL)
case "gemini":
appendEntry("GOOGLE_API_KEY", apiKey)
appendEntry("GEMINI_API_KEY", apiKey)
@@ -587,7 +532,7 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
}
func writeHermesModelEnv(envPath string, account *model.AgentAccount) error {
envMap, err := readAgentEnvMap(envPath)
envMap, err := readHermesEnvMap(envPath)
if err != nil {
return err
}
@@ -603,7 +548,7 @@ func writeHermesModelEnv(envPath string, account *model.AgentAccount) error {
envMap[entry.Key] = entry.Value
order = append(order, entry.Key)
}
return writeAgentEnvMap(envPath, envMap, order)
return writeHermesEnvMap(envPath, envMap, order)
}
func hermesManagedModelEnvKeys() []string {
@@ -611,7 +556,6 @@ func hermesManagedModelEnvKeys() []string {
"OPENROUTER_API_KEY",
"OPENROUTER_BASE_URL",
"ANTHROPIC_API_KEY",
"ANTHROPIC_BASE_URL",
"GOOGLE_API_KEY",
"GEMINI_API_KEY",
"GEMINI_BASE_URL",
@@ -654,6 +598,46 @@ func hermesManagedModelEnvKeys() []string {
return result
}
func writeHermesEnv(envPath string, entries []hermesEnvEntry) error {
envMap, err := readHermesEnvMap(envPath)
if err != nil {
return err
}
for _, entry := range entries {
if entry.Key == "" || entry.Value == "" {
continue
}
envMap[entry.Key] = entry.Value
}
order := make([]string, 0, len(entries))
for _, entry := range entries {
if entry.Key == "" {
continue
}
order = append(order, entry.Key)
}
return writeHermesEnvMap(envPath, envMap, order)
}
func readHermesEnvMap(envPath string) (map[string]string, error) {
fileOp := files.NewFileOp()
if !fileOp.Stat(envPath) {
return map[string]string{}, nil
}
envMap, err := godotenv.Read(envPath)
if err != nil {
return nil, err
}
return envMap, nil
}
func writeHermesEnvMap(envPath string, envMap map[string]string, order []string) error {
if len(envMap) == 0 {
return files.NewFileOp().SaveFile(envPath, "", 0600)
}
return agentenv.WriteWithOrder(envMap, envPath, order)
}
func splitHermesEnvList(value string) []string {
if value == "" {
return []string{}
+13 -13
View File
@@ -34,7 +34,7 @@ print('Restart the Hermes-Agent container to apply the new Weixin settings.')
`
func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil {
return nil, err
}
@@ -78,7 +78,7 @@ func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error)
func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig) error {
envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath)
envMap, err := readHermesEnvMap(envPath)
if err != nil {
return err
}
@@ -102,7 +102,7 @@ func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig)
envMap["QQ_GROUP_ALLOWED_USERS"] = allow
}
}
if err := writeAgentEnvMap(envPath, envMap, []string{
if err := writeHermesEnvMap(envPath, envMap, []string{
"QQ_APP_ID",
"QQ_CLIENT_SECRET",
"QQ_ALLOW_ALL_USERS",
@@ -163,7 +163,7 @@ func deleteHermesQQBotChannelConfig(confDir string) error {
}
func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil {
return nil, err
}
@@ -194,7 +194,7 @@ func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error)
func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig) error {
envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath)
envMap, err := readHermesEnvMap(envPath)
if err != nil {
return err
}
@@ -229,7 +229,7 @@ func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig)
envMap["WECOM_GROUP_ALLOWED_USERS"] = allow
}
}
if err := writeAgentEnvMap(envPath, envMap, []string{
if err := writeHermesEnvMap(envPath, envMap, []string{
"WECOM_BOT_ID",
"WECOM_SECRET",
"WECOM_DM_POLICY",
@@ -282,7 +282,7 @@ func deleteHermesWecomChannelConfig(confDir string) error {
}
func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil {
return nil, err
}
@@ -332,7 +332,7 @@ func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig,
func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkConfig) error {
envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath)
envMap, err := readHermesEnvMap(envPath)
if err != nil {
return err
}
@@ -356,7 +356,7 @@ func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkCo
} else if config.DmPolicy == "open" {
envMap["DINGTALK_ALLOW_ALL_USERS"] = "true"
}
if err := writeAgentEnvMap(envPath, envMap, []string{
if err := writeHermesEnvMap(envPath, envMap, []string{
"DINGTALK_CLIENT_ID",
"DINGTALK_CLIENT_SECRET",
"DINGTALK_ALLOW_ALL_USERS",
@@ -398,7 +398,7 @@ func deleteHermesDingTalkChannelConfig(confDir string) error {
}
func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil {
return nil, err
}
@@ -452,7 +452,7 @@ func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, erro
func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig) error {
envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath)
envMap, err := readHermesEnvMap(envPath)
if err != nil {
return err
}
@@ -488,7 +488,7 @@ func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig
envMap["FEISHU_ALLOWED_USERS"] = allow
}
}
if err := writeAgentEnvMap(envPath, envMap, []string{
if err := writeHermesEnvMap(envPath, envMap, []string{
"FEISHU_APP_ID",
"FEISHU_APP_SECRET",
"FEISHU_DOMAIN",
@@ -529,7 +529,7 @@ func deleteHermesFeishuChannelConfig(confDir string) error {
}
func readHermesWeixinChannelConfig(confDir string) (*dto.AgentWeixinConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil {
return nil, err
}
+267 -179
View File
@@ -4,12 +4,10 @@ import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"path"
"regexp"
"strconv"
"strings"
"time"
@@ -22,10 +20,8 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/common"
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
"github.com/joho/godotenv"
"gorm.io/gorm"
)
@@ -39,39 +35,6 @@ type resolvedAgentAccountInput struct {
Provider string
APIKey string
BaseURL string
APIType string
AuthMode string
}
func ensureAgentAccountNameAvailable(provider, name string, excludeID uint) error {
opts := []repo.DBOption{repo.WithByProvider(provider), repo.WithByName(name)}
if excludeID > 0 {
opts = append(opts, repo.WithByNOTID(excludeID))
}
account, err := agentAccountRepo.GetFirst(opts...)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
if account != nil && account.ID > 0 {
return buserr.New("ErrRecordExist")
}
return nil
}
func agentAccountUsedBySetting(tx *gorm.DB, accountID uint, statusKey, accountIDKey string) (bool, error) {
var settings []model.Setting
if err := tx.Where("key IN ?", []string{statusKey, accountIDKey}).Find(&settings).Error; err != nil {
return false, err
}
values := make(map[string]string, len(settings))
for _, setting := range settings {
values[setting.Key] = setting.Value
}
return strings.EqualFold(strings.TrimSpace(values[statusKey]), constant.StatusEnable) &&
strings.TrimSpace(values[accountIDKey]) == strconv.FormatUint(uint64(accountID), 10), nil
}
func loadOpenclawAgentByID(agentID uint) (*model.Agent, error) {
@@ -96,26 +59,19 @@ func ensureContainerRunning(containerName string) error {
return nil
}
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string) (resolvedAgentAccountInput, error) {
func resolveAgentAccountInput(provider, apiKey, baseURL string) (resolvedAgentAccountInput, error) {
resolvedAPIKey := strings.TrimSpace(apiKey)
resolvedAPIType := strings.TrimSpace(apiType)
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
if err != nil {
return resolvedAgentAccountInput{}, err
}
resolvedBaseURL, err := providercatalog.ResolveBaseURL(provider, resolvedAPIType, baseURL)
if err != nil {
if strings.Contains(err.Error(), "base url is required") {
resolvedBaseURL := strings.TrimSpace(baseURL)
if resolvedBaseURL == "" {
if requiresInitialAgentAccountModels(provider) {
return resolvedAgentAccountInput{}, buserr.New("ErrAgentBaseURLRequired")
}
return resolvedAgentAccountInput{}, err
}
modelID = strings.TrimSpace(modelID)
if modelID == "" {
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
if defaultURL, ok := providercatalog.DefaultBaseURL(provider); ok {
resolvedBaseURL = defaultURL
}
}
if !providercatalog.SkipVerification(provider) {
if err := providercatalog.VerifyAccount(provider, resolvedAPIType, resolvedAuthMode, resolvedBaseURL, resolvedAPIKey, modelID); err != nil {
if err := providercatalog.VerifyAccount(provider, resolvedBaseURL, resolvedAPIKey); err != nil {
return resolvedAgentAccountInput{}, err
}
}
@@ -123,8 +79,6 @@ func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, mode
Provider: provider,
APIKey: resolvedAPIKey,
BaseURL: resolvedBaseURL,
APIType: resolvedAPIType,
AuthMode: resolvedAuthMode,
}, nil
}
@@ -381,24 +335,26 @@ func setOtherConfig(conf map[string]interface{}, config dto.AgentOtherConfig) {
func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem {
agentType := agent.AgentType
item := dto.AgentItem{
ID: agent.ID,
Name: agent.Name,
Remark: agent.Remark,
AgentType: agentType,
Provider: agent.Provider,
ProviderName: providercatalog.DisplayName(agent.Provider),
Model: agent.Model,
APIType: agent.APIType,
BaseURL: agent.BaseURL,
APIKey: maskKey(agent.APIKey),
Token: agent.Token,
Status: agent.Status,
Message: agent.Message,
AppInstallID: agent.AppInstallID,
WebsiteID: agent.WebsiteID,
AccountID: agent.AccountID,
ConfigPath: agent.ConfigPath,
CreatedAt: agent.CreatedAt,
ID: agent.ID,
Name: agent.Name,
Remark: agent.Remark,
AgentType: agentType,
Provider: agent.Provider,
ProviderName: providercatalog.DisplayName(agent.Provider),
Model: agent.Model,
APIType: agent.APIType,
MaxTokens: agent.MaxTokens,
ContextWindow: agent.ContextWindow,
BaseURL: agent.BaseURL,
APIKey: maskKey(agent.APIKey),
Token: agent.Token,
Status: agent.Status,
Message: agent.Message,
AppInstallID: agent.AppInstallID,
WebsiteID: agent.WebsiteID,
AccountID: agent.AccountID,
ConfigPath: agent.ConfigPath,
CreatedAt: agent.CreatedAt,
}
if appInstall != nil && appInstall.ID > 0 {
item.Container = appInstall.ContainerName
@@ -420,11 +376,6 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
item.BridgePort = toInt(bridge)
}
}
if agentType == constant.AppHermesAgent {
auth := readHermesDashboardAuthFromInstall(appInstall)
item.DashboardUsername = auth.Username
item.DashboardPassword = auth.Password
}
}
return item
}
@@ -726,9 +677,13 @@ 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"`
Reasoning bool `json:"reasoning"`
Input []string `json:"input"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Cost modelCost `json:"cost"`
}
func requiresOpenclawProviderModels(provider string) bool {
@@ -748,6 +703,13 @@ func applyOpenclawModelsConfig(conf map[string]interface{}, models *modelsConfig
return nil
}
type modelCost struct {
Input float64 `json:"input"`
Output float64 `json:"output"`
CacheRead float64 `json:"cacheRead"`
CacheWrite float64 `json:"cacheWrite"`
}
type browserConfig struct {
Enabled bool `json:"enabled"`
ExecutablePath string `json:"executablePath"`
@@ -898,15 +860,13 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
return err
}
envMap := map[string]string{
"OPENCLAW_GATEWAY_TOKEN": token,
}
order := []string{"OPENCLAW_GATEWAY_TOKEN"}
envPath := path.Join(confDir, ".env")
lines := []string{fmt.Sprintf("OPENCLAW_GATEWAY_TOKEN=%s", token)}
if envKey := providercatalog.EnvKey(account.Provider); envKey != "" && account.APIKey != "" {
envMap[envKey] = account.APIKey
order = append(order, envKey)
lines = append(lines, fmt.Sprintf("%s=%s", envKey, account.APIKey))
}
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order)
content := strings.Join(lines, "\n") + "\n"
return fileOp.SaveFile(envPath, content, 0600)
}
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
@@ -1093,7 +1053,7 @@ func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel s
}
func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) {
providerPatch, err := providercatalog.BuildOpenClawProviderPatch(account.Provider, model.ID, account.APIType, account.AuthMode, account.BaseURL, account.APIKey)
providerPatch, err := providercatalog.BuildOpenClawProviderPatch(account.Provider, model.ID, account.APIType, account.BaseURL, account.APIKey)
if err != nil {
return "", modelEntry{}, "", modelProvider{}, err
}
@@ -1105,24 +1065,28 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen
}, nil
}
var openclawVisionModelPattern = regexp.MustCompile(`(?i)(\b(gpt-4o|gpt-4\.1|gpt-[5-9]|o[134])\b|\bclaude-(3|4|sonnet|opus|haiku)\b|\bgemini\b|\b(qwen[\w.-]*-?vl|qwen-vl|qwen3\.[5-9]-plus)\b|\b(kimi-k2\.(5|6)|kimi-k2\.7-code|minimax-m3)\b|\b(vision|llava|pixtral|internvl|mllama|minicpm-v|glm-4v|omni)\b|(^|[-_/])vl([-_/]|$))`)
func buildOpenclawModelEntry(modelID string, model dto.AgentAccountModel) modelEntry {
name := strings.TrimSpace(model.Name)
if name == "" {
name = strings.TrimSpace(modelID)
}
entry := modelEntry{ID: strings.TrimSpace(modelID), Name: name}
if openclawVisionModelPattern.MatchString(modelID) {
entry.Input = []string{"text", "image"}
return modelEntry{
ID: strings.TrimSpace(modelID),
Name: name,
Reasoning: model.Reasoning,
Input: sanitizeAgentAccountModelInputs(model.Input),
ContextWindow: model.ContextWindow,
MaxTokens: model.MaxTokens,
Cost: modelCost{},
}
return entry
}
type openclawAccountModelRuntime struct {
StoredModel string
PrimaryModel string
APIType string
StoredModel string
PrimaryModel string
APIType string
MaxTokens int
ContextWindow int
}
func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.AgentAccountModel) (openclawAccountModelRuntime, error) {
@@ -1131,9 +1095,11 @@ func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.Age
return openclawAccountModelRuntime{}, err
}
return openclawAccountModelRuntime{
StoredModel: model.ID,
PrimaryModel: primaryModel,
APIType: account.APIType,
StoredModel: model.ID,
PrimaryModel: primaryModel,
APIType: account.APIType,
MaxTokens: model.MaxTokens,
ContextWindow: model.ContextWindow,
}, nil
}
@@ -1153,11 +1119,15 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto
if account == nil {
return nil, fmt.Errorf("account is required")
}
if account.Provider != "custom" && requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 {
if requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 {
return nil, buserr.New("ErrAgentAccountSingleInitialModel")
}
if len(requested) > 0 {
return normalizeAgentAccountModels(account, requested)
models := make([]dto.AgentAccountModel, 0, len(requested))
for _, item := range requested {
models = append(models, cloneAgentAccountModel(item))
}
return models, nil
}
meta, ok := providercatalog.Get(account.Provider)
if !ok || len(meta.Models) == 0 {
@@ -1169,22 +1139,15 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto
requested = make([]dto.AgentAccountModel, 0, len(meta.Models))
for _, item := range meta.Models {
requested = append(requested, dto.AgentAccountModel{
ID: item.ID,
Name: item.Name,
ID: item.ID,
Name: item.Name,
ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: append([]string(nil), item.Input...),
})
}
return normalizeAgentAccountModels(account, requested)
}
func buildDiscoveredAgentAccountModels(modelIDs []string) []dto.AgentAccountModel {
models := make([]dto.AgentAccountModel, 0, len(modelIDs))
for _, modelID := range modelIDs {
models = append(models, dto.AgentAccountModel{
ID: modelID,
Name: modelID,
})
}
return models
return requested, nil
}
func compactPersistedAgentAccountModelSortOrder(accountID uint) error {
@@ -1212,6 +1175,18 @@ func loadAgentAccountModels(account *model.AgentAccount) ([]dto.AgentAccountMode
return listPersistedAgentAccountModels(account.ID)
}
func cloneAgentAccountModel(model dto.AgentAccountModel) dto.AgentAccountModel {
return dto.AgentAccountModel{
RecordID: model.RecordID,
ID: model.ID,
Name: model.Name,
ContextWindow: model.ContextWindow,
MaxTokens: model.MaxTokens,
Reasoning: model.Reasoning,
Input: append([]string(nil), model.Input...),
}
}
func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, existing []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) {
if account == nil {
return nil, fmt.Errorf("account is required")
@@ -1235,8 +1210,12 @@ func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, exi
continue
}
requested = append(requested, dto.AgentAccountModel{
ID: item.ID,
Name: item.Name,
ID: item.ID,
Name: item.Name,
ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: append([]string(nil), item.Input...),
})
}
if len(requested) == len(existing) {
@@ -1255,10 +1234,18 @@ func listPersistedAgentAccountModels(accountID uint) ([]dto.AgentAccountModel, e
}
result := make([]dto.AgentAccountModel, 0, len(rows))
for _, row := range rows {
inputs := []string{}
if strings.TrimSpace(row.Input) != "" {
_ = json.Unmarshal([]byte(row.Input), &inputs)
}
result = append(result, dto.AgentAccountModel{
RecordID: row.ID,
ID: strings.TrimSpace(row.Model),
Name: strings.TrimSpace(row.Name),
RecordID: row.ID,
ID: strings.TrimSpace(row.Model),
Name: strings.TrimSpace(row.Name),
ContextWindow: row.ContextWindow,
MaxTokens: row.MaxTokens,
Reasoning: row.Reasoning,
Input: sanitizeAgentAccountModelInputs(inputs),
})
}
return result, nil
@@ -1269,11 +1256,19 @@ func replacePersistedAgentAccountModelsWithTx(tx *gorm.DB, accountID uint, model
return err
}
for index, item := range models {
inputPayload, err := json.Marshal(sanitizeAgentAccountModelInputs(item.Input))
if err != nil {
return err
}
record := &model.AgentAccountModel{
AccountID: accountID,
Model: strings.TrimSpace(item.ID),
Name: strings.TrimSpace(item.Name),
SortOrder: index + 1,
AccountID: accountID,
Model: strings.TrimSpace(item.ID),
Name: strings.TrimSpace(item.Name),
ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: string(inputPayload),
SortOrder: index + 1,
}
if err := tx.Create(record).Error; err != nil {
return err
@@ -1314,17 +1309,86 @@ func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAcco
if modelID == "" {
return dto.AgentAccountModel{}, fmt.Errorf("model is required")
}
modelID = providercatalog.NormalizeModelID(account.Provider, modelID)
name := strings.TrimSpace(model.Name)
if name == "" {
name = modelID
}
inputs := sanitizeAgentAccountModelInputs(model.Input)
return dto.AgentAccountModel{
ID: modelID,
Name: name,
ID: normalizeAgentAccountModelID(account.Provider, modelID),
Name: name,
ContextWindow: model.ContextWindow,
MaxTokens: model.MaxTokens,
Reasoning: model.Reasoning,
Input: inputs,
}, nil
}
func normalizeAgentAccountModelID(provider, requestedID string) string {
switch provider {
case "custom", "vllm":
return normalizeCustomModel(requestedID)
case "ollama":
target := strings.TrimSpace(requestedID)
if strings.HasPrefix(target, "ollama/") {
return target
}
target = strings.TrimLeft(strings.TrimSpace(target), "/")
if target == "" {
return ""
}
return "ollama/" + target
default:
target := strings.TrimSpace(requestedID)
if target == "" {
return ""
}
prefix := poolModelPrefix(provider)
if strings.Contains(target, "/") {
parts := strings.SplitN(target, "/", 2)
targetPrefix := parts[0]
targetModel := strings.TrimSpace(parts[1])
if targetModel == "" {
return strings.TrimSpace(target)
}
for _, item := range supportedProviderModelPrefixes(provider) {
if item == targetPrefix {
if prefix != "" {
return prefix + "/" + targetModel
}
return strings.TrimSpace(target)
}
}
return strings.TrimSpace(target)
}
target = strings.TrimLeft(strings.TrimSpace(target), "/")
if prefix == "" {
return target
}
return prefix + "/" + target
}
}
func sanitizeAgentAccountModelInputs(values []string) []string {
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
normalized := value
if normalized != "text" && normalized != "image" {
continue
}
if _, ok := seen[normalized]; ok {
continue
}
seen[normalized] = struct{}{}
result = append(result, normalized)
}
if len(result) == 0 {
return []string{"text"}
}
return result
}
func requiresInitialAgentAccountModels(provider string) bool {
switch provider {
case "custom", "vllm", "ollama":
@@ -1334,14 +1398,36 @@ func requiresInitialAgentAccountModels(provider string) bool {
}
}
func normalizeComparableProviderModelID(provider, modelID string) string {
target := strings.TrimSpace(modelID)
if target == "" {
return ""
}
if !strings.Contains(target, "/") {
return target
}
parts := strings.SplitN(target, "/", 2)
prefix := parts[0]
model := strings.TrimSpace(parts[1])
if model == "" {
return target
}
for _, item := range supportedProviderModelPrefixes(provider) {
if item == prefix {
return model
}
}
return target
}
func sameProviderModelID(provider, left, right string) bool {
leftTrimmed := strings.TrimSpace(left)
rightTrimmed := strings.TrimSpace(right)
if leftTrimmed == rightTrimmed {
return true
}
leftComparable := providercatalog.NormalizeModelID(provider, leftTrimmed)
rightComparable := providercatalog.NormalizeModelID(provider, rightTrimmed)
leftComparable := normalizeComparableProviderModelID(provider, leftTrimmed)
rightComparable := normalizeComparableProviderModelID(provider, rightTrimmed)
return leftComparable != "" && leftComparable == rightComparable
}
@@ -1362,20 +1448,6 @@ func requireAgentAccountModelForProvider(provider string, models []dto.AgentAcco
return selectedAccountModel, nil
}
func resolveAgentAccountVerifyModel(provider, requested string, models []dto.AgentAccountModel) (string, error) {
if len(models) == 0 {
return "", buserr.New("ErrAgentAccountModelsRequired")
}
if strings.TrimSpace(requested) == "" {
return models[0].ID, nil
}
selected, ok := findAgentAccountModelForProvider(provider, models, requested)
if !ok {
return "", buserr.New("ErrAgentModelNotInAccount")
}
return selected.ID, nil
}
func ensureAccountModelsNotBound(account *model.AgentAccount, models []dto.AgentAccountModel) error {
if account == nil || account.ID == 0 {
return nil
@@ -1452,41 +1524,6 @@ func readInstallEnv(envStr string) map[string]interface{} {
return data
}
func readAgentEnvMap(envPath string) (map[string]string, error) {
fileOp := files.NewFileOp()
if !fileOp.Stat(envPath) {
return map[string]string{}, nil
}
envMap, err := godotenv.Read(envPath)
if err != nil {
return nil, err
}
return envMap, nil
}
func writeAgentEnvMap(envPath string, envMap map[string]string, order []string) error {
if len(envMap) == 0 {
return files.NewFileOp().SaveFile(envPath, "", 0600)
}
return agentenv.WriteWithOrder(envMap, envPath, order)
}
func upsertAgentEnv(envPath string, values map[string]string, order []string, overwrite bool) error {
envMap, err := readAgentEnvMap(envPath)
if err != nil {
return err
}
for key, value := range values {
if key == "" {
continue
}
if overwrite || strings.TrimSpace(envMap[key]) == "" {
envMap[key] = value
}
}
return writeAgentEnvMap(envPath, envMap, order)
}
func maskKey(value string) string {
trim := strings.TrimSpace(value)
if len(trim) <= 6 {
@@ -1514,6 +1551,57 @@ func toInt(value interface{}) int {
}
}
func normalizeCustomModel(modelName string) string {
trim := strings.TrimSpace(modelName)
trim = strings.TrimLeft(trim, "/")
if parts := strings.SplitN(trim, "/", 2); len(parts) == 2 {
if strings.EqualFold(parts[0], "custom") {
return strings.TrimLeft(strings.TrimSpace(parts[1]), "/")
}
}
return trim
}
func runtimeProviderModelPrefix(provider string) string {
switch provider {
case "gemini":
return "google"
case "kimi":
return "moonshot"
default:
return provider
}
}
func poolModelPrefix(provider string) string {
meta, ok := providercatalog.Get(provider)
if ok && len(meta.Models) > 0 {
parts := strings.SplitN(strings.TrimSpace(meta.Models[0].ID), "/", 2)
if len(parts) == 2 && strings.TrimSpace(parts[0]) != "" {
return parts[0]
}
}
return provider
}
func supportedProviderModelPrefixes(provider string) []string {
values := []string{poolModelPrefix(provider), runtimeProviderModelPrefix(provider)}
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
target := value
if target == "" {
continue
}
if _, ok := seen[target]; ok {
continue
}
seen[target] = struct{}{}
result = append(result, target)
}
return result
}
func generateToken() string {
bytes := make([]byte, 24)
if _, err := rand.Read(bytes); err != nil {
+5 -1
View File
@@ -162,7 +162,7 @@ func fillAgentWebsiteItems(items []dto.AgentItem, explicitWebsiteMap map[uint]mo
}
}
func UniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]model.Website {
func uniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]model.Website {
websiteMap := make(map[uint]model.Website)
duplicateAppInstallIDs := make(map[uint]struct{})
for _, website := range websites {
@@ -182,6 +182,10 @@ func UniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]m
return websiteMap
}
func UniqueDeploymentWebsiteMapForMigration(websites []model.Website) map[uint]model.Website {
return uniqueDeploymentWebsiteMapByAppInstall(websites)
}
func uniqueUintList(items []uint) []uint {
itemMap := make(map[uint]struct{}, len(items))
uniq := make([]uint, 0, len(items))
-139
View File
@@ -1,139 +0,0 @@
package service
import (
"encoding/json"
"strings"
"github.com/1Panel-dev/1Panel/agent/utils/common"
)
const (
vllmAppKeyForUpgrade = "vllm"
vllmImageEnvKey = "IMAGE"
vllmImageTypeNvidia = "nvidia"
vllmImageTypeIntel = "intel"
vllmImageTypeAscend = "ascend"
)
func resolveVllmVersionFamily(version, image string) string {
normalizedVersion := strings.ToLower(strings.TrimSpace(version))
if strings.HasPrefix(normalizedVersion, vllmImageTypeIntel+"-") {
return vllmImageTypeIntel
}
if strings.HasPrefix(normalizedVersion, vllmImageTypeAscend+"-") {
return vllmImageTypeAscend
}
if strings.HasPrefix(normalizedVersion, vllmImageTypeNvidia+"-") {
return vllmImageTypeNvidia
}
normalizedImage := strings.ToLower(strings.TrimSpace(image))
if strings.Contains(normalizedImage, "intel/") || strings.Contains(normalizedImage, "llm-scaler-vllm") {
return vllmImageTypeIntel
}
if strings.Contains(normalizedImage, "ascend/") || strings.Contains(normalizedImage, "vllm-ascend") {
return vllmImageTypeAscend
}
return vllmImageTypeNvidia
}
func trimVllmVersionFamily(version string) string {
trimmed := strings.TrimSpace(version)
normalized := strings.ToLower(trimmed)
for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel, vllmImageTypeAscend} {
prefix := family + "-"
if strings.HasPrefix(normalized, prefix) {
return strings.TrimSpace(trimmed[len(prefix):])
}
}
return trimmed
}
func buildDefaultVllmImageByVersion(version string) string {
tag := trimVllmVersionFamily(version)
family := resolveVllmVersionFamily(version, "")
if family == vllmImageTypeIntel {
return "intel/llm-scaler-vllm:" + tag
}
if tag != "" && !strings.HasPrefix(strings.ToLower(tag), "v") {
tag = "v" + tag
}
if family == vllmImageTypeAscend {
return "quay.io/ascend/vllm-ascend:" + tag
}
return "vllm/vllm-openai:" + tag
}
func isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage string) bool {
currentFamily := resolveVllmVersionFamily(currentVersion, currentImage)
targetFamily := resolveVllmVersionFamily(targetVersion, "")
return currentFamily == targetFamily
}
func hasVllmVersionFamilyPrefix(version string) bool {
normalized := strings.ToLower(strings.TrimSpace(version))
return strings.HasPrefix(normalized, vllmImageTypeNvidia+"-") ||
strings.HasPrefix(normalized, vllmImageTypeIntel+"-") ||
strings.HasPrefix(normalized, vllmImageTypeAscend+"-")
}
func isVllmUpgradeCandidate(currentVersion, targetVersion, currentImage string) bool {
if strings.TrimSpace(currentVersion) == strings.TrimSpace(targetVersion) {
return false
}
if !isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage) {
return false
}
if common.CompareVersion(targetVersion, currentVersion) {
return true
}
return !hasVllmVersionFamilyPrefix(currentVersion) &&
resolveVllmVersionFamily(targetVersion, "") == vllmImageTypeNvidia &&
trimVllmVersionFamily(currentVersion) == trimVllmVersionFamily(targetVersion)
}
func buildVllmUpgradeImage(currentImage, currentVersion, targetVersion string) string {
trimmedImage := strings.TrimSpace(currentImage)
if trimmedImage == "" || trimmedImage == buildDefaultVllmImageByVersion(currentVersion) {
return buildDefaultVllmImageByVersion(targetVersion)
}
return trimmedImage
}
func loadVllmImageFromEnv(raw string) string {
envs := make(map[string]interface{})
if strings.TrimSpace(raw) == "" {
return ""
}
if err := json.Unmarshal([]byte(raw), &envs); err != nil {
return ""
}
if image, ok := envs[vllmImageEnvKey].(string); ok {
return strings.TrimSpace(image)
}
return ""
}
func setVllmImageInEnvContent(content []byte, image string) []byte {
normalizedImage := strings.TrimSpace(image)
if normalizedImage == "" {
return content
}
lines := strings.Split(string(content), "\n")
replaced := false
for index, line := range lines {
if strings.HasPrefix(line, vllmImageEnvKey+"=") {
lines[index] = vllmImageEnvKey + "=" + normalizedImage
replaced = true
break
}
}
if !replaced {
if len(lines) > 0 && lines[len(lines)-1] == "" {
lines[len(lines)-1] = vllmImageEnvKey + "=" + normalizedImage
lines = append(lines, "")
} else {
lines = append(lines, vllmImageEnvKey+"="+normalizedImage)
}
}
return []byte(strings.Join(lines, "\n"))
}
+1
View File
@@ -705,6 +705,7 @@ func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType st
}
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
if err != nil {
global.LOG.Errorf("alert config not found for type %s: %v", configType, err)
return
}
doSendAlert(alert, alertType, quota, quotaType, params, config)
+1
View File
@@ -93,6 +93,7 @@ func (s *AlertSender) sendByLegacyMethod(method string, quota string, params []d
}
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
if err != nil {
global.LOG.Errorf("alert config not found for type %s: %v", configType, err)
return
}
if !alertUtil.IsAlertConfigEnabled(config) {
+1 -9
View File
@@ -303,9 +303,6 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
filename := filepath.Base(appDetailDTO.DownloadUrl)
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml")
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound {
return appDetailDTO, buserr.New("ErrAppVersionUnavailable")
}
if err != nil {
return appDetailDTO, buserr.WithDetail("ErrGetCompose", err.Error(), err)
}
@@ -429,12 +426,7 @@ 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)
var statusCode int
statusCode, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound {
err = buserr.New("ErrAppVersionUnavailable")
return
}
_, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if err != nil {
return
}
-4
View File
@@ -294,7 +294,6 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
DetailID: req.DetailId,
Backup: req.Backup,
PullImage: req.PullImage,
DeleteImage: req.DeleteImage,
DockerCompose: req.DockerCompose,
TaskID: req.TaskID,
}
@@ -601,9 +600,6 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
filename := filepath.Base(detail.DownloadUrl)
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml")
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound {
return versions, buserr.New("ErrAppVersionUnavailable")
}
if err != nil {
return versions, err
}
+151 -423
View File
@@ -1,6 +1,7 @@
package service
import (
"bufio"
"context"
"encoding/base64"
"encoding/json"
@@ -88,15 +89,7 @@ func checkPort(key string, params map[string]interface{}) (int, error) {
return 0, nil
}
func isPortInUse(port int, protocol string) bool {
return common.ScanPortWithProto(port, normalizeComposeProtocol(protocol))
}
func checkPortExist(port int) error {
return checkPortExistWithProtocol(port, "")
}
func checkPortExistWithProtocol(port int, protocol string) error {
errMap := make(map[string]interface{})
errMap["port"] = port
appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port))
@@ -117,7 +110,7 @@ func checkPortExistWithProtocol(port int, protocol string) error {
errMap["name"] = domain.Domain
return buserr.WithMap("ErrPortExist", errMap, nil)
}
if isPortInUse(port, protocol) {
if common.ScanPort(port) {
return buserr.WithDetail("ErrPortInUsed", port, nil)
}
return nil
@@ -298,8 +291,8 @@ func createLink(ctx context.Context, installTask *task.Task, app model.App, appI
}
resourceId = oldMysqlDb.ID
if oldMysqlDb.ID > 0 {
if err := ensureAppMysqlDBUser(database, dbConfig); err != nil {
return err
if oldMysqlDb.Username != dbConfig.DbUser || oldMysqlDb.Password != dbConfig.Password {
return buserr.New("ErrDbUserNotValid")
}
} else {
var createMysql dto.MysqlDBCreate
@@ -364,13 +357,33 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
return err
}
if deleteReq.DeleteImage {
delImageStr := i18n.GetMsgByKey("TaskDelete") + i18n.GetMsgByKey("Image")
content, err := op.GetContent(install.GetEnvPath())
if err != nil {
return err
}
if err = deleteAppImagesByCompose(t, content, []byte(install.DockerCompose), nil); err != nil {
images, err := docker.GetImagesFromDockerCompose(content, []byte(install.DockerCompose))
if err != nil {
return err
}
client, err := docker.NewClient()
if err != nil {
return err
}
defer client.Close()
for _, image := range images {
imageID, err := client.GetImageIDByName(image)
if err == nil {
imgStr := delImageStr + image
t.Log(imgStr)
if err = client.DeleteImage(imageID); err != nil {
t.LogFailedWithErr(imgStr, err)
continue
}
t.LogSuccess(delImageStr + image)
}
}
}
}
tx, ctx := helper.GetTxAndContext()
@@ -411,20 +424,6 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
switch install.App.Key {
case constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster:
if err = databaseUserGrantRepo.DeleteBy(
ctx,
repo.WithByType(install.App.Key),
databaseUserGrantRepo.WithByDatabase(install.Name),
); err != nil {
return err
}
if err = databaseUserRepo.DeleteBy(
ctx,
repo.WithByType(install.App.Key),
databaseUserRepo.WithByDatabase(install.Name),
); err != nil {
return err
}
_ = mysqlRepo.Delete(ctx, mysqlRepo.WithByMysqlName(install.Name))
case constant.AppMongodb:
_ = mongodbRepo.Delete(ctx, mongodbRepo.WithByMongodbName(install.Name))
@@ -469,107 +468,6 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
return nil
}
type appImageID struct {
name string
id string
}
func getAppImageIDsByCompose(client docker.Client, envContent, composeContent []byte) ([]appImageID, error) {
images, err := docker.GetImagesFromDockerCompose(envContent, composeContent)
if err != nil {
return nil, err
}
imageIDs := make([]appImageID, 0, len(images))
for _, image := range images {
imageID, err := client.GetImageIDByName(image)
if err == nil && imageID != "" {
imageIDs = append(imageIDs, appImageID{name: image, id: imageID})
}
}
return imageIDs, nil
}
func deleteAppImagesByCompose(t *task.Task, envContent, composeContent []byte, excludeImages []string) error {
client, err := docker.NewClient()
if err != nil {
return err
}
defer client.Close()
imageIDs, err := getAppImageIDsByCompose(client, envContent, composeContent)
if err != nil {
return err
}
return deleteAppImagesByIDs(t, client, imageIDs, excludeImages)
}
func deleteAppImagesByIDs(t *task.Task, client docker.Client, imageIDs []appImageID, excludeImages []string) error {
delImageStr := i18n.GetMsgByKey("TaskDelete") + i18n.GetMsgByKey("Image")
excludeImageIDs := make(map[string]struct{}, len(excludeImages))
for _, image := range excludeImages {
imageID, err := client.GetImageIDByName(image)
if err == nil && imageID != "" {
excludeImageIDs[imageID] = struct{}{}
}
}
deletedImageIDs := make(map[string]struct{}, len(imageIDs))
for _, image := range imageIDs {
if _, ok := excludeImageIDs[image.id]; ok {
continue
}
if _, ok := deletedImageIDs[image.id]; ok {
continue
}
deletedImageIDs[image.id] = struct{}{}
imgStr := delImageStr + image.name
t.Log(imgStr)
if err := client.DeleteImage(image.id); err != nil {
t.LogFailedWithErr(imgStr, err)
continue
}
t.LogSuccess(imgStr)
}
return nil
}
func ensureAppMysqlDBUser(database model.Database, dbConfig dto.AppDatabase) error {
const host = "%"
mysqlService := NewIMysqlService()
users, err := mysqlService.ListUsers(dto.MysqlUserSearch{Database: database.Name})
if err != nil {
return err
}
userExists := false
passwordValid := false
for _, user := range users {
if user.Username != dbConfig.DbUser || user.Host != host || user.IsDelete {
continue
}
userExists = true
passwordValid = user.Password == dbConfig.Password
break
}
if !userExists || !passwordValid {
return buserr.New("ErrDbUserNotValid")
}
grants, err := mysqlService.ListGrants(dto.MysqlUserSearch{Database: database.Name})
if err != nil {
return err
}
for _, grant := range grants {
if grant.Database == dbConfig.DbName && grant.Username == dbConfig.DbUser && grant.Host == host {
return nil
}
}
return mysqlService.GrantUser(dto.MysqlGrantCreate{
Database: database.Name,
DB: dbConfig.DbName,
Username: dbConfig.DbUser,
Host: host,
})
}
func deleteLink(del dto.DelAppLink) error {
install := del.Install
resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID))
@@ -579,20 +477,19 @@ func deleteLink(del dto.DelAppLink) error {
for _, re := range resources {
switch re.Key {
case constant.AppMysql, constant.AppMariaDB:
mysqlService := NewIMysqlService()
database, _ := mysqlRepo.Get(repo.WithByID(re.ResourceId))
if reflect.DeepEqual(database, model.DatabaseMysql{}) {
continue
}
if err := deleteMysqlDatabaseForResourceOwner(del.Ctx, dto.MysqlDBDelete{
if err := mysqlService.Delete(del.Ctx, dto.MysqlDBDelete{
ID: database.ID,
ForceDelete: del.ForceDelete,
DeleteBackup: true,
Type: re.Key,
Database: database.MysqlName,
}, dto.DBResource{Type: constant.TypeApp, Name: install.Name}); err != nil {
if isMysqlDatabaseResourceInUseError(err) || !del.ForceDelete {
return err
}
}); err != nil && !del.ForceDelete {
return err
}
case constant.AppPostgresql:
pgsqlService := NewIPostgresqlService()
@@ -667,58 +564,11 @@ func handleUpgradeCompose(install model.AppInstall, detail model.AppDetail) (map
if oldServiceValue["restart"] != nil {
serviceValue["restart"] = oldServiceValue["restart"]
}
if install.App.Key == constant.AppOpenresty {
mergeOpenrestyModuleVolumes(serviceValue, oldServiceValue)
}
servicesMap[install.ServiceName] = serviceValue
composeMap["services"] = servicesMap
return composeMap, nil
}
// mergeOpenrestyModuleVolumes carries the dynamic module mounts of the old
// compose over to the upgraded one when it does not declare them, so built
// module artifacts and their load configuration stay mounted across upgrades.
func mergeOpenrestyModuleVolumes(serviceValue, oldServiceValue map[string]interface{}) {
oldVolumes, ok := oldServiceValue["volumes"].([]interface{})
if !ok {
return
}
newVolumes, _ := serviceValue["volumes"].([]interface{})
existing := make(map[string]struct{}, len(newVolumes))
for _, volume := range newVolumes {
if containerPath, ok := composeVolumeContainerPath(volume); ok {
existing[containerPath] = struct{}{}
}
}
for _, volume := range oldVolumes {
containerPath, ok := composeVolumeContainerPath(volume)
if !ok {
continue
}
if !strings.Contains(containerPath, nginxModuleEnabledConfDir) && !strings.Contains(containerPath, "nginx/modules/1panel") {
continue
}
if _, ok = existing[containerPath]; ok {
continue
}
newVolumes = append(newVolumes, volume)
existing[containerPath] = struct{}{}
}
serviceValue["volumes"] = newVolumes
}
func composeVolumeContainerPath(volume interface{}) (string, bool) {
volumeStr, ok := volume.(string)
if !ok {
return "", false
}
parts := strings.Split(volumeStr, ":")
if len(parts) < 2 {
return "", false
}
return parts[1], true
}
func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) {
if detail.DockerCompose == "" {
return "", nil
@@ -752,35 +602,74 @@ func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string
return string(composeByte), nil
}
func buildNginx(parentTask *task.Task, nginxInstall model.AppInstall, catalogPath string) error {
func buildNginx(parentTask *task.Task) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
fileOp := files.NewFileOp()
buildPath := path.Join(nginxInstall.GetPath(), nginxModuleBuildDir)
buildPath := path.Join(nginxInstall.GetPath(), "build")
if !fileOp.Stat(buildPath) {
return buserr.New("ErrBuildDirNotFound")
}
modules, err := loadNginxModulesWithCatalog(nginxInstall, catalogPath)
moduleConfigPath := path.Join(buildPath, "module.json")
moduleContent, err := fileOp.GetContent(moduleConfigPath)
if err != nil {
return err
}
previousModules := cloneNginxModules(modules)
staticBuild := hasEnabledStaticNginxModules(modules)
if err = configureStaticNginxModules(nginxInstall, modules, ""); err != nil {
return err
}
if staticBuild {
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image"))
parentTask.LogStart(logStr)
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*parentTask), cmd.WithTimeout(120*time.Minute))
if err = cmdMgr.Run("docker", "compose", "-f", nginxInstall.GetComposePath(), "build"); err != nil {
var (
modules []dto.NginxModule
addModuleParams []string
addPackages []string
)
if len(moduleContent) > 0 {
_ = json.Unmarshal(moduleContent, &modules)
bashFile, err := os.OpenFile(path.Join(buildPath, "tmp", "pre.sh"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, constant.DirPerm)
if err != nil {
return err
}
defer bashFile.Close()
bashFileWriter := bufio.NewWriter(bashFile)
for _, module := range modules {
if !module.Enable {
continue
}
_, err = bashFileWriter.WriteString(module.Script + "\n")
if err != nil {
return err
}
addModuleParams = append(addModuleParams, module.Params)
addPackages = append(addPackages, module.Packages...)
}
err = bashFileWriter.Flush()
if err != nil {
return err
}
parentTask.LogSuccess(logStr)
}
modules, err = buildDynamicNginxModules(nginxInstall, modules, nil, false, "", catalogPath, parentTask)
envs, err := gotenv.Read(nginxInstall.GetEnvPath())
if err != nil {
return err
}
return commitNginxModuleBuilds(nginxInstall, previousModules, modules, false, catalogPath)
envs["RESTY_CONFIG_OPTIONS_MORE"] = ""
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = ""
if len(addModuleParams) > 0 {
envs["RESTY_CONFIG_OPTIONS_MORE"] = strings.Join(addModuleParams, " ")
}
if len(addPackages) > 0 {
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = strings.Join(addPackages, " ")
}
_ = gotenv.Write(envs, nginxInstall.GetEnvPath())
if len(addModuleParams) == 0 && len(addPackages) == 0 {
return nil
}
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image"))
parentTask.LogStart(logStr)
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*parentTask), cmd.WithTimeout(60*time.Minute))
if err = cmdMgr.Run("docker", "compose", "-f", nginxInstall.GetComposePath(), "build"); err != nil {
return err
}
parentTask.LogSuccess(logStr)
return nil
}
func upgradeInstall(req request.AppInstallUpgrade) error {
@@ -788,7 +677,6 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil {
return err
}
originalInstall := install
oldVersion := install.Version
detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
if err != nil {
@@ -808,9 +696,8 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
install.Status = constant.StatusUpgrading
var (
upErr error
backupFile string
nginxUpgradeSnapshot *openrestyUpgradeSnapshot
upErr error
backupFile string
)
backUpApp := func(t *task.Task) error {
backupService := NewIBackupService()
@@ -864,15 +751,6 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil {
return err
}
oldEnvContent := append([]byte(nil), content...)
oldDockerCompose := install.DockerCompose
targetNginxCatalogPath := ""
if install.App.Key == constant.AppOpenresty {
nginxUpgradeSnapshot, err = createOpenrestyUpgradeSnapshot(install.GetPath())
if err != nil {
return err
}
}
if install.App.Key == vllmAppKeyForUpgrade {
envs := make(map[string]interface{})
if err = json.Unmarshal([]byte(install.Env), &envs); err != nil {
@@ -887,35 +765,60 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
install.Env = string(paramByte)
content = setVllmImageInEnvContent(content, image)
}
if req.PullImage {
composeContent := []byte(detail.DockerCompose)
if install.App.Key == vllmAppKeyForUpgrade {
composeContent = []byte(install.DockerCompose)
}
if req.DockerCompose != "" {
composeContent = []byte(req.DockerCompose)
}
images, err := docker.GetImagesFromDockerCompose(content, composeContent)
if err != nil {
return err
}
dockerCLi, err := docker.NewClient()
if err != nil {
return err
}
defer dockerCLi.Close()
for _, image := range images {
t.Log(i18n.GetWithName("PullImageStart", image))
if err = dockerCLi.PullImageWithProcess(t, image); err != nil {
err = buserr.WithNameAndErr("ErrDockerPullImage", "", err)
return err
}
exist, err := dockerCLi.ImageExists(image)
if err != nil {
err = buserr.WithNameAndErr("ErrDockerPullImage", "", err)
return err
}
if !exist {
err = buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s not found", image))
return err
}
t.LogSuccess(i18n.GetMsgByKey("PullImage"))
}
}
_ = copyAppDetailMissing(fileOp, detailDir, install.GetPath())
if install.App.Key == constant.AppOpenresty {
installBuildDir := path.Join(install.GetPath(), nginxModuleBuildDir)
detailBuildDir := path.Join(detailDir, nginxModuleBuildDir)
installBuildDir := path.Join(install.GetPath(), "build")
detailBuildDir := path.Join(detailDir, "build")
if !fileOp.Stat(installBuildDir) {
if err := fileOp.CreateDir(installBuildDir, constant.DirPerm); err != nil {
return err
}
}
if err := fileOp.DeleteDir(path.Join(installBuildDir, nginxModuleTmpDir)); err != nil {
if err := fileOp.DeleteDir(path.Join(installBuildDir, "tmp")); err != nil {
return err
}
if err := fileOp.CopyDir(path.Join(detailBuildDir, nginxModuleTmpDir), installBuildDir); err != nil {
if err := fileOp.CopyDir(path.Join(detailBuildDir, "tmp"), installBuildDir); err != nil {
return err
}
if err := fileOp.CopyFile(path.Join(detailBuildDir, "Dockerfile"), installBuildDir); err != nil {
return err
}
if err := syncNginxModuleBuilder(detailBuildDir, installBuildDir); err != nil {
return err
}
targetCatalogSource := path.Join(detailBuildDir, nginxModuleCatalogFile)
if !fileOp.Stat(targetCatalogSource) {
return fmt.Errorf("target OpenResty module catalog not found: %s", targetCatalogSource)
}
targetNginxCatalogPath = path.Join(installBuildDir, nginxModuleCatalogPendingFile)
if err := stageNginxModuleCatalog(targetCatalogSource, targetNginxCatalogPath); err != nil {
return err
}
if err := fileOp.CopyFile(path.Join(detailBuildDir, "nginx.conf"), installBuildDir); err != nil {
return err
}
@@ -953,66 +856,6 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
install.Version = detail.Version
install.AppDetailId = req.DetailID
var oldImageIDs []appImageID
if req.DeleteImage {
dockerCLi, err := docker.NewClient()
if err != nil {
return err
}
oldImageIDs, err = getAppImageIDsByCompose(dockerCLi, oldEnvContent, []byte(oldDockerCompose))
dockerCLi.Close()
if err != nil {
return err
}
}
if req.PullImage {
images, err := docker.GetImagesFromDockerCompose(content, []byte(install.DockerCompose))
if err != nil {
return err
}
dockerCLi, err := docker.NewClient()
if err != nil {
return err
}
defer dockerCLi.Close()
for _, image := range images {
t.Log(i18n.GetWithName("PullImageStart", image))
if pullErr := dockerCLi.PullImageWithProcess(t, image); pullErr != nil {
if exist, _ := dockerCLi.ImageExists(image); exist {
t.Log(i18n.GetMsgByKey("UseExistImage"))
continue
}
return buserr.WithNameAndErr("ErrDockerPullImage", "", pullErr)
}
exist, err := dockerCLi.ImageExists(image)
if err != nil || !exist {
return buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s does not exist after pull: %v", image, err))
}
t.LogSuccess(i18n.GetMsgByKey("PullImage"))
}
}
if install.App.Key == constant.AppOpenresty {
modules, moduleErr := loadNginxModulesWithCatalog(install, targetNginxCatalogPath)
if moduleErr != nil {
return moduleErr
}
// Build dynamic modules for the target version before stopping the
// current container. Static modules retain the full rebuild path.
if !hasEnabledStaticNginxModules(modules) {
previousModules := cloneNginxModules(modules)
modules, moduleErr = buildDynamicNginxModules(install, modules, nil, false, "", targetNginxCatalogPath, t)
if moduleErr != nil {
return moduleErr
}
if moduleErr = saveNginxModulesWithCatalog(install, modules, targetNginxCatalogPath); moduleErr != nil {
removeNginxModuleOutputsNotReferenced(install, modules, previousModules)
return moduleErr
}
}
}
if out, err := compose.Down(install.GetComposePath()); err != nil {
if out != "" {
upErr = errors.New(out)
@@ -1047,7 +890,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
}
if install.App.Key == constant.AppOpenresty {
if err = buildNginx(t, install, targetNginxCatalogPath); err != nil {
if err = buildNginx(t); err != nil {
t.Log(err.Error())
return err
}
@@ -1063,134 +906,36 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
}
t.LogSuccess(logStr)
install.Status = constant.StatusRunning
if install.App.Key == constant.AppOpenresty {
if err = commitStaticNginxModuleBuilds(install, targetNginxCatalogPath, t); err != nil {
return err
}
activeCatalogPath := path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleCatalogFile)
if err = activateNginxModuleCatalogAndCommit(targetNginxCatalogPath, activeCatalogPath, func() error {
return appInstallRepo.Save(context.Background(), &install)
}); err != nil {
return err
}
} else {
if err = appInstallRepo.Save(context.Background(), &install); err != nil {
return err
}
}
if nginxUpgradeSnapshot != nil {
nginxUpgradeSnapshot.Cleanup()
nginxUpgradeSnapshot = nil
}
if req.DeleteImage {
newEnvContent, err := fileOp.GetContent(install.GetEnvPath())
if err != nil {
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
return nil
}
excludeImages, err := docker.GetImagesFromDockerCompose(newEnvContent, []byte(install.DockerCompose))
if err != nil {
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
return nil
}
dockerCLi, err := docker.NewClient()
if err != nil {
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
return nil
}
defer dockerCLi.Close()
if err = deleteAppImagesByIDs(t, dockerCLi, oldImageIDs, excludeImages); err != nil {
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
}
}
return nil
return appInstallRepo.Save(context.Background(), &install)
}
rollBackApp := func(t *task.Task) {
if req.Backup {
t.Log(i18n.GetWithName("AppRecover", install.Name))
recoverErr := NewIBackupService().AppRecover(dto.CommonRecover{
Name: install.App.Key, DetailName: install.Name, Type: "app", DownloadAccountID: 1, File: backupFile,
})
if recoverErr == nil {
if nginxUpgradeSnapshot != nil {
nginxUpgradeSnapshot.Cleanup()
nginxUpgradeSnapshot = nil
}
t.LogSuccess(i18n.GetWithName("AppRecover", install.Name))
if err := NewIBackupService().AppRecover(dto.CommonRecover{Name: install.App.Key, DetailName: install.Name, Type: "app", DownloadAccountID: 1, File: backupFile}); err != nil {
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), err)
return
}
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), recoverErr)
if install.App.Key != constant.AppOpenresty {
return
}
}
if install.App.Key == constant.AppOpenresty && nginxUpgradeSnapshot != nil {
if out, rollbackErr := compose.Down(install.GetComposePath()); rollbackErr != nil {
if out != "" {
rollbackErr = fmt.Errorf("%s: %w", out, rollbackErr)
}
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
}
if rollbackErr := nginxUpgradeSnapshot.Restore(); rollbackErr != nil {
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
return
}
nginxUpgradeSnapshot.Cleanup()
nginxUpgradeSnapshot = nil
if out, rollbackErr := compose.Up(originalInstall.GetComposePath()); rollbackErr != nil {
if out != "" {
rollbackErr = fmt.Errorf("%s: %w", out, rollbackErr)
}
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
return
}
originalInstall.Status = constant.StatusRunning
originalInstall.Message = ""
if rollbackErr := appInstallRepo.Save(context.Background(), &originalInstall); rollbackErr != nil {
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
return
}
install = originalInstall
t.LogSuccess(i18n.GetWithName("AppRecover", install.Name))
return
}
if install.App.Key == constant.AppOpenresty {
if rollbackErr := appInstallRepo.Save(context.Background(), &originalInstall); rollbackErr != nil {
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
return
}
install = originalInstall
t.LogSuccess(i18n.GetWithName("AppRecover", install.Name))
}
}
upgradeTimeout := 1 * time.Hour
if install.App.Key == constant.AppOpenresty {
// Dynamic modules are built serially and each Docker build has its own
// timeout. An outer deadline would start rollback while upgradeApp is
// still mutating the installation because SubTask does not stop its
// action goroutine on timeout.
upgradeTimeout = 0
}
upgradeTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp), upgradeApp, rollBackApp, 0, upgradeTimeout)
upgradeTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp), upgradeApp, rollBackApp, 0, 1*time.Hour)
upgradingInstall := install
if err = appInstallRepo.Save(context.Background(), &upgradingInstall); err != nil {
return err
}
go func() {
if taskErr := upgradeTask.Execute(); taskErr != nil {
err = upgradeTask.Execute()
if err != nil {
existInstall, _ := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
if existInstall.ID > 0 && existInstall.Status != constant.StatusRunning {
existInstall.Status = constant.StatusUpgradeErr
existInstall.Message = taskErr.Error()
existInstall.Message = err.Error()
_ = appInstallRepo.Save(context.Background(), &existInstall)
}
}
}()
return nil
return appInstallRepo.Save(context.Background(), &install)
}
func skipCheckStatus(service types.ServiceConfig) bool {
@@ -1888,9 +1633,6 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
synAppInstall(containersMap, &installed, false)
}
resourceKeys := getAppInstallResourceKeys(installed.ID)
envMap := make(map[string]interface{})
_ = json.Unmarshal([]byte(installed.Env), &envMap)
installDTO := response.AppInstallDTO{
ID: installed.ID,
Name: installed.Name,
@@ -1912,16 +1654,14 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
Website: installed.App.Website,
Document: installed.App.Document,
},
Favorite: installed.Favorite,
SortOrder: installed.SortOrder,
Container: installed.ContainerName,
ServiceName: strings.ToLower(installed.ServiceName),
ResourceKeys: resourceKeys,
Env: envMap,
Favorite: installed.Favorite,
SortOrder: installed.SortOrder,
Container: installed.ContainerName,
ServiceName: strings.ToLower(installed.ServiceName),
}
if !updated && !checkUpdate {
installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys)
installDTO.LinkDB = hasLinkDB(installed.ID)
res = append(res, installDTO)
continue
}
@@ -1929,7 +1669,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
if installed.Version == "latest" {
if checkUpdate {
installDTO.CanUpdate = false
installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys)
installDTO.LinkDB = hasLinkDB(installed.ID)
res = append(res, installDTO)
}
continue
@@ -1958,7 +1698,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
if len(versions) == 0 {
if checkUpdate {
installDTO.CanUpdate = false
installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys)
installDTO.LinkDB = hasLinkDB(installed.ID)
res = append(res, installDTO)
}
continue
@@ -1990,7 +1730,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
res = append(res, installDTO)
}
} else if checkUpdate {
installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys)
installDTO.LinkDB = hasLinkDB(installed.ID)
res = append(res, installDTO)
}
}
@@ -2421,7 +2161,7 @@ func handleOpenrestyFile(appInstall *model.AppInstall) error {
func handleDefaultServer(appInstall *model.AppInstall) error {
installDir := appInstall.GetPath()
defaultConfigPath := path.Join(installDir, nginxModuleConfDir, "default", "00.default.conf")
defaultConfigPath := path.Join(installDir, "conf", "default", "00.default.conf")
fileOp := files.NewFileOp()
content, err := fileOp.GetContent(defaultConfigPath)
if err != nil {
@@ -2435,7 +2175,7 @@ func handleDefaultServer(appInstall *model.AppInstall) error {
}
func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRejectHandshake bool) error {
sslDir := path.Join(appInstall.GetPath(), nginxModuleConfDir, "ssl")
sslDir := path.Join(appInstall.GetPath(), "conf", "ssl")
fileOp := files.NewFileOp()
if !fileOp.Stat(sslDir) {
return errors.New("ssl dir not found")
@@ -2465,7 +2205,7 @@ func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRe
_ = NewIWebsiteSSLService().Delete([]uint{websiteSSL.ID})
}()
}
defaultConfigPath := path.Join(appInstall.GetPath(), nginxModuleConfDir, "default", "00.default.conf")
defaultConfigPath := path.Join(appInstall.GetPath(), "conf", "default", "00.default.conf")
content, err := os.ReadFile(defaultConfigPath)
if err != nil {
return err
@@ -2557,25 +2297,13 @@ func needsUpdate(localTag *model.Tag, remoteTag dto.Tag, translations string) bo
}
func hasLinkDB(installID uint) bool {
return hasLinkDBFromKeys(getAppInstallResourceKeys(installID))
}
func getAppInstallResourceKeys(installID uint) []string {
resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(installID))
keys := make([]string, 0, len(resources))
for _, resource := range resources {
keys = append(keys, resource.Key)
}
return keys
}
func hasLinkDBFromKeys(resourceKeys []string) bool {
hasDB := false
if len(resourceKeys) > 0 {
for _, resourceKey := range resourceKeys {
if resourceKey == constant.AppPostgres || resourceKey == constant.AppMysql ||
resourceKey == constant.AppMariaDB || resourceKey == constant.AppMysqlCluster ||
resourceKey == constant.AppPostgresql || resourceKey == constant.AppPostgresqlCluster {
if len(resources) > 0 {
for _, resource := range resources {
if resource.Key == constant.AppPostgres || resource.Key == constant.AppMysql ||
resource.Key == constant.AppMariaDB || resource.Key == constant.AppMysqlCluster ||
resource.Key == constant.AppPostgresql || resource.Key == constant.AppPostgresqlCluster {
hasDB = true
break
}
+6 -78
View File
@@ -2,7 +2,6 @@ package service
import (
"bufio"
"context"
"encoding/base64"
"encoding/json"
"fmt"
@@ -373,7 +372,7 @@ func (u *BackupService) checkBackupConn(backup *model.BackupAccount) (bool, erro
targetPath = strings.TrimPrefix(targetPath, "/")
}
if _, err := client.Upload(context.Background(), fileItem, targetPath); err != nil {
if _, err := client.Upload(fileItem, targetPath); err != nil {
return false, err
}
_, _ = client.Delete(path.Join(backup.BackupPath, "test/1panel"))
@@ -438,10 +437,6 @@ type backupClientHelper struct {
}
func NewBackupClientMap(ids []string) map[string]backupClientHelper {
return NewBackupClientMapWithContext(context.Background(), ids)
}
func NewBackupClientMapWithContext(ctx context.Context, ids []string) map[string]backupClientHelper {
var accounts []model.BackupAccount
var idItems []uint
for i := 0; i < len(ids); i++ {
@@ -451,7 +446,7 @@ func NewBackupClientMapWithContext(ctx context.Context, ids []string) map[string
accounts, _ = backupRepo.List(repo.WithByIDs(idItems))
clientMap := make(map[string]backupClientHelper)
for _, item := range accounts {
backClient, err := newClientWithContext(ctx, &item, true)
backClient, err := newClient(&item, true)
itemHelper := backupClientHelper{
client: backClient,
name: item.Name,
@@ -468,11 +463,7 @@ func NewBackupClientMapWithContext(ctx context.Context, ids []string) map[string
return clientMap
}
func uploadWithMap(taskItem task.Task, accountMap map[string]backupClientHelper, src, dst, accountIDs string, downloadAccountID, retry uint, cleanOnFailure bool) error {
return uploadWithMapWithContext(context.Background(), taskItem, accountMap, src, dst, accountIDs, downloadAccountID, retry, cleanOnFailure, true)
}
func uploadWithMapWithContext(ctx context.Context, taskItem task.Task, accountMap map[string]backupClientHelper, src, dst, accountIDs string, downloadAccountID, retry uint, cleanOnFailure, removeSrc bool) error {
func uploadWithMap(taskItem task.Task, accountMap map[string]backupClientHelper, src, dst, accountIDs string, downloadAccountID, retry uint) error {
accounts := strings.Split(accountIDs, ",")
for _, account := range accounts {
if len(account) == 0 {
@@ -498,13 +489,10 @@ func uploadWithMapWithContext(ctx context.Context, taskItem task.Task, accountMa
"backup": name,
}))
for i := 0; i < int(retry)+1; i++ {
_, err := itemBackup.client.Upload(ctx, src, path.Join(itemBackup.backupPath, dst))
_, err := itemBackup.client.Upload(src, path.Join(itemBackup.backupPath, dst))
taskItem.LogWithStatus(i18n.GetMsgByKey("Upload"), err)
if err != nil {
if account == fmt.Sprintf("%d", downloadAccountID) {
if cleanOnFailure {
cleanupCronjobBackupArtifacts(accountMap, src, dst)
}
return err
}
} else {
@@ -514,71 +502,11 @@ func uploadWithMapWithContext(ctx context.Context, taskItem task.Task, accountMa
itemBackup.hasBackup = true
accountMap[account] = itemBackup
}
if removeSrc {
os.RemoveAll(src)
}
os.RemoveAll(src)
return nil
}
func cleanupCronjobBackupArtifacts(accountMap map[string]backupClientHelper, src, dst string) {
if err := os.RemoveAll(src); err != nil {
global.LOG.Errorf("remove failed local cronjob backup file %s failed, err: %v", src, err)
}
for _, account := range accountMap {
if !account.isOk {
continue
}
if _, err := account.client.Delete(path.Join(account.backupPath, dst)); err != nil {
global.LOG.Errorf("remove failed cronjob backup file %s failed, err: %v", dst, err)
}
}
}
func markBackupFailed(recordID uint, backupErr error) {
_ = backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": backupErr.Error()})
record, err := backupRepo.GetRecord(repo.WithByID(recordID))
if err != nil || record.ID == 0 {
global.LOG.Errorf("load failed backup record %d for cleanup failed, err: %v", recordID, err)
return
}
filePath := path.Join(record.FileDir, record.FileName)
if err := os.Remove(path.Join(global.Dir.LocalBackupDir, filePath)); err != nil && !os.IsNotExist(err) {
global.LOG.Errorf("remove failed local backup file %s failed, err: %v", filePath, err)
}
cleaned := make(map[string]struct{})
for _, accountID := range strings.Split(record.SourceAccountIDs, ",") {
if accountID == "" {
continue
}
if _, ok := cleaned[accountID]; ok {
continue
}
cleaned[accountID] = struct{}{}
id, err := strconv.Atoi(accountID)
if err != nil {
global.LOG.Errorf("parse backup account %s for failed backup cleanup failed, err: %v", accountID, err)
continue
}
account, storageClient, err := NewBackupClientWithID(uint(id))
if err != nil {
global.LOG.Errorf("new backup client for failed backup cleanup failed, err: %v", err)
continue
}
if _, err := storageClient.Delete(path.Join(account.BackupPath, filePath)); err != nil {
global.LOG.Errorf("remove failed backup file %s failed, err: %v", filePath, err)
}
}
}
func newClient(account *model.BackupAccount, isEncrypt bool) (cloud_storage.CloudStorageClient, error) {
return newClientWithContext(context.Background(), account, isEncrypt)
}
func newClientWithContext(ctx context.Context, account *model.BackupAccount, isEncrypt bool) (cloud_storage.CloudStorageClient, error) {
varMap := make(map[string]interface{})
if len(account.Vars) != 0 {
if err := json.Unmarshal([]byte(account.Vars), &varMap); err != nil {
@@ -603,7 +531,7 @@ func newClientWithContext(ctx context.Context, account *model.BackupAccount, isE
varMap["password"] = account.Credential
}
client, err := cloud_storage.NewCloudStorageClientWithContext(ctx, account.Type, varMap)
client, err := cloud_storage.NewCloudStorageClient(account.Type, varMap)
if err != nil {
return nil, err
}
+9 -74
View File
@@ -63,30 +63,11 @@ func (u *BackupService) AppBackup(req dto.CommonBackup) (*model.BackupRecord, er
return nil, err
}
if !req.IsImmediate {
if err = handleAppBackup(&install, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil {
markBackupFailed(record.ID, err)
global.LOG.Errorf("backup app %s failed, err: %v", req.DetailName, err)
return nil, err
}
return record, nil
if err = handleAppBackup(&install, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil {
global.LOG.Errorf("backup app %s failed, err: %v", req.DetailName, err)
return nil, err
}
backupTask, err := task.NewTaskWithOps(install.Name, task.TaskBackup, task.TaskScopeBackup, req.TaskID, install.ID)
if err != nil {
markBackupFailed(record.ID, err)
record.Status = constant.StatusFailed
record.Message = err.Error()
return nil, err
}
if err = doAppBackup(&install, backupTask, backupDir, fileName, "", req.Secret); err != nil {
markBackupFailed(record.ID, err)
record.Status = constant.StatusFailed
record.Message = err.Error()
return nil, err
}
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusSuccess})
record.Status = constant.StatusSuccess
return record, nil
}
@@ -162,7 +143,7 @@ func handleAppBackup(install *model.AppInstall, parentTask *task.Task, recordID
backupTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 3, time.Hour)
go func() {
if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err)
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return
}
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
@@ -280,7 +261,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
if err != nil {
return err
}
newDB, err := reCreateDB(db.ID, database, backupEnvMap, install.Name)
newDB, err := reCreateDB(db.ID, database, backupEnvMap)
if err != nil {
return err
}
@@ -408,16 +389,10 @@ func doAppBackup(install *model.AppInstall, parentTask *task.Task, backupDir, fi
return nil
}
func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{}, appInstallName string) (*model.DatabaseMysql, error) {
func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{}) (*model.DatabaseMysql, error) {
mysqlService := NewIMysqlService()
ctx := context.Background()
if err := deleteMysqlDatabaseForResourceOwner(
ctx,
dto.MysqlDBDelete{ID: dbID, Database: database.Name, Type: database.Type, DeleteBackup: false, ForceDelete: true},
dto.DBResource{Type: constant.TypeApp, Name: appInstallName},
); err != nil {
return nil, err
}
_ = mysqlService.Delete(ctx, dto.MysqlDBDelete{ID: dbID, Database: database.Name, Type: database.Type, DeleteBackup: false, ForceDelete: true})
dbInfo := getDBCreateInfoFromEnv(envMap, "utf8mb4")
createDB, err := mysqlService.Create(context.Background(), dto.MysqlDBCreate{
@@ -425,57 +400,17 @@ func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{
From: database.From,
Database: database.Name,
Format: dbInfo.Format,
Username: dbInfo.User,
Password: dbInfo.Password,
Permission: "%",
})
if err != nil {
return nil, err
}
if len(dbInfo.User) != 0 {
if err := ensureMysqlDBUser(mysqlService, database, dbInfo); err != nil {
return nil, err
}
}
updateCronjobsDBRef(dbID, createDB.ID)
return createDB, nil
}
func ensureMysqlDBUser(mysqlService IMysqlService, database model.Database, dbInfo dbRecreateInfo) error {
const host = "%"
users, err := mysqlService.ListUsers(dto.MysqlUserSearch{Database: database.Name})
if err != nil {
return err
}
var oldUser dto.MysqlUser
exists := false
for _, user := range users {
if user.Username == dbInfo.User && user.Host == host && !user.IsDelete {
oldUser = user
exists = true
break
}
}
if exists {
if len(oldUser.Password) != 0 && oldUser.Password != dbInfo.Password {
return buserr.New("ErrDbUserNotValid")
}
} else {
if err := mysqlService.CreateUser(dto.MysqlUserCreate{
Database: database.Name,
Username: dbInfo.User,
Host: host,
Password: dbInfo.Password,
}); err != nil {
return err
}
}
return mysqlService.GrantUser(dto.MysqlGrantCreate{
Database: database.Name,
DB: dbInfo.Name,
Username: dbInfo.User,
Host: host,
})
}
func reCreatePostgresqlDB(dbID uint, database model.Database, envMap map[string]interface{}) (*model.DatabasePostgresql, error) {
postgresqlService := NewIPostgresqlService()
_ = postgresqlService.Delete(context.Background(), dto.PostgresqlDBDelete{
+2 -2
View File
@@ -98,7 +98,7 @@ func (u *BackupService) ComposeBackup(req dto.CommonBackup) error {
return err
}
if err := handleComposeBackup(req, nil, record.ID, backupDir, fileName); err != nil {
markBackupFailed(record.ID, err)
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return err
}
return nil
@@ -163,7 +163,7 @@ func handleComposeBackup(req dto.CommonBackup, parentTask *task.Task, recordID u
go func() {
defer composeCtx.close()
if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err)
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return
}
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+18 -82
View File
@@ -106,7 +106,7 @@ func (u *BackupService) ContainerBackup(req dto.CommonBackup) error {
return err
}
if err := handleContainerBackup(req.Name, nil, record.ID, backupDir, fileName, req.TaskID, req.Secret, req.StopBefore); err != nil {
markBackupFailed(record.ID, err)
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return err
}
return nil
@@ -158,7 +158,7 @@ func handleContainerBackup(containerName string, parentTask *task.Task, recordID
go func() {
defer backupCtx.close()
if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err)
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return
}
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
@@ -604,40 +604,23 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
return nil
}
func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
if cli.NewVersionError(ctx, "1.44", "specify mac-address per network") != nil {
removeEndpointMacAddresses(primary, extras)
}
endpointGroups := []map[string]*network.EndpointSettings{extras}
func removeUnsupportedEndpointStaticIPAM(cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
if primary != nil {
endpointGroups = append(endpointGroups, primary.EndpointsConfig)
}
for _, endpoints := range endpointGroups {
for netName, endpoint := range endpoints {
if endpoint == nil || endpoint.IPAMConfig == nil {
continue
}
info, err := cli.NetworkInspect(ctx, netName, network.InspectOptions{})
if err != nil {
continue
}
removeUnsupportedEndpointStaticIP(netName, info, endpoint)
}
removeUnsupportedEndpointStaticIPAMFromEndpoints(cli, primary.EndpointsConfig)
}
removeUnsupportedEndpointStaticIPAMFromEndpoints(cli, extras)
}
func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
if primary != nil {
for _, endpoint := range primary.EndpointsConfig {
if endpoint != nil {
endpoint.MacAddress = ""
}
func removeUnsupportedEndpointStaticIPAMFromEndpoints(cli *client.Client, endpoints map[string]*network.EndpointSettings) {
for netName, endpoint := range endpoints {
if endpoint == nil || endpoint.IPAMConfig == nil {
continue
}
}
for _, endpoint := range extras {
if endpoint != nil {
endpoint.MacAddress = ""
info, err := cli.NetworkInspect(context.Background(), netName, network.InspectOptions{})
if err != nil {
continue
}
removeUnsupportedEndpointStaticIP(netName, info, endpoint)
}
}
@@ -656,7 +639,7 @@ func removeUnsupportedEndpointStaticIP(netName string, info network.Inspect, end
if endpoint.IPAMConfig.IPv6Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv6Address, true) {
endpoint.IPAMConfig.IPv6Address = ""
}
if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" && len(endpoint.IPAMConfig.LinkLocalIPs) == 0 {
if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" {
endpoint.IPAMConfig = nil
}
}
@@ -793,27 +776,13 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
if name == "host" || name == "none" {
continue
}
if endpoint == nil {
if name == primaryName {
config.EndpointsConfig[name] = &network.EndpointSettings{}
} else {
extraNetworks[name] = &network.EndpointSettings{}
}
continue
}
endpointSetting := &network.EndpointSettings{
Links: append([]string(nil), endpoint.Links...),
Aliases: append([]string(nil), endpoint.Aliases...),
DriverOpts: cloneStringMap(endpoint.DriverOpts),
GwPriority: endpoint.GwPriority,
}
endpointSetting := &network.EndpointSettings{Aliases: append([]string(nil), endpoint.Aliases...), MacAddress: endpoint.MacAddress}
if endpoint.IPAMConfig != nil {
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: endpoint.IPAMConfig.IPv4Address,
IPv6Address: endpoint.IPAMConfig.IPv6Address,
LinkLocalIPs: append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...),
IPv4Address: endpoint.IPAMConfig.IPv4Address,
IPv6Address: endpoint.IPAMConfig.IPv6Address,
}
} else if name != "bridge" && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "") {
} else if endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "" {
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: endpoint.IPAddress,
IPv6Address: endpoint.GlobalIPv6Address,
@@ -831,39 +800,6 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
return config, extraNetworks
}
const unsupportedUserSpecifiedIPAddress = "user specified IP address is supported only when connecting to networks with user configured subnets"
func clearUnsupportedDynamicEndpointIPAM(err error, endpoints map[string]*network.EndpointSettings, networkSettings *container.NetworkSettings) bool {
if err == nil || !strings.Contains(err.Error(), unsupportedUserSpecifiedIPAddress) {
return false
}
for name, endpoint := range endpoints {
if !isDynamicContainerNetwork(networkSettings, name) || endpoint == nil || endpoint.IPAMConfig == nil {
continue
}
if strings.Contains(err.Error(), "network "+name+":") {
endpoint.IPAMConfig = nil
return true
}
}
cleared := false
for name, endpoint := range endpoints {
if isDynamicContainerNetwork(networkSettings, name) && endpoint != nil && endpoint.IPAMConfig != nil {
endpoint.IPAMConfig = nil
cleared = true
}
}
return cleared
}
func isDynamicContainerNetwork(networkSettings *container.NetworkSettings, name string) bool {
if networkSettings == nil || name == "bridge" {
return false
}
endpoint := networkSettings.Networks[name]
return endpoint != nil && endpoint.IPAMConfig == nil && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "")
}
func cloneContainerConfig(config *container.Config) *container.Config {
if config == nil {
return &container.Config{}
+2 -2
View File
@@ -54,7 +54,7 @@ func (u *BackupService) MongodbBackup(req dto.CommonBackup) error {
}
if err := handleMongodbBackup(req, nil, record.ID, targetDir, fileName); err != nil {
markBackupFailed(record.ID, err)
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return err
}
return nil
@@ -98,7 +98,7 @@ func handleMongodbBackup(req dto.CommonBackup, parentTask *task.Task, recordID u
)
go func() {
if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err)
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return
}
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+2 -2
View File
@@ -47,7 +47,7 @@ func (u *BackupService) MysqlBackup(req dto.CommonBackup) error {
databaseHelper := DatabaseHelper{Database: req.Name, DBType: req.Type, Name: req.DetailName, Args: req.Args}
if err := handleMysqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID, req.Secret); err != nil {
markBackupFailed(record.ID, err)
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return err
}
return nil
@@ -100,7 +100,7 @@ func handleMysqlBackup(db DatabaseHelper, parentTask *task.Task, recordID uint,
backupTask.AddSubTaskWithOps(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 0, 3*time.Hour)
go func() {
if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err)
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return
}
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+1 -3
View File
@@ -39,12 +39,10 @@ func (u *BackupService) PostgresqlBackup(req dto.CommonBackup) error {
}
if err := backupRepo.CreateRecord(record); err != nil {
global.LOG.Errorf("save backup record failed, err: %v", err)
return err
}
databaseHelper := DatabaseHelper{Database: req.Name, DBType: req.Type, Name: req.DetailName}
if err := handlePostgresqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID, req.Secret); err != nil {
markBackupFailed(record.ID, err)
return err
}
return nil
@@ -90,7 +88,7 @@ func handlePostgresqlBackup(db DatabaseHelper, parentTask *task.Task, recordID u
backupTask.AddSubTaskWithOps(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 0, 3*time.Hour)
go func() {
if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err)
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return
}
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+1 -2
View File
@@ -59,7 +59,6 @@ func (u *BackupService) RedisBackup(req dto.CommonBackup) error {
}
if err := handleRedisBackup(redisInfo, nil, record.ID, backupDir, fileName, req.Secret, req.TaskID); err != nil {
markBackupFailed(record.ID, err)
return err
}
return nil
@@ -130,7 +129,7 @@ func handleRedisBackup(redisInfo *repo.RootInfo, parentTask *task.Task, recordID
itemTask.AddSubTaskWithOps(i18n.GetMsgByKey("TaskBackup"), backupDatabase, nil, 3, time.Hour)
go func() {
if err := itemTask.Execute(); err != nil {
markBackupFailed(recordID, err)
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return
}
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+1 -2
View File
@@ -54,7 +54,6 @@ func (u *BackupService) WebsiteBackup(req dto.CommonBackup) error {
return err
}
if err = handleWebsiteBackup(&website, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil {
markBackupFailed(record.ID, err)
global.LOG.Errorf("backup website %s failed, err: %v", website.Alias, err)
return err
}
@@ -236,7 +235,7 @@ func handleWebsiteBackup(website *model.Website, parentTask *task.Task, recordID
backupTask.AddSubTaskWithOps(task.GetTaskName(website.Alias, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 3, time.Hour)
go func() {
if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err)
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return
}
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+238 -137
View File
@@ -15,7 +15,6 @@ import (
"os/exec"
"path"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
@@ -33,6 +32,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/build"
"github.com/docker/docker/api/types/container"
@@ -53,8 +53,6 @@ import (
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)
List() []dto.ContainerOptions
@@ -485,19 +483,15 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
if err != nil {
return err
}
unlock := containerOperationLock.lock(req.Name)
defer client.Close()
ctx := context.Background()
newContainer, _ := client.ContainerInspect(ctx, req.Name)
if newContainer.ContainerJSONBase != nil {
unlock()
_ = client.Close()
return buserr.New("ErrContainerName")
}
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeContainer, req.TaskID, 1)
if err != nil {
unlock()
_ = client.Close()
global.LOG.Errorf("new task for create container failed, err: %v", err)
return err
}
@@ -533,20 +527,18 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
if err != nil {
return err
}
normalizeContainerEndpointSettings(ctx, client, networkConf, nil)
removeUnsupportedEndpointStaticIPAM(client, networkConf, nil)
con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
if con.ID != "" {
_ = client.ContainerRemove(ctx, con.ID, container.RemoveOptions{RemoveVolumes: true, Force: true})
}
_ = client.ContainerRemove(ctx, req.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
return err
}
err = client.ContainerStart(ctx, con.ID, container.StartOptions{})
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err)
if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
_ = client.ContainerRemove(ctx, con.ID, container.RemoveOptions{RemoveVolumes: true, Force: true})
_ = client.ContainerRemove(ctx, req.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
return fmt.Errorf("create successful but start failed, err: %v", err)
}
return nil
@@ -554,16 +546,12 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
if inThread {
go func() {
defer unlock()
defer client.Close()
if err := taskItem.Execute(); err != nil {
global.LOG.Error(err.Error())
}
}()
return nil
}
defer unlock()
defer client.Close()
return taskItem.Execute()
}
@@ -584,7 +572,21 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
data.Image = oldContainer.Config.Image
if oldContainer.NetworkSettings != nil {
for net, val := range oldContainer.NetworkSettings.Networks {
data.Networks = append(data.Networks, loadContainerNetworkInfo(net, val))
netItem := dto.ContainerNetwork{
Network: net,
MacAddr: val.MacAddress,
}
if val.IPAMConfig != nil {
if netItem.Network != "bridge" {
netItem.Ipv4 = val.IPAMConfig.IPv4Address
netItem.Ipv6 = val.IPAMConfig.IPv6Address
}
} else {
if netItem.Network != "bridge" {
netItem.Ipv4 = val.IPAddress
}
}
data.Networks = append(data.Networks, netItem)
}
}
@@ -630,40 +632,141 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
return &data, nil
}
func loadContainerNetworkInfo(name string, endpoint *network.EndpointSettings) dto.ContainerNetwork {
item := dto.ContainerNetwork{Network: name}
if endpoint == nil {
return item
func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
item.MacAddr = endpoint.MacAddress
item.Links = append([]string(nil), endpoint.Links...)
item.Aliases = append([]string(nil), endpoint.Aliases...)
item.DriverOpts = cloneStringMap(endpoint.DriverOpts)
item.GwPriority = endpoint.GwPriority
if endpoint.IPAMConfig != nil {
item.LinkLocalIPs = append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...)
defer client.Close()
ctx := context.Background()
oldContainer, err := client.ContainerInspect(ctx, req.Name)
if err != nil {
return err
}
if name != "bridge" {
if endpoint.IPAMConfig != nil {
item.Ipv4 = endpoint.IPAMConfig.IPv4Address
item.Ipv6 = endpoint.IPAMConfig.IPv6Address
} else {
item.Ipv4 = endpoint.IPAddress
item.Ipv6 = endpoint.GlobalIPv6Address
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskUpdate, task.TaskScopeContainer, req.TaskID, 1)
if err != nil {
global.LOG.Errorf("new task for create container failed, err: %v", err)
return err
}
go func() {
taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error {
if !checkImageExist(client, req.Image) || req.ForcePull {
if err := pullImages(taskItem, client, req.Image); err != nil {
if !req.ForcePull {
return err
}
return fmt.Errorf("pull image %s failed, err: %v", req.Image, err)
}
}
return nil
}, nil)
taskItem.AddSubTask(i18n.GetWithName("ContainerCreate", req.Name), func(t *task.Task) error {
err := client.ContainerRemove(ctx, req.Name, container.RemoveOptions{Force: true})
taskItem.LogWithStatus(i18n.GetWithName("ContainerRemoveOld", req.Name), err)
if err != nil {
return err
}
config, hostConf, networkConf, err := loadConfigInfo(false, req, &oldContainer)
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerLoadInfo"), err)
if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerRecreate"))
reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings)
return err
}
removeUnsupportedEndpointStaticIPAM(client, networkConf, nil)
con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerRecreate"))
reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings)
return fmt.Errorf("update container failed, err: %v", err)
}
err = client.ContainerStart(ctx, con.ID, container.StartOptions{})
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err)
if err != nil {
return fmt.Errorf("update successful but start failed, err: %v", err)
}
return nil
}, nil)
if err := taskItem.Execute(); err != nil {
global.LOG.Error(err.Error())
}
}
return item
}()
return nil
}
func cloneStringMap(source map[string]string) map[string]string {
if len(source) == 0 {
return nil
func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
result := make(map[string]string, len(source))
for key, value := range source {
result[key] = value
defer client.Close()
ctx := context.Background()
taskItem, err := task.NewTaskWithOps(req.Image, task.TaskUpgrade, task.TaskScopeImage, req.TaskID, 1)
if err != nil {
global.LOG.Errorf("new task for create container failed, err: %v", err)
return err
}
return result
go func() {
taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error {
taskItem.LogStart(i18n.GetWithName("ContainerImagePull", req.Image))
if !checkImageExist(client, req.Image) || req.ForcePull {
if err := pullImages(taskItem, client, req.Image); err != nil {
if !req.ForcePull {
return err
}
return fmt.Errorf("pull image %s failed, err: %v", req.Image, err)
}
}
return nil
}, nil)
for _, item := range req.Names {
var oldContainer container.InspectResponse
taskItem.AddSubTask(i18n.GetWithName("ContainerLoadInfo", item), func(t *task.Task) error {
taskItem.Logf("----------------- %s -----------------", item)
oldContainer, err = client.ContainerInspect(ctx, item)
if err != nil {
return err
}
return nil
}, nil)
taskItem.AddSubTask(i18n.GetWithName("ContainerCreate", item), func(t *task.Task) error {
config := oldContainer.Config
config.Image = req.Image
hostConf := oldContainer.HostConfig
err := client.ContainerRemove(ctx, item, container.RemoveOptions{Force: true})
taskItem.LogWithStatus(i18n.GetWithName("ContainerRemoveOld", item), err)
if err != nil {
return err
}
con, err := createContainerWithOldNetworks(ctx, client, config, hostConf, oldContainer.NetworkSettings, item)
if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerRecreate"))
reCreateAfterUpdate(item, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings)
return fmt.Errorf("upgrade container failed, err: %v", err)
}
err = client.ContainerStart(ctx, con.ID, container.StartOptions{})
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err)
if err != nil {
return fmt.Errorf("upgrade successful but start failed, err: %v", err)
}
return nil
}, nil)
}
if err := taskItem.Execute(); err != nil {
global.LOG.Error(err.Error())
}
}()
return nil
}
func (u *ContainerService) ContainerRename(req dto.ContainerRename) error {
@@ -673,8 +776,6 @@ func (u *ContainerService) ContainerRename(req dto.ContainerRename) error {
return err
}
defer client.Close()
unlock := containerOperationLock.lock(req.Name, req.NewName)
defer unlock()
newContainer, _ := client.ContainerInspect(ctx, req.NewName)
if newContainer.ContainerJSONBase != nil {
@@ -719,48 +820,44 @@ func (u *ContainerService) ContainerCommit(req dto.ContainerCommit) error {
}
func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error {
var err error
ctx := context.Background()
client, err := docker.NewDockerClient()
if err != nil {
return err
}
defer client.Close()
taskItem, err := task.NewTaskWithOps(strings.Join(req.Names, " "), req.Operation, task.TaskScopeContainer, req.TaskID, 1)
if err != nil {
_ = client.Close()
return fmt.Errorf("new task for container commit failed, err: %v", err)
}
for _, item := range req.Names {
item := item
taskItem.AddSubTask(item, func(t *task.Task) error {
unlock := containerOperationLock.lock(item)
defer unlock()
var operationErr error
switch req.Operation {
case constant.ContainerOpStart:
operationErr = client.ContainerStart(ctx, item, container.StartOptions{})
err = client.ContainerStart(ctx, item, container.StartOptions{})
case constant.ContainerOpStop:
operationErr = client.ContainerStop(ctx, item, container.StopOptions{})
err = client.ContainerStop(ctx, item, container.StopOptions{})
case constant.ContainerOpRestart:
operationErr = client.ContainerRestart(ctx, item, container.StopOptions{})
err = client.ContainerRestart(ctx, item, container.StopOptions{})
case constant.ContainerOpKill:
operationErr = client.ContainerKill(ctx, item, "SIGKILL")
err = client.ContainerKill(ctx, item, "SIGKILL")
case constant.ContainerOpPause:
operationErr = client.ContainerPause(ctx, item)
err = client.ContainerPause(ctx, item)
case constant.ContainerOpUnpause:
operationErr = client.ContainerUnpause(ctx, item)
err = client.ContainerUnpause(ctx, item)
case constant.ContainerOpRemove:
operationErr = client.ContainerRemove(ctx, item, container.RemoveOptions{RemoveVolumes: true, Force: true})
err = client.ContainerRemove(ctx, item, container.RemoveOptions{RemoveVolumes: true, Force: true})
}
return operationErr
return err
}, nil)
}
go func() {
defer client.Close()
_ = taskItem.Execute()
}()
return nil
return err
}
func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
@@ -769,8 +866,6 @@ func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
return err
}
defer client.Close()
unlock := containerOperationLock.lock(req.Name)
defer unlock()
ctx := context.Background()
containerItem, err := client.ContainerInspect(ctx, req.Name)
if err != nil {
@@ -941,7 +1036,6 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
if cmd.CheckIllegal(container, since, tail) {
return buserr.New("ErrCmdIllegal")
}
ctx := c.Request.Context()
commandArg := []string{"logs", container}
dockerCommand := global.CONF.DockerConfig.Command
if containerType == "compose" {
@@ -971,11 +1065,10 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
}
var dockerCmd *exec.Cmd
if containerType == "compose" && dockerCommand == "docker-compose" {
dockerCmd = exec.CommandContext(ctx, "docker-compose", commandArg...)
dockerCmd = exec.Command("docker-compose", commandArg...)
} else {
dockerCmd = exec.CommandContext(ctx, "docker", commandArg...)
dockerCmd = exec.Command("docker", commandArg...)
}
dockerCmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
stdout, err := dockerCmd.StdoutPipe()
if err != nil {
return err
@@ -984,20 +1077,15 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
if err := dockerCmd.Start(); err != nil {
return err
}
done := make(chan struct{})
go func() {
select {
case <-ctx.Done():
killContainerLogProcess(dockerCmd)
case <-done:
defer func() {
if dockerCmd.Process != nil {
_ = dockerCmd.Process.Kill()
_ = dockerCmd.Wait()
}
}()
defer close(done)
tempFile, err := os.CreateTemp("", "cmd_output_*.txt")
if err != nil {
killContainerLogProcess(dockerCmd)
_ = dockerCmd.Wait()
return err
}
defer tempFile.Close()
@@ -1006,19 +1094,31 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
global.LOG.Errorf("os.Remove() failed: %v", err)
}
}()
copyErr := copyContainerLogOutput(tempFile, stdout)
waitErr := dockerCmd.Wait()
if copyErr != nil {
return copyErr
}
if waitErr != nil {
if ctx.Err() != nil {
return ctx.Err()
errCh := make(chan error, 1)
go func() {
scanner := bufio.NewScanner(stdout)
var ansiRegex = re.GetRegex(re.AnsiEscapePattern)
for scanner.Scan() {
line := scanner.Text()
cleanLine := ansiRegex.ReplaceAllString(line, "")
if _, err := tempFile.WriteString(cleanLine + "\n"); err != nil {
errCh <- err
return
}
}
return waitErr
}
if _, err := tempFile.Seek(0, io.SeekStart); err != nil {
return err
if err := scanner.Err(); err != nil {
errCh <- err
return
}
errCh <- nil
}()
select {
case err := <-errCh:
if err != nil {
global.LOG.Errorf("Error: %v", err)
}
case <-time.After(40 * time.Second):
global.LOG.Errorf("Download container logs timeout reached")
}
info, _ := tempFile.Stat()
@@ -1028,36 +1128,6 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
return nil
}
func copyContainerLogOutput(dst io.Writer, src io.Reader) error {
reader := bufio.NewReader(src)
for {
line, err := reader.ReadString('\n')
if len(line) > 0 {
cleanLine := containerLogAnsiRegex.ReplaceAllString(line, "")
if _, writeErr := io.WriteString(dst, cleanLine); writeErr != nil {
return writeErr
}
}
if err != nil {
if err == io.EOF {
return nil
}
return err
}
}
}
func killContainerLogProcess(command *exec.Cmd) {
if command == nil || command.Process == nil {
return
}
if pgid, err := syscall.Getpgid(command.Process.Pid); err == nil {
_ = syscall.Kill(-pgid, syscall.SIGKILL)
return
}
_ = command.Process.Kill()
}
func (u *ContainerService) ContainerStats(id string) (*dto.ContainerStats, error) {
client, err := docker.NewDockerClient()
if err != nil {
@@ -1735,7 +1805,7 @@ func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerLis
return data
}
func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error) {
func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) {
portMap := make(nat.PortMap)
if len(ports) == 0 {
return portMap, nil
@@ -1760,7 +1830,7 @@ func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error
portMap[nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))] = []nat.PortBinding{bindItem}
}
for i := hostStart; i <= hostEnd; i++ {
if checkInUse && common.ScanPortWithIP(port.HostIP, i) {
if common.ScanPortWithIP(port.HostIP, i) {
return portMap, buserr.WithDetail("ErrPortInUsed", i, nil)
}
}
@@ -1771,7 +1841,7 @@ func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error
} else {
portItem, _ = strconv.Atoi(port.HostPort)
}
if checkInUse && common.ScanPortWithIP(port.HostIP, portItem) {
if common.ScanPortWithIP(port.HostIP, portItem) {
return portMap, buserr.WithDetail("ErrPortInUsed", portItem, nil)
}
bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP}
@@ -1790,7 +1860,7 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta
}
var networkConf network.NetworkingConfig
portMap, err := checkPortStats(req.ExposedPorts, isCreate)
portMap, err := checkPortStats(req.ExposedPorts)
if err != nil {
return nil, nil, nil, err
}
@@ -1818,21 +1888,15 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta
case "host", "none", "bridge":
hostConf.NetworkMode = container.NetworkMode(item.Network)
}
endpoint := &network.EndpointSettings{
Links: append([]string(nil), item.Links...),
Aliases: append([]string(nil), item.Aliases...),
DriverOpts: cloneStringMap(item.DriverOpts),
GwPriority: item.GwPriority,
MacAddress: item.MacAddr,
if item.Ipv4 != "" || item.Ipv6 != "" {
networkConf.EndpointsConfig[item.Network] = &network.EndpointSettings{
IPAMConfig: &network.EndpointIPAMConfig{
IPv4Address: item.Ipv4,
IPv6Address: item.Ipv6,
}, MacAddress: item.MacAddr}
} else {
networkConf.EndpointsConfig[item.Network] = &network.EndpointSettings{}
}
if item.Ipv4 != "" || item.Ipv6 != "" || len(item.LinkLocalIPs) != 0 {
endpoint.IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: item.Ipv4,
IPv6Address: item.Ipv6,
LinkLocalIPs: append([]string(nil), item.LinkLocalIPs...),
}
}
networkConf.EndpointsConfig[item.Network] = endpoint
}
} else {
return nil, nil, nil, fmt.Errorf("please set up the network")
@@ -1879,6 +1943,43 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta
return &config, &hostConf, &networkConf, nil
}
func reCreateAfterUpdate(name string, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkConf *container.NetworkSettings) {
ctx := context.Background()
oldContainer, err := createContainerWithOldNetworks(ctx, client, config, hostConf, networkConf, name)
if err != nil {
global.LOG.Errorf("recreate after container update failed, err: %v", err)
return
}
if err := client.ContainerStart(ctx, oldContainer.ID, container.StartOptions{}); err != nil {
global.LOG.Errorf("restart after container update failed, err: %v", err)
}
global.LOG.Info("recreate after container update successful")
}
func createContainerWithOldNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkSettings *container.NetworkSettings, name string) (container.CreateResponse, error) {
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(networkSettings, hostConf)
removeUnsupportedEndpointStaticIPAM(client, networkConf, extraNetworks)
created, err := client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name)
if err != nil {
return created, err
}
extraNames := make([]string, 0, len(extraNetworks))
for item := range extraNetworks {
extraNames = append(extraNames, item)
}
sort.Strings(extraNames)
for _, item := range extraNames {
if err := client.NetworkConnect(ctx, item, created.ID, extraNetworks[item]); err != nil {
_ = client.ContainerRemove(ctx, created.ID, container.RemoveOptions{Force: true})
return created, err
}
}
return created, nil
}
func loadVolumeBinds(binds []container.MountPoint) []dto.VolumeHelper {
var datas []dto.VolumeHelper
for _, bind := range binds {
-4
View File
@@ -270,10 +270,6 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
if stdout, err := compose.Up(req.Path); err != nil {
return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err)
}
} else if req.Operation == "rebuild" {
if stdout, err := compose.DownAndUp(req.Path); err != nil {
return fmt.Errorf("docker-compose rebuild failed, std: %s, err: %v", stdout, err)
}
} else {
if stdout, err := compose.Operate(req.Path, req.Operation); err != nil {
return fmt.Errorf("docker-compose %s failed, std: %s, err: %v", req.Operation, stdout, err)
-657
View File
@@ -1,657 +0,0 @@
package service
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/global"
"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/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
ctx := context.Background()
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskUpdate, task.TaskScopeContainer, req.TaskID, 1)
if err != nil {
_ = client.Close()
global.LOG.Errorf("new task for create container failed, err: %v", err)
return err
}
go func() {
defer client.Close()
taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error {
if !checkImageExist(client, req.Image) || req.ForcePull {
if err := pullImages(taskItem, client, req.Image); err != nil {
if !req.ForcePull {
return err
}
return fmt.Errorf("pull image %s failed, err: %v", req.Image, err)
}
}
return nil
}, nil)
taskItem.AddSubTaskWithOps(task.GetTaskName(req.Name, task.TaskUpdate, task.TaskScopeContainer), func(t *task.Task) error {
t.LogStart(i18n.GetWithName("ContainerAcquireLock", req.Name))
unlock := containerOperationLock.lock(req.Name)
defer unlock()
t.LogWithStatus(i18n.GetWithName("ContainerAcquireLock", req.Name), nil)
oldContainer, err := client.ContainerInspect(ctx, req.Name)
if err != nil {
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerLoadInfo"), err)
return err
}
config, hostConf, networkConf, err := loadConfigInfo(false, req, &oldContainer)
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerLoadInfo"), err)
if err != nil {
return err
}
normalizeContainerEndpointSettings(ctx, client, networkConf, nil)
cleanupErr, err := switchContainer(ctx, client, req.Name, oldContainer, func() (container.CreateResponse, error) {
return createContainerWithDynamicIPFallback(func() (container.CreateResponse, error) {
return client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
}, networkConf.EndpointsConfig, oldContainer.NetworkSettings)
}, newContainerSwitchTaskLogger(t))
if err != nil {
return fmt.Errorf("update container failed, err: %v", err)
}
if cleanupErr != nil {
taskItem.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr))
}
return nil
}, nil, 0, 0)
if err := taskItem.Execute(); err != nil {
global.LOG.Error(err.Error())
}
}()
return nil
}
func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
ctx := context.Background()
taskItem, err := task.NewTaskWithOps(req.Image, task.TaskUpgrade, task.TaskScopeImage, req.TaskID, 1)
if err != nil {
_ = client.Close()
global.LOG.Errorf("new task for create container failed, err: %v", err)
return err
}
go func() {
defer client.Close()
taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error {
taskItem.LogStart(i18n.GetWithName("ContainerImagePull", req.Image))
if !checkImageExist(client, req.Image) || req.ForcePull {
if err := pullImages(taskItem, client, req.Image); err != nil {
if !req.ForcePull {
return err
}
return fmt.Errorf("pull image %s failed, err: %v", req.Image, err)
}
}
return nil
}, nil)
var upgradeErrors []error
for _, item := range req.Names {
item := item
taskItem.AddSubTaskWithIgnoreErr(i18n.GetWithName("ContainerUpgradeItem", item), func(t *task.Task) error {
t.Logf("----------------- %s -----------------", item)
t.LogStart(i18n.GetWithName("ContainerAcquireLock", item))
unlock := containerOperationLock.lock(item)
defer unlock()
t.LogWithStatus(i18n.GetWithName("ContainerAcquireLock", item), nil)
oldContainer, inspectErr := client.ContainerInspect(ctx, item)
t.LogWithStatus(i18n.GetWithName("ContainerLoadInfo", item), inspectErr)
if inspectErr != nil {
err := fmt.Errorf("reload container %s failed: %w", item, inspectErr)
upgradeErrors = append(upgradeErrors, err)
return err
}
config := cloneContainerConfig(oldContainer.Config)
config.Image = req.Image
hostConf := cloneContainerHostConfig(oldContainer.HostConfig)
preserveContainerVolumeMounts(hostConf, oldContainer.Mounts)
cleanupErr, err := switchContainer(ctx, client, item, oldContainer, func() (container.CreateResponse, error) {
return createContainerWithOldNetworks(ctx, client, config, hostConf, oldContainer.NetworkSettings, item)
}, newContainerSwitchTaskLogger(t))
if err != nil {
upgradeErr := fmt.Errorf("upgrade container %s failed: %w", item, err)
upgradeErrors = append(upgradeErrors, upgradeErr)
return upgradeErr
}
if cleanupErr != nil {
t.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr))
}
return nil
})
}
taskItem.AddSubTask(i18n.GetMsgByKey("ContainerUpgradeSummary"), func(t *task.Task) error {
return errors.Join(upgradeErrors...)
}, nil)
if err := taskItem.Execute(); err != nil {
global.LOG.Error(err.Error())
}
}()
return nil
}
type containerSwitchClient interface {
ContainerStop(context.Context, string, container.StopOptions) error
ContainerRename(context.Context, string, string) error
ContainerStart(context.Context, string, container.StartOptions) error
ContainerRemove(context.Context, string, container.RemoveOptions) error
ContainerInspect(context.Context, string) (container.InspectResponse, error)
NetworkConnect(context.Context, string, string, *network.EndpointSettings) error
NetworkDisconnect(context.Context, string, string, bool) error
}
type containerOperationMutex struct {
mutex sync.Mutex
locks map[string]*containerOperationLockEntry
}
var containerOperationLock containerOperationMutex
type containerOperationLockEntry struct {
mutex sync.Mutex
references int
}
func (l *containerOperationMutex) lock(names ...string) func() {
nameSet := make(map[string]struct{}, len(names))
for _, name := range names {
if name != "" {
nameSet[name] = struct{}{}
}
}
orderedNames := make([]string, 0, len(nameSet))
for name := range nameSet {
orderedNames = append(orderedNames, name)
}
sort.Strings(orderedNames)
if len(orderedNames) == 0 {
return func() {}
}
l.mutex.Lock()
if l.locks == nil {
l.locks = make(map[string]*containerOperationLockEntry)
}
entries := make([]*containerOperationLockEntry, 0, len(orderedNames))
for _, name := range orderedNames {
entry := l.locks[name]
if entry == nil {
entry = &containerOperationLockEntry{}
l.locks[name] = entry
}
entry.references++
entries = append(entries, entry)
}
l.mutex.Unlock()
for _, entry := range entries {
entry.mutex.Lock()
}
return func() {
for index := len(entries) - 1; index >= 0; index-- {
entries[index].mutex.Unlock()
}
l.mutex.Lock()
defer l.mutex.Unlock()
for index, name := range orderedNames {
entry := entries[index]
entry.references--
if entry.references == 0 {
delete(l.locks, name)
}
}
}
}
type containerNetworkAttachment struct {
name string
endpoint *network.EndpointSettings
isDynamic bool
}
type containerSwitchLogFunc func(messageKey, containerName string, err error)
func newContainerSwitchTaskLogger(t *task.Task) containerSwitchLogFunc {
return func(messageKey, containerName string, err error) {
t.LogWithStatus(i18n.GetWithName(messageKey, containerName), err)
}
}
func logContainerSwitchStep(logger containerSwitchLogFunc, messageKey, containerName string, err error) {
if logger != nil {
logger(messageKey, containerName, err)
}
}
// switchContainer keeps the stopped original container as the rollback target until the replacement starts.
func switchContainer(
ctx context.Context,
cli containerSwitchClient,
name string,
oldContainer container.InspectResponse,
createNew func() (container.CreateResponse, error),
logger containerSwitchLogFunc,
) (cleanupErr error, err error) {
if oldContainer.ID == "" {
return nil, fmt.Errorf("original container ID is empty")
}
wasRunning := oldContainer.State != nil && oldContainer.State.Running
if wasRunning && oldContainer.HostConfig != nil && oldContainer.HostConfig.AutoRemove {
return nil, fmt.Errorf("cannot safely replace container %s with auto-remove enabled", name)
}
if wasRunning {
if err := cli.ContainerStop(ctx, oldContainer.ID, container.StopOptions{}); err != nil {
logContainerSwitchStep(logger, "ContainerStopOld", name, err)
current, inspectErr := cli.ContainerInspect(ctx, oldContainer.ID)
if inspectErr == nil && current.State != nil && !current.State.Running {
restartErr := restartOriginalContainer(ctx, cli, oldContainer.ID)
logContainerSwitchStep(logger, "ContainerRollbackRestartOld", name, restartErr)
return nil, errors.Join(fmt.Errorf("stop original container failed: %w", err), restartErr)
}
return nil, fmt.Errorf("stop original container failed: %w", err)
}
logContainerSwitchStep(logger, "ContainerStopOld", name, nil)
}
backupName := containerSwitchBackupName(oldContainer.ID)
if err := cli.ContainerRename(ctx, oldContainer.ID, backupName); err != nil {
logContainerSwitchStep(logger, "ContainerRenameOld", name, err)
if wasRunning {
restartErr := restartOriginalContainer(ctx, cli, oldContainer.ID)
logContainerSwitchStep(logger, "ContainerRollbackRestartOld", name, restartErr)
return nil, errors.Join(fmt.Errorf("rename original container failed: %w", err), restartErr)
}
return nil, fmt.Errorf("rename original container failed: %w", err)
}
logContainerSwitchStep(logger, "ContainerRenameOld", name, nil)
disconnectedNetworks, disconnectErr := disconnectOriginalContainerNetworks(ctx, cli, oldContainer)
logContainerSwitchStep(logger, "ContainerDisconnectOld", backupName, disconnectErr)
if disconnectErr != nil {
rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, "", disconnectedNetworks, logger)
return nil, errors.Join(disconnectErr, rollbackErr)
}
created, createErr := createNew()
logContainerSwitchStep(logger, "ContainerCreateReplacement", name, createErr)
if createErr != nil {
rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger)
return nil, errors.Join(createErr, rollbackErr)
}
if err := cli.ContainerStart(ctx, created.ID, container.StartOptions{}); err != nil {
logContainerSwitchStep(logger, "ContainerStartReplacement", name, err)
rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger)
return nil, errors.Join(fmt.Errorf("start new container failed: %w", err), rollbackErr)
}
logContainerSwitchStep(logger, "ContainerStartReplacement", name, nil)
if wasRunning {
if err := waitContainerReady(ctx, cli, created.ID); err != nil {
logContainerSwitchStep(logger, "ContainerWaitReplacement", name, err)
rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger)
return nil, errors.Join(fmt.Errorf("new container readiness check failed: %w", err), rollbackErr)
}
logContainerSwitchStep(logger, "ContainerWaitReplacement", name, nil)
}
cleanupErr = cli.ContainerRemove(ctx, oldContainer.ID, container.RemoveOptions{Force: true, RemoveVolumes: false})
logContainerSwitchStep(logger, "ContainerRemoveOld", backupName, cleanupErr)
return cleanupErr, nil
}
const (
containerStartStabilization = 10 * time.Second
containerStartPollInterval = time.Second
containerHealthCheckMinWait = 30 * time.Second
containerHealthCheckMaxWait = 10 * time.Minute
)
func waitContainerReady(ctx context.Context, cli containerSwitchClient, containerID string) error {
info, err := cli.ContainerInspect(ctx, containerID)
if err != nil {
return err
}
if err := checkContainerRunningState(info); err != nil {
return err
}
if info.State.Health == nil {
return waitContainerStable(ctx, cli, containerID, info)
}
timeout := containerHealthCheckTimeout(info.Config)
deadline := time.NewTimer(timeout)
ticker := time.NewTicker(time.Second)
defer deadline.Stop()
defer ticker.Stop()
for {
if info.State.Restarting || info.RestartCount != 0 {
return fmt.Errorf("container restarted %d times during startup", info.RestartCount)
}
if info.State.Health == nil {
return fmt.Errorf("container health status is unavailable")
}
switch info.State.Health.Status {
case container.Healthy:
return nil
case container.Unhealthy:
return fmt.Errorf("container health status is unhealthy")
}
select {
case <-ctx.Done():
return ctx.Err()
case <-deadline.C:
return fmt.Errorf("container health check timed out after %s", timeout)
case <-ticker.C:
info, err = cli.ContainerInspect(ctx, containerID)
if err != nil {
return err
}
if err := checkContainerRunningState(info); err != nil {
return err
}
}
}
}
func waitContainerStable(ctx context.Context, cli containerSwitchClient, containerID string, initial container.InspectResponse) error {
startedAt := initial.State.StartedAt
if err := checkContainerStableState(initial, startedAt); err != nil {
return err
}
deadline := time.NewTimer(containerStartStabilization)
ticker := time.NewTicker(containerStartPollInterval)
defer deadline.Stop()
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-deadline.C:
info, err := cli.ContainerInspect(ctx, containerID)
if err != nil {
return err
}
return checkContainerStableState(info, startedAt)
case <-ticker.C:
info, err := cli.ContainerInspect(ctx, containerID)
if err != nil {
return err
}
if err := checkContainerStableState(info, startedAt); err != nil {
return err
}
}
}
}
func checkContainerStableState(info container.InspectResponse, startedAt string) error {
if err := checkContainerRunningState(info); err != nil {
return err
}
if info.State.Restarting || info.RestartCount != 0 {
return fmt.Errorf("container restarted %d times during startup", info.RestartCount)
}
if startedAt != "" && info.State.StartedAt != startedAt {
return fmt.Errorf("container start time changed during startup")
}
return nil
}
func checkContainerRunningState(info container.InspectResponse) error {
if info.State == nil {
return fmt.Errorf("container state is unavailable")
}
if !info.State.Running {
return fmt.Errorf("container exited with code %d: %s", info.State.ExitCode, info.State.Error)
}
return nil
}
func containerHealthCheckTimeout(config *container.Config) time.Duration {
if config == nil || config.Healthcheck == nil {
return containerHealthCheckMinWait
}
health := config.Healthcheck
interval := health.Interval
if interval <= 0 {
interval = 30 * time.Second
}
checkTimeout := health.Timeout
if checkTimeout <= 0 {
checkTimeout = 30 * time.Second
}
retries := health.Retries
if retries <= 0 {
retries = 3
}
timeout := health.StartPeriod + time.Duration(retries)*(interval+checkTimeout)
if timeout < containerHealthCheckMinWait {
return containerHealthCheckMinWait
}
if timeout > containerHealthCheckMaxWait {
return containerHealthCheckMaxWait
}
return timeout
}
func preserveContainerVolumeMounts(hostConfig *container.HostConfig, oldMounts []container.MountPoint) {
if hostConfig == nil {
return
}
for _, oldMount := range oldMounts {
if oldMount.Type != mount.TypeVolume || oldMount.Name == "" || oldMount.Destination == "" {
continue
}
preserved := false
for index := range hostConfig.Mounts {
if hostConfig.Mounts[index].Target == oldMount.Destination {
hostConfig.Mounts[index].Type = mount.TypeVolume
hostConfig.Mounts[index].Source = oldMount.Name
preserved = true
}
}
for index, raw := range hostConfig.Binds {
destination, mode := containerBindDestinationAndMode(raw)
if destination != oldMount.Destination {
continue
}
hostConfig.Binds[index] = oldMount.Name + ":" + oldMount.Destination
if mode != "" {
hostConfig.Binds[index] += ":" + mode
}
preserved = true
}
if !preserved {
hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{
Type: mount.TypeVolume,
Source: oldMount.Name,
Target: oldMount.Destination,
ReadOnly: !oldMount.RW,
})
}
}
}
func containerBindDestinationAndMode(raw string) (string, string) {
parts := strings.SplitN(raw, ":", 3)
switch len(parts) {
case 1:
return parts[0], ""
case 2:
return parts[1], ""
default:
return parts[1], parts[2]
}
}
func containerSwitchBackupName(containerID string) string {
if len(containerID) > 12 {
containerID = containerID[:12]
}
return "1panel-backup-" + containerID
}
func restartOriginalContainer(ctx context.Context, cli containerSwitchClient, containerID string) error {
if err := cli.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
return fmt.Errorf("restart original container failed: %w", err)
}
return nil
}
func disconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitchClient, oldContainer container.InspectResponse) ([]containerNetworkAttachment, error) {
primary, extras := buildContainerRecoverNetworkConfig(oldContainer.NetworkSettings, oldContainer.HostConfig)
endpoints := make(map[string]*network.EndpointSettings, len(extras)+1)
if primary != nil {
for name, endpoint := range primary.EndpointsConfig {
if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil {
endpoints[name] = endpoint
}
}
}
for name, endpoint := range extras {
if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil {
endpoints[name] = endpoint
}
}
names := make([]string, 0, len(endpoints))
for name := range endpoints {
names = append(names, name)
}
sort.Strings(names)
disconnected := make([]containerNetworkAttachment, 0, len(names))
for _, name := range names {
if err := cli.NetworkDisconnect(ctx, name, oldContainer.ID, true); err != nil {
return disconnected, fmt.Errorf("disconnect original container from network %s failed: %w", name, err)
}
disconnected = append(disconnected, containerNetworkAttachment{
name: name,
endpoint: endpoints[name],
isDynamic: isDynamicContainerNetwork(oldContainer.NetworkSettings, name),
})
}
return disconnected, nil
}
func reconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitchClient, containerID string, attachments []containerNetworkAttachment) error {
var reconnectErr error
for _, attachment := range attachments {
err := cli.NetworkConnect(ctx, attachment.name, containerID, attachment.endpoint)
if err != nil && attachment.isDynamic && strings.Contains(err.Error(), unsupportedUserSpecifiedIPAddress) {
attachment.endpoint.IPAMConfig = nil
err = cli.NetworkConnect(ctx, attachment.name, containerID, attachment.endpoint)
}
if err != nil {
reconnectErr = errors.Join(reconnectErr, fmt.Errorf("reconnect original container to network %s failed: %w", attachment.name, err))
}
}
return reconnectErr
}
func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, oldContainerID, originalName string, wasRunning bool, newContainer string, disconnectedNetworks []containerNetworkAttachment, logger containerSwitchLogFunc) error {
var rollbackErr error
backupName := containerSwitchBackupName(oldContainerID)
if newContainer != "" {
removeErr := cli.ContainerRemove(ctx, newContainer, container.RemoveOptions{Force: true, RemoveVolumes: true})
if client.IsErrNotFound(removeErr) {
removeErr = nil
}
logContainerSwitchStep(logger, "ContainerRollbackRemoveReplacement", originalName, removeErr)
if removeErr != nil {
rollbackErr = errors.Join(rollbackErr, fmt.Errorf("remove failed replacement container failed: %w", removeErr))
}
}
renameErr := cli.ContainerRename(ctx, oldContainerID, originalName)
logContainerSwitchStep(logger, "ContainerRollbackRenameOld", backupName, renameErr)
if renameErr != nil {
rollbackErr = errors.Join(rollbackErr, fmt.Errorf("restore original container name failed: %w", renameErr))
}
currentName := originalName
if renameErr != nil {
currentName = backupName
}
reconnectErr := reconnectOriginalContainerNetworks(ctx, cli, oldContainerID, disconnectedNetworks)
logContainerSwitchStep(logger, "ContainerRollbackReconnectOld", currentName, reconnectErr)
rollbackErr = errors.Join(rollbackErr, reconnectErr)
if wasRunning {
restartErr := restartOriginalContainer(ctx, cli, oldContainerID)
logContainerSwitchStep(logger, "ContainerRollbackRestartOld", currentName, restartErr)
rollbackErr = errors.Join(rollbackErr, restartErr)
}
return rollbackErr
}
func createContainerWithOldNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkSettings *container.NetworkSettings, name string) (container.CreateResponse, error) {
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(networkSettings, hostConf)
normalizeContainerEndpointSettings(ctx, client, networkConf, extraNetworks)
var primaryEndpoints map[string]*network.EndpointSettings
if networkConf != nil {
primaryEndpoints = networkConf.EndpointsConfig
}
created, err := createContainerWithDynamicIPFallback(func() (container.CreateResponse, error) {
return client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name)
}, primaryEndpoints, networkSettings)
if err != nil {
return created, err
}
extraNames := make([]string, 0, len(extraNetworks))
for item := range extraNetworks {
extraNames = append(extraNames, item)
}
sort.Strings(extraNames)
for _, item := range extraNames {
err := client.NetworkConnect(ctx, item, created.ID, extraNetworks[item])
if clearUnsupportedDynamicEndpointIPAM(err, map[string]*network.EndpointSettings{item: extraNetworks[item]}, networkSettings) {
err = client.NetworkConnect(ctx, item, created.ID, extraNetworks[item])
}
if err != nil {
_ = client.ContainerRemove(ctx, created.ID, container.RemoveOptions{Force: true})
return created, err
}
}
return created, nil
}
func createContainerWithDynamicIPFallback(
create func() (container.CreateResponse, error),
endpoints map[string]*network.EndpointSettings,
networkSettings *container.NetworkSettings,
) (container.CreateResponse, error) {
for {
created, err := create()
if err == nil || created.ID != "" || !clearUnsupportedDynamicEndpointIPAM(err, endpoints, networkSettings) {
return created, err
}
}
}
+1 -1
View File
@@ -644,7 +644,7 @@ func (u *CronjobService) HandleStop(id uint) error {
if len(record.TaskID) == 0 {
return nil
}
if cancel, ok := global.LoadTaskCancel(record.TaskID); ok {
if cancel, ok := global.TaskCtxMap[record.TaskID]; ok {
cancel()
}
return nil
+5 -5
View File
@@ -60,7 +60,7 @@ func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time, t
src := path.Join(backupDir, record.FileName)
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil {
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil {
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++
return err
@@ -119,7 +119,7 @@ func (u *CronjobService) handleWebsite(cronjob model.Cronjob, startTime time.Tim
src := path.Join(backupDir, record.FileName)
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil {
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil {
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++
return err
@@ -216,7 +216,7 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
src := path.Join(backupDir, record.FileName)
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil {
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil {
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++
return err
@@ -274,7 +274,7 @@ func (u *CronjobService) handleDirectory(cronjob model.Cronjob, startTime time.T
src := path.Join(backupDir, fileName)
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil {
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil {
return err
}
record.FileDir = path.Dir(dst)
@@ -310,7 +310,7 @@ func (u *CronjobService) handleSystemLog(cronjob model.Cronjob, startTime time.T
src := path.Join(path.Dir(backupDir), fileName)
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil {
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil {
return err
}
record.FileDir = path.Dir(dst)
+7 -16
View File
@@ -187,7 +187,6 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
hostInfo, _ := psutil.HOST.GetHostInfo(false)
currentInfo.Uptime = hostInfo.Uptime
currentInfo.TimeSinceUptime = time.Unix(int64(hostInfo.BootTime), 0).Format(constant.DateTimeLayout)
currentInfo.RunningTime = loadRunningTime(hostInfo.Uptime)
currentInfo.Procs = hostInfo.Procs
currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false)
@@ -229,12 +228,13 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
if ioOption == "all" {
diskInfo, _ := disk.IOCounters()
state := sumDiskIOCounters(diskInfo)
currentInfo.IOReadBytes = state.ReadBytes
currentInfo.IOWriteBytes = state.WriteBytes
currentInfo.IOCount = state.ReadCount + state.WriteCount
currentInfo.IOReadTime = state.ReadTime
currentInfo.IOWriteTime = state.WriteTime
for _, state := range diskInfo {
currentInfo.IOReadBytes += state.ReadBytes
currentInfo.IOWriteBytes += state.WriteBytes
currentInfo.IOCount += (state.ReadCount + state.WriteCount)
currentInfo.IOReadTime += state.ReadTime
currentInfo.IOWriteTime += state.WriteTime
}
} else {
diskInfo, _ := disk.IOCounters(ioOption)
for _, state := range diskInfo {
@@ -267,15 +267,6 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
return &currentInfo
}
func loadRunningTime(uptime uint64) dto.RunningTime {
return dto.RunningTime{
Days: uptime / 86400,
Hours: (uptime % 86400) / 3600,
Minutes: (uptime % 3600) / 60,
Seconds: uptime % 60,
}
}
func (u *DashboardService) LoadTopCPU() []dto.Process {
return loadTopCPU()
}
File diff suppressed because it is too large Load Diff
@@ -1,84 +0,0 @@
package service
import (
"fmt"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
)
func composePortEnvKeys(index int) (containerPort, hostPort, hostIP, protocol string) {
return fmt.Sprintf("CONTAINER_PORT_%d", index),
fmt.Sprintf("HOST_PORT_%d", index),
fmt.Sprintf("HOST_IP_%d", index),
fmt.Sprintf("PORT_PROTOCOL_%d", index)
}
func isComposePortEnvKey(key string) bool {
return strings.HasPrefix(key, "CONTAINER_PORT_") ||
strings.HasPrefix(key, "HOST_PORT_") ||
strings.HasPrefix(key, "HOST_IP_") ||
strings.HasPrefix(key, "PORT_PROTOCOL_")
}
func formatComposePortMapping(hostIP, hostPort, containerPort, protocol string) string {
return fmt.Sprintf("${%s}:${%s}:${%s}/%s", hostIP, hostPort, containerPort, normalizeComposeProtocol(protocol))
}
func normalizeComposeProtocol(protocol string) string {
switch strings.ToLower(strings.TrimSpace(protocol)) {
case "udp":
return "udp"
default:
return "tcp"
}
}
func formatComposeVolume(source, target, mode string) string {
return fmt.Sprintf("%s:%s:%s", source, target, normalizeComposeVolumeMode(mode))
}
func normalizeComposeVolumeMode(mode string) string {
switch strings.ToLower(strings.TrimSpace(mode)) {
case "ro":
return "ro"
default:
return "rw"
}
}
func loadComposeExposedPortsFromEnv(envs map[string]string, defaultHostIP string, strict bool) ([]request.ExposedPort, error) {
var ports []request.ExposedPort
for key, value := range envs {
if !strings.HasPrefix(key, "CONTAINER_PORT_") {
continue
}
index := strings.TrimPrefix(key, "CONTAINER_PORT_")
containerPort, err := strconv.Atoi(value)
if err != nil {
if strict {
return nil, err
}
continue
}
hostPort, err := strconv.Atoi(envs["HOST_PORT_"+index])
if err != nil {
if strict {
return nil, err
}
continue
}
hostIP := envs["HOST_IP_"+index]
if hostIP == "" {
hostIP = defaultHostIP
}
ports = append(ports, request.ExposedPort{
ContainerPort: containerPort,
HostPort: hostPort,
HostIP: hostIP,
Protocol: normalizeComposeProtocol(envs["PORT_PROTOCOL_"+index]),
})
}
return ports, nil
}
+4 -6
View File
@@ -19,12 +19,10 @@ var (
agentAccountRepo = repo.NewIAgentAccountRepo()
agentAccountModelRepo = repo.NewIAgentAccountModelRepo()
mysqlRepo = repo.NewIMysqlRepo()
postgresqlRepo = repo.NewIPostgresqlRepo()
mongodbRepo = repo.NewIMongodbRepo()
databaseRepo = repo.NewIDatabaseRepo()
databaseUserRepo = repo.NewIDatabaseUserRepo()
databaseUserGrantRepo = repo.NewIDatabaseUserGrantRepo()
mysqlRepo = repo.NewIMysqlRepo()
postgresqlRepo = repo.NewIPostgresqlRepo()
mongodbRepo = repo.NewIMongodbRepo()
databaseRepo = repo.NewIDatabaseRepo()
imageRepoRepo = repo.NewIImageRepoRepo()
composeRepo = repo.NewIComposeTemplateRepo()
+6 -15
View File
@@ -485,7 +485,7 @@ func preflightDecompressTool(decompressType files.CompressType) error {
}
func (f *FileService) StopCompress(taskID string) error {
if cancel, ok := global.LoadTaskCancel(taskID); ok {
if cancel, ok := global.TaskCtxMap[taskID]; ok {
cancel()
return nil
}
@@ -493,7 +493,7 @@ func (f *FileService) StopCompress(taskID string) error {
}
func (f *FileService) StopDeCompress(taskID string) error {
if cancel, ok := global.LoadTaskCancel(taskID); ok {
if cancel, ok := global.TaskCtxMap[taskID]; ok {
cancel()
return nil
}
@@ -621,16 +621,9 @@ func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr e
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
}
dstInfo, err := os.Lstat(dstPath)
keepExistingFile := err == nil && dstInfo.Mode().IsRegular()
if err != nil && !os.IsNotExist(err) {
if err := os.RemoveAll(dstPath); err != nil {
return err
}
if !keepExistingFile {
if err := os.RemoveAll(dstPath); err != nil {
return err
}
}
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
return err
}
@@ -654,10 +647,8 @@ func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr e
if _, err := io.Copy(dstFile, srcFile); err != nil {
return err
}
if !keepExistingFile {
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
return err
}
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
return err
}
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
}
@@ -1125,7 +1116,7 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
}
logFilePath = path.Join(global.Dir.LogDir, "ai", safeName)
default:
return nil, buserr.WithName("ErrNotSupportType", req.Type)
return nil, buserr.New("ErrNotSupportType")
}
file, err := os.Open(logFilePath)
+1 -33
View File
@@ -166,12 +166,6 @@ func (u *FirewallService) OperateFirewall(req dto.FirewallOperation) error {
if err != nil {
return err
}
fail2BanState := newFirewallFail2BanState()
if client.Name() == "firewalld" && req.Operation == "stop" {
if err := fail2BanState.rememberBeforeFirewallStop(); err != nil {
return err
}
}
needRestartDocker := false
switch req.Operation {
case "start":
@@ -216,11 +210,6 @@ func (u *FirewallService) OperateFirewall(req dto.FirewallOperation) error {
return fmt.Errorf("failed to restart Docker: %v", err)
}
}
if client.Name() == "firewalld" && req.Operation == "start" {
if err := fail2BanState.restoreAfterFirewallStart(); err != nil {
return err
}
}
return nil
}
@@ -233,7 +222,7 @@ func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool)
req.Chain = iptables.Chain1PanelBasic
}
protos := strings.Split(req.Protocol, "/")
itemAddress := splitFirewallRuleAddresses(req.Address)
itemAddress := strings.Split(strings.TrimSuffix(req.Address, ","), ",")
if client.Name() == "ufw" {
if strings.Contains(req.Port, ",") || strings.Contains(req.Port, "-") {
@@ -526,7 +515,6 @@ func (u *FirewallService) operatePort(client firewall.FirewallClient, req dto.Po
if err := copier.Copy(&fireInfo, &req); err != nil {
return err
}
fireInfo.Address = normalizeFirewallRuleAddress(fireInfo.Address)
if client.Name() == "ufw" {
if len(fireInfo.Address) != 0 && !strings.EqualFold(fireInfo.Address, "Anywhere") {
@@ -541,26 +529,6 @@ func (u *FirewallService) operatePort(client firewall.FirewallClient, req dto.Po
return client.Port(fireInfo, req.Operation)
}
func splitFirewallRuleAddresses(address string) []string {
parts := strings.Split(strings.TrimSuffix(address, ","), ",")
addresses := make([]string, 0, len(parts))
for _, part := range parts {
addresses = append(addresses, normalizeFirewallRuleAddress(part))
}
if len(addresses) == 0 {
return []string{""}
}
return addresses
}
func normalizeFirewallRuleAddress(address string) string {
address = strings.TrimSpace(address)
if strings.EqualFold(address, "Anywhere") {
return ""
}
return address
}
type portOfApp struct {
AppName string
HttpPort string
@@ -1,78 +0,0 @@
package service
import (
"fmt"
"os"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/controller"
)
const fail2BanRestoreWithFirewallMarker = "/run/1panel_fail2ban_restore_with_firewall"
type firewallFail2BanState struct {
markerPath string
isExist func(string) bool
isActive func(string) bool
start func(string) error
}
func newFirewallFail2BanState() *firewallFail2BanState {
return &firewallFail2BanState{
markerPath: fail2BanRestoreWithFirewallMarker,
isExist: func(serviceName string) bool {
exists, err := controller.CheckExist(serviceName)
if err != nil {
global.LOG.Warnf("check %s installation before stopping the firewall failed: %v", serviceName, err)
}
return exists
},
isActive: func(serviceName string) bool {
active, err := controller.CheckActive(serviceName)
if err != nil {
global.LOG.Warnf("check %s status before stopping the firewall failed: %v", serviceName, err)
}
return active
},
start: controller.HandleStart,
}
}
func (s *firewallFail2BanState) rememberBeforeFirewallStop() error {
if !s.isExist("fail2ban.service") {
return nil
}
if !s.isActive("fail2ban.service") {
return nil
}
return s.markForRestore()
}
func (s *firewallFail2BanState) markForRestore() error {
if err := os.WriteFile(s.markerPath, nil, 0600); err != nil {
return fmt.Errorf("mark Fail2Ban for restoration with the firewall: %w", err)
}
return nil
}
func (s *firewallFail2BanState) restoreAfterFirewallStart() error {
_, err := os.Stat(s.markerPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("load Fail2Ban restore marker after starting the firewall: %w", err)
}
if err := s.start("fail2ban.service"); err != nil {
return fmt.Errorf("restore Fail2Ban after starting the firewall: %w", err)
}
return s.clearRestoreMarker()
}
func (s *firewallFail2BanState) clearRestoreMarker() error {
if err := os.Remove(s.markerPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("clear Fail2Ban firewall restore status: %w", err)
}
return nil
}
+1 -1
View File
@@ -502,7 +502,7 @@ func (u *ImageService) ImageRemove(req dto.BatchDelete) error {
return err
}
defer client.Close()
taskItem, err := task.NewTaskWithOps(strings.Join(req.Names, ","), task.TaskDelete, task.TaskScopeImage, req.TaskID, 1)
taskItem, err := task.NewTaskWithOps(task.TaskScopeImage, task.TaskDelete, task.TaskScopeContainer, req.TaskID, 1)
if err != nil {
global.LOG.Errorf("new task for create container failed, err: %v", err)
return err
-465
View File
@@ -1,483 +1,18 @@
package service
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"os/exec"
"sort"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/re"
)
type LogService struct{}
const maxSystemLogCursorSkip = 100
const maxFileSystemLogLines = 1000
var (
syslogPriorityKeywords = []struct {
keyword string
priority string
}{
{"emerg", "0"}, {"panic", "0"}, {"alert", "1"}, {"crit", "2"}, {"fatal", "2"},
{"error", "3"}, {"err", "3"}, {"warn", "4"}, {"notice", "5"}, {"debug", "7"}, {"info", "6"},
}
)
type systemLogCursor struct {
EndTime int64 `json:"endTime"`
Skipped int `json:"skipped"`
JournalCursor string `json:"journalCursor,omitempty"`
}
type ILogService interface {
ListSystemLogFile() ([]string, error)
GetSystemLogStatus() (dto.SystemLogStatus, error)
ReadSystemLog(req dto.SystemLogReq) (dto.SystemLogRes, error)
ListRunningServices() ([]string, error)
}
func (u *LogService) ReadSystemLog(req dto.SystemLogReq) (dto.SystemLogRes, error) {
pageSize := req.PageSize
if pageSize == 0 {
pageSize = 100
}
now := time.Now()
startTime := req.StartTime
if startTime.IsZero() {
startTime = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
}
endTime := req.EndTime
if endTime.IsZero() {
endTime = now
}
cursor, err := decodeSystemLogCursor(req.Cursor)
if err != nil {
return dto.SystemLogRes{}, err
}
if cursor != nil && cursor.JournalCursor == "" {
endTime = time.Unix(0, cursor.EndTime*int64(time.Microsecond))
}
journalctl, err := exec.LookPath("journalctl")
if err != nil {
return u.readFileSystemLog(req, startTime, endTime, pageSize, cursor)
}
queryArgs := buildJournalQueryArgs(req, startTime, endTime, pageSize, cursor)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
output, err := exec.CommandContext(ctx, journalctl, queryArgs...).CombinedOutput()
cancel()
if err != nil {
if journalctlGrepUnsupported(req.Keyword, string(output)) {
return dto.SystemLogRes{}, buserr.New("ErrSystemLogKeywordFilterUnsupported")
}
return dto.SystemLogRes{}, fmt.Errorf("read host system logs failed: %s", strings.TrimSpace(string(output)))
}
content := strings.TrimSpace(string(output))
if content == "" || strings.HasPrefix(content, "-- No entries --") {
return dto.SystemLogRes{Source: "journalctl", Items: []dto.SystemLogItem{}}, nil
}
items := parseJournalLogItems(content)
if cursor != nil && cursor.JournalCursor == "" {
items, err = skipSystemLogCursorItems(items, *cursor)
if err != nil {
return dto.SystemLogRes{}, err
}
}
return buildSystemLogResponse("journalctl", items, pageSize, cursor)
}
func (u *LogService) GetSystemLogStatus() (dto.SystemLogStatus, error) {
journalctl, err := exec.LookPath("journalctl")
if err != nil {
return dto.SystemLogStatus{
Source: "file",
KeywordFilterSupported: true,
}, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
versionOutput, err := exec.CommandContext(ctx, journalctl, "--version").CombinedOutput()
cancel()
if err != nil {
return dto.SystemLogStatus{}, fmt.Errorf("read journalctl version failed: %s", strings.TrimSpace(string(versionOutput)))
}
ctx, cancel = context.WithTimeout(context.Background(), 3*time.Second)
helpOutput, err := exec.CommandContext(ctx, journalctl, "--help").CombinedOutput()
cancel()
if err != nil {
return dto.SystemLogStatus{}, fmt.Errorf("read journalctl capabilities failed: %s", strings.TrimSpace(string(helpOutput)))
}
supported := journalctlHelpSupportsGrep(string(helpOutput))
status := dto.SystemLogStatus{
Source: "journalctl",
Version: firstOutputLine(string(versionOutput)),
KeywordFilterSupported: supported,
}
if !supported {
status.Message = i18n.Get("ErrSystemLogKeywordFilterUnsupported")
}
return status, nil
}
func journalctlHelpSupportsGrep(help string) bool {
return strings.Contains(help, "--grep=")
}
func journalctlGrepUnsupported(keyword, output string) bool {
if strings.TrimSpace(keyword) == "" {
return false
}
output = strings.ToLower(output)
return strings.Contains(output, "grep") &&
(strings.Contains(output, "unrecognized option") || strings.Contains(output, "unknown option"))
}
func firstOutputLine(output string) string {
for _, line := range strings.Split(output, "\n") {
if line = strings.TrimSpace(line); line != "" {
return line
}
}
return ""
}
func buildJournalQueryArgs(req dto.SystemLogReq, startTime, endTime time.Time, pageSize int, cursor *systemLogCursor) []string {
args := []string{
"--no-pager", "--reverse", "--output=json",
"--since", formatJournalQueryTime(startTime),
}
if cursor != nil && cursor.JournalCursor != "" {
args = append(args, "--after-cursor="+cursor.JournalCursor)
} else {
args = append(args, "--until", formatJournalQueryTime(endTime))
}
if service := strings.TrimSpace(req.Service); service != "" {
args = append(args, "-u", service)
}
if priority := strings.TrimSpace(req.Priority); priority != "" {
args = append(args, "--priority", priority+".."+priority)
}
if keyword := strings.TrimSpace(req.Keyword); keyword != "" {
args = append(args, "--grep", keyword)
}
queryLines := pageSize + 1
if cursor != nil && cursor.JournalCursor == "" {
queryLines += cursor.Skipped
}
return append(args, "--lines="+strconv.Itoa(queryLines))
}
func (u *LogService) readFileSystemLog(req dto.SystemLogReq, startTime, endTime time.Time, pageSize int, cursor *systemLogCursor) (dto.SystemLogRes, error) {
items := make([]dto.SystemLogItem, 0)
for _, logFile := range []string{"/var/log/syslog", "/var/log/messages", "/var/log/system.log"} {
if _, err := os.Stat(logFile); err != nil {
continue
}
content, err := readLastSystemLogLines(logFile, maxFileSystemLogLines)
if err != nil {
return dto.SystemLogRes{}, err
}
for _, line := range strings.Split(content, "\n") {
item, ok := parseFileSystemLogItem(line)
if !ok || !matchSystemLogItem(item, req, startTime, endTime) {
continue
}
items = append(items, item)
}
}
sort.SliceStable(items, func(i, j int) bool { return items[i].Timestamp > items[j].Timestamp })
if cursor != nil {
var err error
items, err = skipSystemLogCursorItems(items, *cursor)
if err != nil {
return dto.SystemLogRes{}, err
}
}
return buildSystemLogResponse("file", items, pageSize, cursor)
}
func readLastSystemLogLines(logFile string, lines int) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
output, err := exec.CommandContext(ctx, "tail", "-n", strconv.Itoa(lines), logFile).Output()
if err != nil {
return "", err
}
return string(output), nil
}
func buildSystemLogResponse(source string, items []dto.SystemLogItem, pageSize int, cursor *systemLogCursor) (dto.SystemLogRes, error) {
hasMore := len(items) > pageSize
if hasMore {
items = items[:pageSize]
}
res := dto.SystemLogRes{Source: source, Items: items, HasMore: hasMore}
if !hasMore || len(items) == 0 {
return res, nil
}
lastItem := items[len(items)-1]
next := systemLogCursor{EndTime: lastItem.Timestamp}
if lastItem.Cursor != "" {
next.JournalCursor = lastItem.Cursor
} else {
next.Skipped = countSystemLogTimestamp(items, lastItem.Timestamp)
if cursor != nil && cursor.EndTime == lastItem.Timestamp {
next.Skipped += cursor.Skipped
}
}
nextCursor, err := encodeSystemLogCursor(next)
if err != nil {
return dto.SystemLogRes{}, err
}
res.NextCursor = nextCursor
return res, nil
}
func decodeSystemLogCursor(value string) (*systemLogCursor, error) {
value = strings.TrimSpace(value)
if value == "" {
return nil, nil
}
decoded, err := base64.RawURLEncoding.DecodeString(value)
if err != nil {
return nil, fmt.Errorf("invalid system log cursor")
}
var cursor systemLogCursor
if err := json.Unmarshal(decoded, &cursor); err != nil || cursor.EndTime <= 0 || cursor.Skipped < 0 || cursor.Skipped > maxSystemLogCursorSkip {
return nil, fmt.Errorf("invalid system log cursor")
}
return &cursor, nil
}
func encodeSystemLogCursor(cursor systemLogCursor) (string, error) {
value, err := json.Marshal(cursor)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(value), nil
}
func skipSystemLogCursorItems(items []dto.SystemLogItem, cursor systemLogCursor) ([]dto.SystemLogItem, error) {
skipped := 0
for len(items) > 0 && skipped < cursor.Skipped && items[0].Timestamp == cursor.EndTime {
items = items[1:]
skipped++
}
if skipped != cursor.Skipped {
return nil, fmt.Errorf("system log cursor has expired")
}
return items, nil
}
func countSystemLogTimestamp(items []dto.SystemLogItem, timestamp int64) int {
count := 0
for _, item := range items {
if item.Timestamp == timestamp {
count++
}
}
return count
}
func formatJournalQueryTime(value time.Time) string {
// Fractional seconds are rejected by some journalctl versions.
return value.In(time.Local).Format("2006-01-02 15:04:05")
}
func parseFileSystemLogItem(line string) (dto.SystemLogItem, bool) {
raw := strings.TrimSpace(line)
if raw == "" {
return dto.SystemLogItem{}, false
}
priority := "6"
if matches := re.GetRegex(re.SyslogPRIPattern).FindStringSubmatch(raw); len(matches) == 2 {
value, _ := strconv.Atoi(matches[1])
priority = strconv.Itoa(value % 8)
raw = strings.TrimSpace(strings.TrimPrefix(raw, matches[0]))
}
logTime, rest, ok := parseFileSystemLogTime(raw)
if !ok {
return dto.SystemLogItem{}, false
}
service, message := parseFileSystemLogService(rest)
if priority == "6" {
priority = detectFileSystemLogPriority(message)
}
return dto.SystemLogItem{
Timestamp: logTime.UnixMicro(),
Time: logTime.Format("2006-01-02 15:04:05"),
Priority: priority,
Service: service,
Message: message,
Raw: line,
}, true
}
func parseFileSystemLogTime(value string) (time.Time, string, bool) {
if matches := re.GetRegex(re.SyslogRFC3339Pattern).FindStringSubmatch(value); len(matches) == 3 {
for _, layout := range []string{time.RFC3339Nano, "2006-01-02 15:04:05.999999999Z07:00"} {
if parsed, err := time.Parse(layout, matches[1]); err == nil {
return parsed.Local(), matches[2], true
}
}
for _, layout := range []string{"2006-01-02T15:04:05.999999999", "2006-01-02 15:04:05.999999999"} {
if parsed, err := time.ParseInLocation(layout, matches[1], time.Local); err == nil {
return parsed, matches[2], true
}
}
}
if matches := re.GetRegex(re.SyslogRFC3164Pattern).FindStringSubmatch(value); len(matches) == 3 {
parsed, err := time.ParseInLocation("Jan _2 15:04:05", matches[1], time.Local)
if err != nil {
return time.Time{}, "", false
}
now := time.Now()
parsed = time.Date(now.Year(), parsed.Month(), parsed.Day(), parsed.Hour(), parsed.Minute(), parsed.Second(), 0, time.Local)
if parsed.After(now.Add(24 * time.Hour)) {
parsed = parsed.AddDate(-1, 0, 0)
}
return parsed, matches[2], true
}
return time.Time{}, "", false
}
func parseFileSystemLogService(value string) (string, string) {
if matches := re.GetRegex(re.SyslogServicePattern).FindStringSubmatch(value); len(matches) == 3 {
return matches[1], matches[2]
}
// RFC5424 records have the form HOST APP-NAME PROCID MSGID STRUCTURED-DATA MSG.
fields := strings.Fields(value)
if len(fields) >= 6 {
return fields[1], strings.Join(fields[5:], " ")
}
return "", value
}
func detectFileSystemLogPriority(message string) string {
lowerMessage := strings.ToLower(message)
for _, item := range syslogPriorityKeywords {
if strings.Contains(lowerMessage, item.keyword) {
return item.priority
}
}
return "6"
}
func matchSystemLogItem(item dto.SystemLogItem, req dto.SystemLogReq, startTime, endTime time.Time) bool {
itemTime := time.UnixMicro(item.Timestamp)
if itemTime.Before(startTime) || itemTime.After(endTime) {
return false
}
if priority := strings.TrimSpace(req.Priority); priority != "" && item.Priority != priority {
return false
}
if service := strings.TrimSpace(req.Service); service != "" && !matchFileSystemLogService(item.Service, service) {
return false
}
if keyword := strings.TrimSpace(req.Keyword); keyword != "" && !strings.Contains(strings.ToLower(item.Raw), strings.ToLower(keyword)) {
return false
}
return true
}
func matchFileSystemLogService(itemService, requestedService string) bool {
itemService = strings.TrimSpace(itemService)
requestedService = strings.TrimSpace(requestedService)
if itemService == "" || requestedService == "" {
return itemService == requestedService
}
return strings.EqualFold(strings.TrimSuffix(itemService, ".service"), strings.TrimSuffix(requestedService, ".service"))
}
func (u *LogService) ListRunningServices() ([]string, error) {
systemctl, err := exec.LookPath("systemctl")
if err != nil {
return []string{}, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
args := []string{"list-units", "--type=service", "--state=running", "--no-legend", "--no-pager", "--plain"}
output, err := exec.CommandContext(ctx, systemctl, args...).Output()
if err != nil {
return []string{}, nil
}
services := make([]string, 0)
for _, line := range strings.Split(string(output), "\n") {
fields := strings.Fields(line)
if len(fields) == 0 || !strings.HasSuffix(fields[0], ".service") {
continue
}
services = append(services, fields[0])
}
sort.Strings(services)
return services, nil
}
func parseJournalLogItems(content string) []dto.SystemLogItem {
entries := strings.Split(content, "\n")
items := make([]dto.SystemLogItem, 0, len(entries))
for _, entry := range entries {
var fields map[string]interface{}
if err := json.Unmarshal([]byte(entry), &fields); err != nil {
continue
}
items = append(items, dto.SystemLogItem{
Timestamp: journalFieldMicroseconds(fields),
Cursor: journalFieldString(fields, "__CURSOR"),
Time: formatJournalTimestamp(journalFieldString(fields, "__REALTIME_TIMESTAMP")),
Priority: journalFieldString(fields, "PRIORITY"),
Service: journalFieldString(fields, "_SYSTEMD_UNIT"),
Message: journalFieldString(fields, "MESSAGE"),
Raw: entry,
})
}
return items
}
func journalFieldMicroseconds(fields map[string]interface{}) int64 {
value, _ := strconv.ParseInt(journalFieldString(fields, "__REALTIME_TIMESTAMP"), 10, 64)
return value
}
func journalFieldString(fields map[string]interface{}, key string) string {
value, ok := fields[key]
if !ok || value == nil {
return ""
}
switch item := value.(type) {
case string:
return item
case float64:
return strconv.FormatInt(int64(item), 10)
case []interface{}:
values := make([]string, 0, len(item))
for _, value := range item {
values = append(values, fmt.Sprint(value))
}
return strings.Join(values, " ")
default:
return fmt.Sprint(item)
}
}
func formatJournalTimestamp(value string) string {
microseconds, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return value
}
return time.Unix(0, microseconds*int64(time.Microsecond)).Local().Format("2006-01-02 15:04:05")
}
func NewILogService() ILogService {
+66 -427
View File
@@ -1,24 +1,18 @@
package service
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"path"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/cmd/server/ai"
"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
@@ -31,31 +25,18 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
"github.com/docker/docker/api/types/container"
"github.com/mattn/go-shellwords"
"github.com/subosito/gotenv"
"gopkg.in/yaml.v3"
)
type McpServerService struct{}
const (
defaultMcpGatewayImageNpx = "supercorp/supergateway:3.4.3"
defaultMcpGatewayImageUvx = "supercorp/supergateway:uvx"
defaultMcpProtocolVersion = "2025-06-18"
mcpOutputTransportSSE = "sse"
mcpOutputTransportStreamableHTTP = "streamableHttp"
)
type IMcpServerService interface {
Page(req request.McpServerSearch) response.McpServersRes
Detail(req request.McpServerDetail) (response.McpServerDTO, error)
Create(create request.McpServerCreate) error
Update(req request.McpServerUpdate) error
Delete(id uint) error
Operate(req request.McpServerOperate) error
SyncStatus(req request.McpServerStatusSync) ([]response.McpServerStatusDTO, error)
TestConnection(req request.McpServerConnectionTest) (response.McpServerConnectionTestRes, error)
GetBindDomain() (response.McpBindDomainRes, error)
BindDomain(req request.McpBindDomain) error
UpdateBindDomain(req request.McpBindDomainUpdate) error
@@ -73,36 +54,44 @@ func (m McpServerService) Page(req request.McpServerSearch) response.McpServersR
total, data, _ := mcpServerRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize)
for _, item := range data {
normalizeMcpServerGateway(&item)
items = append(items, response.McpServerDTO{
_ = syncMcpServerContainerStatus(&item)
serverDTO := response.McpServerDTO{
McpServer: item,
Environments: make([]request.Environment, 0),
Volumes: make([]request.Volume, 0),
})
}
project, err := docker.GetComposeProject(item.Name, path.Join(global.Dir.McpDir, item.Name), []byte(item.DockerCompose), []byte(item.Env), true)
if err != nil {
global.LOG.Errorf("get mcp compose project error: %s", err.Error())
continue
}
for _, service := range project.Services {
if service.Environment != nil {
for key, value := range service.Environment {
serverDTO.Environments = append(serverDTO.Environments, request.Environment{
Key: key,
Value: *value,
})
}
}
if service.Volumes != nil {
for _, volume := range service.Volumes {
serverDTO.Volumes = append(serverDTO.Volumes, request.Volume{
Source: volume.Source,
Target: volume.Target,
})
}
}
}
items = append(items, serverDTO)
}
res.Total = total
res.Items = items
return res
}
func (m McpServerService) Detail(req request.McpServerDetail) (response.McpServerDTO, error) {
mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return response.McpServerDTO{}, err
}
normalizeMcpServerGateway(mcpServer)
serverDTO := response.McpServerDTO{
McpServer: *mcpServer,
Environments: make([]request.Environment, 0),
Volumes: make([]request.Volume, 0),
}
if err := loadMcpServerComposeConfig(&serverDTO); err != nil {
return response.McpServerDTO{}, err
}
return serverDTO, nil
}
func (m McpServerService) Update(req request.McpServerUpdate) error {
go pullImage(req.Type)
mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return err
@@ -126,11 +115,7 @@ func (m McpServerService) Update(req request.McpServerUpdate) error {
mcpServer.HostIP = req.HostIP
mcpServer.OutputTransport = req.OutputTransport
mcpServer.Type = req.Type
mcpServer.GatewayImage = req.GatewayImage
mcpServer.ProtocolVersion = req.ProtocolVersion
mcpServer.GatewayArgs = req.GatewayArgs
normalizeMcpServerGateway(mcpServer)
if req.OutputTransport == mcpOutputTransportSSE {
if req.OutputTransport == "sse" {
mcpServer.SsePath = req.SsePath
} else {
mcpServer.StreamableHttpPath = req.StreamableHttpPath
@@ -152,11 +137,12 @@ func (m McpServerService) Update(req request.McpServerUpdate) error {
if err := mcpServerRepo.Save(mcpServer); err != nil {
return err
}
go runMcpServerTask(mcpServer, req.TaskID, task.TaskUpdate)
go startMcp(mcpServer)
return nil
}
func (m McpServerService) Create(create request.McpServerCreate) error {
go pullImage(create.Type)
servers, _ := mcpServerRepo.List()
for _, server := range servers {
if server.Port == create.Port {
@@ -191,12 +177,8 @@ func (m McpServerService) Create(create request.McpServerCreate) error {
HostIP: create.HostIP,
OutputTransport: create.OutputTransport,
Type: create.Type,
GatewayImage: create.GatewayImage,
ProtocolVersion: create.ProtocolVersion,
GatewayArgs: create.GatewayArgs,
}
normalizeMcpServerGateway(mcpServer)
if create.OutputTransport == mcpOutputTransportSSE {
if create.OutputTransport == "sse" {
mcpServer.SsePath = create.SsePath
} else {
mcpServer.StreamableHttpPath = create.StreamableHttpPath
@@ -223,7 +205,7 @@ func (m McpServerService) Create(create request.McpServerCreate) error {
return err
}
addProxy(mcpServer)
go runMcpServerTask(mcpServer, create.TaskID, task.TaskCreate)
go startMcp(mcpServer)
return nil
}
@@ -253,9 +235,6 @@ func (m McpServerService) Operate(req request.McpServerOperate) error {
if err != nil {
return err
}
if err := refreshMcpServerFiles(mcpServer); err != nil {
return err
}
composePath := path.Join(mcpServer.Dir, "docker-compose.yml")
var out string
switch req.Operate {
@@ -276,148 +255,6 @@ func (m McpServerService) Operate(req request.McpServerOperate) error {
return mcpServerRepo.Save(mcpServer)
}
func (m McpServerService) SyncStatus(req request.McpServerStatusSync) ([]response.McpServerStatusDTO, error) {
var (
servers []model.McpServer
err error
)
if len(req.IDs) > 0 {
servers, err = mcpServerRepo.List(repo.WithByIDs(req.IDs))
} else {
servers, err = mcpServerRepo.List()
}
if err != nil {
return nil, err
}
if len(servers) == 0 {
return []response.McpServerStatusDTO{}, nil
}
containerNames := make([]string, 0, len(servers))
for _, server := range servers {
if server.ContainerName != "" {
containerNames = append(containerNames, server.ContainerName)
}
}
containerMap := make(map[string]*container.Summary)
if len(containerNames) > 0 {
cli, err := docker.NewClient()
if err != nil {
return nil, err
}
defer cli.Close()
containers, err := cli.ListContainersByName(containerNames)
if err != nil {
return nil, err
}
for i := range containers {
for _, name := range containers[i].Names {
containerMap[strings.TrimPrefix(name, "/")] = &containers[i]
}
}
}
res := make([]response.McpServerStatusDTO, 0, len(servers))
for i := range servers {
oldStatus := servers[i].Status
applyMcpContainerStatus(&servers[i], containerMap[servers[i].ContainerName])
if servers[i].Status != oldStatus {
if err := mcpServerRepo.Save(&servers[i]); err != nil {
return nil, err
}
}
res = append(res, response.McpServerStatusDTO{
ID: servers[i].ID,
Status: servers[i].Status,
Message: servers[i].Message,
})
}
return res, nil
}
func (m McpServerService) TestConnection(req request.McpServerConnectionTest) (response.McpServerConnectionTestRes, error) {
mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return response.McpServerConnectionTestRes{}, err
}
normalizeMcpServerGateway(mcpServer)
endpoint := buildMcpEndpoint(mcpServer)
res := response.McpServerConnectionTestRes{
Endpoint: endpoint,
OutputTransport: mcpServer.OutputTransport,
ProtocolVersion: mcpServer.ProtocolVersion,
}
if endpoint == "" {
res.Message = "empty endpoint"
return res, nil
}
client := &http.Client{Timeout: 10 * time.Second}
if mcpServer.OutputTransport == mcpOutputTransportSSE {
httpReq, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
res.Message = err.Error()
return res, nil
}
httpRes, err := client.Do(httpReq)
if err != nil {
res.Message = err.Error()
return res, nil
}
defer httpRes.Body.Close()
contentType := httpRes.Header.Get("Content-Type")
if httpRes.StatusCode >= 200 && httpRes.StatusCode < 300 && strings.Contains(contentType, "text/event-stream") {
res.Success = true
res.Message = "ok"
return res, nil
}
res.Message = fmt.Sprintf("unexpected response: status=%d content-type=%s", httpRes.StatusCode, contentType)
return res, nil
}
payload := map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": map[string]interface{}{
"protocolVersion": mcpServer.ProtocolVersion,
"capabilities": map[string]interface{}{},
"clientInfo": map[string]string{
"name": "1Panel",
"version": "1.0.0",
},
},
}
body, _ := json.Marshal(payload)
httpReq, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
res.Message = err.Error()
return res, nil
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json, text/event-stream")
httpRes, err := client.Do(httpReq)
if err != nil {
res.Message = err.Error()
return res, nil
}
defer httpRes.Body.Close()
res.Success, res.Message = handleMcpConnectionHTTPResponse(httpRes)
return res, nil
}
func handleMcpConnectionHTTPResponse(httpRes *http.Response) (bool, string) {
if httpRes.StatusCode >= 200 && httpRes.StatusCode < 300 {
return true, "ok"
}
respBody, _ := io.ReadAll(httpRes.Body)
message := strings.TrimSpace(string(respBody))
if message == "" {
message = httpRes.Status
}
return false, message
}
func (m McpServerService) GetBindDomain() (response.McpBindDomainRes, error) {
var res response.McpBindDomainRes
websiteID := GetWebsiteID()
@@ -697,9 +534,11 @@ func addMCPProxy(websiteID uint) error {
}
func updateMcpServer(mcpServer *model.McpServer) error {
if err := refreshMcpServerFiles(mcpServer); err != nil {
env := handleEnv(mcpServer)
if err := gotenv.Write(env, path.Join(mcpServer.Dir, ".env")); err != nil {
return err
}
_ = mcpServerRepo.Save(mcpServer)
composePath := path.Join(global.Dir.McpDir, mcpServer.Name, "docker-compose.yml")
_, _ = compose.Down(composePath)
if _, err := compose.Up(composePath); err != nil {
@@ -709,159 +548,6 @@ func updateMcpServer(mcpServer *model.McpServer) error {
return mcpServerRepo.Save(mcpServer)
}
func refreshMcpServerFiles(mcpServer *model.McpServer) error {
normalizeMcpServerGateway(mcpServer)
if err := handleCreateParams(mcpServer, nil, nil); err != nil {
return err
}
env := handleEnv(mcpServer)
if err := gotenv.Write(env, path.Join(mcpServer.Dir, ".env")); err != nil {
return err
}
composePath := path.Join(mcpServer.Dir, "docker-compose.yml")
if err := files.NewFileOp().SaveFile(composePath, mcpServer.DockerCompose, 0644); err != nil {
return err
}
return mcpServerRepo.Save(mcpServer)
}
func normalizeMcpServerGateway(mcpServer *model.McpServer) {
if mcpServer.Type == "" {
mcpServer.Type = "npx"
}
if strings.TrimSpace(mcpServer.GatewayImage) == "" {
if mcpServer.Type == "uvx" {
mcpServer.GatewayImage = defaultMcpGatewayImageUvx
} else {
mcpServer.GatewayImage = defaultMcpGatewayImageNpx
}
}
if strings.TrimSpace(mcpServer.ProtocolVersion) == "" {
mcpServer.ProtocolVersion = defaultMcpProtocolVersion
}
}
var managedMcpGatewayArgs = map[string]struct{}{
"--": {},
"stdio": {},
"sse": {},
"streamablehttp": {},
"outputtransport": {},
"port": {},
"baseurl": {},
"ssepath": {},
"messagepath": {},
"streamablehttppath": {},
"protocolversion": {},
}
func normalizeMcpGatewayArgName(name string) string {
if name == "--" || !strings.HasPrefix(name, "--") {
return name
}
name = strings.TrimPrefix(name, "--")
name = strings.TrimPrefix(name, "no-")
name = strings.SplitN(name, ".", 2)[0]
return strings.ToLower(strings.ReplaceAll(name, "-", ""))
}
func parseMcpGatewayArgs(raw string) ([]string, error) {
if strings.TrimSpace(raw) == "" {
return nil, nil
}
parser := shellwords.NewParser()
args, err := parser.Parse(raw)
if err != nil {
return nil, buserr.New("ErrMcpGatewayArgsParse")
}
if parser.Position >= 0 {
return nil, buserr.New("ErrMcpGatewayArgsParse")
}
for _, arg := range args {
name := strings.SplitN(arg, "=", 2)[0]
if _, ok := managedMcpGatewayArgs[normalizeMcpGatewayArgName(name)]; ok {
return nil, buserr.WithName("ErrMcpGatewayArgsConflict", name)
}
}
return args, nil
}
func buildSupergatewayCommand(mcpServer *model.McpServer) ([]string, error) {
normalizeMcpServerGateway(mcpServer)
command := []string{
"--stdio", mcpServer.Command,
"--outputTransport", mcpServer.OutputTransport,
"--port", strconv.Itoa(mcpServer.Port),
}
if mcpServer.OutputTransport == mcpOutputTransportSSE {
command = append(command,
"--baseUrl", mcpServer.BaseURL,
"--ssePath", mcpServer.SsePath,
"--messagePath", fmt.Sprintf("%s/messages", mcpServer.SsePath),
)
} else {
command = append(command,
"--streamableHttpPath", mcpServer.StreamableHttpPath,
"--protocolVersion", mcpServer.ProtocolVersion,
)
}
gatewayArgs, err := parseMcpGatewayArgs(mcpServer.GatewayArgs)
if err != nil {
return nil, err
}
return append(command, gatewayArgs...), nil
}
func buildMcpEndpoint(mcpServer *model.McpServer) string {
if mcpServer.BaseURL == "" {
return ""
}
if mcpServer.OutputTransport == mcpOutputTransportSSE {
return strings.TrimRight(mcpServer.BaseURL, "/") + "/" + strings.TrimLeft(mcpServer.SsePath, "/")
}
return strings.TrimRight(mcpServer.BaseURL, "/") + "/" + strings.TrimLeft(mcpServer.StreamableHttpPath, "/")
}
func loadMcpServerComposeConfig(serverDTO *response.McpServerDTO) error {
serverDTO.Environments = make([]request.Environment, 0)
serverDTO.Volumes = make([]request.Volume, 0)
dir := serverDTO.Dir
if dir == "" {
dir = path.Join(global.Dir.McpDir, serverDTO.Name)
}
project, err := docker.GetComposeProject(serverDTO.Name, dir, []byte(serverDTO.DockerCompose), []byte(serverDTO.Env), true)
if err != nil {
return err
}
for _, service := range project.Services {
if service.Environment != nil {
for key, value := range service.Environment {
if value == nil {
continue
}
serverDTO.Environments = append(serverDTO.Environments, request.Environment{
Key: key,
Value: *value,
})
}
}
if service.Volumes != nil {
for _, volume := range service.Volumes {
mode := "rw"
if volume.ReadOnly {
mode = "ro"
}
serverDTO.Volumes = append(serverDTO.Volumes, request.Volume{
Source: volume.Source,
Target: volume.Target,
Mode: mode,
})
}
}
}
return nil
}
func handleEnv(mcpServer *model.McpServer) gotenv.Env {
env := make(gotenv.Env)
env["CONTAINER_NAME"] = mcpServer.ContainerName
@@ -872,7 +558,6 @@ func handleEnv(mcpServer *model.McpServer) gotenv.Env {
env["HOST_IP"] = mcpServer.HostIP
env["STREAMABLE_HTTP_PATH"] = mcpServer.StreamableHttpPath
env["OUTPUT_TRANSPORT"] = mcpServer.OutputTransport
env["GATEWAY_IMAGE"] = mcpServer.GatewayImage
envStr, _ := gotenv.Marshal(env)
mcpServer.Env = envStr
return env
@@ -907,43 +592,26 @@ func handleCreateParams(mcpServer *model.McpServer, environments []request.Envir
}
delete(services, serviceName)
}
normalizeMcpServerGateway(mcpServer)
command, err := buildSupergatewayCommand(mcpServer)
if err != nil {
return err
}
serviceValue["command"] = command
serviceValue["image"] = mcpServer.GatewayImage
serviceValue["ports"] = []string{
formatComposePortMapping("HOST_IP", "PANEL_APP_PORT_HTTP", "PANEL_APP_PORT_HTTP", ""),
}
oldEnv, hasOldEnv := serviceValue["environment"]
delete(serviceValue, "environment")
if environments != nil && len(environments) > 0 {
if len(environments) > 0 {
envMap := make(map[string]string)
for _, env := range environments {
envMap[env.Key] = env.Value
}
serviceValue["environment"] = envMap
} else if environments == nil {
// Preserve existing custom environment when regenerating compose during start/restart.
if mcpServer.ID > 0 && hasOldEnv {
serviceValue["environment"] = oldEnv
}
}
oldVolumes, hasOldVolumes := serviceValue["volumes"]
delete(serviceValue, "volumes")
if volumes != nil && len(volumes) > 0 {
if len(volumes) > 0 {
volumeList := make([]string, 0)
for _, volume := range volumes {
volumeList = append(volumeList, formatComposeVolume(volume.Source, volume.Target, volume.Mode))
volumeList = append(volumeList, fmt.Sprintf("%s:%s", volume.Source, volume.Target))
}
serviceValue["volumes"] = volumeList
} else if volumes == nil {
// Preserve existing custom volumes when regenerating compose during start/restart.
if mcpServer.ID > 0 && hasOldVolumes {
serviceValue["volumes"] = oldVolumes
}
}
if mcpServer.Type == "npx" {
serviceValue["image"] = "supercorp/supergateway:latest"
} else {
serviceValue["image"] = "supercorp/supergateway:uvx"
}
services[mcpServer.Name] = serviceValue
@@ -955,56 +623,19 @@ func handleCreateParams(mcpServer *model.McpServer, environments []request.Envir
return nil
}
func runMcpServerTask(mcpServer *model.McpServer, taskID, operate string) {
mcpTask, err := task.NewTaskWithOps(mcpServer.Name, operate, task.TaskScopeAI, taskID, mcpServer.ID)
if err != nil {
mcpServer.Status = constant.StatusError
mcpServer.Message = err.Error()
_ = mcpServerRepo.Save(mcpServer)
return
}
mcpTask.AddSubTask(task.GetTaskName(mcpServer.Name, operate, task.TaskScopeAI), func(t *task.Task) error {
return startMcpWithTask(mcpServer, t)
}, nil)
_ = mcpTask.Execute()
}
func startMcp(mcpServer *model.McpServer) {
_ = startMcpWithTask(mcpServer, nil)
}
func startMcpWithTask(mcpServer *model.McpServer, taskItem *task.Task) error {
if err := refreshMcpServerFiles(mcpServer); err != nil {
mcpServer.Status = constant.StatusError
mcpServer.Message = err.Error()
_ = mcpServerRepo.Save(mcpServer)
return err
}
composePath := path.Join(global.Dir.McpDir, mcpServer.Name, "docker-compose.yml")
if mcpServer.Status != constant.StatusNormal {
_, _ = compose.Down(composePath)
}
var err error
var out string
if taskItem != nil {
err = compose.UpWithTask(composePath, taskItem, false)
} else {
out, err = compose.Up(composePath)
}
if err != nil {
if out, err := compose.Up(composePath); err != nil {
mcpServer.Status = constant.StatusError
if out != "" {
mcpServer.Message = out
} else {
mcpServer.Message = err.Error()
}
_ = mcpServerRepo.Save(mcpServer)
return err
mcpServer.Message = out
} else {
mcpServer.Status = constant.StatusRunning
mcpServer.Message = ""
}
return syncMcpServerContainerStatus(mcpServer)
_ = syncMcpServerContainerStatus(mcpServer)
}
func syncMcpServerContainerStatus(mcpServer *model.McpServer) error {
@@ -1019,19 +650,11 @@ func syncMcpServerContainerStatus(mcpServer *model.McpServer) error {
return err
}
if len(containers) == 0 {
applyMcpContainerStatus(mcpServer, nil)
mcpServer.Status = constant.StatusStopped
return mcpServerRepo.Save(mcpServer)
}
applyMcpContainerStatus(mcpServer, &containers[0])
return mcpServerRepo.Save(mcpServer)
}
func applyMcpContainerStatus(mcpServer *model.McpServer, containerSummary *container.Summary) {
if containerSummary == nil {
mcpServer.Status = constant.StatusStopped
return
}
switch containerSummary.State {
container := containers[0]
switch container.State {
case "exited":
mcpServer.Status = constant.StatusError
case "running":
@@ -1045,6 +668,7 @@ func applyMcpContainerStatus(mcpServer *model.McpServer, containerSummary *conta
mcpServer.Status = constant.StatusStopped
}
}
return mcpServerRepo.Save(mcpServer)
}
func GetWebsiteID() uint {
@@ -1055,3 +679,18 @@ func GetWebsiteID() uint {
websiteIDUint, _ := strconv.ParseUint(websiteID.Value, 10, 64)
return uint(websiteIDUint)
}
func pullImage(imageType string) {
if global.CONF.Base.IsOffline {
return
}
if imageType == "npx" {
if err := docker.PullImage("supercorp/supergateway:latest"); err != nil {
global.LOG.Errorf("docker pull mcp image error: %s", err.Error())
}
} else {
if err := docker.PullImage("supercorp/supergateway:uvx"); err != nil {
global.LOG.Errorf("docker pull mcp image error: %s", err.Error())
}
}
}
+10 -52
View File
@@ -4,12 +4,9 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/repo"
@@ -37,12 +34,7 @@ type MonitorService struct {
NetIO chan ([]net.IOCountersStat)
}
var (
monitorCancel context.CancelFunc
hostSysPath = loadHostSysPath()
blockDevicePartitionCache sync.Map
)
var monitorCancel context.CancelFunc
type IMonitorService interface {
Run()
@@ -349,10 +341,18 @@ func (m *MonitorService) Run() {
func (m *MonitorService) loadDiskIO() {
ioStat, _ := disk.IOCounters()
var diskIOList []disk.IOCountersStat
var ioStatAll disk.IOCountersStat
for _, io := range ioStat {
ioStatAll.Name = "all"
ioStatAll.ReadBytes += io.ReadBytes
ioStatAll.WriteBytes += io.WriteBytes
ioStatAll.ReadTime += io.ReadTime
ioStatAll.WriteTime += io.WriteTime
ioStatAll.WriteCount += io.WriteCount
ioStatAll.ReadCount += io.ReadCount
diskIOList = append(diskIOList, io)
}
diskIOList = append(diskIOList, sumDiskIOCounters(ioStat))
diskIOList = append(diskIOList, ioStatAll)
m.DiskIO <- diskIOList
}
@@ -680,45 +680,3 @@ func loadGPUInfoFloat(val string) float64 {
data, _ := strconv.ParseFloat(val, 64)
return data
}
func sumDiskIOCounters(ioStats map[string]disk.IOCountersStat) disk.IOCountersStat {
total := disk.IOCountersStat{Name: "all"}
for name, stat := range ioStats {
if isBlockDevicePartition(name) {
continue
}
total.ReadCount += stat.ReadCount
total.MergedReadCount += stat.MergedReadCount
total.WriteCount += stat.WriteCount
total.MergedWriteCount += stat.MergedWriteCount
total.ReadBytes += stat.ReadBytes
total.WriteBytes += stat.WriteBytes
total.ReadTime += stat.ReadTime
total.WriteTime += stat.WriteTime
total.IopsInProgress += stat.IopsInProgress
total.IoTime += stat.IoTime
total.WeightedIO += stat.WeightedIO
}
return total
}
func isBlockDevicePartition(name string) bool {
deviceName := filepath.Base(name)
if cached, ok := blockDevicePartitionCache.Load(deviceName); ok {
return cached.(bool)
}
_, err := os.Stat(filepath.Join(hostSysPath, "class", "block", deviceName, "partition"))
isPartition := err == nil
actual, _ := blockDevicePartitionCache.LoadOrStore(deviceName, isPartition)
return actual.(bool)
}
func loadHostSysPath() string {
hostSys := os.Getenv("HOST_SYS")
if hostSys == "" {
return "/sys"
}
return hostSys
}
+126 -151
View File
@@ -1,6 +1,8 @@
package service
import (
"bufio"
"encoding/json"
"fmt"
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
@@ -15,6 +17,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/subosito/gotenv"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
@@ -54,7 +57,7 @@ func (n NginxService) GetNginxConfig() (*response.NginxFile, error) {
if err != nil {
return nil, err
}
configPath := path.Join(global.Dir.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, nginxModuleConfDir, "nginx.conf")
configPath := path.Join(global.Dir.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "nginx.conf")
byteContent, err := files.NewFileOp().GetContent(configPath)
if err != nil {
return nil, err
@@ -135,7 +138,7 @@ func (n NginxService) UpdateConfigFile(req request.NginxConfigFileUpdate) error
if err != nil {
return err
}
filePath := path.Join(global.Dir.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, nginxModuleConfDir, "nginx.conf")
filePath := path.Join(global.Dir.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "nginx.conf")
if req.Backup {
backupPath := path.Join(path.Dir(filePath), "bak")
if !fileOp.Stat(backupPath) {
@@ -178,20 +181,71 @@ func (n NginxService) Build(req request.NginxBuildReq) error {
if err = task.CheckTaskIsExecuting(taskName); err != nil {
return err
}
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, nginxInstall.ID); err != nil {
return err
}
buildPath := path.Join(nginxInstall.GetPath(), nginxModuleBuildDir)
if !files.NewFileOp().Stat(buildPath) {
fileOp := files.NewFileOp()
buildPath := path.Join(nginxInstall.GetPath(), "build")
if !fileOp.Stat(buildPath) {
return buserr.New("ErrBuildDirNotFound")
}
moduleConfigPath := path.Join(buildPath, "module.json")
moduleContent, err := fileOp.GetContent(moduleConfigPath)
if err != nil {
return err
}
var (
modules []dto.NginxModule
addModuleParams []string
addPackages []string
)
if len(moduleContent) > 0 {
_ = json.Unmarshal(moduleContent, &modules)
bashFile, err := os.OpenFile(path.Join(buildPath, "tmp", "pre.sh"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, constant.DirPerm)
if err != nil {
return err
}
defer bashFile.Close()
bashFileWriter := bufio.NewWriter(bashFile)
for _, module := range modules {
if !module.Enable {
continue
}
_, err = bashFileWriter.WriteString(module.Script + "\n")
if err != nil {
return err
}
addModuleParams = append(addModuleParams, module.Params)
addPackages = append(addPackages, module.Packages...)
}
err = bashFileWriter.Flush()
if err != nil {
return err
}
}
envs, err := gotenv.Read(nginxInstall.GetEnvPath())
if err != nil {
return err
}
envs["CONTAINER_PACKAGE_URL"] = req.Mirror
envs["RESTY_CONFIG_OPTIONS_MORE"] = ""
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = ""
if len(addModuleParams) > 0 {
envs["RESTY_CONFIG_OPTIONS_MORE"] = strings.Join(addModuleParams, " ")
}
if len(addPackages) > 0 {
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = strings.Join(addPackages, " ")
}
_ = gotenv.Write(envs, nginxInstall.GetEnvPath())
buildTask, err := task.NewTaskWithOps(nginxInstall.Name, task.TaskBuild, task.TaskScopeApp, req.TaskID, nginxInstall.ID)
if err != nil {
return err
}
buildTask.AddSubTaskWithOps("", func(t *task.Task) error {
return executeNginxModuleBuild(nginxInstall, req.Modules, req.Force, req.Mirror, t, true)
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*buildTask), cmd.WithTimeout(120*time.Minute))
if err = cmdMgr.Run("docker", "compose", "-f", nginxInstall.GetComposePath(), "build"); err != nil {
return err
}
_, err = compose.DownAndUp(nginxInstall.GetComposePath())
return err
}, nil, 0, 120*time.Minute)
go func() {
@@ -205,70 +259,27 @@ func (n NginxService) GetModules() (*response.NginxBuildConfig, error) {
if err != nil {
return nil, err
}
modules, err := loadNginxModules(nginxInstall)
fileOp := files.NewFileOp()
var modules []dto.NginxModule
moduleConfigPath := path.Join(nginxInstall.GetPath(), "build", "module.json")
if !fileOp.Stat(moduleConfigPath) {
return nil, nil
}
moduleContent, err := fileOp.GetContent(moduleConfigPath)
if err != nil {
return nil, err
}
target, targetWarning, targetErr := resolveNginxModuleTarget(nginxInstall)
if targetWarning != "" {
global.LOG.Warn(targetWarning)
}
runtimeTarget := target
runtimeTarget.BuilderDigest = ""
setNginxModuleTargetKey(&runtimeTarget)
runtimeTargetErr := targetErr
if runtimeTarget.OpenRestyVersion != "" {
runtimeTargetErr = nil
if len(moduleContent) > 0 {
_ = json.Unmarshal(moduleContent, &modules)
}
var resList []response.NginxModule
for _, module := range modules {
buildStatus := nginxModuleStatusPending
loadStatus := nginxModuleLoadDisabled
var artifacts []dto.NginxModuleArtifact
if module.BuildMode == nginxModuleBuildStatic && runtimeTargetErr == nil {
if build := findCurrentNginxModuleBuild(module, runtimeTarget); build != nil && build.Status == nginxModuleStatusReady {
buildStatus = nginxModuleStatusReady
if module.Enable {
loadStatus = nginxModuleLoadEnabled
}
} else if findLatestNginxModuleBuild(module, runtimeTarget) != nil {
if module.Enable {
loadStatus = nginxModuleLoadEnabled
}
}
} else if targetErr == nil {
if build := findCurrentNginxModuleBuild(module, target); build != nil {
buildStatus = build.Status
artifacts = build.Artifacts
if build.Status == nginxModuleStatusReady {
if module.Enable {
loadStatus = nginxModuleLoadEnabled
}
}
} else if latestBuild := findLatestNginxModuleBuild(module, target); latestBuild != nil {
artifacts = latestBuild.Artifacts
if module.Enable {
loadStatus = nginxModuleLoadEnabled
}
}
}
if module.BuildMode != nginxModuleBuildStatic && module.LastError != "" {
buildStatus = nginxModuleStatusFailed
}
resList = append(resList, response.NginxModule{
Name: module.Name,
Custom: module.Custom,
Script: module.Script,
Packages: strings.Join(module.Packages, ","),
Params: module.Params,
Enable: module.Enable,
BuildMode: module.BuildMode,
Provider: module.Provider,
LoadOrder: module.LoadOrder,
BuildStatus: buildStatus,
LoadStatus: loadStatus,
Artifacts: artifacts,
LastError: module.LastError,
Name: module.Name,
Script: module.Script,
Packages: strings.Join(module.Packages, ","),
Params: module.Params,
Enable: module.Enable,
})
}
envs, err := gotenv.Read(nginxInstall.GetEnvPath())
@@ -277,105 +288,69 @@ func (n NginxService) GetModules() (*response.NginxBuildConfig, error) {
}
return &response.NginxBuildConfig{
Mirror: envs["CONTAINER_PACKAGE_URL"],
DynamicSupported: nginxModuleDynamicSupported(nginxInstall),
Modules: resList,
Mirror: envs["CONTAINER_PACKAGE_URL"],
Modules: resList,
}, nil
}
func applyNginxModuleUpdate(modules []dto.NginxModule, req request.NginxModuleUpdate) ([]dto.NginxModule, *dto.NginxModule, error) {
switch req.Operate {
case nginxModuleOperateCreate:
if err := validateNginxModuleBuildMode(dto.NginxModule{Name: req.Name, BuildMode: req.BuildMode}); err != nil {
return nil, nil, err
}
for _, module := range modules {
if module.Name == req.Name {
return nil, nil, buserr.New("ErrNameIsExist")
}
}
modules = append(modules, dto.NginxModule{
Name: req.Name,
Custom: true,
Script: req.Script,
Packages: strings.Split(req.Packages, ","),
Params: req.Params,
Enable: req.Enable,
BuildMode: req.BuildMode,
Provider: req.Provider,
LoadOrder: req.LoadOrder,
})
case nginxModuleOperateUpdate:
for i := range modules {
if modules[i].Name != req.Name {
continue
}
if modules[i].Custom {
if err := validateNginxModuleBuildMode(dto.NginxModule{Name: req.Name, BuildMode: req.BuildMode}); err != nil {
return nil, nil, err
}
} else {
modules[i].Enable = req.Enable
return modules, nil, nil
}
modules[i].Enable = req.Enable
modules[i].Script = req.Script
modules[i].Packages = strings.Split(req.Packages, ",")
modules[i].Params = req.Params
modules[i].BuildMode = req.BuildMode
modules[i].Provider = req.Provider
modules[i].LoadOrder = req.LoadOrder
return modules, nil, nil
}
return nil, nil, fmt.Errorf("OpenResty module %s not found", req.Name)
case nginxModuleOperateDelete:
for i := range modules {
if modules[i].Name != req.Name {
continue
}
if !modules[i].Custom {
return nil, nil, fmt.Errorf("built-in OpenResty module %s cannot be deleted", req.Name)
}
deleted := modules[i]
modules = append(modules[:i], modules[i+1:]...)
return modules, &deleted, nil
}
return nil, nil, fmt.Errorf("OpenResty module %s not found", req.Name)
default:
return nil, nil, fmt.Errorf("unsupported OpenResty module operation %q", req.Operate)
}
return modules, nil, nil
}
func (n NginxService) UpdateModule(req request.NginxModuleUpdate) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, nginxInstall.ID); err != nil {
return err
fileOp := files.NewFileOp()
var (
modules []dto.NginxModule
)
moduleConfigPath := path.Join(nginxInstall.GetPath(), "build", "module.json")
if !fileOp.Stat(moduleConfigPath) {
_ = fileOp.CreateFile(moduleConfigPath)
}
modules, err := loadNginxModules(nginxInstall)
moduleContent, err := fileOp.GetContent(moduleConfigPath)
if err != nil {
return err
}
oldModules := cloneNginxModules(modules)
modules, deletedModule, err := applyNginxModuleUpdate(modules, req)
if len(moduleContent) > 0 {
_ = json.Unmarshal(moduleContent, &modules)
}
switch req.Operate {
case "create":
for _, module := range modules {
if module.Name == req.Name {
return buserr.New("ErrNameIsExist")
}
}
modules = append(modules, dto.NginxModule{
Name: req.Name,
Script: req.Script,
Packages: strings.Split(req.Packages, ","),
Params: req.Params,
Enable: true,
})
case "update":
for i, module := range modules {
if module.Name == req.Name {
modules[i].Script = req.Script
modules[i].Packages = strings.Split(req.Packages, ",")
modules[i].Params = req.Params
modules[i].Enable = req.Enable
break
}
}
case "delete":
for i, module := range modules {
if module.Name == req.Name {
modules = append(modules[:i], modules[i+1:]...)
break
}
}
}
moduleByte, err := json.Marshal(modules)
if err != nil {
return err
}
if err = saveNginxModules(nginxInstall, modules); err != nil {
return err
}
if err = reconcileDynamicNginxModuleConfig(nginxInstall, modules, true); err != nil {
_ = saveNginxModules(nginxInstall, oldModules)
_ = reconcileDynamicNginxModuleConfig(nginxInstall, oldModules, false)
return err
}
if deletedModule != nil {
_ = removeNginxModuleArtifacts(nginxInstall, *deletedModule)
}
return nil
return fileOp.SaveFileWithByte(moduleConfigPath, moduleByte, constant.DirPerm)
}
func (n NginxService) OperateDefaultHTTPs(req request.NginxDefaultHTTPSUpdate) error {
@@ -391,7 +366,7 @@ func (n NginxService) OperateDefaultHTTPs(req request.NginxDefaultHTTPSUpdate) e
break
}
}
defaultConfigPath := path.Join(appInstall.GetPath(), nginxModuleConfDir, "default", "00.default.conf")
defaultConfigPath := path.Join(appInstall.GetPath(), "conf", "default", "00.default.conf")
content, err := os.ReadFile(defaultConfigPath)
if err != nil {
return err
@@ -431,7 +406,7 @@ func (n NginxService) GetDefaultHttpsStatus() (*response.NginxConfigRes, error)
if err != nil {
return nil, err
}
defaultConfigPath := path.Join(appInstall.GetPath(), nginxModuleConfDir, "default", "00.default.conf")
defaultConfigPath := path.Join(appInstall.GetPath(), "conf", "default", "00.default.conf")
content, err := os.ReadFile(defaultConfigPath)
if err != nil {
return nil, err
File diff suppressed because it is too large Load Diff
-676
View File
@@ -1,676 +0,0 @@
package service
import (
"encoding/json"
"errors"
"os"
"path"
"strings"
"testing"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
)
func TestNormalizeNginxModuleDoesNotInferBuildMode(t *testing.T) {
module := dto.NginxModule{
Name: "legacy",
Packages: []string{"git", "", "git", " curl "},
}
normalizeNginxModule(&module)
if module.BuildMode != "" {
t.Fatalf("build mode must be explicit, got %s", module.BuildMode)
}
if module.Provider != nginxModuleProviderLocal {
t.Fatalf("expected local provider, got %s", module.Provider)
}
if len(module.Packages) != 2 || module.Packages[0] != "git" || module.Packages[1] != "curl" {
t.Fatalf("unexpected normalized packages: %#v", module.Packages)
}
}
func TestNormalizeDynamicModuleParams(t *testing.T) {
params, err := normalizeDynamicModuleParams("--with-http_dav_module --add-module=/tmp/nginx-dav-ext-module")
if err != nil {
t.Fatal(err)
}
expected := "--with-http_dav_module --add-dynamic-module=/tmp/nginx-dav-ext-module"
if params != expected {
t.Fatalf("expected %q, got %q", expected, params)
}
if _, err = normalizeDynamicModuleParams("--add-module=/tmp/module;touch /tmp/unsafe"); err == nil {
t.Fatal("expected shell metacharacters to be rejected")
}
}
func TestFindCurrentAndLatestNginxModuleBuild(t *testing.T) {
target := dto.NginxModuleTarget{Key: "target"}
module := dto.NginxModule{
Name: "example",
Params: "--add-module=/tmp/example",
BuildMode: nginxModuleBuildDynamic,
Provider: nginxModuleProviderLocal,
}
params, err := normalizeDynamicModuleParams(module.Params)
if err != nil {
t.Fatal(err)
}
currentHash, err := nginxModuleBuildHash(module, target, params)
if err != nil {
t.Fatal(err)
}
oldTime := time.Now().Add(-time.Hour)
newTime := time.Now()
module.Builds = []dto.NginxModuleBuild{
{Hash: "old", Status: nginxModuleStatusReady, Target: target, BuiltAt: oldTime},
{Hash: currentHash, Status: nginxModuleStatusReady, Target: target, BuiltAt: newTime},
}
if build := findCurrentNginxModuleBuild(module, target); build == nil || build.Hash != currentHash {
t.Fatalf("current build was not selected: %#v", build)
}
if build := findLatestNginxModuleBuild(module, target); build == nil || build.Hash != currentHash {
t.Fatalf("latest build was not selected: %#v", build)
}
module.Params = "--add-module=/tmp/example-v2"
if build := findCurrentNginxModuleBuild(module, target); build != nil {
t.Fatalf("changed module input should be stale, got %#v", build)
}
if build := findLatestNginxModuleBuild(module, target); build == nil || build.Hash != currentHash {
t.Fatal("the previous ready build should remain available until replacement")
}
}
func TestNginxModulePathNameAvoidsSanitizedNameCollisions(t *testing.T) {
first := nginxModulePathName("example/module")
second := nginxModulePathName("example-module")
if first == second {
t.Fatalf("module path names collided: %s", first)
}
if len(nginxModulePathName(string(make([]byte, 256)))) > 57 {
t.Fatal("module path name should remain safe for Docker resource names")
}
}
func TestRecordNginxModuleBuildFailureKeepsPreviousReadyBuild(t *testing.T) {
target := dto.NginxModuleTarget{Key: "target"}
ready := dto.NginxModuleBuild{
Hash: "ready", Status: nginxModuleStatusReady, Target: target, BuiltAt: time.Now().Add(-time.Hour),
}
original := []dto.NginxModule{{
Name: "example", BuildMode: nginxModuleBuildDynamic,
Builds: []dto.NginxModuleBuild{ready},
}}
failed := dto.NginxModuleBuild{
Hash: "candidate", Status: nginxModuleStatusFailed, Target: target, Error: "load failed", BuiltAt: time.Now(),
}
result := recordNginxModuleBuildFailure(original, "example", failed, &ready)
if len(result[0].Builds) != 1 || result[0].Builds[0].Hash != "ready" {
t.Fatalf("previous ready build was replaced: %#v", result[0].Builds)
}
if result[0].LastError != failed.Error {
t.Fatalf("failure metadata was not retained: %#v", result[0])
}
result[0].Builds[0].Hash = "mutated"
if original[0].Builds[0].Hash != "ready" {
t.Fatal("module clone shares build state with the original")
}
}
func TestHasDynamicNginxModuleBuildTask(t *testing.T) {
dynamicEnabled := dto.NginxModule{Name: "brotli", Enable: true, BuildMode: nginxModuleBuildDynamic}
staticEnabled := dto.NginxModule{Name: "pagespeed", Enable: true, BuildMode: nginxModuleBuildStatic}
disabledDynamic := dto.NginxModule{Name: "waf", Enable: false, BuildMode: nginxModuleBuildDynamic}
if hasDynamicNginxModuleBuildTask(nil, nil) {
t.Fatal("empty module list should not require a dynamic build")
}
if hasDynamicNginxModuleBuildTask([]dto.NginxModule{staticEnabled}, nil) {
t.Fatal("static-only modules should not require a dynamic build")
}
if hasDynamicNginxModuleBuildTask([]dto.NginxModule{dynamicEnabled}, []string{"other"}) {
t.Fatal("enabled module outside the selection should not require a dynamic build")
}
if !hasDynamicNginxModuleBuildTask([]dto.NginxModule{disabledDynamic}, []string{"waf"}) {
t.Fatal("selected module should require a dynamic build even when disabled")
}
if !hasDynamicNginxModuleBuildTask([]dto.NginxModule{dynamicEnabled, staticEnabled}, nil) {
t.Fatal("enabled dynamic module should require a dynamic build")
}
legacy := dto.NginxModule{Name: "legacy", Enable: true}
if hasDynamicNginxModuleBuildTask([]dto.NginxModule{legacy}, nil) {
t.Fatal("legacy module without a build mode stays static and should not require a dynamic build")
}
if legacy.BuildMode != "" {
t.Fatalf("prescan must normalize a copy, got mutated BuildMode %q", legacy.BuildMode)
}
}
func TestNginxModuleStaticBuildErrorHintOnlyForCustomModules(t *testing.T) {
if hint := nginxModuleStaticBuildErrorHint(dto.NginxModule{Name: "builtin"}); hint != "" {
t.Fatalf("built-in module cannot switch build mode, got hint %q", hint)
}
if hint := nginxModuleStaticBuildErrorHint(dto.NginxModule{Name: "custom", Custom: true}); hint == "" {
t.Fatal("custom module should receive the static-build alternative")
}
}
func TestResolveNginxModuleTargetWithoutBuilder(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.27.1.2"}
install.App.Key = constant.AppOpenresty
_, _, err := resolveNginxModuleTarget(install)
if !errors.Is(err, errNginxModuleBuilderMissing) {
t.Fatalf("expected builder-missing sentinel, got %v", err)
}
if !strings.Contains(err.Error(), "dynamic module builder not found") {
t.Fatalf("unexpected error message: %v", err)
}
}
func TestBuildDynamicNginxModulesFailsWithoutBuilder(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
modules := []dto.NginxModule{{
Name: "rtmp", Enable: true, BuildMode: nginxModuleBuildDynamic,
}}
_, err := buildDynamicNginxModules(install, modules, nil, false, "", "", nil)
if !errors.Is(err, errNginxModuleBuilderMissing) {
t.Fatalf("missing target builder must fail the dynamic build, got %v", err)
}
}
func TestMergeOpenrestyModuleVolumes(t *testing.T) {
newService := map[string]interface{}{}
oldService := map[string]interface{}{
"volumes": []interface{}{
"./conf:/etc/nginx/conf.d:ro",
"./modules:/usr/local/openresty/nginx/modules/1panel:ro",
"./conf/modules-enabled:/usr/local/openresty/nginx/conf/modules-enabled:ro",
12345,
},
}
mergeOpenrestyModuleVolumes(newService, oldService)
merged, ok := newService["volumes"].([]interface{})
if !ok || len(merged) != 2 {
t.Fatalf("expected two module mounts to be merged, got %#v", newService["volumes"])
}
for _, volume := range merged {
if volume == "./conf:/etc/nginx/conf.d:ro" {
t.Fatal("unrelated mount should not be merged")
}
}
}
func TestMergeOpenrestyModuleVolumesKeepsExisting(t *testing.T) {
existing := "./modules:/usr/local/openresty/nginx/modules/1panel:ro"
newService := map[string]interface{}{
"volumes": []interface{}{existing, map[string]interface{}{"type": "bind"}},
}
oldService := map[string]interface{}{
"volumes": []interface{}{
"./modules:/usr/local/openresty/nginx/modules/1panel:ro",
"./conf/modules-enabled:/usr/local/openresty/nginx/conf/modules-enabled:ro",
},
}
mergeOpenrestyModuleVolumes(newService, oldService)
merged, ok := newService["volumes"].([]interface{})
if !ok || len(merged) != 3 {
t.Fatalf("expected only the missing mount to be appended, got %#v", newService["volumes"])
}
if merged[0] != existing {
t.Fatalf("existing mounts must keep their order, got %#v", merged)
}
if merged[2] != "./conf/modules-enabled:/usr/local/openresty/nginx/conf/modules-enabled:ro" {
t.Fatalf("missing module mount was not appended, got %#v", merged)
}
}
func TestResolveNginxModuleBuildMirror(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.27.1.2"}
install.App.Key = constant.AppOpenresty
if err := os.MkdirAll(install.GetPath(), constant.DirPerm); err != nil {
t.Fatal(err)
}
if got := resolveNginxModuleBuildMirror(install, "https://mirror.example.com"); got != "https://mirror.example.com" {
t.Fatalf("request mirror should win, got %q", got)
}
if got := resolveNginxModuleBuildMirror(install, ""); got != "" {
t.Fatalf("missing env file should yield an empty mirror, got %q", got)
}
envContent := "CONTAINER_PACKAGE_URL=https://apt.example.com\nOTHER=value\n"
if err := os.WriteFile(install.GetEnvPath(), []byte(envContent), constant.FilePerm); err != nil {
t.Fatal(err)
}
if got := resolveNginxModuleBuildMirror(install, ""); got != "https://apt.example.com" {
t.Fatalf("env CONTAINER_PACKAGE_URL should be the fallback, got %q", got)
}
if got := resolveNginxModuleBuildMirror(install, "https://mirror.example.com"); got != "https://mirror.example.com" {
t.Fatalf("request mirror should still win over the env value, got %q", got)
}
}
func writeNginxModuleCatalogStateFixture(t *testing.T, install model.AppInstall, catalog []dto.NginxModule, state string) {
t.Helper()
buildDir := path.Join(install.GetPath(), nginxModuleBuildDir)
if err := os.MkdirAll(buildDir, constant.DirPerm); err != nil {
t.Fatal(err)
}
catalogContent, err := json.Marshal(catalog)
if err != nil {
t.Fatal(err)
}
if err = os.WriteFile(path.Join(buildDir, nginxModuleCatalogFile), catalogContent, constant.FilePerm); err != nil {
t.Fatal(err)
}
if state != "" {
if err = os.WriteFile(path.Join(buildDir, nginxModuleStoreFile), []byte(state), constant.FilePerm); err != nil {
t.Fatal(err)
}
}
}
func TestLoadNginxModulesBuiltinStateCannotOverrideCatalogDefinition(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{{
Name: "rtmp", Script: "catalog-script", Packages: []string{"unzip"}, Params: "--add-module=/tmp/rtmp",
BuildMode: nginxModuleBuildDynamic, Provider: nginxModuleProviderLocal, LoadOrder: 20,
}}
state := `[{"name":"rtmp","enable":true,"script":"user-script","params":"--with-user","buildMode":"static","loadOrder":99,"lastError":"failed"}]`
writeNginxModuleCatalogStateFixture(t, install, catalog, state)
modules, err := loadNginxModules(install)
if err != nil {
t.Fatal(err)
}
if len(modules) != 1 {
t.Fatalf("expected one merged module, got %#v", modules)
}
module := modules[0]
if !module.Enable || module.LastError != "failed" {
t.Fatalf("builtin state was not applied: %#v", module)
}
if module.Script != "catalog-script" || module.Params != "--add-module=/tmp/rtmp" ||
module.BuildMode != nginxModuleBuildDynamic || module.LoadOrder != 20 {
t.Fatalf("builtin definition must come from catalog: %#v", module)
}
}
func TestLoadNginxModulesWithoutStateShowsDisabledCatalogModules(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{
{Name: "rtmp", Enable: true, BuildMode: nginxModuleBuildDynamic},
{Name: "geoip2", BuildMode: nginxModuleBuildDynamic},
}
writeNginxModuleCatalogStateFixture(t, install, catalog, "")
modules, err := loadNginxModules(install)
if err != nil {
t.Fatal(err)
}
if len(modules) != len(catalog) {
t.Fatalf("expected all catalog modules, got %#v", modules)
}
for _, module := range modules {
if module.Enable || module.Custom {
t.Fatalf("catalog modules must default to disabled built-ins: %#v", module)
}
}
}
func TestSaveNginxModulesPersistsOnlyBuiltinState(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{{
Name: "rtmp", Script: "catalog-script", Params: "--add-module=/tmp/rtmp",
BuildMode: nginxModuleBuildDynamic, Provider: nginxModuleProviderLocal, LoadOrder: 20,
}}
writeNginxModuleCatalogStateFixture(t, install, catalog, "")
modules, err := loadNginxModules(install)
if err != nil {
t.Fatal(err)
}
modules[0].Enable = true
modules[0].LastError = "failed"
if err = saveNginxModules(install, modules); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleStoreFile))
if err != nil {
t.Fatal(err)
}
var states []map[string]any
if err = json.Unmarshal(content, &states); err != nil {
t.Fatal(err)
}
if len(states) != 1 || states[0]["name"] != "rtmp" || states[0]["enable"] != true || states[0]["lastError"] != "failed" {
t.Fatalf("unexpected builtin state: %s", content)
}
for _, key := range []string{"script", "packages", "params", "buildMode", "provider", "loadOrder"} {
if _, exists := states[0][key]; exists {
t.Fatalf("builtin definition field %q leaked into module state: %s", key, content)
}
}
}
func TestSaveNginxModulesOmitsPristineBuiltinState(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{{Name: "rtmp", BuildMode: nginxModuleBuildDynamic}}
writeNginxModuleCatalogStateFixture(t, install, catalog, "")
modules, err := loadNginxModules(install)
if err != nil {
t.Fatal(err)
}
if err = saveNginxModules(install, modules); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleStoreFile))
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(string(content)) != "[]" {
t.Fatalf("pristine built-in state should not be persisted: %s", content)
}
}
func TestLoadAndSaveNginxCustomModule(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
state := `[{"name":"custom","custom":true,"script":"prepare","packages":["git"],"params":"--add-module=/tmp/custom","enable":true,"buildMode":"static","provider":"local","loadOrder":100}]`
writeNginxModuleCatalogStateFixture(t, install, nil, state)
modules, err := loadNginxModules(install)
if err != nil {
t.Fatal(err)
}
if len(modules) != 1 || !modules[0].Custom {
t.Fatalf("custom module source was not restored: %#v", modules)
}
if modules[0].Script != "prepare" || modules[0].BuildMode != nginxModuleBuildStatic || modules[0].LoadOrder != 100 {
t.Fatalf("custom module definition was not restored: %#v", modules[0])
}
if err = saveNginxModules(install, modules); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleStoreFile))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(content), `"custom": true`) || !strings.Contains(string(content), `"buildMode": "static"`) {
t.Fatalf("custom module definition was not persisted: %s", content)
}
}
func TestActivateNginxModuleCatalogReplacesCatalogAtomically(t *testing.T) {
buildDir := t.TempDir()
activePath := path.Join(buildDir, nginxModuleCatalogFile)
pendingPath := path.Join(buildDir, nginxModuleCatalogPendingFile)
sourcePath := path.Join(buildDir, "target.catalog.json")
if err := os.WriteFile(activePath, []byte(`[{"name":"old"}]`), constant.FilePerm); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(sourcePath, []byte(`[{"name":"new"}]`), constant.FilePerm); err != nil {
t.Fatal(err)
}
if err := stageNginxModuleCatalog(sourcePath, pendingPath); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(activePath)
if err != nil {
t.Fatal(err)
}
if string(content) != `[{"name":"old"}]` {
t.Fatalf("staging target catalog changed the active catalog: %s", content)
}
if err := activateNginxModuleCatalog(pendingPath, activePath); err != nil {
t.Fatal(err)
}
content, err = os.ReadFile(activePath)
if err != nil {
t.Fatal(err)
}
if string(content) != `[{"name":"new"}]` {
t.Fatalf("active catalog was not replaced: %s", content)
}
if _, err = os.Stat(pendingPath); !os.IsNotExist(err) {
t.Fatalf("pending catalog should be consumed, got %v", err)
}
}
func TestActivateNginxModuleCatalogRestoresCatalogWhenCommitFails(t *testing.T) {
buildDir := t.TempDir()
activePath := path.Join(buildDir, nginxModuleCatalogFile)
pendingPath := path.Join(buildDir, nginxModuleCatalogPendingFile)
if err := os.WriteFile(activePath, []byte(`[{"name":"old"}]`), constant.FilePerm); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(pendingPath, []byte(`[{"name":"new"}]`), constant.FilePerm); err != nil {
t.Fatal(err)
}
commitErr := errors.New("save install failed")
err := activateNginxModuleCatalogAndCommit(pendingPath, activePath, func() error {
return commitErr
})
if !errors.Is(err, commitErr) {
t.Fatalf("expected commit error, got %v", err)
}
content, readErr := os.ReadFile(activePath)
if readErr != nil {
t.Fatal(readErr)
}
if string(content) != `[{"name":"old"}]` {
t.Fatalf("failed upgrade must restore the old catalog: %s", content)
}
}
func TestLoadNginxModulesRejectsDuplicateCatalogNames(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{
{Name: "rtmp", Params: "--add-module=/tmp/one", BuildMode: nginxModuleBuildDynamic},
{Name: "rtmp", Params: "--add-module=/tmp/two", BuildMode: nginxModuleBuildDynamic},
}
writeNginxModuleCatalogStateFixture(t, install, catalog, "")
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "duplicate") {
t.Fatalf("expected duplicate catalog name error, got %v", err)
}
}
func TestLoadNginxModulesRejectsDuplicateStateNames(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{{Name: "rtmp", Params: "--add-module=/tmp/rtmp", BuildMode: nginxModuleBuildDynamic}}
writeNginxModuleCatalogStateFixture(t, install, catalog, `[{"name":"rtmp","enable":true},{"name":"rtmp","enable":false}]`)
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "duplicate") {
t.Fatalf("expected duplicate state name error, got %v", err)
}
}
func TestLoadNginxModulesRejectsOrphanBuiltinState(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
writeNginxModuleCatalogStateFixture(t, install, nil, `[{"name":"removed","enable":true}]`)
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "missing from the module catalog") {
t.Fatalf("expected orphan builtin state error, got %v", err)
}
}
func TestLoadNginxModulesRejectsCustomCatalogNameConflict(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{{Name: "rtmp", BuildMode: nginxModuleBuildDynamic}}
writeNginxModuleCatalogStateFixture(t, install, catalog, `[{"name":"rtmp","custom":true,"enable":true}]`)
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "conflicts") {
t.Fatalf("expected custom/catalog conflict error, got %v", err)
}
}
func TestLoadNginxModulesRejectsInvalidBuildModes(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
writeNginxModuleCatalogStateFixture(t, install, []dto.NginxModule{{
Name: "rtmp", BuildMode: "auto",
}}, "")
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "invalid build mode") {
t.Fatalf("expected invalid catalog build mode error, got %v", err)
}
writeNginxModuleCatalogStateFixture(t, install, nil, `[{"name":"custom","custom":true,"buildMode":"auto"}]`)
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "invalid build mode") {
t.Fatalf("expected invalid custom build mode error, got %v", err)
}
}
func TestApplyNginxModuleUpdateKeepsBuiltinDefinitionImmutable(t *testing.T) {
modules := []dto.NginxModule{{
Name: "rtmp", Script: "catalog-script", Params: "--add-module=/tmp/rtmp",
BuildMode: nginxModuleBuildDynamic, Provider: nginxModuleProviderLocal, LoadOrder: 20,
}}
req := request.NginxModuleUpdate{
Operate: nginxModuleOperateUpdate, Name: "rtmp", Enable: true, Script: "user-script",
Params: "--with-user", BuildMode: nginxModuleBuildStatic, Provider: "prebuilt", LoadOrder: 99,
}
updated, _, err := applyNginxModuleUpdate(modules, req)
if err != nil {
t.Fatal(err)
}
if !updated[0].Enable {
t.Fatal("builtin enable state was not updated")
}
if updated[0].Script != "catalog-script" || updated[0].Params != "--add-module=/tmp/rtmp" ||
updated[0].BuildMode != nginxModuleBuildDynamic || updated[0].Provider != nginxModuleProviderLocal ||
updated[0].LoadOrder != 20 {
t.Fatalf("builtin definition was modified: %#v", updated[0])
}
}
func TestApplyNginxModuleUpdateRejectsBuiltinDelete(t *testing.T) {
modules := []dto.NginxModule{{Name: "rtmp", BuildMode: nginxModuleBuildDynamic}}
_, _, err := applyNginxModuleUpdate(modules, request.NginxModuleUpdate{
Operate: nginxModuleOperateDelete, Name: "rtmp",
})
if err == nil || !strings.Contains(err.Error(), "cannot be deleted") {
t.Fatalf("expected builtin delete error, got %v", err)
}
}
func TestApplyNginxModuleUpdateCreatesAndDeletesCustomModule(t *testing.T) {
if _, _, err := applyNginxModuleUpdate(nil, request.NginxModuleUpdate{
Operate: nginxModuleOperateCreate, Name: "invalid", BuildMode: "auto",
}); err == nil || !strings.Contains(err.Error(), "invalid build mode") {
t.Fatalf("expected invalid custom build mode error, got %v", err)
}
created, _, err := applyNginxModuleUpdate(nil, request.NginxModuleUpdate{
Operate: nginxModuleOperateCreate, Name: "custom", Script: "prepare", Packages: "git,curl",
Params: "--add-module=/tmp/custom", Enable: true, BuildMode: nginxModuleBuildStatic,
Provider: nginxModuleProviderLocal, LoadOrder: 100,
})
if err != nil {
t.Fatal(err)
}
if len(created) != 1 || !created[0].Custom || created[0].BuildMode != nginxModuleBuildStatic {
t.Fatalf("custom module was not created correctly: %#v", created)
}
updated, _, err := applyNginxModuleUpdate(created, request.NginxModuleUpdate{
Operate: nginxModuleOperateUpdate, Name: "custom", Script: "updated",
Params: "--add-module=/tmp/custom-v2", Enable: false, BuildMode: nginxModuleBuildDynamic,
Provider: nginxModuleProviderLocal, LoadOrder: 75,
})
if err != nil {
t.Fatal(err)
}
if updated[0].Script != "updated" || updated[0].BuildMode != nginxModuleBuildDynamic || updated[0].LoadOrder != 75 {
t.Fatalf("custom module was not updated correctly: %#v", updated[0])
}
remaining, deleted, err := applyNginxModuleUpdate(updated, request.NginxModuleUpdate{
Operate: nginxModuleOperateDelete, Name: "custom",
})
if err != nil {
t.Fatal(err)
}
if len(remaining) != 0 || deleted == nil || deleted.Name != "custom" {
t.Fatalf("custom module was not removed: remaining=%#v deleted=%#v", remaining, deleted)
}
}
+74 -294
View File
@@ -29,7 +29,6 @@ import (
"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/compose"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
@@ -83,47 +82,6 @@ func NewRuntimeService() IRuntimeService {
return &RuntimeService{}
}
var pullRuntimeComposeImages = compose.PullComposeImages
var downRuntimeCompose = compose.Down
type runtimeTaskMeta struct {
resourceName string
operate string
scope string
taskID string
resourceID uint
}
func buildRuntimeCreateTaskMeta(create request.RuntimeCreate, runtimeID uint) runtimeTaskMeta {
return runtimeTaskMeta{
resourceName: create.Name,
operate: task.TaskCreate,
scope: task.TaskScopeRuntime,
taskID: create.TaskID,
resourceID: runtimeID,
}
}
func buildRuntimeDeleteTaskMeta(deleteReq request.RuntimeDelete, runtime *model.Runtime) runtimeTaskMeta {
return runtimeTaskMeta{
resourceName: runtime.Name,
operate: task.TaskDelete,
scope: task.TaskScopeRuntime,
taskID: deleteReq.TaskID,
resourceID: deleteReq.ID,
}
}
func runtimeCreateInitialStatus(create request.RuntimeCreate) string {
if create.Type == constant.RuntimePHP {
if create.Resource == constant.ResourceLocal {
return constant.StatusNormal
}
return constant.StatusBuilding
}
return constant.StatusCreating
}
func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, error) {
var (
opts []repo.DBOption
@@ -146,29 +104,20 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
return nil, err
}
}
if create.Type == constant.RuntimePHP && create.Resource == constant.ResourceLocal {
runtime := &model.Runtime{
Name: create.Name,
Resource: create.Resource,
Type: create.Type,
Version: create.Version,
Status: runtimeCreateInitialStatus(create),
Remark: create.Remark,
}
if err := runtimeRepo.Create(context.Background(), runtime); err != nil {
return nil, err
}
if err := startRuntimeCreateTask(create, runtime, model.App{}, model.AppDetail{}, ""); err != nil {
runtime.Status = constant.StatusError
runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime)
return nil, err
}
return runtime, nil
}
var hostPorts []string
switch create.Type {
case constant.RuntimePHP:
if create.Resource == constant.ResourceLocal {
runtime := &model.Runtime{
Name: create.Name,
Resource: create.Resource,
Type: create.Type,
Version: create.Version,
Status: constant.StatusNormal,
Remark: create.Remark,
}
return nil, runtimeRepo.Create(context.Background(), runtime)
}
exist, _ = runtimeRepo.GetFirst(context.Background(), runtimeRepo.WithImage(create.Image))
if exist != nil {
return nil, buserr.New("ErrImageExist")
@@ -185,9 +134,10 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
if !fileOp.Stat(create.CodeDir) {
return nil, buserr.New("ErrPathNotFound")
}
create.Install = true
for _, export := range create.ExposedPorts {
hostPorts = append(hostPorts, strconv.Itoa(export.HostPort))
if err := checkPortExistWithProtocol(export.HostPort, export.Protocol); err != nil {
if err := checkPortExist(export.HostPort); err != nil {
return nil, err
}
}
@@ -210,6 +160,11 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
}
appVersionDir := filepath.Join(app.GetAppResourcePath(), appDetail.Version)
if !fileOp.Stat(appVersionDir) {
if err = downloadApp(app, appDetail, nil, nil); err != nil {
return nil, err
}
}
runtime := &model.Runtime{
Name: create.Name,
@@ -221,92 +176,24 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
ContainerName: containerName.(string),
Port: strings.Join(hostPorts, ","),
Remark: create.Remark,
Status: runtimeCreateInitialStatus(create),
}
switch create.Type {
case constant.RuntimePHP:
if err = handlePHP(create, runtime, fileOp, appVersionDir); err != nil {
return nil, err
}
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDotNet:
if err = handleRuntime(create, runtime, fileOp, appVersionDir); err != nil {
return nil, err
}
}
if err := runtimeRepo.Create(context.Background(), runtime); err != nil {
return nil, err
}
if err := startRuntimeCreateTask(create, runtime, app, appDetail, appVersionDir); err != nil {
runtime.Status = constant.StatusError
runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime)
return nil, err
}
return runtime, nil
}
func startRuntimeCreateTask(create request.RuntimeCreate, runtime *model.Runtime, app model.App, appDetail model.AppDetail, appVersionDir string) error {
meta := buildRuntimeCreateTaskMeta(create, runtime.ID)
createTask, err := task.NewTaskWithOps(meta.resourceName, meta.operate, meta.scope, meta.taskID, meta.resourceID)
if err != nil {
return err
}
createTask.AddSubTask(task.GetTaskName(create.Name, task.TaskCreate, task.TaskScopeRuntime), func(t *task.Task) error {
return executeRuntimeCreateTask(create, runtime, app, appDetail, appVersionDir, t)
}, nil)
go func() {
_ = createTask.Execute()
}()
return nil
}
func executeRuntimeCreateTask(create request.RuntimeCreate, runtime *model.Runtime, app model.App, appDetail model.AppDetail, appVersionDir string, taskItem *task.Task) error {
if create.Type == constant.RuntimePHP && create.Resource == constant.ResourceLocal {
runtime.Status = constant.StatusNormal
runtime.Message = ""
return runtimeRepo.Save(runtime)
}
if err := downloadApp(app, appDetail, nil, taskItem.Logger); err != nil {
return markRuntimeCreateFailed(runtime, err)
}
go func() {
RequestDownloadCallBack(appDetail.DownloadCallBackUrl)
}()
fileOp := files.NewFileOp()
var err error
switch create.Type {
case constant.RuntimePHP:
err = handlePHP(create, runtime, fileOp, appVersionDir)
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDotNet:
err = handleRuntime(create, runtime, fileOp, appVersionDir)
}
if err != nil {
return markRuntimeCreateFailed(runtime, err)
}
if err = runtimeRepo.Save(runtime); err != nil {
return err
}
switch create.Type {
case constant.RuntimePHP:
return buildRuntimeWithResult(runtime, "", "", false)
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDotNet:
if err := pullRuntimeImagesBeforeStart(create, runtime, taskItem); err != nil {
return markRuntimeCreateFailed(runtime, err)
}
return startRuntimeWithResult(runtime)
}
return nil
}
func pullRuntimeImagesBeforeStart(create request.RuntimeCreate, runtime *model.Runtime, taskItem *task.Task) error {
switch create.Type {
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDotNet:
return pullRuntimeComposeImages(runtime.GetComposePath(), false, taskItem)
default:
return nil
}
}
func markRuntimeCreateFailed(runtime *model.Runtime, err error) error {
runtime.Status = constant.StatusError
runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime)
return err
}
func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.RuntimeDTO, error) {
var (
opts []repo.DBOption
@@ -354,11 +241,30 @@ func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.Runt
runtimeDTO.AppID = detail.AppId
}
for k, v := range envs {
if !isComposePortEnvKey(k) {
runtimeDTO.Params[k] = v
runtimeDTO.Params[k] = v
if strings.Contains(k, "CONTAINER_PORT") || strings.Contains(k, "HOST_PORT") {
if strings.Contains(k, "CONTAINER_PORT") {
matches := re.GetRegex(re.TrailingDigitsPattern).FindStringSubmatch(k)
if len(matches) < 2 {
continue
}
containerPort, err := strconv.Atoi(v)
if err != nil {
continue
}
hostPort, err := strconv.Atoi(envs[fmt.Sprintf("HOST_PORT_%s", matches[1])])
if err != nil {
continue
}
hostIP := envs[fmt.Sprintf("HOST_IP_%s", matches[1])]
runtimeDTO.ExposedPorts = append(runtimeDTO.ExposedPorts, request.ExposedPort{
ContainerPort: containerPort,
HostPort: hostPort,
HostIP: hostIP,
})
}
}
}
runtimeDTO.ExposedPorts, _ = loadComposeExposedPortsFromEnv(envs, "", false)
res = append(res, runtimeDTO)
}
return total, res, nil
@@ -385,37 +291,31 @@ func (r *RuntimeService) Delete(runtimeDelete request.RuntimeDelete) error {
if website.ID > 0 {
return buserr.New("ErrDelWithWebsite")
}
return startRuntimeDeleteTask(runtimeDelete, runtime)
}
func startRuntimeDeleteTask(runtimeDelete request.RuntimeDelete, runtime *model.Runtime) error {
meta := buildRuntimeDeleteTaskMeta(runtimeDelete, runtime)
deleteTask, err := task.NewTaskWithOps(meta.resourceName, meta.operate, meta.scope, meta.taskID, meta.resourceID)
if err != nil {
return err
}
deleteTask.AddSubTask(task.GetTaskName(runtime.Name, task.TaskDelete, task.TaskScopeRuntime), func(t *task.Task) error {
return executeRuntimeDeleteTask(runtimeDelete, runtime, t)
}, nil)
go func() {
_ = deleteTask.Execute()
}()
return nil
}
func executeRuntimeDeleteTask(runtimeDelete request.RuntimeDelete, runtime *model.Runtime, taskItem *task.Task) error {
if runtime.Resource != constant.ResourceAppstore {
if runtimeDelete.DeleteImage {
deleteRuntimeImages(runtime, taskItem)
}
return runtimeRepo.DeleteBy(repo.WithByID(runtimeDelete.ID))
}
projectDir := runtime.GetPath()
if err := stopRuntimeBeforeDelete(runtime, runtimeDelete.ForceDelete, taskItem); err != nil {
if out, err := compose.Down(runtime.GetComposePath()); err != nil && !runtimeDelete.ForceDelete {
if out != "" {
return errors.New(out)
}
return err
}
if runtimeDelete.DeleteImage {
deleteRuntimeImages(runtime, taskItem)
if runtime.Type == constant.RuntimePHP {
client, err := docker.NewClient()
if err != nil {
return err
}
defer client.Close()
imageID, err := client.GetImageIDByName(runtime.Image)
if err != nil {
return err
}
if imageID != "" {
if err := client.DeleteImage(imageID); err != nil {
global.LOG.Errorf("delete image id [%s] error %v", imageID, err)
}
}
}
if err := files.NewFileOp().DeleteDir(projectDir); err != nil && !runtimeDelete.ForceDelete {
return err
@@ -423,121 +323,6 @@ func executeRuntimeDeleteTask(runtimeDelete request.RuntimeDelete, runtime *mode
return runtimeRepo.DeleteBy(repo.WithByID(runtimeDelete.ID))
}
func getRuntimeStopLog() string {
logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("Runtime")
if strings.TrimSpace(logStr) == "" {
return "StopRuntime"
}
return logStr
}
func stopRuntimeBeforeDelete(runtime *model.Runtime, forceDelete bool, taskItem *task.Task) error {
logStr := getRuntimeStopLog()
if taskItem != nil {
taskItem.Log(logStr)
}
out, err := downRuntimeCompose(runtime.GetComposePath())
if err != nil && !forceDelete {
if out != "" {
err = errors.New(out)
}
if taskItem != nil {
taskItem.LogFailedWithErr(logStr, err)
}
return err
}
if taskItem != nil {
taskItem.LogSuccess(logStr)
}
return nil
}
func getRuntimeDeleteImages(runtime *model.Runtime) ([]string, error) {
var images []string
imageMap := make(map[string]struct{})
appendImage := func(image string) {
image = strings.TrimSpace(image)
if image == "" {
return
}
if _, ok := imageMap[image]; ok {
return
}
imageMap[image] = struct{}{}
images = append(images, image)
}
appendImage(runtime.Image)
if runtime.DockerCompose != "" {
composeImages, err := docker.GetImagesFromDockerCompose([]byte(runtime.Env), []byte(runtime.DockerCompose))
if err != nil {
return nil, err
}
for _, image := range composeImages {
appendImage(image)
}
}
return images, nil
}
func deleteRuntimeImages(runtime *model.Runtime, taskItem *task.Task) {
logPrefix := i18n.GetMsgByKey("TaskDelete") + i18n.GetMsgByKey("Image")
images, err := getRuntimeDeleteImages(runtime)
if err != nil {
global.LOG.Errorf("get runtime [%s] delete images error %v", runtime.Name, err)
if taskItem != nil {
taskItem.LogFailedWithErr(logPrefix, err)
}
return
}
if len(images) == 0 {
if taskItem != nil {
taskItem.LogFailedWithErr(logPrefix, errors.New(i18n.GetWithName("ErrImageNotExist", runtime.Name)))
}
return
}
client, err := docker.NewClient()
if err != nil {
global.LOG.Errorf("delete runtime images [%s] error %v", runtime.Name, err)
if taskItem != nil {
taskItem.LogFailedWithErr(logPrefix, err)
}
return
}
defer client.Close()
for _, imageName := range images {
logStr := logPrefix + imageName
if taskItem != nil {
taskItem.Log(logStr)
}
imageID, err := client.GetImageIDByName(imageName)
if err != nil {
global.LOG.Errorf("get runtime image [%s] error %v", imageName, err)
if taskItem != nil {
taskItem.LogFailedWithErr(logStr, err)
}
continue
}
if imageID == "" {
if taskItem != nil {
taskItem.LogFailedWithErr(logStr, errors.New(i18n.GetWithName("ErrImageNotExist", imageName)))
}
continue
}
if err := client.DeleteImage(imageID); err != nil {
global.LOG.Errorf("delete image id [%s] error %v", imageID, err)
if taskItem != nil {
taskItem.LogFailedWithErr(logStr, err)
}
continue
}
if taskItem != nil {
taskItem.LogSuccess(logStr)
}
}
}
func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) {
runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(id))
if err != nil {
@@ -650,11 +435,9 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
return buserr.New("ErrImageExist")
}
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDotNet:
ownedPortKeys := runtimeOwnedPortKeys(runtime.Env)
for _, export := range req.ExposedPorts {
hostPorts = append(hostPorts, strconv.Itoa(export.HostPort))
_, owned := ownedPortKeys[composePortCheckKey(export.HostPort, export.Protocol)]
if err = checkRuntimePortExistWithProtocol(export.HostPort, export.Protocol, !owned, runtime.ID); err != nil {
if err = checkRuntimePortExist(export.HostPort, false, runtime.ID); err != nil {
return err
}
}
@@ -695,7 +478,7 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
Version: req.Version,
Remark: req.Remark,
NodeConfig: request.NodeConfig{
Install: req.Install,
Install: true,
ExposedPorts: req.ExposedPorts,
Environments: req.Environments,
Volumes: req.Volumes,
@@ -1275,7 +1058,6 @@ func (r *RuntimeService) UpdatePHPContainer(req request.PHPContainerConfig) erro
var (
composeContent []byte
)
ownedPortKeys := runtimeOwnedPortKeys(runtime.Env)
for _, export := range req.ExposedPorts {
if strconv.Itoa(export.HostPort) == runtime.Port {
return buserr.WithName("ErrPHPRuntimePortFailed", strconv.Itoa(export.HostPort))
@@ -1283,8 +1065,7 @@ func (r *RuntimeService) UpdatePHPContainer(req request.PHPContainerConfig) erro
if export.ContainerPort == 9000 {
return buserr.New("ErrPHPPortIsDefault")
}
_, owned := ownedPortKeys[composePortCheckKey(export.HostPort, export.Protocol)]
if err = checkRuntimePortExistWithProtocol(export.HostPort, export.Protocol, !owned, runtime.ID); err != nil {
if err = checkRuntimePortExist(export.HostPort, false, runtime.ID); err != nil {
return err
}
}
@@ -1309,7 +1090,7 @@ func (r *RuntimeService) UpdatePHPContainer(req request.PHPContainerConfig) erro
return err
}
for k := range envs {
if isComposePortEnvKey(k) || strings.Contains(k, "APP_PORT") {
if strings.HasPrefix(k, "CONTAINER_PORT_") || strings.HasPrefix(k, "HOST_PORT_") || strings.HasPrefix(k, "HOST_IP_") || strings.Contains(k, "APP_PORT") {
delete(envs, k)
}
}
@@ -1321,7 +1102,6 @@ func (r *RuntimeService) UpdatePHPContainer(req request.PHPContainerConfig) erro
ExposedPorts: req.ExposedPorts,
Environments: req.Environments,
Volumes: req.Volumes,
ExtraHosts: req.ExtraHosts,
},
}
composeContent, err = handleCompose(envs, composeContent, create, projectDir)
-411
View File
@@ -1,411 +0,0 @@
package service
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"regexp"
"runtime"
stdpprof "runtime/pprof"
"sort"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
profile "github.com/google/pprof/profile"
"github.com/shirou/gopsutil/v4/process"
)
const (
diagnosticsDefaultDuration = 15
diagnosticsMaxProfileSize = 64 * 1024 * 1024
diagnosticsMaxSnapshotSize = 16 * 1024 * 1024
diagnosticsDetailedGoroutines = 10_000
diagnosticsBlockProfileRate = 1_000_000
diagnosticsMaxGroups = 200
)
var (
runtimeDiagnosticsInstance = &RuntimeDiagnosticsService{}
goroutineHeaderPattern = regexp.MustCompile(`^goroutine \d+ \[([^]]+)\]:$`)
goroutineArgPattern = regexp.MustCompile(`0x[0-9a-fA-F]+`)
errProfileSizeLimit = errors.New("runtime profile exceeds the 64 MiB size limit")
)
type cappedWriter struct {
writer io.Writer
remaining int64
exceeded bool
}
func (w *cappedWriter) Write(data []byte) (int, error) {
if int64(len(data)) <= w.remaining {
n, err := w.writer.Write(data)
w.remaining -= int64(n)
return n, err
}
w.exceeded = true
if w.remaining <= 0 {
return 0, errProfileSizeLimit
}
allowed := int(w.remaining)
n, err := w.writer.Write(data[:allowed])
w.remaining -= int64(n)
if err != nil {
return n, err
}
return n, errProfileSizeLimit
}
type IRuntimeDiagnosticsService interface {
Summary() (dto.RuntimeDiagnosticsSummary, error)
Goroutines() (dto.RuntimeGoroutineSnapshot, error)
CreateProfile(req dto.RuntimeProfileCreate) (RuntimeProfileResult, error)
}
type RuntimeProfileResult struct {
Path string
Name string
}
type RuntimeDiagnosticsService struct {
captureMu sync.Mutex
processMu sync.Mutex
process *process.Process
}
func NewIRuntimeDiagnosticsService() IRuntimeDiagnosticsService {
return runtimeDiagnosticsInstance
}
func (s *RuntimeDiagnosticsService) Summary() (dto.RuntimeDiagnosticsSummary, error) {
rss, err := s.processRSS()
if err != nil {
return dto.RuntimeDiagnosticsSummary{}, err
}
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
return dto.RuntimeDiagnosticsSummary{
RSS: rss,
HeapAlloc: stats.HeapAlloc,
HeapObjects: stats.HeapObjects,
Goroutines: runtime.NumGoroutine(),
}, nil
}
func (s *RuntimeDiagnosticsService) Goroutines() (dto.RuntimeGoroutineSnapshot, error) {
total := runtime.NumGoroutine()
if total > diagnosticsDetailedGoroutines {
groups, truncated := compactGoroutineSnapshot(total)
return dto.RuntimeGoroutineSnapshot{
Total: total, GroupCount: len(groups), Truncated: truncated, CapturedAt: time.Now(), Goroutines: groups,
}, nil
}
var data bytes.Buffer
writer := &cappedWriter{writer: &data, remaining: diagnosticsMaxSnapshotSize}
if err := stdpprof.Lookup("goroutine").WriteTo(writer, 2); err != nil {
groups, truncated := compactGoroutineSnapshot(total)
return dto.RuntimeGoroutineSnapshot{
Total: total, GroupCount: len(groups), Truncated: truncated, CapturedAt: time.Now(), Goroutines: groups,
}, nil
}
groups, truncated := parseGoroutineDump(&data, diagnosticsMaxGroups)
result := dto.RuntimeGoroutineSnapshot{
Total: total,
GroupCount: len(groups),
Truncated: truncated,
CapturedAt: time.Now(),
}
result.Goroutines = groups
return result, nil
}
func (s *RuntimeDiagnosticsService) CreateProfile(req dto.RuntimeProfileCreate) (RuntimeProfileResult, error) {
if !s.captureMu.TryLock() {
return RuntimeProfileResult{}, errors.New("another runtime profile is being captured")
}
defer s.captureMu.Unlock()
return captureRuntimeProfile(req)
}
func captureRuntimeProfile(req dto.RuntimeProfileCreate) (result RuntimeProfileResult, err error) {
duration := req.Duration
if duration == 0 {
duration = diagnosticsDefaultDuration
}
if duration < 5 || duration > 30 {
return result, errors.New("profile duration must be between 5 and 30 seconds")
}
if req.Type == "heap" || req.Type == "goroutine" {
duration = 0
}
name := fmt.Sprintf("%s-%s-%ds.pb.gz", req.Type, newRuntimeEventID(), duration)
file, err := os.CreateTemp("", "1panel-runtime-profile-*.tmp")
if err != nil {
return result, err
}
writer := &cappedWriter{writer: file, remaining: diagnosticsMaxProfileSize}
removeOnError := true
defer func() {
_ = file.Close()
if removeOnError {
_ = os.Remove(file.Name())
}
}()
switch req.Type {
case "cpu":
if err = stdpprof.StartCPUProfile(writer); err != nil {
return result, err
}
time.Sleep(time.Duration(duration) * time.Second)
stdpprof.StopCPUProfile()
case "heap":
err = stdpprof.Lookup("heap").WriteTo(writer, 0)
case "goroutine":
err = stdpprof.Lookup("goroutine").WriteTo(writer, 0)
case "mutex":
err = captureWindowedRuntimeProfile("mutex", duration, writer, func() func() {
previous := runtime.SetMutexProfileFraction(5)
return func() { runtime.SetMutexProfileFraction(previous) }
})
case "block":
err = captureWindowedRuntimeProfile("block", duration, writer, func() func() {
runtime.SetBlockProfileRate(diagnosticsBlockProfileRate)
return func() { runtime.SetBlockProfileRate(0) }
})
default:
return result, errors.New("unsupported runtime profile type")
}
if err != nil {
return result, err
}
if writer.exceeded {
return result, errProfileSizeLimit
}
if err = file.Close(); err != nil {
return result, err
}
removeOnError = false
return RuntimeProfileResult{Path: file.Name(), Name: name}, nil
}
func captureWindowedRuntimeProfile(name string, duration int, writer io.Writer, enable func() func()) error {
restore := enable()
sampling := true
defer func() {
if sampling {
restore()
}
}()
before, err := readRuntimeProfile(name)
if err != nil {
return err
}
startedAt := time.Now()
time.Sleep(time.Duration(duration) * time.Second)
restore()
sampling = false
after, err := readRuntimeProfile(name)
if err != nil {
return err
}
delta, err := diffRuntimeProfiles(before, after, startedAt, time.Duration(duration)*time.Second)
if err != nil {
return err
}
return delta.Write(writer)
}
func readRuntimeProfile(name string) (*profile.Profile, error) {
var data bytes.Buffer
writer := &cappedWriter{writer: &data, remaining: diagnosticsMaxProfileSize}
if err := stdpprof.Lookup(name).WriteTo(writer, 0); err != nil {
return nil, err
}
if writer.exceeded {
return nil, errProfileSizeLimit
}
return profile.Parse(&data)
}
func diffRuntimeProfiles(before, after *profile.Profile, startedAt time.Time, duration time.Duration) (*profile.Profile, error) {
baseline := before.Copy()
baseline.Scale(-1)
delta, err := profile.Merge([]*profile.Profile{after, baseline})
if err != nil {
return nil, err
}
delta.TimeNanos = startedAt.UnixNano()
delta.DurationNanos = duration.Nanoseconds()
return delta, nil
}
func (s *RuntimeDiagnosticsService) processRSS() (uint64, error) {
s.processMu.Lock()
defer s.processMu.Unlock()
if s.process == nil {
proc, err := process.NewProcess(int32(os.Getpid()))
if err != nil {
return 0, err
}
s.process = proc
}
memoryInfo, err := s.process.MemoryInfo()
if err != nil {
return 0, err
}
if memoryInfo == nil {
return 0, errors.New("process memory information is unavailable")
}
return memoryInfo.RSS, nil
}
func newRuntimeEventID() string {
return time.Now().Format("20060102-150405.000000000")
}
func parseGoroutineDump(reader io.Reader, maxGroups int) ([]dto.RuntimeGoroutineGroup, bool) {
type groupValue struct {
state string
top string
stack []string
count int
}
groups := make(map[string]*groupValue)
truncated := false
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 64*1024), 4*1024*1024)
var block []string
flush := func() {
if len(block) == 0 {
return
}
matches := goroutineHeaderPattern.FindStringSubmatch(block[0])
state := "unknown"
if len(matches) == 2 {
state = matches[1]
}
stack := append([]string(nil), block[1:]...)
if len(stack) > 40 {
stack = stack[:40]
}
top := "runtime"
functionLines := make([]string, 0, len(stack)/2)
for _, line := range stack {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "created by ") {
continue
}
normalized := goroutineArgPattern.ReplaceAllString(trimmed, "0x…")
functionLines = append(functionLines, normalized)
if top == "runtime" {
top = strings.Split(normalized, "(")[0]
}
}
signature := state + "\n" + strings.Join(functionLines, "\n")
if existing, ok := groups[signature]; ok {
existing.count++
return
}
if len(groups) >= maxGroups {
truncated = true
return
}
groups[signature] = &groupValue{state: state, top: top, stack: stack, count: 1}
}
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "goroutine ") && strings.HasSuffix(line, "]:") {
flush()
block = []string{line}
continue
}
if len(block) > 0 {
block = append(block, line)
}
}
if scanner.Err() != nil {
truncated = true
}
flush()
result := make([]dto.RuntimeGoroutineGroup, 0, len(groups))
for _, group := range groups {
result = append(result, dto.RuntimeGoroutineGroup{State: group.state, Top: group.top, Count: group.count, Stack: group.stack})
}
sort.Slice(result, func(i, j int) bool {
if result[i].Count == result[j].Count {
return result[i].Top < result[j].Top
}
return result[i].Count > result[j].Count
})
return result, truncated
}
func compactGoroutineSnapshot(initialSize int) ([]dto.RuntimeGoroutineGroup, bool) {
records := make([]runtime.StackRecord, initialSize+32)
count, ok := runtime.GoroutineProfile(records)
if !ok {
records = make([]runtime.StackRecord, count+32)
count, ok = runtime.GoroutineProfile(records)
}
truncated := !ok
if count > len(records) {
count = len(records)
truncated = true
}
records = records[:count]
type compactGroup struct {
top string
stack []string
count int
}
groups := make(map[string]*compactGroup)
for _, record := range records {
frames := runtime.CallersFrames(record.Stack())
stack := make([]string, 0, 16)
functions := make([]string, 0, 8)
top := "runtime"
for {
frame, more := frames.Next()
if frame.Function != "" {
if top == "runtime" {
top = frame.Function
}
functions = append(functions, frame.Function)
stack = append(stack, frame.Function, fmt.Sprintf("\t%s:%d", frame.File, frame.Line))
}
if !more || len(functions) >= 20 {
break
}
}
signature := strings.Join(functions, "\n")
if existing, exists := groups[signature]; exists {
existing.count++
continue
}
if len(groups) >= diagnosticsMaxGroups {
truncated = true
continue
}
groups[signature] = &compactGroup{top: top, stack: stack, count: 1}
}
result := make([]dto.RuntimeGoroutineGroup, 0, len(groups))
for _, group := range groups {
result = append(result, dto.RuntimeGoroutineGroup{State: "profiled", Top: group.top, Count: group.count, Stack: group.stack})
}
sort.Slice(result, func(i, j int) bool {
if result[i].Count == result[j].Count {
return result[i].Top < result[j].Top
}
return result[i].Count > result[j].Count
})
return result, truncated
}
+68 -79
View File
@@ -19,6 +19,7 @@ import (
cmd2 "github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
@@ -32,6 +33,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/compose"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/pkg/errors"
"github.com/subosito/gotenv"
"gopkg.in/yaml.v3"
@@ -65,11 +67,18 @@ func handleRuntime(create request.RuntimeCreate, runtime *model.Runtime, fileOp
runtime.Env = string(envContent)
runtime.Status = constant.StatusCreating
runtime.CodeDir = create.CodeDir
return
}
func runtimeNodeShouldInstallDependencies(create request.RuntimeCreate) bool {
return create.NodeConfig.Install == nil || *create.NodeConfig.Install
nodeDetail, err := appDetailRepo.GetFirst(repo.WithByID(runtime.AppDetailID))
if err != nil {
return err
}
go func() {
RequestDownloadCallBack(nodeDetail.DownloadCallBackUrl)
}()
go startRuntime(runtime)
return
}
func isPathInsideOrEqual(baseDir, targetDir string) bool {
@@ -122,28 +131,32 @@ func handlePHP(create request.RuntimeCreate, runtime *model.Runtime, fileOp file
runtime.Env = string(envContent)
runtime.Params = string(forms)
runtime.Status = constant.StatusBuilding
go func() {
appDetail, err := appDetailRepo.GetFirst(repo.WithByID(runtime.AppDetailID))
if err == nil {
RequestDownloadCallBack(appDetail.DownloadCallBackUrl)
}
}()
go buildRuntime(runtime, "", "", false)
return
}
func startRuntime(runtime *model.Runtime) {
_ = startRuntimeWithResult(runtime)
}
func startRuntimeWithResult(runtime *model.Runtime) error {
if err := runComposeCmdWithLog("up", runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
runtime.Status = constant.StatusError
runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime)
return err
return
}
if err := SyncRuntimeContainerStatus(runtime); err != nil {
runtime.Status = constant.StatusError
runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime)
return err
return
}
return nil
}
func reCreateRuntime(runtime *model.Runtime) {
@@ -315,10 +328,6 @@ func deleteImageByID(oldImageID, imageName string, client docker.Client) {
}
func buildRuntime(runtime *model.Runtime, oldImageID string, oldEnv string, rebuild bool) {
_ = buildRuntimeWithResult(runtime, oldImageID, oldEnv, rebuild)
}
func buildRuntimeWithResult(runtime *model.Runtime, oldImageID string, oldEnv string, rebuild bool) error {
runtimePath := runtime.GetPath()
composePath := runtime.GetComposePath()
logPath := path.Join(runtimePath, "build.log")
@@ -326,17 +335,11 @@ func buildRuntimeWithResult(runtime *model.Runtime, oldImageID string, oldEnv st
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, constant.FilePerm)
if err != nil {
global.LOG.Errorf("failed to open log file: %v", err)
runtime.Status = constant.StatusError
runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime)
return err
return
}
defer func() {
_ = logFile.Close()
}()
defer func() {
_ = runtimeRepo.Save(runtime)
}()
newPHPVersion := getRuntimeEnv(runtime.Env, "PHP_VERSION")
oldPHPVersion := getRuntimeEnv(oldEnv, "PHP_VERSION")
@@ -368,19 +371,16 @@ func buildRuntimeWithResult(runtime *model.Runtime, oldImageID string, oldEnv st
} else {
runtime.Message = buserr.New("ErrImageBuildErr").Error() + ":" + stderrBuf.String()
}
return err
_ = runtimeRepo.Save(runtime)
return
}
if err = runComposeCmdWithLog(constant.RuntimeDown, runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
runtime.Status = constant.StatusError
runtime.Message = err.Error()
return err
return
}
client, err := docker.NewClient()
if err != nil {
_, _ = logFile.WriteString(fmt.Sprintf("failed to connect to docker client: %v", err))
runtime.Status = constant.StatusError
runtime.Message = err.Error()
return err
return
}
runtime.Message = ""
if rebuild && runtime.ID > 0 {
@@ -406,10 +406,14 @@ func buildRuntimeWithResult(runtime *model.Runtime, oldImageID string, oldEnv st
}
}
defer func() {
_ = runtimeRepo.Save(runtime)
}()
if out, err := compose.Up(composePath); err != nil {
runtime.Status = constant.StatusStartErr
runtime.Message = out
return err
return
}
deleteImageID := ""
extensions := getRuntimeEnv(runtime.Env, "PHP_EXTENSIONS")
@@ -419,14 +423,14 @@ func buildRuntimeWithResult(runtime *model.Runtime, oldImageID string, oldEnv st
if err = cmdMgr.Run("docker", "exec", "-i", runtime.ContainerName, "install-ext", extensions); err != nil {
runtime.Status = constant.StatusError
runtime.Message = buserr.New("ErrImageBuildErr").Error() + ":" + err.Error()
return err
return
}
commitMgr := cmd2.NewCommandMgr(cmd2.WithTimeout(10*time.Minute), cmd2.WithOutputFile(logPath))
err = commitMgr.Run("docker", "commit", runtime.ContainerName, runtime.Image)
if err != nil {
runtime.Status = constant.StatusError
runtime.Message = buserr.New("ErrImageBuildErr").Error() + ":" + err.Error()
return err
return
}
}
if oldImageID != "" {
@@ -439,10 +443,10 @@ func buildRuntimeWithResult(runtime *model.Runtime, oldImageID string, oldEnv st
if out, err := compose.DownAndUp(composePath); err != nil {
runtime.Status = constant.StatusStartErr
runtime.Message = out
return err
return
}
runtime.Status = constant.StatusRunning
return nil
_ = runtimeRepo.Save(runtime)
}
func handleParams(create request.RuntimeCreate, projectDir string) (composeContent []byte, envContent []byte, forms []byte, err error) {
@@ -460,7 +464,7 @@ func handleParams(create request.RuntimeCreate, projectDir string) (composeConte
return
}
for k := range env {
if isComposePortEnvKey(k) || strings.Contains(k, "APP_PORT") {
if strings.HasPrefix(k, "CONTAINER_PORT_") || strings.HasPrefix(k, "HOST_PORT_") || strings.HasPrefix(k, "HOST_IP_") || strings.Contains(k, "APP_PORT") {
delete(env, k)
}
}
@@ -506,7 +510,7 @@ func handleParams(create request.RuntimeCreate, projectDir string) (composeConte
case constant.RuntimeNode:
create.Params["CODE_DIR"] = create.CodeDir
create.Params["NODE_VERSION"] = create.Version
if runtimeNodeShouldInstallDependencies(create) {
if create.NodeConfig.Install {
create.Params["RUN_INSTALL"] = "1"
} else {
create.Params["RUN_INSTALL"] = "0"
@@ -617,16 +621,15 @@ func handleCompose(env gotenv.Env, composeContent []byte, create request.Runtime
if len(create.ExposedPorts) > 0 {
var ports []interface{}
for i, port := range create.ExposedPorts {
containerPortStr, hostPortStr, hostIPStr, protocolStr := composePortEnvKeys(i)
containerPortStr := fmt.Sprintf("CONTAINER_PORT_%d", i)
hostPortStr := fmt.Sprintf("HOST_PORT_%d", i)
existMap[containerPortStr] = struct{}{}
existMap[hostPortStr] = struct{}{}
existMap[hostIPStr] = struct{}{}
existMap[protocolStr] = struct{}{}
ports = append(ports, formatComposePortMapping(hostIPStr, hostPortStr, containerPortStr, port.Protocol))
hostIPStr := fmt.Sprintf("HOST_IP_%d", i)
ports = append(ports, fmt.Sprintf("${%s}:${%s}:${%s}", hostIPStr, hostPortStr, containerPortStr))
create.Params[containerPortStr] = port.ContainerPort
create.Params[hostPortStr] = port.HostPort
create.Params[hostIPStr] = port.HostIP
create.Params[protocolStr] = normalizeComposeProtocol(port.Protocol)
}
if create.Type == constant.RuntimePHP {
ports = append(ports, "127.0.0.1:${PANEL_APP_PORT_HTTP}:9000")
@@ -659,7 +662,7 @@ func handleCompose(env gotenv.Env, composeContent []byte, create request.Runtime
volumes = append(volumes, fmt.Sprintf("%s:%s", k, v))
}
for _, volume := range create.Volumes {
volumes = append(volumes, formatComposeVolume(volume.Source, volume.Target, volume.Mode))
volumes = append(volumes, fmt.Sprintf("%s:%s", volume.Source, volume.Target))
}
var extraHosts []interface{}
@@ -675,7 +678,7 @@ func handleCompose(env gotenv.Env, composeContent []byte, create request.Runtime
break
}
for k := range env {
if isComposePortEnvKey(k) {
if strings.Contains(k, "CONTAINER_PORT_") || strings.Contains(k, "HOST_PORT_") {
if _, ok := existMap[k]; !ok {
delete(env, k)
}
@@ -869,14 +872,9 @@ func getDockerComposeVolumes(yml []byte) ([]request.Volume, error) {
if len(envArray) > 1 {
target = envArray[1]
}
mode := ""
if len(envArray) > 2 {
mode = envArray[2]
}
res = append(res, request.Volume{
Source: source,
Target: target,
Mode: normalizeComposeVolumeMode(mode),
})
}
}
@@ -910,31 +908,7 @@ func getDockerComposeExtraHosts(yml []byte) ([]request.ExtraHost, error) {
return res, nil
}
func composePortCheckKey(port int, protocol string) string {
return fmt.Sprintf("%d/%s", port, normalizeComposeProtocol(protocol))
}
func runtimeOwnedPortKeys(envStr string) map[string]struct{} {
portKeys := make(map[string]struct{})
envs, err := gotenv.Unmarshal(envStr)
if err != nil {
return portKeys
}
exposedPorts, err := loadComposeExposedPortsFromEnv(envs, "", false)
if err != nil {
return portKeys
}
for _, port := range exposedPorts {
portKeys[composePortCheckKey(port.HostPort, port.Protocol)] = struct{}{}
}
return portKeys
}
func checkRuntimePortExist(port int, scanPort bool, runtimeID uint) error {
return checkRuntimePortExistWithProtocol(port, "", scanPort, runtimeID)
}
func checkRuntimePortExistWithProtocol(port int, protocol string, scanPort bool, runtimeID uint) error {
errMap := make(map[string]interface{})
errMap["port"] = port
appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port))
@@ -959,7 +933,7 @@ func checkRuntimePortExistWithProtocol(port int, protocol string, scanPort bool,
errMap["name"] = domain.Domain
return buserr.WithMap("ErrPortExist", errMap, nil)
}
if scanPort && isPortInUse(port, protocol) {
if scanPort && common.ScanPort(port) {
return buserr.WithDetail("ErrPortInUsed", port, nil)
}
return nil
@@ -1032,16 +1006,31 @@ func handleRuntimeDTO(res *response.RuntimeDTO, runtime model.Runtime) error {
return err
}
for k, v := range envs {
if isComposePortEnvKey(k) {
continue
if strings.Contains(k, "CONTAINER_PORT") || strings.Contains(k, "HOST_PORT") {
if strings.Contains(k, "CONTAINER_PORT") {
matches := re.GetRegex(re.TrailingDigitsPattern).FindStringSubmatch(k)
if len(matches) < 2 {
return fmt.Errorf("invalid container port key: %s", k)
}
containerPort, err := strconv.Atoi(v)
if err != nil {
return err
}
hostPort, err := strconv.Atoi(envs[fmt.Sprintf("HOST_PORT_%s", matches[1])])
if err != nil {
return err
}
hostIP := envs[fmt.Sprintf("HOST_IP_%s", matches[1])]
res.ExposedPorts = append(res.ExposedPorts, request.ExposedPort{
ContainerPort: containerPort,
HostPort: hostPort,
HostIP: hostIP,
})
}
} else {
res.Params[k] = v
}
}
res.ExposedPorts, err = loadComposeExposedPortsFromEnv(envs, "", true)
if err != nil {
return err
}
if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok {
res.Source = v
}
+5 -53
View File
@@ -193,20 +193,16 @@ func handleSnapshot(req dto.SnapshotCreate, taskItem *task.Task, jobID, retry, t
req.InterruptStep = ""
}
if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapUpload" {
uploadTimeout := time.Duration(timeout) * time.Second
taskItem.AddSubTaskWithAliasAndOps(
"SnapUpload",
func(t *task.Task) error {
return snapUpload(itemHelper, req.SourceAccountIDs, req.DownloadAccountID, retry, fmt.Sprintf("%s.tar.gz", rootDir), uploadTimeout)
}, nil, int(retry), uploadTimeout,
return snapUpload(itemHelper, req.SourceAccountIDs, req.DownloadAccountID, retry, fmt.Sprintf("%s.tar.gz", rootDir))
}, nil, int(retry), time.Duration(timeout)*time.Second,
)
req.InterruptStep = ""
}
if err := taskItem.Execute(); err != nil {
_ = snapshotRepo.Update(req.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error(), "interrupt_step": taskItem.Task.CurrentStep})
if jobID != 0 {
cleanupFailedCronjobSnapshot(req, rootDir)
}
return err
}
_ = snapshotRepo.Update(req.ID, map[string]interface{}{"status": constant.StatusSuccess, "interrupt_step": ""})
@@ -214,29 +210,6 @@ func handleSnapshot(req dto.SnapshotCreate, taskItem *task.Task, jobID, retry, t
return nil
}
func cleanupFailedCronjobSnapshot(req dto.SnapshotCreate, rootDir string) {
if err := os.RemoveAll(rootDir); err != nil {
global.LOG.Errorf("remove failed cronjob snapshot directory %s failed, err: %v", rootDir, err)
}
fileName := path.Base(rootDir) + ".tar.gz"
filePath := path.Join("system_snapshot", fileName)
if err := os.Remove(path.Join(global.Dir.LocalBackupDir, "tmp/system", fileName)); err != nil && !os.IsNotExist(err) {
global.LOG.Errorf("remove failed cronjob snapshot file %s failed, err: %v", filePath, err)
}
accounts := NewBackupClientMap(strings.Split(req.SourceAccountIDs, ","))
for _, account := range accounts {
if !account.isOk {
global.LOG.Errorf("remove failed cronjob snapshot file %s from %s failed, err: %s", filePath, account.name, account.message)
continue
}
if _, err := account.client.Delete(path.Join(account.backupPath, filePath)); err != nil {
global.LOG.Errorf("remove failed cronjob snapshot file %s from %s failed, err: %v", filePath, account.name, err)
}
}
}
type snapHelper struct {
SnapID uint
SnapName string
@@ -588,36 +561,15 @@ func snapCompress(snap snapHelper, rootDir string, secret string) error {
return nil
}
func snapUpload(snap snapHelper, accounts string, downloadID, retry uint, file string, timeout time.Duration) error {
func snapUpload(snap snapHelper, accounts string, downloadID, retry uint, file string) error {
snap.Task.Log("---------------------- 8 / 8 ----------------------")
snap.Task.LogStart(i18n.GetMsgByKey("SnapUpload"))
src := path.Join(global.Dir.LocalBackupDir, "tmp/system", path.Base(file))
dst := path.Join("system_snapshot", path.Base(file))
ctx := context.Background()
if timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
downloadAccount := fmt.Sprintf("%d", downloadID)
accountMap := NewBackupClientMapWithContext(ctx, []string{downloadAccount})
accountMap := NewBackupClientMap(strings.Split(accounts, ","))
if !accountMap[fmt.Sprintf("%d", downloadID)].isOk {
return buserr.New(i18n.GetMsgWithDetail("LoadBackupFailed", accountMap[fmt.Sprintf("%d", downloadID)].message))
}
remainingAccounts := make([]string, 0)
for _, account := range strings.Split(accounts, ",") {
if account == "" || account == downloadAccount {
continue
}
remainingAccounts = append(remainingAccounts, account)
}
if err := uploadWithMapWithContext(ctx, snap.Task, accountMap, src, dst, downloadAccount, downloadID, retry, false, len(remainingAccounts) == 0); err != nil {
return err
}
if len(remainingAccounts) == 0 {
return nil
}
optionalMap := NewBackupClientMapWithContext(ctx, remainingAccounts)
return uploadWithMapWithContext(ctx, snap.Task, optionalMap, src, dst, strings.Join(remainingAccounts, ","), downloadID, retry, false, true)
return uploadWithMap(snap.Task, accountMap, src, dst, accounts, downloadID, retry)
}
+2 -53
View File
@@ -52,7 +52,6 @@ type ISSHService interface {
LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error)
ExportLog(ctx *gin.Context, req dto.SearchSSHLog) (string, error)
CleanLog() error
SyncRootCert() error
CreateRootCert(req dto.RootCertOperate) error
@@ -659,15 +658,6 @@ type sshFileItem struct {
Year int
}
func isSSHLogFileName(name string) bool {
for _, baseName := range []string{"auth.log", "secure"} {
if name == baseName || strings.HasPrefix(name, baseName+".") || strings.HasPrefix(name, baseName+"-") {
return true
}
}
return false
}
func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error) {
var fileList []sshFileItem
var data []dto.SSHHistory
@@ -677,16 +667,10 @@ func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []d
return 0, data, err
}
for _, item := range fileItems {
if item.IsDir() || !isSSHLogFileName(item.Name()) {
continue
}
info, err := item.Info()
if err != nil {
return 0, data, err
}
if !info.Mode().IsRegular() {
if item.IsDir() || (!strings.HasPrefix(item.Name(), "secure") && !strings.HasPrefix(item.Name(), "auth.log")) {
continue
}
info, _ := item.Info()
itemPath := path.Join(baseDir, info.Name())
if strings.HasSuffix(itemPath, ".gz") {
if _, err := os.Stat(strings.TrimSuffix(itemPath, ".gz")); err == nil {
@@ -741,41 +725,6 @@ func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []d
return int64(total), data, nil
}
func (u *SSHService) CleanLog() error {
return cleanSSHLogFiles("/var/log")
}
func cleanSSHLogFiles(baseDir string) error {
fileItems, err := os.ReadDir(baseDir)
if err != nil {
return err
}
for _, item := range fileItems {
if item.IsDir() || !isSSHLogFileName(item.Name()) {
continue
}
info, err := item.Info()
if err != nil {
return err
}
if !info.Mode().IsRegular() {
continue
}
itemPath := path.Join(baseDir, item.Name())
if item.Name() == "auth.log" || item.Name() == "secure" {
if err := os.Truncate(itemPath, 0); err != nil {
return err
}
continue
}
if err := os.Remove(itemPath); err != nil {
return err
}
}
return nil
}
func (u *SSHService) ExportLog(ctx *gin.Context, req dto.SearchSSHLog) (string, error) {
_, logs, err := u.LoadLog(ctx, req)
if err != nil {
+38 -8
View File
@@ -23,6 +23,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/env"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/re"
)
type TensorRTLLMService struct{}
@@ -58,7 +59,33 @@ func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch) response.TensorR
serverDTO.Image = envs["IMAGE"]
serverDTO.Command = getCommand(item.Env)
serverDTO.ExposedPorts, _ = loadComposeExposedPortsFromEnv(envs, "0.0.0.0", false)
for k, v := range envs {
if strings.Contains(k, "CONTAINER_PORT") || strings.Contains(k, "HOST_PORT") {
if strings.Contains(k, "CONTAINER_PORT") {
matches := re.GetRegex(re.TrailingDigitsPattern).FindStringSubmatch(k)
if len(matches) < 2 {
continue
}
containerPort, err := strconv.Atoi(v)
if err != nil {
continue
}
hostPort, err := strconv.Atoi(envs[fmt.Sprintf("HOST_PORT_%s", matches[1])])
if err != nil {
continue
}
hostIP := envs[fmt.Sprintf("HOST_IP_%s", matches[1])]
if hostIP == "" {
hostIP = "0.0.0.0"
}
serverDTO.ExposedPorts = append(serverDTO.ExposedPorts, request.ExposedPort{
ContainerPort: containerPort,
HostPort: hostPort,
HostIP: hostIP,
})
}
}
}
composeByte, err := files.NewFileOp().GetContent(path.Join(global.Dir.TensorRTLLMDir, item.Name, "docker-compose.yml"))
if err == nil {
@@ -122,9 +149,11 @@ func handleLLMParams(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) e
delete(serviceValue, "ports")
if len(create.ExposedPorts) > 0 {
var ports []interface{}
for i, port := range create.ExposedPorts {
containerPortStr, hostPortStr, hostIPStr, _ := composePortEnvKeys(i)
ports = append(ports, formatComposePortMapping(hostIPStr, hostPortStr, containerPortStr, port.Protocol))
for i := range create.ExposedPorts {
containerPortStr := fmt.Sprintf("CONTAINER_PORT_%d", i)
hostPortStr := fmt.Sprintf("HOST_PORT_%d", i)
hostIPStr := fmt.Sprintf("HOST_IP_%d", i)
ports = append(ports, fmt.Sprintf("${%s}:${%s}:${%s}", hostIPStr, hostPortStr, containerPortStr))
}
serviceValue["ports"] = ports
}
@@ -145,7 +174,7 @@ func handleLLMParams(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) e
volumes = append(volumes, fmt.Sprintf("%s:%s", k, v))
}
for _, volume := range create.Volumes {
volumes = append(volumes, formatComposeVolume(volume.Source, volume.Target, volume.Mode))
volumes = append(volumes, fmt.Sprintf("%s:%s", volume.Source, volume.Target))
}
serviceValue["volumes"] = volumes
@@ -166,11 +195,12 @@ func handleLLMEnv(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) gote
envMap["IMAGE"] = create.Image
envMap["COMMAND"] = create.Command
for i, port := range create.ExposedPorts {
containerPortStr, hostPortStr, hostIPStr, protocolStr := composePortEnvKeys(i)
containerPortStr := fmt.Sprintf("CONTAINER_PORT_%d", i)
hostPortStr := fmt.Sprintf("HOST_PORT_%d", i)
hostIPStr := fmt.Sprintf("HOST_IP_%d", i)
envMap[containerPortStr] = strconv.Itoa(port.ContainerPort)
envMap[hostPortStr] = strconv.Itoa(port.HostPort)
envMap[hostIPStr] = port.HostIP
envMap[protocolStr] = normalizeComposeProtocol(port.Protocol)
}
orders := []string{"MODEL_PATH", "COMMAND"}
envStr, _ := env.MarshalWithOrder(envMap, orders)
@@ -189,7 +219,7 @@ func (t TensorRTLLMService) Create(create request.TensorRTLLMCreate) error {
}
}
for _, export := range create.ExposedPorts {
if err := checkPortExistWithProtocol(export.HostPort, export.Protocol); err != nil {
if err := checkPortExist(export.HostPort); err != nil {
return err
}
}
+3 -16
View File
@@ -12,7 +12,6 @@ const (
vllmImageEnvKey = "IMAGE"
vllmImageTypeNvidia = "nvidia"
vllmImageTypeIntel = "intel"
vllmImageTypeAscend = "ascend"
)
func resolveVllmVersionFamily(version, image string) string {
@@ -20,9 +19,6 @@ func resolveVllmVersionFamily(version, image string) string {
if strings.HasPrefix(normalizedVersion, vllmImageTypeIntel+"-") {
return vllmImageTypeIntel
}
if strings.HasPrefix(normalizedVersion, vllmImageTypeAscend+"-") {
return vllmImageTypeAscend
}
if strings.HasPrefix(normalizedVersion, vllmImageTypeNvidia+"-") {
return vllmImageTypeNvidia
}
@@ -30,16 +26,13 @@ func resolveVllmVersionFamily(version, image string) string {
if strings.Contains(normalizedImage, "intel/") || strings.Contains(normalizedImage, "llm-scaler-vllm") {
return vllmImageTypeIntel
}
if strings.Contains(normalizedImage, "ascend/") || strings.Contains(normalizedImage, "vllm-ascend") {
return vllmImageTypeAscend
}
return vllmImageTypeNvidia
}
func trimVllmVersionFamily(version string) string {
trimmed := strings.TrimSpace(version)
normalized := strings.ToLower(trimmed)
for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel, vllmImageTypeAscend} {
for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel} {
prefix := family + "-"
if strings.HasPrefix(normalized, prefix) {
return strings.TrimSpace(trimmed[len(prefix):])
@@ -50,16 +43,12 @@ func trimVllmVersionFamily(version string) string {
func buildDefaultVllmImageByVersion(version string) string {
tag := trimVllmVersionFamily(version)
family := resolveVllmVersionFamily(version, "")
if family == vllmImageTypeIntel {
if resolveVllmVersionFamily(version, "") == vllmImageTypeIntel {
return "intel/llm-scaler-vllm:" + tag
}
if tag != "" && !strings.HasPrefix(strings.ToLower(tag), "v") {
tag = "v" + tag
}
if family == vllmImageTypeAscend {
return "quay.io/ascend/vllm-ascend:" + tag
}
return "vllm/vllm-openai:" + tag
}
@@ -71,9 +60,7 @@ func isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage str
func hasVllmVersionFamilyPrefix(version string) bool {
normalized := strings.ToLower(strings.TrimSpace(version))
return strings.HasPrefix(normalized, vllmImageTypeNvidia+"-") ||
strings.HasPrefix(normalized, vllmImageTypeIntel+"-") ||
strings.HasPrefix(normalized, vllmImageTypeAscend+"-")
return strings.HasPrefix(normalized, vllmImageTypeNvidia+"-") || strings.HasPrefix(normalized, vllmImageTypeIntel+"-")
}
func isVllmUpgradeCandidate(currentVersion, targetVersion, currentImage string) bool {
+6 -11
View File
@@ -168,13 +168,15 @@ func (w WebsiteService) PageWebsite(req request.WebsiteSearch) (int64, []respons
opts = append(opts, repo.WithOrderRuleBy(req.OrderBy, req.Order), repo.WithOrderRuleBy("updated_at", "descending"))
if req.Name != "" {
domains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithDomainLike(req.Name))
var websiteIds []uint
if len(domains) > 0 {
var websiteIds []uint
for _, domain := range domains {
websiteIds = append(websiteIds, domain.WebsiteID)
}
opts = append(opts, repo.WithByIDs(websiteIds))
} else {
opts = append(opts, websiteRepo.WithDomainLike(req.Name))
}
opts = append(opts, websiteRepo.WithSearchKeyword(req.Name, websiteIds))
}
if req.WebsiteGroupID != 0 {
opts = append(opts, websiteRepo.WithGroupID(req.WebsiteGroupID))
@@ -715,18 +717,11 @@ func (w WebsiteService) DeleteWebsite(req request.WebsiteDelete) error {
if mysqlDB.ID > 0 {
deleteReq := dto.MysqlDBDelete{
ID: mysqlDB.ID,
Type: website.DbType,
Database: mysqlDB.MysqlName,
ForceDelete: req.ForceDelete,
}
if err = deleteMysqlDatabaseForResourceOwner(
context.TODO(),
deleteReq,
dto.DBResource{Type: constant.TypeWebsite, Name: website.PrimaryDomain},
); err != nil {
if isMysqlDatabaseResourceInUseError(err) || !req.ForceDelete {
return err
}
if err = NewIMysqlService().Delete(context.TODO(), deleteReq); err != nil && !req.ForceDelete {
return err
}
}
case constant.AppPostgresql, constant.AppPostgres:
-6
View File
@@ -207,7 +207,6 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
Description: req.Description,
ExecShell: req.ExecShell,
}
setSSLPushConfig(websiteSSL, req.PushNode, req.Nodes)
if req.ExecShell {
websiteSSL.Shell = req.Shell
}
@@ -385,11 +384,6 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
}
}
reloadSystemSSL(websiteSSL, logger)
if websiteSSL.PushNode {
if err = pushSSLToNode(websiteSSL, logger); err != nil {
return nil, err
}
}
return websiteSSL, nil
}
+18 -120
View File
@@ -18,12 +18,12 @@ import (
"github.com/go-acme/lego/v5/certificate"
legoLogger "github.com/go-acme/lego/v5/log"
"github.com/jinzhu/gorm"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
@@ -104,7 +104,6 @@ type IWebsiteSSLService interface {
Delete(ids []uint) error
Update(update request.WebsiteSSLUpdate) error
Upload(req request.WebsiteSSLUpload) error
PushToNode(req request.WebsiteSSLPush) error
ObtainSSL(apply request.WebsiteSSLApply) error
AutoRenewSSL(id uint) error
SyncForRestart() error
@@ -215,7 +214,10 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs
}
websiteSSL.Dir = create.Dir
}
setSSLPushConfig(&websiteSSL, create.PushNode, create.Nodes)
if create.PushNode && global.IsMaster && len(create.Nodes) > 0 {
websiteSSL.PushNode = true
websiteSSL.Nodes = create.Nodes
}
var domains []string
if create.OtherDomains != "" {
@@ -280,47 +282,6 @@ func printSSLLog(logger *log.Logger, msgKey string, params map[string]interface{
logger.Println(i18n.GetMsgWithMap(msgKey, params))
}
func normalizeSSLPushConfig(pushNode bool, nodes string) (bool, string) {
nodes = strings.TrimSpace(nodes)
if !pushNode || nodes == "" {
return false, ""
}
return true, nodes
}
func setSSLPushConfig(websiteSSL *model.WebsiteSSL, pushNode bool, nodes string) {
pushNode, nodes = normalizeSSLPushConfig(pushNode, nodes)
if !global.IsMaster || !xpack.MultiNodeProvider.IsXpack() {
pushNode = false
nodes = ""
}
websiteSSL.PushNode = pushNode
websiteSSL.Nodes = nodes
}
func pushSSLToNode(websiteSSL *model.WebsiteSSL, logger *log.Logger) error {
printSSLLog(logger, "StartPushSSLToNode", nil)
if err := xpack.MultiNodeProvider.PushSSLToNode(websiteSSL); err != nil {
printSSLLog(logger, "PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()})
return err
}
printSSLLog(logger, "PushSSLToNodeSuccess", nil)
return nil
}
func pushSSLToNodeWithNewLogger(websiteSSL *model.WebsiteSSL) error {
if !websiteSSL.PushNode {
return nil
}
logFile, logger := newWebsiteSSLLogger(websiteSSL, false)
if logFile != nil {
defer func() {
_ = logFile.Close()
}()
}
return pushSSLToNode(websiteSSL, logger)
}
func newWebsiteSSLLogger(websiteSSL *model.WebsiteSSL, autoRenew bool) (*os.File, *log.Logger) {
flags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC
if autoRenew {
@@ -461,7 +422,7 @@ func (w WebsiteSSLService) obtainSSL(id uint, autoRenew bool) error {
case constant.Http:
appInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
if errors.Is(err, gormv2.ErrRecordNotFound) {
if gorm.IsRecordNotFoundError(err) {
return buserr.New("ErrOpenrestyNotFound")
}
return err
@@ -606,9 +567,12 @@ func (w WebsiteSSLService) obtainSSL(id uint, autoRenew bool) error {
}
reloadSystemSSL(websiteSSL, logger)
if websiteSSL.PushNode {
if err = pushSSLToNode(websiteSSL, logger); err != nil {
printSSLLog(logger, "StartPushSSLToNode", nil)
if err = xpack.MultiNodeProvider.PushSSLToNode(websiteSSL); err != nil {
printSSLLog(logger, "PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()})
return
}
printSSLLog(logger, "PushSSLToNodeSuccess", nil)
}
}(logFile, logger)
@@ -776,13 +740,13 @@ func (w WebsiteSSLService) Update(update request.WebsiteSSLUpdate) error {
} else {
updateParams["shell"] = ""
}
pushNode, nodes := normalizeSSLPushConfig(update.PushNode, update.Nodes)
if !global.IsMaster || !xpack.MultiNodeProvider.IsXpack() {
pushNode = false
nodes = ""
if update.PushNode {
updateParams["push_node"] = true
updateParams["nodes"] = update.Nodes
} else {
updateParams["push_node"] = false
updateParams["nodes"] = ""
}
updateParams["push_node"] = pushNode
updateParams["nodes"] = nodes
if websiteSSL.Provider != constant.SelfSigned && websiteSSL.Provider != constant.Manual {
acmeAccount, err := websiteAcmeRepo.GetFirst(repo.WithByID(update.AcmeAccountID))
@@ -833,7 +797,6 @@ func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error {
Description: req.Description,
Status: constant.SSLReady,
}
setSSLPushConfig(websiteSSL, req.PushNode, req.Nodes)
var err error
if req.SSLID > 0 {
websiteSSL, err = websiteSSLRepo.GetFirst(repo.WithByID(req.SSLID))
@@ -841,7 +804,6 @@ func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error {
return err
}
websiteSSL.Description = req.Description
setSSLPushConfig(websiteSSL, req.PushNode, req.Nodes)
}
if req.Type == "local" {
fileOp := files.NewFileOp()
@@ -929,73 +891,9 @@ func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error {
if err := UpdateSSLConfig(*websiteSSL); err != nil {
return err
}
if err := websiteSSLRepo.Save(websiteSSL); err != nil {
return err
}
return pushSSLToNodeWithNewLogger(websiteSSL)
return websiteSSLRepo.Save(websiteSSL)
}
if err := websiteSSLRepo.Create(context.Background(), websiteSSL); err != nil {
return err
}
return pushSSLToNodeWithNewLogger(websiteSSL)
}
func (w WebsiteSSLService) PushToNode(req request.WebsiteSSLPush) error {
if !global.IsMaster {
return errors.New("only master node can push SSL to nodes")
}
if !xpack.MultiNodeProvider.IsXpack() {
return errors.New("SSL node push is an XPack feature")
}
pushNode, nodes := normalizeSSLPushConfig(req.PushNode, req.Nodes)
if !pushNode {
return errors.New("please select nodes to push SSL")
}
websiteSSL, err := websiteSSLRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return err
}
if websiteSSL.Provider == constant.FromMaster {
return errors.New("SSL imported from master node can not be pushed")
}
if websiteSSL.Status != constant.SSLReady {
return errors.New("only ready SSL can be pushed")
}
if task.CheckResourceTaskIsExecuting(task.TaskPush, task.TaskScopeWebsite, websiteSSL.ID) {
return buserr.New("TaskIsExecuting")
}
if err := websiteSSLRepo.SaveByMap(websiteSSL, map[string]interface{}{
"push_node": pushNode,
"nodes": nodes,
}); err != nil {
return err
}
websiteSSL.PushNode = pushNode
websiteSSL.Nodes = nodes
if req.Sync {
return xpack.MultiNodeProvider.PushSSLToNode(websiteSSL)
}
pushTask, err := task.NewTask(task.GetTaskName(websiteSSL.PrimaryDomain, task.TaskPush, "SSL"), task.TaskPush, task.TaskScopeWebsite, req.TaskID, websiteSSL.ID)
if err != nil {
return err
}
pushTask.AddSubTask(i18n.GetMsgByKey("StartPushSSLToNode"), func(t *task.Task) error {
t.Log(i18n.GetMsgByKey("StartPushSSLToNode"))
if err := xpack.MultiNodeProvider.PushSSLToNode(websiteSSL); err != nil {
t.Log(i18n.GetMsgWithMap("PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()}))
return err
}
t.Log(i18n.GetMsgByKey("PushSSLToNodeSuccess"))
return nil
}, nil)
go func() {
if err := pushTask.Execute(); err != nil {
global.LOG.Errorf("push ssl to node failed, sslID: %d, err: %v", websiteSSL.ID, err)
}
}()
return nil
return websiteSSLRepo.Create(context.Background(), websiteSSL)
}
func (w WebsiteSSLService) DownloadFile(id uint) (*os.File, error) {
+5 -9
View File
@@ -169,8 +169,8 @@ func NewTask(name, operate, taskScope, taskID string, resourceID uint) (*Task, e
}
taskRepo := repo.NewITaskRepo()
ctx, cancel := context.WithCancel(context.Background())
global.RegisterTaskCancel(taskID, cancel)
task := &Task{TaskCtx: ctx, TaskID: taskID, Name: name, logFile: logFile, Logger: logger, taskRepo: taskRepo, Task: taskModel}
global.TaskCtxMap[taskID] = cancel
task := &Task{TaskCtx: ctx, Name: name, logFile: logFile, Logger: logger, taskRepo: taskRepo, Task: taskModel}
return task, nil
}
@@ -200,7 +200,7 @@ func ReNewTask(name, operate, taskScope, taskID string, resourceID uint) (*Task,
logger.SetOutput(logFile)
logger.Print("\n --------------------------------------------------- \n")
taskItem.Status = constant.StatusExecuting
task := &Task{TaskID: taskID, Name: name, logFile: logFile, Logger: logger, taskRepo: taskRepo, Task: &taskItem}
task := &Task{Name: name, logFile: logFile, Logger: logger, taskRepo: taskRepo, Task: &taskItem}
task.updateTask(&taskItem)
return task, nil
}
@@ -231,7 +231,7 @@ func (t *Task) AddSubTaskWithIgnoreErr(name string, action ActionFunc) {
}
func (s *SubTask) Execute() error {
defer global.RemoveTaskCancel(s.RootTask.TaskID)
defer delete(global.TaskCtxMap, s.RootTask.TaskID)
subTaskName := s.Name
if s.Name == "" {
subTaskName = i18n.GetMsgByKey("SubTask")
@@ -302,11 +302,7 @@ func (t *Task) Execute() error {
continue
}
t.Task.ErrorMsg = err.Error()
if errors.Is(err, context.Canceled) {
t.Task.Status = constant.StatusCanceled
} else {
t.Task.Status = constant.StatusFailed
}
t.Task.Status = constant.StatusFailed
for _, rollback := range t.Rollbacks {
rollback(t)
}
+90 -533
View File
@@ -1247,33 +1247,6 @@
"formatZH": "更新 AI 网关分组 [name]",
"formatEN": "update AI proxy group [name]"
},
"/core/enterprise/ai-proxy/model-groups/create": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建 AI 网关模型组 [name]",
"formatEN": "create AI proxy model group [name]"
},
"/core/enterprise/ai-proxy/model-groups/delete": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除 AI 网关模型组 [id]",
"formatEN": "delete AI proxy model group [id]"
},
"/core/enterprise/ai-proxy/model-groups/update": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新 AI 网关模型组 [name]",
"formatEN": "update AI proxy model group [name]"
},
"/core/enterprise/ai-proxy/service/operate": {
"bodyKeys": [
"operate"
@@ -1283,13 +1256,6 @@
"formatZH": "操作 AI 网关服务 [operate]",
"formatEN": "operate AI proxy service [operate]"
},
"/core/enterprise/ai-proxy/settings/logs/clear": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清空 AI 网关日志",
"formatEN": "clear AI proxy logs"
},
"/core/enterprise/ai-proxy/settings/update": {
"bodyKeys": [],
"paramKeys": [],
@@ -1297,50 +1263,6 @@
"formatZH": "更新 AI 网关设置",
"formatEN": "update AI proxy settings"
},
"/core/enterprise/ai-proxy/smart-route/policy/update": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新 AI 网关智能路由策略",
"formatEN": "update AI proxy smart route policy"
},
"/core/enterprise/ai-proxy/smart-route/samples/create": {
"bodyKeys": [
"type",
"label"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建 AI 网关智能路由样本 [type] [label]",
"formatEN": "create AI proxy smart route sample [type] [label]"
},
"/core/enterprise/ai-proxy/smart-route/samples/delete": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除 AI 网关智能路由样本 [id]",
"formatEN": "delete AI proxy smart route sample [id]"
},
"/core/enterprise/ai-proxy/smart-route/samples/rebuild": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "重建 AI 网关智能路由样本向量",
"formatEN": "rebuild AI proxy smart route sample vectors"
},
"/core/enterprise/ai-proxy/smart-route/samples/update": {
"bodyKeys": [
"id",
"type",
"label"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新 AI 网关智能路由样本 [id] [type] [label]",
"formatEN": "update AI proxy smart route sample [id] [type] [label]"
},
"/core/enterprise/licenses/upload": {
"bodyKeys": [],
"paramKeys": [],
@@ -1446,14 +1368,13 @@
"/core/enterprise/skills-hub/import": {
"bodyKeys": [
"type",
"version",
"repoURL",
"packageURL"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "导入 Skills Hub 技能版本 [version]",
"formatEN": "import Skills Hub skill version [version]"
"formatZH": "导入 Skills Hub 技能 [type]",
"formatEN": "import Skills Hub skill [type]"
},
"/core/enterprise/skills-hub/status": {
"bodyKeys": [
@@ -1557,13 +1478,11 @@
"formatEN": "update Skills Hub install target [name]"
},
"/core/enterprise/skills-hub/upload": {
"bodyKeys": [
"version"
],
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "上传 Skills Hub 技能包版本 [version]",
"formatEN": "upload Skills Hub package version [version]"
"formatZH": "上传 Skills Hub 技能包",
"formatEN": "upload Skills Hub package"
},
"/core/enterprise/users": {
"bodyKeys": [
@@ -1574,25 +1493,6 @@
"formatZH": "添加用户 [name]",
"formatEN": "add user [name]"
},
"/core/enterprise/users/api/update": {
"bodyKeys": [
"id",
"ipWhiteList"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "users",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList]",
"formatEN": "update user [name] api config =\u003e IP White List: [ipWhiteList]"
},
"/core/enterprise/users/del": {
"bodyKeys": [
"ids"
@@ -1629,262 +1529,6 @@
"formatZH": "更新 [name]",
"formatEN": "update user [name]"
},
"/core/enterprise/vms": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name]",
"formatEN": "create VM [name]"
},
"/core/enterprise/vms/del": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
"/core/enterprise/vms/environment/enable": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "启用虚拟机运行环境",
"formatEN": "enable VM runtime environment"
},
"/core/enterprise/vms/iso": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机镜像 [name] [type]",
"formatEN": "create VM ISO [name] [type]"
},
"/core/enterprise/vms/iso/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_isos",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机镜像 [name]",
"formatEN": "delete VM ISO [name]"
},
"/core/enterprise/vms/iso/update": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机镜像 [name] [type]",
"formatEN": "update VM ISO [name] [type]"
},
"/core/enterprise/vms/networks": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机网络 [name] [type]",
"formatEN": "create VM network [name] [type]"
},
"/core/enterprise/vms/networks/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机网络 [name]",
"formatEN": "delete VM network [name]"
},
"/core/enterprise/vms/networks/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机网络 [name]",
"formatEN": "update VM network [name]"
},
"/core/enterprise/vms/operate": {
"bodyKeys": [
"name",
"operate"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "操作虚拟机 [name] [operate]",
"formatEN": "operate VM [name] [operate]"
},
"/core/enterprise/vms/orphans/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理虚拟机孤立数据",
"formatEN": "clean VM orphaned data"
},
"/core/enterprise/vms/rename": {
"bodyKeys": [
"name",
"newName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "重命名虚拟机 [name] 为 [newName]",
"formatEN": "rename VM [name] to [newName]"
},
"/core/enterprise/vms/snapshots": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
"formatEN": "create VM [name] snapshot [snapshotName]"
},
"/core/enterprise/vms/snapshots/del": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
"formatEN": "delete VM [name] snapshot [snapshotName]"
},
"/core/enterprise/vms/snapshots/recover": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
"formatEN": "recover VM [name] snapshot [snapshotName]"
},
"/core/enterprise/vms/storages": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机存储 [name] [type]",
"formatEN": "create VM storage [name] [type]"
},
"/core/enterprise/vms/storages/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机存储 [name]",
"formatEN": "delete VM storage [name]"
},
"/core/enterprise/vms/storages/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机存储 [name]",
"formatEN": "update VM storage [name]"
},
"/core/enterprise/vms/sync": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步虚拟机",
"formatEN": "sync virtual machines"
},
"/core/enterprise/vms/templates": {
"bodyKeys": [
"name",
"templateName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
"formatEN": "create VM [name] template [templateName]"
},
"/core/enterprise/vms/templates/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_templates",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机模板 [name]",
"formatEN": "delete VM template [name]"
},
"/core/enterprise/vms/update": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机 [name]",
"formatEN": "update VM [name]"
},
"/core/groups": {
"bodyKeys": [
"name",
@@ -2209,22 +1853,15 @@
},
"/core/xpack/exchange/file": {
"bodyKeys": [
"sourceNode",
"targetNode",
"source",
"dest",
"sourcePath",
"targetPath"
"destPath"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "交换文件 [sourcePath] 到 [targetPath]",
"formatEN": "exchange file [sourcePath] to [targetPath]"
},
"/core/xpack/exchange/image": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步节点镜像",
"formatEN": "sync images"
"formatZH": "交换文件 [sourcePath] 到 [destPath]",
"formatEN": "exchange file [sourcePath] to [destPath]"
},
"/core/xpack/nodes": {
"bodyKeys": [
@@ -2311,16 +1948,6 @@
"formatZH": "修复节点 [name]",
"formatEN": "fix node [name]"
},
"/core/xpack/nodes/health/setting": {
"bodyKeys": [
"interval",
"loadFailedResources"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新节点健康检查设置",
"formatEN": "update node health check setting"
},
"/core/xpack/nodes/reinstall": {
"bodyKeys": [
"id"
@@ -2539,15 +2166,6 @@
"formatZH": "更新自定义应用配置 [status]",
"formatEN": "update custom app config [status]"
},
"/core/xpack/settings/app/image-prefix": {
"bodyKeys": [
"imagePrefix"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新应用镜像前缀 [imagePrefix]",
"formatEN": "update app image prefix [imagePrefix]"
},
"/core/xpack/settings/reset": {
"bodyKeys": [],
"paramKeys": [],
@@ -2573,11 +2191,27 @@
"formatEN": "update xpack setting [key]"
},
"/core/xpack/sync/app": {
"bodyKeys": [],
"bodyKeys": [
"sourceNode",
"targetNode",
"appInstallID",
"appBackupID"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步节点应用",
"formatEN": "sync app"
"formatZH": "同步应用备份 [appInstallID] 到节点 [targetNode]",
"formatEN": "sync app backup [appInstallID] to node [targetNode]"
},
"/core/xpack/sync/app/install": {
"bodyKeys": [
"appKey",
"version",
"nodes"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "安装应用 [appKey] [version] 到节点 [nodes]",
"formatEN": "install app [appKey] [version] to nodes [nodes]"
},
"/core/xpack/sync/ssl": {
"bodyKeys": [
@@ -2718,14 +2352,34 @@
"formatZH": "更新自定义应用配置 [status]",
"formatEN": "update custom app config [status]"
},
"/custom/app/image-prefix": {
"/custom/app/install": {
"bodyKeys": [
"imagePrefix"
"name",
"appKey"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新应用镜像前缀 [imagePrefix]",
"formatEN": "update app image prefix [imagePrefix]"
"formatZH": "从节点安装应用 [name]",
"formatEN": "install app [name] from node"
},
"/custom/app/install/check": {
"bodyKeys": [
"name",
"appKey"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "检查应用安装 [name]",
"formatEN": "check app install [name]"
},
"/custom/app/push": {
"bodyKeys": [
"appBackupID"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "推送应用备份 [appBackupID] 到节点",
"formatEN": "push app backup [appBackupID] to node"
},
"/custom/app/sync": {
"bodyKeys": [],
@@ -2760,23 +2414,51 @@
"formatZH": "创建 mysql 数据库 [name]",
"formatEN": "create mysql database [name]"
},
"/databases/change/access": {
"/databases/bind": {
"bodyKeys": [
"database"
"database",
"username"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新数据库 [database] root 访问权限",
"formatEN": "Update database [database] root access"
"formatZH": "绑定 mysql 数据库名 [database] [username]",
"formatEN": "bind mysql database [database] [username]"
},
"/databases/change/access": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "database_mysqls",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新数据库 [name] 访问权限",
"formatEN": "Update database [name] access"
},
"/databases/change/password": {
"bodyKeys": [
"database"
"id"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新数据库 [database] root 密码",
"formatEN": "Update database [database] root password"
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "database_mysqls",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新数据库 [name] 密码",
"formatEN": "Update database [name] password"
},
"/databases/common/update/conf": {
"bodyKeys": [
@@ -2872,30 +2554,6 @@
"formatZH": "mysql 数据库 [name] 描述信息修改 [description]",
"formatEN": "The description of the mysql database [name] is modified =\u003e [description]"
},
"/databases/grants": {
"bodyKeys": [
"database",
"db",
"username",
"host"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "授权 mysql 数据库 [database] 用户 [username]@[host] 访问 [db]",
"formatEN": "grant mysql database [database] user [username]@[host] access to [db]"
},
"/databases/grants/del": {
"bodyKeys": [
"database",
"db",
"username",
"host"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "取消 mysql 数据库 [database] 用户 [username]@[host] 对 [db] 的授权",
"formatEN": "revoke mysql database [database] user [username]@[host] access to [db]"
},
"/databases/mongodb": {
"bodyKeys": [
"name"
@@ -3087,63 +2745,6 @@
"formatZH": "redis 数据库持久化配置更新",
"formatEN": "redis database persistence configuration update"
},
"/databases/users": {
"bodyKeys": [
"database",
"username",
"host"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建 mysql 数据库 [database] 用户 [username]@[host]",
"formatEN": "create mysql database [database] user [username]@[host]"
},
"/databases/users/del": {
"bodyKeys": [
"database",
"username",
"host"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除 mysql 数据库 [database] 用户 [username]@[host]",
"formatEN": "delete mysql database [database] user [username]@[host]"
},
"/databases/users/password": {
"bodyKeys": [
"database",
"username",
"host"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新 mysql 数据库 [database] 用户 [username]@[host] 密码",
"formatEN": "update mysql database [database] user [username]@[host] password"
},
"/databases/users/password/save": {
"bodyKeys": [
"database",
"username",
"host"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "补充 mysql 数据库 [database] 用户 [username]@[host] 密码",
"formatEN": "save mysql database [database] user [username]@[host] password locally"
},
"/databases/users/update": {
"bodyKeys": [
"database",
"username",
"host",
"newHost",
"description"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] =\u003e [newHost] 描述 [description]",
"formatEN": "update mysql database [database] user [username] access [host] =\u003e [newHost] description [description]"
},
"/databases/variables/update": {
"bodyKeys": [],
"paramKeys": [],
@@ -3609,13 +3210,6 @@
"formatZH": "修改 SSH 配置文件 [key]",
"formatEN": "update SSH conf [key]"
},
"/hosts/ssh/log/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清空 SSH 登录日志",
"formatEN": "clean SSH login logs"
},
"/hosts/ssh/operate": {
"bodyKeys": [
"operation"
@@ -4846,24 +4440,6 @@
"formatZH": "申请证书 [domain]",
"formatEN": "apply ssl [domain]"
},
"/websites/ssl/push": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "website_ssls",
"output_column": "primary_domain",
"output_value": "domain"
}
],
"formatZH": "推送证书到节点 [domain]",
"formatEN": "Push ssl to nodes [domain]"
},
"/websites/ssl/update": {
"bodyKeys": [
"id"
@@ -5199,15 +4775,6 @@
"formatZH": "更新 CDN 配置",
"formatEN": "update CDN config"
},
"/xpack/waf/config/global/spider": {
"bodyKeys": [
"rules"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新全局蜘蛛允许列表",
"formatEN": "update global spider allowlist"
},
"/xpack/waf/config/global/state": {
"bodyKeys": [
"scope",
@@ -5221,8 +4788,7 @@
"/xpack/waf/config/website/state": {
"bodyKeys": [
"scope",
"state",
"mode"
"state"
],
"paramKeys": [],
"beforeFunctions": [],
@@ -5369,15 +4935,6 @@
"formatZH": "更新[scope]频率访问限制",
"formatEN": "update [scope] CC config"
},
"/xpack/waf/rule/common/apply": {
"bodyKeys": [
"scope"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "应用规则 [scope] 到网站",
"formatEN": "apply rule [scope] to websites"
},
"/xpack/waf/rule/common/create": {
"bodyKeys": [
"scope"
@@ -434,12 +434,5 @@
"file": "lz4.so",
"versions": ["56","70", "71", "72", "73", "74", "80", "81", "82", "83","84","85"],
"installed": false
},
{
"name": "msgpack",
"check": "msgpack",
"file": "msgpack.so",
"versions": ["56","70", "71", "72", "73", "74", "80", "81", "82", "83","84","85"],
"installed": false
}
]
-21
View File
@@ -2,7 +2,6 @@ package global
import (
"context"
"sync"
badger_db "github.com/1Panel-dev/1Panel/agent/init/cache/db"
"github.com/go-playground/validator/v10"
@@ -40,28 +39,8 @@ var (
AlertResourceJobID cron.EntryID
TaskCtxMap = make(map[string]context.CancelFunc)
taskCtxMu sync.RWMutex
)
func RegisterTaskCancel(taskID string, cancel context.CancelFunc) {
taskCtxMu.Lock()
defer taskCtxMu.Unlock()
TaskCtxMap[taskID] = cancel
}
func LoadTaskCancel(taskID string) (context.CancelFunc, bool) {
taskCtxMu.RLock()
defer taskCtxMu.RUnlock()
cancel, ok := TaskCtxMap[taskID]
return cancel, ok
}
func RemoveTaskCancel(taskID string) {
taskCtxMu.Lock()
defer taskCtxMu.Unlock()
delete(TaskCtxMap, taskID)
}
func RepoURL() string {
if CONF.Base.IsEnterprise {
return "https://resource.fit2cloud.com/1panel/package/enterprise"
+51 -52
View File
@@ -4,64 +4,63 @@ go 1.25.10
replace github.com/moby/go-archive => github.com/moby/go-archive v0.1.0
replace github.com/go-acme/lego/v5 => github.com/1Panel-dev/lego/v5 v5.3.1
require (
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/compose-spec/compose-go/v2 v2.13.0
github.com/compose-spec/compose-go/v2 v2.11.0
github.com/creack/pty v1.1.24
github.com/docker/cli v29.6.2+incompatible
github.com/docker/cli v29.5.3+incompatible
github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-connections v0.7.0
github.com/fsnotify/fsnotify v1.10.1
github.com/gin-gonic/gin v1.12.0
github.com/glebarez/sqlite v1.11.0
github.com/go-acme/lego/v5 v5.3.1
github.com/go-acme/lego/v5 v5.2.2
github.com/go-gormigrate/gormigrate/v2 v2.1.6
github.com/go-playground/validator/v10 v10.30.3
github.com/go-redis/redis v6.15.9+incompatible
github.com/go-resty/resty/v2 v2.17.2
github.com/go-sql-driver/mysql v1.10.0
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6
github.com/goh-chunlin/go-onedrive v1.1.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/jackc/pgx/v5 v5.10.0
github.com/jinzhu/copier v0.4.0
github.com/jinzhu/gorm v1.9.16
github.com/joho/godotenv v1.5.1
github.com/klauspost/compress v1.19.1
github.com/mattn/go-shellwords v1.0.14
github.com/klauspost/compress v1.18.6
github.com/mholt/archiver/v4 v4.0.0-alpha.8
github.com/miekg/dns v1.1.72
github.com/minio/minio-go/v7 v7.2.1
github.com/minio/minio-go/v7 v7.2.0
github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/opencontainers/image-spec v1.1.1
github.com/oschwald/maxminddb-golang v1.13.1
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.11
github.com/qiniu/go-sdk/v7 v7.26.17
github.com/pkg/sftp v1.13.10
github.com/qiniu/go-sdk/v7 v7.26.12
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.26.6
github.com/shirou/gopsutil/v4 v4.26.5
github.com/sirupsen/logrus v1.9.4
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/subosito/gotenv v1.6.0
github.com/tencentyun/cos-go-sdk-v5 v0.7.74
github.com/tencentyun/cos-go-sdk-v5 v0.7.73
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19
github.com/upyun/go-sdk v2.1.0+incompatible
go.mongodb.org/mongo-driver/v2 v2.8.0
golang.org/x/crypto v0.54.0
golang.org/x/net v0.57.0
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.org/x/text v0.40.0
go.mongodb.org/mongo-driver/v2 v2.6.0
golang.org/x/crypto v0.53.0
golang.org/x/net v0.55.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.21.0
golang.org/x/sys v0.46.0
golang.org/x/text v0.38.0
golang.org/x/time v0.15.0
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478
gopkg.in/ini.v1 v1.67.3
gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.31.2
gorm.io/gorm v1.31.1
)
require (
@@ -70,28 +69,28 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.3 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.1 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/tea v1.5.2 // indirect
github.com/alibabacloud-go/tea v1.5.0 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 // indirect
github.com/aliyun/credentials-go v1.4.12 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/aws/aws-sdk-go-v2 v1.42.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.30 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.29 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.64.1 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.4.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.32.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.44.1 // indirect
github.com/aws/smithy-go v1.27.3 // indirect
github.com/baidubce/bce-sdk-go v0.9.270 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.8 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.19 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.18 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.24 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.24 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.24 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.25 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.24 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.8 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.1.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.18 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.42.2 // indirect
github.com/aws/smithy-go v1.25.1 // indirect
github.com/baidubce/bce-sdk-go v0.9.267 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
@@ -107,6 +106,7 @@ require (
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/docker/go-units v0.5.0 // indirect
@@ -115,10 +115,12 @@ require (
github.com/ebitengine/purego v0.10.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gammazero/toposort v0.1.1 // indirect
github.com/gin-contrib/sse v1.1.1 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-acme/alidns-20150109/v5 v5.5.0 // indirect
github.com/go-acme/esa-20240910/v3 v3.4.0 // indirect
github.com/go-acme/alidns-20150109/v5 v5.4.1 // indirect
github.com/go-acme/esa-20240910/v3 v3.2.2 // indirect
github.com/go-acme/tencentclouddnspod v1.3.24 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@@ -131,14 +133,15 @@ require (
github.com/gofrs/flock v0.13.0 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/h2non/filetype v1.1.3 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.205 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.198 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jarcoal/httpmock v1.4.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
@@ -149,7 +152,7 @@ require (
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/maxatome/go-testdeep v1.14.0 // indirect
github.com/mattn/go-shellwords v1.0.13 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -172,8 +175,6 @@ require (
github.com/nrdcg/namesilo v0.5.0 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/onsi/gomega v1.37.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/ovh/go-ovh v1.9.0 // indirect
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
@@ -188,8 +189,7 @@ require (
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.133 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.3.131 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.106 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
@@ -198,7 +198,7 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/volcengine/volc-sdk-golang v1.0.251 // indirect
github.com/volcengine/volc-sdk-golang v1.0.249 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
@@ -217,14 +217,13 @@ require (
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/arch v0.26.0 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/tools v0.47.0 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/tools v0.45.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
modernc.org/fileutil v1.4.0 // indirect
modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.48.2 // indirect
software.sslmate.com/src/go-pkcs12 v0.7.3 // indirect
software.sslmate.com/src/go-pkcs12 v0.7.1 // indirect
)
+130 -107
View File
@@ -33,8 +33,6 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/1Panel-dev/lego/v5 v5.3.1 h1:Fp8cUTooZIFG84dJlj6TDfwIyOy0HWqY/1pnqA8v8vs=
github.com/1Panel-dev/lego/v5 v5.3.1/go.mod h1:yNl7Zqt0FK1zjhE1yuGz/rjcECyBeDk2IxkKUzkfEN8=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
@@ -50,6 +48,7 @@ github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
@@ -75,10 +74,9 @@ github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.14/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.1 h1:R8b55YFS4K9x5P5IdgA+QWinVfVmulzqaJG/tr6HhxM=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.1/go.mod h1:OCFim1kMbp2m+V8WS5IBnnVrk6nXaJiDwZpg3uqw8Po=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.2/go.mod h1:CsqHkHPpAKL64jl/fChR249EL0abYEIDWmlwFYgo7/E=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.3 h1:rec08F9BzxIiJ03h2u6BT2QqLVa8ktnpvJ27j5PsjLM=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.3/go.mod h1:eHjVxrT9g8uVYN/nyAwOFQEfkVA154ChiqRc2XnNKYU=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
@@ -99,12 +97,12 @@ github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/Ke
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
github.com/alibabacloud-go/tea v1.5.0 h1:8pUo8WzMChtJT+jpLIcN1vzMi6SW3rihzzBoihPGUvs=
github.com/alibabacloud-go/tea v1.5.0/go.mod h1:hgSs82CkOiehSQMoiFN79dL6zsGX7pVGvnn9SIEs8/0=
github.com/alibabacloud-go/tea v1.5.1/go.mod h1:hgSs82CkOiehSQMoiFN79dL6zsGX7pVGvnn9SIEs8/0=
github.com/alibabacloud-go/tea v1.5.2 h1:DUzV/cRTbJC++rmADfqVu//GuWbaD64ZJ3yW3v2ukNU=
github.com/alibabacloud-go/tea v1.5.2/go.mod h1:hgSs82CkOiehSQMoiFN79dL6zsGX7pVGvnn9SIEs8/0=
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 h1:y6pUIlhjxbZl9ObDAcmA1H3c21eaAxADHTDQmBnAIgA=
github.com/alibabacloud-go/tea-utils/v2 v2.0.9/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
@@ -117,6 +115,7 @@ github.com/aliyun/credentials-go v1.4.12 h1:7D8eXGotNwthZuUEgAMgBoqxmIHwfaPVwW+/
github.com/aliyun/credentials-go v1.4.12/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@@ -126,40 +125,40 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek=
github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM=
github.com/aws/aws-sdk-go-v2/config v1.32.30 h1:XwsEzpTJfQYJbFicz/QMLwAZdyeNVVoOEkbF7R3gPJk=
github.com/aws/aws-sdk-go-v2/config v1.32.30/go.mod h1:Ud32SuMc+/9BGxfpSVld7HrE2o05JwKmXY4M3jOQNZU=
github.com/aws/aws-sdk-go-v2/credentials v1.19.29 h1:WHZGssHH887cO0ox07SIQZsFx3MKD4ps6w0xUEmnKYQ=
github.com/aws/aws-sdk-go-v2/credentials v1.19.29/go.mod h1:Mhl0xR6zjguiuj00XRx2wMx22sAltk7oya39sT7fdg8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 h1:/hi1JADLEW9YYryEz1w4GQu0EtP23pP553Cf9KgsDV4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30/go.mod h1:/3AOgy4K17Dm4ucMZVC/MJkzy5kmfKUcINRHZyo0koQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30/go.mod h1:WueJeNDZvK1fMYEWJIkcivBfEzUkTpBhzlrUKKY8EuA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 h1:jn46zC9LdsVR/ZpMIJqMqb8hHv31BlLx3ulVqNspUOk=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30/go.mod h1:1hTMsAgbdS/AtUi4bw8+gUuh1pceo+eXRLfpSuSQj3M=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 h1:3GUprIsfmGcC5SACIyB0e7E0BM1O1b3Erl5CePYIAeQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31/go.mod h1:7PuV1yl5e2xnUbm+RqvVg5i2iBM8EyijZNoI9wsOoOc=
github.com/aws/aws-sdk-go-v2 v1.41.8 h1:sRs7nG6/RiEBZ/K5UO2sNw0w40U02Nmz1VtARloTZXk=
github.com/aws/aws-sdk-go-v2 v1.41.8/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
github.com/aws/aws-sdk-go-v2/config v1.32.19 h1:qRhIJMbevHUvIE7X4TK8N8zye5+5AhapcslPrvB+qKE=
github.com/aws/aws-sdk-go-v2/config v1.32.19/go.mod h1:RbJ24nfoya63+Mf5VI+CGCGk9vEdv28xPeii+gojRYs=
github.com/aws/aws-sdk-go-v2/credentials v1.19.18 h1:GcXQz2M/0ZvMo0v5DakUqbDBeBM1ZNaivkolEF4Esgw=
github.com/aws/aws-sdk-go-v2/credentials v1.19.18/go.mod h1:sHJ06tMGcD3ZpmMyJqV+VBsGilhSIZPIN+ZFy5Dg0C4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.24 h1:FQm5ApnyzkuJdXLGskPce83CK1CQKC4RUnIHKVe4BU4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.24/go.mod h1:JsC7dqQc55MlZ5mvNsDMMge71u8pVcSzU3RNz2h/5yQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.24 h1:u6kJU2i0va1AgtJsH3RdWKWqHULlTh7zHwb35Womf74=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.24/go.mod h1:7GY+xLcXOFUpCkNwDReft9qOAVg54A4/AnjHIU7sSAY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.24 h1:Xhbcf3KugX6vX7SDyUK205Oicyfg7EGuvoVNyP5L6DM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.24/go.mod h1:rwDgb2HNOGZsnTHylOUedM7Vnl+bCfnXDqUNPsFWYfk=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.25 h1:54CTMmlJ71Rk2dYvM9qZOob+39wjlVja2zDLxCu69Ew=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.25/go.mod h1:BZaHqxsS9vN1fvV5EfEl0OBLOk5+AajWsMu6MjqnZB4=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 h1:/Z5jmNrKsSD7EmDjzAPsm/3L9IuOkzaynklJZ1qX7S4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30/go.mod h1:lEzEZnOosE7zi8Z6royW1cFJTD9fpab4Ul1SBrllewk=
github.com/aws/aws-sdk-go-v2/service/route53 v1.64.1 h1:qL5ELChQ1o1ZbzHvsl8gzQfAZdbQtrDbUyzJkjkMKcg=
github.com/aws/aws-sdk-go-v2/service/route53 v1.64.1/go.mod h1:0hIRXFez1bZsDFMGkLZvNJbByTSVZ4sFZWpxZ39NPuM=
github.com/aws/aws-sdk-go-v2/service/signin v1.4.1 h1:V7ZZ300WPXGjvkyore5DGe0ljVPOxCXie/thWdtSBXE=
github.com/aws/aws-sdk-go-v2/service/signin v1.4.1/go.mod h1:mxC0nT/C8wMMS97DemZPzvUZxvIt+2Iq+eS3JdFZGgg=
github.com/aws/aws-sdk-go-v2/service/sso v1.32.1 h1:gYFYh4iLLcAOJRLNPY2aD2g9DIhKn4eof8UkIrr1rTk=
github.com/aws/aws-sdk-go-v2/service/sso v1.32.1/go.mod h1:u8af9Nqkmqnr96f7v9nHqzZT9XBwbXEkTiqT4ROuJSE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1 h1:arjT9Cm3/WYbGmD5TUZHk4UQn4Lle1fUNZs5FC6CtF0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1/go.mod h1:DMPWJBjYs6+3+f/qhBFEFPPlQ6NlhWjai3dJNvipJ84=
github.com/aws/aws-sdk-go-v2/service/sts v1.44.1 h1:RvfHDg+xvAeZ+5741vUEjpOVtYSIm93W2zhx10Xtydw=
github.com/aws/aws-sdk-go-v2/service/sts v1.44.1/go.mod h1:9gdl4RrflIdpDb2TlXshWgR1F9TeCkvqDx77Vpr4Z/Q=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.24 h1:CQW2FTrflfoslYWLf3fv7vG28Q219+v8YJS5QTQb2+Y=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.24/go.mod h1:Xfx13T+u3nH6EEzgl9fBSO6nDRmze1FvnZNYkctQ2zw=
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.8 h1:1QC1xoZg5XSes1CXQ20Y0qTaaeDj12e7bB0/Yw9sQwI=
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.8/go.mod h1:+GpgmfX7rnzQ8WMbMCMnI+VOjPZCf9yaUlBmRTvQgFE=
github.com/aws/aws-sdk-go-v2/service/signin v1.1.0 h1:yQo3eZ5qFaL1sJWqs1nL6j3yPHA2/R7c6tQ4T+0IO10=
github.com/aws/aws-sdk-go-v2/service/signin v1.1.0/go.mod h1:3Zzou41Qt/ueXfIzHvTEjDNuR5IjCUBVF01SNhrt1e8=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.18 h1:ApLTFdAZfDhZSiY5uskwECKHkSNNF83y2Ru2r7SezWA=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.18/go.mod h1:A9K9qx2l6nK89hp+a350FdGfRkrkH5HdiEjHbiy/Q/c=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.1 h1:4VD7TIZOGzehrgQ8vDE+1c6BQW4ErZPGY8ohZT5LXEE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.1/go.mod h1:er0SFJfdV89Rit5hIJu/EXtv+qC2XMnxoksLmcUFkqM=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.2 h1:XKnxlM4KZH1gktcsh3zSWc7GW4KivEv/OkifmHOhCUY=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.2/go.mod h1:KJYmkQaFB3SUW2j3aBkPsxNmAb4ZsSOvbvCpuxzHJA0=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY=
github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/baidubce/bce-sdk-go v0.9.270 h1:WAYDTBdrE2FU+XQVdKReuDuK9QVCjRdekK3lr3ByDvg=
github.com/baidubce/bce-sdk-go v0.9.270/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/baidubce/bce-sdk-go v0.9.267 h1:0l3UipFvWVsEv+FcontxWRs6ny8xvBeRISLbdNtO/bw=
github.com/baidubce/bce-sdk-go v0.9.267/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@@ -205,8 +204,8 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/compose-spec/compose-go/v2 v2.13.0 h1:2+2oS3v4SrtAOBdZRAZYBsBy47D571p5EXMSCppmTtE=
github.com/compose-spec/compose-go/v2 v2.13.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg=
github.com/compose-spec/compose-go/v2 v2.11.0 h1:xoq/ootgIL6TsHmbJHrkuh7+bzjhPV3NHftHRPPyVXM=
github.com/compose-spec/compose-go/v2 v2.11.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -224,12 +223,15 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 h1:OGNva6WhsKst5OZf7eZOklDztV3hwtTHovdrLHV+MsA=
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v29.6.2+incompatible h1:/bjePvcbbFTnRrMfWJBY7AjfICdsiLVgHn6LwTVOcqw=
github.com/docker/cli v29.6.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs=
github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
@@ -258,6 +260,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
@@ -277,6 +281,8 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg=
github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
@@ -286,10 +292,14 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-acme/alidns-20150109/v5 v5.5.0 h1:/LUHZSQoaDqagi8wCq8f9Rx4AoVr/Jd4rKEV2IQYvxs=
github.com/go-acme/alidns-20150109/v5 v5.5.0/go.mod h1:J2B9WBwSFfZalzlCqSkeyBkqBcWPEZMDkxwhOjjF0MA=
github.com/go-acme/esa-20240910/v3 v3.4.0 h1:t2ih22iRV+BzLkhvRG/i5hkyfkfi4FSPTZ3ves8oQ/0=
github.com/go-acme/esa-20240910/v3 v3.4.0/go.mod h1:QGkL4IcH2mT4K+VoTMMJnq9tOBPNa0E6QdSmKmn0D2s=
github.com/go-acme/alidns-20150109/v5 v5.4.1 h1:Bp+EDtIGxBVKnIq1hPECBFf6eueTiESYqsHkY+T+CIQ=
github.com/go-acme/alidns-20150109/v5 v5.4.1/go.mod h1:L+SrZRmoQFx2UzOqng3z3+LA/gNgFCaLXrwr/ndyW6g=
github.com/go-acme/esa-20240910/v3 v3.2.2 h1:dZ+HKddDMUcPr46gkFYdn5FbtJwIic59+MEaCaH/cxM=
github.com/go-acme/esa-20240910/v3 v3.2.2/go.mod h1:qn4WK9pNgXoS5ypmiZfCWSYdOtWcte9GDuhDjKP752A=
github.com/go-acme/lego/v5 v5.2.2 h1:KqFas/Ak2QDdU+Qm7MO9OEnS35zC000kPmM+5tLdZKk=
github.com/go-acme/lego/v5 v5.2.2/go.mod h1:H/hb7OJKmpVGa8zypgZh0ys5P4VUu++ZNgAEcOwq+OI=
github.com/go-acme/tencentclouddnspod v1.3.24 h1:uCSiOW1EJttcnOON+MVVyVDJguFL/Q4NIGkq1CrT9p8=
github.com/go-acme/tencentclouddnspod v1.3.24/go.mod h1:RKcB2wSoZncjBA0OEFj59s1ko1XDy+ZsAtk+9uMxUF0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -330,6 +340,7 @@ github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGK
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -347,8 +358,12 @@ github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/goh-chunlin/go-onedrive v1.1.1 h1:HGtHk5iG0MZ92zYUtaY04czfZPBIJUr12UuFc+PW8m4=
github.com/goh-chunlin/go-onedrive v1.1.1/go.mod h1:N8qIGHD7tryO734epiBKk5oXcpGwxKET/u3LuBHciTs=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -441,6 +456,9 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
github.com/h2non/filetype v1.1.1/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -471,8 +489,8 @@ github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.205 h1:YnI2+56S5oxByz8BYChRrsmqTUmCp1Ww2KIzZtF4XOU=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.205/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.198 h1:/Qcq965fQ5AjM0lg8ndczox8jqAqp+5J8r4KFrfpb3Y=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.198/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -486,8 +504,8 @@ github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
@@ -496,8 +514,11 @@ github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aW
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
@@ -523,8 +544,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
@@ -550,6 +571,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -564,13 +587,14 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-shellwords v1.0.14 h1:yUKzIgsCnosndOASY6/enly1EAuaXeFSQ7cdyA3OuYg=
github.com/mattn/go-shellwords v1.0.14/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4=
github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM=
github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60VhnHxb2X3T1teMyv5A=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
@@ -584,8 +608,8 @@ github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLT
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw=
github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
github.com/minio/minio-go/v7 v7.2.0 h1:RCJM0R1XOsRs+A3x3UCaf3ZYbByDaLjFeAi+YCQEPhs=
github.com/minio/minio-go/v7 v7.2.0/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@@ -657,15 +681,13 @@ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.2 h1:HFB2fbVIlhIfCfOW81bZFbiC/RvnpXSdhbF2/DJr134=
github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
@@ -695,8 +717,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0=
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -724,8 +746,8 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/qiniu/go-sdk/v7 v7.26.17 h1:SMjjdKak2j1eedAHreMiS6sObDOLuUrvRqPtlV4KPQo=
github.com/qiniu/go-sdk/v7 v7.26.17/go.mod h1:pTwVR1B+8SXcPLhDzBUasiKFTD9F7jRglRDR553BW3k=
github.com/qiniu/go-sdk/v7 v7.26.12 h1:AnWiKjBY62XpULoB/MySKdNmlUo9S9pQB7/0s7QueCo=
github.com/qiniu/go-sdk/v7 v7.26.12/go.mod h1:ri7fGwbio0pRDFr8EK5TUpx0DbnpIMJ2bMSDxGWfCbk=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
@@ -751,8 +773,8 @@ github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9t
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM=
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -807,14 +829,12 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.131/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.133 h1:EKJsaGHbifEKqWCdJ4tJNLxbJ5h/Pm3CaLKzcCS/pkw=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.133/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.3.131 h1:YfzhVkH30GipMEOGIx6nkaJv2E/mWN+G6Vvg+M7SqGg=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.3.131/go.mod h1:Eejvn572HmciFhWFMg6a+eZvkegnEzJrPSnJYKYZX2s=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.24/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.106 h1:YjsxDQwoiB+C0ISjnVk22Wbks02Ir+7Z5wmujcz69Vg=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.106/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0=
github.com/tencentyun/cos-go-sdk-v5 v0.7.74 h1:YCsGhIK+Q5vMcmAiQqPuMyhSQ3zN1p7U+dhlHQD++6c=
github.com/tencentyun/cos-go-sdk-v5 v0.7.74/go.mod h1:STbTNaNKq03u+gscPEGOahKzLcGSYOj6Dzc5zNay7Pg=
github.com/tencentyun/cos-go-sdk-v5 v0.7.73 h1:uFfgp1A7cQaAGR6QP9DsIkoEQ67b8ewj5r1RV6XB540=
github.com/tencentyun/cos-go-sdk-v5 v0.7.73/go.mod h1:STbTNaNKq03u+gscPEGOahKzLcGSYOj6Dzc5zNay7Pg=
github.com/tencentyun/qcloud-cos-sts-sdk v0.0.0-20250515025012-e0eec8a5d123/go.mod h1:b18KQa4IxHbxeseW1GcZox53d7J0z39VNONTxvvlkXw=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
@@ -840,8 +860,8 @@ github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0o
github.com/upyun/go-sdk v2.1.0+incompatible h1:OdjXghQ/TVetWV16Pz3C1/SUpjhGBVPr+cLiqZLLyq0=
github.com/upyun/go-sdk v2.1.0+incompatible/go.mod h1:eu3F5Uz4b9ZE5bE5QsCL6mgSNWRwfj0zpJ9J626HEqs=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/volcengine/volc-sdk-golang v1.0.251 h1:N/3jkDfY+w/U/MC8T91Pvhpv8nBK4ESsxE7ykLWntlg=
github.com/volcengine/volc-sdk-golang v1.0.251/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM=
github.com/volcengine/volc-sdk-golang v1.0.249 h1:BNFGca7CI2t9DktB+FlXBg7GZFezS3NnKAPLmRirLlw=
github.com/volcengine/volc-sdk-golang v1.0.249/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
@@ -878,8 +898,8 @@ go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lL
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8=
go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8=
go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -927,10 +947,12 @@ golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -951,8 +973,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -994,8 +1016,9 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1050,13 +1073,14 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
@@ -1076,8 +1100,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1153,8 +1177,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1168,8 +1192,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1186,8 +1210,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1249,8 +1273,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1317,8 +1341,8 @@ google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478 h1:aLsVTW0lZ8+IY5u
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478/go.mod h1:YJAzKjfHIUHb9T+bfu8L7mthAp7VVXQBUs1PLdBWS7M=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1336,8 +1360,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1379,15 +1403,14 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -1430,5 +1453,5 @@ rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
software.sslmate.com/src/go-pkcs12 v0.7.3 h1:JBQD3FDqYjTeyDAeZQklj2ar88ykBLtALloPJHyAauU=
software.sslmate.com/src/go-pkcs12 v0.7.3/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
software.sslmate.com/src/go-pkcs12 v0.7.1 h1:bxkUPRsvTPNRBZa4M/aSX4PyMOEbq3V8I6hbkG4F4Q8=
software.sslmate.com/src/go-pkcs12 v0.7.1/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

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