mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-12 16:00:33 +00:00
Compare commits
111
Commits
release-2.2.3
...
v2.2.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7ef35740c | ||
|
|
b0d561e33b | ||
|
|
75b362fa9b | ||
|
|
466f373ef6 | ||
|
|
4489641b54 | ||
|
|
1971d9dec2 | ||
|
|
c38d741770 | ||
|
|
122a474032 | ||
|
|
54854e99e7 | ||
|
|
e01fb7c905 | ||
|
|
83a3675a0c | ||
|
|
9dfa451fae | ||
|
|
9204a287fd | ||
|
|
f027507f9a | ||
|
|
01aee89f3b | ||
|
|
17285d4397 | ||
|
|
91ae846418 | ||
|
|
1eb429631d | ||
|
|
6584e3b868 | ||
|
|
14e2294db9 | ||
|
|
26b69bc208 | ||
|
|
2111d4c16b | ||
|
|
b682835b4e | ||
|
|
c34ac2f31e | ||
|
|
c28047374a | ||
|
|
60d16609f7 | ||
|
|
02ca9347dc | ||
|
|
d0187994ee | ||
|
|
be672d604f | ||
|
|
16e3d496eb | ||
|
|
9159ab842d | ||
|
|
0d8835d494 | ||
|
|
9f9e3aacfc | ||
|
|
7bd11fe73e | ||
|
|
e11dc5fadd | ||
|
|
f35b0deb29 | ||
|
|
33b3eecb95 | ||
|
|
6ac7a5f167 | ||
|
|
a5fbbfc460 | ||
|
|
563df3da71 | ||
|
|
13e6bc4fac | ||
|
|
357d77856a | ||
|
|
4279339189 | ||
|
|
380033dfe0 | ||
|
|
97d383ed12 | ||
|
|
52e6a63ebc | ||
|
|
7506e709e2 | ||
|
|
cf37de66fc | ||
|
|
e2754b447d | ||
|
|
c04848a92a | ||
|
|
748f5f070a | ||
|
|
2dcb367622 | ||
|
|
26bd2d08be | ||
|
|
739686ff89 | ||
|
|
68a871a5d7 | ||
|
|
b47bba4827 | ||
|
|
3b91859a49 | ||
|
|
c417bc9c2b | ||
|
|
7c95becfeb | ||
|
|
6b1480c5fa | ||
|
|
7f450544e1 | ||
|
|
0c046edfe5 | ||
|
|
2a2e6607b8 | ||
|
|
f6ed11ac55 | ||
|
|
d330f60b2d | ||
|
|
10c8cafcee | ||
|
|
d1534c88aa | ||
|
|
eb6b4c7cb7 | ||
|
|
c77ddcfebe | ||
|
|
af12526a6c | ||
|
|
b347abe100 | ||
|
|
092b57413b | ||
|
|
e8f35e4e06 | ||
|
|
b019e7526d | ||
|
|
1b06467521 | ||
|
|
7f3fd0cb1d | ||
|
|
3e9848554b | ||
|
|
b02cdbc5f9 | ||
|
|
d7723fd42a | ||
|
|
8b265bb81e | ||
|
|
89b6c94e42 | ||
|
|
219ac160b6 | ||
|
|
51ca1eab18 | ||
|
|
1ab3da1fab | ||
|
|
1d1b12c2fa | ||
|
|
07a658582d | ||
|
|
61da74ae50 | ||
|
|
90334f3cb0 | ||
|
|
fb9278bfc6 | ||
|
|
7750936af6 | ||
|
|
7b695af996 | ||
|
|
2874849557 | ||
|
|
9fd1f3e661 | ||
|
|
85182d9164 | ||
|
|
d9b2ea051f | ||
|
|
097fe6dcfb | ||
|
|
d5beca6d96 | ||
|
|
e3a356f893 | ||
|
|
8be2a9ab02 | ||
|
|
3d7f0f5143 | ||
|
|
1fd62fbaf7 | ||
|
|
f1372dda85 | ||
|
|
64c354adb2 | ||
|
|
a379e1f840 | ||
|
|
270ba799f7 | ||
|
|
60dcfacb88 | ||
|
|
b474d720e9 | ||
|
|
b2192b7252 | ||
|
|
6a988ad96a | ||
|
|
029ac5efee | ||
|
|
582cf1f533 |
@@ -1,25 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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 }}
|
|
||||||
@@ -6,7 +6,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Mirror the Github organization repos to Gitee.
|
- name: Mirror the Github organization repos to Gitee.
|
||||||
uses: Yikun/hub-mirror-action@master
|
uses: Yikun/hub-mirror-action@ba51c01b28a6c9f95a25d4f1bcf6af2a147c0e18 # master
|
||||||
with:
|
with:
|
||||||
src: 'github/1Panel-dev'
|
src: 'github/1Panel-dev'
|
||||||
dst: 'gitee/fit2cloud-feizhiyun'
|
dst: 'gitee/fit2cloud-feizhiyun'
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
<a href="/docs/README.ru.md"><img alt="Русский" src="https://img.shields.io/badge/Русский-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.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.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>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -500,6 +500,27 @@ func (b *BaseApi) GetAgentAccountModels(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, list)
|
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
|
// @Tags AI
|
||||||
// @Summary Create model account model
|
// @Summary Create model account model
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -1357,6 +1378,88 @@ func (b *BaseApi) UninstallAgentSkill(c *gin.Context) {
|
|||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary List OpenClaw plugins
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentPluginsReq true "request"
|
||||||
|
// @Success 200 {array} dto.AgentPluginItem
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/plugins/list [post]
|
||||||
|
func (b *BaseApi) ListAgentPlugins(c *gin.Context) {
|
||||||
|
var req dto.AgentPluginsReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := agentService.ListPlugins(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary Search OpenClaw plugins
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentPluginSearchReq true "request"
|
||||||
|
// @Success 200 {array} dto.AgentPluginSearchItem
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/plugins/search [post]
|
||||||
|
func (b *BaseApi) SearchAgentPlugins(c *gin.Context) {
|
||||||
|
var req dto.AgentPluginSearchReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := agentService.SearchPlugins(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary Install an OpenClaw marketplace plugin
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentPluginMarketInstallReq true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/plugins/install [post]
|
||||||
|
func (b *BaseApi) InstallAgentMarketPlugin(c *gin.Context) {
|
||||||
|
var req dto.AgentPluginMarketInstallReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := agentService.InstallMarketPlugin(req); err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary Operate an OpenClaw plugin
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentPluginOperateReq true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/plugins/operate [post]
|
||||||
|
func (b *BaseApi) OperateAgentPlugin(c *gin.Context) {
|
||||||
|
var req dto.AgentPluginOperateReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := agentService.OperatePlugin(req); err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Login Agent Weixin channel
|
// @Summary Login Agent Weixin channel
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
|||||||
@@ -881,6 +881,26 @@ func (b *BaseApi) ComposeUpdate(c *gin.Context) {
|
|||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags Container Compose
|
||||||
|
// @Summary Pin compose
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.ComposePin true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /containers/compose/pin [post]
|
||||||
|
func (b *BaseApi) ComposePin(c *gin.Context) {
|
||||||
|
var req dto.ComposePin
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := containerService.ComposePin(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags Container Compose
|
// @Tags Container Compose
|
||||||
// @Summary Load compose environment variables
|
// @Summary Load compose environment variables
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
|||||||
@@ -41,20 +41,40 @@ func (b *BaseApi) CreateMysql(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Database Mysql
|
// @Tags Database Mysql
|
||||||
// @Summary Bind user of mysql database
|
// @Summary List mysql users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.BindUser true "request"
|
// @Param request body dto.MysqlUserSearch true "request"
|
||||||
// @Success 200
|
// @Success 200 {array} dto.MysqlUser
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /databases/bind [post]
|
// @Router /databases/users/search [post]
|
||||||
// @x-panel-log {"bodyKeys":["database", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"绑定 mysql 数据库名 [database] [username]","formatEN":"bind mysql database [database] [username]"}
|
func (b *BaseApi) ListMysqlUsers(c *gin.Context) {
|
||||||
func (b *BaseApi) BindUser(c *gin.Context) {
|
var req dto.MysqlUserSearch
|
||||||
var req dto.BindUser
|
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
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"
|
||||||
|
// @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
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if len(req.Password) != 0 {
|
if len(req.Password) != 0 {
|
||||||
password, err := base64.StdEncoding.DecodeString(req.Password)
|
password, err := base64.StdEncoding.DecodeString(req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -63,8 +83,191 @@ func (b *BaseApi) BindUser(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
req.Password = string(password)
|
req.Password = string(password)
|
||||||
}
|
}
|
||||||
|
if err := mysqlService.CreateUser(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
if err := mysqlService.BindUser(req); err != nil {
|
// @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 {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -94,14 +297,14 @@ func (b *BaseApi) UpdateMysqlDescription(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Database Mysql
|
// @Tags Database Mysql
|
||||||
// @Summary Change mysql password
|
// @Summary Change mysql root password
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.ChangeDBInfo true "request"
|
// @Param request body dto.ChangeDBInfo true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /databases/change/password [post]
|
// @Router /databases/change/password [post]
|
||||||
// @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"}
|
// @x-panel-log {"bodyKeys":["database"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新数据库 [database] root 密码","formatEN":"Update database [database] root password"}
|
||||||
func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) {
|
func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) {
|
||||||
var req dto.ChangeDBInfo
|
var req dto.ChangeDBInfo
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
@@ -125,14 +328,14 @@ func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Database Mysql
|
// @Tags Database Mysql
|
||||||
// @Summary Change mysql access
|
// @Summary Change mysql root access
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.ChangeDBInfo true "request"
|
// @Param request body dto.ChangeDBInfo true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /databases/change/access [post]
|
// @Router /databases/change/access [post]
|
||||||
// @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"}
|
// @x-panel-log {"bodyKeys":["database"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新数据库 [database] root 访问权限","formatEN":"Update database [database] root access"}
|
||||||
func (b *BaseApi) ChangeMysqlAccess(c *gin.Context) {
|
func (b *BaseApi) ChangeMysqlAccess(c *gin.Context) {
|
||||||
var req dto.ChangeDBInfo
|
var req dto.ChangeDBInfo
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
|||||||
@@ -37,14 +37,16 @@ var (
|
|||||||
|
|
||||||
cronjobService = service.NewICronjobService()
|
cronjobService = service.NewICronjobService()
|
||||||
|
|
||||||
fileService = service.NewIFileService()
|
fileService = service.NewIFileService()
|
||||||
fileHistoryService = service.NewIFileHistoryService()
|
fileHistoryService = service.NewIFileHistoryService()
|
||||||
fileShareService = service.NewIFileShareService()
|
fileShareService = service.NewIFileShareService()
|
||||||
sshService = service.NewISSHService()
|
sshService = service.NewISSHService()
|
||||||
firewallService = service.NewIFirewallService()
|
firewallService = service.NewIFirewallService()
|
||||||
iptablesService = service.NewIIptablesService()
|
forwardingService = service.NewIForwardingService()
|
||||||
monitorService = service.NewIMonitorService()
|
iptablesService = service.NewIIptablesService()
|
||||||
systemService = service.NewISystemService()
|
monitorService = service.NewIMonitorService()
|
||||||
|
systemService = service.NewISystemService()
|
||||||
|
runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService()
|
||||||
|
|
||||||
deviceService = service.NewIDeviceService()
|
deviceService = service.NewIDeviceService()
|
||||||
fail2banService = service.NewIFail2BanService()
|
fail2banService = service.NewIFail2BanService()
|
||||||
@@ -59,6 +61,7 @@ var (
|
|||||||
websiteDnsAccountService = service.NewIWebsiteDnsAccountService()
|
websiteDnsAccountService = service.NewIWebsiteDnsAccountService()
|
||||||
websiteSSLService = service.NewIWebsiteSSLService()
|
websiteSSLService = service.NewIWebsiteSSLService()
|
||||||
websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService()
|
websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService()
|
||||||
|
websiteTemplateService = service.NewIWebsiteTemplateService()
|
||||||
|
|
||||||
nginxService = service.NewINginxService()
|
nginxService = service.NewINginxService()
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
@@ -28,6 +30,11 @@ import (
|
|||||||
qrcode "github.com/skip2/go-qrcode"
|
qrcode "github.com/skip2/go-qrcode"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var cancelledChunkUploads = struct {
|
||||||
|
sync.RWMutex
|
||||||
|
ids map[string]struct{}
|
||||||
|
}{ids: make(map[string]struct{})}
|
||||||
|
|
||||||
// @Tags File
|
// @Tags File
|
||||||
// @Summary List files
|
// @Summary List files
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -873,7 +880,24 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
filename := c.PostForm("filename")
|
filename := c.PostForm("filename")
|
||||||
fileDir := filepath.Join(tmpDir, 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
|
||||||
|
}
|
||||||
if chunkIndex == 0 {
|
if chunkIndex == 0 {
|
||||||
if fileOp.Stat(fileDir) {
|
if fileOp.Stat(fileDir) {
|
||||||
_ = fileOp.DeleteDir(fileDir)
|
_ = fileOp.DeleteDir(fileDir)
|
||||||
@@ -884,7 +908,7 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
|||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = os.Remove(fileDir)
|
_ = os.RemoveAll(fileDir)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
var (
|
var (
|
||||||
@@ -904,6 +928,11 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
|||||||
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
||||||
return
|
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))
|
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
|
||||||
err = os.WriteFile(chunkPath, chunkData, constant.DirPerm)
|
err = os.WriteFile(chunkPath, chunkData, constant.DirPerm)
|
||||||
@@ -922,12 +951,50 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
|||||||
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if cancellable {
|
||||||
|
cancelledChunkUploads.Lock()
|
||||||
|
delete(cancelledChunkUploads.ids, uploadID)
|
||||||
|
cancelledChunkUploads.Unlock()
|
||||||
|
}
|
||||||
helper.SuccessWithData(c, true)
|
helper.SuccessWithData(c, true)
|
||||||
} else {
|
} else {
|
||||||
return
|
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{
|
var wsUpgrade = websocket.Upgrader{
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -20,7 +20,15 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := firewallService.LoadBaseInfo(req.Name)
|
var (
|
||||||
|
data dto.FirewallBaseInfo
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if req.Name == "forward" {
|
||||||
|
data, err = forwardingService.LoadBaseInfo()
|
||||||
|
} else {
|
||||||
|
data, err = firewallService.LoadBaseInfo(req.Name)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -43,7 +51,21 @@ func (b *BaseApi) SearchFirewallRule(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, list, err := firewallService.SearchWithPage(req)
|
var (
|
||||||
|
total int64
|
||||||
|
list interface{}
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if req.Type == "forward" {
|
||||||
|
total, list, err = forwardingService.SearchWithPage(dto.ForwardRuleSearch{
|
||||||
|
PageInfo: req.PageInfo,
|
||||||
|
Info: req.Info,
|
||||||
|
Status: req.Status,
|
||||||
|
Strategy: req.Strategy,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
total, list, err = firewallService.SearchWithPage(req)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -116,7 +138,7 @@ func (b *BaseApi) OperateForwardRule(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := firewallService.OperateForwardRule(req); err != nil {
|
if err := forwardingService.Operate(req); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -313,7 +335,13 @@ func (b *BaseApi) OperateFilterChain(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := iptablesService.Operate(req); err != nil {
|
var err error
|
||||||
|
if req.Operate == "init-forward" {
|
||||||
|
err = forwardingService.Enable()
|
||||||
|
} else {
|
||||||
|
err = iptablesService.Operate(req)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package v2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,3 +21,57 @@ func (b *BaseApi) GetSystemFiles(c *gin.Context) {
|
|||||||
|
|
||||||
helper.SuccessWithData(c, data)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
@@ -224,6 +224,21 @@ func (b *BaseApi) ExportSSHLogs(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, tmpFile)
|
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
|
// @Tags SSH
|
||||||
// @Summary Load host SSH conf
|
// @Summary Load host SSH conf
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Page website templates
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.WebsiteTemplateSearch true "request"
|
||||||
|
// @Success 200 {object} dto.PageResult
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/search [post]
|
||||||
|
func (b *BaseApi) PageWebsiteTemplate(c *gin.Context) {
|
||||||
|
var req request.WebsiteTemplateSearch
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
total, templates, err := websiteTemplateService.PageTemplate(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, dto.PageResult{
|
||||||
|
Total: total,
|
||||||
|
Items: templates,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Create website template
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.WebsiteTemplateCreate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建网站模板 [name]","formatEN":"Create website template [name]"}
|
||||||
|
func (b *BaseApi) CreateWebsiteTemplate(c *gin.Context) {
|
||||||
|
var req request.WebsiteTemplateCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := websiteTemplateService.CreateTemplate(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Update website template
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.WebsiteTemplateUpdate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/update [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新网站模板 [name]","formatEN":"Update website template [name]"}
|
||||||
|
func (b *BaseApi) UpdateWebsiteTemplate(c *gin.Context) {
|
||||||
|
var req request.WebsiteTemplateUpdate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := websiteTemplateService.UpdateTemplate(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Delete website template
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.OperateByID true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/del [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_templates","output_column":"name","output_value":"name"}],"formatZH":"删除网站模板 [name]","formatEN":"Delete website template [name]"}
|
||||||
|
func (b *BaseApi) DeleteWebsiteTemplate(c *gin.Context) {
|
||||||
|
var req dto.OperateByID
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := websiteTemplateService.DeleteTemplate(req.ID); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Get website template
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.OperateByID true "request"
|
||||||
|
// @Success 200 {object} response.WebsiteTemplateDTO
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/get [post]
|
||||||
|
func (b *BaseApi) GetWebsiteTemplate(c *gin.Context) {
|
||||||
|
var req dto.OperateByID
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
template, err := websiteTemplateService.GetTemplate(req.ID)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, template)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Upload website template zip
|
||||||
|
// @Accept multipart/form-data
|
||||||
|
// @Param file formData file true "file"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/upload [post]
|
||||||
|
func (b *BaseApi) UploadTemplateZip(c *gin.Context) {
|
||||||
|
fileHeader, err := c.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file, err := fileHeader.Open()
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
content, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filePath, variables, err := websiteTemplateService.SaveUploadZip(fileHeader.Filename, content)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, gin.H{"filePath": filePath, "variables": variables})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Preview website template
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.WebsitePreviewReq true "request"
|
||||||
|
// @Success 200 {object} response.WebsitePreviewDTO
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/preview [post]
|
||||||
|
func (b *BaseApi) PreviewWebsiteTemplate(c *gin.Context) {
|
||||||
|
var req request.WebsitePreviewReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
preview, err := websiteTemplateService.Preview(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, preview)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Page website template outputs
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.WebsiteTemplateOutputSearch true "request"
|
||||||
|
// @Success 200 {object} dto.PageResult
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/outputs/search [post]
|
||||||
|
func (b *BaseApi) PageWebsiteTemplateOutput(c *gin.Context) {
|
||||||
|
var req request.WebsiteTemplateOutputSearch
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
total, outputs, err := websiteTemplateService.PageOutput(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, dto.PageResult{
|
||||||
|
Total: total,
|
||||||
|
Items: outputs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Create website template output
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.WebsiteTemplateOutputCreate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/outputs [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成模板产物 [name]","formatEN":"Generate template output [name]"}
|
||||||
|
func (b *BaseApi) CreateWebsiteTemplateOutput(c *gin.Context) {
|
||||||
|
var req request.WebsiteTemplateOutputCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := websiteTemplateService.CreateOutput(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Delete website template output
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.OperateByID true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/outputs/del [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_template_outputs","output_column":"name","output_value":"name"}],"formatZH":"删除模板产物 [name]","formatEN":"Delete template output [name]"}
|
||||||
|
func (b *BaseApi) DeleteWebsiteTemplateOutput(c *gin.Context) {
|
||||||
|
var req dto.OperateByID
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := websiteTemplateService.DeleteOutput(req.ID); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Get website template output
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.OperateByID true "request"
|
||||||
|
// @Success 200 {object} response.WebsiteTemplateOutputDTO
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/outputs/get [post]
|
||||||
|
func (b *BaseApi) GetWebsiteTemplateOutput(c *gin.Context) {
|
||||||
|
var req dto.OperateByID
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
output, err := websiteTemplateService.GetOutput(req.ID)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, output)
|
||||||
|
}
|
||||||
+106
-38
@@ -118,8 +118,6 @@ type AgentItem struct {
|
|||||||
ProviderName string `json:"providerName"`
|
ProviderName string `json:"providerName"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
APIType string `json:"apiType"`
|
APIType string `json:"apiType"`
|
||||||
MaxTokens int `json:"maxTokens"`
|
|
||||||
ContextWindow int `json:"contextWindow"`
|
|
||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
APIKey string `json:"apiKey"`
|
APIKey string `json:"apiKey"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
@@ -292,19 +290,22 @@ type AgentOverviewSnapshot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountModel struct {
|
type AgentAccountModel struct {
|
||||||
RecordID uint `json:"recordId"`
|
RecordID uint `json:"recordId"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ContextWindow int `json:"contextWindow"`
|
|
||||||
MaxTokens int `json:"maxTokens"`
|
|
||||||
Reasoning bool `json:"reasoning"`
|
|
||||||
Input []string `json:"input"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountModelReq struct {
|
type AgentAccountModelReq struct {
|
||||||
AccountID uint `json:"accountId" validate:"required"`
|
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 {
|
type AgentAccountModelCreateReq struct {
|
||||||
AccountID uint `json:"accountId" validate:"required"`
|
AccountID uint `json:"accountId" validate:"required"`
|
||||||
Model AgentAccountModel `json:"model" validate:"required"`
|
Model AgentAccountModel `json:"model" validate:"required"`
|
||||||
@@ -321,31 +322,40 @@ type AgentAccountModelDeleteReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountCreateReq struct {
|
type AgentAccountCreateReq struct {
|
||||||
Provider string `json:"provider" validate:"required"`
|
Provider string `json:"provider" validate:"required"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
APIKey string `json:"apiKey" validate:"required"`
|
APIKey string `json:"apiKey" validate:"required"`
|
||||||
RememberAPIKey bool `json:"rememberApiKey"`
|
RememberAPIKey bool `json:"rememberApiKey"`
|
||||||
BaseURL string `json:"baseURL"`
|
BaseURL string `json:"baseURL"`
|
||||||
Models []AgentAccountModel `json:"models"`
|
Models []AgentAccountModel `json:"models"`
|
||||||
APIType string `json:"apiType" validate:"required"`
|
APIType string `json:"apiType" validate:"required"`
|
||||||
Remark string `json:"remark"`
|
AuthMode string `json:"authMode"`
|
||||||
|
VerifyModel string `json:"verifyModel"`
|
||||||
|
ValidateAvailability *bool `json:"validateAvailability"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountUpdateReq struct {
|
type AgentAccountUpdateReq struct {
|
||||||
ID uint `json:"id" validate:"required"`
|
ID uint `json:"id" validate:"required"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
APIKey string `json:"apiKey" validate:"required"`
|
APIKey string `json:"apiKey" validate:"required"`
|
||||||
RememberAPIKey bool `json:"rememberApiKey"`
|
RememberAPIKey bool `json:"rememberApiKey"`
|
||||||
BaseURL string `json:"baseURL"`
|
BaseURL string `json:"baseURL"`
|
||||||
APIType string `json:"apiType" validate:"required"`
|
APIType string `json:"apiType" validate:"required"`
|
||||||
Remark string `json:"remark"`
|
AuthMode string `json:"authMode"`
|
||||||
SyncAgents bool `json:"syncAgents"`
|
VerifyModel string `json:"verifyModel"`
|
||||||
|
ValidateAvailability *bool `json:"validateAvailability"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
SyncAgents bool `json:"syncAgents"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountVerifyReq struct {
|
type AgentAccountVerifyReq struct {
|
||||||
Provider string `json:"provider" validate:"required"`
|
Provider string `json:"provider" validate:"required"`
|
||||||
APIKey string `json:"apiKey" validate:"required"`
|
APIKey string `json:"apiKey" validate:"required"`
|
||||||
BaseURL string `json:"baseURL"`
|
BaseURL string `json:"baseURL"`
|
||||||
|
APIType string `json:"apiType" validate:"required"`
|
||||||
|
AuthMode string `json:"authMode"`
|
||||||
|
Model string `json:"model"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountDeleteReq struct {
|
type AgentAccountDeleteReq struct {
|
||||||
@@ -355,6 +365,8 @@ type AgentAccountDeleteReq struct {
|
|||||||
type AgentAccountSearch struct {
|
type AgentAccountSearch struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
|
APIType string `json:"apiType"`
|
||||||
|
TextOnly bool `json:"textOnly"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,26 +385,36 @@ type AgentAccountInfo struct {
|
|||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
Models []AgentAccountModel `json:"models"`
|
Models []AgentAccountModel `json:"models"`
|
||||||
APIType string `json:"apiType"`
|
APIType string `json:"apiType"`
|
||||||
|
AuthMode string `json:"authMode"`
|
||||||
|
VerifyModel string `json:"verifyModel"`
|
||||||
Verified bool `json:"verified"`
|
Verified bool `json:"verified"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderModelInfo struct {
|
type ProviderModelInfo struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ContextWindow int `json:"contextWindow"`
|
}
|
||||||
MaxTokens int `json:"maxTokens"`
|
|
||||||
Reasoning bool `json:"reasoning"`
|
type ProviderAPIInfo struct {
|
||||||
Input []string `json:"input"`
|
APIType string `json:"apiType"`
|
||||||
|
BaseURL string `json:"baseUrl"`
|
||||||
|
EditableBaseURL bool `json:"editableBaseUrl"`
|
||||||
|
SupportsModelDiscovery bool `json:"supportsModelDiscovery"`
|
||||||
|
DefaultAuthMode string `json:"defaultAuthMode"`
|
||||||
|
AuthModes []string `json:"authModes"`
|
||||||
|
Models []ProviderModelInfo `json:"models"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderInfo struct {
|
type ProviderInfo struct {
|
||||||
Sort uint `json:"-"`
|
Sort uint `json:"-"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
Models []ProviderModelInfo `json:"models"`
|
DefaultAPIType string `json:"defaultApiType"`
|
||||||
|
APITypes []ProviderAPIInfo `json:"apiTypes"`
|
||||||
|
Models []ProviderModelInfo `json:"models"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentFeishuConfigReq struct {
|
type AgentFeishuConfigReq struct {
|
||||||
@@ -582,6 +604,52 @@ type AgentPluginStatus struct {
|
|||||||
Upgradable bool `json:"upgradable"`
|
Upgradable bool `json:"upgradable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AgentPluginsReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginSearchReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
Keyword string `json:"keyword" validate:"required,max=100"`
|
||||||
|
Limit int `json:"limit" validate:"omitempty,min=1,max=100"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginMarketInstallReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
Package string `json:"package" validate:"required,max=200"`
|
||||||
|
Version string `json:"version" validate:"required,max=100"`
|
||||||
|
TaskID string `json:"taskID" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginOperateReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
PluginID string `json:"pluginId" validate:"required,max=200"`
|
||||||
|
Operate string `json:"operate" validate:"required,oneof=enable disable update uninstall"`
|
||||||
|
TaskID string `json:"taskID" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginSearchItem struct {
|
||||||
|
Package string `json:"package"`
|
||||||
|
PluginID string `json:"pluginId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
VerificationTier string `json:"verificationTier"`
|
||||||
|
Categories []string `json:"categories"`
|
||||||
|
Official bool `json:"official"`
|
||||||
|
Downloads int64 `json:"downloads"`
|
||||||
|
Score float64 `json:"score"`
|
||||||
|
}
|
||||||
|
|
||||||
type AgentDiscordConfigUpdateReq struct {
|
type AgentDiscordConfigUpdateReq struct {
|
||||||
AgentID uint `json:"agentId" validate:"required"`
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
@@ -662,9 +730,9 @@ type AgentSecurityConfig struct {
|
|||||||
|
|
||||||
type AgentOtherConfigUpdateReq struct {
|
type AgentOtherConfigUpdateReq struct {
|
||||||
AgentID uint `json:"agentId" validate:"required"`
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
UserTimezone string `json:"userTimezone" validate:"required"`
|
UserTimezone string `json:"userTimezone"`
|
||||||
BrowserEnabled bool `json:"browserEnabled"`
|
BrowserEnabled bool `json:"browserEnabled"`
|
||||||
NPMRegistry string `json:"npmRegistry" validate:"required"`
|
NPMRegistry string `json:"npmRegistry"`
|
||||||
DashboardUsername string `json:"dashboardUsername"`
|
DashboardUsername string `json:"dashboardUsername"`
|
||||||
DashboardPassword string `json:"dashboardPassword"`
|
DashboardPassword string `json:"dashboardPassword"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,8 +113,10 @@ type DiskDTO struct {
|
|||||||
|
|
||||||
type AlertLogSearch struct {
|
type AlertLogSearch struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
Count uint `json:"count"`
|
Count uint `json:"count"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
StartTime time.Time `json:"startTime"`
|
||||||
|
EndTime time.Time `json:"endTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertLogDTO struct {
|
type AlertLogDTO struct {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package dto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
)
|
)
|
||||||
@@ -133,6 +134,7 @@ type Locale struct {
|
|||||||
Tr string `json:"tr"`
|
Tr string `json:"tr"`
|
||||||
Es string `json:"es-es" yaml:"es-es"`
|
Es string `json:"es-es" yaml:"es-es"`
|
||||||
Fa string `json:"fa"`
|
Fa string `json:"fa"`
|
||||||
|
Lo string `json:"lo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppForm struct {
|
type AppForm struct {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ package dto
|
|||||||
|
|
||||||
type SearchWithPage struct {
|
type SearchWithPage struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
Info string `json:"info"`
|
Info string `json:"info"`
|
||||||
|
ExcludeAppStore bool `json:"excludeAppStore"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchPageWithType struct {
|
type SearchPageWithType struct {
|
||||||
|
|||||||
@@ -134,10 +134,15 @@ type ExtraHost struct {
|
|||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
}
|
}
|
||||||
type ContainerNetwork struct {
|
type ContainerNetwork struct {
|
||||||
Network string `json:"network"`
|
Network string `json:"network"`
|
||||||
Ipv4 string `json:"ipv4"`
|
Ipv4 string `json:"ipv4"`
|
||||||
Ipv6 string `json:"ipv6"`
|
Ipv6 string `json:"ipv6"`
|
||||||
MacAddr string `json:"macAddr"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContainerCreateByCommand struct {
|
type ContainerCreateByCommand struct {
|
||||||
@@ -295,6 +300,7 @@ type ComposeInfo struct {
|
|||||||
ConfigFile string `json:"configFile"`
|
ConfigFile string `json:"configFile"`
|
||||||
Workdir string `json:"workdir"`
|
Workdir string `json:"workdir"`
|
||||||
ComposeFileExists bool `json:"composeFileExists"`
|
ComposeFileExists bool `json:"composeFileExists"`
|
||||||
|
IsPinned bool `json:"isPinned"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Containers []ComposeContainer `json:"containers"`
|
Containers []ComposeContainer `json:"containers"`
|
||||||
Env string `json:"env"`
|
Env string `json:"env"`
|
||||||
@@ -309,6 +315,7 @@ type ComposeContainer struct {
|
|||||||
type ComposeCreate struct {
|
type ComposeCreate struct {
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
DirName string `json:"dirName"`
|
||||||
From string `json:"from" validate:"required,oneof=edit path template"`
|
From string `json:"from" validate:"required,oneof=edit path template"`
|
||||||
File string `json:"file"`
|
File string `json:"file"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
@@ -332,6 +339,10 @@ type ComposeUpdate struct {
|
|||||||
Env string `json:"env"`
|
Env string `json:"env"`
|
||||||
ForcePull bool `json:"forcePull"`
|
ForcePull bool `json:"forcePull"`
|
||||||
}
|
}
|
||||||
|
type ComposePin struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
IsPinned bool `json:"isPinned"`
|
||||||
|
}
|
||||||
type ComposeLogClean struct {
|
type ComposeLogClean struct {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Path string `json:"path" validate:"required"`
|
Path string `json:"path" validate:"required"`
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ type SearchRecord struct {
|
|||||||
|
|
||||||
type Record struct {
|
type Record struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
|
CronjobID uint `json:"cronjobID"`
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
StartTime string `json:"startTime"`
|
StartTime string `json:"startTime"`
|
||||||
Records string `json:"records"`
|
Records string `json:"records"`
|
||||||
|
|||||||
+69
-10
@@ -60,25 +60,84 @@ type MysqlDBCreate struct {
|
|||||||
Database string `json:"database" validate:"required"`
|
Database string `json:"database" validate:"required"`
|
||||||
Format string `json:"format" validate:"required"`
|
Format string `json:"format" validate:"required"`
|
||||||
Collation string `json:"collation"`
|
Collation string `json:"collation"`
|
||||||
Username string `json:"username" validate:"required"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password" validate:"required"`
|
Password string `json:"password"`
|
||||||
Permission string `json:"permission" validate:"required"`
|
Permission string `json:"permission" validate:"required"`
|
||||||
Description string `json:"description"`
|
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 {
|
type MysqlFormatCollationOption struct {
|
||||||
Format string `json:"format"`
|
Format string `json:"format"`
|
||||||
Collations []string `json:"collations"`
|
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 {
|
type MysqlLoadDB struct {
|
||||||
From string `json:"from" validate:"required,oneof=local remote"`
|
From string `json:"from" validate:"required,oneof=local remote"`
|
||||||
Type string `json:"type" validate:"required,oneof=mysql mariadb mysql-cluster"`
|
Type string `json:"type" validate:"required,oneof=mysql mariadb mysql-cluster"`
|
||||||
|
|||||||
@@ -35,19 +35,6 @@ type PortRuleOperate struct {
|
|||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForwardRuleOperate struct {
|
|
||||||
ForceDelete bool `json:"forceDelete"`
|
|
||||||
Rules []struct {
|
|
||||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
|
||||||
Num string `json:"num"`
|
|
||||||
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
|
|
||||||
Interface string `json:"interface"`
|
|
||||||
Port string `json:"port" validate:"required"`
|
|
||||||
TargetIP string `json:"targetIP"`
|
|
||||||
TargetPort string `json:"targetPort" validate:"required"`
|
|
||||||
} `json:"rules"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateFirewallDescription struct {
|
type UpdateFirewallDescription struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Chain string `json:"chain"`
|
Chain string `json:"chain"`
|
||||||
@@ -86,7 +73,7 @@ type BatchRuleOperate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IptablesOp struct {
|
type IptablesOp struct {
|
||||||
Name string `json:"name" validate:"required,oneof=1PANEL_INPUT 1PANEL_OUTPUT 1PANEL_BASIC"`
|
Name string `json:"name" validate:"required,oneof=1PANEL_INPUT 1PANEL_OUTPUT 1PANEL_BASIC 1PANEL_FORWARD"`
|
||||||
Operate string `json:"operate" validate:"required,oneof=init-base init-forward init-advance bind-base unbind-base bind unbind"`
|
Operate string `json:"operate" validate:"required,oneof=init-base init-forward init-advance bind-base unbind-base bind unbind"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type ForwardRuleSearch struct {
|
||||||
|
PageInfo
|
||||||
|
Info string `json:"info"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Strategy string `json:"strategy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForwardRule preserves the existing firewall search response shape while
|
||||||
|
// keeping forwarding data separate from the filter client model.
|
||||||
|
type ForwardRule struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Chain string `json:"chain"`
|
||||||
|
Family string `json:"family"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Port string `json:"port"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Strategy string `json:"strategy"`
|
||||||
|
|
||||||
|
Num string `json:"num"`
|
||||||
|
TargetIP string `json:"targetIP"`
|
||||||
|
TargetPort string `json:"targetPort"`
|
||||||
|
Interface string `json:"interface"`
|
||||||
|
|
||||||
|
UsedStatus string `json:"usedStatus"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardRuleOperate struct {
|
||||||
|
ForceDelete bool `json:"forceDelete"`
|
||||||
|
Rules []ForwardRuleOperation `json:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardRuleOperation struct {
|
||||||
|
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
||||||
|
Num string `json:"num"`
|
||||||
|
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
|
||||||
|
Interface string `json:"interface"`
|
||||||
|
Port string `json:"port" validate:"required"`
|
||||||
|
TargetIP string `json:"targetIP"`
|
||||||
|
TargetPort string `json:"targetPort" validate:"required"`
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,3 +16,37 @@ type SearchTaskLogReq struct {
|
|||||||
type TaskDTO struct {
|
type TaskDTO struct {
|
||||||
model.Task
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
+39
-5
@@ -1,6 +1,8 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
|
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
|
||||||
)
|
)
|
||||||
@@ -87,9 +89,41 @@ var LBAlgorithms = map[string]struct{}{"ip_hash": {}, "least_conn": {}}
|
|||||||
var RealIPKeys = map[string]struct{}{"X-Forwarded-For": {}, "X-Real-IP": {}, "CF-Connecting-IP": {}}
|
var RealIPKeys = map[string]struct{}{"X-Forwarded-For": {}, "X-Real-IP": {}, "CF-Connecting-IP": {}}
|
||||||
|
|
||||||
type NginxModule struct {
|
type NginxModule struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Script string `json:"script"`
|
Custom bool `json:"custom,omitempty"`
|
||||||
Packages []string `json:"packages"`
|
Script string `json:"script"`
|
||||||
Params string `json:"params"`
|
Packages []string `json:"packages"`
|
||||||
Enable bool `json:"enable"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type McpServerCreate struct {
|
|||||||
Type string `json:"type" validate:"required"`
|
Type string `json:"type" validate:"required"`
|
||||||
GatewayImage string `json:"gatewayImage"`
|
GatewayImage string `json:"gatewayImage"`
|
||||||
ProtocolVersion string `json:"protocolVersion"`
|
ProtocolVersion string `json:"protocolVersion"`
|
||||||
|
GatewayArgs string `json:"gatewayArgs" validate:"max=4096"`
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,17 +114,22 @@ type NginxRedirectUpdate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NginxBuildReq struct {
|
type NginxBuildReq struct {
|
||||||
TaskID string `json:"taskID" validate:"required"`
|
TaskID string `json:"taskID" validate:"required"`
|
||||||
Mirror string `json:"mirror" validate:"required"`
|
Mirror string `json:"mirror" validate:"required"`
|
||||||
|
Modules []string `json:"modules"`
|
||||||
|
Force bool `json:"force"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NginxModuleUpdate struct {
|
type NginxModuleUpdate struct {
|
||||||
Operate string `json:"operate" validate:"required,oneof=create delete update"`
|
Operate string `json:"operate" validate:"required,oneof=create delete update"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Script string `json:"script"`
|
Script string `json:"script"`
|
||||||
Packages string `json:"packages"`
|
Packages string `json:"packages"`
|
||||||
Enable bool `json:"enable"`
|
Enable bool `json:"enable"`
|
||||||
Params string `json:"params"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NginxOperateReq struct {
|
type NginxOperateReq struct {
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ type WebsiteCreate struct {
|
|||||||
|
|
||||||
SiteDir string `json:"siteDir"`
|
SiteDir string `json:"siteDir"`
|
||||||
|
|
||||||
|
TemplateOutputID uint `json:"templateOutputID"`
|
||||||
|
|
||||||
RuntimeConfig
|
RuntimeConfig
|
||||||
FtpConfig
|
FtpConfig
|
||||||
DataBaseConfig
|
DataBaseConfig
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebsiteTemplateSearch struct {
|
||||||
|
dto.PageInfo
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateCreate struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Type string `json:"type" validate:"required,oneof=single multi"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
Variables string `json:"variables"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateUpdate struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Type string `json:"type" validate:"required,oneof=single multi"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
Variables string `json:"variables"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateOutputSearch struct {
|
||||||
|
dto.PageInfo
|
||||||
|
TemplateID uint `json:"templateID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateOutputCreate struct {
|
||||||
|
TemplateID uint `json:"templateID" validate:"required"`
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
VariableValues map[string]string `json:"variableValues"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsitePreviewReq struct {
|
||||||
|
TemplateID uint `json:"templateID" validate:"required"`
|
||||||
|
VariableValues map[string]string `json:"variableValues"`
|
||||||
|
}
|
||||||
@@ -69,16 +69,25 @@ type NginxProxyCache struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NginxModule struct {
|
type NginxModule struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Script string `json:"script"`
|
Custom bool `json:"custom"`
|
||||||
Packages string `json:"packages"`
|
Script string `json:"script"`
|
||||||
Params string `json:"params"`
|
Packages string `json:"packages"`
|
||||||
Enable bool `json:"enable"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NginxBuildConfig struct {
|
type NginxBuildConfig struct {
|
||||||
Mirror string `json:"mirror"`
|
Mirror string `json:"mirror"`
|
||||||
Modules []NginxModule `json:"modules"`
|
DynamicSupported bool `json:"dynamicSupported"`
|
||||||
|
Modules []NginxModule `json:"modules"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NginxConfigRes struct {
|
type NginxConfigRes struct {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebsiteTemplateDTO struct {
|
||||||
|
model.WebsiteTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateOutputDTO struct {
|
||||||
|
model.WebsiteTemplateOutput
|
||||||
|
TemplateName string `json:"templateName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsitePreviewDTO struct {
|
||||||
|
HTML string `json:"html"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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"`
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ type AgentAccount struct {
|
|||||||
APIKey string `json:"apiKey"`
|
APIKey string `json:"apiKey"`
|
||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
APIType string `json:"apiType"`
|
APIType string `json:"apiType"`
|
||||||
|
AuthMode string `json:"authMode"`
|
||||||
|
VerifyModel string `json:"verifyModel"`
|
||||||
RememberAPIKey bool `json:"rememberApiKey"`
|
RememberAPIKey bool `json:"rememberApiKey"`
|
||||||
Verified bool `json:"verified"`
|
Verified bool `json:"verified"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
|
|||||||
@@ -2,14 +2,10 @@ package model
|
|||||||
|
|
||||||
type AgentAccountModel struct {
|
type AgentAccountModel struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
AccountID uint `json:"accountId" gorm:"index"`
|
AccountID uint `json:"accountId" gorm:"index"`
|
||||||
Model string `json:"model" gorm:"index"`
|
Model string `json:"model" gorm:"index"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ContextWindow int `json:"contextWindow"`
|
SortOrder int `json:"sortOrder" gorm:"index"`
|
||||||
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 {
|
func (AgentAccountModel) TableName() string {
|
||||||
|
|||||||
@@ -31,4 +31,5 @@ type BackupRecord struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
Args string `gorm:"not null;default:''" json:"args"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type ComposeTemplate struct {
|
|||||||
type Compose struct {
|
type Compose struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
|
IsPinned bool `json:"isPinned"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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"`
|
||||||
|
}
|
||||||
@@ -8,4 +8,6 @@ type Ftp struct {
|
|||||||
Status string `gorm:"not null" json:"status"`
|
Status string `gorm:"not null" json:"status"`
|
||||||
Path string `gorm:"not null" json:"path"`
|
Path string `gorm:"not null" json:"path"`
|
||||||
Description string `gorm:"not null" json:"description"`
|
Description string `gorm:"not null" json:"description"`
|
||||||
|
UID uint `gorm:"column:uid;not null;default:1000" json:"-"`
|
||||||
|
GID uint `gorm:"column:gid;not null;default:1000" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ type McpServer struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
GatewayImage string `json:"gatewayImage"`
|
GatewayImage string `json:"gatewayImage"`
|
||||||
ProtocolVersion string `json:"protocolVersion"`
|
ProtocolVersion string `json:"protocolVersion"`
|
||||||
|
GatewayArgs string `json:"gatewayArgs"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type WebsiteTemplate struct {
|
||||||
|
BaseModel
|
||||||
|
Name string `gorm:"not null" json:"name"`
|
||||||
|
Type string `gorm:"not null" json:"type"` // single | multi
|
||||||
|
Content string `gorm:"type:longtext" json:"content"`
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
Variables string `gorm:"type:text" json:"variables"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WebsiteTemplate) TableName() string {
|
||||||
|
return "website_templates"
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateOutput struct {
|
||||||
|
BaseModel
|
||||||
|
Name string `gorm:"not null" json:"name"`
|
||||||
|
TemplateID uint `gorm:"not null" json:"templateID"`
|
||||||
|
TemplateType string `json:"templateType"`
|
||||||
|
VariableValues string `gorm:"type:text" json:"variableValues"`
|
||||||
|
OutputPath string `json:"outputPath"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WebsiteTemplateOutput) TableName() string {
|
||||||
|
return "website_template_outputs"
|
||||||
|
}
|
||||||
+356
-324
@@ -1,318 +1,248 @@
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type APIConfig struct {
|
||||||
ID string
|
APIType string
|
||||||
Name string
|
BaseURL string
|
||||||
ContextWindow int
|
EditableBaseURL bool
|
||||||
MaxTokens int
|
DiscoverModels bool
|
||||||
Reasoning bool
|
DefaultAuthMode string
|
||||||
Input []string
|
AuthModes []string
|
||||||
|
Models []Model
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeDefault struct {
|
const (
|
||||||
APIType string
|
AuthModeBearer = "bearer"
|
||||||
ContextWindow int
|
AuthModeXAPIKey = "x-api-key"
|
||||||
MaxTokens int
|
)
|
||||||
Input []string
|
|
||||||
|
type Model struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Meta struct {
|
type Meta struct {
|
||||||
Key string
|
Key string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
|
DisplayNameKey string
|
||||||
Sort uint
|
Sort uint
|
||||||
DefaultBaseURL string
|
DefaultAPIType string
|
||||||
|
APIConfigs []APIConfig
|
||||||
EnvKey string
|
EnvKey string
|
||||||
Default RuntimeDefault
|
|
||||||
Models []Model
|
Models []Model
|
||||||
}
|
}
|
||||||
|
|
||||||
var catalog = map[string]Meta{
|
var catalog = map[string]Meta{
|
||||||
"custom": {
|
"custom": {
|
||||||
Key: "custom",
|
Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY",
|
||||||
DisplayName: "Custom",
|
APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images"),
|
||||||
Sort: 10,
|
|
||||||
DefaultBaseURL: "",
|
|
||||||
EnvKey: "CUSTOM_API_KEY",
|
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "openai-completions",
|
|
||||||
ContextWindow: 128000,
|
|
||||||
MaxTokens: 8192,
|
|
||||||
Input: []string{"text"},
|
|
||||||
},
|
|
||||||
Models: []Model{},
|
|
||||||
},
|
},
|
||||||
"ollama": {
|
"ollama": {
|
||||||
Key: "ollama",
|
Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses",
|
||||||
DisplayName: "Ollama",
|
APIConfigs: editableAPIConfigs(false, "openai-responses", "openai-completions"),
|
||||||
Sort: 15,
|
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "openai-responses",
|
|
||||||
ContextWindow: 160000,
|
|
||||||
MaxTokens: 8192,
|
|
||||||
Input: []string{"text"},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"vllm": {
|
"vllm": {
|
||||||
Key: "vllm",
|
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY",
|
||||||
DisplayName: "vLLM",
|
APIConfigs: editableAPIConfigs(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images"),
|
||||||
Sort: 20,
|
|
||||||
DefaultBaseURL: "",
|
|
||||||
EnvKey: "VLLM_API_KEY",
|
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "openai-completions",
|
|
||||||
ContextWindow: 128000,
|
|
||||||
MaxTokens: 8192,
|
|
||||||
Input: []string{"text"},
|
|
||||||
},
|
|
||||||
Models: []Model{},
|
|
||||||
},
|
},
|
||||||
"deepseek": {
|
"deepseek": {
|
||||||
Key: "deepseek",
|
Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY",
|
||||||
DisplayName: "DeepSeek",
|
APIConfigs: []APIConfig{
|
||||||
Sort: 25,
|
{APIType: "openai-completions", BaseURL: "https://api.deepseek.com"},
|
||||||
DefaultBaseURL: "https://api.deepseek.com/v1",
|
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
|
||||||
EnvKey: "DEEPSEEK_API_KEY",
|
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "openai-completions",
|
|
||||||
ContextWindow: 131072,
|
|
||||||
MaxTokens: 8192,
|
|
||||||
Input: []string{"text"},
|
|
||||||
},
|
|
||||||
Models: []Model{
|
|
||||||
{ID: "deepseek/deepseek-v4-flash", Name: "deepseek-v4-flash"},
|
|
||||||
{ID: "deepseek/deepseek-v4-pro", Name: "deepseek-v4-pro"},
|
|
||||||
},
|
},
|
||||||
|
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
||||||
},
|
},
|
||||||
"bailian-coding-plan": {
|
"bailian-coding-plan": {
|
||||||
Key: "bailian-coding-plan",
|
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
|
||||||
DisplayName: "阿里云百炼 Coding Plan",
|
APIConfigs: []APIConfig{
|
||||||
Sort: 30,
|
{APIType: "openai-completions", BaseURL: "https://coding.dashscope.aliyuncs.com/v1"},
|
||||||
DefaultBaseURL: "https://coding.dashscope.aliyuncs.com/v1",
|
anthropicAPIConfig("https://coding.dashscope.aliyuncs.com/apps/anthropic", AuthModeBearer),
|
||||||
EnvKey: "QWEN_API_KEY",
|
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "openai-completions",
|
|
||||||
ContextWindow: 256000,
|
|
||||||
MaxTokens: 8192,
|
|
||||||
Input: []string{"text"},
|
|
||||||
},
|
},
|
||||||
Models: []Model{
|
Models: []Model{
|
||||||
{ID: "bailian-coding-plan/qwen3.5-plus", Name: "Qwen3.5-Plus", Reasoning: true},
|
{ID: "qwen3-coder-plus", Name: "Qwen3-Coder-Plus"},
|
||||||
{ID: "bailian-coding-plan/qwen3-max", Name: "Qwen3-Max", Reasoning: true},
|
{ID: "qwen3-max-2026-01-23", Name: "Qwen3-Max-2026-01-23"},
|
||||||
{ID: "bailian-coding-plan/qwen3-coder-next", Name: "Qwen3-Coder-Next", Reasoning: true},
|
{ID: "qwen3-coder-next", Name: "Qwen3-Coder-Next"},
|
||||||
{ID: "bailian-coding-plan/qwen3-coder-plus", Name: "Qwen3-Coder-Plus", Reasoning: true},
|
{ID: "glm-4.7", Name: "GLM-4.7"},
|
||||||
{ID: "bailian-coding-plan/minimax-m2.5", Name: "MiniMax M2.5", Reasoning: true},
|
{ID: "kimi-k2.5", Name: "Kimi K2.5"},
|
||||||
{ID: "bailian-coding-plan/glm-5", Name: "GLM-5", Reasoning: true},
|
{ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"},
|
||||||
{ID: "bailian-coding-plan/kimi-k2.5", Name: "Kimi-k2.5", Reasoning: true},
|
{ID: "glm-5", Name: "GLM-5"},
|
||||||
{ID: "bailian-coding-plan/glm-4.7", Name: "GLM-4.7", Reasoning: true},
|
{ID: "MiniMax-M2.5", Name: "MiniMax M2.5"},
|
||||||
|
{ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"},
|
||||||
|
{ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"ark-coding-plan": {
|
"ark-coding-plan": {
|
||||||
Key: "ark-coding-plan",
|
Key: "ark-coding-plan", DisplayNameKey: "AIProviderArkCodingPlan", Sort: 35, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY",
|
||||||
DisplayName: "方舟 Coding Plan",
|
APIConfigs: []APIConfig{
|
||||||
Sort: 35,
|
{APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3"},
|
||||||
DefaultBaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3",
|
anthropicAPIConfig("https://ark.cn-beijing.volces.com/api/coding", AuthModeBearer),
|
||||||
EnvKey: "ARK_API_KEY",
|
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "openai-completions",
|
|
||||||
ContextWindow: 256000,
|
|
||||||
MaxTokens: 4096,
|
|
||||||
Input: []string{"text"},
|
|
||||||
},
|
},
|
||||||
Models: []Model{
|
Models: []Model{
|
||||||
{ID: "ark-coding-plan/ark-code-latest", Name: "Ark Coding Plan", ContextWindow: 256000, MaxTokens: 4096},
|
{ID: "ark-code-latest", Name: "Ark Coding Plan"}, {ID: "doubao-seed-code", Name: "Doubao Seed Code"},
|
||||||
{ID: "ark-coding-plan/doubao-seed-code", Name: "Doubao Seed Code", ContextWindow: 256000, MaxTokens: 4096},
|
{ID: "glm-4.7", Name: "GLM 4.7 Coding"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"},
|
||||||
{ID: "ark-coding-plan/glm-4.7", Name: "GLM 4.7 Coding", ContextWindow: 200000, MaxTokens: 4096},
|
{ID: "kimi-k2.5", Name: "Kimi K2.5 Coding"}, {ID: "doubao-seed-code-preview-251028", Name: "Doubao Seed Code Preview"},
|
||||||
{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": {
|
"zai": {
|
||||||
Key: "zai",
|
Key: "zai", DisplayName: "Z.ai", Sort: 40, DefaultAPIType: "openai-completions", EnvKey: "ZAI_API_KEY",
|
||||||
DisplayName: "Z.ai",
|
APIConfigs: []APIConfig{
|
||||||
Sort: 40,
|
{APIType: "openai-completions", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true},
|
||||||
DefaultBaseURL: "https://open.bigmodel.cn/api/paas/v4",
|
{APIType: "openai-images", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true},
|
||||||
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},
|
|
||||||
},
|
},
|
||||||
|
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"}},
|
||||||
},
|
},
|
||||||
"minimax": {
|
"minimax": {
|
||||||
Key: "minimax",
|
Key: "minimax", DisplayName: "MiniMax (CN)", Sort: 45, DefaultAPIType: "anthropic-messages", EnvKey: "MINIMAX_API_KEY",
|
||||||
DisplayName: "MiniMax (CN)",
|
APIConfigs: []APIConfig{
|
||||||
Sort: 45,
|
anthropicAPIConfig("https://api.minimaxi.com/anthropic", AuthModeXAPIKey, AuthModeBearer),
|
||||||
DefaultBaseURL: "https://api.minimaxi.com/anthropic",
|
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
|
||||||
EnvKey: "MINIMAX_API_KEY",
|
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"},
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "anthropic-messages",
|
|
||||||
ContextWindow: 200000,
|
|
||||||
MaxTokens: 8192,
|
|
||||||
Input: []string{"text"},
|
|
||||||
},
|
|
||||||
Models: []Model{
|
|
||||||
{ID: "minimax/MiniMax-M3", Name: "MiniMax M3", ContextWindow: 1000000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
|
|
||||||
{ID: "minimax/MiniMax-M2.7", Name: "MiniMax M2.7"},
|
|
||||||
{ID: "minimax/MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 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": {
|
"xiaomi": {
|
||||||
Key: "xiaomi",
|
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
|
||||||
DisplayName: "Xiaomi",
|
APIConfigs: []APIConfig{
|
||||||
Sort: 46,
|
{APIType: "openai-completions", BaseURL: "https://api.xiaomimimo.com/v1"},
|
||||||
DefaultBaseURL: "https://api.xiaomimimo.com/v1",
|
{APIType: "openai-responses", BaseURL: "https://api.xiaomimimo.com/v1"},
|
||||||
EnvKey: "XIAOMI_API_KEY",
|
anthropicAPIConfig("https://api.xiaomimimo.com/anthropic", AuthModeBearer),
|
||||||
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.5", Name: "Xiaomi MiMo V2.5"}, {ID: "mimo-v2.5-pro", Name: "Xiaomi MiMo V2.5 Pro"}},
|
||||||
},
|
},
|
||||||
"kimi": {
|
"kimi": {
|
||||||
Key: "kimi",
|
Key: "kimi", DisplayName: "Kimi (CN)", Sort: 50, DefaultAPIType: "openai-completions", EnvKey: "KIMI_API_KEY",
|
||||||
DisplayName: "Kimi (CN)",
|
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.cn/v1"}},
|
||||||
Sort: 50,
|
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"}},
|
||||||
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": {
|
"kimi-coding": {
|
||||||
Key: "kimi-coding",
|
Key: "kimi-coding", DisplayName: "Kimi Coding", Sort: 51, DefaultAPIType: "anthropic-messages", EnvKey: "KIMI_API_KEY",
|
||||||
DisplayName: "Kimi Coding",
|
APIConfigs: []APIConfig{anthropicAPIConfig("https://api.kimi.com/coding/", AuthModeXAPIKey)},
|
||||||
Sort: 51,
|
Models: []Model{{ID: "kimi-code", Name: "Kimi Code"}, {ID: "k2p5", Name: "Kimi K2.5"}},
|
||||||
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": {
|
"openai": {
|
||||||
Key: "openai",
|
Key: "openai", DisplayName: "OpenAI", Sort: 55, DefaultAPIType: "openai-responses", EnvKey: "OPENAI_API_KEY",
|
||||||
DisplayName: "OpenAI",
|
APIConfigs: []APIConfig{
|
||||||
Sort: 55,
|
{APIType: "openai-responses", BaseURL: "https://api.openai.com/v1"},
|
||||||
DefaultBaseURL: "https://api.openai.com/v1",
|
{APIType: "openai-completions", BaseURL: "https://api.openai.com/v1"},
|
||||||
EnvKey: "OPENAI_API_KEY",
|
{APIType: "openai-images", BaseURL: "https://api.openai.com/v1"},
|
||||||
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": {
|
"openrouter": {
|
||||||
Key: "openrouter",
|
Key: "openrouter", DisplayName: "OpenRouter", Sort: 56, DefaultAPIType: "openai-completions", EnvKey: "OPENROUTER_API_KEY",
|
||||||
DisplayName: "OpenRouter",
|
APIConfigs: []APIConfig{
|
||||||
Sort: 56,
|
{APIType: "openai-completions", BaseURL: "https://openrouter.ai/api/v1"},
|
||||||
DefaultBaseURL: "https://openrouter.ai/api/v1",
|
{APIType: "openrouter-images", BaseURL: "https://openrouter.ai"},
|
||||||
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"},
|
|
||||||
},
|
},
|
||||||
|
Models: []Model{{ID: "openrouter/free", Name: "openrouter/free"}, {ID: "openrouter/auto", Name: "openrouter/auto"}},
|
||||||
},
|
},
|
||||||
"anthropic": {
|
"anthropic": {
|
||||||
Key: "anthropic",
|
Key: "anthropic", DisplayName: "Anthropic", Sort: 60, DefaultAPIType: "anthropic-messages", EnvKey: "ANTHROPIC_API_KEY",
|
||||||
DisplayName: "Anthropic",
|
APIConfigs: []APIConfig{anthropicAPIConfig("https://api.anthropic.com", AuthModeXAPIKey)},
|
||||||
Sort: 60,
|
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"}},
|
||||||
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": {
|
"gemini": {
|
||||||
Key: "gemini",
|
Key: "gemini", DisplayName: "Gemini", Sort: 65, DefaultAPIType: "gemini-generate-content", EnvKey: "GEMINI_API_KEY",
|
||||||
DisplayName: "Gemini",
|
APIConfigs: []APIConfig{{APIType: "gemini-generate-content", BaseURL: "https://generativelanguage.googleapis.com"}},
|
||||||
Sort: 65,
|
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"}},
|
||||||
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": {
|
"moonshot": {
|
||||||
Key: "moonshot",
|
Key: "moonshot", DisplayName: "Moonshot (Global)", Sort: 70, DefaultAPIType: "openai-completions", EnvKey: "MOONSHOT_API_KEY",
|
||||||
DisplayName: "Moonshot (Global)",
|
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.ai/v1"}},
|
||||||
Sort: 70,
|
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"}},
|
||||||
DefaultBaseURL: "https://api.moonshot.ai/v1",
|
},
|
||||||
EnvKey: "MOONSHOT_API_KEY",
|
"bailian": {
|
||||||
Default: RuntimeDefault{
|
Key: "bailian", DisplayNameKey: "AIProviderBailian", Sort: 31, DefaultAPIType: "openai-completions", EnvKey: "DASHSCOPE_API_KEY",
|
||||||
APIType: "openai-completions",
|
APIConfigs: []APIConfig{
|
||||||
ContextWindow: 256000,
|
{
|
||||||
MaxTokens: 8192,
|
APIType: "openai-completions", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||||
Input: []string{"text"},
|
DiscoverModels: true,
|
||||||
},
|
Models: []Model{{ID: "qwen3.7-plus", Name: "qwen3.7-plus"}, {ID: "qwen3.6-plus", Name: "qwen3.6-plus"}, {ID: "qwen3.6-flash", Name: "qwen3.6-flash"}},
|
||||||
Models: []Model{
|
},
|
||||||
{ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true},
|
{
|
||||||
{ID: "moonshot/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
|
APIType: "openai-responses", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||||
{ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true},
|
DiscoverModels: true,
|
||||||
|
Models: []Model{{ID: "qwen3.7-plus", Name: "qwen3.7-plus"}, {ID: "qwen3.6-plus", Name: "qwen3.6-plus"}, {ID: "qwen3.6-flash", Name: "qwen3.6-flash"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIType: "anthropic-messages", BaseURL: "https://dashscope.aliyuncs.com/apps/anthropic",
|
||||||
|
DefaultAuthMode: AuthModeBearer,
|
||||||
|
AuthModes: []string{AuthModeBearer},
|
||||||
|
Models: []Model{{ID: "qwen3.7-plus", Name: "qwen3.7-plus"}, {ID: "qwen3.6-plus", Name: "qwen3.6-plus"}, {ID: "qwen3.6-flash", Name: "qwen3.6-flash"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIType: "dashscope-images", BaseURL: "https://dashscope.aliyuncs.com",
|
||||||
|
Models: []Model{
|
||||||
|
{ID: "qwen-image-2.0-pro", Name: "qwen-image-2.0-pro"},
|
||||||
|
{ID: "qwen-image-2.0", Name: "qwen-image-2.0"},
|
||||||
|
{ID: "wan2.7-image-pro", Name: "wan2.7-image-pro"},
|
||||||
|
{ID: "wan2.7-image", Name: "wan2.7-image"},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"ark": {
|
||||||
|
Key: "ark", DisplayNameKey: "AIProviderArk", Sort: 36, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY",
|
||||||
|
APIConfigs: []APIConfig{
|
||||||
|
{
|
||||||
|
APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
|
||||||
|
DiscoverModels: true,
|
||||||
|
Models: []Model{{ID: "doubao-seed-2-0-pro-260215", Name: "doubao-seed-2-0-pro-260215"}, {ID: "doubao-seed-2-0-lite-260215", Name: "doubao-seed-2-0-lite-260215"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIType: "openai-responses", BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
|
||||||
|
DiscoverModels: true,
|
||||||
|
Models: []Model{{ID: "doubao-seed-2-0-pro-260215", Name: "doubao-seed-2-0-pro-260215"}, {ID: "doubao-seed-2-0-lite-260215", Name: "doubao-seed-2-0-lite-260215"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIType: "openai-images", BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
|
||||||
|
Models: []Model{
|
||||||
|
{ID: "doubao-seedream-5-0-260128", Name: "doubao-seedream-5-0-260128"},
|
||||||
|
{ID: "doubao-seedream-5-0-lite-260128", Name: "doubao-seedream-5-0-lite-260128"},
|
||||||
|
{ID: "doubao-seedream-4-5-251128", Name: "doubao-seedream-4-5-251128"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func editableAPIConfigs(discoverModels bool, 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,
|
||||||
|
DiscoverModels: discoverModels && (apiType == "openai-completions" || apiType == "openai-responses"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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) {
|
func Get(key string) (Meta, bool) {
|
||||||
@@ -331,12 +261,142 @@ func All() map[string]Meta {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultBaseURL(key string) (string, bool) {
|
func FindAPIConfig(key, apiType string) (APIConfig, bool) {
|
||||||
meta, ok := catalog[key]
|
meta, ok := catalog[key]
|
||||||
if !ok || strings.TrimSpace(meta.DefaultBaseURL) == "" {
|
if !ok {
|
||||||
|
return APIConfig{}, false
|
||||||
|
}
|
||||||
|
target := strings.TrimSpace(apiType)
|
||||||
|
if target == "" {
|
||||||
|
target = meta.DefaultAPIType
|
||||||
|
}
|
||||||
|
for _, config := range meta.APIConfigs {
|
||||||
|
if config.APIType == target {
|
||||||
|
config.AuthModes = append([]string(nil), config.AuthModes...)
|
||||||
|
config.Models = append([]Model(nil), config.Models...)
|
||||||
|
return config, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return APIConfig{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultModels(key, apiType string) []Model {
|
||||||
|
meta, ok := catalog[key]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
target := strings.TrimSpace(apiType)
|
||||||
|
if target == "" {
|
||||||
|
target = meta.DefaultAPIType
|
||||||
|
}
|
||||||
|
for _, config := range meta.APIConfigs {
|
||||||
|
if config.APIType != target {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(config.Models) > 0 {
|
||||||
|
return append([]Model(nil), config.Models...)
|
||||||
|
}
|
||||||
|
if IsImageAPIType(config.APIType) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return append([]Model(nil), meta.Models...)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) == "" {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
return meta.DefaultBaseURL, true
|
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")
|
||||||
|
}
|
||||||
|
if key == "custom" && IsImageAPIType(config.APIType) {
|
||||||
|
return baseURL, nil
|
||||||
|
}
|
||||||
|
parsed.Path = normalizeEndpointPath(config.APIType, parsed.Path)
|
||||||
|
parsed.RawQuery = ""
|
||||||
|
parsed.Fragment = ""
|
||||||
|
return strings.TrimRight(parsed.String(), "/"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeEndpointPath(apiType, value string) string {
|
||||||
|
if IsImageAPIType(apiType) {
|
||||||
|
return strings.TrimRight(value, "/")
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsImageAPIType(apiType string) bool {
|
||||||
|
switch apiType {
|
||||||
|
case "openai-images", "dashscope-images", "minimax-images", "openrouter-images":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnvKey(key string) string {
|
func EnvKey(key string) string {
|
||||||
@@ -349,89 +409,61 @@ func EnvKey(key string) string {
|
|||||||
|
|
||||||
func DisplayName(key string) string {
|
func DisplayName(key string) string {
|
||||||
meta, ok := catalog[key]
|
meta, ok := catalog[key]
|
||||||
if !ok {
|
if !ok || strings.TrimSpace(meta.DisplayName) == "" {
|
||||||
return key
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(meta.DisplayName) == "" {
|
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
return meta.DisplayName
|
return meta.DisplayName
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindModel(key, modelID string) (Model, bool) {
|
func DisplayNameKey(key string) string {
|
||||||
meta, ok := Get(key)
|
meta, ok := catalog[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
return Model{}, false
|
return ""
|
||||||
}
|
}
|
||||||
for _, item := range meta.Models {
|
return meta.DisplayNameKey
|
||||||
if item.ID == modelID {
|
}
|
||||||
return item, true
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Model{}, false
|
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"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneMeta(meta Meta) Meta {
|
func cloneMeta(meta Meta) Meta {
|
||||||
clone := meta
|
clone := meta
|
||||||
if len(meta.Default.Input) > 0 {
|
clone.APIConfigs = make([]APIConfig, len(meta.APIConfigs))
|
||||||
clone.Default.Input = make([]string, len(meta.Default.Input))
|
for index, config := range meta.APIConfigs {
|
||||||
copy(clone.Default.Input, meta.Default.Input)
|
clone.APIConfigs[index] = config
|
||||||
}
|
clone.APIConfigs[index].AuthModes = append([]string(nil), config.AuthModes...)
|
||||||
if len(meta.Models) > 0 {
|
clone.APIConfigs[index].Models = append([]Model(nil), config.Models...)
|
||||||
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
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"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, "/models"):
|
||||||
|
return base
|
||||||
|
case hasAPIVersionSuffix(base):
|
||||||
|
return base + "/models"
|
||||||
|
default:
|
||||||
|
return base + "/v1/models"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasAPIVersionSuffix(value string) bool {
|
||||||
|
segment := value[strings.LastIndex(value, "/")+1:]
|
||||||
|
if len(segment) < 2 || segment[0] != 'v' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err := strconv.Atoi(segment[1:])
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
@@ -15,72 +15,52 @@ type OpenClawProviderPatch struct {
|
|||||||
AuthHeader bool
|
AuthHeader bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildOpenClawProviderPatch(provider, modelName, apiType, baseURL, apiKey string) (*OpenClawProviderPatch, error) {
|
func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL, apiKey string) (*OpenClawProviderPatch, error) {
|
||||||
if modelName == "" {
|
if modelName == "" {
|
||||||
return nil, fmt.Errorf("model is required")
|
return nil, fmt.Errorf("model is required")
|
||||||
}
|
}
|
||||||
resolvedAPIType, _, _ := ResolveRuntimeParams(provider, apiType, 0, 0)
|
resolvedAPIType := apiType
|
||||||
modelID := resolveOpenClawModelID(provider, modelName)
|
if _, ok := FindAPIConfig(provider, resolvedAPIType); !ok {
|
||||||
switch provider {
|
resolvedAPIType = DefaultAPIType(provider)
|
||||||
case "deepseek":
|
|
||||||
return newOpenClawProviderPatch(modelName, "deepseek", modelID, apiKey, baseURL, "openai-completions", false), nil
|
|
||||||
case "gemini":
|
|
||||||
return newOpenClawProviderPatch("google/"+modelID, "google", modelID, apiKey, baseURL, resolvedAPIType, false), nil
|
|
||||||
case "moonshot", "kimi":
|
|
||||||
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":
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
if IsImageAPIType(resolvedAPIType) {
|
||||||
|
return nil, fmt.Errorf("api type %s does not support text generation", resolvedAPIType)
|
||||||
func buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey string) *OpenClawProviderPatch {
|
}
|
||||||
|
resolvedAuthMode, err := ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
usesBearer := resolvedAuthMode == AuthModeBearer
|
||||||
|
modelID := NormalizeModelID(provider, modelName)
|
||||||
providerKey := provider
|
providerKey := provider
|
||||||
primaryModel := modelName
|
preserveQualifiedModel := false
|
||||||
if provider == "kimi" {
|
switch provider {
|
||||||
|
case "gemini":
|
||||||
|
providerKey = "google"
|
||||||
|
resolvedAPIType = "google-generative-ai"
|
||||||
|
usesBearer = false
|
||||||
|
case "moonshot", "kimi":
|
||||||
providerKey = "moonshot"
|
providerKey = "moonshot"
|
||||||
primaryModel = "moonshot/" + modelID
|
resolvedAPIType = "openai-completions"
|
||||||
|
usesBearer = false
|
||||||
|
case "ollama":
|
||||||
|
apiKey = "ollama"
|
||||||
|
usesBearer = false
|
||||||
|
case "openai", "openrouter", "anthropic":
|
||||||
|
preserveQualifiedModel = strings.Contains(modelName, "/")
|
||||||
}
|
}
|
||||||
return newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, "openai-completions", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, apiType string, authHeader bool) *OpenClawProviderPatch {
|
primaryModel := providerKey + "/" + modelID
|
||||||
|
if preserveQualifiedModel {
|
||||||
|
primaryModel = modelName
|
||||||
|
}
|
||||||
return &OpenClawProviderPatch{
|
return &OpenClawProviderPatch{
|
||||||
PrimaryModel: primaryModel,
|
PrimaryModel: primaryModel,
|
||||||
ProviderKey: providerKey,
|
ProviderKey: providerKey,
|
||||||
ModelID: modelID,
|
ModelID: modelID,
|
||||||
APIKey: apiKey,
|
APIKey: apiKey,
|
||||||
BaseURL: baseURL,
|
BaseURL: baseURL,
|
||||||
APIType: apiType,
|
APIType: resolvedAPIType,
|
||||||
AuthHeader: authHeader,
|
AuthHeader: usesBearer,
|
||||||
}
|
}, nil
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-118
@@ -3,7 +3,9 @@ package provider
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -18,160 +20,140 @@ type VerifyRequest struct {
|
|||||||
Body []byte
|
Body []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
type verifyErrorResponse struct {
|
||||||
defaultVerifyTimeout = 30 * time.Second
|
Error struct {
|
||||||
)
|
Message string `json:"message"`
|
||||||
|
} `json:"error"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
func SkipVerification(key string) bool {
|
const defaultVerifyTimeout = 30 * time.Second
|
||||||
switch key {
|
|
||||||
case "custom", "vllm", "ollama", "kimi-coding":
|
func SkipVerification(provider string) bool {
|
||||||
|
switch provider {
|
||||||
|
case "vllm", "ollama", "kimi-coding":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func VerifyAccount(provider, baseURL, apiKey string) error {
|
func VerifyAccount(provider, apiType, authMode, baseURL, apiKey, model string) error {
|
||||||
req := BuildVerifyRequest(provider, baseURL, apiKey)
|
req := BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model)
|
||||||
var body *bytes.Buffer
|
httpReq, err := http.NewRequest(req.Method, req.URL, bytes.NewReader(req.Body))
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for key, value := range req.Headers {
|
for key, value := range req.Headers {
|
||||||
httpReq.Header.Set(key, value)
|
httpReq.Header.Set(key, value)
|
||||||
}
|
}
|
||||||
resp, err := (&http.Client{Timeout: verifyTimeout()}).Do(httpReq)
|
httpReq.Header.Set("Accept", "application/json")
|
||||||
|
resp, err := (&http.Client{Timeout: defaultVerifyTimeout}).Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return buserr.WithErr("ErrAgentAccountUnavailable", err)
|
return buserr.WithErr("ErrAgentAccountUnavailable", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||||
return buserr.WithErr("ErrAgentAccountUnavailable", fmt.Errorf("verify failed: %s", resp.Status))
|
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)))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyTimeout() time.Duration {
|
func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model string) VerifyRequest {
|
||||||
return defaultVerifyTimeout
|
baseURL = strings.TrimSpace(baseURL)
|
||||||
}
|
if provider != "custom" || !IsImageAPIType(apiType) {
|
||||||
|
baseURL = strings.TrimRight(baseURL, "/")
|
||||||
|
}
|
||||||
|
headers := map[string]string{"Content-Type": "application/json"}
|
||||||
|
request := VerifyRequest{Method: http.MethodPost, Headers: headers}
|
||||||
|
|
||||||
func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest {
|
if provider == "gemini" {
|
||||||
base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
request.URL = baseURL + "/v1beta/models/" + strings.TrimSpace(model) + ":generateContent"
|
||||||
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["x-goog-api-key"] = apiKey
|
||||||
headers["Content-Type"] = "application/json"
|
|
||||||
request.Body = mustJSON(map[string]interface{}{
|
request.Body = mustJSON(map[string]interface{}{
|
||||||
"contents": []map[string]interface{}{{
|
"contents": []map[string]interface{}{{"parts": []map[string]string{{"text": "test"}}}},
|
||||||
"parts": []map[string]string{{
|
})
|
||||||
"text": "Explain how AI works in a few words",
|
return request
|
||||||
}},
|
}
|
||||||
|
|
||||||
|
switch apiType {
|
||||||
|
case "openai-images":
|
||||||
|
request.URL = imageVerifyURL(provider, baseURL, "/images/generations")
|
||||||
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
|
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
|
||||||
|
case "dashscope-images":
|
||||||
|
request.URL = imageVerifyURL(provider, baseURL, "/api/v1/services/aigc/multimodal-generation/generation")
|
||||||
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
|
request.Body = mustJSON(map[string]interface{}{
|
||||||
|
"model": model,
|
||||||
|
"input": map[string]interface{}{"messages": []map[string]interface{}{
|
||||||
|
{"role": "user", "content": []map[string]string{{"text": "test"}}},
|
||||||
}},
|
}},
|
||||||
|
"parameters": map[string]interface{}{"n": 1},
|
||||||
})
|
})
|
||||||
case "zai":
|
case "minimax-images":
|
||||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
request.URL = imageVerifyURL(provider, baseURL, "/v1/image_generation")
|
||||||
request.URL = base + "/models"
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
case "bailian-coding-plan":
|
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
|
||||||
request.Method = http.MethodPost
|
case "openrouter-images":
|
||||||
if !strings.Contains(base, "/v1") {
|
request.URL = imageVerifyURL(provider, baseURL, "/api/v1/images")
|
||||||
base = base + "/v1"
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
|
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
|
||||||
|
case "anthropic-messages":
|
||||||
|
request.URL = baseURL + "/v1/messages"
|
||||||
|
if authMode == AuthModeBearer {
|
||||||
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
|
} else {
|
||||||
|
headers["x-api-key"] = apiKey
|
||||||
}
|
}
|
||||||
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["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{}{
|
request.Body = mustJSON(map[string]interface{}{
|
||||||
"model": "MiniMax-M3",
|
"model": model, "max_tokens": 1, "stream": false,
|
||||||
"max_tokens": 1,
|
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
|
||||||
"messages": []map[string]interface{}{{
|
|
||||||
"role": "user",
|
|
||||||
"content": []map[string]string{{
|
|
||||||
"type": "text",
|
|
||||||
"text": "test",
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
})
|
})
|
||||||
case "xiaomi":
|
case "openai-responses":
|
||||||
request.Method = http.MethodPost
|
request.URL = baseURL + "/responses"
|
||||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
headers["Content-Type"] = "application/json"
|
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": 1, "stream": false})
|
||||||
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:
|
default:
|
||||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
request.URL = baseURL + "/chat/completions"
|
||||||
if strings.Contains(base, "/v1") {
|
if provider != "ollama" || strings.TrimSpace(apiKey) != "" {
|
||||||
request.URL = base + "/models"
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
} else {
|
|
||||||
request.URL = base + "/v1/models"
|
|
||||||
}
|
}
|
||||||
|
request.Body = mustJSON(map[string]interface{}{
|
||||||
|
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": 1, "stream": false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func imageVerifyURL(provider, baseURL, endpoint string) string {
|
||||||
|
if provider == "custom" || strings.HasSuffix(strings.ToLower(baseURL), endpoint) {
|
||||||
|
return baseURL
|
||||||
|
}
|
||||||
|
return baseURL + endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if message == "" {
|
||||||
|
return fmt.Sprintf("validation request returned status %d", statusCode)
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
func mustJSON(value interface{}) []byte {
|
func mustJSON(value interface{}) []byte {
|
||||||
payload, err := json.Marshal(value)
|
payload, err := json.Marshal(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type IAgentAccountRepo interface {
|
|||||||
Save(account *model.AgentAccount) error
|
Save(account *model.AgentAccount) error
|
||||||
DeleteByID(id uint) error
|
DeleteByID(id uint) error
|
||||||
List(opts ...DBOption) ([]model.AgentAccount, error)
|
List(opts ...DBOption) ([]model.AgentAccount, error)
|
||||||
CountByProviders(providers []string) (map[string]int64, error)
|
CountTextByProviders(providers []string) (map[string]int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAgentAccountRepo() IAgentAccountRepo {
|
func NewIAgentAccountRepo() IAgentAccountRepo {
|
||||||
@@ -67,7 +67,7 @@ func (a AgentAccountRepo) List(opts ...DBOption) ([]model.AgentAccount, error) {
|
|||||||
return accounts, nil
|
return accounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentAccountRepo) CountByProviders(providers []string) (map[string]int64, error) {
|
func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]int64, error) {
|
||||||
normalizedProviders := normalizeProviders(providers)
|
normalizedProviders := normalizeProviders(providers)
|
||||||
counts := make(map[string]int64, len(normalizedProviders))
|
counts := make(map[string]int64, len(normalizedProviders))
|
||||||
for _, provider := range normalizedProviders {
|
for _, provider := range normalizedProviders {
|
||||||
@@ -86,6 +86,7 @@ func (a AgentAccountRepo) CountByProviders(providers []string) (map[string]int64
|
|||||||
Model(&model.AgentAccount{}).
|
Model(&model.AgentAccount{}).
|
||||||
Select("provider, COUNT(*) as count").
|
Select("provider, COUNT(*) as count").
|
||||||
Where("provider IN ?", normalizedProviders).
|
Where("provider IN ?", normalizedProviders).
|
||||||
|
Where("api_type NOT LIKE ?", "%-images").
|
||||||
Group("provider").
|
Group("provider").
|
||||||
Scan(&rows).Error; err != nil {
|
Scan(&rows).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type IAppInstallResourceRpo interface {
|
|||||||
WithAppInstallId(appInstallId uint) DBOption
|
WithAppInstallId(appInstallId uint) DBOption
|
||||||
WithLinkId(linkId uint) DBOption
|
WithLinkId(linkId uint) DBOption
|
||||||
WithResourceId(resourceId uint) DBOption
|
WithResourceId(resourceId uint) DBOption
|
||||||
|
WithResourceIds(resourceIds []uint) DBOption
|
||||||
GetBy(opts ...DBOption) ([]model.AppInstallResource, error)
|
GetBy(opts ...DBOption) ([]model.AppInstallResource, error)
|
||||||
GetFirst(opts ...DBOption) (model.AppInstallResource, error)
|
GetFirst(opts ...DBOption) (model.AppInstallResource, error)
|
||||||
Create(ctx context.Context, resource *model.AppInstallResource) error
|
Create(ctx context.Context, resource *model.AppInstallResource) error
|
||||||
@@ -44,6 +45,12 @@ 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) {
|
func (a AppInstallResourceRpo) GetBy(opts ...DBOption) ([]model.AppInstallResource, error) {
|
||||||
db := global.DB.Model(&model.AppInstallResource{})
|
db := global.DB.Model(&model.AppInstallResource{})
|
||||||
var resources []model.AppInstallResource
|
var resources []model.AppInstallResource
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ func WithByName(name string) DBOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithByPath(path string) DBOption {
|
||||||
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
|
return g.Where("path = ?", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithByAddr(addr string) DBOption {
|
func WithByAddr(addr string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
return g.Where("addr = ?", addr)
|
return g.Where("addr = ?", addr)
|
||||||
@@ -94,6 +100,18 @@ func WithByProvider(provider string) DBOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithByAPIType(apiType string) DBOption {
|
||||||
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
|
return g.Where("api_type = ?", apiType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTextAPIType() DBOption {
|
||||||
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
|
return g.Where("api_type NOT LIKE ?", "%-images")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithByModel(model string) DBOption {
|
func WithByModel(model string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
if len(model) == 0 {
|
if len(model) == 0 {
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ type IWebsiteRepo interface {
|
|||||||
WithParentID(websiteID uint) DBOption
|
WithParentID(websiteID uint) DBOption
|
||||||
WithType(websiteType string) DBOption
|
WithType(websiteType string) DBOption
|
||||||
WithDBType(dbType string) DBOption
|
WithDBType(dbType string) DBOption
|
||||||
|
WithDBTypes(dbTypes []string) DBOption
|
||||||
WithDBID(dbID uint) DBOption
|
WithDBID(dbID uint) DBOption
|
||||||
|
|
||||||
Page(page, size int, opts ...DBOption) (int64, []model.Website, error)
|
Page(page, size int, opts ...DBOption) (int64, []model.Website, error)
|
||||||
@@ -132,6 +133,12 @@ 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 {
|
func (w *WebsiteRepo) WithDBID(dbID uint) DBOption {
|
||||||
return func(db *gorm.DB) *gorm.DB {
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
return db.Where("db_id = ?", dbID)
|
return db.Where("db_id = ?", dbID)
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IWebsiteTemplateRepo interface {
|
||||||
|
Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplate, error)
|
||||||
|
GetFirst(opts ...DBOption) (*model.WebsiteTemplate, error)
|
||||||
|
List(opts ...DBOption) ([]model.WebsiteTemplate, error)
|
||||||
|
Create(template *model.WebsiteTemplate) error
|
||||||
|
Save(template *model.WebsiteTemplate) error
|
||||||
|
DeleteBy(opts ...DBOption) error
|
||||||
|
WithName(name string) DBOption
|
||||||
|
WithType(templateType string) DBOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIWebsiteTemplateRepo() IWebsiteTemplateRepo {
|
||||||
|
return &WebsiteTemplateRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateRepo struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateRepo) WithName(name string) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("name like ?", "%"+name+"%")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateRepo) WithType(templateType string) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("type = ?", templateType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplate, error) {
|
||||||
|
var templates []model.WebsiteTemplate
|
||||||
|
db := getDb(opts...).Model(&model.WebsiteTemplate{})
|
||||||
|
count := int64(0)
|
||||||
|
db = db.Count(&count)
|
||||||
|
err := db.Limit(size).Offset(size * (page - 1)).Find(&templates).Error
|
||||||
|
return count, templates, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateRepo) GetFirst(opts ...DBOption) (*model.WebsiteTemplate, error) {
|
||||||
|
var template model.WebsiteTemplate
|
||||||
|
db := getDb(opts...).Model(&model.WebsiteTemplate{})
|
||||||
|
if err := db.First(&template).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &template, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateRepo) List(opts ...DBOption) ([]model.WebsiteTemplate, error) {
|
||||||
|
var templates []model.WebsiteTemplate
|
||||||
|
err := getDb(opts...).Model(&model.WebsiteTemplate{}).Find(&templates).Error
|
||||||
|
return templates, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateRepo) Create(template *model.WebsiteTemplate) error {
|
||||||
|
return getDb().Create(template).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateRepo) Save(template *model.WebsiteTemplate) error {
|
||||||
|
return getDb().Save(template).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateRepo) DeleteBy(opts ...DBOption) error {
|
||||||
|
return getDb(opts...).Delete(&model.WebsiteTemplate{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type IWebsiteTemplateOutputRepo interface {
|
||||||
|
Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplateOutput, error)
|
||||||
|
GetFirst(opts ...DBOption) (*model.WebsiteTemplateOutput, error)
|
||||||
|
List(opts ...DBOption) ([]model.WebsiteTemplateOutput, error)
|
||||||
|
Create(output *model.WebsiteTemplateOutput) error
|
||||||
|
Save(output *model.WebsiteTemplateOutput) error
|
||||||
|
DeleteBy(opts ...DBOption) error
|
||||||
|
WithByTemplateID(templateID uint) DBOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIWebsiteTemplateOutputRepo() IWebsiteTemplateOutputRepo {
|
||||||
|
return &WebsiteTemplateOutputRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateOutputRepo struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateOutputRepo) WithByTemplateID(templateID uint) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("template_id = ?", templateID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateOutputRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplateOutput, error) {
|
||||||
|
var outputs []model.WebsiteTemplateOutput
|
||||||
|
db := getDb(opts...).Model(&model.WebsiteTemplateOutput{})
|
||||||
|
count := int64(0)
|
||||||
|
db = db.Count(&count)
|
||||||
|
err := db.Limit(size).Offset(size * (page - 1)).Find(&outputs).Error
|
||||||
|
return count, outputs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateOutputRepo) GetFirst(opts ...DBOption) (*model.WebsiteTemplateOutput, error) {
|
||||||
|
var output model.WebsiteTemplateOutput
|
||||||
|
db := getDb(opts...).Model(&model.WebsiteTemplateOutput{})
|
||||||
|
if err := db.First(&output).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateOutputRepo) List(opts ...DBOption) ([]model.WebsiteTemplateOutput, error) {
|
||||||
|
var outputs []model.WebsiteTemplateOutput
|
||||||
|
err := getDb(opts...).Model(&model.WebsiteTemplateOutput{}).Find(&outputs).Error
|
||||||
|
return outputs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateOutputRepo) Create(output *model.WebsiteTemplateOutput) error {
|
||||||
|
return getDb().Create(output).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateOutputRepo) Save(output *model.WebsiteTemplateOutput) error {
|
||||||
|
return getDb().Save(output).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateOutputRepo) DeleteBy(opts ...DBOption) error {
|
||||||
|
return getDb(opts...).Delete(&model.WebsiteTemplateOutput{}).Error
|
||||||
|
}
|
||||||
+238
-122
@@ -8,8 +8,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
@@ -30,6 +30,8 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var agentAccountMutationMu sync.Mutex
|
||||||
|
|
||||||
type IAgentService interface {
|
type IAgentService interface {
|
||||||
Create(req dto.AgentCreateReq) (*dto.AgentItem, error)
|
Create(req dto.AgentCreateReq) (*dto.AgentItem, error)
|
||||||
BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error)
|
BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error)
|
||||||
@@ -77,6 +79,7 @@ type IAgentService interface {
|
|||||||
PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error)
|
PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error)
|
||||||
CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error)
|
CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error)
|
||||||
GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
|
GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
|
||||||
|
DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error)
|
||||||
CreateAccountModel(req dto.AgentAccountModelCreateReq) error
|
CreateAccountModel(req dto.AgentAccountModelCreateReq) error
|
||||||
UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error
|
UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error
|
||||||
DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error
|
DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error
|
||||||
@@ -102,6 +105,10 @@ type IAgentService interface {
|
|||||||
UpgradePlugin(req dto.AgentPluginUpgradeReq) error
|
UpgradePlugin(req dto.AgentPluginUpgradeReq) error
|
||||||
UninstallPlugin(req dto.AgentPluginUninstallReq) error
|
UninstallPlugin(req dto.AgentPluginUninstallReq) error
|
||||||
CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error)
|
CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error)
|
||||||
|
ListPlugins(req dto.AgentPluginsReq) ([]dto.AgentPluginItem, error)
|
||||||
|
SearchPlugins(req dto.AgentPluginSearchReq) ([]dto.AgentPluginSearchItem, error)
|
||||||
|
InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) error
|
||||||
|
OperatePlugin(req dto.AgentPluginOperateReq) error
|
||||||
ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
|
ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +151,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 {
|
if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 {
|
||||||
return nil, buserr.New("ErrNameIsExist")
|
return nil, buserr.New("ErrNameIsExist")
|
||||||
}
|
}
|
||||||
if !xpack.MultiNodeProvider.IsXpack() {
|
if !global.CONF.Base.IsEnterprise && !xpack.MultiNodeProvider.IsXpack() {
|
||||||
count, _, err := agentRepo.Page(1, 1)
|
count, _, err := agentRepo.Page(1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -165,8 +172,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
provider := ""
|
provider := ""
|
||||||
baseURL := ""
|
baseURL := ""
|
||||||
apiType := ""
|
apiType := ""
|
||||||
maxTokens := 0
|
|
||||||
contextWindow := 0
|
|
||||||
apiKey := ""
|
apiKey := ""
|
||||||
runtimeModel := ""
|
runtimeModel := ""
|
||||||
accountID := uint(0)
|
accountID := uint(0)
|
||||||
@@ -176,7 +181,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
var allowedOrigins []string
|
var allowedOrigins []string
|
||||||
var account *model.AgentAccount
|
var account *model.AgentAccount
|
||||||
var installHooks *appInstallHooks
|
var installHooks *appInstallHooks
|
||||||
var hermesAuth hermesDashboardAuth
|
var dashboardAuth agentDashboardAuth
|
||||||
|
|
||||||
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
|
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
|
||||||
if req.AccountID == 0 {
|
if req.AccountID == 0 {
|
||||||
@@ -197,8 +202,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
}
|
}
|
||||||
storedModel = resolvedRuntime.StoredModel
|
storedModel = resolvedRuntime.StoredModel
|
||||||
apiType = resolvedRuntime.APIType
|
apiType = resolvedRuntime.APIType
|
||||||
maxTokens = resolvedRuntime.MaxTokens
|
|
||||||
contextWindow = resolvedRuntime.ContextWindow
|
|
||||||
runtimeModel = resolvedRuntime.PrimaryModel
|
runtimeModel = resolvedRuntime.PrimaryModel
|
||||||
apiKey = account.APIKey
|
apiKey = account.APIKey
|
||||||
accountID = account.ID
|
accountID = account.ID
|
||||||
@@ -223,15 +226,17 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if agentType == constant.AppHermesAgent {
|
} else if agentType == constant.AppHermesAgent {
|
||||||
hermesAuth = normalizeHermesDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||||
installHooks = &appInstallHooks{
|
installHooks = &appInstallHooks{
|
||||||
AfterCopyData: func(appInstall *model.AppInstall) error {
|
AfterCopyData: func(appInstall *model.AppInstall) error {
|
||||||
if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil {
|
if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeHermesDashboardAuthEnv(path.Join(appInstall.GetPath(), ".env"), hermesAuth, false)
|
return writeAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType, dashboardAuth, false)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
} else if agentType == constant.AppCopaw {
|
||||||
|
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
@@ -247,15 +252,16 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
params["PROVIDER"] = provider
|
params["PROVIDER"] = provider
|
||||||
params["MODEL"] = runtimeModel
|
params["MODEL"] = runtimeModel
|
||||||
params["API_TYPE"] = apiType
|
params["API_TYPE"] = apiType
|
||||||
params["MAX_TOKENS"] = maxTokens
|
|
||||||
params["CONTEXT_WINDOW"] = contextWindow
|
|
||||||
params["BASE_URL"] = baseURL
|
params["BASE_URL"] = baseURL
|
||||||
params["API_KEY"] = apiKey
|
params["API_KEY"] = apiKey
|
||||||
params["OPENCLAW_GATEWAY_TOKEN"] = token
|
params["OPENCLAW_GATEWAY_TOKEN"] = token
|
||||||
}
|
}
|
||||||
if agentType == constant.AppHermesAgent {
|
if usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType); ok {
|
||||||
params[hermesDashboardUsernameEnvKey] = hermesAuth.Username
|
params[usernameKey] = dashboardAuth.Username
|
||||||
params[hermesDashboardPasswordEnvKey] = hermesAuth.Password
|
params[passwordKey] = dashboardAuth.Password
|
||||||
|
if agentType == constant.AppCopaw {
|
||||||
|
params[qwenPawAuthEnabledEnvKey] = "true"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
|
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
|
||||||
@@ -291,22 +297,20 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
configPath = path.Join(appInstall.GetPath(), "data", "config.yaml")
|
configPath = path.Join(appInstall.GetPath(), "data", "config.yaml")
|
||||||
}
|
}
|
||||||
agent := &model.Agent{
|
agent := &model.Agent{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Remark: req.Remark,
|
Remark: req.Remark,
|
||||||
AgentType: agentType,
|
AgentType: agentType,
|
||||||
Provider: provider,
|
Provider: provider,
|
||||||
Model: storedModel,
|
Model: storedModel,
|
||||||
APIType: apiType,
|
APIType: apiType,
|
||||||
MaxTokens: maxTokens,
|
BaseURL: baseURL,
|
||||||
ContextWindow: contextWindow,
|
APIKey: apiKey,
|
||||||
BaseURL: baseURL,
|
Token: token,
|
||||||
APIKey: apiKey,
|
Status: appInstall.Status,
|
||||||
Token: token,
|
Message: appInstall.Message,
|
||||||
Status: appInstall.Status,
|
AppInstallID: appInstall.ID,
|
||||||
Message: appInstall.Message,
|
AccountID: accountID,
|
||||||
AppInstallID: appInstall.ID,
|
ConfigPath: configPath,
|
||||||
AccountID: accountID,
|
|
||||||
ConfigPath: configPath,
|
|
||||||
}
|
}
|
||||||
if err := agentRepo.Create(agent); err != nil {
|
if err := agentRepo.Create(agent); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -404,7 +408,7 @@ func (a AgentService) BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dt
|
|||||||
results = append(results, result)
|
results = append(results, result)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchSkillInstallTaskID(req.TaskID, agent.ID), agent.ID)
|
installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchTaskID(req.TaskID, "batch-skill-install", agent.ID), agent.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Message = err.Error()
|
result.Message = err.Error()
|
||||||
results = append(results, result)
|
results = append(results, result)
|
||||||
@@ -446,7 +450,7 @@ func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBat
|
|||||||
if operate == constant.Delete {
|
if operate == constant.Delete {
|
||||||
if err := a.Delete(dto.AgentDeleteReq{
|
if err := a.Delete(dto.AgentDeleteReq{
|
||||||
ID: agent.ID,
|
ID: agent.ID,
|
||||||
TaskID: buildBatchOperateTaskID(req.TaskID, agent.ID),
|
TaskID: buildBatchTaskID(req.TaskID, "batch-operate", agent.ID),
|
||||||
ForceDelete: req.ForceDelete,
|
ForceDelete: req.ForceDelete,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
result.Message = err.Error()
|
result.Message = err.Error()
|
||||||
@@ -474,7 +478,7 @@ func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBat
|
|||||||
if err := NewIAppInstalledService().Operate(request.AppInstalledOperate{
|
if err := NewIAppInstalledService().Operate(request.AppInstalledOperate{
|
||||||
InstallId: install.ID,
|
InstallId: install.ID,
|
||||||
Operate: operate,
|
Operate: operate,
|
||||||
TaskID: buildBatchOperateTaskID(req.TaskID, agent.ID),
|
TaskID: buildBatchTaskID(req.TaskID, "batch-operate", agent.ID),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
result.Message = err.Error()
|
result.Message = err.Error()
|
||||||
} else {
|
} else {
|
||||||
@@ -558,35 +562,19 @@ func buildBatchUpgradePlans(req dto.AgentBatchUpgradeReq) ([]batchUpgradePlan, [
|
|||||||
DetailID: detail.ID,
|
DetailID: detail.ID,
|
||||||
Backup: req.Backup,
|
Backup: req.Backup,
|
||||||
PullImage: req.PullImage,
|
PullImage: req.PullImage,
|
||||||
TaskID: buildBatchUpgradeTaskID(req.TaskID, install.ID),
|
TaskID: buildBatchTaskID(req.TaskID, "batch-upgrade", install.ID),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return plans, results, nil
|
return plans, results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildBatchUpgradeTaskID(taskID string, appInstallID uint) string {
|
func buildBatchTaskID(taskID, prefix string, id uint) string {
|
||||||
taskID = strings.TrimSpace(taskID)
|
taskID = strings.TrimSpace(taskID)
|
||||||
if taskID == "" {
|
if taskID == "" {
|
||||||
taskID = fmt.Sprintf("batch-upgrade-%d-%d", appInstallID, time.Now().UnixNano())
|
taskID = fmt.Sprintf("%s-%d-%d", prefix, id, time.Now().UnixNano())
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s-%d", taskID, appInstallID)
|
return fmt.Sprintf("%s-%d", taskID, id)
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
func batchOperateSkipMessage(operate constant.AppOperate, status string) string {
|
||||||
@@ -715,6 +703,7 @@ func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (u
|
|||||||
account.RememberAPIKey = snapshot.RememberAPIKey
|
account.RememberAPIKey = snapshot.RememberAPIKey
|
||||||
account.BaseURL = snapshot.BaseURL
|
account.BaseURL = snapshot.BaseURL
|
||||||
account.APIType = snapshot.APIType
|
account.APIType = snapshot.APIType
|
||||||
|
account.AuthMode = snapshot.AuthMode
|
||||||
account.Remark = snapshot.Remark
|
account.Remark = snapshot.Remark
|
||||||
account.Verified = true
|
account.Verified = true
|
||||||
|
|
||||||
@@ -722,6 +711,11 @@ func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (u
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
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 err := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
if account.ID == 0 {
|
if account.ID == 0 {
|
||||||
if err := tx.Create(account).Error; err != nil {
|
if err := tx.Create(account).Error; err != nil {
|
||||||
@@ -959,7 +953,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
modelName := resolvedRuntime.StoredModel
|
modelName := resolvedRuntime.StoredModel
|
||||||
apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
|
apiType := resolvedRuntime.APIType
|
||||||
confDir := path.Dir(agent.ConfigPath)
|
confDir := path.Dir(agent.ConfigPath)
|
||||||
if agent.AgentType == constant.AppHermesAgent {
|
if agent.AgentType == constant.AppHermesAgent {
|
||||||
cfg, err := readHermesConfig(agent.ConfigPath)
|
cfg, err := readHermesConfig(agent.ConfigPath)
|
||||||
@@ -980,8 +974,6 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
|||||||
agent.Provider = account.Provider
|
agent.Provider = account.Provider
|
||||||
agent.Model = modelName
|
agent.Model = modelName
|
||||||
agent.APIType = apiType
|
agent.APIType = apiType
|
||||||
agent.MaxTokens = maxTokens
|
|
||||||
agent.ContextWindow = contextWindow
|
|
||||||
agent.BaseURL = account.BaseURL
|
agent.BaseURL = account.BaseURL
|
||||||
agent.APIKey = account.APIKey
|
agent.APIKey = account.APIKey
|
||||||
agent.AccountID = account.ID
|
agent.AccountID = account.ID
|
||||||
@@ -995,20 +987,38 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
|
|||||||
models := make([]dto.ProviderModelInfo, 0, len(def.Models))
|
models := make([]dto.ProviderModelInfo, 0, len(def.Models))
|
||||||
for _, item := range def.Models {
|
for _, item := range def.Models {
|
||||||
models = append(models, dto.ProviderModelInfo{
|
models = append(models, dto.ProviderModelInfo{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
Name: item.Name,
|
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 {
|
||||||
|
apiModels := make([]dto.ProviderModelInfo, 0, len(item.Models))
|
||||||
|
for _, model := range item.Models {
|
||||||
|
apiModels = append(apiModels, dto.ProviderModelInfo{
|
||||||
|
ID: model.ID,
|
||||||
|
Name: model.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
apiTypes = append(apiTypes, dto.ProviderAPIInfo{
|
||||||
|
APIType: item.APIType,
|
||||||
|
BaseURL: item.BaseURL,
|
||||||
|
EditableBaseURL: item.EditableBaseURL,
|
||||||
|
SupportsModelDiscovery: item.DiscoverModels,
|
||||||
|
DefaultAuthMode: item.DefaultAuthMode,
|
||||||
|
AuthModes: item.AuthModes,
|
||||||
|
Models: apiModels,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
baseURL, _ := providercatalog.DefaultBaseURL(key)
|
||||||
providers = append(providers, dto.ProviderInfo{
|
providers = append(providers, dto.ProviderInfo{
|
||||||
Sort: def.Sort,
|
Sort: def.Sort,
|
||||||
Provider: key,
|
Provider: key,
|
||||||
DisplayName: def.DisplayName,
|
DisplayName: localizedAgentProviderName(key),
|
||||||
BaseURL: def.DefaultBaseURL,
|
BaseURL: baseURL,
|
||||||
Models: models,
|
DefaultAPIType: def.DefaultAPIType,
|
||||||
|
APITypes: apiTypes,
|
||||||
|
Models: models,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
sort.Slice(providers, func(i, j int) bool {
|
sort.Slice(providers, func(i, j int) bool {
|
||||||
@@ -1018,11 +1028,22 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
||||||
|
agentAccountMutationMu.Lock()
|
||||||
|
defer agentAccountMutationMu.Unlock()
|
||||||
provider := req.Provider
|
provider := req.Provider
|
||||||
if exist, _ := agentAccountRepo.GetFirst(repo.WithByProvider(provider), repo.WithByName(req.Name)); exist != nil && exist.ID > 0 {
|
if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil {
|
||||||
return buserr.New("ErrRecordExist")
|
return err
|
||||||
}
|
}
|
||||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL)
|
initialModels, err := buildInitialAgentAccountModels(&model.AgentAccount{Provider: provider, APIType: req.APIType}, req.Models)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
verifyModel, err := resolveAgentAccountVerifyModel(provider, req.VerifyModel, initialModels)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||||
|
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1032,14 +1053,12 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
|||||||
APIKey: resolvedInput.APIKey,
|
APIKey: resolvedInput.APIKey,
|
||||||
RememberAPIKey: req.RememberAPIKey,
|
RememberAPIKey: req.RememberAPIKey,
|
||||||
BaseURL: resolvedInput.BaseURL,
|
BaseURL: resolvedInput.BaseURL,
|
||||||
APIType: req.APIType,
|
APIType: resolvedInput.APIType,
|
||||||
|
AuthMode: resolvedInput.AuthMode,
|
||||||
|
VerifyModel: verifyModel,
|
||||||
Verified: true,
|
Verified: true,
|
||||||
Remark: req.Remark,
|
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 := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
if err := tx.Create(account).Error; err != nil {
|
if err := tx.Create(account).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1056,12 +1075,33 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||||
|
agentAccountMutationMu.Lock()
|
||||||
|
defer agentAccountMutationMu.Unlock()
|
||||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
|
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if req.APIType != account.APIType {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "API type cannot be changed", nil)
|
||||||
|
}
|
||||||
provider := account.Provider
|
provider := account.Provider
|
||||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL)
|
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
|
||||||
|
}
|
||||||
|
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||||
|
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1069,7 +1109,9 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
account.APIKey = resolvedInput.APIKey
|
account.APIKey = resolvedInput.APIKey
|
||||||
account.RememberAPIKey = req.RememberAPIKey
|
account.RememberAPIKey = req.RememberAPIKey
|
||||||
account.BaseURL = resolvedInput.BaseURL
|
account.BaseURL = resolvedInput.BaseURL
|
||||||
account.APIType = req.APIType
|
account.APIType = resolvedInput.APIType
|
||||||
|
account.AuthMode = resolvedInput.AuthMode
|
||||||
|
account.VerifyModel = verifyModel
|
||||||
account.Remark = req.Remark
|
account.Remark = req.Remark
|
||||||
account.Verified = true
|
account.Verified = true
|
||||||
|
|
||||||
@@ -1091,6 +1133,12 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
|||||||
if strings.TrimSpace(req.Provider) != "" {
|
if strings.TrimSpace(req.Provider) != "" {
|
||||||
opts = append(opts, repo.WithByProvider(req.Provider))
|
opts = append(opts, repo.WithByProvider(req.Provider))
|
||||||
}
|
}
|
||||||
|
if apiType := strings.TrimSpace(req.APIType); apiType != "" {
|
||||||
|
opts = append(opts, repo.WithByAPIType(apiType))
|
||||||
|
}
|
||||||
|
if req.TextOnly {
|
||||||
|
opts = append(opts, repo.WithTextAPIType())
|
||||||
|
}
|
||||||
if strings.TrimSpace(req.Name) != "" {
|
if strings.TrimSpace(req.Name) != "" {
|
||||||
opts = append(opts, repo.WithByLikeName(req.Name))
|
opts = append(opts, repo.WithByLikeName(req.Name))
|
||||||
}
|
}
|
||||||
@@ -1108,30 +1156,46 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
|||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
MasterAccountID: item.MasterAccountID,
|
MasterAccountID: item.MasterAccountID,
|
||||||
Provider: item.Provider,
|
Provider: item.Provider,
|
||||||
ProviderName: providercatalog.DisplayName(item.Provider),
|
ProviderName: localizedAgentProviderName(item.Provider),
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
APIKey: apiKey,
|
APIKey: apiKey,
|
||||||
RememberAPIKey: item.RememberAPIKey,
|
RememberAPIKey: item.RememberAPIKey,
|
||||||
BaseURL: item.BaseURL,
|
BaseURL: item.BaseURL,
|
||||||
Models: nil,
|
Models: nil,
|
||||||
APIType: item.APIType,
|
APIType: item.APIType,
|
||||||
|
AuthMode: item.AuthMode,
|
||||||
|
VerifyModel: item.VerifyModel,
|
||||||
Verified: item.Verified,
|
Verified: item.Verified,
|
||||||
Remark: item.Remark,
|
Remark: item.Remark,
|
||||||
CreatedAt: item.CreatedAt,
|
CreatedAt: item.CreatedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for i := range items {
|
if len(list) > 0 {
|
||||||
models, err := loadAgentAccountModels(&list[i])
|
accountIDs := make([]uint, 0, len(list))
|
||||||
if err != nil {
|
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 {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
items[i].Models = models
|
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]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return count, items, nil
|
return count, items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) {
|
func (a AgentService) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) {
|
||||||
return agentAccountRepo.CountByProviders(req.Providers)
|
return agentAccountRepo.CountTextByProviders(req.Providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) {
|
func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) {
|
||||||
@@ -1142,6 +1206,22 @@ func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.Agen
|
|||||||
return loadAgentAccountModels(account)
|
return loadAgentAccountModels(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AgentService) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) {
|
||||||
|
config, ok := providercatalog.FindAPIConfig(req.Provider, req.APIType)
|
||||||
|
if !ok || !config.DiscoverModels {
|
||||||
|
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 {
|
func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) error {
|
||||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
|
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1151,24 +1231,19 @@ func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
nextModel := cloneAgentAccountModel(req.Model)
|
nextModel, err := normalizeAgentAccountModel(account, req.Model)
|
||||||
if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok {
|
|
||||||
return buserr.New("ErrRecordExist")
|
|
||||||
}
|
|
||||||
inputPayload, err := json.Marshal(nextModel.Input)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok {
|
||||||
|
return buserr.New("ErrRecordExist")
|
||||||
|
}
|
||||||
sortOrder := len(models) + 1
|
sortOrder := len(models) + 1
|
||||||
record := &model.AgentAccountModel{
|
record := &model.AgentAccountModel{
|
||||||
AccountID: account.ID,
|
AccountID: account.ID,
|
||||||
Model: nextModel.ID,
|
Model: nextModel.ID,
|
||||||
Name: nextModel.Name,
|
Name: nextModel.Name,
|
||||||
ContextWindow: nextModel.ContextWindow,
|
SortOrder: sortOrder,
|
||||||
MaxTokens: nextModel.MaxTokens,
|
|
||||||
Reasoning: nextModel.Reasoning,
|
|
||||||
Input: string(inputPayload),
|
|
||||||
SortOrder: sortOrder,
|
|
||||||
}
|
}
|
||||||
if err := agentAccountModelRepo.Create(record); err != nil {
|
if err := agentAccountModelRepo.Create(record); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1189,7 +1264,11 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
nextModel := cloneAgentAccountModel(req.Model)
|
nextModel, err := normalizeAgentAccountModel(account, req.Model)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nextModel.RecordID = req.Model.RecordID
|
||||||
for _, item := range models {
|
for _, item := range models {
|
||||||
if item.RecordID == req.Model.RecordID {
|
if item.RecordID == req.Model.RecordID {
|
||||||
continue
|
continue
|
||||||
@@ -1209,19 +1288,18 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
|
|||||||
if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
|
if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
inputPayload, err := json.Marshal(nextModel.Input)
|
previousModelID := record.Model
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
record.Model = nextModel.ID
|
record.Model = nextModel.ID
|
||||||
record.Name = nextModel.Name
|
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 {
|
if err := agentAccountModelRepo.Save(record); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if sameProviderModelID(account.Provider, account.VerifyModel, previousModelID) {
|
||||||
|
account.VerifyModel = nextModel.ID
|
||||||
|
if err := agentAccountRepo.Save(account); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
terminalai.InvalidateTerminalRuntimeCache()
|
terminalai.InvalidateTerminalRuntimeCache()
|
||||||
terminalai.InvalidateFileAIRuntimeCache()
|
terminalai.InvalidateFileAIRuntimeCache()
|
||||||
return a.syncAgentsByAccount(account)
|
return a.syncAgentsByAccount(account)
|
||||||
@@ -1232,9 +1310,13 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID)); err != nil {
|
record, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID))
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if sameProviderModelID(account.Provider, account.VerifyModel, record.Model) {
|
||||||
|
return buserr.New("ErrAgentVerifyModelInUse")
|
||||||
|
}
|
||||||
models, err := loadAgentAccountModels(account)
|
models, err := loadAgentAccountModels(account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1268,25 +1350,45 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
||||||
_, err := resolveAgentAccountInput(req.Provider, req.APIKey, req.BaseURL)
|
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
||||||
if exists, _ := agentRepo.GetFirst(repo.WithByAccountID(req.ID)); exists != nil && exists.ID > 0 {
|
agentAccountMutationMu.Lock()
|
||||||
return buserr.New("ErrAgentAccountBound")
|
defer agentAccountMutationMu.Unlock()
|
||||||
}
|
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
if aiStatus, _ := settingRepo.GetValueByKey("AIStatus"); strings.EqualFold(strings.TrimSpace(aiStatus), constant.StatusEnable) {
|
var agentCount int64
|
||||||
if aiAccountID, _ := settingRepo.GetValueByKey("AIAccountID"); strings.TrimSpace(aiAccountID) == strconv.FormatUint(uint64(req.ID), 10) {
|
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 {
|
||||||
return buserr.New("ErrTerminalAIAccountInUse")
|
return buserr.New("ErrTerminalAIAccountInUse")
|
||||||
}
|
}
|
||||||
}
|
used, err = agentAccountUsedBySetting(tx, req.ID, "FileAIStatus", "FileAIAccountID")
|
||||||
if err := agentAccountModelRepo.Delete(repo.WithByAccountID(req.ID)); err != nil {
|
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 {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
terminalai.InvalidateTerminalRuntimeCache()
|
terminalai.InvalidateTerminalRuntimeCache()
|
||||||
terminalai.InvalidateFileAIRuntimeCache()
|
terminalai.InvalidateFileAIRuntimeCache()
|
||||||
return agentAccountRepo.DeleteByID(req.ID)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error) {
|
func (a AgentService) GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error) {
|
||||||
@@ -1338,7 +1440,7 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
auth := readHermesDashboardAuthFromInstall(install)
|
auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
|
||||||
return &dto.AgentOtherConfig{
|
return &dto.AgentOtherConfig{
|
||||||
UserTimezone: cfg.Timezone,
|
UserTimezone: cfg.Timezone,
|
||||||
BrowserEnabled: true,
|
BrowserEnabled: true,
|
||||||
@@ -1347,6 +1449,13 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
|
|||||||
DashboardPassword: auth.Password,
|
DashboardPassword: auth.Password,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
if agent.AgentType == constant.AppCopaw {
|
||||||
|
auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
|
||||||
|
return &dto.AgentOtherConfig{
|
||||||
|
DashboardUsername: auth.Username,
|
||||||
|
DashboardPassword: auth.Password,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1365,16 +1474,19 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if agent.AgentType == constant.AppHermesAgent {
|
if agent.AgentType == constant.AppHermesAgent {
|
||||||
|
if strings.TrimSpace(req.UserTimezone) == "" {
|
||||||
|
return buserr.New("ErrInvalidParams")
|
||||||
|
}
|
||||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
|
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
previousAuth := readHermesDashboardAuthFromInstall(install)
|
previousAuth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
|
||||||
nextAuth := normalizeHermesDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
nextAuth := normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||||
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
|
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := writeHermesDashboardAuthEnv(path.Join(install.GetPath(), ".env"), nextAuth, true); err != nil {
|
if err := writeAgentDashboardAuthEnv(install.GetEnvPath(), agent.AgentType, nextAuth, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
operate := constant.Restart
|
operate := constant.Restart
|
||||||
@@ -1386,6 +1498,12 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
|||||||
Operate: operate,
|
Operate: operate,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if agent.AgentType == constant.AppCopaw {
|
||||||
|
return updateQwenPawDashboardAuth(install, normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.UserTimezone) == "" || strings.TrimSpace(req.NPMRegistry) == "" {
|
||||||
|
return buserr.New("ErrInvalidParams")
|
||||||
|
}
|
||||||
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1557,7 +1675,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
modelName := resolvedRuntime.StoredModel
|
modelName := resolvedRuntime.StoredModel
|
||||||
apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
|
apiType := resolvedRuntime.APIType
|
||||||
confDir := path.Dir(agent.ConfigPath)
|
confDir := path.Dir(agent.ConfigPath)
|
||||||
switch agent.AgentType {
|
switch agent.AgentType {
|
||||||
case constant.AppOpenclaw:
|
case constant.AppOpenclaw:
|
||||||
@@ -1585,8 +1703,6 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
|
|||||||
agent.Provider = account.Provider
|
agent.Provider = account.Provider
|
||||||
agent.Model = modelName
|
agent.Model = modelName
|
||||||
agent.APIType = apiType
|
agent.APIType = apiType
|
||||||
agent.MaxTokens = maxTokens
|
|
||||||
agent.ContextWindow = contextWindow
|
|
||||||
_ = agentRepo.Save(&agent)
|
_ = agentRepo.Save(&agent)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type qwenPawAuthStatus struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
HasUsers bool `json:"has_users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type qwenPawLoginResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateQwenPawDashboardAuth(install *model.AppInstall, next agentDashboardAuth) error {
|
||||||
|
if install == nil || install.ID == 0 {
|
||||||
|
return buserr.New("ErrRecordNotFound")
|
||||||
|
}
|
||||||
|
current, err := readAgentDashboardAuthEnv(install.GetEnvPath(), constant.AppCopaw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if current == next {
|
||||||
|
return writeAgentDashboardAuthEnv(install.GetEnvPath(), constant.AppCopaw, next, true)
|
||||||
|
}
|
||||||
|
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := fmt.Sprintf("http://127.0.0.1:%d/api/auth", install.HttpPort)
|
||||||
|
var status qwenPawAuthStatus
|
||||||
|
if _, err := requestQwenPawAuth(http.MethodGet, baseURL+"/status", nil, "", &status); err != nil {
|
||||||
|
return buserr.WithMap("ErrQwenPawAuthRequest", map[string]interface{}{"err": err.Error()}, err)
|
||||||
|
}
|
||||||
|
if !status.Enabled {
|
||||||
|
return buserr.New("ErrQwenPawAuthDisabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !status.HasUsers {
|
||||||
|
payload := map[string]string{"username": next.Username, "password": next.Password}
|
||||||
|
if _, err := requestQwenPawAuth(http.MethodPost, baseURL+"/register", payload, "", nil); err != nil {
|
||||||
|
return buserr.WithMap("ErrQwenPawAuthRequest", map[string]interface{}{"err": err.Error()}, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var login qwenPawLoginResponse
|
||||||
|
payload := map[string]string{"username": current.Username, "password": current.Password}
|
||||||
|
statusCode, err := requestQwenPawAuth(http.MethodPost, baseURL+"/login", payload, "", &login)
|
||||||
|
if statusCode == http.StatusUnauthorized {
|
||||||
|
return buserr.New("ErrQwenPawAuthOutOfSync")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return buserr.WithMap("ErrQwenPawAuthRequest", map[string]interface{}{"err": err.Error()}, err)
|
||||||
|
}
|
||||||
|
payload = map[string]string{"current_password": current.Password}
|
||||||
|
if current.Username != next.Username {
|
||||||
|
payload["new_username"] = next.Username
|
||||||
|
}
|
||||||
|
if current.Password != next.Password {
|
||||||
|
payload["new_password"] = next.Password
|
||||||
|
}
|
||||||
|
if _, err := requestQwenPawAuth(http.MethodPost, baseURL+"/update-profile", payload, login.Token, nil); err != nil {
|
||||||
|
return buserr.WithMap("ErrQwenPawAuthRequest", map[string]interface{}{"err": err.Error()}, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return writeAgentDashboardAuthEnv(install.GetEnvPath(), constant.AppCopaw, next, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestQwenPawAuth(method, reqURL string, payload interface{}, token string, result interface{}) (int, error) {
|
||||||
|
var body io.Reader
|
||||||
|
if payload != nil {
|
||||||
|
data, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
body = bytes.NewReader(data)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, reqURL, body)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if token != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
}
|
||||||
|
resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
data, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||||
|
if err != nil {
|
||||||
|
return resp.StatusCode, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||||
|
detail := strings.TrimSpace(string(data))
|
||||||
|
var errorResponse struct {
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(data, &errorResponse) == nil && strings.TrimSpace(errorResponse.Detail) != "" {
|
||||||
|
detail = strings.TrimSpace(errorResponse.Detail)
|
||||||
|
}
|
||||||
|
if detail == "" {
|
||||||
|
detail = resp.Status
|
||||||
|
}
|
||||||
|
return resp.StatusCode, errors.New(detail)
|
||||||
|
}
|
||||||
|
if result != nil && len(data) > 0 {
|
||||||
|
if err := json.Unmarshal(data, result); err != nil {
|
||||||
|
return resp.StatusCode, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp.StatusCode, nil
|
||||||
|
}
|
||||||
@@ -19,13 +19,6 @@ import (
|
|||||||
|
|
||||||
const hermesWorkspaceDir = "/opt/data/workspace"
|
const hermesWorkspaceDir = "/opt/data/workspace"
|
||||||
const hermesExecutablePath = "/opt/hermes/.venv/bin/hermes"
|
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 {
|
type hermesConfig struct {
|
||||||
Model hermesModelConfig `yaml:"model"`
|
Model hermesModelConfig `yaml:"model"`
|
||||||
@@ -76,7 +69,7 @@ func writeHermesConfig(confDir string, account *model.AgentAccount, modelName st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
provider := resolveHermesProvider(account.Provider)
|
provider := resolveHermesProvider(account.Provider, account.APIType)
|
||||||
configPath := path.Join(confDir, "config.yaml")
|
configPath := path.Join(confDir, "config.yaml")
|
||||||
cfg, err := readHermesConfigMap(configPath)
|
cfg, err := readHermesConfigMap(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -117,52 +110,6 @@ func prepareHermesInstallFiles(appInstall *model.AppInstall, account *model.Agen
|
|||||||
return files.NewFileOp().ChownR(dataDir, "1000", "1000", true)
|
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) {
|
func readHermesConfig(configPath string) (*hermesConfig, error) {
|
||||||
content, err := files.NewFileOp().GetContent(configPath)
|
content, err := files.NewFileOp().GetContent(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -458,11 +405,18 @@ func normalizeHermesTimezone(timezone string) string {
|
|||||||
return timezone
|
return timezone
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveHermesProvider(provider string) string {
|
func resolveHermesProvider(provider, apiType string) string {
|
||||||
|
if apiType == "anthropic-messages" && (provider == "deepseek" || provider == "bailian-coding-plan" || provider == "ark-coding-plan" || provider == "xiaomi") {
|
||||||
|
return "anthropic"
|
||||||
|
}
|
||||||
switch provider {
|
switch provider {
|
||||||
case "":
|
case "":
|
||||||
return "custom"
|
return "custom"
|
||||||
case "openrouter", "anthropic", "gemini", "zai", "kimi-coding", "xiaomi":
|
case "moonshot":
|
||||||
|
return "kimi-coding"
|
||||||
|
case "kimi":
|
||||||
|
return "kimi-coding-cn"
|
||||||
|
case "openrouter", "anthropic", "deepseek", "gemini", "zai", "kimi-coding", "xiaomi":
|
||||||
return provider
|
return provider
|
||||||
case "minimax":
|
case "minimax":
|
||||||
return "minimax-cn"
|
return "minimax-cn"
|
||||||
@@ -479,17 +433,7 @@ func resolveHermesModel(sourceProvider, targetProvider, modelName string) string
|
|||||||
if targetProvider != "custom" {
|
if targetProvider != "custom" {
|
||||||
return target
|
return target
|
||||||
}
|
}
|
||||||
if sourceProvider == "custom" || sourceProvider == "vllm" {
|
return providercatalog.NormalizeModelID(sourceProvider, target)
|
||||||
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) {
|
func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) (string, error) {
|
||||||
@@ -500,7 +444,7 @@ func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels [
|
|||||||
if configuredModel == "" {
|
if configuredModel == "" {
|
||||||
return "", buserr.New("ErrAgentModelNotInAccount")
|
return "", buserr.New("ErrAgentModelNotInAccount")
|
||||||
}
|
}
|
||||||
provider := resolveHermesProvider(account.Provider)
|
provider := resolveHermesProvider(account.Provider, account.APIType)
|
||||||
for _, item := range accountModels {
|
for _, item := range accountModels {
|
||||||
if resolveHermesModel(account.Provider, provider, item.ID) == configuredModel {
|
if resolveHermesModel(account.Provider, provider, item.ID) == configuredModel {
|
||||||
return item.ID, nil
|
return item.ID, nil
|
||||||
@@ -513,7 +457,8 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
|
|||||||
if account == nil {
|
if account == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if resolveHermesProvider(account.Provider) == "custom" {
|
resolvedProvider := resolveHermesProvider(account.Provider, account.APIType)
|
||||||
|
if resolvedProvider == "custom" {
|
||||||
if account.APIKey == "" {
|
if account.APIKey == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -531,6 +476,11 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
|
|||||||
}
|
}
|
||||||
entries = append(entries, hermesEnvEntry{Key: key, Value: value})
|
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 {
|
switch account.Provider {
|
||||||
case "openrouter":
|
case "openrouter":
|
||||||
@@ -538,6 +488,7 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
|
|||||||
appendEntry("OPENROUTER_BASE_URL", baseURL)
|
appendEntry("OPENROUTER_BASE_URL", baseURL)
|
||||||
case "anthropic":
|
case "anthropic":
|
||||||
appendEntry("ANTHROPIC_API_KEY", apiKey)
|
appendEntry("ANTHROPIC_API_KEY", apiKey)
|
||||||
|
appendEntry("ANTHROPIC_BASE_URL", baseURL)
|
||||||
case "gemini":
|
case "gemini":
|
||||||
appendEntry("GOOGLE_API_KEY", apiKey)
|
appendEntry("GOOGLE_API_KEY", apiKey)
|
||||||
appendEntry("GEMINI_API_KEY", apiKey)
|
appendEntry("GEMINI_API_KEY", apiKey)
|
||||||
@@ -607,6 +558,7 @@ func hermesManagedModelEnvKeys() []string {
|
|||||||
"OPENROUTER_API_KEY",
|
"OPENROUTER_API_KEY",
|
||||||
"OPENROUTER_BASE_URL",
|
"OPENROUTER_BASE_URL",
|
||||||
"ANTHROPIC_API_KEY",
|
"ANTHROPIC_API_KEY",
|
||||||
|
"ANTHROPIC_BASE_URL",
|
||||||
"GOOGLE_API_KEY",
|
"GOOGLE_API_KEY",
|
||||||
"GEMINI_API_KEY",
|
"GEMINI_API_KEY",
|
||||||
"GEMINI_BASE_URL",
|
"GEMINI_BASE_URL",
|
||||||
|
|||||||
@@ -0,0 +1,315 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
openclawPluginPackagePattern = regexp.MustCompile(`^(@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*$`)
|
||||||
|
openclawPluginVersionPattern = regexp.MustCompile(`^[0-9A-Za-z][0-9A-Za-z._-]*$`)
|
||||||
|
openclawPluginIDPattern = regexp.MustCompile(`^(@[A-Za-z0-9][A-Za-z0-9._-]*/)?[A-Za-z0-9][A-Za-z0-9._-]*$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type openclawPluginListOutput struct {
|
||||||
|
Plugins []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
} `json:"plugins"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type openclawPluginIndexItem struct {
|
||||||
|
PluginID string `json:"pluginId"`
|
||||||
|
PackageName string `json:"packageName"`
|
||||||
|
PackageVersion string `json:"packageVersion"`
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type openclawPluginSearchOutput struct {
|
||||||
|
Results []struct {
|
||||||
|
Score float64 `json:"score"`
|
||||||
|
Package struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
RuntimeID string `json:"runtimeId"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
LatestVersion string `json:"latestVersion"`
|
||||||
|
Categories []string `json:"categories"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
IsOfficial bool `json:"isOfficial"`
|
||||||
|
VerificationTier string `json:"verificationTier"`
|
||||||
|
Stats struct {
|
||||||
|
Downloads int64 `json:"downloads"`
|
||||||
|
} `json:"stats"`
|
||||||
|
} `json:"package"`
|
||||||
|
} `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AgentService) ListPlugins(req dto.AgentPluginsReq) ([]dto.AgentPluginItem, error) {
|
||||||
|
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if plugins, err := readOpenclawPluginIndex(filepath.Join(filepath.Dir(agent.ConfigPath), "state", "openclaw.sqlite")); err == nil {
|
||||||
|
return plugins, nil
|
||||||
|
}
|
||||||
|
output, err := cmd.RunDockerExecWithStdout(2*time.Minute, install.ContainerName, "openclaw", "plugins", "list", "--json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseOpenclawPluginList([]byte(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AgentService) SearchPlugins(req dto.AgentPluginSearchReq) ([]dto.AgentPluginSearchItem, error) {
|
||||||
|
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
limit := req.Limit
|
||||||
|
if limit == 0 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
output, err := cmd.RunDockerExecWithStdout(
|
||||||
|
2*time.Minute,
|
||||||
|
install.ContainerName,
|
||||||
|
"openclaw", "plugins", "search", strings.TrimSpace(req.Keyword), "--limit", fmt.Sprint(limit), "--json",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseOpenclawPluginSearch([]byte(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AgentService) InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) error {
|
||||||
|
spec, err := buildOpenclawPluginInstallSpec(req.Package, req.Version)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
taskName := fmt.Sprintf("%s [%s]", i18n.GetMsgByKey("AgentPluginInstall"), req.Package)
|
||||||
|
installTask, err := task.NewTask(taskName, task.TaskInstall, task.TaskScopeAI, req.TaskID, req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
installTask.AddSubTask(taskName, func(t *task.Task) error {
|
||||||
|
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
|
||||||
|
return mgr.Run("docker", "exec", install.ContainerName, "openclaw", "plugins", "install", spec)
|
||||||
|
}, nil)
|
||||||
|
addOpenclawPluginRestartTask(installTask, install)
|
||||||
|
go executeAgentPluginTask(installTask)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AgentService) OperatePlugin(req dto.AgentPluginOperateReq) error {
|
||||||
|
if !openclawPluginIDPattern.MatchString(req.PluginID) {
|
||||||
|
return buserr.New("ErrInvalidChar")
|
||||||
|
}
|
||||||
|
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if req.Operate == "update" || req.Operate == "uninstall" {
|
||||||
|
plugins, err := a.ListPlugins(dto.AgentPluginsReq{AgentID: req.AgentID})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, plugin := range plugins {
|
||||||
|
if plugin.ID == req.PluginID && plugin.Origin == "bundled" {
|
||||||
|
return buserr.WithName("ErrNotSupportType", req.Operate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
taskType := map[string]string{
|
||||||
|
"enable": task.TaskUpdate,
|
||||||
|
"disable": task.TaskUpdate,
|
||||||
|
"update": task.TaskUpgrade,
|
||||||
|
"uninstall": task.TaskUninstall,
|
||||||
|
}[req.Operate]
|
||||||
|
taskName := fmt.Sprintf("%s [%s]", i18n.GetMsgByKey(map[string]string{
|
||||||
|
"enable": "AgentPluginEnable",
|
||||||
|
"disable": "AgentPluginDisable",
|
||||||
|
"update": "AgentPluginUpdate",
|
||||||
|
"uninstall": "AgentPluginUninstall",
|
||||||
|
}[req.Operate]), req.PluginID)
|
||||||
|
operateTask, err := task.NewTask(taskName, taskType, task.TaskScopeAI, req.TaskID, req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
operateTask.AddSubTask(taskName, func(t *task.Task) error {
|
||||||
|
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
|
||||||
|
if req.Operate == "uninstall" {
|
||||||
|
if err := uninstallOpenclawPlugin(mgr, install.ContainerName, req.PluginID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cleanupManagedOpenclawPlugin(agent, req.PluginID)
|
||||||
|
}
|
||||||
|
return mgr.Run("docker", "exec", install.ContainerName, "openclaw", "plugins", req.Operate, req.PluginID)
|
||||||
|
}, nil)
|
||||||
|
addOpenclawPluginRestartTask(operateTask, install)
|
||||||
|
go executeAgentPluginTask(operateTask)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOpenclawPluginList(raw []byte) ([]dto.AgentPluginItem, error) {
|
||||||
|
payload, err := extractEmbeddedJSON(string(raw))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return []dto.AgentPluginItem{}, nil
|
||||||
|
}
|
||||||
|
var output openclawPluginListOutput
|
||||||
|
if err := json.Unmarshal(payload, &output); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := make([]dto.AgentPluginItem, 0, len(output.Plugins))
|
||||||
|
for _, plugin := range output.Plugins {
|
||||||
|
items = append(items, dto.AgentPluginItem{
|
||||||
|
ID: plugin.ID,
|
||||||
|
Name: plugin.Name,
|
||||||
|
Version: plugin.Version,
|
||||||
|
Origin: plugin.Origin,
|
||||||
|
Enabled: plugin.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readOpenclawPluginIndex(dbPath string) ([]dto.AgentPluginItem, error) {
|
||||||
|
db, err := sql.Open("sqlite", "file:"+filepath.ToSlash(dbPath)+"?mode=ro")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
var raw []byte
|
||||||
|
if err := db.QueryRow(
|
||||||
|
"SELECT plugins_json FROM installed_plugin_index WHERE index_key = ?",
|
||||||
|
"installed-plugin-index",
|
||||||
|
).Scan(&raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var plugins []openclawPluginIndexItem
|
||||||
|
if err := json.Unmarshal(raw, &plugins); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := make([]dto.AgentPluginItem, 0, len(plugins))
|
||||||
|
for _, plugin := range plugins {
|
||||||
|
name := plugin.PackageName
|
||||||
|
if name == "" {
|
||||||
|
name = plugin.PluginID
|
||||||
|
}
|
||||||
|
items = append(items, dto.AgentPluginItem{
|
||||||
|
ID: plugin.PluginID,
|
||||||
|
Name: name,
|
||||||
|
Version: plugin.PackageVersion,
|
||||||
|
Origin: plugin.Origin,
|
||||||
|
Enabled: plugin.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOpenclawPluginSearch(raw []byte) ([]dto.AgentPluginSearchItem, error) {
|
||||||
|
payload, err := extractEmbeddedJSON(string(raw))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return []dto.AgentPluginSearchItem{}, nil
|
||||||
|
}
|
||||||
|
var output openclawPluginSearchOutput
|
||||||
|
if err := json.Unmarshal(payload, &output); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := make([]dto.AgentPluginSearchItem, 0, len(output.Results))
|
||||||
|
for _, result := range output.Results {
|
||||||
|
items = append(items, dto.AgentPluginSearchItem{
|
||||||
|
Package: result.Package.Name,
|
||||||
|
PluginID: result.Package.RuntimeID,
|
||||||
|
Name: result.Package.DisplayName,
|
||||||
|
Description: result.Package.Summary,
|
||||||
|
Version: result.Package.LatestVersion,
|
||||||
|
Channel: result.Package.Channel,
|
||||||
|
VerificationTier: result.Package.VerificationTier,
|
||||||
|
Categories: append([]string{}, result.Package.Categories...),
|
||||||
|
Official: result.Package.IsOfficial,
|
||||||
|
Downloads: result.Package.Stats.Downloads,
|
||||||
|
Score: result.Score,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildOpenclawPluginInstallSpec(packageName, version string) (string, error) {
|
||||||
|
packageName = strings.TrimSpace(packageName)
|
||||||
|
version = strings.TrimSpace(version)
|
||||||
|
if !openclawPluginPackagePattern.MatchString(packageName) || !openclawPluginVersionPattern.MatchString(version) {
|
||||||
|
return "", buserr.New("ErrInvalidChar")
|
||||||
|
}
|
||||||
|
return "clawhub:" + packageName + "@" + version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupManagedOpenclawPlugin(agent *model.Agent, pluginID string) error {
|
||||||
|
pluginType := map[string]string{
|
||||||
|
"openclaw-lark": "feishu",
|
||||||
|
"openclaw-qqbot": "qqbot",
|
||||||
|
"wecom-openclaw-plugin": "wecom",
|
||||||
|
"dingtalk-connector": "dingtalk",
|
||||||
|
"openclaw-weixin": "weixin",
|
||||||
|
}[pluginID]
|
||||||
|
if pluginType == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cleanupOpenclawPluginConfig(conf, pluginType)
|
||||||
|
return writeOpenclawConfigRaw(agent.ConfigPath, conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addOpenclawPluginRestartTask(t *task.Task, install *model.AppInstall) {
|
||||||
|
t.AddSubTask(task.GetTaskName("OpenClaw", task.TaskRestart, task.TaskScopeAI), func(t *task.Task) error {
|
||||||
|
output, err := compose.Restart(install.GetComposePath())
|
||||||
|
if output != "" {
|
||||||
|
t.Log(output)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeAgentPluginTask(t *task.Task) {
|
||||||
|
if err := t.Execute(); err != nil {
|
||||||
|
global.LOG.Errorf("operate openclaw plugin failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+229
-269
@@ -4,10 +4,12 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -19,6 +21,7 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
|
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||||
@@ -37,6 +40,39 @@ type resolvedAgentAccountInput struct {
|
|||||||
Provider string
|
Provider string
|
||||||
APIKey string
|
APIKey string
|
||||||
BaseURL 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) {
|
func loadOpenclawAgentByID(agentID uint) (*model.Agent, error) {
|
||||||
@@ -61,19 +97,27 @@ func ensureContainerRunning(containerName string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveAgentAccountInput(provider, apiKey, baseURL string) (resolvedAgentAccountInput, error) {
|
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability bool) (resolvedAgentAccountInput, error) {
|
||||||
resolvedAPIKey := strings.TrimSpace(apiKey)
|
resolvedAPIKey := strings.TrimSpace(apiKey)
|
||||||
resolvedBaseURL := strings.TrimSpace(baseURL)
|
resolvedAPIType := strings.TrimSpace(apiType)
|
||||||
if resolvedBaseURL == "" {
|
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||||
if requiresInitialAgentAccountModels(provider) {
|
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") {
|
||||||
return resolvedAgentAccountInput{}, buserr.New("ErrAgentBaseURLRequired")
|
return resolvedAgentAccountInput{}, buserr.New("ErrAgentBaseURLRequired")
|
||||||
}
|
}
|
||||||
if defaultURL, ok := providercatalog.DefaultBaseURL(provider); ok {
|
return resolvedAgentAccountInput{}, err
|
||||||
resolvedBaseURL = defaultURL
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !providercatalog.SkipVerification(provider) {
|
modelID = strings.TrimSpace(modelID)
|
||||||
if err := providercatalog.VerifyAccount(provider, resolvedBaseURL, resolvedAPIKey); err != nil {
|
if modelID == "" {
|
||||||
|
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
||||||
|
}
|
||||||
|
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
|
||||||
|
if validateAvailability && (imageAPI || !providercatalog.SkipVerification(provider)) {
|
||||||
|
if err := providercatalog.VerifyAccount(provider, resolvedAPIType, resolvedAuthMode, resolvedBaseURL, resolvedAPIKey, modelID); err != nil {
|
||||||
return resolvedAgentAccountInput{}, err
|
return resolvedAgentAccountInput{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,6 +125,8 @@ func resolveAgentAccountInput(provider, apiKey, baseURL string) (resolvedAgentAc
|
|||||||
Provider: provider,
|
Provider: provider,
|
||||||
APIKey: resolvedAPIKey,
|
APIKey: resolvedAPIKey,
|
||||||
BaseURL: resolvedBaseURL,
|
BaseURL: resolvedBaseURL,
|
||||||
|
APIType: resolvedAPIType,
|
||||||
|
AuthMode: resolvedAuthMode,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,26 +383,24 @@ func setOtherConfig(conf map[string]interface{}, config dto.AgentOtherConfig) {
|
|||||||
func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem {
|
func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem {
|
||||||
agentType := agent.AgentType
|
agentType := agent.AgentType
|
||||||
item := dto.AgentItem{
|
item := dto.AgentItem{
|
||||||
ID: agent.ID,
|
ID: agent.ID,
|
||||||
Name: agent.Name,
|
Name: agent.Name,
|
||||||
Remark: agent.Remark,
|
Remark: agent.Remark,
|
||||||
AgentType: agentType,
|
AgentType: agentType,
|
||||||
Provider: agent.Provider,
|
Provider: agent.Provider,
|
||||||
ProviderName: providercatalog.DisplayName(agent.Provider),
|
ProviderName: localizedAgentProviderName(agent.Provider),
|
||||||
Model: agent.Model,
|
Model: agent.Model,
|
||||||
APIType: agent.APIType,
|
APIType: agent.APIType,
|
||||||
MaxTokens: agent.MaxTokens,
|
BaseURL: agent.BaseURL,
|
||||||
ContextWindow: agent.ContextWindow,
|
APIKey: maskKey(agent.APIKey),
|
||||||
BaseURL: agent.BaseURL,
|
Token: agent.Token,
|
||||||
APIKey: maskKey(agent.APIKey),
|
Status: agent.Status,
|
||||||
Token: agent.Token,
|
Message: agent.Message,
|
||||||
Status: agent.Status,
|
AppInstallID: agent.AppInstallID,
|
||||||
Message: agent.Message,
|
WebsiteID: agent.WebsiteID,
|
||||||
AppInstallID: agent.AppInstallID,
|
AccountID: agent.AccountID,
|
||||||
WebsiteID: agent.WebsiteID,
|
ConfigPath: agent.ConfigPath,
|
||||||
AccountID: agent.AccountID,
|
CreatedAt: agent.CreatedAt,
|
||||||
ConfigPath: agent.ConfigPath,
|
|
||||||
CreatedAt: agent.CreatedAt,
|
|
||||||
}
|
}
|
||||||
if appInstall != nil && appInstall.ID > 0 {
|
if appInstall != nil && appInstall.ID > 0 {
|
||||||
item.Container = appInstall.ContainerName
|
item.Container = appInstall.ContainerName
|
||||||
@@ -378,8 +422,8 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
|||||||
item.BridgePort = toInt(bridge)
|
item.BridgePort = toInt(bridge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if agentType == constant.AppHermesAgent {
|
if _, _, ok := agentDashboardAuthEnvKeys(agentType); ok {
|
||||||
auth := readHermesDashboardAuthFromInstall(appInstall)
|
auth := readAgentDashboardAuthFromInstall(appInstall, agentType)
|
||||||
item.DashboardUsername = auth.Username
|
item.DashboardUsername = auth.Username
|
||||||
item.DashboardPassword = auth.Password
|
item.DashboardPassword = auth.Password
|
||||||
}
|
}
|
||||||
@@ -387,6 +431,15 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
|||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func localizedAgentProviderName(provider string) string {
|
||||||
|
if key := providercatalog.DisplayNameKey(provider); key != "" {
|
||||||
|
if name := strings.TrimSpace(i18n.GetMsgByKey(key)); name != "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return providercatalog.DisplayName(provider)
|
||||||
|
}
|
||||||
|
|
||||||
func isAgentAppKey(appKey string) bool {
|
func isAgentAppKey(appKey string) bool {
|
||||||
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
||||||
}
|
}
|
||||||
@@ -684,13 +737,9 @@ type modelProvider struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type modelEntry struct {
|
type modelEntry struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Reasoning bool `json:"reasoning"`
|
Input []string `json:"input,omitempty"`
|
||||||
Input []string `json:"input"`
|
|
||||||
ContextWindow int `json:"contextWindow"`
|
|
||||||
MaxTokens int `json:"maxTokens"`
|
|
||||||
Cost modelCost `json:"cost"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func requiresOpenclawProviderModels(provider string) bool {
|
func requiresOpenclawProviderModels(provider string) bool {
|
||||||
@@ -710,13 +759,6 @@ func applyOpenclawModelsConfig(conf map[string]interface{}, models *modelsConfig
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type modelCost struct {
|
|
||||||
Input float64 `json:"input"`
|
|
||||||
Output float64 `json:"output"`
|
|
||||||
CacheRead float64 `json:"cacheRead"`
|
|
||||||
CacheWrite float64 `json:"cacheWrite"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type browserConfig struct {
|
type browserConfig struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
ExecutablePath string `json:"executablePath"`
|
ExecutablePath string `json:"executablePath"`
|
||||||
@@ -1062,7 +1104,7 @@ func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel s
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) {
|
func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) {
|
||||||
providerPatch, err := providercatalog.BuildOpenClawProviderPatch(account.Provider, model.ID, account.APIType, account.BaseURL, account.APIKey)
|
providerPatch, err := providercatalog.BuildOpenClawProviderPatch(account.Provider, model.ID, account.APIType, account.AuthMode, account.BaseURL, account.APIKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", modelEntry{}, "", modelProvider{}, err
|
return "", modelEntry{}, "", modelProvider{}, err
|
||||||
}
|
}
|
||||||
@@ -1074,28 +1116,24 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen
|
|||||||
}, nil
|
}, 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 {
|
func buildOpenclawModelEntry(modelID string, model dto.AgentAccountModel) modelEntry {
|
||||||
name := strings.TrimSpace(model.Name)
|
name := strings.TrimSpace(model.Name)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = strings.TrimSpace(modelID)
|
name = strings.TrimSpace(modelID)
|
||||||
}
|
}
|
||||||
return modelEntry{
|
entry := modelEntry{ID: strings.TrimSpace(modelID), Name: name}
|
||||||
ID: strings.TrimSpace(modelID),
|
if openclawVisionModelPattern.MatchString(modelID) {
|
||||||
Name: name,
|
entry.Input = []string{"text", "image"}
|
||||||
Reasoning: model.Reasoning,
|
|
||||||
Input: sanitizeAgentAccountModelInputs(model.Input),
|
|
||||||
ContextWindow: model.ContextWindow,
|
|
||||||
MaxTokens: model.MaxTokens,
|
|
||||||
Cost: modelCost{},
|
|
||||||
}
|
}
|
||||||
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
type openclawAccountModelRuntime struct {
|
type openclawAccountModelRuntime struct {
|
||||||
StoredModel string
|
StoredModel string
|
||||||
PrimaryModel string
|
PrimaryModel string
|
||||||
APIType string
|
APIType string
|
||||||
MaxTokens int
|
|
||||||
ContextWindow int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.AgentAccountModel) (openclawAccountModelRuntime, error) {
|
func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.AgentAccountModel) (openclawAccountModelRuntime, error) {
|
||||||
@@ -1104,11 +1142,9 @@ func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.Age
|
|||||||
return openclawAccountModelRuntime{}, err
|
return openclawAccountModelRuntime{}, err
|
||||||
}
|
}
|
||||||
return openclawAccountModelRuntime{
|
return openclawAccountModelRuntime{
|
||||||
StoredModel: model.ID,
|
StoredModel: model.ID,
|
||||||
PrimaryModel: primaryModel,
|
PrimaryModel: primaryModel,
|
||||||
APIType: account.APIType,
|
APIType: account.APIType,
|
||||||
MaxTokens: model.MaxTokens,
|
|
||||||
ContextWindow: model.ContextWindow,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1128,35 +1164,38 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto
|
|||||||
if account == nil {
|
if account == nil {
|
||||||
return nil, fmt.Errorf("account is required")
|
return nil, fmt.Errorf("account is required")
|
||||||
}
|
}
|
||||||
if requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 {
|
if account.Provider != "custom" && requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 {
|
||||||
return nil, buserr.New("ErrAgentAccountSingleInitialModel")
|
return nil, buserr.New("ErrAgentAccountSingleInitialModel")
|
||||||
}
|
}
|
||||||
if len(requested) > 0 {
|
if len(requested) > 0 {
|
||||||
models := make([]dto.AgentAccountModel, 0, len(requested))
|
return normalizeAgentAccountModels(account, requested)
|
||||||
for _, item := range requested {
|
|
||||||
models = append(models, cloneAgentAccountModel(item))
|
|
||||||
}
|
|
||||||
return models, nil
|
|
||||||
}
|
}
|
||||||
meta, ok := providercatalog.Get(account.Provider)
|
defaultModels := providercatalog.DefaultModels(account.Provider, account.APIType)
|
||||||
if !ok || len(meta.Models) == 0 {
|
if len(defaultModels) == 0 {
|
||||||
if requiresInitialAgentAccountModels(account.Provider) {
|
if requiresInitialAgentAccountModels(account.Provider) {
|
||||||
return nil, buserr.New("ErrAgentAccountModelsRequired")
|
return nil, buserr.New("ErrAgentAccountModelsRequired")
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
requested = make([]dto.AgentAccountModel, 0, len(meta.Models))
|
requested = make([]dto.AgentAccountModel, 0, len(defaultModels))
|
||||||
for _, item := range meta.Models {
|
for _, item := range defaultModels {
|
||||||
requested = append(requested, dto.AgentAccountModel{
|
requested = append(requested, dto.AgentAccountModel{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
ContextWindow: item.ContextWindow,
|
|
||||||
MaxTokens: item.MaxTokens,
|
|
||||||
Reasoning: item.Reasoning,
|
|
||||||
Input: append([]string(nil), item.Input...),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return requested, nil
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func compactPersistedAgentAccountModelSortOrder(accountID uint) error {
|
func compactPersistedAgentAccountModelSortOrder(accountID uint) error {
|
||||||
@@ -1184,18 +1223,6 @@ func loadAgentAccountModels(account *model.AgentAccount) ([]dto.AgentAccountMode
|
|||||||
return listPersistedAgentAccountModels(account.ID)
|
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) {
|
func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, existing []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) {
|
||||||
if account == nil {
|
if account == nil {
|
||||||
return nil, fmt.Errorf("account is required")
|
return nil, fmt.Errorf("account is required")
|
||||||
@@ -1219,12 +1246,8 @@ func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, exi
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
requested = append(requested, dto.AgentAccountModel{
|
requested = append(requested, dto.AgentAccountModel{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
ContextWindow: item.ContextWindow,
|
|
||||||
MaxTokens: item.MaxTokens,
|
|
||||||
Reasoning: item.Reasoning,
|
|
||||||
Input: append([]string(nil), item.Input...),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(requested) == len(existing) {
|
if len(requested) == len(existing) {
|
||||||
@@ -1243,18 +1266,10 @@ func listPersistedAgentAccountModels(accountID uint) ([]dto.AgentAccountModel, e
|
|||||||
}
|
}
|
||||||
result := make([]dto.AgentAccountModel, 0, len(rows))
|
result := make([]dto.AgentAccountModel, 0, len(rows))
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
inputs := []string{}
|
|
||||||
if strings.TrimSpace(row.Input) != "" {
|
|
||||||
_ = json.Unmarshal([]byte(row.Input), &inputs)
|
|
||||||
}
|
|
||||||
result = append(result, dto.AgentAccountModel{
|
result = append(result, dto.AgentAccountModel{
|
||||||
RecordID: row.ID,
|
RecordID: row.ID,
|
||||||
ID: strings.TrimSpace(row.Model),
|
ID: strings.TrimSpace(row.Model),
|
||||||
Name: strings.TrimSpace(row.Name),
|
Name: strings.TrimSpace(row.Name),
|
||||||
ContextWindow: row.ContextWindow,
|
|
||||||
MaxTokens: row.MaxTokens,
|
|
||||||
Reasoning: row.Reasoning,
|
|
||||||
Input: sanitizeAgentAccountModelInputs(inputs),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
@@ -1265,19 +1280,11 @@ func replacePersistedAgentAccountModelsWithTx(tx *gorm.DB, accountID uint, model
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for index, item := range models {
|
for index, item := range models {
|
||||||
inputPayload, err := json.Marshal(sanitizeAgentAccountModelInputs(item.Input))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
record := &model.AgentAccountModel{
|
record := &model.AgentAccountModel{
|
||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
Model: strings.TrimSpace(item.ID),
|
Model: strings.TrimSpace(item.ID),
|
||||||
Name: strings.TrimSpace(item.Name),
|
Name: strings.TrimSpace(item.Name),
|
||||||
ContextWindow: item.ContextWindow,
|
SortOrder: index + 1,
|
||||||
MaxTokens: item.MaxTokens,
|
|
||||||
Reasoning: item.Reasoning,
|
|
||||||
Input: string(inputPayload),
|
|
||||||
SortOrder: index + 1,
|
|
||||||
}
|
}
|
||||||
if err := tx.Create(record).Error; err != nil {
|
if err := tx.Create(record).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1318,86 +1325,17 @@ func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAcco
|
|||||||
if modelID == "" {
|
if modelID == "" {
|
||||||
return dto.AgentAccountModel{}, fmt.Errorf("model is required")
|
return dto.AgentAccountModel{}, fmt.Errorf("model is required")
|
||||||
}
|
}
|
||||||
|
modelID = providercatalog.NormalizeModelID(account.Provider, modelID)
|
||||||
name := strings.TrimSpace(model.Name)
|
name := strings.TrimSpace(model.Name)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = modelID
|
name = modelID
|
||||||
}
|
}
|
||||||
inputs := sanitizeAgentAccountModelInputs(model.Input)
|
|
||||||
return dto.AgentAccountModel{
|
return dto.AgentAccountModel{
|
||||||
ID: normalizeAgentAccountModelID(account.Provider, modelID),
|
ID: modelID,
|
||||||
Name: name,
|
Name: name,
|
||||||
ContextWindow: model.ContextWindow,
|
|
||||||
MaxTokens: model.MaxTokens,
|
|
||||||
Reasoning: model.Reasoning,
|
|
||||||
Input: inputs,
|
|
||||||
}, nil
|
}, 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 {
|
func requiresInitialAgentAccountModels(provider string) bool {
|
||||||
switch provider {
|
switch provider {
|
||||||
case "custom", "vllm", "ollama":
|
case "custom", "vllm", "ollama":
|
||||||
@@ -1407,36 +1345,14 @@ 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 {
|
func sameProviderModelID(provider, left, right string) bool {
|
||||||
leftTrimmed := strings.TrimSpace(left)
|
leftTrimmed := strings.TrimSpace(left)
|
||||||
rightTrimmed := strings.TrimSpace(right)
|
rightTrimmed := strings.TrimSpace(right)
|
||||||
if leftTrimmed == rightTrimmed {
|
if leftTrimmed == rightTrimmed {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
leftComparable := normalizeComparableProviderModelID(provider, leftTrimmed)
|
leftComparable := providercatalog.NormalizeModelID(provider, leftTrimmed)
|
||||||
rightComparable := normalizeComparableProviderModelID(provider, rightTrimmed)
|
rightComparable := providercatalog.NormalizeModelID(provider, rightTrimmed)
|
||||||
return leftComparable != "" && leftComparable == rightComparable
|
return leftComparable != "" && leftComparable == rightComparable
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1457,6 +1373,20 @@ func requireAgentAccountModelForProvider(provider string, models []dto.AgentAcco
|
|||||||
return selectedAccountModel, nil
|
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 {
|
func ensureAccountModelsNotBound(account *model.AgentAccount, models []dto.AgentAccountModel) error {
|
||||||
if account == nil || account.ID == 0 {
|
if account == nil || account.ID == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -1533,6 +1463,87 @@ func readInstallEnv(envStr string) map[string]interface{} {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
hermesDashboardUsernameEnvKey = "HERMES_DASHBOARD_USERNAME"
|
||||||
|
hermesDashboardPasswordEnvKey = "HERMES_DASHBOARD_PASSWORD"
|
||||||
|
qwenPawAuthEnabledEnvKey = "QWENPAW_AUTH_ENABLED"
|
||||||
|
qwenPawAuthUsernameEnvKey = "QWENPAW_AUTH_USERNAME"
|
||||||
|
qwenPawAuthPasswordEnvKey = "QWENPAW_AUTH_PASSWORD"
|
||||||
|
)
|
||||||
|
|
||||||
|
type agentDashboardAuth struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAgentDashboardAuth(username, password string) agentDashboardAuth {
|
||||||
|
auth := agentDashboardAuth{
|
||||||
|
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 agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
|
||||||
|
switch agentType {
|
||||||
|
case constant.AppHermesAgent:
|
||||||
|
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
|
||||||
|
case constant.AppCopaw:
|
||||||
|
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
|
||||||
|
default:
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAgentDashboardAuthEnv(envPath, agentType string, auth agentDashboardAuth, overwrite bool) error {
|
||||||
|
usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("dashboard auth is not supported for %s", agentType)
|
||||||
|
}
|
||||||
|
values := map[string]string{
|
||||||
|
usernameKey: auth.Username,
|
||||||
|
passwordKey: auth.Password,
|
||||||
|
}
|
||||||
|
order := []string{usernameKey, passwordKey}
|
||||||
|
if agentType == constant.AppCopaw {
|
||||||
|
values[qwenPawAuthEnabledEnvKey] = "true"
|
||||||
|
order = append([]string{qwenPawAuthEnabledEnvKey}, order...)
|
||||||
|
}
|
||||||
|
return upsertAgentEnv(envPath, values, order, overwrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAgentDashboardAuthEnv(envPath, agentType string) (agentDashboardAuth, error) {
|
||||||
|
usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType)
|
||||||
|
if !ok {
|
||||||
|
return agentDashboardAuth{}, fmt.Errorf("dashboard auth is not supported for %s", agentType)
|
||||||
|
}
|
||||||
|
envMap, err := readAgentEnvMap(envPath)
|
||||||
|
if err != nil {
|
||||||
|
return agentDashboardAuth{}, err
|
||||||
|
}
|
||||||
|
return agentDashboardAuth{
|
||||||
|
Username: strings.TrimSpace(envMap[usernameKey]),
|
||||||
|
Password: strings.TrimSpace(envMap[passwordKey]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAgentDashboardAuthFromInstall(appInstall *model.AppInstall, agentType string) agentDashboardAuth {
|
||||||
|
if appInstall == nil || appInstall.ID == 0 {
|
||||||
|
return agentDashboardAuth{}
|
||||||
|
}
|
||||||
|
auth, err := readAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType)
|
||||||
|
if err != nil {
|
||||||
|
return agentDashboardAuth{}
|
||||||
|
}
|
||||||
|
return auth
|
||||||
|
}
|
||||||
|
|
||||||
func readAgentEnvMap(envPath string) (map[string]string, error) {
|
func readAgentEnvMap(envPath string) (map[string]string, error) {
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
if !fileOp.Stat(envPath) {
|
if !fileOp.Stat(envPath) {
|
||||||
@@ -1595,57 +1606,6 @@ 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 {
|
func generateToken() string {
|
||||||
bytes := make([]byte, 24)
|
bytes := make([]byte, 24)
|
||||||
if _, err := rand.Read(bytes); err != nil {
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
|
|||||||
@@ -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)
|
websiteMap := make(map[uint]model.Website)
|
||||||
duplicateAppInstallIDs := make(map[uint]struct{})
|
duplicateAppInstallIDs := make(map[uint]struct{})
|
||||||
for _, website := range websites {
|
for _, website := range websites {
|
||||||
@@ -182,10 +182,6 @@ func uniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]m
|
|||||||
return websiteMap
|
return websiteMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func UniqueDeploymentWebsiteMapForMigration(websites []model.Website) map[uint]model.Website {
|
|
||||||
return uniqueDeploymentWebsiteMapByAppInstall(websites)
|
|
||||||
}
|
|
||||||
|
|
||||||
func uniqueUintList(items []uint) []uint {
|
func uniqueUintList(items []uint) []uint {
|
||||||
itemMap := make(map[uint]struct{}, len(items))
|
itemMap := make(map[uint]struct{}, len(items))
|
||||||
uniq := make([]uint, 0, len(items))
|
uniq := make([]uint, 0, len(items))
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
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"))
|
||||||
|
}
|
||||||
@@ -384,6 +384,9 @@ func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.Ale
|
|||||||
if search.Count != 0 {
|
if search.Count != 0 {
|
||||||
opts = append(opts, alertRepo.WithByCount(search.Count))
|
opts = append(opts, alertRepo.WithByCount(search.Count))
|
||||||
}
|
}
|
||||||
|
if !search.StartTime.IsZero() && !search.EndTime.IsZero() {
|
||||||
|
opts = append(opts, repo.WithByCreatedAt(search.StartTime, search.EndTime))
|
||||||
|
}
|
||||||
opts = append(opts, repo.WithOrderDesc("created_at"))
|
opts = append(opts, repo.WithOrderDesc("created_at"))
|
||||||
|
|
||||||
total, alerts, err := alertRepo.PageLog(search.Page, search.PageSize, opts...)
|
total, alerts, err := alertRepo.PageLog(search.Page, search.PageSize, opts...)
|
||||||
|
|||||||
@@ -303,6 +303,9 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
|||||||
filename := filepath.Base(appDetailDTO.DownloadUrl)
|
filename := filepath.Base(appDetailDTO.DownloadUrl)
|
||||||
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml")
|
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml")
|
||||||
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||||
|
if statusCode == http.StatusNotFound {
|
||||||
|
return appDetailDTO, buserr.New("ErrAppVersionUnavailable")
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return appDetailDTO, buserr.WithDetail("ErrGetCompose", err.Error(), err)
|
return appDetailDTO, buserr.WithDetail("ErrGetCompose", err.Error(), err)
|
||||||
}
|
}
|
||||||
@@ -426,7 +429,12 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
} else {
|
} else {
|
||||||
if appDetail.DockerCompose == "" {
|
if appDetail.DockerCompose == "" {
|
||||||
dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version)
|
dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version)
|
||||||
_, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
var statusCode int
|
||||||
|
statusCode, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||||
|
if statusCode == http.StatusNotFound {
|
||||||
|
err = buserr.New("ErrAppVersionUnavailable")
|
||||||
|
return
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,6 +601,9 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
|||||||
filename := filepath.Base(detail.DownloadUrl)
|
filename := filepath.Base(detail.DownloadUrl)
|
||||||
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml")
|
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml")
|
||||||
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||||
|
if statusCode == http.StatusNotFound {
|
||||||
|
return versions, buserr.New("ErrAppVersionUnavailable")
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return versions, err
|
return versions, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,928 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
|
"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/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type appUpgradePhase int
|
||||||
|
|
||||||
|
const (
|
||||||
|
appUpgradePreparing appUpgradePhase = iota
|
||||||
|
appUpgradePrepared
|
||||||
|
appUpgradeStopped
|
||||||
|
appUpgradeBackedUp
|
||||||
|
appUpgradeDown
|
||||||
|
appUpgradeMutated
|
||||||
|
appUpgradeStarted
|
||||||
|
appUpgradeReady
|
||||||
|
appUpgradeCommitted
|
||||||
|
)
|
||||||
|
|
||||||
|
const composeServiceLabel = "com.docker.compose.service"
|
||||||
|
|
||||||
|
var appUpgradeLocks sync.Map
|
||||||
|
|
||||||
|
type appUpgradeSnapshot interface {
|
||||||
|
Restore() error
|
||||||
|
Cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
type upgradeFileSnapshot struct {
|
||||||
|
installPath string
|
||||||
|
backupPath string
|
||||||
|
paths []string
|
||||||
|
existing map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type appUpgradeContext struct {
|
||||||
|
req request.AppInstallUpgrade
|
||||||
|
original model.AppInstall
|
||||||
|
candidate model.AppInstall
|
||||||
|
detail model.AppDetail
|
||||||
|
|
||||||
|
phase appUpgradePhase
|
||||||
|
stopAttempted bool
|
||||||
|
downAttempted bool
|
||||||
|
rollbackErr error
|
||||||
|
|
||||||
|
detailDir string
|
||||||
|
stageDir string
|
||||||
|
envContent []byte
|
||||||
|
oldEnvContent []byte
|
||||||
|
oldDockerCompose string
|
||||||
|
oldImageIDs []appImageID
|
||||||
|
backupFile string
|
||||||
|
snapshot appUpgradeSnapshot
|
||||||
|
createdPaths []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||||
|
install, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if install.Status == constant.StatusUpgrading {
|
||||||
|
return buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, install.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, loaded := appUpgradeLocks.LoadOrStore(install.ID, struct{}{}); loaded {
|
||||||
|
return buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
releaseLock := true
|
||||||
|
defer func() {
|
||||||
|
if releaseLock {
|
||||||
|
appUpgradeLocks.Delete(install.ID)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if install.App.Key == vllmAppKeyForUpgrade && !isVllmUpgradeVersionAllowed(install.Version, detail.Version, loadVllmImageFromEnv(install.Env)) {
|
||||||
|
return errors.New("vLLM can only upgrade within the same image type")
|
||||||
|
}
|
||||||
|
if install.Version == detail.Version {
|
||||||
|
return errors.New("two version is same")
|
||||||
|
}
|
||||||
|
|
||||||
|
upgradeTask, err := task.NewTaskWithOps(install.Name, task.TaskUpgrade, task.TaskScopeApp, req.TaskID, install.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx := &appUpgradeContext{
|
||||||
|
req: req,
|
||||||
|
original: install,
|
||||||
|
candidate: install,
|
||||||
|
detail: detail,
|
||||||
|
phase: appUpgradePreparing,
|
||||||
|
oldDockerCompose: install.DockerCompose,
|
||||||
|
}
|
||||||
|
upgradeTask.AddSubTaskWithOps(i18n.GetMsgByKey("UpgradePrepare"), ctx.prepare, nil, 0, 0)
|
||||||
|
upgradeTask.AddSubTaskWithOps(
|
||||||
|
task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp),
|
||||||
|
ctx.cutover,
|
||||||
|
func(t *task.Task) {
|
||||||
|
ctx.rollbackErr = ctx.rollback(t)
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
upgradingInstall := install
|
||||||
|
upgradingInstall.Status = constant.StatusUpgrading
|
||||||
|
upgradingInstall.Message = ""
|
||||||
|
if err = appInstallRepo.Save(context.Background(), &upgradingInstall); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseLock = false
|
||||||
|
go func() {
|
||||||
|
defer appUpgradeLocks.Delete(install.ID)
|
||||||
|
defer ctx.cleanup()
|
||||||
|
taskErr := upgradeTask.Execute()
|
||||||
|
if taskErr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctx.rollbackErr != nil {
|
||||||
|
taskErr = fmt.Errorf("%w; %s: %v", taskErr, i18n.GetMsgByKey("UpgradeRollbackFailed"), ctx.rollbackErr)
|
||||||
|
upgradeTask.Task.ErrorMsg = taskErr.Error()
|
||||||
|
_ = repo.NewITaskRepo().Update(context.Background(), upgradeTask.Task)
|
||||||
|
}
|
||||||
|
if !ctx.stopAttempted || ctx.rollbackErr == nil {
|
||||||
|
restored := ctx.original
|
||||||
|
_ = appInstallRepo.Save(context.Background(), &restored)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
failed := ctx.original
|
||||||
|
failed.Status = constant.StatusUpgradeErr
|
||||||
|
failed.Message = taskErr.Error()
|
||||||
|
_ = appInstallRepo.Save(context.Background(), &failed)
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) prepare(t *task.Task) error {
|
||||||
|
fileOp := files.NewFileOp()
|
||||||
|
u.detailDir = path.Join(u.original.App.GetAppResourcePath(), u.detail.Version)
|
||||||
|
if u.original.App.Resource == constant.AppResourceRemote {
|
||||||
|
if err := downloadApp(u.original.App, u.detail, nil, t.Logger); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !fileOp.Stat(u.detailDir) {
|
||||||
|
return buserr.WithName("ErrFileNotFound", u.detailDir)
|
||||||
|
}
|
||||||
|
if u.detail.DockerCompose == "" {
|
||||||
|
composeContent, err := fileOp.GetContent(path.Join(u.detailDir, "docker-compose.yml"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.detail.DockerCompose = string(composeContent)
|
||||||
|
_ = appDetailRepo.Update(context.Background(), u.detail)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(u.detail.DockerCompose) == "" && strings.TrimSpace(u.req.DockerCompose) == "" {
|
||||||
|
return buserr.WithName("ErrFileNotFound", "docker-compose.yml")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
u.oldEnvContent, err = fileOp.GetContent(u.original.GetEnvPath())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.stageDir, err = os.MkdirTemp(u.original.GetAppPath(), "."+u.original.Name+"-upgrade-")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = fileOp.CopyDirWithNewName(u.detailDir, u.stageDir, "."); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, ".env"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if u.original.App.Key == constant.AppOpenclaw {
|
||||||
|
if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, path.Join("data", "conf", "openclaw.json")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if u.original.App.Key == constant.AppOpenresty {
|
||||||
|
for _, relativePath := range []string{
|
||||||
|
nginxModuleBuildDir,
|
||||||
|
nginxModuleModulesDir,
|
||||||
|
path.Join(nginxModuleConfDir, nginxModuleEnabledConfDir),
|
||||||
|
} {
|
||||||
|
if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, relativePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stagedInstall := u.original
|
||||||
|
stagedInstall.Name = path.Base(u.stageDir)
|
||||||
|
stagedInstall.Version = u.detail.Version
|
||||||
|
stagedInstall.AppDetailId = u.req.DetailID
|
||||||
|
if stagedInstall.App.Key == vllmAppKeyForUpgrade {
|
||||||
|
envs := make(map[string]interface{})
|
||||||
|
if err = json.Unmarshal([]byte(stagedInstall.Env), &envs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
image := buildVllmUpgradeImage(loadVllmImageFromEnv(stagedInstall.Env), u.original.Version, u.detail.Version)
|
||||||
|
envs[vllmImageEnvKey] = image
|
||||||
|
paramBytes, marshalErr := json.Marshal(envs)
|
||||||
|
if marshalErr != nil {
|
||||||
|
return marshalErr
|
||||||
|
}
|
||||||
|
stagedInstall.Env = string(paramBytes)
|
||||||
|
}
|
||||||
|
if err = migrateOpenclawProtocolUpgrade(&stagedInstall, u.original.Version, u.detail.Version); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.candidate = stagedInstall
|
||||||
|
u.candidate.Name = u.original.Name
|
||||||
|
u.candidate.DockerCompose, err = renderUpgradeCompose(u.candidate, u.detail, u.req.DockerCompose)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(u.candidate.DockerCompose) == "" {
|
||||||
|
return buserr.WithName("ErrFileNotFound", "docker-compose.yml")
|
||||||
|
}
|
||||||
|
|
||||||
|
u.envContent, err = renderUpgradeEnv(&u.candidate, u.oldEnvContent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = writeUpgradeFile(path.Join(u.stageDir, ".env"), u.envContent, constant.FilePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = writeUpgradeFile(path.Join(u.stageDir, "docker-compose.yml"), []byte(u.candidate.DockerCompose), constant.FilePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
project, err := docker.GetComposeProject(u.original.Name, u.stageDir, []byte(u.candidate.DockerCompose), u.envContent, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hasBuild := false
|
||||||
|
for _, service := range project.Services {
|
||||||
|
if service.Image == "" && service.Build == nil {
|
||||||
|
return fmt.Errorf("compose service %s has neither image nor build configuration", service.Name)
|
||||||
|
}
|
||||||
|
hasBuild = hasBuild || service.Build != nil
|
||||||
|
}
|
||||||
|
if u.req.DeleteImage {
|
||||||
|
dockerClient, clientErr := docker.NewClient()
|
||||||
|
if clientErr != nil {
|
||||||
|
return clientErr
|
||||||
|
}
|
||||||
|
u.oldImageIDs, err = getAppImageIDsByCompose(dockerClient, u.oldEnvContent, []byte(u.oldDockerCompose))
|
||||||
|
dockerClient.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
images := make([]string, 0, len(project.Services))
|
||||||
|
for _, service := range project.Services {
|
||||||
|
if service.Image != "" {
|
||||||
|
images = append(images, service.Image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = prepareUpgradeImages(t, images, u.req.PullImage); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if u.candidate.App.Key == constant.AppOpenresty {
|
||||||
|
if err = u.prepareOpenresty(t, stagedInstall); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = verifyUpgradeImages(images); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if hasBuild {
|
||||||
|
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image"))
|
||||||
|
t.LogStart(logStr)
|
||||||
|
if err = compose.BuildWithTask(path.Join(u.stageDir, "docker-compose.yml"), project.Name, t); err != nil {
|
||||||
|
t.LogFailedWithErr(logStr, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.LogSuccess(logStr)
|
||||||
|
if err = verifyUpgradeImages(images); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if u.original.App.Resource == constant.AppResourceRemote {
|
||||||
|
go RequestDownloadCallBack(u.detail.DownloadCallBackUrl)
|
||||||
|
}
|
||||||
|
u.phase = appUpgradePrepared
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) prepareOpenresty(t *task.Task, stagedInstall model.AppInstall) error {
|
||||||
|
fileOp := files.NewFileOp()
|
||||||
|
detailBuildDir := path.Join(u.detailDir, nginxModuleBuildDir)
|
||||||
|
installBuildDir := path.Join(u.stageDir, nginxModuleBuildDir)
|
||||||
|
if !fileOp.Stat(installBuildDir) {
|
||||||
|
if err := fileOp.CreateDir(installBuildDir, constant.DirPerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := copyAppDetailMissing(fileOp, detailBuildDir, installBuildDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := fileOp.DeleteDir(path.Join(installBuildDir, nginxModuleTmpDir)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := fileOp.CopyDir(path.Join(detailBuildDir, nginxModuleTmpDir), installBuildDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, fileName := range []string{"Dockerfile", "nginx.conf", "nginx.vh.default.conf"} {
|
||||||
|
if err := fileOp.CopyFile(path.Join(detailBuildDir, fileName), 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)
|
||||||
|
}
|
||||||
|
targetCatalogPath := path.Join(installBuildDir, nginxModuleCatalogPendingFile)
|
||||||
|
if err := stageNginxModuleCatalog(targetCatalogSource, targetCatalogPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stagedInstall.Name = path.Base(u.stageDir)
|
||||||
|
stagedInstall.Version = u.candidate.Version
|
||||||
|
stagedInstall.Env = u.candidate.Env
|
||||||
|
stagedInstall.DockerCompose = u.candidate.DockerCompose
|
||||||
|
return buildNginx(t, stagedInstall, targetCatalogPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) cutover(t *task.Task) error {
|
||||||
|
u.stopAttempted = true
|
||||||
|
t.LogStart(i18n.GetMsgByKey("UpgradeStop"))
|
||||||
|
if out, err := compose.Stop(u.original.GetComposePath()); err != nil {
|
||||||
|
if out != "" {
|
||||||
|
err = fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeStop"), err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.LogSuccess(i18n.GetMsgByKey("UpgradeStop"))
|
||||||
|
u.phase = appUpgradeStopped
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if u.original.App.Key == constant.AppOpenresty {
|
||||||
|
u.snapshot, err = createOpenrestyUpgradeSnapshot(u.original.GetPath())
|
||||||
|
} else {
|
||||||
|
snapshotPaths := []string{".env", "docker-compose.yml", "scripts"}
|
||||||
|
if u.original.App.Key == constant.AppOpenclaw {
|
||||||
|
snapshotPaths = append(snapshotPaths, path.Join("data", "conf", "openclaw.json"))
|
||||||
|
}
|
||||||
|
u.snapshot, err = createUpgradeFileSnapshot(u.original.GetPath(), snapshotPaths)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.req.Backup {
|
||||||
|
if err = u.backup(t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.phase = appUpgradeBackedUp
|
||||||
|
} else {
|
||||||
|
t.Log(i18n.GetMsgByKey("UpgradeBackupDisabled"))
|
||||||
|
}
|
||||||
|
|
||||||
|
u.downAttempted = true
|
||||||
|
if out, downErr := compose.Down(u.original.GetComposePath()); downErr != nil {
|
||||||
|
if out != "" {
|
||||||
|
downErr = fmt.Errorf("%s: %w", out, downErr)
|
||||||
|
}
|
||||||
|
return downErr
|
||||||
|
}
|
||||||
|
u.phase = appUpgradeDown
|
||||||
|
|
||||||
|
u.phase = appUpgradeMutated
|
||||||
|
if err = u.applyStagedFiles(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = writeUpgradeFile(u.original.GetEnvPath(), u.envContent, constant.FilePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = runScript(t, &u.candidate, "upgrade"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = writeUpgradeFile(u.original.GetComposePath(), []byte(u.candidate.DockerCompose), constant.FilePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("Run"), i18n.GetMsgByKey("App"))
|
||||||
|
t.LogStart(logStr)
|
||||||
|
if out, upErr := compose.UpWithoutPull(u.original.GetComposePath()); upErr != nil {
|
||||||
|
if out != "" {
|
||||||
|
upErr = fmt.Errorf("%s: %w", out, upErr)
|
||||||
|
}
|
||||||
|
t.LogFailedWithErr(logStr, upErr)
|
||||||
|
return upErr
|
||||||
|
}
|
||||||
|
t.LogSuccess(logStr)
|
||||||
|
u.phase = appUpgradeStarted
|
||||||
|
|
||||||
|
t.LogStart(i18n.GetMsgByKey("UpgradeWaitReady"))
|
||||||
|
containerNames, err := waitAppContainersReady(context.Background(), u.candidate)
|
||||||
|
if err != nil {
|
||||||
|
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeWaitReady"), err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.LogSuccess(i18n.GetMsgByKey("UpgradeWaitReady"))
|
||||||
|
u.phase = appUpgradeReady
|
||||||
|
u.candidate.ContainerName = strings.Join(containerNames, ",")
|
||||||
|
u.candidate.Status = constant.StatusRunning
|
||||||
|
u.candidate.Message = ""
|
||||||
|
|
||||||
|
if u.candidate.App.Key == constant.AppOpenresty {
|
||||||
|
liveCatalogPath := path.Join(u.candidate.GetPath(), nginxModuleBuildDir, nginxModuleCatalogPendingFile)
|
||||||
|
if err = commitStaticNginxModuleBuilds(u.candidate, liveCatalogPath, t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
activeCatalogPath := path.Join(u.candidate.GetPath(), nginxModuleBuildDir, nginxModuleCatalogFile)
|
||||||
|
if err = activateNginxModuleCatalogAndCommit(liveCatalogPath, activeCatalogPath, func() error {
|
||||||
|
return appInstallRepo.Save(context.Background(), &u.candidate)
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err = appInstallRepo.Save(context.Background(), &u.candidate); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.phase = appUpgradeCommitted
|
||||||
|
u.deleteOldImages(t)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) backup(t *task.Task) error {
|
||||||
|
fileName := fmt.Sprintf("upgrade_backup_%s_%s.tar.gz", u.original.Name, time.Now().Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
|
||||||
|
record, err := backupAppWithParentTask(&u.original, t, fileName)
|
||||||
|
if err != nil {
|
||||||
|
return buserr.WithNameAndErr("ErrAppBackup", u.original.Name, err)
|
||||||
|
}
|
||||||
|
u.backupFile = path.Join(global.Dir.LocalBackupDir, record.FileDir, record.FileName)
|
||||||
|
info, err := os.Stat(u.backupFile)
|
||||||
|
if err != nil || info.Size() == 0 || record.Status != constant.StatusSuccess {
|
||||||
|
if err == nil {
|
||||||
|
err = errors.New("backup archive is empty or incomplete")
|
||||||
|
}
|
||||||
|
markBackupFailed(record.ID, err)
|
||||||
|
return buserr.WithNameAndErr("ErrAppBackup", u.original.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
backupRecordService := NewIBackupRecordService()
|
||||||
|
backups, _ := backupRecordService.ListAppRecords(u.original.App.Key, u.original.Name, "upgrade_backup")
|
||||||
|
if len(backups) > 3 {
|
||||||
|
deleteIDs := make([]uint, 0, len(backups)-3)
|
||||||
|
for _, backup := range backups[:len(backups)-3] {
|
||||||
|
deleteIDs = append(deleteIDs, backup.ID)
|
||||||
|
}
|
||||||
|
_ = backupRecordService.BatchDeleteRecord(deleteIDs)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) applyStagedFiles() error {
|
||||||
|
fileOp := files.NewFileOp()
|
||||||
|
if err := copyAppDetailMissingTracked(fileOp, u.detailDir, u.original.GetPath(), &u.createdPaths); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := replaceUpgradePath(u.stageDir, u.original.GetPath(), "scripts"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if u.original.App.Key == constant.AppOpenclaw {
|
||||||
|
if err := replaceUpgradePath(u.stageDir, u.original.GetPath(), path.Join("data", "conf", "openclaw.json")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if u.original.App.Key == constant.AppOpenresty {
|
||||||
|
for _, relativePath := range []string{
|
||||||
|
nginxModuleBuildDir,
|
||||||
|
nginxModuleModulesDir,
|
||||||
|
path.Join(nginxModuleConfDir, nginxModuleEnabledConfDir),
|
||||||
|
path.Join(nginxModuleConfDir, "nginx.conf"),
|
||||||
|
} {
|
||||||
|
if err := replaceUpgradePath(u.stageDir, u.original.GetPath(), relativePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
|
||||||
|
if !u.stopAttempted {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
logStr := i18n.GetWithName("AppRecover", u.original.Name)
|
||||||
|
t.LogStart(logStr)
|
||||||
|
defer func() {
|
||||||
|
if rollbackErr != nil {
|
||||||
|
t.LogFailedWithErr(logStr, rollbackErr)
|
||||||
|
} else {
|
||||||
|
t.LogSuccess(logStr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if !u.downAttempted {
|
||||||
|
if out, err := compose.Operate(u.original.GetComposePath(), "start"); err != nil {
|
||||||
|
if out != "" {
|
||||||
|
err = fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return u.finishRollback()
|
||||||
|
}
|
||||||
|
if u.phase < appUpgradeMutated {
|
||||||
|
if out, err := compose.UpWithoutPull(u.original.GetComposePath()); err != nil {
|
||||||
|
if out != "" {
|
||||||
|
err = fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return u.finishRollback()
|
||||||
|
}
|
||||||
|
|
||||||
|
if out, err := compose.Down(u.original.GetComposePath()); err != nil {
|
||||||
|
if out != "" {
|
||||||
|
err = fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
rollbackErr = err
|
||||||
|
}
|
||||||
|
if u.backupFile != "" {
|
||||||
|
_ = u.restoreManagedFiles()
|
||||||
|
if err := handleAppRecover(&u.original, t, u.backupFile, true, "", ""); err != nil {
|
||||||
|
_, _ = compose.UpWithoutPull(u.original.GetComposePath())
|
||||||
|
return errors.Join(rollbackErr, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := u.restoreManagedFiles(); err != nil {
|
||||||
|
return errors.Join(rollbackErr, err)
|
||||||
|
}
|
||||||
|
if out, err := compose.UpWithoutPull(u.original.GetComposePath()); err != nil {
|
||||||
|
if out != "" {
|
||||||
|
err = fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
return errors.Join(rollbackErr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.Join(rollbackErr, u.finishRollback())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) finishRollback() error {
|
||||||
|
if _, err := waitAppContainersReady(context.Background(), u.original); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
restored := u.original
|
||||||
|
if err := appInstallRepo.Save(context.Background(), &restored); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) restoreManagedFiles() error {
|
||||||
|
var restoreErr error
|
||||||
|
if u.snapshot != nil {
|
||||||
|
restoreErr = u.snapshot.Restore()
|
||||||
|
}
|
||||||
|
for index := len(u.createdPaths) - 1; index >= 0; index-- {
|
||||||
|
if err := os.RemoveAll(u.createdPaths[index]); err != nil {
|
||||||
|
restoreErr = errors.Join(restoreErr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return restoreErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) deleteOldImages(t *task.Task) {
|
||||||
|
if !u.req.DeleteImage {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
excludeImages, err := docker.GetImagesFromDockerCompose(u.envContent, []byte(u.candidate.DockerCompose))
|
||||||
|
if err != nil {
|
||||||
|
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dockerClient, err := docker.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dockerClient.Close()
|
||||||
|
if err = deleteAppImagesByIDs(t, dockerClient, u.oldImageIDs, excludeImages); err != nil {
|
||||||
|
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) cleanup() {
|
||||||
|
if u.snapshot != nil {
|
||||||
|
u.snapshot.Cleanup()
|
||||||
|
}
|
||||||
|
if u.stageDir != "" {
|
||||||
|
_ = os.RemoveAll(u.stageDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type upgradeImageClient interface {
|
||||||
|
PullImageWithProcess(*task.Task, string) error
|
||||||
|
ImageExists(string) (bool, error)
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareUpgradeImages(t *task.Task, images []string, pull bool) error {
|
||||||
|
dockerClient, err := docker.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return prepareUpgradeImagesWithClient(t, dockerClient, images, pull)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareUpgradeImagesWithClient(t *task.Task, dockerClient upgradeImageClient, images []string, pull bool) error {
|
||||||
|
defer dockerClient.Close()
|
||||||
|
seen := make(map[string]struct{}, len(images))
|
||||||
|
for _, image := range images {
|
||||||
|
image = strings.TrimSpace(image)
|
||||||
|
if image == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[image]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[image] = struct{}{}
|
||||||
|
if pull {
|
||||||
|
if t != nil {
|
||||||
|
t.Log(i18n.GetWithName("PullImageStart", image))
|
||||||
|
}
|
||||||
|
if pullErr := dockerClient.PullImageWithProcess(t, image); pullErr != nil {
|
||||||
|
if exists, _ := dockerClient.ImageExists(image); exists {
|
||||||
|
if t != nil {
|
||||||
|
t.Log(i18n.GetMsgByKey("UseExistImage"))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return buserr.WithNameAndErr("ErrDockerPullImage", "", pullErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exists, inspectErr := dockerClient.ImageExists(image)
|
||||||
|
if inspectErr != nil || !exists {
|
||||||
|
return buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s is not available locally: %v", image, inspectErr))
|
||||||
|
}
|
||||||
|
if pull && t != nil {
|
||||||
|
t.LogSuccess(i18n.GetMsgByKey("PullImage"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyUpgradeImages(images []string) error {
|
||||||
|
dockerClient, err := docker.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dockerClient.Close()
|
||||||
|
for _, image := range images {
|
||||||
|
exists, inspectErr := dockerClient.ImageExists(image)
|
||||||
|
if inspectErr != nil || !exists {
|
||||||
|
return buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s is not available locally: %v", image, inspectErr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderUpgradeEnv(install *model.AppInstall, original []byte) ([]byte, error) {
|
||||||
|
originalEnv := make(map[string]string)
|
||||||
|
if len(original) > 0 {
|
||||||
|
var err error
|
||||||
|
originalEnv, err = godotenv.UnmarshalBytes(original)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
params := make(map[string]string, len(originalEnv))
|
||||||
|
maps.Copy(params, originalEnv)
|
||||||
|
envs := make(map[string]interface{})
|
||||||
|
if err := json.Unmarshal([]byte(install.Env), &envs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
handleMap(envs, params)
|
||||||
|
if install.App.Key == constant.AppOpenresty {
|
||||||
|
for _, key := range []string{"CONTAINER_PACKAGE_URL", "RESTY_ADD_PACKAGE_BUILDDEPS", "RESTY_CONFIG_OPTIONS_MORE"} {
|
||||||
|
if value, ok := originalEnv[key]; ok {
|
||||||
|
params[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if websiteDir := strings.TrimSpace(originalEnv["WEBSITE_DIR"]); websiteDir != "" {
|
||||||
|
params["WEBSITE_DIR"] = websiteDir
|
||||||
|
}
|
||||||
|
websiteDir := strings.TrimSpace(params["WEBSITE_DIR"])
|
||||||
|
if websiteDir == "" {
|
||||||
|
websiteDir = NewISettingService().GetWebsiteDir()
|
||||||
|
}
|
||||||
|
if !path.IsAbs(websiteDir) {
|
||||||
|
websiteDir = path.Join(global.Dir.DataDir, websiteDir)
|
||||||
|
}
|
||||||
|
params["WEBSITE_DIR"] = websiteDir
|
||||||
|
envs["WEBSITE_DIR"] = websiteDir
|
||||||
|
content, marshalErr := json.Marshal(envs)
|
||||||
|
if marshalErr != nil {
|
||||||
|
return nil, marshalErr
|
||||||
|
}
|
||||||
|
install.Env = string(content)
|
||||||
|
}
|
||||||
|
content, err := godotenv.Marshal(params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []byte(content), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderUpgradeCompose(install model.AppInstall, detail model.AppDetail, customCompose string) (string, error) {
|
||||||
|
if customCompose != "" {
|
||||||
|
return customCompose, nil
|
||||||
|
}
|
||||||
|
if install.App.Key == vllmAppKeyForUpgrade {
|
||||||
|
return install.DockerCompose, nil
|
||||||
|
}
|
||||||
|
return getUpgradeCompose(install, detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeUpgradeFile(filePath string, content []byte, mode os.FileMode) error {
|
||||||
|
tmp, err := os.CreateTemp(path.Dir(filePath), "."+path.Base(filePath)+".*")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tmpPath := tmp.Name()
|
||||||
|
defer os.Remove(tmpPath)
|
||||||
|
if err = tmp.Chmod(mode); err == nil {
|
||||||
|
_, err = tmp.Write(content)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = tmp.Sync()
|
||||||
|
}
|
||||||
|
if closeErr := tmp.Close(); err == nil {
|
||||||
|
err = closeErr
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(tmpPath, filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyUpgradeStageFile(sourceRoot, targetRoot, relativePath string) error {
|
||||||
|
source := path.Join(sourceRoot, relativePath)
|
||||||
|
if _, err := os.Stat(source); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target := path.Join(targetRoot, relativePath)
|
||||||
|
_ = os.RemoveAll(target)
|
||||||
|
return copyOpenrestyUpgradeSnapshotEntry(source, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceUpgradePath(sourceRoot, targetRoot, relativePath string) error {
|
||||||
|
source := path.Join(sourceRoot, relativePath)
|
||||||
|
if _, err := os.Stat(source); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target := path.Join(targetRoot, relativePath)
|
||||||
|
if err := os.RemoveAll(target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return copyOpenrestyUpgradeSnapshotEntry(source, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createUpgradeFileSnapshot(installPath string, paths []string) (*upgradeFileSnapshot, error) {
|
||||||
|
backupPath, err := os.MkdirTemp("", "1panel-app-upgrade-*")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
snapshot := &upgradeFileSnapshot{
|
||||||
|
installPath: installPath,
|
||||||
|
backupPath: backupPath,
|
||||||
|
paths: paths,
|
||||||
|
existing: make(map[string]bool, len(paths)),
|
||||||
|
}
|
||||||
|
for _, relativePath := range paths {
|
||||||
|
source := path.Join(installPath, relativePath)
|
||||||
|
if _, err = os.Stat(source); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
snapshot.Cleanup()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
snapshot.existing[relativePath] = true
|
||||||
|
if err = copyOpenrestyUpgradeSnapshotEntry(source, path.Join(backupPath, relativePath)); err != nil {
|
||||||
|
snapshot.Cleanup()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return snapshot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *upgradeFileSnapshot) Restore() error {
|
||||||
|
for _, relativePath := range s.paths {
|
||||||
|
target := path.Join(s.installPath, relativePath)
|
||||||
|
if err := os.RemoveAll(target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !s.existing[relativePath] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := copyOpenrestyUpgradeSnapshotEntry(path.Join(s.backupPath, relativePath), target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *upgradeFileSnapshot) Cleanup() {
|
||||||
|
if s != nil && s.backupPath != "" {
|
||||||
|
_ = os.RemoveAll(s.backupPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type appContainerReadinessClient interface {
|
||||||
|
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
|
||||||
|
ContainerInspect(context.Context, string) (container.InspectResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitAppContainersReady(ctx context.Context, install model.AppInstall) ([]string, error) {
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
return waitAppContainersReadyWithClient(ctx, client, install)
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitAppContainersReadyWithClient(ctx context.Context, client appContainerReadinessClient, install model.AppInstall) ([]string, error) {
|
||||||
|
envContent, err := os.ReadFile(install.GetEnvPath())
|
||||||
|
if err != nil {
|
||||||
|
envContent, err = renderUpgradeEnv(&install, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), envContent, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
expectedServices := make(map[string]struct{})
|
||||||
|
for _, service := range project.Services {
|
||||||
|
if !skipCheckStatus(service) {
|
||||||
|
expectedServices[service.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(expectedServices) == 0 {
|
||||||
|
return strings.Split(install.ContainerName, ","), nil
|
||||||
|
}
|
||||||
|
options := container.ListOptions{
|
||||||
|
All: true,
|
||||||
|
Filters: filters.NewArgs(
|
||||||
|
filters.Arg("label", composeWorkdirLabel+"="+install.GetPath()),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
containers, err := client.ContainerList(ctx, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
foundServices := make(map[string]bool, len(expectedServices))
|
||||||
|
containerNames := make([]string, 0, len(containers))
|
||||||
|
for _, item := range containers {
|
||||||
|
serviceName := item.Labels[composeServiceLabel]
|
||||||
|
if _, ok := expectedServices[serviceName]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = waitContainerReady(ctx, client, item.ID); err != nil {
|
||||||
|
return nil, fmt.Errorf("container %s is not ready: %w", serviceName, err)
|
||||||
|
}
|
||||||
|
foundServices[serviceName] = true
|
||||||
|
if len(item.Names) > 0 {
|
||||||
|
containerNames = append(containerNames, strings.TrimPrefix(item.Names[0], "/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for serviceName := range expectedServices {
|
||||||
|
if !foundServices[serviceName] {
|
||||||
|
return nil, fmt.Errorf("container for service %s was not created", serviceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(containerNames)
|
||||||
|
return containerNames, nil
|
||||||
|
}
|
||||||
+135
-365
@@ -1,7 +1,6 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -10,7 +9,6 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -299,8 +297,8 @@ func createLink(ctx context.Context, installTask *task.Task, app model.App, appI
|
|||||||
}
|
}
|
||||||
resourceId = oldMysqlDb.ID
|
resourceId = oldMysqlDb.ID
|
||||||
if oldMysqlDb.ID > 0 {
|
if oldMysqlDb.ID > 0 {
|
||||||
if oldMysqlDb.Username != dbConfig.DbUser || oldMysqlDb.Password != dbConfig.Password {
|
if err := ensureAppMysqlDBUser(database, dbConfig); err != nil {
|
||||||
return buserr.New("ErrDbUserNotValid")
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var createMysql dto.MysqlDBCreate
|
var createMysql dto.MysqlDBCreate
|
||||||
@@ -412,6 +410,20 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
|||||||
|
|
||||||
switch install.App.Key {
|
switch install.App.Key {
|
||||||
case constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster:
|
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))
|
_ = mysqlRepo.Delete(ctx, mysqlRepo.WithByMysqlName(install.Name))
|
||||||
case constant.AppMongodb:
|
case constant.AppMongodb:
|
||||||
_ = mongodbRepo.Delete(ctx, mongodbRepo.WithByMongodbName(install.Name))
|
_ = mongodbRepo.Delete(ctx, mongodbRepo.WithByMongodbName(install.Name))
|
||||||
@@ -519,6 +531,44 @@ func deleteAppImagesByIDs(t *task.Task, client docker.Client, imageIDs []appImag
|
|||||||
return nil
|
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 {
|
func deleteLink(del dto.DelAppLink) error {
|
||||||
install := del.Install
|
install := del.Install
|
||||||
resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID))
|
resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID))
|
||||||
@@ -528,19 +578,20 @@ func deleteLink(del dto.DelAppLink) error {
|
|||||||
for _, re := range resources {
|
for _, re := range resources {
|
||||||
switch re.Key {
|
switch re.Key {
|
||||||
case constant.AppMysql, constant.AppMariaDB:
|
case constant.AppMysql, constant.AppMariaDB:
|
||||||
mysqlService := NewIMysqlService()
|
|
||||||
database, _ := mysqlRepo.Get(repo.WithByID(re.ResourceId))
|
database, _ := mysqlRepo.Get(repo.WithByID(re.ResourceId))
|
||||||
if reflect.DeepEqual(database, model.DatabaseMysql{}) {
|
if reflect.DeepEqual(database, model.DatabaseMysql{}) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := mysqlService.Delete(del.Ctx, dto.MysqlDBDelete{
|
if err := deleteMysqlDatabaseForResourceOwner(del.Ctx, dto.MysqlDBDelete{
|
||||||
ID: database.ID,
|
ID: database.ID,
|
||||||
ForceDelete: del.ForceDelete,
|
ForceDelete: del.ForceDelete,
|
||||||
DeleteBackup: true,
|
DeleteBackup: true,
|
||||||
Type: re.Key,
|
Type: re.Key,
|
||||||
Database: database.MysqlName,
|
Database: database.MysqlName,
|
||||||
}); err != nil && !del.ForceDelete {
|
}, dto.DBResource{Type: constant.TypeApp, Name: install.Name}); err != nil {
|
||||||
return err
|
if isMysqlDatabaseResourceInUseError(err) || !del.ForceDelete {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case constant.AppPostgresql:
|
case constant.AppPostgresql:
|
||||||
pgsqlService := NewIPostgresqlService()
|
pgsqlService := NewIPostgresqlService()
|
||||||
@@ -615,11 +666,58 @@ func handleUpgradeCompose(install model.AppInstall, detail model.AppDetail) (map
|
|||||||
if oldServiceValue["restart"] != nil {
|
if oldServiceValue["restart"] != nil {
|
||||||
serviceValue["restart"] = oldServiceValue["restart"]
|
serviceValue["restart"] = oldServiceValue["restart"]
|
||||||
}
|
}
|
||||||
|
if install.App.Key == constant.AppOpenresty {
|
||||||
|
mergeOpenrestyModuleVolumes(serviceValue, oldServiceValue)
|
||||||
|
}
|
||||||
servicesMap[install.ServiceName] = serviceValue
|
servicesMap[install.ServiceName] = serviceValue
|
||||||
composeMap["services"] = servicesMap
|
composeMap["services"] = servicesMap
|
||||||
return composeMap, nil
|
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) {
|
func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) {
|
||||||
if detail.DockerCompose == "" {
|
if detail.DockerCompose == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
@@ -653,370 +751,35 @@ func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string
|
|||||||
return string(composeByte), nil
|
return string(composeByte), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildNginx(parentTask *task.Task) error {
|
func buildNginx(parentTask *task.Task, nginxInstall model.AppInstall, catalogPath string) error {
|
||||||
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
buildPath := path.Join(nginxInstall.GetPath(), "build")
|
buildPath := path.Join(nginxInstall.GetPath(), nginxModuleBuildDir)
|
||||||
if !fileOp.Stat(buildPath) {
|
if !fileOp.Stat(buildPath) {
|
||||||
return buserr.New("ErrBuildDirNotFound")
|
return buserr.New("ErrBuildDirNotFound")
|
||||||
}
|
}
|
||||||
moduleConfigPath := path.Join(buildPath, "module.json")
|
modules, err := loadNginxModulesWithCatalog(nginxInstall, catalogPath)
|
||||||
moduleContent, err := fileOp.GetContent(moduleConfigPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var (
|
previousModules := cloneNginxModules(modules)
|
||||||
modules []dto.NginxModule
|
staticBuild := hasEnabledStaticNginxModules(modules)
|
||||||
addModuleParams []string
|
if err = configureStaticNginxModules(nginxInstall, modules, ""); err != nil {
|
||||||
addPackages []string
|
return err
|
||||||
)
|
|
||||||
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 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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parentTask.LogSuccess(logStr)
|
||||||
|
}
|
||||||
|
modules, err = buildDynamicNginxModules(nginxInstall, modules, nil, false, "", catalogPath, parentTask)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
envs["RESTY_CONFIG_OPTIONS_MORE"] = ""
|
return commitNginxModuleBuilds(nginxInstall, previousModules, modules, false, catalogPath)
|
||||||
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 {
|
|
||||||
install, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
oldVersion := install.Version
|
|
||||||
detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if install.App.Key == vllmAppKeyForUpgrade && !isVllmUpgradeVersionAllowed(install.Version, detail.Version, loadVllmImageFromEnv(install.Env)) {
|
|
||||||
return errors.New("vLLM can only upgrade within the same image type")
|
|
||||||
}
|
|
||||||
if install.Version == detail.Version {
|
|
||||||
return errors.New("two version is same")
|
|
||||||
}
|
|
||||||
|
|
||||||
upgradeTask, err := task.NewTaskWithOps(install.Name, task.TaskUpgrade, task.TaskScopeApp, req.TaskID, install.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
install.Status = constant.StatusUpgrading
|
|
||||||
|
|
||||||
var (
|
|
||||||
upErr error
|
|
||||||
backupFile string
|
|
||||||
)
|
|
||||||
backUpApp := func(t *task.Task) error {
|
|
||||||
backupService := NewIBackupService()
|
|
||||||
backupRecordService := NewIBackupRecordService()
|
|
||||||
fileName := fmt.Sprintf("upgrade_backup_%s_%s.tar.gz", install.Name, time.Now().Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
|
|
||||||
backupRecord, err := backupService.AppBackup(dto.CommonBackup{Name: install.App.Key, DetailName: install.Name, FileName: fileName})
|
|
||||||
if err == nil {
|
|
||||||
backups, _ := backupRecordService.ListAppRecords(install.App.Key, install.Name, "upgrade_backup")
|
|
||||||
if len(backups) > 3 {
|
|
||||||
backupsToDelete := backups[:len(backups)-3]
|
|
||||||
var deleteIDs []uint
|
|
||||||
for _, backup := range backupsToDelete {
|
|
||||||
deleteIDs = append(deleteIDs, backup.ID)
|
|
||||||
}
|
|
||||||
_ = backupRecordService.BatchDeleteRecord(deleteIDs)
|
|
||||||
}
|
|
||||||
backupFile = path.Join(global.Dir.LocalBackupDir, backupRecord.FileDir, backupRecord.FileName)
|
|
||||||
} else {
|
|
||||||
return buserr.WithNameAndErr("ErrAppBackup", install.Name, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if req.Backup {
|
|
||||||
upgradeTask.AddSubTask(task.GetTaskName(install.Name, task.TaskBackup, task.TaskScopeApp), backUpApp, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
upgradeApp := func(t *task.Task) error {
|
|
||||||
fileOp := files.NewFileOp()
|
|
||||||
detailDir := path.Join(global.Dir.ResourceDir, "apps", install.App.Resource, install.App.Key, detail.Version)
|
|
||||||
if install.App.Resource == constant.AppResourceRemote {
|
|
||||||
if err = downloadApp(install.App, detail, &install, t.Logger); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if detail.DockerCompose == "" {
|
|
||||||
composeDetail, err := fileOp.GetContent(path.Join(detailDir, "docker-compose.yml"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
detail.DockerCompose = string(composeDetail)
|
|
||||||
_ = appDetailRepo.Update(context.Background(), detail)
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
RequestDownloadCallBack(detail.DownloadCallBackUrl)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
if install.App.Resource == constant.AppResourceLocal {
|
|
||||||
detailDir = path.Join(global.Dir.ResourceDir, "apps", "local", strings.TrimPrefix(install.App.Key, "local"), detail.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := fileOp.GetContent(install.GetEnvPath())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
oldEnvContent := append([]byte(nil), content...)
|
|
||||||
oldDockerCompose := install.DockerCompose
|
|
||||||
if install.App.Key == vllmAppKeyForUpgrade {
|
|
||||||
envs := make(map[string]interface{})
|
|
||||||
if err = json.Unmarshal([]byte(install.Env), &envs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
image := buildVllmUpgradeImage(loadVllmImageFromEnv(install.Env), oldVersion, detail.Version)
|
|
||||||
envs[vllmImageEnvKey] = image
|
|
||||||
paramByte, err := json.Marshal(envs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
install.Env = string(paramByte)
|
|
||||||
content = setVllmImageInEnvContent(content, image)
|
|
||||||
}
|
|
||||||
_ = copyAppDetailMissing(fileOp, detailDir, install.GetPath())
|
|
||||||
if install.App.Key == constant.AppOpenresty {
|
|
||||||
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, "tmp")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
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 := fileOp.CopyFile(path.Join(detailBuildDir, "nginx.conf"), installBuildDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := fileOp.CopyFile(path.Join(detailBuildDir, "nginx.vh.default.conf"), installBuildDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sourceScripts := path.Join(detailDir, "scripts")
|
|
||||||
if fileOp.Stat(sourceScripts) {
|
|
||||||
dstScripts := path.Join(install.GetPath(), "scripts")
|
|
||||||
_ = fileOp.DeleteDir(dstScripts)
|
|
||||||
_ = fileOp.CreateDir(dstScripts, constant.DirPerm)
|
|
||||||
scriptCmd := exec.Command("cp", "-rf", sourceScripts+"/.", dstScripts+"/")
|
|
||||||
_, _ = scriptCmd.CombinedOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
var newCompose string
|
|
||||||
if err = migrateOpenclawProtocolUpgrade(&install, oldVersion, detail.Version); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if req.DockerCompose == "" {
|
|
||||||
if install.App.Key == vllmAppKeyForUpgrade {
|
|
||||||
newCompose = install.DockerCompose
|
|
||||||
} else {
|
|
||||||
newCompose, err = getUpgradeCompose(install, detail)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newCompose = req.DockerCompose
|
|
||||||
}
|
|
||||||
|
|
||||||
install.DockerCompose = newCompose
|
|
||||||
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 out, err := compose.Down(install.GetComposePath()); err != nil {
|
|
||||||
if out != "" {
|
|
||||||
upErr = errors.New(out)
|
|
||||||
return upErr
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
envs := make(map[string]interface{})
|
|
||||||
if err = json.Unmarshal([]byte(install.Env), &envs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
envParams := make(map[string]string, len(envs))
|
|
||||||
if install.App.Key == constant.AppOpenresty {
|
|
||||||
packageUrl, _ := env.GetEnvValueByKey(install.GetEnvPath(), "CONTAINER_PACKAGE_URL")
|
|
||||||
addPackage, _ := env.GetEnvValueByKey(install.GetEnvPath(), "RESTY_ADD_PACKAGE_BUILDDEPS")
|
|
||||||
options, _ := env.GetEnvValueByKey(install.GetEnvPath(), "RESTY_CONFIG_OPTIONS_MORE")
|
|
||||||
envParams["CONTAINER_PACKAGE_URL"] = packageUrl
|
|
||||||
envParams["RESTY_ADD_PACKAGE_BUILDDEPS"] = addPackage
|
|
||||||
envParams["RESTY_CONFIG_OPTIONS_MORE"] = options
|
|
||||||
}
|
|
||||||
handleMap(envs, envParams)
|
|
||||||
if err = env.Write(envParams, install.GetEnvPath()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = runScript(t, &install, "upgrade"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = fileOp.WriteFile(install.GetComposePath(), strings.NewReader(install.DockerCompose), constant.FilePerm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if install.App.Key == constant.AppOpenresty {
|
|
||||||
if err = buildNginx(t); err != nil {
|
|
||||||
t.Log(err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("Run"), i18n.GetMsgByKey("App"))
|
|
||||||
t.Log(logStr)
|
|
||||||
if out, err := compose.Up(install.GetComposePath()); err != nil {
|
|
||||||
if out != "" {
|
|
||||||
return errors.New(out)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.LogSuccess(logStr)
|
|
||||||
install.Status = constant.StatusRunning
|
|
||||||
if err = appInstallRepo.Save(context.Background(), &install); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
rollBackApp := func(t *task.Task) {
|
|
||||||
if req.Backup {
|
|
||||||
t.Log(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.LogSuccess(i18n.GetWithName("AppRecover", install.Name))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
upgradeTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp), upgradeApp, rollBackApp, 0, 1*time.Hour)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
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 = err.Error()
|
|
||||||
_ = appInstallRepo.Save(context.Background(), &existInstall)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return appInstallRepo.Save(context.Background(), &install)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func skipCheckStatus(service types.ServiceConfig) bool {
|
func skipCheckStatus(service types.ServiceConfig) bool {
|
||||||
@@ -2059,6 +1822,10 @@ func isHostModel(dockerCompose string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
|
func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
|
||||||
|
return copyAppDetailMissingTracked(fileOp, srcDir, dstDir, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyAppDetailMissingTracked(fileOp files.FileOp, srcDir, dstDir string, createdPaths *[]string) error {
|
||||||
entries, err := os.ReadDir(srcDir)
|
entries, err := os.ReadDir(srcDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -2070,6 +1837,9 @@ func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
|
|||||||
srcPath := path.Join(srcDir, entry.Name())
|
srcPath := path.Join(srcDir, entry.Name())
|
||||||
dstPath := path.Join(dstDir, entry.Name())
|
dstPath := path.Join(dstDir, entry.Name())
|
||||||
if !fileOp.Stat(dstPath) {
|
if !fileOp.Stat(dstPath) {
|
||||||
|
if createdPaths != nil {
|
||||||
|
*createdPaths = append(*createdPaths, dstPath)
|
||||||
|
}
|
||||||
if entry.IsDir() {
|
if entry.IsDir() {
|
||||||
if err := fileOp.CopyDir(srcPath, dstDir); err != nil {
|
if err := fileOp.CopyDir(srcPath, dstDir); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -2084,7 +1854,7 @@ func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
|
|||||||
if !entry.IsDir() {
|
if !entry.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := copyAppDetailMissing(fileOp, srcPath, dstPath); err != nil {
|
if err := copyAppDetailMissingTracked(fileOp, srcPath, dstPath, createdPaths); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2247,7 +2017,7 @@ func handleOpenrestyFile(appInstall *model.AppInstall) error {
|
|||||||
|
|
||||||
func handleDefaultServer(appInstall *model.AppInstall) error {
|
func handleDefaultServer(appInstall *model.AppInstall) error {
|
||||||
installDir := appInstall.GetPath()
|
installDir := appInstall.GetPath()
|
||||||
defaultConfigPath := path.Join(installDir, "conf", "default", "00.default.conf")
|
defaultConfigPath := path.Join(installDir, nginxModuleConfDir, "default", "00.default.conf")
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
content, err := fileOp.GetContent(defaultConfigPath)
|
content, err := fileOp.GetContent(defaultConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2261,7 +2031,7 @@ func handleDefaultServer(appInstall *model.AppInstall) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRejectHandshake bool) error {
|
func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRejectHandshake bool) error {
|
||||||
sslDir := path.Join(appInstall.GetPath(), "conf", "ssl")
|
sslDir := path.Join(appInstall.GetPath(), nginxModuleConfDir, "ssl")
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
if !fileOp.Stat(sslDir) {
|
if !fileOp.Stat(sslDir) {
|
||||||
return errors.New("ssl dir not found")
|
return errors.New("ssl dir not found")
|
||||||
@@ -2291,7 +2061,7 @@ func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRe
|
|||||||
_ = NewIWebsiteSSLService().Delete([]uint{websiteSSL.ID})
|
_ = NewIWebsiteSSLService().Delete([]uint{websiteSSL.ID})
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
defaultConfigPath := path.Join(appInstall.GetPath(), "conf", "default", "00.default.conf")
|
defaultConfigPath := path.Join(appInstall.GetPath(), nginxModuleConfDir, "default", "00.default.conf")
|
||||||
content, err := os.ReadFile(defaultConfigPath)
|
content, err := os.ReadFile(defaultConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -372,7 +373,7 @@ func (u *BackupService) checkBackupConn(backup *model.BackupAccount) (bool, erro
|
|||||||
targetPath = strings.TrimPrefix(targetPath, "/")
|
targetPath = strings.TrimPrefix(targetPath, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := client.Upload(fileItem, targetPath); err != nil {
|
if _, err := client.Upload(context.Background(), fileItem, targetPath); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
_, _ = client.Delete(path.Join(backup.BackupPath, "test/1panel"))
|
_, _ = client.Delete(path.Join(backup.BackupPath, "test/1panel"))
|
||||||
@@ -437,6 +438,10 @@ type backupClientHelper struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewBackupClientMap(ids []string) map[string]backupClientHelper {
|
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 accounts []model.BackupAccount
|
||||||
var idItems []uint
|
var idItems []uint
|
||||||
for i := 0; i < len(ids); i++ {
|
for i := 0; i < len(ids); i++ {
|
||||||
@@ -446,7 +451,7 @@ func NewBackupClientMap(ids []string) map[string]backupClientHelper {
|
|||||||
accounts, _ = backupRepo.List(repo.WithByIDs(idItems))
|
accounts, _ = backupRepo.List(repo.WithByIDs(idItems))
|
||||||
clientMap := make(map[string]backupClientHelper)
|
clientMap := make(map[string]backupClientHelper)
|
||||||
for _, item := range accounts {
|
for _, item := range accounts {
|
||||||
backClient, err := newClient(&item, true)
|
backClient, err := newClientWithContext(ctx, &item, true)
|
||||||
itemHelper := backupClientHelper{
|
itemHelper := backupClientHelper{
|
||||||
client: backClient,
|
client: backClient,
|
||||||
name: item.Name,
|
name: item.Name,
|
||||||
@@ -463,7 +468,11 @@ func NewBackupClientMap(ids []string) map[string]backupClientHelper {
|
|||||||
return clientMap
|
return clientMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func uploadWithMap(taskItem task.Task, accountMap map[string]backupClientHelper, src, dst, accountIDs string, downloadAccountID, retry uint) error {
|
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 {
|
||||||
accounts := strings.Split(accountIDs, ",")
|
accounts := strings.Split(accountIDs, ",")
|
||||||
for _, account := range accounts {
|
for _, account := range accounts {
|
||||||
if len(account) == 0 {
|
if len(account) == 0 {
|
||||||
@@ -489,10 +498,13 @@ func uploadWithMap(taskItem task.Task, accountMap map[string]backupClientHelper,
|
|||||||
"backup": name,
|
"backup": name,
|
||||||
}))
|
}))
|
||||||
for i := 0; i < int(retry)+1; i++ {
|
for i := 0; i < int(retry)+1; i++ {
|
||||||
_, err := itemBackup.client.Upload(src, path.Join(itemBackup.backupPath, dst))
|
_, err := itemBackup.client.Upload(ctx, src, path.Join(itemBackup.backupPath, dst))
|
||||||
taskItem.LogWithStatus(i18n.GetMsgByKey("Upload"), err)
|
taskItem.LogWithStatus(i18n.GetMsgByKey("Upload"), err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if account == fmt.Sprintf("%d", downloadAccountID) {
|
if account == fmt.Sprintf("%d", downloadAccountID) {
|
||||||
|
if cleanOnFailure {
|
||||||
|
cleanupCronjobBackupArtifacts(accountMap, src, dst)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -502,11 +514,71 @@ func uploadWithMap(taskItem task.Task, accountMap map[string]backupClientHelper,
|
|||||||
itemBackup.hasBackup = true
|
itemBackup.hasBackup = true
|
||||||
accountMap[account] = itemBackup
|
accountMap[account] = itemBackup
|
||||||
}
|
}
|
||||||
os.RemoveAll(src)
|
if removeSrc {
|
||||||
|
os.RemoveAll(src)
|
||||||
|
}
|
||||||
return nil
|
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) {
|
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{})
|
varMap := make(map[string]interface{})
|
||||||
if len(account.Vars) != 0 {
|
if len(account.Vars) != 0 {
|
||||||
if err := json.Unmarshal([]byte(account.Vars), &varMap); err != nil {
|
if err := json.Unmarshal([]byte(account.Vars), &varMap); err != nil {
|
||||||
@@ -531,7 +603,7 @@ func newClient(account *model.BackupAccount, isEncrypt bool) (cloud_storage.Clou
|
|||||||
varMap["password"] = account.Credential
|
varMap["password"] = account.Credential
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := cloud_storage.NewCloudStorageClient(account.Type, varMap)
|
client, err := cloud_storage.NewCloudStorageClientWithContext(ctx, account.Type, varMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func (u *BackupService) AppBackup(req dto.CommonBackup) (*model.BackupRecord, er
|
|||||||
|
|
||||||
if !req.IsImmediate {
|
if !req.IsImmediate {
|
||||||
if err = handleAppBackup(&install, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil {
|
if err = handleAppBackup(&install, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil {
|
||||||
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
markBackupFailed(record.ID, err)
|
||||||
global.LOG.Errorf("backup app %s failed, err: %v", req.DetailName, err)
|
global.LOG.Errorf("backup app %s failed, err: %v", req.DetailName, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -74,13 +74,13 @@ func (u *BackupService) AppBackup(req dto.CommonBackup) (*model.BackupRecord, er
|
|||||||
|
|
||||||
backupTask, err := task.NewTaskWithOps(install.Name, task.TaskBackup, task.TaskScopeBackup, req.TaskID, install.ID)
|
backupTask, err := task.NewTaskWithOps(install.Name, task.TaskBackup, task.TaskScopeBackup, req.TaskID, install.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
markBackupFailed(record.ID, err)
|
||||||
record.Status = constant.StatusFailed
|
record.Status = constant.StatusFailed
|
||||||
record.Message = err.Error()
|
record.Message = err.Error()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err = doAppBackup(&install, backupTask, backupDir, fileName, "", req.Secret); err != nil {
|
if err = doAppBackup(&install, backupTask, backupDir, fileName, "", req.Secret); err != nil {
|
||||||
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
markBackupFailed(record.ID, err)
|
||||||
record.Status = constant.StatusFailed
|
record.Status = constant.StatusFailed
|
||||||
record.Message = err.Error()
|
record.Message = err.Error()
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -90,6 +90,34 @@ func (u *BackupService) AppBackup(req dto.CommonBackup) (*model.BackupRecord, er
|
|||||||
return record, nil
|
return record, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func backupAppWithParentTask(install *model.AppInstall, parentTask *task.Task, fileName string) (*model.BackupRecord, error) {
|
||||||
|
itemDir := fmt.Sprintf("app/%s/%s", install.App.Key, install.Name)
|
||||||
|
backupDir := path.Join(global.Dir.LocalBackupDir, itemDir)
|
||||||
|
record := &model.BackupRecord{
|
||||||
|
Type: "app",
|
||||||
|
Name: install.App.Key,
|
||||||
|
DetailName: install.Name,
|
||||||
|
SourceAccountIDs: "1",
|
||||||
|
DownloadAccountID: 1,
|
||||||
|
FileDir: itemDir,
|
||||||
|
FileName: fileName,
|
||||||
|
TaskID: parentTask.TaskID,
|
||||||
|
Status: constant.StatusWaiting,
|
||||||
|
}
|
||||||
|
if err := backupRepo.CreateRecord(record); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := handleAppBackup(install, parentTask, record.ID, backupDir, fileName, "", "", parentTask.TaskID); err != nil {
|
||||||
|
markBackupFailed(record.ID, err)
|
||||||
|
record.Status = constant.StatusFailed
|
||||||
|
record.Message = err.Error()
|
||||||
|
return record, err
|
||||||
|
}
|
||||||
|
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||||
|
record.Status = constant.StatusSuccess
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *BackupService) AppRecover(req dto.CommonRecover) error {
|
func (u *BackupService) AppRecover(req dto.CommonRecover) error {
|
||||||
app, err := appRepo.GetFirst(appRepo.WithKey(req.Name))
|
app, err := appRepo.GetFirst(appRepo.WithKey(req.Name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -162,7 +190,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)
|
backupTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 3, time.Hour)
|
||||||
go func() {
|
go func() {
|
||||||
if err := backupTask.Execute(); err != nil {
|
if err := backupTask.Execute(); err != nil {
|
||||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
markBackupFailed(recordID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||||
@@ -203,7 +231,11 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
_, _ = compose.Up(install.GetComposePath())
|
if isRollback {
|
||||||
|
_, _ = compose.UpWithoutPull(install.GetComposePath())
|
||||||
|
} else {
|
||||||
|
_, _ = compose.Up(install.GetComposePath())
|
||||||
|
}
|
||||||
_ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", ""))
|
_ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", ""))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -280,7 +312,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
newDB, err := reCreateDB(db.ID, database, backupEnvMap)
|
newDB, err := reCreateDB(db.ID, database, backupEnvMap, install.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -408,10 +440,16 @@ func doAppBackup(install *model.AppInstall, parentTask *task.Task, backupDir, fi
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{}) (*model.DatabaseMysql, error) {
|
func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{}, appInstallName string) (*model.DatabaseMysql, error) {
|
||||||
mysqlService := NewIMysqlService()
|
mysqlService := NewIMysqlService()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
_ = mysqlService.Delete(ctx, dto.MysqlDBDelete{ID: dbID, Database: database.Name, Type: database.Type, DeleteBackup: false, ForceDelete: true})
|
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
|
||||||
|
}
|
||||||
|
|
||||||
dbInfo := getDBCreateInfoFromEnv(envMap, "utf8mb4")
|
dbInfo := getDBCreateInfoFromEnv(envMap, "utf8mb4")
|
||||||
createDB, err := mysqlService.Create(context.Background(), dto.MysqlDBCreate{
|
createDB, err := mysqlService.Create(context.Background(), dto.MysqlDBCreate{
|
||||||
@@ -419,17 +457,57 @@ func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{
|
|||||||
From: database.From,
|
From: database.From,
|
||||||
Database: database.Name,
|
Database: database.Name,
|
||||||
Format: dbInfo.Format,
|
Format: dbInfo.Format,
|
||||||
Username: dbInfo.User,
|
|
||||||
Password: dbInfo.Password,
|
|
||||||
Permission: "%",
|
Permission: "%",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(dbInfo.User) != 0 {
|
||||||
|
if err := ensureMysqlDBUser(mysqlService, database, dbInfo); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
updateCronjobsDBRef(dbID, createDB.ID)
|
updateCronjobsDBRef(dbID, createDB.ID)
|
||||||
return createDB, nil
|
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) {
|
func reCreatePostgresqlDB(dbID uint, database model.Database, envMap map[string]interface{}) (*model.DatabasePostgresql, error) {
|
||||||
postgresqlService := NewIPostgresqlService()
|
postgresqlService := NewIPostgresqlService()
|
||||||
_ = postgresqlService.Delete(context.Background(), dto.PostgresqlDBDelete{
|
_ = postgresqlService.Delete(context.Background(), dto.PostgresqlDBDelete{
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ func (u *BackupService) ComposeBackup(req dto.CommonBackup) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := handleComposeBackup(req, nil, record.ID, backupDir, fileName); err != nil {
|
if err := handleComposeBackup(req, nil, record.ID, backupDir, fileName); err != nil {
|
||||||
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
markBackupFailed(record.ID, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -163,7 +163,7 @@ func handleComposeBackup(req dto.CommonBackup, parentTask *task.Task, recordID u
|
|||||||
go func() {
|
go func() {
|
||||||
defer composeCtx.close()
|
defer composeCtx.close()
|
||||||
if err := backupTask.Execute(); err != nil {
|
if err := backupTask.Execute(); err != nil {
|
||||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
markBackupFailed(recordID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func (u *BackupService) ContainerBackup(req dto.CommonBackup) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := handleContainerBackup(req.Name, nil, record.ID, backupDir, fileName, req.TaskID, req.Secret, req.StopBefore); err != nil {
|
if err := handleContainerBackup(req.Name, nil, record.ID, backupDir, fileName, req.TaskID, req.Secret, req.StopBefore); err != nil {
|
||||||
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
markBackupFailed(record.ID, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -158,7 +158,7 @@ func handleContainerBackup(containerName string, parentTask *task.Task, recordID
|
|||||||
go func() {
|
go func() {
|
||||||
defer backupCtx.close()
|
defer backupCtx.close()
|
||||||
if err := backupTask.Execute(); err != nil {
|
if err := backupTask.Execute(); err != nil {
|
||||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
markBackupFailed(recordID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||||
@@ -604,23 +604,40 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeUnsupportedEndpointStaticIPAM(cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
|
func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
|
||||||
|
if cli.NewVersionError(ctx, "1.44", "specify mac-address per network") != nil {
|
||||||
|
removeEndpointMacAddresses(primary, extras)
|
||||||
|
}
|
||||||
|
endpointGroups := []map[string]*network.EndpointSettings{extras}
|
||||||
if primary != nil {
|
if primary != nil {
|
||||||
removeUnsupportedEndpointStaticIPAMFromEndpoints(cli, primary.EndpointsConfig)
|
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, extras)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeUnsupportedEndpointStaticIPAMFromEndpoints(cli *client.Client, endpoints map[string]*network.EndpointSettings) {
|
func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
|
||||||
for netName, endpoint := range endpoints {
|
if primary != nil {
|
||||||
if endpoint == nil || endpoint.IPAMConfig == nil {
|
for _, endpoint := range primary.EndpointsConfig {
|
||||||
continue
|
if endpoint != nil {
|
||||||
|
endpoint.MacAddress = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
info, err := cli.NetworkInspect(context.Background(), netName, network.InspectOptions{})
|
}
|
||||||
if err != nil {
|
for _, endpoint := range extras {
|
||||||
continue
|
if endpoint != nil {
|
||||||
|
endpoint.MacAddress = ""
|
||||||
}
|
}
|
||||||
removeUnsupportedEndpointStaticIP(netName, info, endpoint)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -639,7 +656,7 @@ func removeUnsupportedEndpointStaticIP(netName string, info network.Inspect, end
|
|||||||
if endpoint.IPAMConfig.IPv6Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv6Address, true) {
|
if endpoint.IPAMConfig.IPv6Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv6Address, true) {
|
||||||
endpoint.IPAMConfig.IPv6Address = ""
|
endpoint.IPAMConfig.IPv6Address = ""
|
||||||
}
|
}
|
||||||
if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" {
|
if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" && len(endpoint.IPAMConfig.LinkLocalIPs) == 0 {
|
||||||
endpoint.IPAMConfig = nil
|
endpoint.IPAMConfig = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -776,11 +793,30 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
|
|||||||
if name == "host" || name == "none" {
|
if name == "host" || name == "none" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
endpointSetting := &network.EndpointSettings{Aliases: append([]string(nil), endpoint.Aliases...), MacAddress: endpoint.MacAddress}
|
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,
|
||||||
|
}
|
||||||
if endpoint.IPAMConfig != nil {
|
if endpoint.IPAMConfig != nil {
|
||||||
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
|
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
|
||||||
IPv4Address: endpoint.IPAMConfig.IPv4Address,
|
IPv4Address: endpoint.IPAMConfig.IPv4Address,
|
||||||
IPv6Address: endpoint.IPAMConfig.IPv6Address,
|
IPv6Address: endpoint.IPAMConfig.IPv6Address,
|
||||||
|
LinkLocalIPs: append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...),
|
||||||
|
}
|
||||||
|
} else if name != "bridge" && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "") {
|
||||||
|
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
|
||||||
|
IPv4Address: endpoint.IPAddress,
|
||||||
|
IPv6Address: endpoint.GlobalIPv6Address,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if name == primaryName {
|
if name == primaryName {
|
||||||
@@ -795,6 +831,39 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
|
|||||||
return config, extraNetworks
|
return config, extraNetworks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unsupportedUserSpecifiedIPAddress = "user specified IP address is supported only when connecting to networks with user configured subnets"
|
||||||
|
|
||||||
|
func clearUnsupportedDynamicEndpointIPAM(err error, endpoints map[string]*network.EndpointSettings, networkSettings *container.NetworkSettings) bool {
|
||||||
|
if err == nil || !strings.Contains(err.Error(), unsupportedUserSpecifiedIPAddress) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for name, endpoint := range endpoints {
|
||||||
|
if !isDynamicContainerNetwork(networkSettings, name) || endpoint == nil || endpoint.IPAMConfig == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "network "+name+":") {
|
||||||
|
endpoint.IPAMConfig = nil
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cleared := false
|
||||||
|
for name, endpoint := range endpoints {
|
||||||
|
if isDynamicContainerNetwork(networkSettings, name) && endpoint != nil && endpoint.IPAMConfig != nil {
|
||||||
|
endpoint.IPAMConfig = nil
|
||||||
|
cleared = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleared
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDynamicContainerNetwork(networkSettings *container.NetworkSettings, name string) bool {
|
||||||
|
if networkSettings == nil || name == "bridge" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
endpoint := networkSettings.Networks[name]
|
||||||
|
return endpoint != nil && endpoint.IPAMConfig == nil && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "")
|
||||||
|
}
|
||||||
|
|
||||||
func cloneContainerConfig(config *container.Config) *container.Config {
|
func cloneContainerConfig(config *container.Config) *container.Config {
|
||||||
if config == nil {
|
if config == nil {
|
||||||
return &container.Config{}
|
return &container.Config{}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func (u *BackupService) MongodbBackup(req dto.CommonBackup) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := handleMongodbBackup(req, nil, record.ID, targetDir, fileName); err != nil {
|
if err := handleMongodbBackup(req, nil, record.ID, targetDir, fileName); err != nil {
|
||||||
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
markBackupFailed(record.ID, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -98,7 +98,7 @@ func handleMongodbBackup(req dto.CommonBackup, parentTask *task.Task, recordID u
|
|||||||
)
|
)
|
||||||
go func() {
|
go func() {
|
||||||
if err := backupTask.Execute(); err != nil {
|
if err := backupTask.Execute(); err != nil {
|
||||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
markBackupFailed(recordID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -39,6 +40,7 @@ func (u *BackupService) MysqlBackup(req dto.CommonBackup) error {
|
|||||||
TaskID: req.TaskID,
|
TaskID: req.TaskID,
|
||||||
Status: constant.StatusWaiting,
|
Status: constant.StatusWaiting,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
|
Args: encodeBackupArgs(req.Args),
|
||||||
}
|
}
|
||||||
if err := backupRepo.CreateRecord(record); err != nil {
|
if err := backupRepo.CreateRecord(record); err != nil {
|
||||||
global.LOG.Errorf("save backup record failed, err: %v", err)
|
global.LOG.Errorf("save backup record failed, err: %v", err)
|
||||||
@@ -47,7 +49,7 @@ func (u *BackupService) MysqlBackup(req dto.CommonBackup) error {
|
|||||||
|
|
||||||
databaseHelper := DatabaseHelper{Database: req.Name, DBType: req.Type, Name: req.DetailName, Args: req.Args}
|
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 {
|
if err := handleMysqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID, req.Secret); err != nil {
|
||||||
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
markBackupFailed(record.ID, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -100,7 +102,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)
|
backupTask.AddSubTaskWithOps(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 0, 3*time.Hour)
|
||||||
go func() {
|
go func() {
|
||||||
if err := backupTask.Execute(); err != nil {
|
if err := backupTask.Execute(); err != nil {
|
||||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
markBackupFailed(recordID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||||
@@ -143,6 +145,14 @@ func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback
|
|||||||
|
|
||||||
if !isRollback {
|
if !isRollback {
|
||||||
rollbackFile := path.Join(global.Dir.TmpDir, fmt.Sprintf("database/%s/%s_%s.sql.gz", req.Type, req.DetailName, time.Now().Format(constant.DateTimeSlimLayout)))
|
rollbackFile := path.Join(global.Dir.TmpDir, fmt.Sprintf("database/%s/%s_%s.sql.gz", req.Type, req.DetailName, time.Now().Format(constant.DateTimeSlimLayout)))
|
||||||
|
var rollbackArgs []string
|
||||||
|
if req.BackupRecordID != 0 {
|
||||||
|
record, err := backupRepo.GetRecord(repo.WithByID(req.BackupRecordID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rollbackArgs = decodeBackupArgs(record.Args)
|
||||||
|
}
|
||||||
if err := cli.Backup(client.BackupInfo{
|
if err := cli.Backup(client.BackupInfo{
|
||||||
Name: req.DetailName,
|
Name: req.DetailName,
|
||||||
Type: req.Type,
|
Type: req.Type,
|
||||||
@@ -150,6 +160,7 @@ func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback
|
|||||||
Format: dbInfo.Format,
|
Format: dbInfo.Format,
|
||||||
TargetDir: path.Dir(rollbackFile),
|
TargetDir: path.Dir(rollbackFile),
|
||||||
FileName: path.Base(rollbackFile),
|
FileName: path.Base(rollbackFile),
|
||||||
|
Args: rollbackArgs,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("backup mysql db %s for rollback before recover failed, err: %v", req.DetailName, err)
|
return fmt.Errorf("backup mysql db %s for rollback before recover failed, err: %v", req.DetailName, err)
|
||||||
}
|
}
|
||||||
@@ -242,6 +253,36 @@ func doMysqlBackup(db DatabaseHelper, targetDir, fileName, secret string) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func encodeBackupArgs(args []string) string {
|
||||||
|
var items []string
|
||||||
|
for _, arg := range args {
|
||||||
|
if len(arg) != 0 {
|
||||||
|
items = append(items, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(items)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Warnf("marshal backup args failed: %v", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeBackupArgs(value string) []string {
|
||||||
|
if len(value) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var args []string
|
||||||
|
if err := json.Unmarshal([]byte(value), &args); err != nil {
|
||||||
|
global.LOG.Warnf("unmarshal backup args failed: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
func loadSqlFile(file string) (string, error) {
|
func loadSqlFile(file string) (string, error) {
|
||||||
if !strings.HasSuffix(file, ".tar.gz") && !strings.HasSuffix(file, ".zip") {
|
if !strings.HasSuffix(file, ".tar.gz") && !strings.HasSuffix(file, ".zip") {
|
||||||
return file, nil
|
return file, nil
|
||||||
|
|||||||
@@ -39,10 +39,12 @@ func (u *BackupService) PostgresqlBackup(req dto.CommonBackup) error {
|
|||||||
}
|
}
|
||||||
if err := backupRepo.CreateRecord(record); err != nil {
|
if err := backupRepo.CreateRecord(record); err != nil {
|
||||||
global.LOG.Errorf("save backup record failed, err: %v", err)
|
global.LOG.Errorf("save backup record failed, err: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
databaseHelper := DatabaseHelper{Database: req.Name, DBType: req.Type, Name: req.DetailName}
|
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 {
|
if err := handlePostgresqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID, req.Secret); err != nil {
|
||||||
|
markBackupFailed(record.ID, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -88,7 +90,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)
|
backupTask.AddSubTaskWithOps(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 0, 3*time.Hour)
|
||||||
go func() {
|
go func() {
|
||||||
if err := backupTask.Execute(); err != nil {
|
if err := backupTask.Execute(); err != nil {
|
||||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
markBackupFailed(recordID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ func (u *BackupService) RedisBackup(req dto.CommonBackup) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := handleRedisBackup(redisInfo, nil, record.ID, backupDir, fileName, req.Secret, req.TaskID); err != nil {
|
if err := handleRedisBackup(redisInfo, nil, record.ID, backupDir, fileName, req.Secret, req.TaskID); err != nil {
|
||||||
|
markBackupFailed(record.ID, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -129,7 +130,7 @@ func handleRedisBackup(redisInfo *repo.RootInfo, parentTask *task.Task, recordID
|
|||||||
itemTask.AddSubTaskWithOps(i18n.GetMsgByKey("TaskBackup"), backupDatabase, nil, 3, time.Hour)
|
itemTask.AddSubTaskWithOps(i18n.GetMsgByKey("TaskBackup"), backupDatabase, nil, 3, time.Hour)
|
||||||
go func() {
|
go func() {
|
||||||
if err := itemTask.Execute(); err != nil {
|
if err := itemTask.Execute(); err != nil {
|
||||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
markBackupFailed(recordID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ func (u *BackupService) WebsiteBackup(req dto.CommonBackup) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = handleWebsiteBackup(&website, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil {
|
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)
|
global.LOG.Errorf("backup website %s failed, err: %v", website.Alias, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -235,7 +236,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)
|
backupTask.AddSubTaskWithOps(task.GetTaskName(website.Alias, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 3, time.Hour)
|
||||||
go func() {
|
go func() {
|
||||||
if err := backupTask.Execute(); err != nil {
|
if err := backupTask.Execute(); err != nil {
|
||||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
markBackupFailed(recordID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||||
|
|||||||
+83
-207
@@ -71,6 +71,7 @@ type IContainerService interface {
|
|||||||
ComposeOperation(req dto.ComposeOperation) error
|
ComposeOperation(req dto.ComposeOperation) error
|
||||||
TestCompose(req dto.ComposeCreate) (bool, error)
|
TestCompose(req dto.ComposeCreate) (bool, error)
|
||||||
ComposeUpdate(req dto.ComposeUpdate) error
|
ComposeUpdate(req dto.ComposeUpdate) error
|
||||||
|
ComposePin(req dto.ComposePin) error
|
||||||
ComposeLogClean(req dto.ComposeLogClean) error
|
ComposeLogClean(req dto.ComposeLogClean) error
|
||||||
|
|
||||||
ContainerCreate(req dto.ContainerOperate, inThread bool) error
|
ContainerCreate(req dto.ContainerOperate, inThread bool) error
|
||||||
@@ -485,15 +486,19 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer client.Close()
|
unlock := containerOperationLock.lock(req.Name)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
newContainer, _ := client.ContainerInspect(ctx, req.Name)
|
newContainer, _ := client.ContainerInspect(ctx, req.Name)
|
||||||
if newContainer.ContainerJSONBase != nil {
|
if newContainer.ContainerJSONBase != nil {
|
||||||
|
unlock()
|
||||||
|
_ = client.Close()
|
||||||
return buserr.New("ErrContainerName")
|
return buserr.New("ErrContainerName")
|
||||||
}
|
}
|
||||||
|
|
||||||
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeContainer, req.TaskID, 1)
|
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeContainer, req.TaskID, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
unlock()
|
||||||
|
_ = client.Close()
|
||||||
global.LOG.Errorf("new task for create container failed, err: %v", err)
|
global.LOG.Errorf("new task for create container failed, err: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -529,18 +534,20 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
removeUnsupportedEndpointStaticIPAM(client, networkConf, nil)
|
normalizeContainerEndpointSettings(ctx, client, networkConf, nil)
|
||||||
con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
|
con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
|
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
|
||||||
_ = client.ContainerRemove(ctx, req.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
if con.ID != "" {
|
||||||
|
_ = client.ContainerRemove(ctx, con.ID, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = client.ContainerStart(ctx, con.ID, container.StartOptions{})
|
err = client.ContainerStart(ctx, con.ID, container.StartOptions{})
|
||||||
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err)
|
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
|
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
|
||||||
_ = client.ContainerRemove(ctx, req.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
_ = client.ContainerRemove(ctx, con.ID, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
||||||
return fmt.Errorf("create successful but start failed, err: %v", err)
|
return fmt.Errorf("create successful but start failed, err: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -548,12 +555,16 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
|
|||||||
|
|
||||||
if inThread {
|
if inThread {
|
||||||
go func() {
|
go func() {
|
||||||
|
defer unlock()
|
||||||
|
defer client.Close()
|
||||||
if err := taskItem.Execute(); err != nil {
|
if err := taskItem.Execute(); err != nil {
|
||||||
global.LOG.Error(err.Error())
|
global.LOG.Error(err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
defer unlock()
|
||||||
|
defer client.Close()
|
||||||
return taskItem.Execute()
|
return taskItem.Execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,21 +585,7 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
|
|||||||
data.Image = oldContainer.Config.Image
|
data.Image = oldContainer.Config.Image
|
||||||
if oldContainer.NetworkSettings != nil {
|
if oldContainer.NetworkSettings != nil {
|
||||||
for net, val := range oldContainer.NetworkSettings.Networks {
|
for net, val := range oldContainer.NetworkSettings.Networks {
|
||||||
netItem := dto.ContainerNetwork{
|
data.Networks = append(data.Networks, loadContainerNetworkInfo(net, val))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,141 +631,40 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
|
|||||||
return &data, nil
|
return &data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
|
func loadContainerNetworkInfo(name string, endpoint *network.EndpointSettings) dto.ContainerNetwork {
|
||||||
client, err := docker.NewDockerClient()
|
item := dto.ContainerNetwork{Network: name}
|
||||||
if err != nil {
|
if endpoint == nil {
|
||||||
return err
|
return item
|
||||||
}
|
}
|
||||||
defer client.Close()
|
item.MacAddr = endpoint.MacAddress
|
||||||
ctx := context.Background()
|
item.Links = append([]string(nil), endpoint.Links...)
|
||||||
oldContainer, err := client.ContainerInspect(ctx, req.Name)
|
item.Aliases = append([]string(nil), endpoint.Aliases...)
|
||||||
if err != nil {
|
item.DriverOpts = cloneStringMap(endpoint.DriverOpts)
|
||||||
return err
|
item.GwPriority = endpoint.GwPriority
|
||||||
|
if endpoint.IPAMConfig != nil {
|
||||||
|
item.LinkLocalIPs = append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...)
|
||||||
}
|
}
|
||||||
|
if name != "bridge" {
|
||||||
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskUpdate, task.TaskScopeContainer, req.TaskID, 1)
|
if endpoint.IPAMConfig != nil {
|
||||||
if err != nil {
|
item.Ipv4 = endpoint.IPAMConfig.IPv4Address
|
||||||
global.LOG.Errorf("new task for create container failed, err: %v", err)
|
item.Ipv6 = endpoint.IPAMConfig.IPv6Address
|
||||||
return err
|
} else {
|
||||||
}
|
item.Ipv4 = endpoint.IPAddress
|
||||||
go func() {
|
item.Ipv6 = endpoint.GlobalIPv6Address
|
||||||
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 (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
|
func cloneStringMap(source map[string]string) map[string]string {
|
||||||
client, err := docker.NewDockerClient()
|
if len(source) == 0 {
|
||||||
if err != nil {
|
return nil
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
defer client.Close()
|
result := make(map[string]string, len(source))
|
||||||
ctx := context.Background()
|
for key, value := range source {
|
||||||
taskItem, err := task.NewTaskWithOps(req.Image, task.TaskUpgrade, task.TaskScopeImage, req.TaskID, 1)
|
result[key] = value
|
||||||
if err != nil {
|
|
||||||
global.LOG.Errorf("new task for create container failed, err: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
go func() {
|
return result
|
||||||
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 {
|
func (u *ContainerService) ContainerRename(req dto.ContainerRename) error {
|
||||||
@@ -778,6 +674,8 @@ func (u *ContainerService) ContainerRename(req dto.ContainerRename) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
unlock := containerOperationLock.lock(req.Name, req.NewName)
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
newContainer, _ := client.ContainerInspect(ctx, req.NewName)
|
newContainer, _ := client.ContainerInspect(ctx, req.NewName)
|
||||||
if newContainer.ContainerJSONBase != nil {
|
if newContainer.ContainerJSONBase != nil {
|
||||||
@@ -822,44 +720,48 @@ func (u *ContainerService) ContainerCommit(req dto.ContainerCommit) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error {
|
func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error {
|
||||||
var err error
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer client.Close()
|
|
||||||
taskItem, err := task.NewTaskWithOps(strings.Join(req.Names, " "), req.Operation, task.TaskScopeContainer, req.TaskID, 1)
|
taskItem, err := task.NewTaskWithOps(strings.Join(req.Names, " "), req.Operation, task.TaskScopeContainer, req.TaskID, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
_ = client.Close()
|
||||||
return fmt.Errorf("new task for container commit failed, err: %v", err)
|
return fmt.Errorf("new task for container commit failed, err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range req.Names {
|
for _, item := range req.Names {
|
||||||
|
item := item
|
||||||
taskItem.AddSubTask(item, func(t *task.Task) error {
|
taskItem.AddSubTask(item, func(t *task.Task) error {
|
||||||
|
unlock := containerOperationLock.lock(item)
|
||||||
|
defer unlock()
|
||||||
|
var operationErr error
|
||||||
switch req.Operation {
|
switch req.Operation {
|
||||||
case constant.ContainerOpStart:
|
case constant.ContainerOpStart:
|
||||||
err = client.ContainerStart(ctx, item, container.StartOptions{})
|
operationErr = client.ContainerStart(ctx, item, container.StartOptions{})
|
||||||
case constant.ContainerOpStop:
|
case constant.ContainerOpStop:
|
||||||
err = client.ContainerStop(ctx, item, container.StopOptions{})
|
operationErr = client.ContainerStop(ctx, item, container.StopOptions{})
|
||||||
case constant.ContainerOpRestart:
|
case constant.ContainerOpRestart:
|
||||||
err = client.ContainerRestart(ctx, item, container.StopOptions{})
|
operationErr = client.ContainerRestart(ctx, item, container.StopOptions{})
|
||||||
case constant.ContainerOpKill:
|
case constant.ContainerOpKill:
|
||||||
err = client.ContainerKill(ctx, item, "SIGKILL")
|
operationErr = client.ContainerKill(ctx, item, "SIGKILL")
|
||||||
case constant.ContainerOpPause:
|
case constant.ContainerOpPause:
|
||||||
err = client.ContainerPause(ctx, item)
|
operationErr = client.ContainerPause(ctx, item)
|
||||||
case constant.ContainerOpUnpause:
|
case constant.ContainerOpUnpause:
|
||||||
err = client.ContainerUnpause(ctx, item)
|
operationErr = client.ContainerUnpause(ctx, item)
|
||||||
case constant.ContainerOpRemove:
|
case constant.ContainerOpRemove:
|
||||||
err = client.ContainerRemove(ctx, item, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
operationErr = client.ContainerRemove(ctx, item, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
||||||
}
|
}
|
||||||
return err
|
return operationErr
|
||||||
}, nil)
|
}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
defer client.Close()
|
||||||
_ = taskItem.Execute()
|
_ = taskItem.Execute()
|
||||||
}()
|
}()
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
|
func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
|
||||||
@@ -868,6 +770,8 @@ func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
unlock := containerOperationLock.lock(req.Name)
|
||||||
|
defer unlock()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
containerItem, err := client.ContainerInspect(ctx, req.Name)
|
containerItem, err := client.ContainerInspect(ctx, req.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1832,7 +1736,7 @@ func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerLis
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) {
|
func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error) {
|
||||||
portMap := make(nat.PortMap)
|
portMap := make(nat.PortMap)
|
||||||
if len(ports) == 0 {
|
if len(ports) == 0 {
|
||||||
return portMap, nil
|
return portMap, nil
|
||||||
@@ -1857,7 +1761,7 @@ func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) {
|
|||||||
portMap[nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))] = []nat.PortBinding{bindItem}
|
portMap[nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))] = []nat.PortBinding{bindItem}
|
||||||
}
|
}
|
||||||
for i := hostStart; i <= hostEnd; i++ {
|
for i := hostStart; i <= hostEnd; i++ {
|
||||||
if common.ScanPortWithIP(port.HostIP, i) {
|
if checkInUse && common.ScanPortWithIP(port.HostIP, i) {
|
||||||
return portMap, buserr.WithDetail("ErrPortInUsed", i, nil)
|
return portMap, buserr.WithDetail("ErrPortInUsed", i, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1868,7 +1772,7 @@ func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) {
|
|||||||
} else {
|
} else {
|
||||||
portItem, _ = strconv.Atoi(port.HostPort)
|
portItem, _ = strconv.Atoi(port.HostPort)
|
||||||
}
|
}
|
||||||
if common.ScanPortWithIP(port.HostIP, portItem) {
|
if checkInUse && common.ScanPortWithIP(port.HostIP, portItem) {
|
||||||
return portMap, buserr.WithDetail("ErrPortInUsed", portItem, nil)
|
return portMap, buserr.WithDetail("ErrPortInUsed", portItem, nil)
|
||||||
}
|
}
|
||||||
bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP}
|
bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP}
|
||||||
@@ -1887,7 +1791,7 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta
|
|||||||
}
|
}
|
||||||
var networkConf network.NetworkingConfig
|
var networkConf network.NetworkingConfig
|
||||||
|
|
||||||
portMap, err := checkPortStats(req.ExposedPorts)
|
portMap, err := checkPortStats(req.ExposedPorts, isCreate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -1915,15 +1819,21 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta
|
|||||||
case "host", "none", "bridge":
|
case "host", "none", "bridge":
|
||||||
hostConf.NetworkMode = container.NetworkMode(item.Network)
|
hostConf.NetworkMode = container.NetworkMode(item.Network)
|
||||||
}
|
}
|
||||||
if item.Ipv4 != "" || item.Ipv6 != "" {
|
endpoint := &network.EndpointSettings{
|
||||||
networkConf.EndpointsConfig[item.Network] = &network.EndpointSettings{
|
Links: append([]string(nil), item.Links...),
|
||||||
IPAMConfig: &network.EndpointIPAMConfig{
|
Aliases: append([]string(nil), item.Aliases...),
|
||||||
IPv4Address: item.Ipv4,
|
DriverOpts: cloneStringMap(item.DriverOpts),
|
||||||
IPv6Address: item.Ipv6,
|
GwPriority: item.GwPriority,
|
||||||
}, MacAddress: item.MacAddr}
|
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 {
|
} else {
|
||||||
return nil, nil, nil, fmt.Errorf("please set up the network")
|
return nil, nil, nil, fmt.Errorf("please set up the network")
|
||||||
@@ -1970,43 +1880,6 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta
|
|||||||
return &config, &hostConf, &networkConf, nil
|
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 {
|
func loadVolumeBinds(binds []container.MountPoint) []dto.VolumeHelper {
|
||||||
var datas []dto.VolumeHelper
|
var datas []dto.VolumeHelper
|
||||||
for _, bind := range binds {
|
for _, bind := range binds {
|
||||||
@@ -2153,6 +2026,9 @@ func loadComposeCount(client *client.Client) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, compose := range composeCreatedByLocal {
|
for _, compose := range composeCreatedByLocal {
|
||||||
|
if len(compose.Path) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if _, has := composeMap[compose.Name]; !has {
|
if _, has := composeMap[compose.Name]; !has {
|
||||||
composeMap[compose.Name] = struct{}{}
|
composeMap[compose.Name] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -23,8 +24,10 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const composeProjectLabel = "com.docker.compose.project"
|
const composeProjectLabel = "com.docker.compose.project"
|
||||||
@@ -52,7 +55,15 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
composeCreatedByLocal, _ := composeRepo.ListRecord()
|
composeRecords, _ := composeRepo.ListRecord()
|
||||||
|
pinnedByName := make(map[string]bool, len(composeRecords))
|
||||||
|
composeCreatedByLocal := make([]model.Compose, 0, len(composeRecords))
|
||||||
|
for _, record := range composeRecords {
|
||||||
|
pinnedByName[record.Name] = record.IsPinned
|
||||||
|
if len(record.Path) != 0 {
|
||||||
|
composeCreatedByLocal = append(composeCreatedByLocal, record)
|
||||||
|
}
|
||||||
|
}
|
||||||
composeLocalMap := make(map[string]dto.ComposeInfo)
|
composeLocalMap := make(map[string]dto.ComposeInfo)
|
||||||
for _, localItem := range composeCreatedByLocal {
|
for _, localItem := range composeCreatedByLocal {
|
||||||
composeItemLocal := dto.ComposeInfo{
|
composeItemLocal := dto.ComposeInfo{
|
||||||
@@ -136,6 +147,7 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
for key, value := range mergedMap {
|
for key, value := range mergedMap {
|
||||||
value.Name = key
|
value.Name = key
|
||||||
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
|
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
|
||||||
|
value.IsPinned = pinnedByName[key]
|
||||||
records = append(records, value)
|
records = append(records, value)
|
||||||
}
|
}
|
||||||
if len(req.Info) != 0 {
|
if len(req.Info) != 0 {
|
||||||
@@ -149,7 +161,21 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if req.ExcludeAppStore {
|
||||||
|
length, count := len(records), 0
|
||||||
|
for count < length {
|
||||||
|
if records[count].CreatedBy == "Apps" {
|
||||||
|
records = append(records[:count], records[(count+1):]...)
|
||||||
|
length--
|
||||||
|
} else {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
sort.Slice(records, func(i, j int) bool {
|
sort.Slice(records, func(i, j int) bool {
|
||||||
|
if records[i].IsPinned != records[j].IsPinned {
|
||||||
|
return records[i].IsPinned
|
||||||
|
}
|
||||||
return records[i].CreatedAt > records[j].CreatedAt
|
return records[i].CreatedAt > records[j].CreatedAt
|
||||||
})
|
})
|
||||||
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||||
@@ -189,53 +215,62 @@ func composeFileExists(workdir, configFile string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
||||||
if cmd.CheckIllegal(req.Path) {
|
if err := validateComposeCreateName(req); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if hasIllegalComposeCreateInput(req) {
|
||||||
return false, buserr.New("ErrCmdIllegal")
|
return false, buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
composeItem, _ := composeRepo.GetRecord(repo.WithByName(req.Name))
|
projectName, err := resolveComposeCreateProjectName(req)
|
||||||
if composeItem.ID != 0 {
|
|
||||||
return false, buserr.New("ErrRecordExist")
|
|
||||||
}
|
|
||||||
if err := u.loadPath(&req); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
cmd := getComposeCmd(req.Path, "config")
|
|
||||||
stdout, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("docker-compose config failed, std: %s, err: %v", string(stdout), err)
|
return false, err
|
||||||
|
}
|
||||||
|
if err := checkComposeCreateDuplicate(req, projectName); err != nil {
|
||||||
|
return false, err
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
|
func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
|
||||||
if cmd.CheckIllegal(req.Name, req.Path) {
|
if err := validateComposeCreateName(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if hasIllegalComposeCreateInput(req) {
|
||||||
return buserr.New("ErrCmdIllegal")
|
return buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
|
projectName, err := resolveComposeCreateProjectName(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := checkComposeCreateDuplicate(req, projectName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := u.loadPath(&req); err != nil {
|
if err := u.loadPath(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if req.From == "path" {
|
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
||||||
req.Name = path.Base(path.Dir(req.Path))
|
return err
|
||||||
}
|
}
|
||||||
|
req.Name = projectName
|
||||||
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeCompose, req.TaskID, 1)
|
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeCompose, req.TaskID, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("new task for image build failed, err: %v", err)
|
return fmt.Errorf("new task for image build failed, err: %v", err)
|
||||||
}
|
}
|
||||||
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
go func() {
|
go func() {
|
||||||
taskItem.AddSubTask(i18n.GetMsgByKey("ComposeCreate"), func(t *task.Task) error {
|
taskItem.AddSubTask(i18n.GetMsgByKey("ComposeCreate"), func(t *task.Task) error {
|
||||||
err := compose.UpWithTask(req.Path, t, req.ForcePull)
|
err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name)
|
||||||
t.LogWithStatus(i18n.GetMsgByKey("ComposeCreate"), err)
|
t.LogWithStatus(i18n.GetMsgByKey("ComposeCreate"), err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = compose.Down(req.Path)
|
_, _ = compose.Down(req.Path, req.Name)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_ = composeRepo.CreateRecord(&model.Compose{Name: strings.ToLower(req.Name), Path: req.Path})
|
recordName := strings.ToLower(req.Name)
|
||||||
|
record, _ := composeRepo.GetRecord(repo.WithByName(recordName))
|
||||||
|
if record.ID == 0 {
|
||||||
|
_ = composeRepo.CreateRecord(&model.Compose{Name: recordName, Path: req.Path})
|
||||||
|
} else {
|
||||||
|
_ = composeRepo.UpdateRecord(recordName, map[string]interface{}{"path": req.Path})
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}, nil)
|
}, nil)
|
||||||
_ = taskItem.Execute()
|
_ = taskItem.Execute()
|
||||||
@@ -244,6 +279,243 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkComposeRecordName(name string) error {
|
||||||
|
composeItem, _ := composeRepo.GetRecord(repo.WithByName(name))
|
||||||
|
if composeItem.ID != 0 && len(composeItem.Path) != 0 {
|
||||||
|
return buserr.New("ErrRecordExist")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkComposeCreateDuplicate(req dto.ComposeCreate, projectName string) error {
|
||||||
|
if err := checkComposeRecordName(projectName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if req.From == "path" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
composeItem, _ := composeRepo.GetRecord(repo.WithByPath(composeCreatePath(req)))
|
||||||
|
if composeItem.ID != 0 && composeItem.Path != "" {
|
||||||
|
return buserr.New("ErrRecordExist")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateComposeCreateName(req dto.ComposeCreate) error {
|
||||||
|
if req.From == "path" {
|
||||||
|
name := strings.TrimSpace(req.Name)
|
||||||
|
if name != "" && !re.GetRegex(re.ComposeNamePattern).MatchString(name) {
|
||||||
|
return buserr.New("ErrComposeNameInvalid")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !re.GetRegex(re.ComposeNamePattern).MatchString(composeCreateDirName(req)) {
|
||||||
|
return buserr.New("ErrComposeNameInvalid")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasIllegalComposeCreateInput(req dto.ComposeCreate) bool {
|
||||||
|
if req.From == "path" {
|
||||||
|
return cmd.CheckIllegal(req.Name, req.Path)
|
||||||
|
}
|
||||||
|
return cmd.CheckIllegal(composeCreateDirName(req))
|
||||||
|
}
|
||||||
|
|
||||||
|
func composeCreateDirName(req dto.ComposeCreate) string {
|
||||||
|
dirName := strings.TrimSpace(req.DirName)
|
||||||
|
if dirName == "" {
|
||||||
|
// Keep compatibility with callers that used name as both the directory and
|
||||||
|
// Compose project name before dirName was introduced.
|
||||||
|
return strings.TrimSpace(req.Name)
|
||||||
|
}
|
||||||
|
return dirName
|
||||||
|
}
|
||||||
|
|
||||||
|
func composeCreatePath(req dto.ComposeCreate) string {
|
||||||
|
return filepath.Join(global.Dir.DataDir, "docker", "compose", composeCreateDirName(req), "docker-compose.yml")
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveComposeCreateProjectName(req dto.ComposeCreate) (string, error) {
|
||||||
|
if req.From == "path" {
|
||||||
|
envPath, err := createComposeTempFile(
|
||||||
|
filepath.Dir(primaryComposePath(req.Path)),
|
||||||
|
".1panel-compose-*.env",
|
||||||
|
req.Env,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer os.Remove(envPath)
|
||||||
|
return resolveComposeProjectName(req.Path, req.Name, envPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Dir(composeCreatePath(req))
|
||||||
|
cleanupDir, err := prepareComposeStagingDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer cleanupDir()
|
||||||
|
|
||||||
|
composePath, err := createComposeTempFile(dir, ".1panel-compose-*.yml", req.File)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer os.Remove(composePath)
|
||||||
|
|
||||||
|
envPath, err := createComposeTempFile(dir, ".1panel-compose-*.env", req.Env)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer os.Remove(envPath)
|
||||||
|
|
||||||
|
return resolveComposeProjectName(composePath, "", envPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createComposeTempFile(dir, pattern, content string) (string, error) {
|
||||||
|
file, err := os.CreateTemp(dir, pattern)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
filePath := file.Name()
|
||||||
|
if _, err := file.WriteString(content); err != nil {
|
||||||
|
_ = file.Close()
|
||||||
|
_ = os.Remove(filePath)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
_ = os.Remove(filePath)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareComposeStagingDir(dir string) (func(), error) {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dir), os.ModePerm); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
created := false
|
||||||
|
if err := os.Mkdir(dir, os.ModePerm); err != nil {
|
||||||
|
if !errors.Is(err, os.ErrExist) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
created = true
|
||||||
|
}
|
||||||
|
return func() {
|
||||||
|
if created {
|
||||||
|
_ = os.Remove(dir)
|
||||||
|
}
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveComposeProjectName(composePath, fallbackName, envFile string) (string, error) {
|
||||||
|
// Preserve the name resolved by Compose (including a top-level name) so the
|
||||||
|
// container label and the local record always use the same project identity.
|
||||||
|
parentName := normalizeComposeProjectName(path.Base(path.Dir(primaryComposePath(composePath))))
|
||||||
|
fallbackName = strings.TrimSpace(fallbackName)
|
||||||
|
stdout, err := runComposeConfig(composePath, "", envFile)
|
||||||
|
if err == nil {
|
||||||
|
projectName, parseErr := loadComposeProjectName(stdout)
|
||||||
|
if parseErr != nil {
|
||||||
|
return "", parseErr
|
||||||
|
}
|
||||||
|
if projectName != "" {
|
||||||
|
if !re.GetRegex(re.ComposeNamePattern).MatchString(projectName) {
|
||||||
|
return "", buserr.New("ErrComposeNameInvalid")
|
||||||
|
}
|
||||||
|
return projectName, nil
|
||||||
|
}
|
||||||
|
if parentName != "" {
|
||||||
|
return parentName, nil
|
||||||
|
}
|
||||||
|
if fallbackName != "" {
|
||||||
|
if _, fallbackErr := runComposeConfig(composePath, fallbackName, envFile); fallbackErr != nil {
|
||||||
|
return "", fallbackErr
|
||||||
|
}
|
||||||
|
return fallbackName, nil
|
||||||
|
}
|
||||||
|
return "", buserr.New("ErrComposeProjectNameEmpty")
|
||||||
|
}
|
||||||
|
if !isComposeProjectNameEmptyError(err) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveErr := err
|
||||||
|
if parentName != "" {
|
||||||
|
if _, parentErr := runComposeConfig(composePath, parentName, envFile); parentErr == nil {
|
||||||
|
return parentName, nil
|
||||||
|
} else {
|
||||||
|
resolveErr = parentErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fallbackName != "" && fallbackName != parentName {
|
||||||
|
if _, fallbackErr := runComposeConfig(composePath, fallbackName, envFile); fallbackErr == nil {
|
||||||
|
return fallbackName, nil
|
||||||
|
} else {
|
||||||
|
return "", fallbackErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if parentName == "" && fallbackName == "" {
|
||||||
|
return "", buserr.New("ErrComposeProjectNameEmpty")
|
||||||
|
}
|
||||||
|
return "", resolveErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func runComposeConfig(composePath, projectName, envFile string) ([]byte, error) {
|
||||||
|
configCmd := getComposeCmdWithEnv(composePath, "config", envFile, projectName)
|
||||||
|
stdout, err := configCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
var stderr []byte
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
stderr = exitErr.Stderr
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("docker-compose config failed, std: %s, err: %v", mergeComposeOutput(stdout, stderr), err)
|
||||||
|
}
|
||||||
|
return stdout, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeComposeOutput(stdout, stderr []byte) string {
|
||||||
|
outputs := make([]string, 0, 2)
|
||||||
|
if output := strings.TrimSpace(string(stdout)); output != "" {
|
||||||
|
outputs = append(outputs, output)
|
||||||
|
}
|
||||||
|
if output := strings.TrimSpace(string(stderr)); output != "" {
|
||||||
|
outputs = append(outputs, output)
|
||||||
|
}
|
||||||
|
return strings.Join(outputs, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadComposeProjectName(config []byte) (string, error) {
|
||||||
|
var project struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(config, &project); err != nil {
|
||||||
|
return "", buserr.WithDetail("ErrComposeProjectNameParse", err.Error(), err)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(project.Name), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func primaryComposePath(composePath string) string {
|
||||||
|
if index := strings.Index(composePath, ","); index >= 0 {
|
||||||
|
return composePath[:index]
|
||||||
|
}
|
||||||
|
return composePath
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeComposeProjectName(name string) string {
|
||||||
|
name = re.GetRegex(re.ComposeDisallowedCharsPattern).
|
||||||
|
ReplaceAllString(strings.ToLower(strings.TrimSpace(name)), "")
|
||||||
|
return strings.TrimLeft(name, "_-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isComposeProjectNameEmptyError(err error) bool {
|
||||||
|
message := strings.ToLower(err.Error())
|
||||||
|
return strings.Contains(message, "project name must not be empty") ||
|
||||||
|
strings.Contains(message, "project name can't be empty")
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
||||||
if len(req.Path) == 0 && req.Operation == "delete" {
|
if len(req.Path) == 0 && req.Operation == "delete" {
|
||||||
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
||||||
@@ -267,15 +539,15 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if req.Operation == "up" {
|
if req.Operation == "up" {
|
||||||
if stdout, err := compose.Up(req.Path); err != nil {
|
if stdout, err := compose.Up(req.Path, req.Name); err != nil {
|
||||||
return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err)
|
return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err)
|
||||||
}
|
}
|
||||||
} else if req.Operation == "rebuild" {
|
} else if req.Operation == "rebuild" {
|
||||||
if stdout, err := compose.DownAndUp(req.Path); err != nil {
|
if stdout, err := compose.DownAndUp(req.Path, req.Name); err != nil {
|
||||||
return fmt.Errorf("docker-compose rebuild failed, std: %s, err: %v", stdout, err)
|
return fmt.Errorf("docker-compose rebuild failed, std: %s, err: %v", stdout, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if stdout, err := compose.Operate(req.Path, req.Operation); err != nil {
|
if stdout, err := compose.Operate(req.Path, req.Operation, req.Name); err != nil {
|
||||||
return fmt.Errorf("docker-compose %s failed, std: %s, err: %v", req.Operation, stdout, err)
|
return fmt.Errorf("docker-compose %s failed, std: %s, err: %v", req.Operation, stdout, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,9 +583,9 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := compose.UpWithTask(req.Path, t, req.ForcePull); err != nil {
|
if err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name); err != nil {
|
||||||
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
|
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
|
||||||
if err := recreateCompose(string(oldFile), req.Path); err != nil {
|
if err := recreateCompose(string(oldFile), req.Path, req.Name); err != nil {
|
||||||
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
|
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("update failed when handle compose up, err: %s", err)
|
return fmt.Errorf("update failed when handle compose up, err: %s", err)
|
||||||
@@ -327,6 +599,20 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ContainerService) ComposePin(req dto.ComposePin) error {
|
||||||
|
record, _ := composeRepo.GetRecord(repo.WithByName(req.Name))
|
||||||
|
if record.ID == 0 {
|
||||||
|
if !req.IsPinned {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return composeRepo.CreateRecord(&model.Compose{Name: req.Name, IsPinned: true})
|
||||||
|
}
|
||||||
|
if !req.IsPinned && len(record.Path) == 0 {
|
||||||
|
return composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
||||||
|
}
|
||||||
|
return composeRepo.UpdateRecord(req.Name, map[string]interface{}{"is_pinned": req.IsPinned})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ComposeLogClean(req dto.ComposeLogClean) error {
|
func (u *ContainerService) ComposeLogClean(req dto.ComposeLogClean) error {
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -389,15 +675,15 @@ func (u *ContainerService) LoadComposeEnv(name string) (string, error) {
|
|||||||
|
|
||||||
func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
||||||
if req.From == "template" || req.From == "edit" {
|
if req.From == "template" || req.From == "edit" {
|
||||||
dir := fmt.Sprintf("%s/docker/compose/%s", global.Dir.DataDir, req.Name)
|
composePath := composeCreatePath(*req)
|
||||||
|
dir := filepath.Dir(composePath)
|
||||||
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
|
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
|
||||||
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
|
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
path := fmt.Sprintf("%s/docker-compose.yml", dir)
|
file, err := os.OpenFile(composePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, constant.FilePerm)
|
||||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, constant.FilePerm)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -405,14 +691,14 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
|||||||
write := bufio.NewWriter(file)
|
write := bufio.NewWriter(file)
|
||||||
_, _ = write.WriteString(string(req.File))
|
_, _ = write.WriteString(string(req.File))
|
||||||
write.Flush()
|
write.Flush()
|
||||||
req.Path = path
|
req.Path = composePath
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeContainerForCompose(composeName, composePath string) error {
|
func removeContainerForCompose(composeName, composePath string) error {
|
||||||
if _, err := os.Stat(composePath); err == nil {
|
if _, err := os.Stat(composePath); err == nil {
|
||||||
if stdout, err := compose.Operate(composePath, "down"); err != nil {
|
if stdout, err := compose.Operate(composePath, "down", composeName); err != nil {
|
||||||
return errors.New(stdout)
|
return errors.New(stdout)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -437,7 +723,7 @@ func removeContainerForCompose(composeName, composePath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func recreateCompose(content, path string) error {
|
func recreateCompose(content, path, projectName string) error {
|
||||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
|
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -447,7 +733,7 @@ func recreateCompose(content, path string) error {
|
|||||||
_, _ = write.WriteString(content)
|
_, _ = write.WriteString(content)
|
||||||
write.Flush()
|
write.Flush()
|
||||||
|
|
||||||
if stdout, err := compose.Up(path); err != nil {
|
if stdout, err := compose.Up(path, projectName); err != nil {
|
||||||
return errors.New(string(stdout))
|
return errors.New(string(stdout))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -0,0 +1,663 @@
|
|||||||
|
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 containerInspectClient interface {
|
||||||
|
ContainerInspect(context.Context, string) (container.InspectResponse, 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 containerInspectClient, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
initialRestartCount := info.RestartCount
|
||||||
|
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 != initialRestartCount {
|
||||||
|
return fmt.Errorf("container restart count changed from %d to %d during startup", initialRestartCount, 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 containerInspectClient, containerID string, initial container.InspectResponse) error {
|
||||||
|
startedAt := initial.State.StartedAt
|
||||||
|
restartCount := initial.RestartCount
|
||||||
|
if err := checkContainerStableState(initial, startedAt, restartCount); 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, restartCount)
|
||||||
|
case <-ticker.C:
|
||||||
|
info, err := cli.ContainerInspect(ctx, containerID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := checkContainerStableState(info, startedAt, restartCount); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkContainerStableState(info container.InspectResponse, startedAt string, restartCount int) error {
|
||||||
|
if err := checkContainerRunningState(info); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.State.Restarting || info.RestartCount != restartCount {
|
||||||
|
return fmt.Errorf("container restart count changed from %d to %d during startup", restartCount, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -538,11 +538,14 @@ func (u *CronjobService) CleanRecord(req dto.CronjobClean) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, del := range delRecords {
|
for _, del := range delRecords {
|
||||||
|
if del.Status == constant.StatusWaiting || del.Status == constant.StatusRunning {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := cronjobRepo.DeleteRecord(repo.WithByID(del.ID)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
_ = os.RemoveAll(del.Records)
|
_ = os.RemoveAll(del.Records)
|
||||||
}
|
}
|
||||||
if err := cronjobRepo.DeleteRecord(cronjobRepo.WithByJobID(int(req.CronjobID))); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,7 +647,7 @@ func (u *CronjobService) HandleStop(id uint) error {
|
|||||||
if len(record.TaskID) == 0 {
|
if len(record.TaskID) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if cancel, ok := global.TaskCtxMap[record.TaskID]; ok {
|
if cancel, ok := global.LoadTaskCancel(record.TaskID); ok {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time, t
|
|||||||
|
|
||||||
src := path.Join(backupDir, record.FileName)
|
src := path.Join(backupDir, record.FileName)
|
||||||
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
|
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
|
||||||
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil {
|
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil {
|
||||||
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
|
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
|
||||||
retry++
|
retry++
|
||||||
return err
|
return err
|
||||||
@@ -119,7 +119,7 @@ func (u *CronjobService) handleWebsite(cronjob model.Cronjob, startTime time.Tim
|
|||||||
|
|
||||||
src := path.Join(backupDir, record.FileName)
|
src := path.Join(backupDir, record.FileName)
|
||||||
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
|
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
|
||||||
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil {
|
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil {
|
||||||
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
|
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
|
||||||
retry++
|
retry++
|
||||||
return err
|
return err
|
||||||
@@ -163,6 +163,7 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
|
|||||||
record.Name = dbInfo.Database
|
record.Name = dbInfo.Database
|
||||||
record.DetailName = dbInfo.Name
|
record.DetailName = dbInfo.Name
|
||||||
record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
|
record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
|
||||||
|
record.Args = encodeBackupArgs(dbInfo.Args)
|
||||||
|
|
||||||
backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name))
|
backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name))
|
||||||
switch dbInfo.DBType {
|
switch dbInfo.DBType {
|
||||||
@@ -216,7 +217,7 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
|
|||||||
|
|
||||||
src := path.Join(backupDir, record.FileName)
|
src := path.Join(backupDir, record.FileName)
|
||||||
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
|
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
|
||||||
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil {
|
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil {
|
||||||
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
|
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
|
||||||
retry++
|
retry++
|
||||||
return err
|
return err
|
||||||
@@ -274,7 +275,7 @@ func (u *CronjobService) handleDirectory(cronjob model.Cronjob, startTime time.T
|
|||||||
|
|
||||||
src := path.Join(backupDir, fileName)
|
src := path.Join(backupDir, fileName)
|
||||||
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
|
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
|
||||||
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil {
|
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
record.FileDir = path.Dir(dst)
|
record.FileDir = path.Dir(dst)
|
||||||
@@ -310,7 +311,7 @@ func (u *CronjobService) handleSystemLog(cronjob model.Cronjob, startTime time.T
|
|||||||
|
|
||||||
src := path.Join(path.Dir(backupDir), fileName)
|
src := path.Join(path.Dir(backupDir), fileName)
|
||||||
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
|
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
|
||||||
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil {
|
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
record.FileDir = path.Dir(dst)
|
record.FileDir = path.Dir(dst)
|
||||||
|
|||||||
@@ -184,10 +184,30 @@ func (u *DashboardService) LoadBaseInfo(ioOption string, netOption string) (*dto
|
|||||||
|
|
||||||
func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent {
|
func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent {
|
||||||
var currentInfo dto.DashboardCurrent
|
var currentInfo dto.DashboardCurrent
|
||||||
hostInfo, _ := psutil.HOST.GetHostInfo(false)
|
shotTime := time.Now()
|
||||||
currentInfo.Uptime = hostInfo.Uptime
|
hostInfo, err := psutil.HOST.GetHostInfo(false)
|
||||||
currentInfo.TimeSinceUptime = time.Unix(int64(hostInfo.BootTime), 0).Format(constant.DateTimeLayout)
|
if err != nil {
|
||||||
currentInfo.RunningTime = loadRunningTime(hostInfo.Uptime)
|
global.LOG.Errorf("load host info failed: %v", err)
|
||||||
|
currentInfo.ShotTime = shotTime
|
||||||
|
return ¤tInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
uptime := hostInfo.Uptime
|
||||||
|
var bootTime uint64
|
||||||
|
if now := shotTime.Unix(); now > 0 {
|
||||||
|
nowUnix := uint64(now)
|
||||||
|
if hostInfo.BootTime > 0 && hostInfo.BootTime <= nowUnix {
|
||||||
|
bootTime = hostInfo.BootTime
|
||||||
|
uptime = nowUnix - bootTime
|
||||||
|
} else if uptime <= nowUnix {
|
||||||
|
bootTime = nowUnix - uptime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentInfo.Uptime = uptime
|
||||||
|
currentInfo.RunningTime = loadRunningTime(uptime)
|
||||||
|
if bootTime > 0 {
|
||||||
|
currentInfo.TimeSinceUptime = time.Unix(int64(bootTime), 0).Format(constant.DateTimeLayout)
|
||||||
|
}
|
||||||
currentInfo.Procs = hostInfo.Procs
|
currentInfo.Procs = hostInfo.Procs
|
||||||
currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false)
|
currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false)
|
||||||
|
|
||||||
@@ -229,13 +249,12 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
|
|||||||
|
|
||||||
if ioOption == "all" {
|
if ioOption == "all" {
|
||||||
diskInfo, _ := disk.IOCounters()
|
diskInfo, _ := disk.IOCounters()
|
||||||
for _, state := range diskInfo {
|
state := sumDiskIOCounters(diskInfo)
|
||||||
currentInfo.IOReadBytes += state.ReadBytes
|
currentInfo.IOReadBytes = state.ReadBytes
|
||||||
currentInfo.IOWriteBytes += state.WriteBytes
|
currentInfo.IOWriteBytes = state.WriteBytes
|
||||||
currentInfo.IOCount += (state.ReadCount + state.WriteCount)
|
currentInfo.IOCount = state.ReadCount + state.WriteCount
|
||||||
currentInfo.IOReadTime += state.ReadTime
|
currentInfo.IOReadTime = state.ReadTime
|
||||||
currentInfo.IOWriteTime += state.WriteTime
|
currentInfo.IOWriteTime = state.WriteTime
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
diskInfo, _ := disk.IOCounters(ioOption)
|
diskInfo, _ := disk.IOCounters(ioOption)
|
||||||
for _, state := range diskInfo {
|
for _, state := range diskInfo {
|
||||||
@@ -264,7 +283,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentInfo.ShotTime = time.Now()
|
currentInfo.ShotTime = shotTime
|
||||||
return ¤tInfo
|
return ¤tInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+984
-129
File diff suppressed because it is too large
Load Diff
@@ -28,10 +28,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
rollbackPath = "1panel/tmp"
|
rollbackPath = "1panel/tmp"
|
||||||
upgradePath = "1panel/tmp/upgrade"
|
communityRestorePath = "1panel/tmp/community-restore"
|
||||||
uploadPath = "1panel/uploads"
|
upgradePath = "1panel/tmp/upgrade"
|
||||||
downloadPath = "1panel/download"
|
uploadPath = "1panel/uploads"
|
||||||
|
downloadPath = "1panel/download"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (u *DeviceService) Scan() dto.CleanData {
|
func (u *DeviceService) Scan() dto.CleanData {
|
||||||
@@ -58,7 +59,7 @@ func (u *DeviceService) Scan() dto.CleanData {
|
|||||||
|
|
||||||
SystemClean.BackupClean = loadBackupTree(fileOp)
|
SystemClean.BackupClean = loadBackupTree(fileOp)
|
||||||
|
|
||||||
rollBackTree := loadRollBackTree(fileOp)
|
rollBackTree := loadRollBackTree()
|
||||||
rollbackSize := uint64(0)
|
rollbackSize := uint64(0)
|
||||||
for _, rollback := range rollBackTree {
|
for _, rollback := range rollBackTree {
|
||||||
rollbackSize += rollback.Size
|
rollbackSize += rollback.Size
|
||||||
@@ -113,12 +114,15 @@ func (u *DeviceService) Clean(req []dto.Clean) {
|
|||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app"))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app"))
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database"))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database"))
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website"))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website"))
|
||||||
|
dropFileOrDir(path.Join(global.Dir.BaseDir, communityRestorePath))
|
||||||
case "rollback_app":
|
case "rollback_app":
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app", item.Name))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app", item.Name))
|
||||||
case "rollback_database":
|
case "rollback_database":
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database", item.Name))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database", item.Name))
|
||||||
case "rollback_website":
|
case "rollback_website":
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website", item.Name))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website", item.Name))
|
||||||
|
case "rollback_community_restore":
|
||||||
|
dropFileOrDir(path.Join(global.Dir.BaseDir, communityRestorePath, item.Name))
|
||||||
|
|
||||||
case "upload":
|
case "upload":
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, item.Name))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, item.Name))
|
||||||
@@ -214,6 +218,7 @@ func doSystemClean(taskItem *task.Task) func(t *task.Task) error {
|
|||||||
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "app"), taskItem, &size, &fileCount)
|
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "app"), taskItem, &size, &fileCount)
|
||||||
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "website"), taskItem, &size, &fileCount)
|
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "website"), taskItem, &size, &fileCount)
|
||||||
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "database"), taskItem, &size, &fileCount)
|
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "database"), taskItem, &size, &fileCount)
|
||||||
|
dropWithTask(path.Join(global.Dir.BaseDir, communityRestorePath), taskItem, &size, &fileCount)
|
||||||
|
|
||||||
upgrades := path.Join(global.Dir.BaseDir, upgradePath)
|
upgrades := path.Join(global.Dir.BaseDir, upgradePath)
|
||||||
oldUpgradeFiles, _ := os.ReadDir(upgrades)
|
oldUpgradeFiles, _ := os.ReadDir(upgrades)
|
||||||
@@ -606,20 +611,21 @@ func isExactPathMatch(path string, excludePaths []string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadRollBackTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
func loadRollBackTree() []dto.CleanTree {
|
||||||
var treeData []dto.CleanTree
|
var treeData []dto.CleanTree
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "app"), "rollback_app", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "app"), "rollback_app")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "website"), "rollback_website", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "website"), "rollback_website")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "database"), "rollback_database", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "database"), "rollback_database")
|
||||||
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, communityRestorePath), "rollback_community_restore")
|
||||||
|
|
||||||
return treeData
|
return treeData
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadUploadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
func loadUploadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||||
var treeData []dto.CleanTree
|
var treeData []dto.CleanTree
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "app"), "upload_app", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "app"), "upload_app")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "website"), "upload_website", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "website"), "upload_website")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "database"), "upload_database", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "database"), "upload_database")
|
||||||
|
|
||||||
path5 := path.Join(global.Dir.BaseDir, uploadPath)
|
path5 := path.Join(global.Dir.BaseDir, uploadPath)
|
||||||
uploadTreeData := loadTreeWithAllFile(true, path5, "upload", path5, fileOp)
|
uploadTreeData := loadTreeWithAllFile(true, path5, "upload", path5, fileOp)
|
||||||
@@ -630,9 +636,9 @@ func loadUploadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
|||||||
|
|
||||||
func loadDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
func loadDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||||
var treeData []dto.CleanTree
|
var treeData []dto.CleanTree
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "app"), "download_app", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "app"), "download_app")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "website"), "download_website", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "website"), "download_website")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "database"), "download_database", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "database"), "download_database")
|
||||||
|
|
||||||
path5 := path.Join(global.Dir.BaseDir, downloadPath)
|
path5 := path.Join(global.Dir.BaseDir, downloadPath)
|
||||||
uploadTreeData := loadTreeWithAllFile(true, path5, "download", path5, fileOp)
|
uploadTreeData := loadTreeWithAllFile(true, path5, "download", path5, fileOp)
|
||||||
@@ -814,16 +820,59 @@ func loadContainerTree() []dto.CleanTree {
|
|||||||
return treeData
|
return treeData
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTreeWithCheck(treeData []dto.CleanTree, pathItem, treeType string, fileOp fileUtils.FileOp) []dto.CleanTree {
|
func loadTreeWithCheck(treeData []dto.CleanTree, pathItem, treeType string) []dto.CleanTree {
|
||||||
size, _ := fileOp.GetDirSize(pathItem)
|
list, size := loadTreeWithFileSize(true, pathItem, treeType, pathItem)
|
||||||
if size == 0 {
|
if len(list) == 0 || size == 0 {
|
||||||
return treeData
|
return treeData
|
||||||
}
|
}
|
||||||
list := loadTreeWithAllFile(true, pathItem, treeType, pathItem, fileOp)
|
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: treeType, Size: size, IsCheck: size > 0, Children: list, Type: treeType, IsRecommend: true, CanDelete: false})
|
||||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: treeType, Size: uint64(size), IsCheck: size > 0, Children: list, Type: treeType, IsRecommend: true, CanDelete: false})
|
|
||||||
return treeData
|
return treeData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadTreeWithFileSize(isCheck bool, originalPath, treeType, pathItem string) ([]dto.CleanTree, uint64) {
|
||||||
|
var (
|
||||||
|
lists []dto.CleanTree
|
||||||
|
total uint64
|
||||||
|
)
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(pathItem)
|
||||||
|
if err != nil {
|
||||||
|
return lists, total
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
item := dto.CleanTree{
|
||||||
|
ID: uuid.NewString(),
|
||||||
|
Label: entry.Name(),
|
||||||
|
Type: treeType,
|
||||||
|
Name: strings.TrimPrefix(path.Join(pathItem, entry.Name()), originalPath+"/"),
|
||||||
|
IsCheck: isCheck,
|
||||||
|
IsRecommend: isCheck,
|
||||||
|
CanDelete: true,
|
||||||
|
}
|
||||||
|
entryPath := path.Join(pathItem, entry.Name())
|
||||||
|
if entry.IsDir() {
|
||||||
|
children, size := loadTreeWithFileSize(isCheck, originalPath, treeType, entryPath)
|
||||||
|
if len(children) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item.Children = children
|
||||||
|
item.Size = size
|
||||||
|
} else {
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item.Size = uint64(info.Size())
|
||||||
|
}
|
||||||
|
if item.Size == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total += item.Size
|
||||||
|
lists = append(lists, item)
|
||||||
|
}
|
||||||
|
return lists, total
|
||||||
|
}
|
||||||
|
|
||||||
func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree {
|
func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||||
var lists []dto.CleanTree
|
var lists []dto.CleanTree
|
||||||
files, err := os.ReadDir(pathItem)
|
files, err := os.ReadDir(pathItem)
|
||||||
|
|||||||
+14
-10
@@ -19,10 +19,12 @@ var (
|
|||||||
agentAccountRepo = repo.NewIAgentAccountRepo()
|
agentAccountRepo = repo.NewIAgentAccountRepo()
|
||||||
agentAccountModelRepo = repo.NewIAgentAccountModelRepo()
|
agentAccountModelRepo = repo.NewIAgentAccountModelRepo()
|
||||||
|
|
||||||
mysqlRepo = repo.NewIMysqlRepo()
|
mysqlRepo = repo.NewIMysqlRepo()
|
||||||
postgresqlRepo = repo.NewIPostgresqlRepo()
|
postgresqlRepo = repo.NewIPostgresqlRepo()
|
||||||
mongodbRepo = repo.NewIMongodbRepo()
|
mongodbRepo = repo.NewIMongodbRepo()
|
||||||
databaseRepo = repo.NewIDatabaseRepo()
|
databaseRepo = repo.NewIDatabaseRepo()
|
||||||
|
databaseUserRepo = repo.NewIDatabaseUserRepo()
|
||||||
|
databaseUserGrantRepo = repo.NewIDatabaseUserGrantRepo()
|
||||||
|
|
||||||
imageRepoRepo = repo.NewIImageRepoRepo()
|
imageRepoRepo = repo.NewIImageRepoRepo()
|
||||||
composeRepo = repo.NewIComposeTemplateRepo()
|
composeRepo = repo.NewIComposeTemplateRepo()
|
||||||
@@ -38,12 +40,14 @@ var (
|
|||||||
settingRepo = repo.NewISettingRepo()
|
settingRepo = repo.NewISettingRepo()
|
||||||
backupRepo = repo.NewIBackupRepo()
|
backupRepo = repo.NewIBackupRepo()
|
||||||
|
|
||||||
websiteRepo = repo.NewIWebsiteRepo()
|
websiteRepo = repo.NewIWebsiteRepo()
|
||||||
websiteDomainRepo = repo.NewIWebsiteDomainRepo()
|
websiteDomainRepo = repo.NewIWebsiteDomainRepo()
|
||||||
websiteDnsRepo = repo.NewIWebsiteDnsAccountRepo()
|
websiteDnsRepo = repo.NewIWebsiteDnsAccountRepo()
|
||||||
websiteSSLRepo = repo.NewISSLRepo()
|
websiteSSLRepo = repo.NewISSLRepo()
|
||||||
websiteAcmeRepo = repo.NewIAcmeAccountRepo()
|
websiteAcmeRepo = repo.NewIAcmeAccountRepo()
|
||||||
websiteCARepo = repo.NewIWebsiteCARepo()
|
websiteCARepo = repo.NewIWebsiteCARepo()
|
||||||
|
websiteTemplateRepo = repo.NewIWebsiteTemplateRepo()
|
||||||
|
websiteTemplateOutputRepo = repo.NewIWebsiteTemplateOutputRepo()
|
||||||
|
|
||||||
snapshotRepo = repo.NewISnapshotRepo()
|
snapshotRepo = repo.NewISnapshotRepo()
|
||||||
|
|
||||||
|
|||||||
+65
-11
@@ -16,6 +16,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@@ -466,7 +467,7 @@ func (f *FileService) Compress(c request.FileCompress) error {
|
|||||||
|
|
||||||
func preflightCompressTool(compressType files.CompressType) error {
|
func preflightCompressTool(compressType files.CompressType) error {
|
||||||
switch compressType {
|
switch compressType {
|
||||||
case files.TarGz, files.Rar, files.X7z:
|
case files.Tar, files.Gz, files.Bz2, files.TarBz2, files.Tgz, files.TarGz, files.Xz, files.TarXz, files.Rar, files.X7z:
|
||||||
_, err := files.NewShellArchiver(compressType)
|
_, err := files.NewShellArchiver(compressType)
|
||||||
return err
|
return err
|
||||||
default:
|
default:
|
||||||
@@ -476,7 +477,7 @@ func preflightCompressTool(compressType files.CompressType) error {
|
|||||||
|
|
||||||
func preflightDecompressTool(decompressType files.CompressType) error {
|
func preflightDecompressTool(decompressType files.CompressType) error {
|
||||||
switch decompressType {
|
switch decompressType {
|
||||||
case files.Rar, files.X7z:
|
case files.Rar:
|
||||||
_, err := files.NewExtractShellArchiver(decompressType)
|
_, err := files.NewExtractShellArchiver(decompressType)
|
||||||
return err
|
return err
|
||||||
default:
|
default:
|
||||||
@@ -485,7 +486,7 @@ func preflightDecompressTool(decompressType files.CompressType) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileService) StopCompress(taskID string) error {
|
func (f *FileService) StopCompress(taskID string) error {
|
||||||
if cancel, ok := global.TaskCtxMap[taskID]; ok {
|
if cancel, ok := global.LoadTaskCancel(taskID); ok {
|
||||||
cancel()
|
cancel()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -493,7 +494,7 @@ func (f *FileService) StopCompress(taskID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileService) StopDeCompress(taskID string) error {
|
func (f *FileService) StopDeCompress(taskID string) error {
|
||||||
if cancel, ok := global.TaskCtxMap[taskID]; ok {
|
if cancel, ok := global.LoadTaskCancel(taskID); ok {
|
||||||
cancel()
|
cancel()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -533,7 +534,10 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
|
|||||||
_ = os.RemoveAll(c.Dst)
|
_ = os.RemoveAll(c.Dst)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if err := fo.Decompress(t.TaskCtx, c.Path, tempDst, files.CompressType(c.Type), c.Secret); err != nil {
|
if err := fo.DecompressWithOptions(t.TaskCtx, c.Path, tempDst, files.CompressType(c.Type), c.Secret, files.DecompressOptions{
|
||||||
|
PreserveOwner: true,
|
||||||
|
AllowCLIReextract: true,
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := fo.CreateDir(c.Dst, constant.DirPerm); err != nil {
|
if err := fo.CreateDir(c.Dst, constant.DirPerm); err != nil {
|
||||||
@@ -551,19 +555,42 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyDecompressTree(ctx context.Context, srcDir, dstDir string) error {
|
func copyDecompressTree(ctx context.Context, srcDir, dstDir string) error {
|
||||||
|
state := decompressCopyState{hardlinks: make(map[decompressFileIdentity]string)}
|
||||||
entries, err := os.ReadDir(srcDir)
|
entries, err := os.ReadDir(srcDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if err := copyDecompressEntry(ctx, filepath.Join(srcDir, entry.Name()), filepath.Join(dstDir, entry.Name())); err != nil {
|
if err := copyDecompressEntryWithState(ctx, filepath.Join(srcDir, entry.Name()), filepath.Join(dstDir, entry.Name()), &state); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type decompressFileIdentity struct {
|
||||||
|
device uint64
|
||||||
|
inode uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type decompressCopyState struct {
|
||||||
|
hardlinks map[decompressFileIdentity]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func decompressHardlinkIdentity(info os.FileInfo) (decompressFileIdentity, bool) {
|
||||||
|
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||||
|
if !ok || stat.Nlink < 2 {
|
||||||
|
return decompressFileIdentity{}, false
|
||||||
|
}
|
||||||
|
return decompressFileIdentity{device: uint64(stat.Dev), inode: uint64(stat.Ino)}, true
|
||||||
|
}
|
||||||
|
|
||||||
func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr error) {
|
func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr error) {
|
||||||
|
state := decompressCopyState{hardlinks: make(map[decompressFileIdentity]string)}
|
||||||
|
return copyDecompressEntryWithState(ctx, srcPath, dstPath, &state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyDecompressEntryWithState(ctx context.Context, srcPath, dstPath string, state *decompressCopyState) (retErr error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -605,13 +632,16 @@ func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr e
|
|||||||
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
|
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := os.Chmod(dstPath, info.Mode().Perm()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
entries, err := os.ReadDir(srcPath)
|
entries, err := os.ReadDir(srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if err := copyDecompressEntry(ctx, filepath.Join(srcPath, entry.Name()), filepath.Join(dstPath, entry.Name())); err != nil {
|
if err := copyDecompressEntryWithState(ctx, filepath.Join(srcPath, entry.Name()), filepath.Join(dstPath, entry.Name()), state); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -621,12 +651,28 @@ func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr e
|
|||||||
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
|
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.RemoveAll(dstPath); err != nil {
|
dstInfo, err := os.Lstat(dstPath)
|
||||||
|
keepExistingFile := err == nil && dstInfo.Mode().IsRegular()
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !keepExistingFile {
|
||||||
|
if err := os.RemoveAll(dstPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
|
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
identity, isHardlink := decompressHardlinkIdentity(info)
|
||||||
|
if !keepExistingFile && isHardlink {
|
||||||
|
if existingPath, ok := state.hardlinks[identity]; ok {
|
||||||
|
if err := os.Link(existingPath, dstPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
srcFile, err := os.Open(srcPath)
|
srcFile, err := os.Open(srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -647,8 +693,16 @@ func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr e
|
|||||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
|
if !keepExistingFile {
|
||||||
return err
|
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.Chmod(dstPath, info.Mode().Perm()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isHardlink {
|
||||||
|
state.hardlinks[identity] = dstPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
|
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
|
||||||
}
|
}
|
||||||
@@ -658,7 +712,7 @@ func applyDecompressOwnership(srcPath, dstPath string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
stat, ok := info.Sys().(*unix.Stat_t)
|
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
@@ -28,7 +26,6 @@ type IFirewallService interface {
|
|||||||
SearchWithPage(search dto.RuleSearch) (int64, interface{}, error)
|
SearchWithPage(search dto.RuleSearch) (int64, interface{}, error)
|
||||||
OperateFirewall(req dto.FirewallOperation) error
|
OperateFirewall(req dto.FirewallOperation) error
|
||||||
OperatePortRule(req dto.PortRuleOperate, reload bool) error
|
OperatePortRule(req dto.PortRuleOperate, reload bool) error
|
||||||
OperateForwardRule(req dto.ForwardRuleOperate) error
|
|
||||||
OperateAddressRule(req dto.AddrRuleOperate, reload bool) error
|
OperateAddressRule(req dto.AddrRuleOperate, reload bool) error
|
||||||
UpdatePortRule(req dto.PortRuleUpdate) error
|
UpdatePortRule(req dto.PortRuleUpdate) error
|
||||||
UpdateAddrRule(req dto.AddrRuleUpdate) error
|
UpdateAddrRule(req dto.AddrRuleUpdate) error
|
||||||
@@ -84,8 +81,6 @@ func (u *FirewallService) SearchWithPage(req dto.RuleSearch) (int64, interface{}
|
|||||||
switch req.Type {
|
switch req.Type {
|
||||||
case "port":
|
case "port":
|
||||||
rules, err = client.ListPort()
|
rules, err = client.ListPort()
|
||||||
case "forward":
|
|
||||||
rules, err = client.ListForward()
|
|
||||||
case "address":
|
case "address":
|
||||||
rules, err = client.ListAddress()
|
rules, err = client.ListAddress()
|
||||||
}
|
}
|
||||||
@@ -166,6 +161,12 @@ func (u *FirewallService) OperateFirewall(req dto.FirewallOperation) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
fail2BanState := newFirewallFail2BanState()
|
||||||
|
if client.Name() == "firewalld" && req.Operation == "stop" {
|
||||||
|
if err := fail2BanState.rememberBeforeFirewallStop(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
needRestartDocker := false
|
needRestartDocker := false
|
||||||
switch req.Operation {
|
switch req.Operation {
|
||||||
case "start":
|
case "start":
|
||||||
@@ -210,6 +211,11 @@ func (u *FirewallService) OperateFirewall(req dto.FirewallOperation) error {
|
|||||||
return fmt.Errorf("failed to restart Docker: %v", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,96 +312,6 @@ func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *FirewallService) OperateForwardRule(req dto.ForwardRuleOperate) error {
|
|
||||||
client, err := firewall.NewFirewallClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
rules, _ := client.ListForward()
|
|
||||||
i := 0
|
|
||||||
for _, rule := range rules {
|
|
||||||
shouldKeep := true
|
|
||||||
for i := range req.Rules {
|
|
||||||
reqRule := &req.Rules[i]
|
|
||||||
if reqRule.TargetIP == "" {
|
|
||||||
reqRule.TargetIP = "127.0.0.1"
|
|
||||||
}
|
|
||||||
|
|
||||||
if reqRule.Operation == "remove" {
|
|
||||||
for _, proto := range strings.Split(reqRule.Protocol, "/") {
|
|
||||||
if reqRule.Port == rule.Port &&
|
|
||||||
reqRule.TargetPort == rule.TargetPort &&
|
|
||||||
reqRule.TargetIP == rule.TargetIP &&
|
|
||||||
proto == rule.Protocol &&
|
|
||||||
reqRule.Interface == rule.Interface {
|
|
||||||
shouldKeep = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if shouldKeep {
|
|
||||||
rules[i] = rule
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rules = rules[:i]
|
|
||||||
|
|
||||||
for _, rule := range rules {
|
|
||||||
for _, reqRule := range req.Rules {
|
|
||||||
if reqRule.Operation == "remove" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, proto := range strings.Split(reqRule.Protocol, "/") {
|
|
||||||
if reqRule.Port == rule.Port &&
|
|
||||||
reqRule.TargetPort == rule.TargetPort &&
|
|
||||||
reqRule.TargetIP == rule.TargetIP &&
|
|
||||||
proto == rule.Protocol &&
|
|
||||||
reqRule.Interface == rule.Interface {
|
|
||||||
return buserr.New("ErrRecordExist")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.SliceStable(req.Rules, func(i, j int) bool {
|
|
||||||
if req.Rules[i].Operation == "remove" && req.Rules[j].Operation != "remove" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if req.Rules[i].Operation != "remove" && req.Rules[j].Operation == "remove" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
n1, _ := strconv.Atoi(req.Rules[i].Num)
|
|
||||||
n2, _ := strconv.Atoi(req.Rules[j].Num)
|
|
||||||
return n1 > n2
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, r := range req.Rules {
|
|
||||||
for _, p := range strings.Split(r.Protocol, "/") {
|
|
||||||
if r.TargetIP == "" {
|
|
||||||
r.TargetIP = "127.0.0.1"
|
|
||||||
}
|
|
||||||
if err = client.PortForward(fireClient.Forward{
|
|
||||||
Num: r.Num,
|
|
||||||
Protocol: p,
|
|
||||||
Port: r.Port,
|
|
||||||
TargetIP: r.TargetIP,
|
|
||||||
TargetPort: r.TargetPort,
|
|
||||||
Interface: r.Interface,
|
|
||||||
}, r.Operation); err != nil {
|
|
||||||
if req.ForceDelete {
|
|
||||||
global.LOG.Error(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *FirewallService) OperateAddressRule(req dto.AddrRuleOperate, reload bool) error {
|
func (u *FirewallService) OperateAddressRule(req dto.AddrRuleOperate, reload bool) error {
|
||||||
client, err := firewall.NewFirewallClient()
|
client, err := firewall.NewFirewallClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -510,7 +426,7 @@ func OperateFirewallPort(oldPorts, newPorts []int) error {
|
|||||||
return client.Reload()
|
return client.Reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *FirewallService) operatePort(client firewall.FirewallClient, req dto.PortRuleOperate) error {
|
func (u *FirewallService) operatePort(client firewall.FilterClient, req dto.PortRuleOperate) error {
|
||||||
var fireInfo fireClient.FireInfo
|
var fireInfo fireClient.FireInfo
|
||||||
if err := copier.Copy(&fireInfo, &req); err != nil {
|
if err := copier.Copy(&fireInfo, &req); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -578,7 +494,7 @@ func (u *FirewallService) loadPortByApp() []portOfApp {
|
|||||||
return datas
|
return datas
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *FirewallService) cleanUnUsedData(client firewall.FirewallClient) {
|
func (u *FirewallService) cleanUnUsedData(client firewall.FilterClient) {
|
||||||
list, _ := client.ListPort()
|
list, _ := client.ListPort()
|
||||||
addressList, _ := client.ListAddress()
|
addressList, _ := client.ListAddress()
|
||||||
list = append(list, addressList...)
|
list = append(list, addressList...)
|
||||||
@@ -602,7 +518,7 @@ func (u *FirewallService) cleanUnUsedData(client firewall.FirewallClient) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *FirewallService) addPortsBeforeStart(client firewall.FirewallClient) error {
|
func (u *FirewallService) addPortsBeforeStart(client firewall.FilterClient) error {
|
||||||
if client.Name() == "iptables" {
|
if client.Name() == "iptables" {
|
||||||
isInit, _ := iptables.LoadInitStatus("iptables", "base")
|
isInit, _ := iptables.LoadInitStatus("iptables", "base")
|
||||||
if !isInit {
|
if !isInit {
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
||||||
|
)
|
||||||
|
|
||||||
|
const fail2BanRestoreWithFirewallMarker = "/run/1panel_fail2ban_restore_with_firewall"
|
||||||
|
|
||||||
|
type firewallFail2BanState struct {
|
||||||
|
markerPath string
|
||||||
|
isExist func(string) bool
|
||||||
|
isActive func(string) bool
|
||||||
|
start func(string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFirewallFail2BanState() *firewallFail2BanState {
|
||||||
|
return &firewallFail2BanState{
|
||||||
|
markerPath: fail2BanRestoreWithFirewallMarker,
|
||||||
|
isExist: func(serviceName string) bool {
|
||||||
|
exists, err := controller.CheckExist(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Warnf("check %s installation before stopping the firewall failed: %v", serviceName, err)
|
||||||
|
}
|
||||||
|
return exists
|
||||||
|
},
|
||||||
|
isActive: func(serviceName string) bool {
|
||||||
|
active, err := controller.CheckActive(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Warnf("check %s status before stopping the firewall failed: %v", serviceName, err)
|
||||||
|
}
|
||||||
|
return active
|
||||||
|
},
|
||||||
|
start: controller.HandleStart,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *firewallFail2BanState) rememberBeforeFirewallStop() error {
|
||||||
|
if !s.isExist("fail2ban.service") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !s.isActive("fail2ban.service") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.markForRestore()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *firewallFail2BanState) markForRestore() error {
|
||||||
|
if err := os.WriteFile(s.markerPath, nil, 0600); err != nil {
|
||||||
|
return fmt.Errorf("mark Fail2Ban for restoration with the firewall: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *firewallFail2BanState) restoreAfterFirewallStart() error {
|
||||||
|
_, err := os.Stat(s.markerPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("load Fail2Ban restore marker after starting the firewall: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.start("fail2ban.service"); err != nil {
|
||||||
|
return fmt.Errorf("restore Fail2Ban after starting the firewall: %w", err)
|
||||||
|
}
|
||||||
|
return s.clearRestoreMarker()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *firewallFail2BanState) clearRestoreMarker() error {
|
||||||
|
if err := os.Remove(s.markerPath); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("clear Fail2Ban firewall restore status: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -138,7 +138,7 @@ func syncFirewallPortWhiteListAfterUpdate(oldValue string) error {
|
|||||||
return syncFirewallClientPortWhiteList(client, oldPortWhiteList, portWhiteList)
|
return syncFirewallClientPortWhiteList(client, oldPortWhiteList, portWhiteList)
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncFirewallClientPortWhiteList(client firewall.FirewallClient, oldPortWhiteList, portWhiteList []firewallPortWhitelist) error {
|
func syncFirewallClientPortWhiteList(client firewall.FilterClient, oldPortWhiteList, portWhiteList []firewallPortWhitelist) error {
|
||||||
oldPorts := firewallPortWhiteListMap(oldPortWhiteList)
|
oldPorts := firewallPortWhiteListMap(oldPortWhiteList)
|
||||||
newPorts := firewallPortWhiteListMap(portWhiteList)
|
newPorts := firewallPortWhiteListMap(portWhiteList)
|
||||||
for _, item := range oldPortWhiteList {
|
for _, item := range oldPortWhiteList {
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||||
|
forwardClient "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IForwardingService interface {
|
||||||
|
LoadBaseInfo() (dto.FirewallBaseInfo, error)
|
||||||
|
SearchWithPage(search dto.ForwardRuleSearch) (int64, interface{}, error)
|
||||||
|
Operate(req dto.ForwardRuleOperate) error
|
||||||
|
Enable() error
|
||||||
|
Replay() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardingService struct {
|
||||||
|
adapterFactory func() (forwardClient.Adapter, error)
|
||||||
|
filterFactory func() (firewall.FilterClient, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIForwardingService() IForwardingService {
|
||||||
|
return &ForwardingService{
|
||||||
|
adapterFactory: newForwardingAdapter,
|
||||||
|
filterFactory: firewall.NewFirewallClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newForwardingAdapter() (forwardClient.Adapter, error) {
|
||||||
|
client, err := firewall.NewFirewallClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return forwardClient.NewAdapter(client.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) LoadBaseInfo() (dto.FirewallBaseInfo, error) {
|
||||||
|
baseInfo := dto.FirewallBaseInfo{Version: "-", Name: "-"}
|
||||||
|
adapter, err := s.adapterFactory()
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("load forwarding failed, err: %v", err)
|
||||||
|
return baseInfo, nil
|
||||||
|
}
|
||||||
|
filter, err := s.filterFactory()
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("load firewall status failed, err: %v", err)
|
||||||
|
return baseInfo, nil
|
||||||
|
}
|
||||||
|
baseInfo.IsExist = true
|
||||||
|
baseInfo.Name = adapter.Name()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
baseInfo.PingStatus = firewall.LoadPingStatus()
|
||||||
|
baseInfo.Version, _ = filter.Version()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
baseInfo.IsActive, _ = filter.Status()
|
||||||
|
baseInfo.IsInit, baseInfo.IsBind = adapter.InitStatus()
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
return baseInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) SearchWithPage(req dto.ForwardRuleSearch) (int64, interface{}, error) {
|
||||||
|
adapter, err := s.adapterFactory()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
rules, err := adapter.List()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
if req.Strategy != "" {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var filtered []forwardClient.Rule
|
||||||
|
for _, rule := range rules {
|
||||||
|
if req.Info != "" && !strings.Contains(rule.Port, req.Info) &&
|
||||||
|
!strings.Contains(rule.TargetPort, req.Info) && !strings.Contains(rule.TargetIP, req.Info) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, rule)
|
||||||
|
}
|
||||||
|
total := len(filtered)
|
||||||
|
start, end := (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||||
|
if start > total {
|
||||||
|
return int64(total), make([]dto.ForwardRule, 0), nil
|
||||||
|
}
|
||||||
|
if end > total {
|
||||||
|
end = total
|
||||||
|
}
|
||||||
|
pageRules := filtered[start:end]
|
||||||
|
var items []dto.ForwardRule
|
||||||
|
if pageRules != nil {
|
||||||
|
items = make([]dto.ForwardRule, 0, len(pageRules))
|
||||||
|
}
|
||||||
|
for _, rule := range pageRules {
|
||||||
|
items = append(items, dto.ForwardRule{
|
||||||
|
Num: rule.Num,
|
||||||
|
Protocol: rule.Protocol,
|
||||||
|
Port: rule.Port,
|
||||||
|
TargetIP: rule.TargetIP,
|
||||||
|
TargetPort: rule.TargetPort,
|
||||||
|
Interface: rule.Interface,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return int64(total), items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) Operate(req dto.ForwardRuleOperate) error {
|
||||||
|
adapter, err := s.adapterFactory()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rules, _ := adapter.List()
|
||||||
|
kept := rules[:0]
|
||||||
|
for _, rule := range rules {
|
||||||
|
shouldKeep := true
|
||||||
|
for i := range req.Rules {
|
||||||
|
reqRule := &req.Rules[i]
|
||||||
|
if reqRule.TargetIP == "" {
|
||||||
|
reqRule.TargetIP = "127.0.0.1"
|
||||||
|
}
|
||||||
|
if reqRule.Operation == "remove" && requestMatchesForwardRule(*reqRule, rule) {
|
||||||
|
shouldKeep = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if shouldKeep {
|
||||||
|
kept = append(kept, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rule := range kept {
|
||||||
|
for _, reqRule := range req.Rules {
|
||||||
|
if reqRule.Operation != "remove" && requestMatchesForwardRule(reqRule, rule) {
|
||||||
|
return buserr.New("ErrRecordExist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.SliceStable(req.Rules, func(i, j int) bool {
|
||||||
|
if req.Rules[i].Operation == "remove" && req.Rules[j].Operation != "remove" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if req.Rules[i].Operation != "remove" && req.Rules[j].Operation == "remove" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n1, _ := strconv.Atoi(req.Rules[i].Num)
|
||||||
|
n2, _ := strconv.Atoi(req.Rules[j].Num)
|
||||||
|
return n1 > n2
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, rule := range req.Rules {
|
||||||
|
for _, protocol := range strings.Split(rule.Protocol, "/") {
|
||||||
|
targetIP := rule.TargetIP
|
||||||
|
if targetIP == "" {
|
||||||
|
targetIP = "127.0.0.1"
|
||||||
|
}
|
||||||
|
err := adapter.Operate(forwardClient.Rule{
|
||||||
|
Num: rule.Num,
|
||||||
|
Protocol: protocol,
|
||||||
|
Port: rule.Port,
|
||||||
|
TargetIP: targetIP,
|
||||||
|
TargetPort: rule.TargetPort,
|
||||||
|
Interface: rule.Interface,
|
||||||
|
}, rule.Operation)
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if req.ForceDelete {
|
||||||
|
global.LOG.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestMatchesForwardRule(req dto.ForwardRuleOperation, rule forwardClient.Rule) bool {
|
||||||
|
for _, protocol := range strings.Split(req.Protocol, "/") {
|
||||||
|
if req.Port == rule.Port && req.TargetPort == rule.TargetPort && req.TargetIP == rule.TargetIP &&
|
||||||
|
protocol == rule.Protocol && req.Interface == rule.Interface {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) Enable() error {
|
||||||
|
adapter, err := s.adapterFactory()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := adapter.Enable(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if adapter.Name() != "firewalld" {
|
||||||
|
_ = settingRepo.Update("IptablesForwardStatus", constant.StatusEnable)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) Replay() error {
|
||||||
|
adapter, err := s.adapterFactory()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := adapter.Replay(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if adapter.Name() == "firewalld" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
status, _ := settingRepo.GetValueByKey("IptablesForwardStatus")
|
||||||
|
if status == constant.StatusEnable {
|
||||||
|
return adapter.Enable()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||||
|
forwardClient "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
type forwardingCall struct {
|
||||||
|
rule forwardClient.Rule
|
||||||
|
operation string
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeForwardingAdapter struct {
|
||||||
|
name string
|
||||||
|
rules []forwardClient.Rule
|
||||||
|
listErr error
|
||||||
|
operateErr error
|
||||||
|
calls []forwardingCall
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeForwardingAdapter) Name() string { return f.name }
|
||||||
|
|
||||||
|
func (f *fakeForwardingAdapter) List() ([]forwardClient.Rule, error) {
|
||||||
|
return append([]forwardClient.Rule(nil), f.rules...), f.listErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeForwardingAdapter) Operate(rule forwardClient.Rule, operation string) error {
|
||||||
|
f.calls = append(f.calls, forwardingCall{rule: rule, operation: operation})
|
||||||
|
return f.operateErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeForwardingAdapter) Enable() error { return nil }
|
||||||
|
func (f *fakeForwardingAdapter) InitStatus() (bool, bool) { return true, true }
|
||||||
|
func (f *fakeForwardingAdapter) Replay() error { return nil }
|
||||||
|
|
||||||
|
func forwardingServiceWithAdapter(adapter forwardClient.Adapter) *ForwardingService {
|
||||||
|
return &ForwardingService{
|
||||||
|
adapterFactory: func() (forwardClient.Adapter, error) { return adapter, nil },
|
||||||
|
filterFactory: firewall.NewFirewallClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardingAndFilterInterfacesAreSeparated(t *testing.T) {
|
||||||
|
filterType := reflect.TypeOf((*firewall.FilterClient)(nil)).Elem()
|
||||||
|
for _, method := range []string{"ListForward", "PortForward", "EnableForward"} {
|
||||||
|
if _, ok := filterType.MethodByName(method); ok {
|
||||||
|
t.Fatalf("filter interface still exposes %s", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
firewallServiceType := reflect.TypeOf((*IFirewallService)(nil)).Elem()
|
||||||
|
if _, ok := firewallServiceType.MethodByName("OperateForwardRule"); ok {
|
||||||
|
t.Fatal("firewall service still owns forwarding writes")
|
||||||
|
}
|
||||||
|
forwardingServiceType := reflect.TypeOf((*IForwardingService)(nil)).Elem()
|
||||||
|
for _, method := range []string{"LoadBaseInfo", "SearchWithPage", "Operate", "Enable", "Replay"} {
|
||||||
|
if _, ok := forwardingServiceType.MethodByName(method); !ok {
|
||||||
|
t.Fatalf("forwarding service missing %s", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardingInitRequestContract(t *testing.T) {
|
||||||
|
req := dto.IptablesOp{Name: "1PANEL_FORWARD", Operate: "init-forward"}
|
||||||
|
if err := validator.New().Struct(req); err != nil {
|
||||||
|
t.Fatalf("frontend forwarding initialization request must remain valid: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardingSearchPreservesAPIShapeAndPagination(t *testing.T) {
|
||||||
|
adapter := &fakeForwardingAdapter{name: "iptables", rules: []forwardClient.Rule{
|
||||||
|
{Num: "1", Protocol: "tcp", Port: "8080", TargetIP: "10.0.0.2", TargetPort: "80", Interface: "eth0"},
|
||||||
|
{Num: "2", Protocol: "udp", Port: "5353", TargetIP: "127.0.0.1", TargetPort: "53"},
|
||||||
|
}}
|
||||||
|
service := forwardingServiceWithAdapter(adapter)
|
||||||
|
total, value, err := service.SearchWithPage(dto.ForwardRuleSearch{PageInfo: dto.PageInfo{Page: 1, PageSize: 10}, Info: "10.0.0.2"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if total != 1 {
|
||||||
|
t.Fatalf("got total %d want 1", total)
|
||||||
|
}
|
||||||
|
items, ok := value.([]dto.ForwardRule)
|
||||||
|
if !ok || len(items) != 1 || items[0].Port != "8080" {
|
||||||
|
t.Fatalf("unexpected items: %#v", value)
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(items[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var fields map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &fields); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wantFields := []string{"id", "chain", "family", "address", "port", "protocol", "strategy", "num", "targetIP", "targetPort", "interface", "usedStatus", "description"}
|
||||||
|
for _, field := range wantFields {
|
||||||
|
if _, ok := fields[field]; !ok {
|
||||||
|
t.Fatalf("forward response dropped compatibility field %q: %s", field, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardingOperatePreservesDuplicateAndOrderingContracts(t *testing.T) {
|
||||||
|
existing := &fakeForwardingAdapter{name: "ufw", rules: []forwardClient.Rule{
|
||||||
|
{Protocol: "tcp", Port: "8080", TargetIP: "127.0.0.1", TargetPort: "80"},
|
||||||
|
}}
|
||||||
|
service := forwardingServiceWithAdapter(existing)
|
||||||
|
err := service.Operate(dto.ForwardRuleOperate{Rules: []dto.ForwardRuleOperation{{
|
||||||
|
Operation: "add", Protocol: "tcp", Port: "8080", TargetPort: "80",
|
||||||
|
}}})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("duplicate forwarding rule must be rejected")
|
||||||
|
}
|
||||||
|
if len(existing.calls) != 0 {
|
||||||
|
t.Fatalf("duplicate check wrote forwarding state: %#v", existing.calls)
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter := &fakeForwardingAdapter{name: "iptables"}
|
||||||
|
service = forwardingServiceWithAdapter(adapter)
|
||||||
|
err = service.Operate(dto.ForwardRuleOperate{Rules: []dto.ForwardRuleOperation{
|
||||||
|
{Operation: "add", Protocol: "tcp/udp", Port: "9000", TargetIP: "10.0.0.2", TargetPort: "90"},
|
||||||
|
{Operation: "remove", Num: "1", Protocol: "tcp", Port: "8001", TargetIP: "10.0.0.2", TargetPort: "81"},
|
||||||
|
{Operation: "remove", Num: "3", Protocol: "tcp", Port: "8003", TargetIP: "10.0.0.2", TargetPort: "83"},
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := []forwardingCall{
|
||||||
|
{operation: "remove", rule: forwardClient.Rule{Num: "3", Protocol: "tcp", Port: "8003", TargetIP: "10.0.0.2", TargetPort: "83"}},
|
||||||
|
{operation: "remove", rule: forwardClient.Rule{Num: "1", Protocol: "tcp", Port: "8001", TargetIP: "10.0.0.2", TargetPort: "81"}},
|
||||||
|
{operation: "add", rule: forwardClient.Rule{Protocol: "tcp", Port: "9000", TargetIP: "10.0.0.2", TargetPort: "90"}},
|
||||||
|
{operation: "add", rule: forwardClient.Rule{Protocol: "udp", Port: "9000", TargetIP: "10.0.0.2", TargetPort: "90"}},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(adapter.calls, want) {
|
||||||
|
t.Fatalf("operation order changed\ngot %#v\nwant %#v", adapter.calls, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardingSearchReturnsAdapterError(t *testing.T) {
|
||||||
|
wantErr := errors.New("list failed")
|
||||||
|
service := forwardingServiceWithAdapter(&fakeForwardingAdapter{name: "firewalld", listErr: wantErr})
|
||||||
|
_, _, err := service.SearchWithPage(dto.ForwardRuleSearch{PageInfo: dto.PageInfo{Page: 1, PageSize: 20}})
|
||||||
|
if !errors.Is(err, wantErr) {
|
||||||
|
t.Fatalf("got %v want %v", err, wantErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
-21
@@ -1,6 +1,8 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ type IFtpService interface {
|
|||||||
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
|
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
|
||||||
Operate(operation string) error
|
Operate(operation string) error
|
||||||
Create(req dto.FtpCreate) (uint, error)
|
Create(req dto.FtpCreate) (uint, error)
|
||||||
|
CreateWebsite(req dto.FtpCreate) (uint, error)
|
||||||
Delete(req dto.BatchDeleteReq) error
|
Delete(req dto.BatchDeleteReq) error
|
||||||
Update(req dto.FtpUpdate) error
|
Update(req dto.FtpUpdate) error
|
||||||
Sync() error
|
Sync() error
|
||||||
@@ -34,11 +37,7 @@ func NewIFtpService() IFtpService {
|
|||||||
|
|
||||||
func (f *FtpService) LoadBaseInfo() (dto.FtpBaseInfo, error) {
|
func (f *FtpService) LoadBaseInfo() (dto.FtpBaseInfo, error) {
|
||||||
var baseInfo dto.FtpBaseInfo
|
var baseInfo dto.FtpBaseInfo
|
||||||
client, err := toolbox.NewFtpClient()
|
baseInfo.IsActive, baseInfo.IsExist = toolbox.FtpStatus()
|
||||||
if err != nil {
|
|
||||||
return baseInfo, err
|
|
||||||
}
|
|
||||||
baseInfo.IsActive, baseInfo.IsExist = client.Status()
|
|
||||||
return baseInfo, nil
|
return baseInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +98,7 @@ func (f *FtpService) Sync() error {
|
|||||||
}
|
}
|
||||||
lists, err := client.LoadList()
|
lists, err := client.LoadList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
listsInDB, err := ftpRepo.GetList()
|
listsInDB, err := ftpRepo.GetList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -113,13 +112,24 @@ func (f *FtpService) Sync() error {
|
|||||||
for _, item := range lists {
|
for _, item := range lists {
|
||||||
if itemInDB, ok := currentData[item.User]; ok {
|
if itemInDB, ok := currentData[item.User]; ok {
|
||||||
sameData[item.User] = struct{}{}
|
sameData[item.User] = struct{}{}
|
||||||
if item.Path != itemInDB.Path || item.Status != itemInDB.Status {
|
if item.Path != itemInDB.Path || item.Status != itemInDB.Status || item.UID != itemInDB.UID || item.GID != itemInDB.GID {
|
||||||
if err := ftpRepo.Update(itemInDB.ID, map[string]interface{}{"path": item.Path, "status": item.Status}); err != nil {
|
if err := ftpRepo.Update(itemInDB.ID, map[string]interface{}{
|
||||||
|
"path": item.Path,
|
||||||
|
"status": item.Status,
|
||||||
|
"uid": item.UID,
|
||||||
|
"gid": item.GID,
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := ftpRepo.Create(&model.Ftp{User: item.User, Path: item.Path, Status: item.Status}); err != nil {
|
if err := ftpRepo.Create(&model.Ftp{
|
||||||
|
User: item.User,
|
||||||
|
Path: item.Path,
|
||||||
|
Status: item.Status,
|
||||||
|
UID: item.UID,
|
||||||
|
GID: item.GID,
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,6 +143,21 @@ func (f *FtpService) Sync() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FtpService) Create(req dto.FtpCreate) (uint, error) {
|
func (f *FtpService) Create(req dto.FtpCreate) (uint, error) {
|
||||||
|
return f.create(req, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FtpService) CreateWebsite(req dto.FtpCreate) (uint, error) {
|
||||||
|
return f.create(req, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FtpService) create(req dto.FtpCreate, website bool) (uint, error) {
|
||||||
|
if err := toolbox.ValidateFtpRootPath(req.Path); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
client, err := toolbox.NewFtpClient()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
if _, err := os.Stat(req.Path); err != nil {
|
if _, err := os.Stat(req.Path); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(req.Path, os.ModePerm); err != nil {
|
if err := os.MkdirAll(req.Path, os.ModePerm); err != nil {
|
||||||
@@ -150,20 +175,28 @@ func (f *FtpService) Create(req dto.FtpCreate) (uint, error) {
|
|||||||
if userInDB.ID != 0 {
|
if userInDB.ID != 0 {
|
||||||
return 0, buserr.New("ErrRecordExist")
|
return 0, buserr.New("ErrRecordExist")
|
||||||
}
|
}
|
||||||
client, err := toolbox.NewFtpClient()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if err := client.UserAdd(req.User, req.Password, req.Path); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
var ftp model.Ftp
|
var ftp model.Ftp
|
||||||
if err := copier.Copy(&ftp, &req); err != nil {
|
if err := copier.Copy(&ftp, &req); err != nil {
|
||||||
return 0, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
return 0, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
}
|
}
|
||||||
|
uid, gid := uint(constant.WebsiteUID), uint(constant.WebsiteGID)
|
||||||
|
if !website {
|
||||||
|
uid, gid, err = toolbox.EnsureStandaloneFtpIdentity()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := client.UserAdd(req.User, req.Password, req.Path, uid, gid); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
ftp.Status = constant.StatusEnable
|
ftp.Status = constant.StatusEnable
|
||||||
ftp.Password = pass
|
ftp.Password = pass
|
||||||
|
ftp.UID = uid
|
||||||
|
ftp.GID = gid
|
||||||
if err := ftpRepo.Create(&ftp); err != nil {
|
if err := ftpRepo.Create(&ftp); err != nil {
|
||||||
|
if rollbackErr := client.UserDel(req.User); rollbackErr != nil {
|
||||||
|
return 0, errors.Join(err, fmt.Errorf("rollback FTP user %s failed: %w", req.User, rollbackErr))
|
||||||
|
}
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return ftp.ID, nil
|
return ftp.ID, nil
|
||||||
@@ -186,6 +219,13 @@ func (f *FtpService) Delete(req dto.BatchDeleteReq) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FtpService) Update(req dto.FtpUpdate) error {
|
func (f *FtpService) Update(req dto.FtpUpdate) error {
|
||||||
|
if err := toolbox.ValidateFtpRootPath(req.Path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client, err := toolbox.NewFtpClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if _, err := os.Stat(req.Path); err != nil {
|
if _, err := os.Stat(req.Path); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(req.Path, os.ModePerm); err != nil {
|
if err := os.MkdirAll(req.Path, os.ModePerm); err != nil {
|
||||||
@@ -209,10 +249,6 @@ func (f *FtpService) Update(req dto.FtpUpdate) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := toolbox.NewFtpClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
needReload := false
|
needReload := false
|
||||||
updates := make(map[string]interface{})
|
updates := make(map[string]interface{})
|
||||||
if req.Password != passItem {
|
if req.Password != passItem {
|
||||||
@@ -230,7 +266,11 @@ func (f *FtpService) Update(req dto.FtpUpdate) error {
|
|||||||
needReload = true
|
needReload = true
|
||||||
}
|
}
|
||||||
if req.Path != ftpItem.Path {
|
if req.Path != ftpItem.Path {
|
||||||
if err := client.SetPath(ftpItem.User, req.Path); err != nil {
|
uid, gid := ftpItem.UID, ftpItem.GID
|
||||||
|
if uid == 0 || gid == 0 {
|
||||||
|
uid, gid = uint(constant.WebsiteUID), uint(constant.WebsiteGID)
|
||||||
|
}
|
||||||
|
if err := client.SetPath(ftpItem.User, req.Path, uid, gid); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
updates["path"] = req.Path
|
updates["path"] = req.Path
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client/iptables"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client/iptables"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -195,12 +194,6 @@ func (s *IptablesService) Operate(req dto.IptablesOp) error {
|
|||||||
}
|
}
|
||||||
_ = settingRepo.Update("IptablesStatus", constant.StatusEnable)
|
_ = settingRepo.Update("IptablesStatus", constant.StatusEnable)
|
||||||
return nil
|
return nil
|
||||||
case "init-forward":
|
|
||||||
if err := client.EnableIptablesForward(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = settingRepo.Update("IptablesForwardStatus", constant.StatusEnable)
|
|
||||||
return nil
|
|
||||||
case "init-advance":
|
case "init-advance":
|
||||||
if err := iptables.AddChain(iptables.FilterTab, iptables.Chain1PanelInput); err != nil {
|
if err := iptables.AddChain(iptables.FilterTab, iptables.Chain1PanelInput); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,18 +1,524 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LogService struct{}
|
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 {
|
type ILogService interface {
|
||||||
ListSystemLogFile() ([]string, error)
|
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)
|
||||||
|
output, err := executeJournalQuery(journalctl, queryArgs)
|
||||||
|
if err != nil {
|
||||||
|
return handleJournalQueryError(req, output, err, func() ([]byte, error) {
|
||||||
|
probeReq := req
|
||||||
|
probeReq.Keyword = ""
|
||||||
|
probeReq.Priority = ""
|
||||||
|
probeReq.Service = ""
|
||||||
|
probeArgs := buildJournalQueryArgs(probeReq, startTime, endTime, 1, cursor)
|
||||||
|
return executeJournalQuery(journalctl, probeArgs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
content := strings.TrimSpace(string(output))
|
||||||
|
if content == "" || strings.HasPrefix(content, "-- No entries --") {
|
||||||
|
return dto.SystemLogRes{Source: "journalctl", Items: []dto.SystemLogItem{}}, nil
|
||||||
|
}
|
||||||
|
items := trimJournalLogItemsToStartTime(parseJournalLogItems(content), startTime)
|
||||||
|
if cursor != nil && cursor.JournalCursor == "" {
|
||||||
|
items, err = skipSystemLogCursorItems(items, *cursor)
|
||||||
|
if err != nil {
|
||||||
|
return dto.SystemLogRes{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buildSystemLogResponse("journalctl", items, pageSize, cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeJournalQuery(journalctl string, queryArgs []string) ([]byte, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
return exec.CommandContext(ctx, journalctl, queryArgs...).CombinedOutput()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 handleJournalQueryError(
|
||||||
|
req dto.SystemLogReq,
|
||||||
|
output []byte,
|
||||||
|
queryErr error,
|
||||||
|
probe func() ([]byte, error),
|
||||||
|
) (dto.SystemLogRes, error) {
|
||||||
|
if hasSystemLogFilter(req) && probe != nil {
|
||||||
|
probeOutput, probeErr := probe()
|
||||||
|
if probeErr == nil {
|
||||||
|
return dto.SystemLogRes{Source: "journalctl", Items: []dto.SystemLogItem{}}, nil
|
||||||
|
}
|
||||||
|
return dto.SystemLogRes{}, newJournalQueryError(probeOutput, probeErr)
|
||||||
|
}
|
||||||
|
return dto.SystemLogRes{}, newJournalQueryError(output, queryErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newJournalQueryError(output []byte, queryErr error) error {
|
||||||
|
message := strings.TrimSpace(string(output))
|
||||||
|
if message == "" && queryErr != nil {
|
||||||
|
message = queryErr.Error()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("read host system logs failed: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimJournalLogItemsToStartTime(items []dto.SystemLogItem, startTime time.Time) []dto.SystemLogItem {
|
||||||
|
startTimestamp := startTime.UnixMicro()
|
||||||
|
for i, item := range items {
|
||||||
|
if item.Timestamp < startTimestamp {
|
||||||
|
return items[:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasSystemLogFilter(req dto.SystemLogReq) bool {
|
||||||
|
return strings.TrimSpace(req.Keyword) != "" ||
|
||||||
|
strings.TrimSpace(req.Priority) != "" ||
|
||||||
|
strings.TrimSpace(req.Service) != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
if cursor != nil && cursor.JournalCursor != "" {
|
||||||
|
args = append(args, "--after-cursor="+cursor.JournalCursor)
|
||||||
|
} else {
|
||||||
|
args = append(args,
|
||||||
|
"--since", formatJournalQueryTime(startTime),
|
||||||
|
"--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", regexp.QuoteMeta(keyword), "--case-sensitive=no")
|
||||||
|
}
|
||||||
|
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 {
|
func NewILogService() ILogService {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
|
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
|
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/mattn/go-shellwords"
|
||||||
"github.com/subosito/gotenv"
|
"github.com/subosito/gotenv"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
@@ -127,6 +128,7 @@ func (m McpServerService) Update(req request.McpServerUpdate) error {
|
|||||||
mcpServer.Type = req.Type
|
mcpServer.Type = req.Type
|
||||||
mcpServer.GatewayImage = req.GatewayImage
|
mcpServer.GatewayImage = req.GatewayImage
|
||||||
mcpServer.ProtocolVersion = req.ProtocolVersion
|
mcpServer.ProtocolVersion = req.ProtocolVersion
|
||||||
|
mcpServer.GatewayArgs = req.GatewayArgs
|
||||||
normalizeMcpServerGateway(mcpServer)
|
normalizeMcpServerGateway(mcpServer)
|
||||||
if req.OutputTransport == mcpOutputTransportSSE {
|
if req.OutputTransport == mcpOutputTransportSSE {
|
||||||
mcpServer.SsePath = req.SsePath
|
mcpServer.SsePath = req.SsePath
|
||||||
@@ -191,6 +193,7 @@ func (m McpServerService) Create(create request.McpServerCreate) error {
|
|||||||
Type: create.Type,
|
Type: create.Type,
|
||||||
GatewayImage: create.GatewayImage,
|
GatewayImage: create.GatewayImage,
|
||||||
ProtocolVersion: create.ProtocolVersion,
|
ProtocolVersion: create.ProtocolVersion,
|
||||||
|
GatewayArgs: create.GatewayArgs,
|
||||||
}
|
}
|
||||||
normalizeMcpServerGateway(mcpServer)
|
normalizeMcpServerGateway(mcpServer)
|
||||||
if create.OutputTransport == mcpOutputTransportSSE {
|
if create.OutputTransport == mcpOutputTransportSSE {
|
||||||
@@ -738,7 +741,52 @@ func normalizeMcpServerGateway(mcpServer *model.McpServer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSupergatewayCommand(mcpServer *model.McpServer) []string {
|
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)
|
normalizeMcpServerGateway(mcpServer)
|
||||||
command := []string{
|
command := []string{
|
||||||
"--stdio", mcpServer.Command,
|
"--stdio", mcpServer.Command,
|
||||||
@@ -746,14 +794,22 @@ func buildSupergatewayCommand(mcpServer *model.McpServer) []string {
|
|||||||
"--port", strconv.Itoa(mcpServer.Port),
|
"--port", strconv.Itoa(mcpServer.Port),
|
||||||
}
|
}
|
||||||
if mcpServer.OutputTransport == mcpOutputTransportSSE {
|
if mcpServer.OutputTransport == mcpOutputTransportSSE {
|
||||||
return append(command,
|
command = append(command,
|
||||||
"--baseUrl", mcpServer.BaseURL,
|
"--baseUrl", mcpServer.BaseURL,
|
||||||
"--ssePath", mcpServer.SsePath,
|
"--ssePath", mcpServer.SsePath,
|
||||||
"--messagePath", fmt.Sprintf("%s/messages", mcpServer.SsePath),
|
"--messagePath", fmt.Sprintf("%s/messages", mcpServer.SsePath),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
command = append(command,
|
||||||
|
"--streamableHttpPath", mcpServer.StreamableHttpPath,
|
||||||
|
"--protocolVersion", mcpServer.ProtocolVersion,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
command = append(command, "--streamableHttpPath", mcpServer.StreamableHttpPath)
|
gatewayArgs, err := parseMcpGatewayArgs(mcpServer.GatewayArgs)
|
||||||
return append(command, "--protocolVersion", mcpServer.ProtocolVersion)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return append(command, gatewayArgs...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildMcpEndpoint(mcpServer *model.McpServer) string {
|
func buildMcpEndpoint(mcpServer *model.McpServer) string {
|
||||||
@@ -852,7 +908,11 @@ func handleCreateParams(mcpServer *model.McpServer, environments []request.Envir
|
|||||||
delete(services, serviceName)
|
delete(services, serviceName)
|
||||||
}
|
}
|
||||||
normalizeMcpServerGateway(mcpServer)
|
normalizeMcpServerGateway(mcpServer)
|
||||||
serviceValue["command"] = buildSupergatewayCommand(mcpServer)
|
command, err := buildSupergatewayCommand(mcpServer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
serviceValue["command"] = command
|
||||||
serviceValue["image"] = mcpServer.GatewayImage
|
serviceValue["image"] = mcpServer.GatewayImage
|
||||||
serviceValue["ports"] = []string{
|
serviceValue["ports"] = []string{
|
||||||
formatComposePortMapping("HOST_IP", "PANEL_APP_PORT_HTTP", "PANEL_APP_PORT_HTTP", ""),
|
formatComposePortMapping("HOST_IP", "PANEL_APP_PORT_HTTP", "PANEL_APP_PORT_HTTP", ""),
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
@@ -34,7 +37,12 @@ type MonitorService struct {
|
|||||||
NetIO chan ([]net.IOCountersStat)
|
NetIO chan ([]net.IOCountersStat)
|
||||||
}
|
}
|
||||||
|
|
||||||
var monitorCancel context.CancelFunc
|
var (
|
||||||
|
monitorCancel context.CancelFunc
|
||||||
|
hostSysPath = loadHostSysPath()
|
||||||
|
|
||||||
|
blockDevicePartitionCache sync.Map
|
||||||
|
)
|
||||||
|
|
||||||
type IMonitorService interface {
|
type IMonitorService interface {
|
||||||
Run()
|
Run()
|
||||||
@@ -341,18 +349,10 @@ func (m *MonitorService) Run() {
|
|||||||
func (m *MonitorService) loadDiskIO() {
|
func (m *MonitorService) loadDiskIO() {
|
||||||
ioStat, _ := disk.IOCounters()
|
ioStat, _ := disk.IOCounters()
|
||||||
var diskIOList []disk.IOCountersStat
|
var diskIOList []disk.IOCountersStat
|
||||||
var ioStatAll disk.IOCountersStat
|
|
||||||
for _, io := range ioStat {
|
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, io)
|
||||||
}
|
}
|
||||||
diskIOList = append(diskIOList, ioStatAll)
|
diskIOList = append(diskIOList, sumDiskIOCounters(ioStat))
|
||||||
m.DiskIO <- diskIOList
|
m.DiskIO <- diskIOList
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,3 +680,45 @@ func loadGPUInfoFloat(val string) float64 {
|
|||||||
data, _ := strconv.ParseFloat(val, 64)
|
data, _ := strconv.ParseFloat(val, 64)
|
||||||
return data
|
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
|
||||||
|
}
|
||||||
|
|||||||
+151
-126
@@ -1,8 +1,6 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
|
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
|
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
|
||||||
@@ -17,7 +15,6 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
|
||||||
"github.com/subosito/gotenv"
|
"github.com/subosito/gotenv"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||||
@@ -57,7 +54,7 @@ func (n NginxService) GetNginxConfig() (*response.NginxFile, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
configPath := path.Join(global.Dir.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "nginx.conf")
|
configPath := path.Join(global.Dir.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, nginxModuleConfDir, "nginx.conf")
|
||||||
byteContent, err := files.NewFileOp().GetContent(configPath)
|
byteContent, err := files.NewFileOp().GetContent(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -138,7 +135,7 @@ func (n NginxService) UpdateConfigFile(req request.NginxConfigFileUpdate) error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
filePath := path.Join(global.Dir.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "nginx.conf")
|
filePath := path.Join(global.Dir.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, nginxModuleConfDir, "nginx.conf")
|
||||||
if req.Backup {
|
if req.Backup {
|
||||||
backupPath := path.Join(path.Dir(filePath), "bak")
|
backupPath := path.Join(path.Dir(filePath), "bak")
|
||||||
if !fileOp.Stat(backupPath) {
|
if !fileOp.Stat(backupPath) {
|
||||||
@@ -181,71 +178,20 @@ func (n NginxService) Build(req request.NginxBuildReq) error {
|
|||||||
if err = task.CheckTaskIsExecuting(taskName); err != nil {
|
if err = task.CheckTaskIsExecuting(taskName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fileOp := files.NewFileOp()
|
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, nginxInstall.ID); err != nil {
|
||||||
buildPath := path.Join(nginxInstall.GetPath(), "build")
|
return err
|
||||||
if !fileOp.Stat(buildPath) {
|
}
|
||||||
|
buildPath := path.Join(nginxInstall.GetPath(), nginxModuleBuildDir)
|
||||||
|
if !files.NewFileOp().Stat(buildPath) {
|
||||||
return buserr.New("ErrBuildDirNotFound")
|
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)
|
buildTask, err := task.NewTaskWithOps(nginxInstall.Name, task.TaskBuild, task.TaskScopeApp, req.TaskID, nginxInstall.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
buildTask.AddSubTaskWithOps("", func(t *task.Task) error {
|
buildTask.AddSubTaskWithOps("", func(t *task.Task) error {
|
||||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*buildTask), cmd.WithTimeout(120*time.Minute))
|
return executeNginxModuleBuild(nginxInstall, req.Modules, req.Force, req.Mirror, t, true)
|
||||||
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)
|
}, nil, 0, 120*time.Minute)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -259,27 +205,70 @@ func (n NginxService) GetModules() (*response.NginxBuildConfig, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fileOp := files.NewFileOp()
|
modules, err := loadNginxModules(nginxInstall)
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(moduleContent) > 0 {
|
target, targetWarning, targetErr := resolveNginxModuleTarget(nginxInstall)
|
||||||
_ = json.Unmarshal(moduleContent, &modules)
|
if targetWarning != "" {
|
||||||
|
global.LOG.Warn(targetWarning)
|
||||||
|
}
|
||||||
|
runtimeTarget := target
|
||||||
|
runtimeTarget.BuilderDigest = ""
|
||||||
|
setNginxModuleTargetKey(&runtimeTarget)
|
||||||
|
runtimeTargetErr := targetErr
|
||||||
|
if runtimeTarget.OpenRestyVersion != "" {
|
||||||
|
runtimeTargetErr = nil
|
||||||
}
|
}
|
||||||
var resList []response.NginxModule
|
var resList []response.NginxModule
|
||||||
for _, module := range modules {
|
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{
|
resList = append(resList, response.NginxModule{
|
||||||
Name: module.Name,
|
Name: module.Name,
|
||||||
Script: module.Script,
|
Custom: module.Custom,
|
||||||
Packages: strings.Join(module.Packages, ","),
|
Script: module.Script,
|
||||||
Params: module.Params,
|
Packages: strings.Join(module.Packages, ","),
|
||||||
Enable: module.Enable,
|
Params: module.Params,
|
||||||
|
Enable: module.Enable,
|
||||||
|
BuildMode: module.BuildMode,
|
||||||
|
Provider: module.Provider,
|
||||||
|
LoadOrder: module.LoadOrder,
|
||||||
|
BuildStatus: buildStatus,
|
||||||
|
LoadStatus: loadStatus,
|
||||||
|
Artifacts: artifacts,
|
||||||
|
LastError: module.LastError,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
envs, err := gotenv.Read(nginxInstall.GetEnvPath())
|
envs, err := gotenv.Read(nginxInstall.GetEnvPath())
|
||||||
@@ -288,69 +277,105 @@ func (n NginxService) GetModules() (*response.NginxBuildConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &response.NginxBuildConfig{
|
return &response.NginxBuildConfig{
|
||||||
Mirror: envs["CONTAINER_PACKAGE_URL"],
|
Mirror: envs["CONTAINER_PACKAGE_URL"],
|
||||||
Modules: resList,
|
DynamicSupported: nginxModuleDynamicSupported(nginxInstall),
|
||||||
|
Modules: resList,
|
||||||
}, nil
|
}, 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 {
|
func (n NginxService) UpdateModule(req request.NginxModuleUpdate) error {
|
||||||
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
|
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fileOp := files.NewFileOp()
|
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, nginxInstall.ID); err != nil {
|
||||||
var (
|
return err
|
||||||
modules []dto.NginxModule
|
|
||||||
)
|
|
||||||
moduleConfigPath := path.Join(nginxInstall.GetPath(), "build", "module.json")
|
|
||||||
if !fileOp.Stat(moduleConfigPath) {
|
|
||||||
_ = fileOp.CreateFile(moduleConfigPath)
|
|
||||||
}
|
}
|
||||||
moduleContent, err := fileOp.GetContent(moduleConfigPath)
|
modules, err := loadNginxModules(nginxInstall)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(moduleContent) > 0 {
|
oldModules := cloneNginxModules(modules)
|
||||||
_ = json.Unmarshal(moduleContent, &modules)
|
modules, deletedModule, err := applyNginxModuleUpdate(modules, req)
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return fileOp.SaveFileWithByte(moduleConfigPath, moduleByte, constant.DirPerm)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n NginxService) OperateDefaultHTTPs(req request.NginxDefaultHTTPSUpdate) error {
|
func (n NginxService) OperateDefaultHTTPs(req request.NginxDefaultHTTPSUpdate) error {
|
||||||
@@ -366,7 +391,7 @@ func (n NginxService) OperateDefaultHTTPs(req request.NginxDefaultHTTPSUpdate) e
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defaultConfigPath := path.Join(appInstall.GetPath(), "conf", "default", "00.default.conf")
|
defaultConfigPath := path.Join(appInstall.GetPath(), nginxModuleConfDir, "default", "00.default.conf")
|
||||||
content, err := os.ReadFile(defaultConfigPath)
|
content, err := os.ReadFile(defaultConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -406,7 +431,7 @@ func (n NginxService) GetDefaultHttpsStatus() (*response.NginxConfigRes, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defaultConfigPath := path.Join(appInstall.GetPath(), "conf", "default", "00.default.conf")
|
defaultConfigPath := path.Join(appInstall.GetPath(), nginxModuleConfDir, "default", "00.default.conf")
|
||||||
content, err := os.ReadFile(defaultConfigPath)
|
content, err := os.ReadFile(defaultConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
|
agentPsutil "github.com/1Panel-dev/1Panel/agent/utils/psutil"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/websocket"
|
"github.com/1Panel-dev/1Panel/agent/utils/websocket"
|
||||||
"github.com/shirou/gopsutil/v4/net"
|
"github.com/shirou/gopsutil/v4/net"
|
||||||
"github.com/shirou/gopsutil/v4/process"
|
"github.com/shirou/gopsutil/v4/process"
|
||||||
@@ -128,7 +129,7 @@ func (ps *ProcessService) GetProcessInfoByPID(pid int32) (*websocket.PsProcessDa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if createTime, err := p.CreateTime(); err == nil {
|
if createTime, err := agentPsutil.NewProcessCreateTimeResolver().CreateTime(p); err == nil {
|
||||||
data.StartTime = time.Unix(createTime/1000, 0).Format("2006-01-02 15:04:05")
|
data.StartTime = time.Unix(createTime/1000, 0).Format("2006-01-02 15:04:05")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,411 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -166,21 +166,28 @@ func reCreateRuntime(runtime *model.Runtime) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getComposeCmd(composePath, operate string) *exec.Cmd {
|
func getComposeCmd(composePath, operate string, projectName ...string) *exec.Cmd {
|
||||||
|
return getComposeCmdWithEnv(composePath, operate, "", projectName...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getComposeCmdWithEnv(composePath, operate, envFile string, projectName ...string) *exec.Cmd {
|
||||||
dockerCommand := global.CONF.DockerConfig.Command
|
dockerCommand := global.CONF.DockerConfig.Command
|
||||||
|
args := make([]string, 0, 9)
|
||||||
|
if envFile != "" {
|
||||||
|
args = append(args, "--env-file", envFile)
|
||||||
|
}
|
||||||
|
if len(projectName) > 0 && strings.TrimSpace(projectName[0]) != "" {
|
||||||
|
args = append(args, "--project-name", projectName[0])
|
||||||
|
}
|
||||||
|
args = append(args, "-f", composePath, operate)
|
||||||
|
if operate == "up" {
|
||||||
|
args = append(args, "-d")
|
||||||
|
}
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
if dockerCommand == "docker-compose" {
|
if dockerCommand == "docker-compose" {
|
||||||
if operate == "up" {
|
cmd = exec.Command("docker-compose", args...)
|
||||||
cmd = exec.Command("docker-compose", "-f", composePath, operate, "-d")
|
|
||||||
} else {
|
|
||||||
cmd = exec.Command("docker-compose", "-f", composePath, operate)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if operate == "up" {
|
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
|
||||||
cmd = exec.Command("docker", "compose", "-f", composePath, operate, "-d")
|
|
||||||
} else {
|
|
||||||
cmd = exec.Command("docker", "compose", "-f", composePath, operate)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,16 +193,20 @@ func handleSnapshot(req dto.SnapshotCreate, taskItem *task.Task, jobID, retry, t
|
|||||||
req.InterruptStep = ""
|
req.InterruptStep = ""
|
||||||
}
|
}
|
||||||
if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapUpload" {
|
if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapUpload" {
|
||||||
|
uploadTimeout := time.Duration(timeout) * time.Second
|
||||||
taskItem.AddSubTaskWithAliasAndOps(
|
taskItem.AddSubTaskWithAliasAndOps(
|
||||||
"SnapUpload",
|
"SnapUpload",
|
||||||
func(t *task.Task) error {
|
func(t *task.Task) error {
|
||||||
return snapUpload(itemHelper, req.SourceAccountIDs, req.DownloadAccountID, retry, fmt.Sprintf("%s.tar.gz", rootDir))
|
return snapUpload(itemHelper, req.SourceAccountIDs, req.DownloadAccountID, retry, fmt.Sprintf("%s.tar.gz", rootDir), uploadTimeout)
|
||||||
}, nil, int(retry), time.Duration(timeout)*time.Second,
|
}, nil, int(retry), uploadTimeout,
|
||||||
)
|
)
|
||||||
req.InterruptStep = ""
|
req.InterruptStep = ""
|
||||||
}
|
}
|
||||||
if err := taskItem.Execute(); err != nil {
|
if err := taskItem.Execute(); err != nil {
|
||||||
_ = snapshotRepo.Update(req.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error(), "interrupt_step": taskItem.Task.CurrentStep})
|
_ = 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
|
return err
|
||||||
}
|
}
|
||||||
_ = snapshotRepo.Update(req.ID, map[string]interface{}{"status": constant.StatusSuccess, "interrupt_step": ""})
|
_ = snapshotRepo.Update(req.ID, map[string]interface{}{"status": constant.StatusSuccess, "interrupt_step": ""})
|
||||||
@@ -210,6 +214,29 @@ func handleSnapshot(req dto.SnapshotCreate, taskItem *task.Task, jobID, retry, t
|
|||||||
return nil
|
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 {
|
type snapHelper struct {
|
||||||
SnapID uint
|
SnapID uint
|
||||||
SnapName string
|
SnapName string
|
||||||
@@ -561,15 +588,36 @@ func snapCompress(snap snapHelper, rootDir string, secret string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func snapUpload(snap snapHelper, accounts string, downloadID, retry uint, file string) error {
|
func snapUpload(snap snapHelper, accounts string, downloadID, retry uint, file string, timeout time.Duration) error {
|
||||||
snap.Task.Log("---------------------- 8 / 8 ----------------------")
|
snap.Task.Log("---------------------- 8 / 8 ----------------------")
|
||||||
snap.Task.LogStart(i18n.GetMsgByKey("SnapUpload"))
|
snap.Task.LogStart(i18n.GetMsgByKey("SnapUpload"))
|
||||||
|
|
||||||
src := path.Join(global.Dir.LocalBackupDir, "tmp/system", path.Base(file))
|
src := path.Join(global.Dir.LocalBackupDir, "tmp/system", path.Base(file))
|
||||||
dst := path.Join("system_snapshot", path.Base(file))
|
dst := path.Join("system_snapshot", path.Base(file))
|
||||||
accountMap := NewBackupClientMap(strings.Split(accounts, ","))
|
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})
|
||||||
if !accountMap[fmt.Sprintf("%d", downloadID)].isOk {
|
if !accountMap[fmt.Sprintf("%d", downloadID)].isOk {
|
||||||
return buserr.New(i18n.GetMsgWithDetail("LoadBackupFailed", accountMap[fmt.Sprintf("%d", downloadID)].message))
|
return buserr.New(i18n.GetMsgWithDetail("LoadBackupFailed", accountMap[fmt.Sprintf("%d", downloadID)].message))
|
||||||
}
|
}
|
||||||
return uploadWithMap(snap.Task, accountMap, src, dst, accounts, downloadID, retry)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -250,7 +250,11 @@ func handleDownloadSnapshot(itemHelper *snapRecoverHelper, snap model.Snapshot,
|
|||||||
itemHelper.Task.LogStart(i18n.GetMsgByKey("RecoverDownload"))
|
itemHelper.Task.LogStart(i18n.GetMsgByKey("RecoverDownload"))
|
||||||
|
|
||||||
account, client, err := NewBackupClientWithID(snap.DownloadAccountID)
|
account, client, err := NewBackupClientWithID(snap.DownloadAccountID)
|
||||||
itemHelper.Task.LogWithStatus(i18n.GetWithName("RecoverDownloadAccount", fmt.Sprintf("%s - %s", account.Type, account.Name)), err)
|
if err != nil {
|
||||||
|
itemHelper.Task.LogWithStatus(i18n.GetWithName("RecoverDownloadAccount", "-"), err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
itemHelper.Task.LogWithStatus(i18n.GetWithName("RecoverDownloadAccount", fmt.Sprintf("%s - %s", account.Type, account.Name)), nil)
|
||||||
targetPath := ""
|
targetPath := ""
|
||||||
if len(account.BackupPath) != 0 {
|
if len(account.BackupPath) != 0 {
|
||||||
targetPath = path.Join(account.BackupPath, fmt.Sprintf("system_snapshot/%s.tar.gz", snap.Name))
|
targetPath = path.Join(account.BackupPath, fmt.Sprintf("system_snapshot/%s.tar.gz", snap.Name))
|
||||||
@@ -259,11 +263,23 @@ func handleDownloadSnapshot(itemHelper *snapRecoverHelper, snap model.Snapshot,
|
|||||||
}
|
}
|
||||||
filePath := fmt.Sprintf("%s/%s.tar.gz", targetDir, snap.Name)
|
filePath := fmt.Sprintf("%s/%s.tar.gz", targetDir, snap.Name)
|
||||||
_ = os.RemoveAll(filePath)
|
_ = os.RemoveAll(filePath)
|
||||||
_, err = client.Download(targetPath, filePath)
|
err = prepareSnapshotRecoverFile(account.Type, targetPath, filePath, func() error {
|
||||||
|
_, downloadErr := client.Download(targetPath, filePath)
|
||||||
|
return downloadErr
|
||||||
|
})
|
||||||
itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("Download"), err)
|
itemHelper.Task.LogWithStatus(i18n.GetMsgByKey("Download"), err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prepareSnapshotRecoverFile(accountType, sourcePath, targetPath string, download func() error) error {
|
||||||
|
if accountType == constant.Local {
|
||||||
|
if err := os.Link(sourcePath, targetPath); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return download()
|
||||||
|
}
|
||||||
|
|
||||||
func backupBeforeRecover(name string, itemHelper *snapRecoverHelper) error {
|
func backupBeforeRecover(name string, itemHelper *snapRecoverHelper) error {
|
||||||
itemHelper.Task.Log("---------------------- 3 / 11 ----------------------")
|
itemHelper.Task.Log("---------------------- 3 / 11 ----------------------")
|
||||||
itemHelper.Task.LogStart(i18n.GetMsgByKey("BackupBeforeRecover"))
|
itemHelper.Task.LogStart(i18n.GetMsgByKey("BackupBeforeRecover"))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user