mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-10 16:00:31 +00:00
Compare commits
232
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fe78e4fe6 | ||
|
|
8415a1ce28 | ||
|
|
faf4933a02 | ||
|
|
c692a9f5e3 | ||
|
|
e7f0c21715 | ||
|
|
da7e8f1dbf | ||
|
|
486cd5d0ad | ||
|
|
00ce5a7b36 | ||
|
|
c17f055e06 | ||
|
|
f22df4b29c | ||
|
|
b9bab90a14 | ||
|
|
ce14a104cc | ||
|
|
2eaabccf94 | ||
|
|
e119239d3d | ||
|
|
14a57af4e9 | ||
|
|
2701af9054 | ||
|
|
4954067736 | ||
|
|
b962144e7d | ||
|
|
8f1be42b6b | ||
|
|
c6969f231b | ||
|
|
3571fb8d86 | ||
|
|
d21288788e | ||
|
|
f65e4b06ab | ||
|
|
d26bc9a3a4 | ||
|
|
7088a903fe | ||
|
|
864ebeed82 | ||
|
|
c1b2b92708 | ||
|
|
2189d9229a | ||
|
|
d0183f460e | ||
|
|
d27d6db1ea | ||
|
|
d2bb3813d9 | ||
|
|
cac73e6d45 | ||
|
|
4712ceaf2c | ||
|
|
65243c68c4 | ||
|
|
256e79ca81 | ||
|
|
4861eb69cb | ||
|
|
fb8cf15537 | ||
|
|
c4a6791271 | ||
|
|
f58e147636 | ||
|
|
387e9fbeed | ||
|
|
4eb627bc79 | ||
|
|
850c86229c | ||
|
|
f984917a66 | ||
|
|
8588217fbf | ||
|
|
a2307c5f64 | ||
|
|
5923290de8 | ||
|
|
1c994fba4a | ||
|
|
415ab96aab | ||
|
|
19bb823b05 | ||
|
|
3a5371652e | ||
|
|
a267b4148a | ||
|
|
36a01eb60d | ||
|
|
65f6fdd045 | ||
|
|
75b60b32e4 | ||
|
|
6cb65e2290 | ||
|
|
0bad1b471f | ||
|
|
3814525edd | ||
|
|
8162dd1856 | ||
|
|
e833787020 | ||
|
|
673ffac516 | ||
|
|
e864610015 | ||
|
|
78402e1b7d | ||
|
|
782bc1e67c | ||
|
|
86e4ed6f64 | ||
|
|
ee8bac39af | ||
|
|
fe742b9f41 | ||
|
|
9a5bd9bcba | ||
|
|
b9c8e39560 | ||
|
|
6b20ff0b13 | ||
|
|
89bd32b6d4 | ||
|
|
005f240fb7 | ||
|
|
75da53e374 | ||
|
|
2485b0aa5e | ||
|
|
ed51a5e1fa | ||
|
|
56870504ac | ||
|
|
7aefb47cc3 | ||
|
|
aba41c0aea | ||
|
|
9300bf4141 | ||
|
|
b7ec17b3e3 | ||
|
|
2fcfe56a30 | ||
|
|
63b2d4e4d5 | ||
|
|
4cd77d8ee1 | ||
|
|
53a7347bea | ||
|
|
a02c25ebcc | ||
|
|
605c8cc6db | ||
|
|
033cc7c2d1 | ||
|
|
7c1ddb5b4c | ||
|
|
eb0f5264d7 | ||
|
|
5ad12c6fe4 | ||
|
|
61dacce5e0 | ||
|
|
e3f0381a26 | ||
|
|
6bc9dd96af | ||
|
|
e23f338b31 | ||
|
|
6e08b50e3c | ||
|
|
536712cd55 | ||
|
|
191ff0cda4 | ||
|
|
671f781564 | ||
|
|
30dc36b95d | ||
|
|
fac4aec680 | ||
|
|
8eac9a1808 | ||
|
|
ce74d96617 | ||
|
|
a15e77d605 | ||
|
|
bad022f524 | ||
|
|
50a54d0613 | ||
|
|
a47e41a8b7 | ||
|
|
f938443e55 | ||
|
|
b90abd2b28 | ||
|
|
da5682a600 | ||
|
|
81b72d9b7d | ||
|
|
6e13143286 | ||
|
|
70fc628c81 | ||
|
|
9858881ce6 | ||
|
|
eb6a8c7646 | ||
|
|
a71aea8aec | ||
|
|
918c441f88 | ||
|
|
960b4b0345 | ||
|
|
3aa4bfaa82 | ||
|
|
b3bdf9ef7e | ||
|
|
2948b8ffe8 | ||
|
|
e99c6c08a5 | ||
|
|
6fb389b2ed | ||
|
|
c09833cbde | ||
|
|
fa2ad69154 | ||
|
|
51d84455a3 | ||
|
|
d88d98d8a8 | ||
|
|
5aec466c8e | ||
|
|
0ee93774d5 | ||
|
|
7be7368bb9 | ||
|
|
eab0bb4a94 | ||
|
|
9f74f2077a | ||
|
|
a6e2efa6c9 | ||
|
|
205ef3009a | ||
|
|
d7edbd1e95 | ||
|
|
b361f464c5 | ||
|
|
fb377d2e99 | ||
|
|
deddd392ba | ||
|
|
3ab10848c8 | ||
|
|
433f1a940f | ||
|
|
1f12c09eb5 | ||
|
|
31e6d523f9 | ||
|
|
3c0bd051bf | ||
|
|
3c2d92dc5f | ||
|
|
262bd14bc8 | ||
|
|
f15ff46e34 | ||
|
|
fc1ec4e1b0 | ||
|
|
53f75826d8 | ||
|
|
ddfb816ef1 | ||
|
|
18428d108e | ||
|
|
3506c5dd3b | ||
|
|
2dffd06b1b | ||
|
|
12f2484d12 | ||
|
|
2dea44acf6 | ||
|
|
205f76c65d | ||
|
|
86af4fbd4d | ||
|
|
7915230121 | ||
|
|
1b27db7daa | ||
|
|
7370dcaa55 | ||
|
|
6a378b6863 | ||
|
|
6f6747a584 | ||
|
|
825221b2bb | ||
|
|
1e9d4b592e | ||
|
|
4a51db4764 | ||
|
|
afea71c81c | ||
|
|
9c8ca2ab3c | ||
|
|
a04875f64b | ||
|
|
7ec0bdb3f7 | ||
|
|
d2dbb6486e | ||
|
|
14728f889e | ||
|
|
ff199245b0 | ||
|
|
667807e249 | ||
|
|
63e09c8c47 | ||
|
|
17a8835d59 | ||
|
|
b306bfa77a | ||
|
|
b1eff2a893 | ||
|
|
5ac7c80881 | ||
|
|
d402f67fc3 | ||
|
|
c13793c445 | ||
|
|
daa3f6b206 | ||
|
|
a2d85c911d | ||
|
|
1b76c91e1b | ||
|
|
d663a4397a | ||
|
|
d1558c5eae | ||
|
|
0da4f77a2e | ||
|
|
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 |
@@ -1,9 +1,6 @@
|
||||
<p align="center"><a href="https://1panel.pro"><img src="https://resource.1panel.pro/img/1panel-logo.png" alt="1Panel" width="300" /></a></p>
|
||||
|
||||
<h3 align="center">The open-source VPS control panel with native AI agent support</h3>
|
||||
|
||||
<p align="center">
|
||||
Trusted by <strong>2,000,000+</strong> self-hosters worldwide
|
||||
Loved by a global community of <strong>2.5M+</strong> self-hosters.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -12,7 +9,6 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://shields.io/github/license/1Panel-dev/1Panel?color=%231890FF" alt="License: GPL v3"></a>
|
||||
<a href="https://app.codacy.com/gh/1Panel-dev/1Panel"><img src="https://app.codacy.com/project/badge/Grade/da67574fd82b473992781d1386b937ef" alt="Codacy"></a>
|
||||
<a href="https://discord.gg/bUpUqWqdRr"><img src="https://img.shields.io/discord/1318846410149335080?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb" alt="Discord"></a>
|
||||
<a href="https://github.com/1Panel-dev/1Panel/releases"><img src="https://img.shields.io/github/v/release/1Panel-dev/1Panel" alt="GitHub release"></a>
|
||||
<a href="https://github.com/1Panel-dev/1Panel"><img src="https://img.shields.io/github/stars/1Panel-dev/1Panel?color=%231890FF&style=flat-square" alt="Stars"></a>
|
||||
@@ -41,34 +37,28 @@
|
||||
|
||||
## What is 1Panel?
|
||||
|
||||
1Panel is a modern, open-source VPS control panel — and the only one with **native AI agent support**. Run Ollama models, deploy OpenClaw agents, and manage your entire server stack from one clean web interface. No CLI memorization required.
|
||||
|
||||
👉 Watch the [2-minute introduction](https://www.youtube.com/watch?v=Jl_wqp-XA08)
|
||||
1Panel is a modern, open-source Linux server management panel and a lightweight AI management platform. Through an intuitive web interface, it provides users with comprehensive, one-stop server management capabilities:
|
||||
- **AI Management**: Offers a unified management platform from bare metal to agents (Metal-to-Agent). It integrates an AI gateway, and Skills Hub, while supporting centralized management of agents and models.
|
||||
- **Efficient Visual Operations**: Easily manage Linux servers through a web-based GUI, streamlining tasks such as host monitoring, file management, database management, and container management.
|
||||
- **Rapid Website Deployment**: Deeply integrates with popular website builders like WordPress and Halo. It enables one-click domain binding and SSL certificate configuration, significantly lowering the barrier to website creation.
|
||||
- **Curated App Store**: Features a built-in store of high-quality open-source applications, providing one-click installation and upgrade services to effortlessly extend server capabilities.
|
||||
- **Enterprise-Grade Security**: Deploys applications based on container technology to effectively minimize vulnerability exposure. It also provides security features such as WAF and log auditing to ensure comprehensive server protection.
|
||||
- **One-Click Data Backup**: Supports one-click backup and restoration, and integrates with various cloud storage solutions to ensure data security and prevent loss.
|
||||
|
||||
## Why 1Panel?
|
||||
|
||||
| | 1Panel | cPanel / Plesk | aaPanel | Webmin |
|
||||
|--|--------|----------------|---------|--------|
|
||||
| Free & open source | ✅ | ❌ | Partial | ✅ |
|
||||
| Native AI agent runtime | ✅ | ❌ | ❌ | ❌ |
|
||||
| AI management | ✅ | ❌ | ❌ | ❌ |
|
||||
| One-click app marketplace | ✅ 165+ apps | ❌ | ✅ | ❌ |
|
||||
| Modern UI (post-2020) | ✅ | ❌ | Partial | ❌ |
|
||||
| Docker / container management | ✅ | ❌ | ❌ | ❌ |
|
||||
| Active development | ✅ | ✅ | ✅ | Slow |
|
||||
|
||||
## Key Features
|
||||
|
||||
- **AI Agent Runtime**: Deploy Ollama LLMs, spin up OpenClaw personal agents, and monitor GPU utilization — all from the dashboard. No separate AI stack to manage.
|
||||
- **One-Click Website Deployment**: Launch production-ready websites with automatic domain binding, SSL provisioning, and Nginx config — zero manual setup.
|
||||
- **App Marketplace**: 165+ trusted open-source apps (Nextcloud, Bitwarden, Umami, NocoBase, and more) installed and updated with a single click.
|
||||
- **Docker & Container Management**: Create, start, stop, and inspect containers, images, networks, and volumes through a visual UI — no CLI juggling.
|
||||
- **Security Out of the Box**: Firewall rules, fail2ban, container isolation, WAF, and audit logs — configured and running from day one.
|
||||
- **Backup & Restore**: Schedule automated backups to AWS S3, Cloudflare R2, or local storage. Restore any snapshot in one click.
|
||||
|
||||
## Quick Start
|
||||
|
||||
> **Requirements:** Linux VPS (Debian / Ubuntu / CentOS / Rocky), 1 GB RAM, internet access.
|
||||
> Takes ~60 seconds.
|
||||
Prepare your Linux server and run the following script:
|
||||
|
||||
```bash
|
||||
bash -c "$(curl -sSL https://resource.1panel.pro/v2/quick_start.sh)"
|
||||
@@ -83,24 +73,20 @@ Run `1pctl user-info` via SSH if you need to retrieve your access credentials.
|
||||
|
||||
## Pro Edition
|
||||
|
||||
1Panel OSS is free forever. Pro adds features built for teams and production workloads:
|
||||
1Panel OSS is free forever. 1Panel Pro and Ent adds features built for teams and production workloads:
|
||||
|
||||
| Feature | OSS | Pro |
|
||||
|---------|:---:|:---:|
|
||||
| One-click app installs | ✅ | ✅ |
|
||||
| AI agents (OpenClaw) | 1 agent | Unlimited |
|
||||
| WAF & advanced security | Basic | ✅ |
|
||||
| Website tamper protection | ❌ | ✅ |
|
||||
| Website uptime monitoring | ❌ | ✅ |
|
||||
| Multi-node management | ❌ | ✅ |
|
||||
| Custom logo & theme | ❌ | ✅ |
|
||||
| Priority support | ❌ | ✅ |
|
||||
|
||||
**From $80/year.** [Compare plans & start 30-day free trial →](https://1panel.pro/pricing)
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#1Panel-dev/1Panel&Date)
|
||||
| Feature | OSS | Pro | Ent |
|
||||
|---------|:---:|:---:|:---:|
|
||||
| One-click app installs | ✅ | ✅ | ✅ |
|
||||
| AI agents (OpenClaw) | 5 agent | Unlimited | ✅ |
|
||||
| WAF & advanced security | Basic | ✅ | ✅ |
|
||||
| Website tamper protection | ❌ | ✅ | ✅ |
|
||||
| Website uptime monitoring | ❌ | ✅ | ✅ |
|
||||
| Multi-node management | ❌ | ✅ | ✅ |
|
||||
| Custom logo & theme | ❌ | ✅ | ✅ |
|
||||
| KVM Web UI | ❌ | ❌ | ✅ |
|
||||
| AI Gateway | ❌ | ❌ | ✅ |
|
||||
| Priority support | ❌ | ❌ | ✅ |
|
||||
|
||||
## Community & Support
|
||||
|
||||
|
||||
@@ -1378,6 +1378,88 @@ func (b *BaseApi) UninstallAgentSkill(c *gin.Context) {
|
||||
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
|
||||
// @Summary Login Agent Weixin channel
|
||||
// @Accept json
|
||||
|
||||
@@ -2,11 +2,14 @@ package v2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -294,6 +297,34 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if err := alertService.UpdateAlertConfig(req, loadAuditUser(c)); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, repo.ErrAlertConfigRevisionConflict):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "ALERT_CONFIG_REVISION_CONFLICT", "ErrInvalidParams", err)
|
||||
case errors.Is(err, repo.ErrAlertConfigRevisionRequired):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "ALERT_CONFIG_REVISION_REQUIRED", "ErrInvalidParams", err)
|
||||
default:
|
||||
helper.InternalServer(c, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Alert
|
||||
// @Summary Update alert config status
|
||||
// @Accept json
|
||||
// @Param request body dto.AlertConfigStatusUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /alert/config/status [post]
|
||||
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [id][status]","formatEN":"update alert config status [id][status]"}
|
||||
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
|
||||
var req dto.AlertConfigStatusUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c)); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
@@ -346,6 +377,15 @@ func (b *BaseApi) TestAlertConfig(c *gin.Context) {
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if req.Type == constant.Custom {
|
||||
result, err := alertService.TestCustomAlertConfig(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
return
|
||||
}
|
||||
flag, err := alertService.TestAlertConfig(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
@@ -439,7 +439,7 @@ func (b *BaseApi) ContainerItemStats(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := containerService.ContainerItemStats(req)
|
||||
data, err := containerService.ContainerItemStats(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -881,6 +881,26 @@ func (b *BaseApi) ComposeUpdate(c *gin.Context) {
|
||||
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
|
||||
// @Summary Load compose environment variables
|
||||
// @Accept json
|
||||
|
||||
@@ -86,17 +86,23 @@ func (b *BaseApi) CheckHasCli(c *gin.Context) {
|
||||
|
||||
// @Tags Database Redis
|
||||
// @Summary Install redis-cli
|
||||
// @Success 200
|
||||
// @Accept json
|
||||
// @Param request body dto.RedisCliInstall true "request"
|
||||
// @Success 200 {string} string
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/redis/install/cli [post]
|
||||
func (b *BaseApi) InstallCli(c *gin.Context) {
|
||||
if err := redisService.InstallCli(); err != nil {
|
||||
var req dto.RedisCliInstall
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := redisService.InstallCli(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags Database Redis
|
||||
|
||||
@@ -42,7 +42,9 @@ var (
|
||||
fileShareService = service.NewIFileShareService()
|
||||
sshService = service.NewISSHService()
|
||||
firewallService = service.NewIFirewallService()
|
||||
iptablesService = service.NewIIptablesService()
|
||||
firewallSettingService = service.NewIFirewallSettingService()
|
||||
forwardingService = service.NewIForwardingService()
|
||||
dockerPortGuardService = service.NewIDockerPortGuardService()
|
||||
monitorService = service.NewIMonitorService()
|
||||
systemService = service.NewISystemService()
|
||||
runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService()
|
||||
@@ -60,6 +62,7 @@ var (
|
||||
websiteDnsAccountService = service.NewIWebsiteDnsAccountService()
|
||||
websiteSSLService = service.NewIWebsiteSSLService()
|
||||
websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService()
|
||||
websiteTemplateService = service.NewIWebsiteTemplateService()
|
||||
|
||||
nginxService = service.NewINginxService()
|
||||
|
||||
|
||||
+470
-23
@@ -35,6 +35,81 @@ var cancelledChunkUploads = struct {
|
||||
ids map[string]struct{}
|
||||
}{ids: make(map[string]struct{})}
|
||||
|
||||
type chunkUploadLock struct {
|
||||
mutex sync.Mutex
|
||||
refs int
|
||||
}
|
||||
|
||||
var chunkUploadLocks = struct {
|
||||
sync.Mutex
|
||||
items map[string]*chunkUploadLock
|
||||
}{items: make(map[string]*chunkUploadLock)}
|
||||
|
||||
type completedChunkUpload struct {
|
||||
dstDir string
|
||||
filename string
|
||||
fileSize int64
|
||||
}
|
||||
|
||||
var completedChunkUploads = struct {
|
||||
sync.RWMutex
|
||||
items map[string]completedChunkUpload
|
||||
}{items: make(map[string]completedChunkUpload)}
|
||||
|
||||
var activeChunkUploadTTL = 24 * time.Hour
|
||||
|
||||
var (
|
||||
errChunkUploadCancelled = errors.New("upload cancelled")
|
||||
errInvalidChunkUpload = errors.New("invalid chunk upload")
|
||||
)
|
||||
|
||||
type activeChunkUpload struct {
|
||||
upload completedChunkUpload
|
||||
expiresAt time.Time
|
||||
timer *time.Timer
|
||||
}
|
||||
|
||||
var activeChunkUploads = struct {
|
||||
sync.RWMutex
|
||||
items map[string]activeChunkUpload
|
||||
}{items: make(map[string]activeChunkUpload)}
|
||||
|
||||
type resumableUploadChunk struct {
|
||||
UploadID string
|
||||
Filename string
|
||||
DstDir string
|
||||
ChunkIndex int
|
||||
ChunkCount int
|
||||
Offset int64
|
||||
FileSize int64
|
||||
Overwrite bool
|
||||
}
|
||||
|
||||
func invalidChunkUploadError(message string) error {
|
||||
return fmt.Errorf("%w: %s", errInvalidChunkUpload, message)
|
||||
}
|
||||
|
||||
func isRetryableChunkUploadError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, errChunkUploadCancelled) ||
|
||||
errors.Is(err, errInvalidChunkUpload) ||
|
||||
errors.Is(err, os.ErrExist) ||
|
||||
errors.Is(err, os.ErrPermission) ||
|
||||
errors.Is(err, os.ErrInvalid) ||
|
||||
errors.Is(err, syscall.ENOSPC) ||
|
||||
errors.Is(err, syscall.EDQUOT) ||
|
||||
errors.Is(err, syscall.EROFS) ||
|
||||
errors.Is(err, syscall.EFBIG) ||
|
||||
errors.Is(err, syscall.ENAMETOOLONG) ||
|
||||
errors.Is(err, syscall.ENOTDIR) ||
|
||||
errors.Is(err, syscall.EISDIR) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary List files
|
||||
// @Accept json
|
||||
@@ -474,11 +549,7 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
dstInfo, statErr := os.Stat(dstFilename)
|
||||
if overwrite {
|
||||
_ = os.Remove(dstFilename)
|
||||
}
|
||||
|
||||
err = os.Rename(tmpFilename, dstFilename)
|
||||
err = finalizeUploadedFile(tmpFilename, dstFilename, overwrite)
|
||||
if err != nil {
|
||||
_ = os.Remove(tmpFilename)
|
||||
e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
|
||||
@@ -613,10 +684,35 @@ func (b *BaseApi) StopWget(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
files.CancelDownload(req.Key)
|
||||
if err := files.CancelDownload(req.Key); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Remove finished download progress records without deleting files
|
||||
// @Accept json
|
||||
// @Param request body request.FileProcessRemoveReq true "request"
|
||||
// @Success 200 {object} response.FileProcessKeys
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/wget/process/remove [post]
|
||||
// @x-panel-log {"bodyKeys":["keys"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"移除已结束下载记录 [keys]","formatEN":"Remove finished download records [keys]"}
|
||||
func (b *BaseApi) RemoveWgetRecords(c *gin.Context) {
|
||||
var req request.FileProcessRemoveReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
keys, err := files.RemoveDownloadRecords(req.Keys)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, response.FileProcessKeys{Keys: keys})
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Move file
|
||||
// @Accept json
|
||||
@@ -638,6 +734,26 @@ func (b *BaseApi) MoveFile(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Stop file move task
|
||||
// @Accept json
|
||||
// @Param request body request.FileMoveStopReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/move/stop [post]
|
||||
func (b *BaseApi) StopMoveFile(c *gin.Context) {
|
||||
var req request.FileMoveStopReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := fileService.StopMvFile(req.TaskID); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Download file
|
||||
// @Accept json
|
||||
@@ -792,6 +908,289 @@ func (b *BaseApi) DepthDirSize(c *gin.Context) {
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
||||
func lockChunkUpload(uploadID string) func() {
|
||||
chunkUploadLocks.Lock()
|
||||
lock, ok := chunkUploadLocks.items[uploadID]
|
||||
if !ok {
|
||||
lock = &chunkUploadLock{}
|
||||
chunkUploadLocks.items[uploadID] = lock
|
||||
}
|
||||
lock.refs++
|
||||
chunkUploadLocks.Unlock()
|
||||
|
||||
lock.mutex.Lock()
|
||||
return func() {
|
||||
lock.mutex.Unlock()
|
||||
chunkUploadLocks.Lock()
|
||||
lock.refs--
|
||||
if lock.refs == 0 {
|
||||
delete(chunkUploadLocks.items, uploadID)
|
||||
}
|
||||
chunkUploadLocks.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func resumableUploadPartPath(dstDir, uploadID string) string {
|
||||
return filepath.Join(dstDir, fmt.Sprintf(".1panel-upload-%s.part", uploadID))
|
||||
}
|
||||
|
||||
func finalizeUploadedFile(tmpFile, dstFile string, overwrite bool) error {
|
||||
if overwrite {
|
||||
return os.Rename(tmpFile, dstFile)
|
||||
}
|
||||
if err := os.Link(tmpFile, dstFile); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(tmpFile); err != nil {
|
||||
if rollbackErr := os.Remove(dstFile); rollbackErr != nil {
|
||||
return fmt.Errorf("remove upload temporary file failed: %v, rollback destination failed: %w", err, rollbackErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerActiveChunkUpload(uploadID string, upload completedChunkUpload) error {
|
||||
activeChunkUploads.Lock()
|
||||
defer activeChunkUploads.Unlock()
|
||||
if active, ok := activeChunkUploads.items[uploadID]; ok {
|
||||
if active.upload != upload {
|
||||
return invalidChunkUploadError("upload ID is already used by another file")
|
||||
}
|
||||
active.timer.Stop()
|
||||
}
|
||||
expiresAt := time.Now().Add(activeChunkUploadTTL)
|
||||
timer := time.AfterFunc(activeChunkUploadTTL, func() {
|
||||
expireActiveChunkUpload(uploadID, expiresAt)
|
||||
})
|
||||
activeChunkUploads.items[uploadID] = activeChunkUpload{
|
||||
upload: upload,
|
||||
expiresAt: expiresAt,
|
||||
timer: timer,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadActiveChunkUpload(uploadID string) (completedChunkUpload, bool) {
|
||||
activeChunkUploads.RLock()
|
||||
active, ok := activeChunkUploads.items[uploadID]
|
||||
activeChunkUploads.RUnlock()
|
||||
return active.upload, ok
|
||||
}
|
||||
|
||||
func deleteActiveChunkUpload(uploadID string) {
|
||||
activeChunkUploads.Lock()
|
||||
if active, ok := activeChunkUploads.items[uploadID]; ok {
|
||||
active.timer.Stop()
|
||||
}
|
||||
delete(activeChunkUploads.items, uploadID)
|
||||
activeChunkUploads.Unlock()
|
||||
}
|
||||
|
||||
func discardActiveChunkUpload(uploadID, partFile string) error {
|
||||
deleteActiveChunkUpload(uploadID)
|
||||
if err := os.Remove(partFile); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("remove upload temporary file failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func finalizeActiveChunkUpload(uploadID, partFile, dstFile string, overwrite bool) error {
|
||||
if err := finalizeUploadedFile(partFile, dstFile, overwrite); err != nil {
|
||||
if removeErr := discardActiveChunkUpload(uploadID, partFile); removeErr != nil {
|
||||
return errors.Join(err, removeErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func expireActiveChunkUpload(uploadID string, expiresAt time.Time) {
|
||||
unlock := lockChunkUpload(uploadID)
|
||||
defer unlock()
|
||||
activeChunkUploads.Lock()
|
||||
active, ok := activeChunkUploads.items[uploadID]
|
||||
if !ok || !active.expiresAt.Equal(expiresAt) {
|
||||
activeChunkUploads.Unlock()
|
||||
return
|
||||
}
|
||||
delete(activeChunkUploads.items, uploadID)
|
||||
activeChunkUploads.Unlock()
|
||||
partFile := resumableUploadPartPath(active.upload.dstDir, uploadID)
|
||||
if err := os.Remove(partFile); err != nil && !os.IsNotExist(err) {
|
||||
global.LOG.Warnf("remove inactive upload part [%s] failed: %v", partFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
func removeActiveResumableUploadPart(uploadID string) error {
|
||||
unlock := lockChunkUpload(uploadID)
|
||||
defer unlock()
|
||||
upload, ok := loadActiveChunkUpload(uploadID)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
err := os.Remove(resumableUploadPartPath(upload.dstDir, uploadID))
|
||||
if err == nil || os.IsNotExist(err) {
|
||||
deleteActiveChunkUpload(uploadID)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func loadCompletedChunkUpload(uploadID string) (completedChunkUpload, bool) {
|
||||
completedChunkUploads.RLock()
|
||||
completed, ok := completedChunkUploads.items[uploadID]
|
||||
completedChunkUploads.RUnlock()
|
||||
return completed, ok
|
||||
}
|
||||
|
||||
func markChunkUploadCompleted(uploadID string, completed completedChunkUpload) {
|
||||
completedChunkUploads.Lock()
|
||||
completedChunkUploads.items[uploadID] = completed
|
||||
completedChunkUploads.Unlock()
|
||||
time.AfterFunc(10*time.Minute, func() {
|
||||
completedChunkUploads.Lock()
|
||||
delete(completedChunkUploads.items, uploadID)
|
||||
completedChunkUploads.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
func writeResumableUploadChunk(chunk resumableUploadChunk, chunkData []byte) error {
|
||||
unlock := lockChunkUpload(chunk.UploadID)
|
||||
defer unlock()
|
||||
if chunkUploadCancelled(chunk.UploadID) {
|
||||
return errChunkUploadCancelled
|
||||
}
|
||||
if chunk.UploadID == "" || filepath.Base(chunk.UploadID) != chunk.UploadID || strings.ContainsAny(chunk.UploadID, `/\`) {
|
||||
return invalidChunkUploadError("invalid upload ID")
|
||||
}
|
||||
if chunk.Filename == "" || filepath.Base(chunk.Filename) != chunk.Filename || strings.ContainsAny(chunk.Filename, `/\`) {
|
||||
return invalidChunkUploadError("invalid filename")
|
||||
}
|
||||
if strings.TrimSpace(chunk.DstDir) == "" {
|
||||
return invalidChunkUploadError("upload destination is required")
|
||||
}
|
||||
dstDir := filepath.Clean(strings.TrimSpace(chunk.DstDir))
|
||||
|
||||
if chunk.ChunkCount <= 0 || chunk.ChunkIndex < 0 || chunk.ChunkIndex >= chunk.ChunkCount {
|
||||
return invalidChunkUploadError("invalid chunk index")
|
||||
}
|
||||
if chunk.FileSize <= 0 || chunk.Offset < 0 || chunk.Offset > chunk.FileSize {
|
||||
return invalidChunkUploadError("invalid upload offset")
|
||||
}
|
||||
chunkEnd := chunk.Offset + int64(len(chunkData))
|
||||
if chunkEnd > chunk.FileSize {
|
||||
return invalidChunkUploadError("chunk exceeds file size")
|
||||
}
|
||||
if chunk.ChunkIndex+1 == chunk.ChunkCount {
|
||||
if chunkEnd != chunk.FileSize {
|
||||
return invalidChunkUploadError("final chunk does not match file size")
|
||||
}
|
||||
} else if chunkEnd >= chunk.FileSize {
|
||||
return invalidChunkUploadError("non-final chunk reaches file size")
|
||||
}
|
||||
if completed, ok := loadCompletedChunkUpload(chunk.UploadID); ok {
|
||||
if completed.dstDir == dstDir && completed.filename == chunk.Filename && completed.fileSize == chunk.FileSize {
|
||||
return nil
|
||||
}
|
||||
return invalidChunkUploadError("upload ID has already completed another file")
|
||||
}
|
||||
upload := completedChunkUpload{dstDir: dstDir, filename: chunk.Filename, fileSize: chunk.FileSize}
|
||||
if err := registerActiveChunkUpload(chunk.UploadID, upload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mode, err := files.GetParentMode(dstDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.MkdirAll(dstDir, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
dstDirInfo, err := os.Stat(dstDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !dstDirInfo.IsDir() {
|
||||
return invalidChunkUploadError(fmt.Sprintf("upload destination [%s] is not a directory", dstDir))
|
||||
}
|
||||
|
||||
dstFile := filepath.Join(dstDir, chunk.Filename)
|
||||
partFile := resumableUploadPartPath(dstDir, chunk.UploadID)
|
||||
if dstFile == partFile {
|
||||
return invalidChunkUploadError("filename conflicts with upload temporary file")
|
||||
}
|
||||
fileMode := dstDirInfo.Mode().Perm()
|
||||
ownerInfo := dstDirInfo
|
||||
if dstInfo, statErr := os.Stat(dstFile); statErr == nil {
|
||||
if !chunk.Overwrite {
|
||||
if err := discardActiveChunkUpload(chunk.UploadID, partFile); err != nil {
|
||||
return errors.Join(os.ErrExist, err)
|
||||
}
|
||||
return os.ErrExist
|
||||
}
|
||||
fileMode = dstInfo.Mode().Perm()
|
||||
ownerInfo = dstInfo
|
||||
} else if !os.IsNotExist(statErr) {
|
||||
return statErr
|
||||
}
|
||||
|
||||
part, err := os.OpenFile(partFile, os.O_CREATE|os.O_RDWR, fileMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
partClosed := false
|
||||
defer func() {
|
||||
if !partClosed {
|
||||
_ = part.Close()
|
||||
}
|
||||
}()
|
||||
if stat, statErr := part.Stat(); statErr != nil {
|
||||
return statErr
|
||||
} else if chunk.Offset > stat.Size() {
|
||||
return invalidChunkUploadError(fmt.Sprintf("unexpected upload offset %d, current size is %d", chunk.Offset, stat.Size()))
|
||||
} else if chunk.Offset < stat.Size() && chunkEnd > stat.Size() {
|
||||
if err = part.Truncate(chunk.Offset); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err = part.WriteAt(chunkData, chunk.Offset); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if chunk.ChunkIndex+1 != chunk.ChunkCount {
|
||||
return nil
|
||||
}
|
||||
partInfo, err := part.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if partInfo.Size() != chunk.FileSize {
|
||||
return invalidChunkUploadError(fmt.Sprintf("uploaded file size mismatch: expected %d, got %d", chunk.FileSize, partInfo.Size()))
|
||||
}
|
||||
if err = part.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
partClosed = true
|
||||
if err = os.Chmod(partFile, fileMode); err != nil {
|
||||
return err
|
||||
}
|
||||
if stat, ok := ownerInfo.Sys().(*syscall.Stat_t); ok {
|
||||
if err = os.Chown(partFile, int(stat.Uid), int(stat.Gid)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if chunkUploadCancelled(chunk.UploadID) {
|
||||
return errChunkUploadCancelled
|
||||
}
|
||||
if err = finalizeActiveChunkUpload(chunk.UploadID, partFile, dstFile, chunk.Overwrite); err != nil {
|
||||
return err
|
||||
}
|
||||
markChunkUploadCompleted(chunk.UploadID, upload)
|
||||
deleteActiveChunkUpload(chunk.UploadID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int, overwrite bool) error {
|
||||
defer func() {
|
||||
_ = os.RemoveAll(fileDir)
|
||||
@@ -871,6 +1270,10 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if chunkCount <= 0 || chunkIndex < 0 || chunkIndex >= chunkCount {
|
||||
helper.BadRequest(c, errors.New("invalid chunk index"))
|
||||
return
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
tmpDir := path.Join(global.Dir.TmpDir, "upload")
|
||||
if !fileOp.Stat(tmpDir) {
|
||||
@@ -885,20 +1288,25 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
uploadID := strings.TrimSpace(c.PostForm("uploadID"))
|
||||
resumable := c.PostForm("fileSize") != "" || c.PostForm("offset") != ""
|
||||
cancellable := uploadID != ""
|
||||
if cancellable && (filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`)) {
|
||||
helper.BadRequest(c, errors.New("invalid upload ID"))
|
||||
return
|
||||
}
|
||||
if resumable && !cancellable {
|
||||
helper.BadRequest(c, errors.New("upload ID is required"))
|
||||
return
|
||||
}
|
||||
if !cancellable {
|
||||
uploadID = filename
|
||||
}
|
||||
fileDir := filepath.Join(tmpDir, uploadID)
|
||||
if cancellable && chunkUploadCancelled(uploadID) {
|
||||
helper.BadRequest(c, errors.New("upload cancelled"))
|
||||
helper.BadRequest(c, errChunkUploadCancelled)
|
||||
return
|
||||
}
|
||||
if chunkIndex == 0 {
|
||||
if !resumable && chunkIndex == 0 {
|
||||
if fileOp.Stat(fileDir) {
|
||||
_ = fileOp.DeleteDir(fileDir)
|
||||
}
|
||||
@@ -907,32 +1315,67 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
||||
filePath := filepath.Join(fileDir, filename)
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if !resumable && err != nil {
|
||||
_ = os.RemoveAll(fileDir)
|
||||
}
|
||||
}()
|
||||
var (
|
||||
emptyFile *os.File
|
||||
chunkData []byte
|
||||
)
|
||||
|
||||
emptyFile, err = os.Create(filePath)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
defer emptyFile.Close()
|
||||
|
||||
chunkData, err = io.ReadAll(uploadFile)
|
||||
chunkData, err := io.ReadAll(uploadFile)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
||||
return
|
||||
}
|
||||
if cancellable && chunkUploadCancelled(uploadID) {
|
||||
err = errors.New("upload cancelled")
|
||||
err = errChunkUploadCancelled
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if resumable {
|
||||
offset, parseErr := strconv.ParseInt(c.PostForm("offset"), 10, 64)
|
||||
if parseErr != nil {
|
||||
helper.BadRequest(c, parseErr)
|
||||
return
|
||||
}
|
||||
fileSize, parseErr := strconv.ParseInt(c.PostForm("fileSize"), 10, 64)
|
||||
if parseErr != nil {
|
||||
helper.BadRequest(c, parseErr)
|
||||
return
|
||||
}
|
||||
overwrite := true
|
||||
if ow := c.PostForm("overwrite"); ow != "" {
|
||||
overwrite, _ = strconv.ParseBool(ow)
|
||||
}
|
||||
err = writeResumableUploadChunk(resumableUploadChunk{
|
||||
UploadID: uploadID,
|
||||
Filename: filename,
|
||||
DstDir: c.PostForm("path"),
|
||||
ChunkIndex: chunkIndex,
|
||||
ChunkCount: chunkCount,
|
||||
Offset: offset,
|
||||
FileSize: fileSize,
|
||||
Overwrite: overwrite,
|
||||
}, chunkData)
|
||||
if err != nil {
|
||||
uploadErr := buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err)
|
||||
helper.ErrorWithDetailAndData(c, http.StatusInternalServerError, "ErrInternalServer", uploadErr, gin.H{
|
||||
"retryable": isRetryableChunkUploadError(err),
|
||||
})
|
||||
return
|
||||
}
|
||||
if chunkIndex+1 == chunkCount {
|
||||
cancelledChunkUploads.Lock()
|
||||
delete(cancelledChunkUploads.ids, uploadID)
|
||||
cancelledChunkUploads.Unlock()
|
||||
}
|
||||
helper.SuccessWithData(c, true)
|
||||
return
|
||||
}
|
||||
|
||||
emptyFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
defer emptyFile.Close()
|
||||
|
||||
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
|
||||
err = os.WriteFile(chunkPath, chunkData, constant.DirPerm)
|
||||
@@ -985,6 +1428,10 @@ func (b *BaseApi) StopChunkUpload(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
if err := removeActiveResumableUploadPart(uploadID); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
|
||||
+587
-246
@@ -1,26 +1,53 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"net/http"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (b *BaseApi) UpdatePanelFirewallPort(c *gin.Context) {
|
||||
if !global.IsMaster {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
OldPort uint `json:"oldPort" validate:"required,min=1,max=65535"`
|
||||
NewPort uint `json:"newPort" validate:"required,min=1,max=65535"`
|
||||
}
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallService.UpdatePanelPort(c.Request.Context(), request.OldPort, request.NewPort); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Load firewall base info
|
||||
// @Accept json
|
||||
// @Param request body dto.OperationWithName true "request"
|
||||
// @Success 200 {object} dto.FirewallBaseInfo
|
||||
// @Success 200 {object} dto.FirewallSubsystemStatus
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/base [post]
|
||||
func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
||||
var req dto.OperationWithName
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
var request dto.OperationWithName
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := firewallService.LoadBaseInfo(req.Name)
|
||||
data, err := firewallService.LoadBaseInfo(request.Name)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -29,311 +56,625 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Page firewall rules
|
||||
// @Accept json
|
||||
// @Param request body dto.RuleSearch true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/search [post]
|
||||
func (b *BaseApi) SearchFirewallRule(c *gin.Context) {
|
||||
var req dto.RuleSearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
total, list, err := firewallService.SearchWithPage(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Items: list,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Operate firewall
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallOperation true "request"
|
||||
// @Success 200
|
||||
// @Param request body dto.FirewallLifecycleOperation true "request"
|
||||
// @Success 200 {object} dto.FirewallLifecycleOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] 防火墙","formatEN":"[operation] firewall"}
|
||||
func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
||||
var req dto.FirewallOperation
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
var request dto.FirewallLifecycleOperation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := firewallService.OperateFirewall(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Create group
|
||||
// @Accept json
|
||||
// @Param request body dto.PortRuleOperate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/port [post]
|
||||
// @x-panel-log {"bodyKeys":["port","strategy"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加端口规则 [strategy] [port]","formatEN":"create port rules [strategy][port]"}
|
||||
func (b *BaseApi) OperatePortRule(c *gin.Context) {
|
||||
var req dto.PortRuleOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := firewallService.OperatePortRule(req, true); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// OperateForwardRule
|
||||
// @Tags Firewall
|
||||
// @Summary Operate forward rule
|
||||
// @Accept json
|
||||
// @Param request body dto.ForwardRuleOperate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/forward [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新端口转发规则","formatEN":"update port forward rules"}
|
||||
func (b *BaseApi) OperateForwardRule(c *gin.Context) {
|
||||
var req dto.ForwardRuleOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := firewallService.OperateForwardRule(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Operate Ip rule
|
||||
// @Accept json
|
||||
// @Param request body dto.AddrRuleOperate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/ip [post]
|
||||
// @x-panel-log {"bodyKeys":["strategy","address"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加 ip 规则 [strategy] [address]","formatEN":"create address rules [strategy][address]"}
|
||||
func (b *BaseApi) OperateIPRule(c *gin.Context) {
|
||||
var req dto.AddrRuleOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := firewallService.OperateAddressRule(req, true); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Batch operate rule
|
||||
// @Accept json
|
||||
// @Param request body dto.BatchRuleOperate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/batch [post]
|
||||
func (b *BaseApi) BatchOperateRule(c *gin.Context) {
|
||||
var req dto.BatchRuleOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := firewallService.BatchOperateRule(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Update rule description
|
||||
// @Accept json
|
||||
// @Param request body dto.UpdateFirewallDescription true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/update/description [post]
|
||||
func (b *BaseApi) UpdateFirewallDescription(c *gin.Context) {
|
||||
var req dto.UpdateFirewallDescription
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := firewallService.UpdateDescription(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Update port rule
|
||||
// @Accept json
|
||||
// @Param request body dto.PortRuleUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/update/port [post]
|
||||
func (b *BaseApi) UpdatePortRule(c *gin.Context) {
|
||||
var req dto.PortRuleUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := firewallService.UpdatePortRule(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Update Ip rule
|
||||
// @Accept json
|
||||
// @Param request body dto.AddrRuleUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/update/addr [post]
|
||||
func (b *BaseApi) UpdateAddrRule(c *gin.Context) {
|
||||
var req dto.AddrRuleUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := firewallService.UpdateAddrRule(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary search iptables filter rules
|
||||
// @Accept json
|
||||
// @Param request body dto.SearchPageWithType true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/filter/rule/search [post]
|
||||
func (b *BaseApi) SearchFilterRules(c *gin.Context) {
|
||||
var req dto.SearchPageWithType
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
total, list, err := iptablesService.Search(req)
|
||||
result, err := firewallService.QueueFirewallOperation(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Items: list,
|
||||
Total: total,
|
||||
})
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Operate iptables filter rule
|
||||
// @Summary Load forwarding base info
|
||||
// @Accept json
|
||||
// @Param request body dto.IptablesRuleOp true "request"
|
||||
// @Success 200
|
||||
// @Success 200 {object} dto.FirewallSubsystemStatus
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/filter/rule/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["operation","chain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] filter规则到 [chain]","formatEN":"[operation] filter rule to [chain]"}
|
||||
func (b *BaseApi) OperateFilterRule(c *gin.Context) {
|
||||
var req dto.IptablesRuleOp
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
// @Router /hosts/firewall/forward/base [post]
|
||||
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) {
|
||||
data, err := forwardingService.LoadBaseInfo(c.Request.Context())
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
if err := iptablesService.OperateRule(req, true); err != nil {
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Page forwarding rules
|
||||
// @Accept json
|
||||
// @Param request body dto.ForwardRuleSearch true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/forward/search [post]
|
||||
func (b *BaseApi) SearchForwardingRules(c *gin.Context) {
|
||||
var request dto.ForwardRuleSearch
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
total, items, err := forwardingService.SearchRules(c.Request.Context(), request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, dto.PageResult{Items: items, Total: total})
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Batch operate iptables filter rules
|
||||
// @Summary Operate forwarding rules
|
||||
// @Accept json
|
||||
// @Param request body dto.IptablesBatchOperate true "request"
|
||||
// @Success 200
|
||||
// @Param request body dto.ForwardRuleOperate true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/filter/rule/batch [post]
|
||||
func (b *BaseApi) BatchOperateFilterRule(c *gin.Context) {
|
||||
var req dto.IptablesBatchOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
// @Router /hosts/firewall/forward/operate [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新端口转发规则","formatEN":"update port forward rules"}
|
||||
func (b *BaseApi) OperateForwardingRules(c *gin.Context) {
|
||||
var request dto.ForwardRuleOperate
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := iptablesService.BatchOperate(req); err != nil {
|
||||
result, err := forwardingService.OperateRules(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Apply/Unload/Init iptables filter
|
||||
// @Summary Enable forwarding
|
||||
// @Accept json
|
||||
// @Param request body dto.IptablesOp true "request"
|
||||
// @Success 200
|
||||
// @Param request body dto.FirewallInitializationTask true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/forward/enable [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"初始化并启用端口转发","formatEN":"initialize and enable port forwarding"}
|
||||
func (b *BaseApi) EnableForwarding(c *gin.Context) {
|
||||
var request dto.FirewallInitializationTask
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := forwardingService.QueueInitialization(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Apply/Unload/Init firewall filter chain
|
||||
// @Accept json
|
||||
// @Param request body dto.FilterChainOperation true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/filter/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] iptables filter 防火墙","formatEN":"[operate] iptables filter firewall"}
|
||||
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 防火墙过滤链","formatEN":"[operate] firewall filter chain"}
|
||||
func (b *BaseApi) OperateFilterChain(c *gin.Context) {
|
||||
var req dto.IptablesOp
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
var request dto.FilterChainOperation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := iptablesService.Operate(req); err != nil {
|
||||
if request.Operate == "init-base" {
|
||||
result, err := firewallService.QueueFilterChainInitialization(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
return
|
||||
}
|
||||
if err := firewallService.OperateFilterChain(request); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, dto.FilterChainOperationResponse{})
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary List unified firewall v2 rules
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleInventory true "request"
|
||||
// @Success 200 {object} dto.FirewallRuleInventoryResponse
|
||||
// @Failure 400 {object} dto.Response
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/search [post]
|
||||
func (b *BaseApi) SearchFirewallRules(c *gin.Context) {
|
||||
var request dto.FirewallRuleInventory
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
inventory, err := firewallService.Inventory(c.Request.Context(), request)
|
||||
if err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, inventory)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Reset firewall rules
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleReset true "request"
|
||||
// @Success 200 {object} dto.FirewallRuleResetResponse
|
||||
// @Failure 400 {object} dto.Response
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/reset [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"重置防火墙规则","formatEN":"reset firewall rules"}
|
||||
func (b *BaseApi) ResetFirewallRules(c *gin.Context) {
|
||||
var request dto.FirewallRuleReset
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallService.Reset(c.Request.Context(), request)
|
||||
if err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Load one provider-native firewall object definition
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallNativeDetail true "request"
|
||||
// @Success 200 {string} string
|
||||
// @Failure 400 {object} dto.Response
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/native/detail [post]
|
||||
func (b *BaseApi) LoadFirewallNativeDetail(c *gin.Context) {
|
||||
var request dto.FirewallNativeDetail
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
info, err := firewallService.LoadFirewallNativeDetail(c.Request.Context(), request)
|
||||
if err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, info)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Queue firewall rule creation
|
||||
// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleCreate true "request"
|
||||
// @Success 200 {object} dto.FirewallRuleCreateResponse
|
||||
// @Failure 400 {object} dto.Response
|
||||
// @Failure 409 {object} dto.Response
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加防火墙规则","formatEN":"create firewall rules"}
|
||||
func (b *BaseApi) CreateFirewallRules(c *gin.Context) {
|
||||
var request dto.FirewallRuleCreate
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallService.Create(c.Request.Context(), request)
|
||||
if err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Queue firewall rule deletion
|
||||
// @Description Deletes non-whitelist rules by scope and instance key. Returns a taskID immediately; results are written to the task log.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleDelete true "request"
|
||||
// @Success 200 {object} dto.FirewallRuleDeleteResponse
|
||||
// @Failure 400 {object} dto.Response
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/delete [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除防火墙规则","formatEN":"delete firewall rules"}
|
||||
func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
|
||||
var request dto.FirewallRuleDelete
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallService.Delete(c.Request.Context(), request)
|
||||
if err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Update a firewall rule
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleUpdate true "request"
|
||||
// @Success 200
|
||||
// @Failure 400 {object} dto.Response
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/update [post]
|
||||
// @x-panel-log {"bodyKeys":["instanceKey"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [instanceKey]","formatEN":"update firewall rule [instanceKey]"}
|
||||
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||
var request dto.FirewallRuleUpdate
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallService.Update(c.Request.Context(), request); err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary load chain status with name
|
||||
// @Summary Reorder a firewall rule
|
||||
// @Accept json
|
||||
// @Param request body dto.OperationWithName true "request"
|
||||
// @Param request body dto.FirewallRuleReorder true "request"
|
||||
// @Success 200
|
||||
// @Failure 400 {object} dto.Response
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/reorder [post]
|
||||
// @x-panel-log {"bodyKeys":["instanceKey"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [instanceKey]","formatEN":"reorder firewall rule [instanceKey]"}
|
||||
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
|
||||
var request dto.FirewallRuleReorder
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallService.Reorder(c.Request.Context(), request); err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
func handleFirewallRuleError(c *gin.Context, err error) {
|
||||
var businessErr buserr.BusinessError
|
||||
isBusinessError := errors.As(err, &businessErr)
|
||||
switch {
|
||||
case errors.Is(err, filter.ErrProtectedRule):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
|
||||
case errors.Is(err, filter.ErrRuleStale):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err)
|
||||
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
|
||||
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||
case isBusinessError && businessErr.Msg == "ErrRecordExist":
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_DUPLICATE", Message: err.Error()})
|
||||
c.Abort()
|
||||
case isBusinessError && businessErr.Msg == "ErrFirewallRuleConflict":
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_CONFLICT", Message: err.Error()})
|
||||
c.Abort()
|
||||
case isBusinessError && businessErr.Msg == "ErrInvalidParams":
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusBadRequest, ErrorCode: "FW_RULE_UNSUPPORTED", Message: err.Error()})
|
||||
c.Abort()
|
||||
default:
|
||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err)
|
||||
}
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Load firewall settings
|
||||
// @Success 200 {object} dto.FirewallSettings
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/settings [get]
|
||||
func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
|
||||
data, err := firewallSettingService.Load(c.Request.Context())
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Create firewall port whitelist rules
|
||||
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallPortWhitelistCreate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/filter/chain/status [post]
|
||||
func (b *BaseApi) LoadChainStatus(c *gin.Context) {
|
||||
var req dto.OperationWithName
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
// @Router /hosts/firewall/settings/whitelist [post]
|
||||
// @x-panel-log {"bodyKeys":["rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建防火墙端口白名单","formatEN":"create firewall port whitelist"}
|
||||
func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) {
|
||||
var request dto.FirewallPortWhitelistCreate
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallSettingService.CreatePortWhitelist(c.Request.Context(), request); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, iptablesService.LoadChainStatus(req))
|
||||
// @Tags Firewall
|
||||
// @Summary Update firewall port whitelist rules
|
||||
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/settings/whitelist/update [post]
|
||||
// @x-panel-log {"bodyKeys":["oldRule","rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"编辑防火墙端口白名单","formatEN":"update firewall port whitelist"}
|
||||
func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
|
||||
var request dto.FirewallPortWhitelistUpdate
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallSettingService.UpdatePortWhitelist(c.Request.Context(), request); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Delete firewall port whitelist rules
|
||||
// @Description Removes whitelist configuration; existing firewall rules are not removed.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallPortWhitelistDelete true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/settings/whitelist/delete [post]
|
||||
// @x-panel-log {"bodyKeys":["rules"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除防火墙端口白名单","formatEN":"delete firewall port whitelist"}
|
||||
func (b *BaseApi) DeleteFirewallPortWhitelist(c *gin.Context) {
|
||||
var request dto.FirewallPortWhitelistDelete
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallSettingService.DeletePortWhitelist(c.Request.Context(), request); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Operate firewall backend
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallBackendOperation true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/settings/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["subsystem","backend","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"防火墙子系统 [subsystem] 后端 [operation] [backend]","formatEN":"[operation] firewall [subsystem] backend [backend]"}
|
||||
func (b *BaseApi) OperateFirewallBackend(c *gin.Context) {
|
||||
var request dto.FirewallBackendOperation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallSettingService.Operate(c.Request.Context(), request); err != nil {
|
||||
var businessErr buserr.BusinessError
|
||||
if errors.As(err, &businessErr) && businessErr.Msg == "ErrFirewallBackendCleanupRequired" {
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_BACKEND_CLEANUP_REQUIRED", Message: err.Error()})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary List Docker port guard status and policies
|
||||
// @Success 200 {object} dto.DockerPortGuardList
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/docker/ports [get]
|
||||
func (b *BaseApi) ListDockerPortGuard(c *gin.Context) {
|
||||
data, err := dockerPortGuardService.LoadOverview(c.Request.Context())
|
||||
if err != nil {
|
||||
handleDockerPortGuardError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary List Docker published ports
|
||||
// @Success 200 {array} dto.DockerPortGuardContainer
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/docker/endpoints [get]
|
||||
func (b *BaseApi) ListDockerPublishedPorts(c *gin.Context) {
|
||||
data, err := dockerPortGuardService.LoadPublishedPorts(c.Request.Context())
|
||||
if err != nil {
|
||||
handleDockerPortGuardError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Operate Docker port guard
|
||||
// @Accept json
|
||||
// @Param request body dto.DockerPortGuardOperation true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/docker/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] Docker 端口防护","formatEN":"[operation] Docker port guard"}
|
||||
func (b *BaseApi) OperateDockerPortGuard(c *gin.Context) {
|
||||
var request dto.DockerPortGuardOperation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if request.Operation == "initialize" {
|
||||
result, err := dockerPortGuardService.QueueInitialization(request)
|
||||
if err != nil {
|
||||
handleDockerPortGuardError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
return
|
||||
}
|
||||
if err := dockerPortGuardService.Operate(c.Request.Context(), request); err != nil {
|
||||
handleDockerPortGuardError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Delete Docker port guard policies
|
||||
// @Accept json
|
||||
// @Param request body dto.DockerPortGuardPolicyBatchDelete true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/docker/policies/delete/batch [post]
|
||||
// @x-panel-log {"bodyKeys":["uuids"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 Docker 端口防护策略 [uuids]","formatEN":"delete Docker port guard policies [uuids]"}
|
||||
func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
|
||||
var request dto.DockerPortGuardPolicyBatchDelete
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := dockerPortGuardService.DeletePolicies(request)
|
||||
if err != nil {
|
||||
handleDockerPortGuardError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Batch upsert Docker port guard policies
|
||||
// @Accept json
|
||||
// @Param request body dto.DockerPortGuardPolicyBatch true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/docker/policies/batch [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量更新 Docker 端口防护策略","formatEN":"batch update Docker port guard policies"}
|
||||
func (b *BaseApi) UpsertDockerPortGuardPolicies(c *gin.Context) {
|
||||
var request dto.DockerPortGuardPolicyBatch
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := dockerPortGuardService.UpsertPolicies(request)
|
||||
if err != nil {
|
||||
handleDockerPortGuardError(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
func handleDockerPortGuardError(c *gin.Context, err error) {
|
||||
var businessErr buserr.BusinessError
|
||||
if errors.As(err, &businessErr) {
|
||||
code, errorCode := http.StatusInternalServerError, ""
|
||||
switch businessErr.Msg {
|
||||
case "ErrDockerIptablesChainUnavailable":
|
||||
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE"
|
||||
case "ErrDockerNftablesChainUnavailable":
|
||||
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_NFTABLES_CHAIN_UNAVAILABLE"
|
||||
case "ErrInvalidParams":
|
||||
code, errorCode = http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID"
|
||||
case "ErrDockerFailed":
|
||||
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE"
|
||||
}
|
||||
if errorCode != "" {
|
||||
c.JSON(http.StatusOK, dto.Response{Code: code, ErrorCode: errorCode, Message: err.Error()})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
if errors.Is(err, docker.ErrUnavailable) {
|
||||
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE", "ErrDockerFailed", err)
|
||||
return
|
||||
}
|
||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary List firewall rule backups
|
||||
// @Param subsystem query string false "Firewall subsystem" Enums(system,forwarding,docker) default(system)
|
||||
// @Success 200 {object} dto.FirewallRuleBackups
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/backups [get]
|
||||
func (b *BaseApi) ListFirewallRuleBackups(c *gin.Context) {
|
||||
result, err := firewallService.ListRuleBackups(c.Request.Context(), c.DefaultQuery("subsystem", "system"))
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Initialize, repair or bind one firewall address family
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallFamilyOperation true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/family/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["subsystem","family","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] [subsystem] [family] 防火墙链","formatEN":"[operation] [subsystem] [family] firewall chains"}
|
||||
func (b *BaseApi) OperateFirewallFamily(c *gin.Context) {
|
||||
var request dto.FirewallFamilyOperation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallSettingService.OperateFamily(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Update firewall IPv6 support
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallIPv6Operation true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/settings/ipv6 [post]
|
||||
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"设置防火墙 IPv6 支持为 [status]","formatEN":"Set firewall IPv6 support to [status]"}
|
||||
func (b *BaseApi) OperateFirewallIPv6(c *gin.Context) {
|
||||
var request dto.FirewallIPv6Operation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallSettingService.OperateIPv6(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
+8
-16
@@ -3,9 +3,8 @@ package v2
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -17,27 +16,20 @@ import (
|
||||
// @Security Timestamp
|
||||
// @Router /ai/gpu/load [get]
|
||||
func (b *BaseApi) LoadGpuInfo(c *gin.Context) {
|
||||
ok, client := gpu.New()
|
||||
ok, client := accelerator.New()
|
||||
if ok {
|
||||
info, err := client.LoadGpuInfo()
|
||||
snapshot, err := client.Collect(c.Request.Context())
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, info)
|
||||
return
|
||||
}
|
||||
xpuOK, xpuClient := xpu.New()
|
||||
if xpuOK {
|
||||
info, err := xpuClient.LoadGpuInfo()
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
if warning := snapshot.Warning(); warning != nil {
|
||||
global.LOG.Warnf("load realtime accelerator data partially failed, err: %v", warning)
|
||||
}
|
||||
helper.SuccessWithData(c, info)
|
||||
helper.SuccessWithData(c, &snapshot.Info)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, &common.GpuInfo{})
|
||||
helper.SuccessWithData(c, &accelerator.Info{})
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
|
||||
@@ -30,6 +30,26 @@ func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
||||
ctx.Abort()
|
||||
}
|
||||
|
||||
func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey string, err error) {
|
||||
res := dto.Response{
|
||||
Code: code,
|
||||
ErrorCode: businessCode,
|
||||
Message: i18n.GetMsgWithDetail(msgKey, err.Error()),
|
||||
}
|
||||
ctx.JSON(http.StatusOK, res)
|
||||
ctx.Abort()
|
||||
}
|
||||
|
||||
func ErrorWithDetailAndData(ctx *gin.Context, code int, msgKey string, err error, data interface{}) {
|
||||
res := dto.Response{
|
||||
Code: code,
|
||||
Data: data,
|
||||
}
|
||||
res.Message = i18n.GetMsgWithDetail(msgKey, err.Error())
|
||||
ctx.JSON(http.StatusOK, res)
|
||||
ctx.Abort()
|
||||
}
|
||||
|
||||
func InternalServer(ctx *gin.Context, err error) {
|
||||
ErrorWithDetail(ctx, http.StatusInternalServerError, "ErrInternalServer", err)
|
||||
}
|
||||
|
||||
+10
-23
@@ -1,13 +1,9 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
// @Tags Monitor
|
||||
@@ -32,14 +28,19 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags Monitor
|
||||
// @Summary Clean monitor data
|
||||
// @Summary Clean host or GPU monitor data
|
||||
// @Param request body dto.MonitorClean true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/monitor/clean [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空监控数据","formatEN":"clean monitor datas"}
|
||||
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 [type] 监控数据","formatEN":"clean [type] monitoring data"}
|
||||
func (b *BaseApi) CleanMonitor(c *gin.Context) {
|
||||
if err := monitorService.CleanData(); err != nil {
|
||||
var req dto.MonitorClean
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := monitorService.CleanData(req.Type); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
@@ -91,14 +92,7 @@ func (b *BaseApi) UpdateMonitorSetting(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/monitor/netoptions [get]
|
||||
func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
||||
netStat, _ := net.IOCounters(true)
|
||||
var options []string
|
||||
options = append(options, "all")
|
||||
for _, net := range netStat {
|
||||
options = append(options, net.Name)
|
||||
}
|
||||
sort.Strings(options)
|
||||
helper.SuccessWithData(c, options)
|
||||
helper.SuccessWithData(c, monitorService.LoadNetworkOptions())
|
||||
}
|
||||
|
||||
// @Tags Monitor
|
||||
@@ -108,12 +102,5 @@ func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/monitor/iooptions [get]
|
||||
func (b *BaseApi) GetIOOptions(c *gin.Context) {
|
||||
diskStat, _ := disk.IOCounters()
|
||||
var options []string
|
||||
options = append(options, "all")
|
||||
for _, net := range diskStat {
|
||||
options = append(options, net.Name)
|
||||
}
|
||||
sort.Strings(options)
|
||||
helper.SuccessWithData(c, options)
|
||||
helper.SuccessWithData(c, monitorService.LoadIOOptions())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (b *BaseApi) LoadVLLMMonitor(c *gin.Context) {
|
||||
var req dto.MonitorVLLMSearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := monitorService.LoadVLLMMonitorData(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
func (b *BaseApi) LoadVLLMCurrent(c *gin.Context) {
|
||||
var req dto.MonitorVLLMCurrent
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := monitorService.LoadVLLMCurrent(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
func (b *BaseApi) CleanVLLMMonitor(c *gin.Context) {
|
||||
var req dto.MonitorVLLMClean
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := monitorService.CleanVLLMMonitor(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
@@ -169,7 +169,7 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /runtimes/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [id]","formatEN":"Operate runtime [id]"}
|
||||
// @x-panel-log {"bodyKeys":["ID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ID","isList":false,"db":"runtimes","output_column":"name","output_value":"name"}],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"}
|
||||
func (b *BaseApi) OperateRuntime(c *gin.Context) {
|
||||
var req request.RuntimeOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
|
||||
@@ -83,6 +83,7 @@ func (b *BaseApi) CreateRootCert(c *gin.Context) {
|
||||
}
|
||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if err := sshService.CreateRootCert(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
@@ -107,6 +108,7 @@ func (b *BaseApi) EditRootCert(c *gin.Context) {
|
||||
}
|
||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if err := sshService.EditRootCert(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
+153
-31
@@ -1,11 +1,14 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
@@ -19,29 +22,35 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// @Tags Terminal
|
||||
// @Summary Ws local terminal
|
||||
// @Param command query string false "command"
|
||||
// @Param session query string false "session id to reattach"
|
||||
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/terminal/local [get]
|
||||
func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
|
||||
b.runSSHSession(c, loadLocalConn, c.DefaultQuery("command", ""))
|
||||
b.runSSHSession(c, "local", loadLocalConn, c.DefaultQuery("command", ""))
|
||||
}
|
||||
|
||||
// @Tags Terminal
|
||||
// @Summary Ws host SSH
|
||||
// @Param id query integer false "id"
|
||||
// @Param command query string false "command"
|
||||
// @Param session query string false "session id to reattach"
|
||||
// @Param title query string false "session title shown in the session list"
|
||||
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/terminal/ssh [get]
|
||||
func (b *BaseApi) WsHostSSH(c *gin.Context) {
|
||||
b.runSSHSession(c, func() (*ssh.SSHClient, error) {
|
||||
b.runSSHSession(c, "ssh", func() (*ssh.SSHClient, error) {
|
||||
hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0"))
|
||||
if hostID <= 0 {
|
||||
return nil, errors.New("missing host id")
|
||||
@@ -65,26 +74,33 @@ func (b *BaseApi) WsContainerTerminal(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
slave, err := loadContainerTerminalCommand(c)
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
defer slave.Close()
|
||||
|
||||
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
|
||||
if wshandleError(wsConn, err) {
|
||||
identity, ok := loadTerminalIdentity(c)
|
||||
if !ok {
|
||||
_ = wshandleError(wsConn, errors.New("missing terminal identity"))
|
||||
return
|
||||
}
|
||||
|
||||
quitChan := make(chan bool, 3)
|
||||
tty.Start(quitChan)
|
||||
go slave.Wait(quitChan)
|
||||
opts := terminal.SessionOptions{
|
||||
Identity: identity,
|
||||
Kind: "container",
|
||||
Target: containerTerminalTarget(c),
|
||||
Cols: cols,
|
||||
Rows: rows,
|
||||
}
|
||||
if err := terminal.ServeCommand(wsConn, strings.TrimSpace(c.Query("session")), opts, func() (*terminal.LocalCommand, error) {
|
||||
return loadContainerTerminalCommand(c)
|
||||
}); err != nil {
|
||||
_ = wshandleError(wsConn, err)
|
||||
}
|
||||
}
|
||||
|
||||
<-quitChan
|
||||
|
||||
global.LOG.Info("websocket finished")
|
||||
closeTerminalConn(wsConn)
|
||||
func containerTerminalTarget(c *gin.Context) string {
|
||||
query := c.Request.URL.Query()
|
||||
for _, key := range []string{"cols", "rows", "session", "terminalRevalidate"} {
|
||||
query.Del(key)
|
||||
}
|
||||
sum := sha256.Sum256([]byte(query.Encode()))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
|
||||
@@ -115,32 +131,138 @@ func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
|
||||
return wsConn, cols, rows, true
|
||||
}
|
||||
|
||||
func (b *BaseApi) runSSHSession(c *gin.Context, connect func() (*ssh.SSHClient, error), command string) {
|
||||
func (b *BaseApi) runSSHSession(c *gin.Context, kind string, connect func() (*ssh.SSHClient, error), command string) {
|
||||
wsConn, cols, rows, ok := prepareTerminalSession(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
client, clientErr := connect()
|
||||
if wshandleError(wsConn, errors.WithMessage(clientErr, "failed to set up the connection. Please check the host information")) {
|
||||
identity, ok := loadTerminalIdentity(c)
|
||||
if !ok {
|
||||
_ = wshandleError(wsConn, errors.New("missing terminal identity"))
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, command)
|
||||
if wshandleError(wsConn, err) {
|
||||
hostID := 0
|
||||
if kind == "ssh" {
|
||||
hostID, _ = strconv.Atoi(c.DefaultQuery("id", "0"))
|
||||
}
|
||||
opts := terminal.SessionOptions{
|
||||
Identity: identity,
|
||||
Kind: kind,
|
||||
Title: sanitizeTerminalTitle(c.Query("title")),
|
||||
Persistent: c.Query("terminalPersistent") == "true",
|
||||
HostID: uint(max(hostID, 0)),
|
||||
Cols: cols,
|
||||
Rows: rows,
|
||||
InitCmd: command,
|
||||
}
|
||||
err := terminal.Serve(wsConn, strings.TrimSpace(c.Query("session")), opts, func() (*gossh.Client, error) {
|
||||
client, err := connect()
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to set up the connection. Please check the host information")
|
||||
}
|
||||
return client.Client, nil
|
||||
})
|
||||
if err != nil {
|
||||
_ = wshandleError(wsConn, err)
|
||||
}
|
||||
}
|
||||
|
||||
// @Tags Terminal
|
||||
// @Summary List the caller's live terminal sessions
|
||||
// @Success 200 {array} terminal.Info
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/terminal/sessions/search [post]
|
||||
func (b *BaseApi) SearchTerminalSessions(c *gin.Context) {
|
||||
identity, ok := loadTerminalIdentity(c)
|
||||
if !ok {
|
||||
helper.BadRequest(c, errors.New("missing terminal identity"))
|
||||
return
|
||||
}
|
||||
defer sws.Close()
|
||||
helper.SuccessWithData(c, terminal.List(identity))
|
||||
}
|
||||
|
||||
quitChan := make(chan bool, 3)
|
||||
sws.Start(quitChan)
|
||||
go sws.Wait(quitChan)
|
||||
// @Tags Terminal
|
||||
// @Summary Close a terminal session
|
||||
// @Accept json
|
||||
// @Param request body dto.TerminalSessionClose true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/terminal/sessions/close [post]
|
||||
func (b *BaseApi) CloseTerminalSession(c *gin.Context) {
|
||||
var req dto.TerminalSessionClose
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
identity, ok := loadTerminalIdentity(c)
|
||||
if !ok {
|
||||
helper.BadRequest(c, errors.New("missing terminal identity"))
|
||||
return
|
||||
}
|
||||
if err := terminal.CloseSession(req.ID, identity); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
<-quitChan
|
||||
// @Tags Terminal
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/terminal/sessions/closeAll [post]
|
||||
func (b *BaseApi) CloseAllTerminalSessions(c *gin.Context) {
|
||||
identity, ok := loadTerminalIdentity(c)
|
||||
if !ok {
|
||||
helper.BadRequest(c, errors.New("missing terminal identity"))
|
||||
return
|
||||
}
|
||||
terminal.Revoke("auth_session", identity.UserID, identity.AuthSessionID)
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
closeTerminalConn(wsConn)
|
||||
func (b *BaseApi) RevokeTerminalSessions(c *gin.Context) {
|
||||
var req dto.TerminalSessionRevoke
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if (req.Scope == "auth_session" && (req.UserID == "" || req.AuthSessionID == "")) ||
|
||||
(req.Scope == "user" && req.UserID == "") {
|
||||
helper.BadRequest(c, errors.New("missing terminal revocation identity"))
|
||||
return
|
||||
}
|
||||
terminal.Revoke(req.Scope, req.UserID, req.AuthSessionID)
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
func loadTerminalIdentity(c *gin.Context) (terminal.Identity, bool) {
|
||||
identity := terminal.Identity{
|
||||
UserID: strings.TrimSpace(c.GetHeader(terminal.HeaderUserID)),
|
||||
AuthSessionID: strings.TrimSpace(c.GetHeader(terminal.HeaderAuthSessionID)),
|
||||
}
|
||||
if value := c.GetHeader(terminal.HeaderAuthLeaseUntil); value != "" {
|
||||
millis, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || millis <= 0 {
|
||||
return terminal.Identity{}, false
|
||||
}
|
||||
identity.AuthLeaseUntil = time.UnixMilli(millis)
|
||||
if maximum := time.Now().Add(90 * time.Second); identity.AuthLeaseUntil.After(maximum) {
|
||||
identity.AuthLeaseUntil = maximum
|
||||
}
|
||||
}
|
||||
return identity, identity.Valid()
|
||||
}
|
||||
|
||||
// sanitizeTerminalTitle keeps the title a short single line.
|
||||
func sanitizeTerminalTitle(title string) string {
|
||||
title = strings.Join(strings.Fields(title), " ")
|
||||
if r := []rune(title); len(r) > 64 {
|
||||
title = string(r[:64])
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
func closeTerminalConn(wsConn *websocket.Conn) {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (b *BaseApi) TerminalCapabilities(c *gin.Context) {
|
||||
helper.SuccessWithData(c, gin.H{"apiKeyLeaseVersion": 1})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+95
-34
@@ -162,16 +162,25 @@ type AgentWebsiteBindReq struct {
|
||||
}
|
||||
|
||||
type AgentModelConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
Model string `json:"model" validate:"required"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
Model string `json:"model" validate:"required"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
Metadata []AgentModelMetadata `json:"metadata" validate:"dive"`
|
||||
}
|
||||
|
||||
type AgentModelConfig struct {
|
||||
AccountID uint `json:"accountId"`
|
||||
Model string `json:"model"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
AccountID uint `json:"accountId"`
|
||||
Model string `json:"model"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
Metadata []AgentModelMetadata `json:"metadata"`
|
||||
}
|
||||
|
||||
type AgentModelMetadata struct {
|
||||
Model string `json:"model" validate:"required"`
|
||||
InputMode string `json:"inputMode" validate:"required,oneof=auto text image"`
|
||||
ContextWindow int `json:"contextWindow" validate:"min=0"`
|
||||
MaxTokens int `json:"maxTokens" validate:"min=0"`
|
||||
}
|
||||
|
||||
type AgentHermesChatSessionItem struct {
|
||||
@@ -322,29 +331,31 @@ type AgentAccountModelDeleteReq struct {
|
||||
}
|
||||
|
||||
type AgentAccountCreateReq struct {
|
||||
Provider string `json:"provider" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
APIKey string `json:"apiKey" validate:"required"`
|
||||
RememberAPIKey bool `json:"rememberApiKey"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
Models []AgentAccountModel `json:"models"`
|
||||
APIType string `json:"apiType" validate:"required"`
|
||||
AuthMode string `json:"authMode"`
|
||||
VerifyModel string `json:"verifyModel"`
|
||||
Remark string `json:"remark"`
|
||||
Provider string `json:"provider" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
APIKey string `json:"apiKey" validate:"required"`
|
||||
RememberAPIKey bool `json:"rememberApiKey"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
Models []AgentAccountModel `json:"models"`
|
||||
APIType string `json:"apiType" validate:"required"`
|
||||
AuthMode string `json:"authMode"`
|
||||
VerifyModel string `json:"verifyModel"`
|
||||
ValidateAvailability *bool `json:"validateAvailability"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type AgentAccountUpdateReq struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
APIKey string `json:"apiKey" validate:"required"`
|
||||
RememberAPIKey bool `json:"rememberApiKey"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
APIType string `json:"apiType" validate:"required"`
|
||||
AuthMode string `json:"authMode"`
|
||||
VerifyModel string `json:"verifyModel"`
|
||||
Remark string `json:"remark"`
|
||||
SyncAgents bool `json:"syncAgents"`
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
APIKey string `json:"apiKey" validate:"required"`
|
||||
RememberAPIKey bool `json:"rememberApiKey"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
APIType string `json:"apiType" validate:"required"`
|
||||
AuthMode string `json:"authMode"`
|
||||
VerifyModel string `json:"verifyModel"`
|
||||
ValidateAvailability *bool `json:"validateAvailability"`
|
||||
Remark string `json:"remark"`
|
||||
SyncAgents bool `json:"syncAgents"`
|
||||
}
|
||||
|
||||
type AgentAccountVerifyReq struct {
|
||||
@@ -363,6 +374,8 @@ type AgentAccountDeleteReq struct {
|
||||
type AgentAccountSearch struct {
|
||||
PageInfo
|
||||
Provider string `json:"provider"`
|
||||
APIType string `json:"apiType"`
|
||||
TextOnly bool `json:"textOnly"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
@@ -394,11 +407,13 @@ type ProviderModelInfo struct {
|
||||
}
|
||||
|
||||
type ProviderAPIInfo struct {
|
||||
APIType string `json:"apiType"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
EditableBaseURL bool `json:"editableBaseUrl"`
|
||||
DefaultAuthMode string `json:"defaultAuthMode"`
|
||||
AuthModes []string `json:"authModes"`
|
||||
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 {
|
||||
@@ -598,6 +613,52 @@ type AgentPluginStatus struct {
|
||||
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 {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
@@ -678,9 +739,9 @@ type AgentSecurityConfig struct {
|
||||
|
||||
type AgentOtherConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
UserTimezone string `json:"userTimezone" validate:"required"`
|
||||
UserTimezone string `json:"userTimezone"`
|
||||
BrowserEnabled bool `json:"browserEnabled"`
|
||||
NPMRegistry string `json:"npmRegistry" validate:"required"`
|
||||
NPMRegistry string `json:"npmRegistry"`
|
||||
DashboardUsername string `json:"dashboardUsername"`
|
||||
DashboardPassword string `json:"dashboardPassword"`
|
||||
}
|
||||
|
||||
+35
-18
@@ -21,6 +21,7 @@ type AlertBase struct {
|
||||
}
|
||||
|
||||
type PushAlert struct {
|
||||
Result string `json:"result,omitempty"`
|
||||
TaskName string `json:"taskName"`
|
||||
AlertType string `json:"alertType"`
|
||||
EntryID uint `json:"entryID"`
|
||||
@@ -53,6 +54,7 @@ type AlertDTO struct {
|
||||
Method string `json:"method"`
|
||||
Title string `json:"title"`
|
||||
Project string `json:"project"`
|
||||
TaskName string `json:"taskName,omitempty"`
|
||||
Status string `json:"status"`
|
||||
SendCount uint `json:"sendCount"`
|
||||
AdvancedParams string `json:"advancedParams"`
|
||||
@@ -113,8 +115,10 @@ type DiskDTO struct {
|
||||
|
||||
type AlertLogSearch struct {
|
||||
PageInfo
|
||||
Count uint `json:"count"`
|
||||
Status string `json:"status"`
|
||||
Count uint `json:"count"`
|
||||
Status string `json:"status"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
}
|
||||
|
||||
type AlertLogDTO struct {
|
||||
@@ -149,16 +153,25 @@ type AlertLog struct {
|
||||
}
|
||||
|
||||
type AlertDetail struct {
|
||||
LicenseId string `json:"licenseId"`
|
||||
Type string `json:"type"`
|
||||
SubType string `json:"subType"`
|
||||
Title string `json:"title"`
|
||||
Method string `json:"method"`
|
||||
LicenseCode string `json:"licenseCode"`
|
||||
DeviceId string `json:"deviceId"`
|
||||
Project string `json:"project"`
|
||||
Params []Param `json:"params"`
|
||||
Phone string `json:"phone"`
|
||||
LicenseId string `json:"licenseId"`
|
||||
Type string `json:"type"`
|
||||
SubType string `json:"subType"`
|
||||
Title string `json:"title"`
|
||||
Method string `json:"method"`
|
||||
LicenseCode string `json:"licenseCode"`
|
||||
DeviceId string `json:"deviceId"`
|
||||
Project string `json:"project"`
|
||||
Params []Param `json:"params"`
|
||||
Phone string `json:"phone"`
|
||||
Task *AlertTaskMetadata `json:"task,omitempty"`
|
||||
}
|
||||
|
||||
type AlertTaskMetadata struct {
|
||||
AlertID uint `json:"alertId"`
|
||||
Type string `json:"type"`
|
||||
Quota string `json:"quota"`
|
||||
QuotaType string `json:"quotaType"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
type AlertRule struct {
|
||||
@@ -293,15 +306,19 @@ type OfflineQueryRequest struct {
|
||||
}
|
||||
|
||||
type AlertConfigUpdate struct {
|
||||
ID uint `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Config string `json:"config"`
|
||||
DisplayName string `json:"displayName"`
|
||||
ID uint `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Config string `json:"config"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Revision *time.Time `json:"revision"`
|
||||
}
|
||||
|
||||
type AlertConfigTest struct {
|
||||
ID uint `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Config string `json:"config"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Sender string `json:"sender"`
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package dto
|
||||
|
||||
const AlertCustomWebhookSchemaVersion = 1
|
||||
|
||||
type AlertConfigStatusUpdate struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Status string `json:"status" validate:"required,oneof=Enable Disable"`
|
||||
}
|
||||
|
||||
type AlertCustomWebhookSecretMutation struct {
|
||||
Action string `json:"action,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type AlertCustomWebhookURL struct {
|
||||
AlertCustomWebhookSecretMutation
|
||||
Configured bool `json:"configured"`
|
||||
Masked string `json:"masked,omitempty"`
|
||||
}
|
||||
|
||||
type AlertCustomWebhookBody struct {
|
||||
Type string `json:"type"`
|
||||
Template string `json:"template,omitempty"`
|
||||
Fields []AlertCustomWebhookFormField `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
type AlertCustomWebhookFormField struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type AlertCustomWebhookHeader struct {
|
||||
UID string `json:"uid"`
|
||||
Key string `json:"key"`
|
||||
Secret bool `json:"secret"`
|
||||
Action string `json:"action,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Configured bool `json:"configured,omitempty"`
|
||||
Masked string `json:"masked,omitempty"`
|
||||
}
|
||||
|
||||
type AlertCustomWebhookConfig struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
State string `json:"state,omitempty"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Preset string `json:"preset"`
|
||||
Method string `json:"method"`
|
||||
URL AlertCustomWebhookURL `json:"url"`
|
||||
Body AlertCustomWebhookBody `json:"body"`
|
||||
Headers []AlertCustomWebhookHeader `json:"headers"`
|
||||
}
|
||||
|
||||
type AlertCustomWebhookSecretConfig struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
}
|
||||
|
||||
type AlertCustomWebhookResolvedConfig struct {
|
||||
SchemaVersion int
|
||||
DisplayName string
|
||||
Preset string
|
||||
Method string
|
||||
URL string
|
||||
Body AlertCustomWebhookBody
|
||||
Headers []AlertCustomWebhookResolvedHeader
|
||||
}
|
||||
|
||||
type AlertCustomWebhookResolvedHeader struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
type AlertConfigTestResult struct {
|
||||
Success bool `json:"success"`
|
||||
StatusCode int `json:"statusCode,omitempty"`
|
||||
Duration int64 `json:"duration,omitempty"` // milliseconds
|
||||
Message string `json:"message,omitempty"`
|
||||
Response string `json:"response,omitempty"`
|
||||
}
|
||||
@@ -2,7 +2,8 @@ package dto
|
||||
|
||||
type SearchWithPage struct {
|
||||
PageInfo
|
||||
Info string `json:"info"`
|
||||
Info string `json:"info"`
|
||||
ExcludeAppStore bool `json:"excludeAppStore"`
|
||||
}
|
||||
|
||||
type SearchPageWithType struct {
|
||||
|
||||
@@ -6,9 +6,10 @@ type PageResult struct {
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
Code int `json:"code"`
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
|
||||
@@ -300,6 +300,7 @@ type ComposeInfo struct {
|
||||
ConfigFile string `json:"configFile"`
|
||||
Workdir string `json:"workdir"`
|
||||
ComposeFileExists bool `json:"composeFileExists"`
|
||||
IsPinned bool `json:"isPinned"`
|
||||
Path string `json:"path"`
|
||||
Containers []ComposeContainer `json:"containers"`
|
||||
Env string `json:"env"`
|
||||
@@ -314,6 +315,7 @@ type ComposeContainer struct {
|
||||
type ComposeCreate struct {
|
||||
TaskID string `json:"taskID"`
|
||||
Name string `json:"name"`
|
||||
DirName string `json:"dirName"`
|
||||
From string `json:"from" validate:"required,oneof=edit path template"`
|
||||
File string `json:"file"`
|
||||
Path string `json:"path"`
|
||||
@@ -337,6 +339,10 @@ type ComposeUpdate struct {
|
||||
Env string `json:"env"`
|
||||
ForcePull bool `json:"forcePull"`
|
||||
}
|
||||
type ComposePin struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
IsPinned bool `json:"isPinned"`
|
||||
}
|
||||
type ComposeLogClean struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Path string `json:"path" validate:"required"`
|
||||
|
||||
@@ -51,9 +51,10 @@ type CronjobOperate struct {
|
||||
Secret string `json:"secret"`
|
||||
Args string `json:"args"`
|
||||
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertMethod string `json:"alertMethod"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertMethod string `json:"alertMethod"`
|
||||
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
|
||||
|
||||
CleanLogConfig
|
||||
}
|
||||
@@ -126,7 +127,8 @@ type CronjobInfo struct {
|
||||
Secret string `json:"secret"`
|
||||
Args string `json:"args"`
|
||||
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTriggerMode string `json:"alertTriggerMode"`
|
||||
}
|
||||
|
||||
type CronjobImport struct {
|
||||
@@ -169,9 +171,10 @@ type CronjobTrans struct {
|
||||
SourceAccounts []string `json:"sourceAccounts"`
|
||||
DownloadAccount string `json:"downloadAccount"`
|
||||
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertMethod string `json:"alertMethod"`
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertMethod string `json:"alertMethod"`
|
||||
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
|
||||
}
|
||||
type TransHelper struct {
|
||||
Name string `json:"name"`
|
||||
@@ -197,6 +200,7 @@ type SearchRecord struct {
|
||||
|
||||
type Record struct {
|
||||
ID uint `json:"id"`
|
||||
CronjobID uint `json:"cronjobID"`
|
||||
TaskID string `json:"taskID"`
|
||||
StartTime string `json:"startTime"`
|
||||
Records string `json:"records"`
|
||||
|
||||
@@ -121,6 +121,7 @@ type DashboardCurrent struct {
|
||||
NetBytesRecv uint64 `json:"netBytesRecv"`
|
||||
|
||||
GPUData []GPUInfo `json:"gpuData"`
|
||||
NPUData []NPUInfo `json:"npuData"`
|
||||
XPUData []XPUInfo `json:"xpuData"`
|
||||
|
||||
TopCPUItems []Process `json:"topCPUItems"`
|
||||
@@ -156,8 +157,12 @@ type DiskInfo struct {
|
||||
}
|
||||
|
||||
type GPUInfo struct {
|
||||
Type string `json:"type"`
|
||||
Index uint `json:"index"`
|
||||
NPUIndex uint `json:"npuIndex"`
|
||||
ChipIndex uint `json:"chipIndex"`
|
||||
ProductName string `json:"productName"`
|
||||
BusID string `json:"busID"`
|
||||
GPUUtil string `json:"gpuUtil"`
|
||||
Temperature string `json:"temperature"`
|
||||
PerformanceState string `json:"performanceState"`
|
||||
@@ -170,6 +175,27 @@ type GPUInfo struct {
|
||||
FanSpeed string `json:"fanSpeed"`
|
||||
}
|
||||
|
||||
type NPUInfo struct {
|
||||
Type string `json:"type"`
|
||||
Index uint `json:"index"`
|
||||
NPUIndex uint `json:"npuIndex"`
|
||||
ChipIndex uint `json:"chipIndex"`
|
||||
ProductName string `json:"productName"`
|
||||
BusID string `json:"busID"`
|
||||
Health string `json:"health"`
|
||||
Temperature string `json:"temperature"`
|
||||
PowerDraw string `json:"powerDraw"`
|
||||
AICore string `json:"aiCore"`
|
||||
MemUsed string `json:"memUsed"`
|
||||
MemTotal string `json:"memTotal"`
|
||||
MemoryUsed string `json:"memoryUsed"`
|
||||
MemoryTotal string `json:"memoryTotal"`
|
||||
HBMUsed string `json:"hbmUsed"`
|
||||
HBMTotal string `json:"hbmTotal"`
|
||||
HugepagesUsed string `json:"hugepagesUsed"`
|
||||
HugepagesTotal string `json:"hugepagesTotal"`
|
||||
}
|
||||
|
||||
type AppLauncher struct {
|
||||
Key string `json:"key"`
|
||||
Type string `json:"type"`
|
||||
@@ -202,11 +228,13 @@ type LauncherOption struct {
|
||||
}
|
||||
|
||||
type XPUInfo struct {
|
||||
DeviceID int `json:"deviceID"`
|
||||
DeviceName string `json:"deviceName"`
|
||||
Memory string `json:"memory"`
|
||||
Temperature string `json:"temperature"`
|
||||
MemoryUsed string `json:"memoryUsed"`
|
||||
Power string `json:"power"`
|
||||
MemoryUtil string `json:"memoryUtil"`
|
||||
DeviceID int `json:"deviceID"`
|
||||
DeviceName string `json:"deviceName"`
|
||||
PciBdfAddress string `json:"pciBdfAddress"`
|
||||
Memory string `json:"memory"`
|
||||
Temperature string `json:"temperature"`
|
||||
GPUUtil string `json:"gpuUtil"`
|
||||
MemoryUsed string `json:"memoryUsed"`
|
||||
Power string `json:"power"`
|
||||
MemoryUtil string `json:"memoryUtil"`
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ type DBBaseInfo struct {
|
||||
Port int64 `json:"port"`
|
||||
}
|
||||
|
||||
type RedisCliInstall struct {
|
||||
TaskID string `json:"taskID" validate:"omitempty,uuid"`
|
||||
}
|
||||
|
||||
// mysql
|
||||
type MysqlDBSearch struct {
|
||||
PageInfo
|
||||
|
||||
+324
-84
@@ -1,113 +1,353 @@
|
||||
package dto
|
||||
|
||||
type FirewallBaseInfo struct {
|
||||
Name string `json:"name"`
|
||||
IsExist bool `json:"isExist"`
|
||||
IsActive bool `json:"isActive"`
|
||||
IsInit bool `json:"isInit"`
|
||||
IsBind bool `json:"isBind"`
|
||||
Version string `json:"version"`
|
||||
PingStatus string `json:"pingStatus"`
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||
)
|
||||
|
||||
type FirewallSubsystemStatus struct {
|
||||
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||
Name string `json:"name"`
|
||||
Backend string `json:"backend"`
|
||||
ConflictBackend string `json:"conflictBackend,omitempty"`
|
||||
IsExist bool `json:"isExist"`
|
||||
IsActive bool `json:"isActive"`
|
||||
IsInit bool `json:"isInit"`
|
||||
IsBind bool `json:"isBind"`
|
||||
Version string `json:"version"`
|
||||
PingStatus string `json:"pingStatus"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
|
||||
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
||||
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
||||
}
|
||||
|
||||
type RuleSearch struct {
|
||||
PageInfo
|
||||
Info string `json:"info"`
|
||||
Status string `json:"status"`
|
||||
Strategy string `json:"strategy"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
}
|
||||
|
||||
type FirewallOperation struct {
|
||||
type FirewallLifecycleOperation struct {
|
||||
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
|
||||
WithDockerRestart bool `json:"withDockerRestart"`
|
||||
}
|
||||
|
||||
type PortRuleOperate struct {
|
||||
ID uint `json:"id"`
|
||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
||||
Chain string `json:"chain"`
|
||||
Address string `json:"address"`
|
||||
Port string `json:"port" validate:"required"`
|
||||
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
|
||||
Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
|
||||
|
||||
Description string `json:"description"`
|
||||
type FirewallLifecycleOperationResponse struct {
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Queued bool `json:"queued"`
|
||||
}
|
||||
|
||||
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 FirewallBackendOption struct {
|
||||
Name string `json:"name"`
|
||||
Installed bool `json:"installed"`
|
||||
Active bool `json:"active"`
|
||||
Initialized bool `json:"initialized"`
|
||||
Bound bool `json:"bound"`
|
||||
Supported bool `json:"supported"`
|
||||
SupportReason string `json:"supportReason,omitempty"`
|
||||
Implementation string `json:"implementation,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
||||
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
||||
}
|
||||
|
||||
type UpdateFirewallDescription struct {
|
||||
Type string `json:"type"`
|
||||
Chain string `json:"chain"`
|
||||
SrcIP string `json:"srcIP"`
|
||||
DstIP string `json:"dstIP"`
|
||||
SrcPort string `json:"srcPort"`
|
||||
DstPort string `json:"dstPort"`
|
||||
Protocol string `json:"protocol"`
|
||||
Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
|
||||
|
||||
Description string `json:"description"`
|
||||
type FirewallBackendFamilyStatus struct {
|
||||
Partial bool `json:"partial"`
|
||||
Available bool `json:"available"`
|
||||
Initialized bool `json:"initialized"`
|
||||
Bound bool `json:"bound"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
RAInterfaces []string `json:"raInterfaces,omitempty"`
|
||||
}
|
||||
|
||||
type AddrRuleOperate struct {
|
||||
ID uint `json:"id"`
|
||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
||||
Address string `json:"address" validate:"required"`
|
||||
Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
|
||||
|
||||
Description string `json:"description"`
|
||||
type FirewallBackendGroup struct {
|
||||
Selected string `json:"selected"`
|
||||
Current string `json:"current,omitempty"`
|
||||
Options []FirewallBackendOption `json:"options"`
|
||||
}
|
||||
|
||||
type PortRuleUpdate struct {
|
||||
OldRule PortRuleOperate `json:"oldRule"`
|
||||
NewRule PortRuleOperate `json:"newRule"`
|
||||
type FirewallSettings struct {
|
||||
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||
System FirewallBackendGroup `json:"system"`
|
||||
Forwarding FirewallBackendGroup `json:"forwarding"`
|
||||
Docker FirewallBackendGroup `json:"docker"`
|
||||
PingStatus string `json:"pingStatus"`
|
||||
PortWhitelist []filter.PortWhitelist `json:"portWhiteList"`
|
||||
PanelPort string `json:"panelPort"`
|
||||
SSHPort string `json:"sshPort"`
|
||||
}
|
||||
|
||||
type AddrRuleUpdate struct {
|
||||
OldRule AddrRuleOperate `json:"oldRule"`
|
||||
NewRule AddrRuleOperate `json:"newRule"`
|
||||
type FirewallPortWhitelistCreate struct {
|
||||
Rule filter.PortWhitelist `json:"rule" validate:"required"`
|
||||
}
|
||||
|
||||
type BatchRuleOperate struct {
|
||||
Type string `json:"type" validate:"required"`
|
||||
Rules []PortRuleOperate `json:"rules"`
|
||||
type FirewallPortWhitelistUpdate struct {
|
||||
OldRule filter.PortWhitelist `json:"oldRule" validate:"required"`
|
||||
Rule filter.PortWhitelist `json:"rule" validate:"required"`
|
||||
}
|
||||
|
||||
type IptablesOp struct {
|
||||
Name string `json:"name" validate:"required,oneof=1PANEL_INPUT 1PANEL_OUTPUT 1PANEL_BASIC"`
|
||||
Operate string `json:"operate" validate:"required,oneof=init-base init-forward init-advance bind-base unbind-base bind unbind"`
|
||||
type FirewallPortWhitelistDelete struct {
|
||||
Rule *filter.PortWhitelist `json:"rule" validate:"required"`
|
||||
}
|
||||
|
||||
type IptablesRuleOp struct {
|
||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
||||
ID uint `json:"id"`
|
||||
Chain string `json:"chain" validate:"required,oneof=1PANEL_BASIC 1PANEL_BASIC_BEFORE 1PANEL_INPUT 1PANEL_OUTPUT"`
|
||||
Protocol string `json:"protocol"`
|
||||
SrcIP string `json:"srcIP"`
|
||||
SrcPort uint `json:"srcPort"`
|
||||
DstIP string `json:"dstIP"`
|
||||
DstPort uint `json:"dstPort"`
|
||||
Strategy string `json:"strategy" validate:"required,oneof=accept drop reject"`
|
||||
Description string `json:"description"`
|
||||
type FirewallBackendOperation struct {
|
||||
Subsystem string `json:"subsystem" validate:"required,oneof=system forwarding docker"`
|
||||
Backend string `json:"backend" validate:"required,oneof=firewalld ufw iptables nftables"`
|
||||
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"`
|
||||
}
|
||||
|
||||
type IptablesBatchOperate struct {
|
||||
Rules []IptablesRuleOp `json:"rules"`
|
||||
type FirewallIPv6Operation struct {
|
||||
Status string `json:"status" validate:"required,oneof=Enable Disable"`
|
||||
}
|
||||
|
||||
type IptablesChainStatus struct {
|
||||
IsBind bool `json:"isBind"`
|
||||
DefaultStrategy string `json:"defaultStrategy"`
|
||||
type FirewallFamilyOperation struct {
|
||||
Subsystem string `json:"subsystem" validate:"required,oneof=system forwarding docker"`
|
||||
Backend string `json:"backend" validate:"required,oneof=iptables nftables"`
|
||||
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
|
||||
Operation string `json:"operation" validate:"required,oneof=initialize repair bind"`
|
||||
}
|
||||
|
||||
type FilterChainOperation struct {
|
||||
Name string `json:"name" validate:"required,eq=1PANEL_BASIC"`
|
||||
Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FilterChainOperationResponse struct {
|
||||
TaskID string `json:"taskID"`
|
||||
Queued bool `json:"queued"`
|
||||
}
|
||||
|
||||
type FirewallInitializationTask struct {
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FirewallSystemPort = firewall.SystemPort
|
||||
|
||||
type FirewallRuleInventoryResponse struct {
|
||||
IPv4Range filter.PositionRange `json:"ipv4Range"`
|
||||
IPv6Range filter.PositionRange `json:"ipv6Range"`
|
||||
Total int64 `json:"total"`
|
||||
AllTotal int64 `json:"allTotal"`
|
||||
Items []filter.InventoryItem `json:"items"`
|
||||
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleBackup struct {
|
||||
Name string `json:"name"`
|
||||
Provider filter.Provider `json:"provider"`
|
||||
RuleCount int `json:"ruleCount"`
|
||||
ModifiedAt int64 `json:"modifiedAt"`
|
||||
}
|
||||
|
||||
type FirewallRuleBackups struct {
|
||||
Directory string `json:"directory"`
|
||||
Files []FirewallRuleBackup `json:"files"`
|
||||
}
|
||||
|
||||
type FirewallRuleResetResponse struct {
|
||||
BackupPath string `json:"backupPath"`
|
||||
Removed int `json:"removed"`
|
||||
Disabled bool `json:"disabled"`
|
||||
}
|
||||
|
||||
type FirewallRuleReset struct {
|
||||
Subsystem string `json:"subsystem,omitempty" validate:"omitempty,oneof=system forwarding docker"`
|
||||
Backup *bool `json:"backup,omitempty" default:"true"`
|
||||
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
||||
WithDockerRestart bool `json:"withDockerRestart"`
|
||||
}
|
||||
|
||||
type FirewallRuleInventory struct {
|
||||
PageInfo
|
||||
Scope filter.Scope `json:"scope,omitempty"`
|
||||
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
|
||||
All bool `json:"all,omitempty"`
|
||||
Info string `json:"info"`
|
||||
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
|
||||
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
|
||||
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
|
||||
}
|
||||
|
||||
type FirewallNativeDetail struct {
|
||||
Provider filter.Provider `json:"provider" validate:"required,oneof=firewalld ufw"`
|
||||
NativeKind filter.NativeKind `json:"nativeKind" validate:"required,oneof=zone_service ufw_application"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Permanent bool `json:"permanent"`
|
||||
}
|
||||
|
||||
type DockerPortGuardBase struct {
|
||||
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
IsExist bool `json:"isExist"`
|
||||
Initialized bool `json:"initialized"`
|
||||
Bound bool `json:"bound"`
|
||||
IPv4 DockerPortGuardFamilyStatus `json:"ipv4"`
|
||||
IPv6 DockerPortGuardFamilyStatus `json:"ipv6"`
|
||||
Backend string `json:"backend"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type DockerPortGuardFamilyStatus struct {
|
||||
Partial bool `json:"partial"`
|
||||
State string `json:"state"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Initialized bool `json:"initialized"`
|
||||
Bound bool `json:"bound"`
|
||||
Effective bool `json:"effective"`
|
||||
}
|
||||
|
||||
type DockerPortGuardEndpoint struct {
|
||||
Family string `json:"family"`
|
||||
HostIP string `json:"hostIP"`
|
||||
HostPort uint16 `json:"hostPort"`
|
||||
Protocol string `json:"protocol"`
|
||||
ContainerID string `json:"containerID"`
|
||||
ContainerName string `json:"containerName"`
|
||||
ContainerState string `json:"containerState,omitempty"`
|
||||
ContainerPort uint16 `json:"containerPort"`
|
||||
Compose string `json:"compose,omitempty"`
|
||||
Application string `json:"application,omitempty"`
|
||||
PolicyUUID string `json:"policyUUID,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Sources []string `json:"sources"`
|
||||
Effective bool `json:"effective"`
|
||||
TrafficPath string `json:"trafficPath"`
|
||||
ManagementTarget string `json:"managementTarget"`
|
||||
ManagementReason string `json:"managementReason,omitempty"`
|
||||
}
|
||||
|
||||
type DockerPortGuardPortGroup struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Endpoint DockerPortGuardEndpoint `json:"endpoint"`
|
||||
Endpoints []DockerPortGuardEndpoint `json:"endpoints"`
|
||||
}
|
||||
|
||||
type DockerPortGuardContainer struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Compose string `json:"compose,omitempty"`
|
||||
Application string `json:"application,omitempty"`
|
||||
Endpoints []DockerPortGuardEndpoint `json:"endpoints"`
|
||||
PortGroups []DockerPortGuardPortGroup `json:"portGroups"`
|
||||
}
|
||||
|
||||
type DockerPortGuardList struct {
|
||||
Base DockerPortGuardBase `json:"base"`
|
||||
Containers []DockerPortGuardContainer `json:"containers"`
|
||||
OrphanPolicies []DockerPortGuardEndpoint `json:"orphanPolicies"`
|
||||
}
|
||||
|
||||
type DockerPortGuardEndpointIdentity struct {
|
||||
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
|
||||
HostIP string `json:"hostIP" validate:"required,max=45"`
|
||||
HostPort uint16 `json:"hostPort" validate:"required,min=1"`
|
||||
Protocol string `json:"protocol" validate:"required,oneof=tcp udp"`
|
||||
}
|
||||
|
||||
type DockerPortGuardPolicyBatch struct {
|
||||
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
|
||||
Import bool `json:"import"`
|
||||
}
|
||||
|
||||
type DockerPortGuardPolicyBatchDelete struct {
|
||||
UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"`
|
||||
}
|
||||
|
||||
type DockerPortGuardPolicy struct {
|
||||
DockerPortGuardEndpointIdentity
|
||||
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all accept_sources accept_all"`
|
||||
Sources []string `json:"sources" validate:"dive,required,max=64"`
|
||||
}
|
||||
|
||||
type DockerPortGuardOperation struct {
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FirewallRuleCreateItem struct {
|
||||
Raw string `json:"raw,omitempty"`
|
||||
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
|
||||
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
||||
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
|
||||
}
|
||||
|
||||
type FirewallRuleCreate struct {
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
Initialize bool `json:"initialize"`
|
||||
Items []FirewallRuleCreateItem `json:"items" validate:"dive"`
|
||||
}
|
||||
|
||||
type FirewallRuleCreateResponse struct {
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Queued bool `json:"queued,omitempty"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
Skipped int `json:"skipped"`
|
||||
Errors []FirewallRuleCreateFailure `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleCreateFailure struct {
|
||||
Index int `json:"index"`
|
||||
Status string `json:"status"`
|
||||
Rule filter.FirewallRule `json:"rule"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleDelete struct {
|
||||
Targets []FirewallRuleDeleteItem `json:"targets" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteItem struct {
|
||||
FirewallRuleDeleteTarget
|
||||
Observed filter.ObservedRule `json:"observed" validate:"required"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteTarget struct {
|
||||
Scope filter.Scope `json:"scope" validate:"required"`
|
||||
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteResponse struct {
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Queued bool `json:"queued,omitempty"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
Errors []FirewallRuleDeleteFailure `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteFailure struct {
|
||||
Index int `json:"index"`
|
||||
InstanceKey string `json:"instanceKey"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type FirewallRuleUpdate struct {
|
||||
FirewallRuleDeleteTarget
|
||||
Rule *filter.FirewallRule `json:"rule,omitempty" validate:"required_without_all=Description OrderIndex Priority,excluded_with=Description OrderIndex Priority"`
|
||||
Description *string `json:"description,omitempty" validate:"excluded_with=Rule"`
|
||||
OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"`
|
||||
Priority *int `json:"priority,omitempty" validate:"excluded_with=Rule OrderIndex"`
|
||||
}
|
||||
|
||||
type FirewallRuleReorder struct {
|
||||
FirewallRuleDeleteTarget
|
||||
TargetPosition *int64 `json:"targetPosition"`
|
||||
Priority *int `json:"priority"`
|
||||
}
|
||||
|
||||
type FirewallRuleExportItem struct {
|
||||
filter.FirewallRule
|
||||
Raw string `json:"raw,omitempty"`
|
||||
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallSubsystemBackup struct {
|
||||
Families []string `json:"families,omitempty"`
|
||||
Subsystem string `json:"subsystem"`
|
||||
Provider filter.Provider `json:"provider"`
|
||||
Forwarding []forwarding.Rule `json:"forwarding"`
|
||||
Docker *dockerfirewall.PolicyInventory `json:"docker,omitempty"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package dto
|
||||
|
||||
type ForwardRuleSearch struct {
|
||||
PageInfo
|
||||
All bool `json:"all,omitempty"`
|
||||
Info string `json:"info"`
|
||||
Status string `json:"status"`
|
||||
Strategy string `json:"strategy"`
|
||||
}
|
||||
|
||||
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 {
|
||||
Import bool `json:"import"`
|
||||
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type ForwardRuleOperation struct {
|
||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
||||
Num string `json:"num"`
|
||||
Family string `json:"family" validate:"omitempty,oneof=ipv4 ipv6"`
|
||||
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"`
|
||||
}
|
||||
+89
-18
@@ -1,6 +1,10 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
)
|
||||
|
||||
type MonitorSearch struct {
|
||||
Param string `json:"param" validate:"required,oneof=all cpu memory load io network"`
|
||||
@@ -26,50 +30,92 @@ type Process struct {
|
||||
}
|
||||
|
||||
type MonitorSetting struct {
|
||||
MonitorStatus string `json:"monitorStatus"`
|
||||
MonitorStoreDays string `json:"monitorStoreDays"`
|
||||
MonitorInterval string `json:"monitorInterval"`
|
||||
DefaultNetwork string `json:"defaultNetwork"`
|
||||
DefaultIO string `json:"defaultIO"`
|
||||
GPUMonitorStatus string `json:"gpuMonitorStatus"`
|
||||
GPUMonitorStoreDays string `json:"gpuMonitorStoreDays"`
|
||||
GPUMonitorInterval string `json:"gpuMonitorInterval"`
|
||||
VLLMMonitorStatus string `json:"vllmMonitorStatus"`
|
||||
VLLMMonitorStoreDays string `json:"vllmMonitorStoreDays"`
|
||||
VLLMMonitorInterval string `json:"vllmMonitorInterval"`
|
||||
MonitorStatus string `json:"monitorStatus"`
|
||||
MonitorStoreDays string `json:"monitorStoreDays"`
|
||||
MonitorInterval string `json:"monitorInterval"`
|
||||
DefaultNetwork string `json:"defaultNetwork"`
|
||||
DefaultIO string `json:"defaultIO"`
|
||||
}
|
||||
|
||||
type MonitorSettingUpdate struct {
|
||||
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval DefaultNetwork DefaultIO"`
|
||||
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval GPUMonitorStatus GPUMonitorStoreDays GPUMonitorInterval VLLMMonitorStatus VLLMMonitorStoreDays VLLMMonitorInterval DefaultNetwork DefaultIO"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type MonitorGPUOptions struct {
|
||||
Supported bool `json:"supported"`
|
||||
GPUType string `json:"gpuType"`
|
||||
ChartHide []GPUChartHide `json:"chartHide"`
|
||||
Options []string `json:"options"`
|
||||
}
|
||||
type GPUChartHide struct {
|
||||
DeviceID string `json:"deviceID"`
|
||||
Legacy bool `json:"legacy"`
|
||||
ProductName string `json:"productName"`
|
||||
Type string `json:"type"`
|
||||
Process bool `json:"process"`
|
||||
GPU bool `json:"gpu"`
|
||||
Memory bool `json:"memory"`
|
||||
Power bool `json:"power"`
|
||||
PowerLimit bool `json:"powerLimit"`
|
||||
Temperature bool `json:"temperature"`
|
||||
Speed bool `json:"speed"`
|
||||
}
|
||||
type MonitorGPUSearch struct {
|
||||
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
|
||||
DeviceID string `json:"deviceID"`
|
||||
Legacy bool `json:"legacy"`
|
||||
ProductName string `json:"productName"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
}
|
||||
type MonitorGPUData struct {
|
||||
Date []time.Time `json:"date"`
|
||||
GPUValue []float64 `json:"gpuValue"`
|
||||
TemperatureValue []float64 `json:"temperatureValue"`
|
||||
PowerTotal []float64 `json:"powerTotal"`
|
||||
PowerUsed []float64 `json:"powerUsed"`
|
||||
PowerPercent []float64 `json:"powerPercent"`
|
||||
MemoryTotal []float64 `json:"memoryTotal"`
|
||||
MemoryUsed []float64 `json:"memoryUsed"`
|
||||
MemoryPercent []float64 `json:"memoryPercent"`
|
||||
SpeedValue []int `json:"speedValue"`
|
||||
MemoryActivity []*float64 `json:"memoryActivity"`
|
||||
EncoderUtil []*float64 `json:"encoderUtil"`
|
||||
DecoderUtil []*float64 `json:"decoderUtil"`
|
||||
JPEGUtil []*float64 `json:"jpegUtil"`
|
||||
OFAUtil []*float64 `json:"ofaUtil"`
|
||||
MediaUtil []*float64 `json:"mediaUtil"`
|
||||
ComputeUtil []*float64 `json:"computeUtil"`
|
||||
CopyUtil []*float64 `json:"copyUtil"`
|
||||
HotspotTemperature []*float64 `json:"hotspotTemperature"`
|
||||
FanRPM []*float64 `json:"fanRPM"`
|
||||
AICPUUtil []*float64 `json:"aiCPUUtil"`
|
||||
CtrlCPUUtil []*float64 `json:"ctrlCPUUtil"`
|
||||
DDRUsed []*float64 `json:"ddrUsed"`
|
||||
DDRTotal []*float64 `json:"ddrTotal"`
|
||||
HBMUsed []*float64 `json:"hbmUsed"`
|
||||
HBMTotal []*float64 `json:"hbmTotal"`
|
||||
DDRBandwidth []*float64 `json:"ddrBandwidth"`
|
||||
HBMBandwidth []*float64 `json:"hbmBandwidth"`
|
||||
MemoryBandwidth []*float64 `json:"memoryBandwidth"`
|
||||
MediaFrequency []*float64 `json:"mediaFrequency"`
|
||||
HugepagesUsed []*float64 `json:"hugepagesUsed"`
|
||||
HugepagesTotal []*float64 `json:"hugepagesTotal"`
|
||||
|
||||
ProcessCount []int `json:"processCount"`
|
||||
BucketSeconds int64 `json:"bucketSeconds"`
|
||||
SampleCount int64 `json:"sampleCount"`
|
||||
MemoryTemperatureValue []*float64 `json:"memoryTemperatureValue"`
|
||||
FrequencyValue []*float64 `json:"frequencyValue"`
|
||||
MemoryFrequencyValue []*float64 `json:"memoryFrequencyValue"`
|
||||
Date []time.Time `json:"date"`
|
||||
GPUValue []*float64 `json:"gpuValue"`
|
||||
TemperatureValue []*float64 `json:"temperatureValue"`
|
||||
PowerTotal []*float64 `json:"powerTotal"`
|
||||
PowerUsed []*float64 `json:"powerUsed"`
|
||||
PowerPercent []*float64 `json:"powerPercent"`
|
||||
MemoryTotal []*float64 `json:"memoryTotal"`
|
||||
MemoryUsed []*float64 `json:"memoryUsed"`
|
||||
MemoryPercent []*float64 `json:"memoryPercent"`
|
||||
SpeedValue []*float64 `json:"speedValue"`
|
||||
|
||||
ProcessCount []*float64 `json:"processCount"`
|
||||
GPUProcesses [][]GPUProcess `json:"gpuProcesses"`
|
||||
}
|
||||
|
||||
@@ -79,3 +125,28 @@ type GPUProcess struct {
|
||||
ProcessName string `json:"processName"`
|
||||
UsedMemory string `json:"usedMemory"`
|
||||
}
|
||||
|
||||
type MonitorVLLMSearch struct {
|
||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||
StartTime time.Time `json:"startTime" validate:"required"`
|
||||
EndTime time.Time `json:"endTime" validate:"required"`
|
||||
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
|
||||
}
|
||||
|
||||
type MonitorVLLMData struct {
|
||||
SampleCount int64 `json:"sampleCount"`
|
||||
BucketSeconds int64 `json:"bucketSeconds"`
|
||||
Points []model.MonitorVLLM `json:"points"`
|
||||
}
|
||||
|
||||
type MonitorVLLMCurrent struct {
|
||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||
}
|
||||
|
||||
type MonitorVLLMClean struct {
|
||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||
}
|
||||
|
||||
type MonitorClean struct {
|
||||
Type string `json:"type" validate:"required,oneof=host gpu"`
|
||||
}
|
||||
|
||||
@@ -51,13 +51,19 @@ const (
|
||||
CACHE NginxKey = "cache"
|
||||
HttpPer NginxKey = "http-per"
|
||||
ProxyCache NginxKey = "proxy-cache"
|
||||
Brotli NginxKey = "brotli"
|
||||
)
|
||||
|
||||
// BrotliKeys are served from the panel-managed http.d file rather than
|
||||
// nginx.conf, because the module is optional: its directives must disappear
|
||||
// together with the module, otherwise nginx refuses to start.
|
||||
var BrotliKeys = []string{"brotli", "brotli_comp_level", "brotli_min_length", "brotli_types"}
|
||||
|
||||
var ScopeKeyMap = map[NginxKey][]string{
|
||||
Index: {"index"},
|
||||
LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"},
|
||||
SSL: {"ssl_certificate", "ssl_certificate_key"},
|
||||
HttpPer: {"server_names_hash_bucket_size", "client_header_buffer_size", "client_max_body_size", "keepalive_timeout", "gzip", "gzip_min_length", "gzip_comp_level"},
|
||||
HttpPer: {"server_names_hash_bucket_size", "client_header_buffer_size", "client_max_body_size", "keepalive_timeout", "gzip", "gzip_min_length", "gzip_comp_level", "gzip_types", "gzip_vary", "gzip_proxied"},
|
||||
}
|
||||
|
||||
var StaticFileKeyMap = map[NginxKey]struct {
|
||||
|
||||
@@ -50,6 +50,10 @@ type AppContainerConfig struct {
|
||||
Type string `json:"type"`
|
||||
SpecifyIP string `json:"specifyIP"`
|
||||
RestartPolicy string `json:"restartPolicy" validate:"omitempty,oneof=always unless-stopped no on-failure"`
|
||||
|
||||
KeepServiceName bool `json:"-"`
|
||||
SkipComposeCommonConfig bool `json:"-"`
|
||||
UseLifecycleScripts bool `json:"-"`
|
||||
}
|
||||
|
||||
type AppInstalledSearch struct {
|
||||
@@ -92,6 +96,8 @@ type AppInstalledOperate struct {
|
||||
TaskID string `json:"taskID"`
|
||||
DeleteImage bool `json:"deleteImage"`
|
||||
Favorite bool `json:"favorite"`
|
||||
|
||||
UseLifecycleScripts bool `json:"-"`
|
||||
}
|
||||
|
||||
type AppInstallUpgrade struct {
|
||||
@@ -111,11 +117,14 @@ type AppInstallDelete struct {
|
||||
DeleteDB bool `json:"deleteDB"`
|
||||
DeleteImage bool `json:"deleteImage"`
|
||||
TaskID string `json:"taskID"`
|
||||
|
||||
UseLifecycleScripts bool `json:"-"`
|
||||
}
|
||||
|
||||
type AppInstalledUpdate struct {
|
||||
InstallId uint `json:"installId" validate:"required"`
|
||||
Params map[string]interface{} `json:"params" validate:"required"`
|
||||
TaskID string `json:"-"`
|
||||
AppContainerConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ type FileWget struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
IgnoreCertificate bool `json:"ignoreCertificate"`
|
||||
UseProxy bool `json:"useProxy"`
|
||||
UseServerFilename bool `json:"useServerFilename"`
|
||||
}
|
||||
|
||||
type FileMove struct {
|
||||
@@ -131,6 +132,11 @@ type FileMove struct {
|
||||
Name string `json:"name"`
|
||||
Cover bool `json:"cover"`
|
||||
CoverPaths []string `json:"coverPaths"`
|
||||
TaskID string `json:"taskID"`
|
||||
}
|
||||
|
||||
type FileMoveStopReq struct {
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
}
|
||||
|
||||
type FileDownload struct {
|
||||
@@ -153,6 +159,10 @@ type FileProcessReq struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type FileProcessRemoveReq struct {
|
||||
Keys []string `json:"keys" validate:"required,min=1,max=1000"`
|
||||
}
|
||||
|
||||
type FileRoleUpdate struct {
|
||||
Path string `json:"path" validate:"required"`
|
||||
User string `json:"user" validate:"required"`
|
||||
|
||||
@@ -66,14 +66,15 @@ type RuntimeDelete struct {
|
||||
}
|
||||
|
||||
type RuntimeUpdate struct {
|
||||
Name string `json:"name"`
|
||||
ID uint `json:"id"`
|
||||
Image string `json:"image"`
|
||||
Version string `json:"version"`
|
||||
Rebuild bool `json:"rebuild"`
|
||||
Source string `json:"source"`
|
||||
CodeDir string `json:"codeDir"`
|
||||
Remark string `json:"remark"`
|
||||
AppDetailID uint `json:"appDetailId"`
|
||||
Name string `json:"name"`
|
||||
ID uint `json:"id"`
|
||||
Image string `json:"image"`
|
||||
Version string `json:"version"`
|
||||
Rebuild bool `json:"rebuild"`
|
||||
Source string `json:"source"`
|
||||
CodeDir string `json:"codeDir"`
|
||||
Remark string `json:"remark"`
|
||||
|
||||
Params map[string]interface{} `json:"params"`
|
||||
NodeConfig
|
||||
|
||||
@@ -34,6 +34,8 @@ type WebsiteCreate struct {
|
||||
|
||||
SiteDir string `json:"siteDir"`
|
||||
|
||||
TemplateOutputID uint `json:"templateOutputID"`
|
||||
|
||||
RuntimeConfig
|
||||
FtpConfig
|
||||
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"`
|
||||
}
|
||||
@@ -17,6 +17,17 @@ type NginxParam struct {
|
||||
Params []string `json:"params"`
|
||||
}
|
||||
|
||||
// NginxBrotliRes carries the brotli settings together with where they live.
|
||||
// ManagedExternally is true when the user defined brotli by hand, in which
|
||||
// case the panel only reports the values and must not write its own copy.
|
||||
// ManagedUnavailable is true when the panel could not wire the managed
|
||||
// configuration into nginx.conf at all, so the reported values are inert.
|
||||
type NginxBrotliRes struct {
|
||||
Params []NginxParam `json:"params"`
|
||||
ManagedExternally bool `json:"managedExternally"`
|
||||
ManagedUnavailable bool `json:"managedUnavailable"`
|
||||
}
|
||||
|
||||
type NginxAuthRes struct {
|
||||
Enable bool `json:"enable"`
|
||||
Items []dto.NginxAuth `json:"items"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -35,7 +35,7 @@ type SettingUpdate struct {
|
||||
}
|
||||
|
||||
type AgentSettingUpdate struct {
|
||||
Key string `json:"key" validate:"required,oneof=SystemIP DockerSockPath FileRecycleBin FirewallPortWhiteList"`
|
||||
Key string `json:"key" validate:"required,oneof=SystemIP DockerSockPath FileRecycleBin"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ type RootCertOperate struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Mode string `json:"mode"`
|
||||
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
|
||||
EncryptionMode string `json:"encryptionMode"`
|
||||
PassPhrase string `json:"passPhrase"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package dto
|
||||
|
||||
type TerminalSessionClose struct {
|
||||
ID string `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type TerminalSessionRevoke struct {
|
||||
Scope string `json:"scope" validate:"required,oneof=auth_session user all"`
|
||||
UserID string `json:"userId"`
|
||||
AuthSessionID string `json:"authSessionId"`
|
||||
}
|
||||
+27
-10
@@ -1,5 +1,12 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Alert struct {
|
||||
BaseModel
|
||||
|
||||
@@ -18,10 +25,11 @@ type Alert struct {
|
||||
|
||||
type AlertTask struct {
|
||||
BaseModel
|
||||
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
||||
Quota string `gorm:"type:varchar(64)" json:"quota"`
|
||||
QuotaType string `gorm:"type:varchar(64)" json:"quotaType"`
|
||||
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
|
||||
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
||||
Quota string `gorm:"type:varchar(64)" json:"quota"`
|
||||
QuotaType string `gorm:"type:varchar(64)" json:"quotaType"`
|
||||
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
|
||||
DeliveryLogID *uint `gorm:"uniqueIndex" json:"-"`
|
||||
}
|
||||
|
||||
type AlertLog struct {
|
||||
@@ -41,12 +49,21 @@ type AlertLog struct {
|
||||
|
||||
type AlertConfig struct {
|
||||
BaseModel
|
||||
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
||||
Title string `gorm:"type:varchar(64);not null" json:"title"`
|
||||
Status string `gorm:"type:varchar(64);not null" json:"status"`
|
||||
Config string `gorm:"type:varchar(256);not null" json:"config"`
|
||||
CreateUser string `gorm:"type:varchar(256)" json:"createUser"`
|
||||
UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"`
|
||||
UID string `gorm:"type:varchar(64);not null;uniqueIndex" json:"uid"`
|
||||
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
||||
Title string `gorm:"type:varchar(64);not null" json:"title"`
|
||||
Status string `gorm:"type:varchar(64);not null" json:"status"`
|
||||
Config string `gorm:"type:text;not null" json:"config"`
|
||||
SecretConfig string `gorm:"type:text;not null;default:''" json:"-"`
|
||||
CreateUser string `gorm:"type:varchar(256)" json:"createUser"`
|
||||
UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"`
|
||||
}
|
||||
|
||||
func (a *AlertConfig) BeforeCreate(_ *gorm.DB) error {
|
||||
if strings.TrimSpace(a.UID) == "" {
|
||||
a.UID = uuid.NewString()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type LoginLog struct {
|
||||
|
||||
@@ -31,4 +31,5 @@ type BackupRecord struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Description string `json:"description"`
|
||||
Args string `gorm:"not null;default:''" json:"args"`
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ type ComposeTemplate struct {
|
||||
type Compose struct {
|
||||
BaseModel
|
||||
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
IsPinned bool `json:"isPinned"`
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package model
|
||||
|
||||
type Firewall struct {
|
||||
BaseModel
|
||||
|
||||
Type string `json:"type"`
|
||||
Port string `json:"port"` // Deprecated
|
||||
Address string `json:"address"` // Deprecated
|
||||
|
||||
Chain string `json:"chain"`
|
||||
Protocol string `json:"protocol"`
|
||||
SrcIP string `json:"srcIP"`
|
||||
SrcPort string `json:"srcPort"`
|
||||
DstIP string `json:"dstIP"`
|
||||
DstPort string `json:"dstPort"`
|
||||
Strategy string `gorm:"not null" json:"strategy"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
@@ -8,4 +8,6 @@ type Ftp struct {
|
||||
Status string `gorm:"not null" json:"status"`
|
||||
Path string `gorm:"not null" json:"path"`
|
||||
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:"-"`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type MonitorBase struct {
|
||||
BaseModel
|
||||
Cpu float64 `json:"cpu"`
|
||||
@@ -33,14 +35,78 @@ type MonitorNetwork struct {
|
||||
}
|
||||
|
||||
type MonitorGPU struct {
|
||||
MemoryUtil *float64 `json:"memoryUtil"`
|
||||
MemoryActivity *float64 `json:"memoryActivity"`
|
||||
EncoderUtil *float64 `json:"encoderUtil"`
|
||||
DecoderUtil *float64 `json:"decoderUtil"`
|
||||
JPEGUtil *float64 `json:"jpegUtil"`
|
||||
OFAUtil *float64 `json:"ofaUtil"`
|
||||
MediaUtil *float64 `json:"mediaUtil"`
|
||||
ComputeUtil *float64 `json:"computeUtil"`
|
||||
CopyUtil *float64 `json:"copyUtil"`
|
||||
HotspotTemperature *float64 `json:"hotspotTemperature"`
|
||||
FanRPM *float64 `json:"fanRPM"`
|
||||
AICPUUtil *float64 `json:"aiCPUUtil"`
|
||||
CtrlCPUUtil *float64 `json:"ctrlCPUUtil"`
|
||||
DDRUsed *float64 `json:"ddrUsed"`
|
||||
DDRTotal *float64 `json:"ddrTotal"`
|
||||
HBMUsed *float64 `json:"hbmUsed"`
|
||||
HBMTotal *float64 `json:"hbmTotal"`
|
||||
DDRBandwidth *float64 `json:"ddrBandwidth"`
|
||||
HBMBandwidth *float64 `json:"hbmBandwidth"`
|
||||
MemoryBandwidth *float64 `json:"memoryBandwidth"`
|
||||
MediaFrequency *float64 `json:"mediaFrequency"`
|
||||
HugepagesUsed *float64 `json:"hugepagesUsed"`
|
||||
HugepagesTotal *float64 `json:"hugepagesTotal"`
|
||||
|
||||
MemoryTemperature *float64 `json:"memoryTemperature"`
|
||||
DeviceID string `json:"deviceID"`
|
||||
DeviceType string `json:"deviceType"`
|
||||
ProcessStatus string `json:"processStatus"`
|
||||
Frequency *float64 `json:"frequency"`
|
||||
MemoryFrequency *float64 `json:"memoryFrequency"`
|
||||
IntervalSeconds int `json:"intervalSeconds"`
|
||||
BaseModel
|
||||
ProductName string `json:"productName"`
|
||||
GPUUtil float64 `json:"gpuUtil"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
PowerDraw float64 `json:"powerDraw"`
|
||||
MaxPowerLimit float64 `json:"maxPowerLimit"`
|
||||
MemUsed float64 `json:"memUsed"`
|
||||
MemTotal float64 `json:"memTotal"`
|
||||
FanSpeed int `json:"fanSpeed"`
|
||||
Processes string `json:"processes"`
|
||||
ProductName string `json:"productName"`
|
||||
GPUUtil *float64 `json:"gpuUtil"`
|
||||
Temperature *float64 `json:"temperature"`
|
||||
PowerDraw *float64 `json:"powerDraw"`
|
||||
MaxPowerLimit *float64 `json:"maxPowerLimit"`
|
||||
MemUsed *float64 `json:"memUsed"`
|
||||
MemTotal *float64 `json:"memTotal"`
|
||||
FanSpeed *float64 `json:"fanSpeed"`
|
||||
Processes string `json:"processes"`
|
||||
}
|
||||
|
||||
type MonitorVLLM struct {
|
||||
ID uint `json:"-" gorm:"primarykey;autoIncrement"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
AppInstallID uint `json:"appInstallID"`
|
||||
Status string `json:"status"`
|
||||
RawMetrics string `json:"-"`
|
||||
HistogramDeltas string `json:"-"`
|
||||
|
||||
Running *float64 `json:"running"`
|
||||
Waiting *float64 `json:"waiting"`
|
||||
CacheUsage *float64 `json:"cacheUsage"`
|
||||
PromptThroughput *float64 `json:"promptThroughput"`
|
||||
GenerationThroughput *float64 `json:"generationThroughput"`
|
||||
RequestThroughput *float64 `json:"requestThroughput"`
|
||||
TimeToFirstToken *float64 `json:"timeToFirstToken"`
|
||||
TimePerOutputToken *float64 `json:"timePerOutputToken"`
|
||||
RequestLatency *float64 `json:"requestLatency"`
|
||||
PrefillTime *float64 `json:"prefillTime"`
|
||||
DecodeTime *float64 `json:"decodeTime"`
|
||||
TimeToFirstTokenP50 *float64 `json:"timeToFirstTokenP50"`
|
||||
TimeToFirstTokenP90 *float64 `json:"timeToFirstTokenP90"`
|
||||
TimeToFirstTokenP95 *float64 `json:"timeToFirstTokenP95"`
|
||||
TimeToFirstTokenP99 *float64 `json:"timeToFirstTokenP99"`
|
||||
TimePerOutputTokenP50 *float64 `json:"timePerOutputTokenP50"`
|
||||
TimePerOutputTokenP90 *float64 `json:"timePerOutputTokenP90"`
|
||||
TimePerOutputTokenP95 *float64 `json:"timePerOutputTokenP95"`
|
||||
TimePerOutputTokenP99 *float64 `json:"timePerOutputTokenP99"`
|
||||
RequestLatencyP50 *float64 `json:"requestLatencyP50"`
|
||||
RequestLatencyP90 *float64 `json:"requestLatencyP90"`
|
||||
RequestLatencyP95 *float64 `json:"requestLatencyP95"`
|
||||
RequestLatencyP99 *float64 `json:"requestLatencyP99"`
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
+153
-13
@@ -10,8 +10,10 @@ type APIConfig struct {
|
||||
APIType string
|
||||
BaseURL string
|
||||
EditableBaseURL bool
|
||||
DiscoverModels bool
|
||||
DefaultAuthMode string
|
||||
AuthModes []string
|
||||
Models []Model
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -27,6 +29,7 @@ type Model struct {
|
||||
type Meta struct {
|
||||
Key string
|
||||
DisplayName string
|
||||
DisplayNameKey string
|
||||
Sort uint
|
||||
DefaultAPIType string
|
||||
APIConfigs []APIConfig
|
||||
@@ -37,26 +40,36 @@ type Meta struct {
|
||||
var catalog = map[string]Meta{
|
||||
"custom": {
|
||||
Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY",
|
||||
APIConfigs: editableAPIConfigs("openai-completions", "openai-responses", "anthropic-messages"),
|
||||
APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "dashscope-images", "openai-embeddings"),
|
||||
},
|
||||
"ollama": {
|
||||
Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses",
|
||||
APIConfigs: editableAPIConfigs("openai-responses", "openai-completions"),
|
||||
APIConfigs: editableAPIConfigs(false, "openai-responses", "openai-completions", "openai-embeddings"),
|
||||
},
|
||||
// llmman (https://github.com/llmmanorg/llmman): local runner with Ollama/OpenAI-compatible routes on 127.0.0.1:17434.
|
||||
"llmman": {
|
||||
Key: "llmman", DisplayName: "llmman", Sort: 16, DefaultAPIType: "openai-responses",
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-responses", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
|
||||
{APIType: "openai-completions", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
|
||||
{APIType: "openai-embeddings", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
|
||||
},
|
||||
},
|
||||
"vllm": {
|
||||
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY",
|
||||
APIConfigs: editableAPIConfigs("openai-completions", "openai-responses", "anthropic-messages"),
|
||||
APIConfigs: editableAPIConfigs(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
|
||||
},
|
||||
"deepseek": {
|
||||
Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY",
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-completions", BaseURL: "https://api.deepseek.com/v1"},
|
||||
{APIType: "openai-completions", BaseURL: "https://api.deepseek.com"},
|
||||
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"},
|
||||
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
|
||||
},
|
||||
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
||||
},
|
||||
"bailian-coding-plan": {
|
||||
Key: "bailian-coding-plan", DisplayName: "阿里云百炼 Coding Plan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
|
||||
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-completions", BaseURL: "https://coding.dashscope.aliyuncs.com/v1"},
|
||||
anthropicAPIConfig("https://coding.dashscope.aliyuncs.com/apps/anthropic", AuthModeBearer),
|
||||
@@ -75,7 +88,7 @@ var catalog = map[string]Meta{
|
||||
},
|
||||
},
|
||||
"ark-coding-plan": {
|
||||
Key: "ark-coding-plan", DisplayName: "方舟 Coding Plan", Sort: 35, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY",
|
||||
Key: "ark-coding-plan", DisplayNameKey: "AIProviderArkCodingPlan", Sort: 35, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY",
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3"},
|
||||
anthropicAPIConfig("https://ark.cn-beijing.volces.com/api/coding", AuthModeBearer),
|
||||
@@ -88,14 +101,18 @@ var catalog = map[string]Meta{
|
||||
},
|
||||
"zai": {
|
||||
Key: "zai", DisplayName: "Z.ai", Sort: 40, DefaultAPIType: "openai-completions", EnvKey: "ZAI_API_KEY",
|
||||
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true}},
|
||||
Models: []Model{{ID: "glm-5", Name: "GLM-5"}, {ID: "glm-4.7", Name: "GLM-4.7"}, {ID: "glm-4.7-flash", Name: "GLM-4.7-Flash"}, {ID: "glm-4.7-flashx", Name: "GLM-4.7-FlashX"}},
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-completions", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true},
|
||||
{APIType: "openai-images", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true},
|
||||
},
|
||||
Models: []Model{{ID: "glm-5", Name: "GLM-5"}, {ID: "glm-4.7", Name: "GLM-4.7"}, {ID: "glm-4.7-flash", Name: "GLM-4.7-Flash"}, {ID: "glm-4.7-flashx", Name: "GLM-4.7-FlashX"}},
|
||||
},
|
||||
"minimax": {
|
||||
Key: "minimax", DisplayName: "MiniMax (CN)", Sort: 45, DefaultAPIType: "anthropic-messages", EnvKey: "MINIMAX_API_KEY",
|
||||
APIConfigs: []APIConfig{
|
||||
anthropicAPIConfig("https://api.minimaxi.com/anthropic", AuthModeXAPIKey, AuthModeBearer),
|
||||
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
|
||||
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"},
|
||||
},
|
||||
Models: []Model{{ID: "MiniMax-M3", Name: "MiniMax M3"}, {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}},
|
||||
},
|
||||
@@ -103,9 +120,10 @@ var catalog = map[string]Meta{
|
||||
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-completions", BaseURL: "https://api.xiaomimimo.com/v1"},
|
||||
{APIType: "openai-responses", BaseURL: "https://api.xiaomimimo.com/v1"},
|
||||
anthropicAPIConfig("https://api.xiaomimimo.com/anthropic", AuthModeBearer),
|
||||
},
|
||||
Models: []Model{{ID: "mimo-v2-flash", Name: "Xiaomi MiMo V2 Flash"}, {ID: "mimo-v2-pro", Name: "Xiaomi MiMo V2 Pro"}, {ID: "mimo-v2-omni", Name: "Xiaomi MiMo V2 Omni"}},
|
||||
Models: []Model{{ID: "mimo-v2.5", Name: "Xiaomi MiMo V2.5"}, {ID: "mimo-v2.5-pro", Name: "Xiaomi MiMo V2.5 Pro"}},
|
||||
},
|
||||
"kimi": {
|
||||
Key: "kimi", DisplayName: "Kimi (CN)", Sort: 50, DefaultAPIType: "openai-completions", EnvKey: "KIMI_API_KEY",
|
||||
@@ -122,13 +140,21 @@ var catalog = map[string]Meta{
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-responses", BaseURL: "https://api.openai.com/v1"},
|
||||
{APIType: "openai-completions", BaseURL: "https://api.openai.com/v1"},
|
||||
{APIType: "openai-images", BaseURL: "https://api.openai.com/v1"},
|
||||
{APIType: "openai-embeddings", BaseURL: "https://api.openai.com/v1", Models: []Model{
|
||||
{ID: "text-embedding-3-small", Name: "text-embedding-3-small"},
|
||||
{ID: "text-embedding-3-large", Name: "text-embedding-3-large"},
|
||||
}},
|
||||
},
|
||||
Models: []Model{{ID: "gpt-5.4", Name: "gpt-5.4"}, {ID: "gpt-5.4-pro", Name: "gpt-5.4-pro"}, {ID: "gpt-5.4-mini", Name: "gpt-5.4-mini"}, {ID: "gpt-5.4-nano", Name: "gpt-5.4-nano"}},
|
||||
},
|
||||
"openrouter": {
|
||||
Key: "openrouter", DisplayName: "OpenRouter", Sort: 56, DefaultAPIType: "openai-completions", EnvKey: "OPENROUTER_API_KEY",
|
||||
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://openrouter.ai/api/v1"}},
|
||||
Models: []Model{{ID: "openrouter/free", Name: "openrouter/free"}, {ID: "openrouter/auto", Name: "openrouter/auto"}},
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-completions", BaseURL: "https://openrouter.ai/api/v1"},
|
||||
{APIType: "openrouter-images", BaseURL: "https://openrouter.ai"},
|
||||
},
|
||||
Models: []Model{{ID: "openrouter/free", Name: "openrouter/free"}, {ID: "openrouter/auto", Name: "openrouter/auto"}},
|
||||
},
|
||||
"anthropic": {
|
||||
Key: "anthropic", DisplayName: "Anthropic", Sort: 60, DefaultAPIType: "anthropic-messages", EnvKey: "ANTHROPIC_API_KEY",
|
||||
@@ -145,9 +171,62 @@ var catalog = map[string]Meta{
|
||||
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.ai/v1"}},
|
||||
Models: []Model{{ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}},
|
||||
},
|
||||
"bailian": {
|
||||
Key: "bailian", DisplayNameKey: "AIProviderBailian", Sort: 31, DefaultAPIType: "openai-completions", EnvKey: "DASHSCOPE_API_KEY",
|
||||
APIConfigs: []APIConfig{
|
||||
{
|
||||
APIType: "openai-completions", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
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: "openai-responses", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
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(apiTypes ...string) []APIConfig {
|
||||
func editableAPIConfigs(discoverModels bool, apiTypes ...string) []APIConfig {
|
||||
configs := make([]APIConfig, 0, len(apiTypes))
|
||||
for _, apiType := range apiTypes {
|
||||
if apiType == "anthropic-messages" {
|
||||
@@ -156,7 +235,11 @@ func editableAPIConfigs(apiTypes ...string) []APIConfig {
|
||||
configs = append(configs, config)
|
||||
continue
|
||||
}
|
||||
configs = append(configs, APIConfig{APIType: apiType, EditableBaseURL: true})
|
||||
configs = append(configs, APIConfig{
|
||||
APIType: apiType,
|
||||
EditableBaseURL: true,
|
||||
DiscoverModels: discoverModels && (apiType == "openai-completions" || apiType == "openai-responses"),
|
||||
})
|
||||
}
|
||||
return configs
|
||||
}
|
||||
@@ -203,12 +286,38 @@ func FindAPIConfig(key, apiType string) (APIConfig, bool) {
|
||||
}
|
||||
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) || IsEmbeddingAPIType(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 {
|
||||
@@ -264,6 +373,9 @@ func ResolveBaseURL(key, apiType, requested string) (string, error) {
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||
return "", fmt.Errorf("invalid base url")
|
||||
}
|
||||
if key == "custom" && (IsImageAPIType(config.APIType) || IsEmbeddingAPIType(config.APIType)) {
|
||||
return baseURL, nil
|
||||
}
|
||||
parsed.Path = normalizeEndpointPath(config.APIType, parsed.Path)
|
||||
parsed.RawQuery = ""
|
||||
parsed.Fragment = ""
|
||||
@@ -271,6 +383,9 @@ func ResolveBaseURL(key, apiType, requested string) (string, error) {
|
||||
}
|
||||
|
||||
func normalizeEndpointPath(apiType, value string) string {
|
||||
if IsImageAPIType(apiType) {
|
||||
return strings.TrimRight(value, "/")
|
||||
}
|
||||
path := strings.TrimRight(value, "/")
|
||||
suffixes := []string{}
|
||||
switch apiType {
|
||||
@@ -280,6 +395,8 @@ func normalizeEndpointPath(apiType, value string) string {
|
||||
suffixes = []string{"/responses"}
|
||||
case "anthropic-messages":
|
||||
suffixes = []string{"/v1/messages", "/messages"}
|
||||
case "openai-embeddings":
|
||||
suffixes = []string{"/v1/embeddings", "/embeddings"}
|
||||
}
|
||||
for _, suffix := range suffixes {
|
||||
if strings.HasSuffix(strings.ToLower(path), suffix) {
|
||||
@@ -289,6 +406,19 @@ func normalizeEndpointPath(apiType, value string) string {
|
||||
return path
|
||||
}
|
||||
|
||||
func IsEmbeddingAPIType(apiType string) bool {
|
||||
return apiType == "openai-embeddings"
|
||||
}
|
||||
|
||||
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 {
|
||||
meta, ok := catalog[key]
|
||||
if !ok {
|
||||
@@ -305,6 +435,14 @@ func DisplayName(key string) string {
|
||||
return meta.DisplayName
|
||||
}
|
||||
|
||||
func DisplayNameKey(key string) string {
|
||||
meta, ok := catalog[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return meta.DisplayNameKey
|
||||
}
|
||||
|
||||
func NormalizeModelID(provider, modelID string) string {
|
||||
target := strings.TrimLeft(strings.TrimSpace(modelID), "/")
|
||||
for _, prefix := range legacyModelPrefixes[provider] {
|
||||
@@ -324,6 +462,7 @@ var legacyModelPrefixes = map[string][]string{
|
||||
"custom": {"custom"},
|
||||
"vllm": {"custom"},
|
||||
"ollama": {"ollama"},
|
||||
"llmman": {"llmman"},
|
||||
"deepseek": {"deepseek"},
|
||||
"bailian-coding-plan": {"bailian-coding-plan"},
|
||||
"ark-coding-plan": {"ark-coding-plan"},
|
||||
@@ -344,6 +483,7 @@ func cloneMeta(meta Meta) Meta {
|
||||
for index, config := range meta.APIConfigs {
|
||||
clone.APIConfigs[index] = config
|
||||
clone.APIConfigs[index].AuthModes = append([]string(nil), config.AuthModes...)
|
||||
clone.APIConfigs[index].Models = append([]Model(nil), config.Models...)
|
||||
}
|
||||
clone.Models = append([]Model(nil), meta.Models...)
|
||||
return clone
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -54,11 +55,20 @@ func buildModelDiscoveryURL(baseURL string) string {
|
||||
base = normalizeEndpointPath(apiType, base)
|
||||
}
|
||||
switch {
|
||||
case strings.HasSuffix(base, "/v1/models"):
|
||||
case strings.HasSuffix(base, "/models"):
|
||||
return base
|
||||
case strings.HasSuffix(base, "/v1"):
|
||||
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
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL,
|
||||
if _, ok := FindAPIConfig(provider, resolvedAPIType); !ok {
|
||||
resolvedAPIType = DefaultAPIType(provider)
|
||||
}
|
||||
if IsImageAPIType(resolvedAPIType) || IsEmbeddingAPIType(resolvedAPIType) {
|
||||
return nil, fmt.Errorf("api type %s does not support text generation", resolvedAPIType)
|
||||
}
|
||||
resolvedAuthMode, err := ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -40,8 +43,8 @@ func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL,
|
||||
providerKey = "moonshot"
|
||||
resolvedAPIType = "openai-completions"
|
||||
usesBearer = false
|
||||
case "ollama":
|
||||
apiKey = "ollama"
|
||||
case "ollama", "llmman":
|
||||
apiKey = provider
|
||||
usesBearer = false
|
||||
case "openai", "openrouter", "anthropic":
|
||||
preserveQualifiedModel = strings.Contains(modelName, "/")
|
||||
|
||||
@@ -27,11 +27,14 @@ type verifyErrorResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
const defaultVerifyTimeout = 30 * time.Second
|
||||
const (
|
||||
defaultVerifyTimeout = 30 * time.Second
|
||||
defaultVerifyMaxTokens = 16
|
||||
)
|
||||
|
||||
func SkipVerification(provider string) bool {
|
||||
switch provider {
|
||||
case "vllm", "ollama", "kimi-coding":
|
||||
case "vllm", "ollama", "llmman", "kimi-coding":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -64,7 +67,10 @@ func VerifyAccount(provider, apiType, authMode, baseURL, apiKey, model string) e
|
||||
}
|
||||
|
||||
func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model string) VerifyRequest {
|
||||
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
||||
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}
|
||||
|
||||
@@ -78,6 +84,32 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
||||
}
|
||||
|
||||
switch apiType {
|
||||
case "openai-embeddings":
|
||||
request.URL = embeddingVerifyURL(baseURL)
|
||||
headers["Authorization"] = "Bearer " + apiKey
|
||||
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "ping"})
|
||||
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 "minimax-images":
|
||||
request.URL = imageVerifyURL(provider, baseURL, "/v1/image_generation")
|
||||
headers["Authorization"] = "Bearer " + apiKey
|
||||
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
|
||||
case "openrouter-images":
|
||||
request.URL = imageVerifyURL(provider, baseURL, "/api/v1/images")
|
||||
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 {
|
||||
@@ -87,25 +119,43 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
||||
}
|
||||
headers["anthropic-version"] = "2023-06-01"
|
||||
request.Body = mustJSON(map[string]interface{}{
|
||||
"model": model, "max_tokens": 1, "stream": false,
|
||||
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
||||
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
|
||||
})
|
||||
case "openai-responses":
|
||||
request.URL = baseURL + "/responses"
|
||||
headers["Authorization"] = "Bearer " + apiKey
|
||||
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": 1, "stream": false})
|
||||
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": defaultVerifyMaxTokens, "stream": false})
|
||||
default:
|
||||
request.URL = baseURL + "/chat/completions"
|
||||
if provider != "ollama" || strings.TrimSpace(apiKey) != "" {
|
||||
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
|
||||
headers["Authorization"] = "Bearer " + apiKey
|
||||
}
|
||||
request.Body = mustJSON(map[string]interface{}{
|
||||
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": 1, "stream": false,
|
||||
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
||||
})
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
func embeddingVerifyURL(baseURL string) string {
|
||||
lowerBaseURL := strings.ToLower(baseURL)
|
||||
if strings.HasSuffix(lowerBaseURL, "/embeddings") {
|
||||
return baseURL
|
||||
}
|
||||
if strings.HasSuffix(lowerBaseURL, "/v1") {
|
||||
return baseURL + "/embeddings"
|
||||
}
|
||||
return baseURL + "/v1/embeddings"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -17,7 +17,7 @@ type IAgentAccountRepo interface {
|
||||
Save(account *model.AgentAccount) error
|
||||
DeleteByID(id uint) error
|
||||
List(opts ...DBOption) ([]model.AgentAccount, error)
|
||||
CountByProviders(providers []string) (map[string]int64, error)
|
||||
CountTextByProviders(providers []string) (map[string]int64, error)
|
||||
}
|
||||
|
||||
func NewIAgentAccountRepo() IAgentAccountRepo {
|
||||
@@ -67,7 +67,7 @@ func (a AgentAccountRepo) List(opts ...DBOption) ([]model.AgentAccount, error) {
|
||||
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)
|
||||
counts := make(map[string]int64, len(normalizedProviders))
|
||||
for _, provider := range normalizedProviders {
|
||||
@@ -86,6 +86,7 @@ func (a AgentAccountRepo) CountByProviders(providers []string) (map[string]int64
|
||||
Model(&model.AgentAccount{}).
|
||||
Select("provider, COUNT(*) as count").
|
||||
Where("provider IN ?", normalizedProviders).
|
||||
Scopes(WithTextAPIType()).
|
||||
Group("provider").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
|
||||
+217
-9
@@ -1,20 +1,30 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/genproto/googleapis/type/date"
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
"time"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type AlertRepo struct{}
|
||||
|
||||
var (
|
||||
ErrAlertConfigRevisionConflict = errors.New("alert config revision conflict")
|
||||
ErrAlertConfigRevisionRequired = errors.New("alert config revision is required")
|
||||
)
|
||||
|
||||
type IAlertRepo interface {
|
||||
WithByType(alertType string) DBOption
|
||||
WithByStatusIn(status []string) DBOption
|
||||
@@ -24,6 +34,7 @@ type IAlertRepo interface {
|
||||
WithByCreateAt(date *date.Date) DBOption
|
||||
WithByLicenseId(licenseId string) DBOption
|
||||
WithByRecordId(recordId uint) DBOption
|
||||
WithByDeliveryLogID(logID uint) DBOption
|
||||
WithByAlertMethodContainsConfigID(id uint) DBOption
|
||||
WithByMethodConfigIDs(ids []uint) DBOption
|
||||
|
||||
@@ -45,6 +56,8 @@ type IAlertRepo interface {
|
||||
CleanAlertLogs() error
|
||||
|
||||
CreateAlertTask(alertTaskBase *model.AlertTask) error
|
||||
CreatePendingAlertTask(logID, alertID uint, alertTask *model.AlertTask) (bool, error)
|
||||
FinalizePendingAlertTask(logID uint, succeeded bool, message string, fallback *model.AlertTask) (bool, error)
|
||||
DeleteAlertTask(opts ...DBOption) error
|
||||
GetAlertTask(opts ...DBOption) (model.AlertTask, error)
|
||||
LoadTaskCount(alertType string, project string, method string) (uint, uint, error)
|
||||
@@ -55,6 +68,7 @@ type IAlertRepo interface {
|
||||
GetConfigById(id uint) (model.AlertConfig, error)
|
||||
AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error)
|
||||
UpdateAlertConfig(maps map[string]interface{}, opts ...DBOption) error
|
||||
UpdateAlertConfigWithRevision(maps map[string]interface{}, revision *time.Time, opts ...DBOption) error
|
||||
CreateAlertConfig(config *model.AlertConfig) error
|
||||
DeleteAlertConfig(opts ...DBOption) error
|
||||
|
||||
@@ -223,13 +237,78 @@ func (a *AlertRepo) DeleteLog(opts ...DBOption) error {
|
||||
}
|
||||
|
||||
func (a *AlertRepo) CleanAlertLogs() error {
|
||||
return global.AlertDB.Where("1 = 1").Delete(&model.AlertLog{}).Error
|
||||
return global.AlertDB.Where("status <> ?", constant.AlertPushing).Delete(&model.AlertLog{}).Error
|
||||
}
|
||||
|
||||
func (a *AlertRepo) CreateAlertTask(alertTaskBase *model.AlertTask) error {
|
||||
return global.AlertDB.Model(&model.AlertTask{}).Create(&alertTaskBase).Error
|
||||
}
|
||||
|
||||
func (a *AlertRepo) CreatePendingAlertTask(logID, alertID uint, alertTask *model.AlertTask) (bool, error) {
|
||||
if alertTask == nil {
|
||||
return false, fmt.Errorf("pending alert task is required")
|
||||
}
|
||||
created := false
|
||||
err := global.AlertDB.Transaction(func(tx *gorm.DB) error {
|
||||
var log model.AlertLog
|
||||
if err := tx.Where("id = ? AND status = ?", logID, constant.AlertPushing).First(&log).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if log.AlertId != alertID || log.Type != alertTask.Type || log.Method != alertTask.Method {
|
||||
return fmt.Errorf("pending alert task does not match delivery log %d", logID)
|
||||
}
|
||||
alertTask.DeliveryLogID = &logID
|
||||
result := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "delivery_log_id"}},
|
||||
DoNothing: true,
|
||||
}).Create(alertTask)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
created = result.RowsAffected > 0
|
||||
return nil
|
||||
})
|
||||
return created, err
|
||||
}
|
||||
|
||||
func (a *AlertRepo) FinalizePendingAlertTask(logID uint, succeeded bool, message string, fallback *model.AlertTask) (bool, error) {
|
||||
finalized := false
|
||||
err := global.AlertDB.Transaction(func(tx *gorm.DB) error {
|
||||
status := constant.AlertError
|
||||
if succeeded {
|
||||
status = constant.AlertSuccess
|
||||
message = ""
|
||||
}
|
||||
result := tx.Model(&model.AlertLog{}).
|
||||
Where("id = ? AND status = ?", logID, constant.AlertPushing).
|
||||
Updates(map[string]interface{}{"status": status, "message": message})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil
|
||||
}
|
||||
finalized = true
|
||||
if !succeeded {
|
||||
return tx.Where("delivery_log_id = ?", logID).Delete(&model.AlertTask{}).Error
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := tx.Model(&model.AlertTask{}).Where("delivery_log_id = ?", logID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
if fallback == nil {
|
||||
return fmt.Errorf("pending alert task metadata is unavailable for delivery log %d", logID)
|
||||
}
|
||||
fallback.DeliveryLogID = &logID
|
||||
return tx.Create(fallback).Error
|
||||
})
|
||||
return finalized, err
|
||||
}
|
||||
|
||||
func (a *AlertRepo) DeleteAlertTask(opts ...DBOption) error {
|
||||
db, _ := getAlertDB(opts...)
|
||||
return db.Delete(&model.AlertTask{}).Error
|
||||
@@ -310,7 +389,23 @@ func (a *AlertRepo) UpdateAlertConfig(maps map[string]interface{}, opts ...DBOpt
|
||||
return db.Model(&model.AlertConfig{}).Updates(maps).Error
|
||||
}
|
||||
|
||||
func (a *AlertRepo) UpdateAlertConfigWithRevision(maps map[string]interface{}, revision *time.Time, opts ...DBOption) error {
|
||||
if revision == nil {
|
||||
return a.UpdateAlertConfig(maps, opts...)
|
||||
}
|
||||
db, _ := getAlertDB(opts...)
|
||||
result := db.Model(&model.AlertConfig{}).Where("updated_at = ?", *revision).Updates(maps)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return ErrAlertConfigRevisionConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AlertRepo) CreateAlertConfig(config *model.AlertConfig) error {
|
||||
ensureAlertConfigUID(config)
|
||||
return global.AlertDB.Model(&model.AlertConfig{}).Create(config).Error
|
||||
}
|
||||
|
||||
@@ -338,6 +433,12 @@ func (a *AlertRepo) WithByTypeNotIn(types []string) DBOption {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AlertRepo) WithByDeliveryLogID(logID uint) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("delivery_log_id = ?", logID)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AlertRepo) PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error) {
|
||||
var configs []model.AlertConfig
|
||||
db := global.AlertDB.Model(&model.AlertConfig{})
|
||||
@@ -378,26 +479,44 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
oldConfigMap := make(map[string]uint)
|
||||
oldConfigMap := make(map[string]model.AlertConfig)
|
||||
oldConfigByUID := make(map[string]model.AlertConfig)
|
||||
oldConfigByType := make(map[string][]model.AlertConfig)
|
||||
oldConfigByKey := make(map[string][]model.AlertConfig)
|
||||
consumedConfigIDs := make(map[uint]struct{})
|
||||
for _, item := range oldConfigs {
|
||||
if strings.TrimSpace(item.UID) != "" {
|
||||
oldConfigByUID[item.UID] = item
|
||||
}
|
||||
if singletonTypes[item.Type] {
|
||||
oldConfigMap[item.Type] = item.ID
|
||||
oldConfigMap[item.Type] = item
|
||||
continue
|
||||
}
|
||||
oldConfigByType[item.Type] = append(oldConfigByType[item.Type], item)
|
||||
oldConfigByKey[alertConfigSyncKey(item)] = append(oldConfigByKey[alertConfigSyncKey(item)], item)
|
||||
}
|
||||
for _, item := range data {
|
||||
if uid := strings.TrimSpace(item.UID); uid != "" {
|
||||
if matched, ok := oldConfigByUID[uid]; ok && matched.Type != item.Type {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("alert config UID %q belongs to type %q, not %q", uid, matched.Type, item.Type)
|
||||
}
|
||||
}
|
||||
if singletonTypes[item.Type] {
|
||||
if val, ok := oldConfigMap[item.Type]; ok {
|
||||
item.ID = val
|
||||
if matched, ok := oldConfigMap[item.Type]; ok {
|
||||
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
delete(oldConfigMap, item.Type)
|
||||
consumedConfigIDs[item.ID] = struct{}{}
|
||||
} else {
|
||||
item.ID = 0
|
||||
ensureAlertConfigUID(&item)
|
||||
if err := validateAlertConfigSyncSecret(&item); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
if item.ID == 0 {
|
||||
if err := tx.Create(&item).Error; err != nil {
|
||||
@@ -411,9 +530,31 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.TrimSpace(item.UID) != "" {
|
||||
if matched, ok := oldConfigByUID[item.UID]; ok {
|
||||
delete(oldConfigByUID, item.UID)
|
||||
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
consumedConfigIDs[item.ID] = struct{}{}
|
||||
if err := tx.Save(&item).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
deleteAlertConfigByID(oldConfigByType, matched.ID)
|
||||
deleteAlertConfigByID(oldConfigByKey, matched.ID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
key := alertConfigSyncKey(item)
|
||||
if matched, ok := popAlertConfigByKey(oldConfigByKey, key); ok {
|
||||
item.ID = matched.ID
|
||||
delete(oldConfigByUID, matched.UID)
|
||||
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
consumedConfigIDs[item.ID] = struct{}{}
|
||||
if err := tx.Save(&item).Error; err != nil {
|
||||
tx.Rollback()
|
||||
@@ -424,7 +565,12 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
||||
}
|
||||
|
||||
if matched, ok := popUnusedAlertConfigByType(oldConfigByType, usedConfigIDs, item.Type); ok {
|
||||
item.ID = matched.ID
|
||||
delete(oldConfigByUID, matched.UID)
|
||||
deleteAlertConfigByID(oldConfigByKey, matched.ID)
|
||||
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
consumedConfigIDs[item.ID] = struct{}{}
|
||||
if err := tx.Save(&item).Error; err != nil {
|
||||
tx.Rollback()
|
||||
@@ -434,6 +580,11 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
||||
}
|
||||
|
||||
item.ID = 0
|
||||
ensureAlertConfigUID(&item)
|
||||
if err := validateAlertConfigSyncSecret(&item); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&item).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
@@ -458,6 +609,63 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureAlertConfigUID(config *model.AlertConfig) {
|
||||
if config != nil && strings.TrimSpace(config.UID) == "" {
|
||||
config.UID = uuid.NewString()
|
||||
}
|
||||
}
|
||||
|
||||
func inheritAlertConfigSyncState(incoming *model.AlertConfig, existing model.AlertConfig) error {
|
||||
if incoming.Type != existing.Type {
|
||||
return fmt.Errorf("alert config UID %q belongs to type %q, not %q", incoming.UID, existing.Type, incoming.Type)
|
||||
}
|
||||
preserveExistingCustom := incoming.Type == constant.Custom &&
|
||||
existing.Status == constant.AlertDisable &&
|
||||
incoming.Title == existing.Title &&
|
||||
incoming.Status == existing.Status &&
|
||||
incoming.Config == existing.Config &&
|
||||
(incoming.SecretConfig == "" || incoming.SecretConfig == existing.SecretConfig)
|
||||
incoming.ID = existing.ID
|
||||
if strings.TrimSpace(incoming.UID) == "" {
|
||||
incoming.UID = existing.UID
|
||||
}
|
||||
if incoming.Type == constant.Custom && incoming.SecretConfig == "" {
|
||||
incoming.SecretConfig = existing.SecretConfig
|
||||
}
|
||||
if preserveExistingCustom {
|
||||
return nil
|
||||
}
|
||||
return validateAlertConfigSyncSecret(incoming)
|
||||
}
|
||||
|
||||
func validateAlertConfigSyncSecret(incoming *model.AlertConfig) error {
|
||||
if incoming.Type != constant.Custom {
|
||||
incoming.SecretConfig = ""
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(incoming.SecretConfig) == "" {
|
||||
return fmt.Errorf("custom webhook sync secret is missing")
|
||||
}
|
||||
var version struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(incoming.Config), &version); err != nil || version.SchemaVersion != 1 {
|
||||
return fmt.Errorf("custom webhook sync config must use schemaVersion 1")
|
||||
}
|
||||
secret := incoming.SecretConfig
|
||||
for _, prefix := range []string{"core:v1:", "agent:v1:"} {
|
||||
if !strings.HasPrefix(secret, prefix) {
|
||||
continue
|
||||
}
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, prefix))
|
||||
if err != nil || len(ciphertext) < 32 || len(ciphertext)%16 != 0 {
|
||||
return fmt.Errorf("custom webhook sync secret envelope is invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("custom webhook sync secret must use a versioned envelope")
|
||||
}
|
||||
|
||||
func loadUsedAlertConfigIDs(tx *gorm.DB) (map[uint]struct{}, error) {
|
||||
var alerts []model.Alert
|
||||
if err := tx.Select("method").Find(&alerts).Error; err != nil {
|
||||
|
||||
@@ -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 {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
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 ? AND api_type <> ?", "%-images", "openai-embeddings")
|
||||
}
|
||||
}
|
||||
|
||||
func WithByModel(model string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
if len(model) == 0 {
|
||||
|
||||
@@ -22,11 +22,6 @@ type IHostRepo interface {
|
||||
WithByPort(port uint) DBOption
|
||||
WithByUser(user string) DBOption
|
||||
|
||||
GetFirewallRecord(opts ...DBOption) (model.Firewall, error)
|
||||
ListFirewallRecord(opts ...DBOption) ([]model.Firewall, error)
|
||||
SaveFirewallRecord(firewall *model.Firewall) error
|
||||
DeleteFirewallRecordByID(id uint) error
|
||||
|
||||
SyncCert(data []model.RootCert) error
|
||||
GetCert(opts ...DBOption) (model.RootCert, error)
|
||||
PageCert(limit, offset int, opts ...DBOption) (int64, []model.RootCert, error)
|
||||
@@ -34,8 +29,6 @@ type IHostRepo interface {
|
||||
SaveCert(cert *model.RootCert) error
|
||||
UpdateCert(id uint, vars map[string]interface{}) error
|
||||
DeleteCert(opts ...DBOption) error
|
||||
|
||||
WithByChain(chain string) DBOption
|
||||
}
|
||||
|
||||
func NewIHostRepo() IHostRepo {
|
||||
@@ -116,65 +109,6 @@ func (h *HostRepo) Delete(opts ...DBOption) error {
|
||||
return db.Delete(&model.Host{}).Error
|
||||
}
|
||||
|
||||
func (h *HostRepo) GetFirewallRecord(opts ...DBOption) (model.Firewall, error) {
|
||||
var firewall model.Firewall
|
||||
db := global.DB
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
err := db.First(&firewall).Error
|
||||
return firewall, err
|
||||
}
|
||||
|
||||
func (h *HostRepo) ListFirewallRecord(opts ...DBOption) ([]model.Firewall, error) {
|
||||
var firewalls []model.Firewall
|
||||
db := global.DB
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
if err := global.DB.Find(&firewalls).Error; err != nil {
|
||||
return firewalls, nil
|
||||
}
|
||||
return firewalls, nil
|
||||
}
|
||||
|
||||
func (h *HostRepo) SaveFirewallRecord(firewall *model.Firewall) error {
|
||||
if firewall.ID != 0 {
|
||||
return global.DB.Save(firewall).Error
|
||||
}
|
||||
var data model.Firewall
|
||||
switch firewall.Type {
|
||||
case "port":
|
||||
_ = global.DB.Where("type = ? AND dst_port = ? AND protocol = ? AND src_ip = ? AND strategy = ?", "port",
|
||||
firewall.DstPort,
|
||||
firewall.Protocol,
|
||||
firewall.SrcIP,
|
||||
firewall.Strategy,
|
||||
).First(&data).Error
|
||||
case "ip":
|
||||
_ = global.DB.Where("type = ? AND src_ip = ? AND strategy = ?", "address", firewall.SrcIP, firewall.Strategy).First(&data)
|
||||
default:
|
||||
_ = global.DB.Where("type = ? AND chain = ? AND src_port = ? AND dst_port = ? AND protocol = ? AND src_ip = ? AND dst_ip = ? AND strategy = ?",
|
||||
firewall.Type,
|
||||
firewall.Chain,
|
||||
firewall.SrcPort,
|
||||
firewall.DstPort,
|
||||
firewall.Protocol,
|
||||
firewall.SrcIP,
|
||||
firewall.DstIP,
|
||||
firewall.Strategy,
|
||||
).First(&data).Error
|
||||
}
|
||||
if data.ID != 0 {
|
||||
firewall.ID = data.ID
|
||||
}
|
||||
return global.DB.Save(firewall).Error
|
||||
}
|
||||
|
||||
func (h *HostRepo) DeleteFirewallRecordByID(id uint) error {
|
||||
return global.DB.Where("id = ?", id).Delete(&model.Firewall{}).Error
|
||||
}
|
||||
|
||||
func (u *HostRepo) GetCert(opts ...DBOption) (model.RootCert, error) {
|
||||
var cert model.RootCert
|
||||
db := global.DB
|
||||
@@ -253,9 +187,3 @@ func (u *HostRepo) SyncCert(data []model.RootCert) error {
|
||||
tx.Commit()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *HostRepo) WithByChain(chain string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("chain = ?", chain)
|
||||
}
|
||||
}
|
||||
|
||||
+106
-13
@@ -1,6 +1,8 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
@@ -10,11 +12,25 @@ import (
|
||||
|
||||
type MonitorRepo struct{}
|
||||
|
||||
type GPUHistoryPoint struct {
|
||||
model.MonitorGPU
|
||||
Bucket int64
|
||||
PowerPercent *float64
|
||||
MemoryPercent *float64
|
||||
ProcessCount *float64
|
||||
}
|
||||
|
||||
type IMonitorRepo interface {
|
||||
CleanHost() error
|
||||
CleanGPU() error
|
||||
GetBase(opts ...DBOption) ([]model.MonitorBase, error)
|
||||
GetGPU(opts ...DBOption) ([]model.MonitorGPU, error)
|
||||
CountGPU(opts ...DBOption) (int64, error)
|
||||
GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error)
|
||||
GetGPUDevices() ([]model.MonitorGPU, error)
|
||||
GetIO(opts ...DBOption) ([]model.MonitorIO, error)
|
||||
GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, error)
|
||||
GetIONames() ([]string, error)
|
||||
GetNetworkNames() ([]string, error)
|
||||
|
||||
CreateMonitorBase(model model.MonitorBase) error
|
||||
BatchCreateMonitorGPU(list []model.MonitorGPU) error
|
||||
@@ -25,13 +41,26 @@ type IMonitorRepo interface {
|
||||
DelMonitorIO(timeForDelete time.Time) error
|
||||
DelMonitorNet(timeForDelete time.Time) error
|
||||
|
||||
WithByProductName(name string) DBOption
|
||||
WithByGPUDevice(deviceID, name string, legacy bool) DBOption
|
||||
}
|
||||
|
||||
func NewIMonitorRepo() IMonitorRepo {
|
||||
return &MonitorRepo{}
|
||||
}
|
||||
|
||||
func (s *MonitorRepo) CleanHost() error {
|
||||
for _, item := range []interface{}{&model.MonitorBase{}, &model.MonitorIO{}, &model.MonitorNetwork{}} {
|
||||
if err := global.MonitorDB.Where("1 = 1").Delete(item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MonitorRepo) CleanGPU() error {
|
||||
return global.GPUMonitorDB.Where("1 = 1").Delete(&model.MonitorGPU{}).Error
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) GetBase(opts ...DBOption) ([]model.MonitorBase, error) {
|
||||
var data []model.MonitorBase
|
||||
db := global.MonitorDB
|
||||
@@ -59,20 +88,25 @@ func (u *MonitorRepo) GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, erro
|
||||
err := db.Find(&data).Error
|
||||
return data, err
|
||||
}
|
||||
func (u *MonitorRepo) GetGPU(opts ...DBOption) ([]model.MonitorGPU, error) {
|
||||
var data []model.MonitorGPU
|
||||
db := global.GPUMonitorDB
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
err := db.Find(&data).Error
|
||||
return data, err
|
||||
func (u *MonitorRepo) GetIONames() ([]string, error) {
|
||||
var names []string
|
||||
err := global.MonitorDB.Model(&model.MonitorIO{}).Distinct().Pluck("name", &names).Error
|
||||
return names, err
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) GetNetworkNames() ([]string, error) {
|
||||
var names []string
|
||||
err := global.MonitorDB.Model(&model.MonitorNetwork{}).Distinct().Pluck("name", &names).Error
|
||||
return names, err
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
|
||||
return global.MonitorDB.Create(&model).Error
|
||||
}
|
||||
func (s *MonitorRepo) BatchCreateMonitorGPU(list []model.MonitorGPU) error {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
return global.GPUMonitorDB.CreateInBatches(&list, len(list)).Error
|
||||
}
|
||||
func (u *MonitorRepo) BatchCreateMonitorIO(ioList []model.MonitorIO) error {
|
||||
@@ -94,8 +128,67 @@ func (s *MonitorRepo) DelMonitorGPU(timeForDelete time.Time) error {
|
||||
return global.GPUMonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorGPU{}).Error
|
||||
}
|
||||
|
||||
func (s *MonitorRepo) WithByProductName(name string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("product_name = ?", name)
|
||||
func (u *MonitorRepo) GetGPUDevices() ([]model.MonitorGPU, error) {
|
||||
var data []model.MonitorGPU
|
||||
err := global.GPUMonitorDB.Model(&model.MonitorGPU{}).Select("device_id, product_name, device_type").Group("device_id, product_name, device_type").Order("product_name, device_id").Find(&data).Error
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) WithByGPUDevice(deviceID, name string, legacy bool) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
if deviceID != "" {
|
||||
return db.Where("device_id = ?", deviceID)
|
||||
}
|
||||
db = db.Where("product_name = ?", name)
|
||||
if legacy {
|
||||
db = db.Where("device_id IS NULL OR device_id = ''")
|
||||
}
|
||||
return db
|
||||
}
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) CountGPU(opts ...DBOption) (int64, error) {
|
||||
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
var count int64
|
||||
err := db.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error) {
|
||||
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
expressions := []string{
|
||||
"CASE WHEN max_power_limit > 0 THEN 100.0 * power_draw / max_power_limit END",
|
||||
"CASE WHEN mem_total > 0 AND mem_used IS NOT NULL THEN 100.0 * mem_used / mem_total ELSE memory_util END",
|
||||
"CASE WHEN (process_status = 'ok' OR process_status IS NULL OR process_status = '') AND json_valid(processes) THEN CASE WHEN json_type(processes) = 'array' THEN json_array_length(processes) END END",
|
||||
}
|
||||
aliases := []string{"power_percent", "memory_percent", "process_count"}
|
||||
columns := []string{"*"}
|
||||
if bucketSeconds > 0 {
|
||||
operation := "AVG"
|
||||
if aggregation == "max" {
|
||||
operation = "MAX"
|
||||
}
|
||||
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), bucketSeconds)}
|
||||
for _, column := range []string{"memory_activity", "encoder_util", "decoder_util", "jpeg_util", "ofa_util", "media_util", "compute_util", "copy_util", "hotspot_temperature", "fan_rpm", "ai_cpu_util", "ctrl_cpu_util", "ddr_used", "ddr_total", "hbm_used", "hbm_total", "ddr_bandwidth", "hbm_bandwidth", "memory_bandwidth", "media_frequency", "hugepages_used", "hugepages_total", "gpu_util", "temperature", "memory_temperature", "power_draw", "max_power_limit", "mem_used", "mem_total", "frequency", "memory_frequency", "fan_speed"} {
|
||||
columns = append(columns, operation+"("+column+") AS "+column)
|
||||
}
|
||||
for i := range expressions {
|
||||
expressions[i] = operation + "(" + expressions[i] + ")"
|
||||
}
|
||||
db = db.Group("bucket").Order("bucket ASC")
|
||||
} else {
|
||||
db = db.Order("created_at ASC, id ASC")
|
||||
}
|
||||
for i, expression := range expressions {
|
||||
columns = append(columns, expression+" AS "+aliases[i])
|
||||
}
|
||||
var data []GPUHistoryPoint
|
||||
err := db.Select(strings.Join(columns, ", ")).Scan(&data).Error
|
||||
return data, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type VLLMMonitorRepo struct{}
|
||||
|
||||
type VLLMHistoryPoint struct {
|
||||
model.MonitorVLLM
|
||||
Bucket int64
|
||||
HistogramSamples string
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
|
||||
return global.VLLMMonitorDB.Create(point).Error
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) Latest(id uint) (model.MonitorVLLM, error) {
|
||||
var point model.MonitorVLLM
|
||||
db := global.VLLMMonitorDB.Where("app_install_id = ?", id)
|
||||
err := db.Order("created_at DESC, id DESC").First(&point).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return point, nil
|
||||
}
|
||||
return point, err
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) CleanTarget(id uint) error {
|
||||
return global.VLLMMonitorDB.Where("app_install_id = ?", id).Delete(&model.MonitorVLLM{}).Error
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) DeleteBefore(before time.Time) error {
|
||||
return global.VLLMMonitorDB.Where("created_at < ?", before).Delete(&model.MonitorVLLM{}).Error
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) Count(id uint, start, end time.Time) (int64, error) {
|
||||
var count int64
|
||||
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
|
||||
err := db.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) History(id uint, start, end time.Time, seconds int64, aggregation string) ([]VLLMHistoryPoint, error) {
|
||||
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
|
||||
metrics := []string{"running", "waiting", "cache_usage", "prompt_throughput", "generation_throughput", "request_throughput", "time_to_first_token", "time_per_output_token", "request_latency", "prefill_time", "decode_time", "time_to_first_token_p50", "time_to_first_token_p90", "time_to_first_token_p95", "time_to_first_token_p99", "time_per_output_token_p50", "time_per_output_token_p90", "time_per_output_token_p95", "time_per_output_token_p99", "request_latency_p50", "request_latency_p90", "request_latency_p95", "request_latency_p99"}
|
||||
var columns []string
|
||||
if seconds > 0 {
|
||||
operation := "AVG"
|
||||
if aggregation == "max" {
|
||||
operation = "MAX"
|
||||
}
|
||||
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), seconds)}
|
||||
for _, column := range metrics {
|
||||
if aggregation != "max" && (strings.HasPrefix(column, "time_to_first_token_p") || strings.HasPrefix(column, "time_per_output_token_p") || strings.HasPrefix(column, "request_latency_p")) {
|
||||
continue
|
||||
}
|
||||
columns = append(columns, operation+"("+column+") AS "+column)
|
||||
}
|
||||
if aggregation != "max" {
|
||||
columns = append(columns, "json_group_array(json(NULLIF(histogram_deltas, ''))) AS histogram_samples")
|
||||
}
|
||||
db = db.Group("bucket").Order("bucket ASC")
|
||||
} else {
|
||||
columns = append([]string{"id", "created_at", "app_install_id", "status"}, metrics...)
|
||||
db = db.Order("created_at ASC, id ASC")
|
||||
}
|
||||
var points []VLLMHistoryPoint
|
||||
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
|
||||
return points, err
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type SettingRepo struct{}
|
||||
@@ -19,12 +21,16 @@ type ISettingRepo interface {
|
||||
WithByKey(key string) DBOption
|
||||
|
||||
UpdateOrCreate(key, value string) error
|
||||
UpdateValues(map[string]string) error
|
||||
|
||||
GetDescription(opts ...DBOption) (model.CommonDescription, error)
|
||||
GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error)
|
||||
CreateDescription(data *model.CommonDescription) error
|
||||
SaveDescriptions(context.Context, []model.CommonDescription) error
|
||||
UpdateDescription(id string, val map[string]interface{}) error
|
||||
DelDescription(id string) error
|
||||
DeleteDescriptions(context.Context, string, []string, bool) (int64, error)
|
||||
WithDescriptionIDs(ids []string) DBOption
|
||||
WithByDescriptionID(id string) DBOption
|
||||
}
|
||||
|
||||
@@ -90,6 +96,25 @@ func (s *SettingRepo) UpdateOrCreate(key, value string) error {
|
||||
return global.DB.Model(&setting).UpdateColumn("value", value).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) UpdateValues(values map[string]string) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
for key, value := range values {
|
||||
var setting model.Setting
|
||||
err := tx.Where("key = ?", key).First(&setting).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err := tx.Create(&model.Setting{Key: key, Value: value}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else if err := tx.Model(&setting).UpdateColumn("value", value).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SettingRepo) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) {
|
||||
var lists []model.CommonDescription
|
||||
db := global.DB.Model(&model.CommonDescription{})
|
||||
@@ -111,14 +136,42 @@ func (s *SettingRepo) GetDescription(opts ...DBOption) (model.CommonDescription,
|
||||
func (s *SettingRepo) CreateDescription(data *model.CommonDescription) error {
|
||||
return global.DB.Create(data).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) SaveDescriptions(ctx context.Context, descriptions []model.CommonDescription) error {
|
||||
return global.DB.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"description"}),
|
||||
}).CreateInBatches(&descriptions, 100).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) UpdateDescription(id string, val map[string]interface{}) error {
|
||||
return global.DB.Model(&model.CommonDescription{}).Where("id = ?", id).Updates(val).Error
|
||||
}
|
||||
func (s *SettingRepo) DelDescription(id string) error {
|
||||
return global.DB.Where("id = ?", id).Delete(&model.CommonDescription{}).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) DeleteDescriptions(ctx context.Context, kind string, ids []string, emptyOnly bool) (int64, error) {
|
||||
var deleted int64
|
||||
for start := 0; start < len(ids); start += 500 {
|
||||
query := global.DB.WithContext(ctx).Where("type = ? AND id IN ?", kind, ids[start:min(start+500, len(ids))])
|
||||
if emptyOnly {
|
||||
query = query.Where("description = ? AND is_pinned = ?", "", false)
|
||||
}
|
||||
result := query.Delete(&model.CommonDescription{})
|
||||
deleted += result.RowsAffected
|
||||
if result.Error != nil {
|
||||
return deleted, result.Error
|
||||
}
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
func (s *SettingRepo) WithByDescriptionID(id string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("id = ?", id)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SettingRepo) WithDescriptionIDs(ids []string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB { return db.Where("id IN ?", ids) }
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
@@ -110,7 +111,9 @@ func (t TaskRepo) Update(ctx context.Context, task *model.Task) error {
|
||||
}
|
||||
|
||||
func (t TaskRepo) UpdateRunningTaskToFailed() error {
|
||||
return getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Updates(map[string]interface{}{"status": constant.StatusFailed, "error_msg": "1Panel restart causes failure"}).Error
|
||||
return getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Updates(map[string]interface{}{
|
||||
"status": constant.StatusFailed, "error_msg": constant.InterruptedMsg, "end_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (t TaskRepo) CountExecutingTask() (int64, error) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+72
-25
@@ -105,6 +105,10 @@ type IAgentService interface {
|
||||
UpgradePlugin(req dto.AgentPluginUpgradeReq) error
|
||||
UninstallPlugin(req dto.AgentPluginUninstallReq) 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
|
||||
}
|
||||
|
||||
@@ -177,7 +181,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
var allowedOrigins []string
|
||||
var account *model.AgentAccount
|
||||
var installHooks *appInstallHooks
|
||||
var hermesAuth hermesDashboardAuth
|
||||
var dashboardAuth agentDashboardAuth
|
||||
|
||||
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
|
||||
if req.AccountID == 0 {
|
||||
@@ -222,15 +226,17 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
},
|
||||
}
|
||||
} else if agentType == constant.AppHermesAgent {
|
||||
hermesAuth = normalizeHermesDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||
installHooks = &appInstallHooks{
|
||||
AfterCopyData: func(appInstall *model.AppInstall) error {
|
||||
if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeHermesDashboardAuthEnv(path.Join(appInstall.GetPath(), ".env"), hermesAuth, false)
|
||||
return writeAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType, dashboardAuth, false)
|
||||
},
|
||||
}
|
||||
} else if agentType == constant.AppCopaw {
|
||||
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
@@ -250,9 +256,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
params["API_KEY"] = apiKey
|
||||
params["OPENCLAW_GATEWAY_TOKEN"] = token
|
||||
}
|
||||
if agentType == constant.AppHermesAgent {
|
||||
params[hermesDashboardUsernameEnvKey] = hermesAuth.Username
|
||||
params[hermesDashboardPasswordEnvKey] = hermesAuth.Password
|
||||
if usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType); ok {
|
||||
params[usernameKey] = dashboardAuth.Username
|
||||
params[passwordKey] = dashboardAuth.Password
|
||||
if agentType == constant.AppCopaw {
|
||||
params[qwenPawAuthEnabledEnvKey] = "true"
|
||||
}
|
||||
}
|
||||
|
||||
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
|
||||
@@ -927,6 +936,7 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig,
|
||||
AccountID: agent.AccountID,
|
||||
Model: model,
|
||||
Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
|
||||
Metadata: extractOpenclawModelMetadata(conf, account, models),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -958,7 +968,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil {
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks, req.Metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -984,19 +994,28 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
|
||||
}
|
||||
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,
|
||||
DefaultAuthMode: item.DefaultAuthMode,
|
||||
AuthModes: item.AuthModes,
|
||||
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{
|
||||
Sort: def.Sort,
|
||||
Provider: key,
|
||||
DisplayName: def.DisplayName,
|
||||
DisplayName: localizedAgentProviderName(key),
|
||||
BaseURL: baseURL,
|
||||
DefaultAPIType: def.DefaultAPIType,
|
||||
APITypes: apiTypes,
|
||||
@@ -1016,7 +1035,7 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
||||
if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
initialModels, err := buildInitialAgentAccountModels(&model.AgentAccount{Provider: provider}, req.Models)
|
||||
initialModels, err := buildInitialAgentAccountModels(&model.AgentAccount{Provider: provider, APIType: req.APIType}, req.Models)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1024,7 +1043,8 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel)
|
||||
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1062,6 +1082,9 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.APIType != account.APIType {
|
||||
return buserr.WithDetail("ErrInvalidParams", "API type cannot be changed", nil)
|
||||
}
|
||||
provider := account.Provider
|
||||
if err := ensureAgentAccountNameAvailable(provider, req.Name, account.ID); err != nil {
|
||||
return err
|
||||
@@ -1078,7 +1101,8 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel)
|
||||
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1110,6 +1134,12 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
||||
if strings.TrimSpace(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) != "" {
|
||||
opts = append(opts, repo.WithByLikeName(req.Name))
|
||||
}
|
||||
@@ -1127,7 +1157,7 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
||||
ID: item.ID,
|
||||
MasterAccountID: item.MasterAccountID,
|
||||
Provider: item.Provider,
|
||||
ProviderName: providercatalog.DisplayName(item.Provider),
|
||||
ProviderName: localizedAgentProviderName(item.Provider),
|
||||
Name: item.Name,
|
||||
APIKey: apiKey,
|
||||
RememberAPIKey: item.RememberAPIKey,
|
||||
@@ -1166,7 +1196,7 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -1178,7 +1208,8 @@ func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.Agen
|
||||
}
|
||||
|
||||
func (a AgentService) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) {
|
||||
if req.APIType != "openai-completions" && req.APIType != "openai-responses" {
|
||||
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)
|
||||
@@ -1320,7 +1351,7 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error {
|
||||
}
|
||||
|
||||
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
||||
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model)
|
||||
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1410,7 +1441,7 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
auth := readHermesDashboardAuthFromInstall(install)
|
||||
auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
|
||||
return &dto.AgentOtherConfig{
|
||||
UserTimezone: cfg.Timezone,
|
||||
BrowserEnabled: true,
|
||||
@@ -1419,6 +1450,13 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
|
||||
DashboardPassword: auth.Password,
|
||||
}, 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1437,16 +1475,19 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
||||
return err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
if strings.TrimSpace(req.UserTimezone) == "" {
|
||||
return buserr.New("ErrInvalidParams")
|
||||
}
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
previousAuth := readHermesDashboardAuthFromInstall(install)
|
||||
nextAuth := normalizeHermesDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||
previousAuth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
|
||||
nextAuth := normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeHermesDashboardAuthEnv(path.Join(install.GetPath(), ".env"), nextAuth, true); err != nil {
|
||||
if err := writeAgentDashboardAuthEnv(install.GetEnvPath(), agent.AgentType, nextAuth, true); err != nil {
|
||||
return err
|
||||
}
|
||||
operate := constant.Restart
|
||||
@@ -1458,6 +1499,12 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -1638,7 +1685,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
|
||||
return err
|
||||
}
|
||||
fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil {
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
case constant.AppHermesAgent:
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -1320,6 +1321,10 @@ func appendPluginAllow(conf map[string]interface{}, pluginID string) {
|
||||
}
|
||||
|
||||
func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID string) error {
|
||||
help, err := cmd.RunDockerExecWithStdout(time.Minute, containerName, "openclaw", "plugins", "install", "--help")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workdir := path.Join(openclawPluginPackageTmpDir, pluginID)
|
||||
defer func() {
|
||||
_ = mgr.Run("docker", "exec", containerName, "rm", "-rf", workdir)
|
||||
@@ -1341,7 +1346,19 @@ func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID
|
||||
if pkgPath == "" {
|
||||
return fmt.Errorf("openclaw plugin package not found")
|
||||
}
|
||||
return mgr.Run("docker", "exec", containerName, "openclaw", "plugins", "install", pkgPath, "--dangerously-force-unsafe-install")
|
||||
args := []string{"exec", containerName, "openclaw", "plugins", "install", pkgPath}
|
||||
// Newer CLIs require source confirmation; older releases do not support --force.
|
||||
options := strings.Fields(help)
|
||||
if slices.Contains(options, "--force") {
|
||||
args = append(args, "--force")
|
||||
} else if slices.Contains(options, "--dangerously-force-unsafe-install") {
|
||||
args = append(args, "--dangerously-force-unsafe-install")
|
||||
}
|
||||
// Source confirmation does not grant the selected channel plugin's capabilities.
|
||||
if slices.Contains(options, "--accept-capabilities") {
|
||||
args = append(args, "--accept-capabilities")
|
||||
}
|
||||
return mgr.Run("docker", args...)
|
||||
}
|
||||
|
||||
func uninstallOpenclawPlugin(mgr *cmd.CommandHelper, containerName, pluginID string) error {
|
||||
|
||||
@@ -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 hermesExecutablePath = "/opt/hermes/.venv/bin/hermes"
|
||||
const hermesDashboardUsernameEnvKey = "HERMES_DASHBOARD_USERNAME"
|
||||
const hermesDashboardPasswordEnvKey = "HERMES_DASHBOARD_PASSWORD"
|
||||
|
||||
type hermesDashboardAuth struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
type hermesConfig struct {
|
||||
Model hermesModelConfig `yaml:"model"`
|
||||
@@ -117,52 +110,6 @@ func prepareHermesInstallFiles(appInstall *model.AppInstall, account *model.Agen
|
||||
return files.NewFileOp().ChownR(dataDir, "1000", "1000", true)
|
||||
}
|
||||
|
||||
func normalizeHermesDashboardAuth(username, password string) hermesDashboardAuth {
|
||||
auth := hermesDashboardAuth{
|
||||
Username: strings.TrimSpace(username),
|
||||
Password: strings.TrimSpace(password),
|
||||
}
|
||||
if auth.Username == "" {
|
||||
auth.Username = "admin"
|
||||
}
|
||||
if auth.Password == "" {
|
||||
auth.Password = common.RandStr(8)
|
||||
}
|
||||
return auth
|
||||
}
|
||||
|
||||
func writeHermesDashboardAuthEnv(envPath string, auth hermesDashboardAuth, overwrite bool) error {
|
||||
return upsertAgentEnv(envPath, map[string]string{
|
||||
hermesDashboardUsernameEnvKey: auth.Username,
|
||||
hermesDashboardPasswordEnvKey: auth.Password,
|
||||
}, []string{
|
||||
hermesDashboardUsernameEnvKey,
|
||||
hermesDashboardPasswordEnvKey,
|
||||
}, overwrite)
|
||||
}
|
||||
|
||||
func readHermesDashboardAuthEnv(envPath string) (hermesDashboardAuth, error) {
|
||||
envMap, err := readAgentEnvMap(envPath)
|
||||
if err != nil {
|
||||
return hermesDashboardAuth{}, err
|
||||
}
|
||||
return hermesDashboardAuth{
|
||||
Username: strings.TrimSpace(envMap[hermesDashboardUsernameEnvKey]),
|
||||
Password: strings.TrimSpace(envMap[hermesDashboardPasswordEnvKey]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readHermesDashboardAuthFromInstall(appInstall *model.AppInstall) hermesDashboardAuth {
|
||||
if appInstall == nil || appInstall.ID == 0 {
|
||||
return hermesDashboardAuth{}
|
||||
}
|
||||
auth, err := readHermesDashboardAuthEnv(path.Join(appInstall.GetPath(), ".env"))
|
||||
if err != nil {
|
||||
return hermesDashboardAuth{}
|
||||
}
|
||||
return auth
|
||||
}
|
||||
|
||||
func readHermesConfig(configPath string) (*hermesConfig, error) {
|
||||
content, err := files.NewFileOp().GetContent(configPath)
|
||||
if err != nil {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
@@ -96,7 +98,7 @@ func ensureContainerRunning(containerName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string) (resolvedAgentAccountInput, error) {
|
||||
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability bool) (resolvedAgentAccountInput, error) {
|
||||
resolvedAPIKey := strings.TrimSpace(apiKey)
|
||||
resolvedAPIType := strings.TrimSpace(apiType)
|
||||
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||
@@ -114,7 +116,8 @@ func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, mode
|
||||
if modelID == "" {
|
||||
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
||||
}
|
||||
if !providercatalog.SkipVerification(provider) {
|
||||
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
|
||||
if validateAvailability && (imageAPI || providercatalog.IsEmbeddingAPIType(resolvedAPIType) || !providercatalog.SkipVerification(provider)) {
|
||||
if err := providercatalog.VerifyAccount(provider, resolvedAPIType, resolvedAuthMode, resolvedBaseURL, resolvedAPIKey, modelID); err != nil {
|
||||
return resolvedAgentAccountInput{}, err
|
||||
}
|
||||
@@ -386,7 +389,7 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
||||
Remark: agent.Remark,
|
||||
AgentType: agentType,
|
||||
Provider: agent.Provider,
|
||||
ProviderName: providercatalog.DisplayName(agent.Provider),
|
||||
ProviderName: localizedAgentProviderName(agent.Provider),
|
||||
Model: agent.Model,
|
||||
APIType: agent.APIType,
|
||||
BaseURL: agent.BaseURL,
|
||||
@@ -420,8 +423,8 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
||||
item.BridgePort = toInt(bridge)
|
||||
}
|
||||
}
|
||||
if agentType == constant.AppHermesAgent {
|
||||
auth := readHermesDashboardAuthFromInstall(appInstall)
|
||||
if _, _, ok := agentDashboardAuthEnvKeys(agentType); ok {
|
||||
auth := readAgentDashboardAuthFromInstall(appInstall, agentType)
|
||||
item.DashboardUsername = auth.Username
|
||||
item.DashboardPassword = auth.Password
|
||||
}
|
||||
@@ -429,6 +432,15 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
||||
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 {
|
||||
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
||||
}
|
||||
@@ -726,9 +738,11 @@ type modelProvider struct {
|
||||
}
|
||||
|
||||
type modelEntry struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Input []string `json:"input,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Input []string `json:"input,omitempty"`
|
||||
ContextWindow int `json:"contextWindow,omitempty"`
|
||||
MaxTokens int `json:"maxTokens,omitempty"`
|
||||
}
|
||||
|
||||
func requiresOpenclawProviderModels(provider string) bool {
|
||||
@@ -756,7 +770,7 @@ type browserConfig struct {
|
||||
DefaultProfile string `json:"defaultProfile"`
|
||||
}
|
||||
|
||||
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string) error {
|
||||
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string, metadata []dto.AgentModelMetadata) error {
|
||||
if strings.TrimSpace(confDir) == "" {
|
||||
return fmt.Errorf("config dir is required")
|
||||
}
|
||||
@@ -841,6 +855,7 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
}
|
||||
conf = initial
|
||||
} else {
|
||||
preserveOpenclawModelMetadata(conf, cfg.Models)
|
||||
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -895,6 +910,9 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
if allowedOrigins != nil {
|
||||
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
|
||||
}
|
||||
if err := applyOpenclawModelMetadata(conf, account, metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -909,6 +927,144 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order)
|
||||
}
|
||||
|
||||
func readOpenclawModelsConfig(conf map[string]interface{}) *modelsConfig {
|
||||
raw, ok := conf["models"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
payload, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var models modelsConfig
|
||||
if err := json.Unmarshal(payload, &models); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &models
|
||||
}
|
||||
|
||||
func preserveOpenclawModelMetadata(conf map[string]interface{}, next *modelsConfig) {
|
||||
current := readOpenclawModelsConfig(conf)
|
||||
if current == nil || next == nil {
|
||||
return
|
||||
}
|
||||
for providerID, nextProvider := range next.Providers {
|
||||
currentProvider, ok := current.Providers[providerID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
byID := make(map[string]modelEntry, len(currentProvider.Models))
|
||||
for _, entry := range currentProvider.Models {
|
||||
byID[entry.ID] = entry
|
||||
}
|
||||
for index := range nextProvider.Models {
|
||||
currentEntry, ok := byID[nextProvider.Models[index].ID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
nextProvider.Models[index].Input = currentEntry.Input
|
||||
nextProvider.Models[index].ContextWindow = currentEntry.ContextWindow
|
||||
nextProvider.Models[index].MaxTokens = currentEntry.MaxTokens
|
||||
}
|
||||
next.Providers[providerID] = nextProvider
|
||||
}
|
||||
}
|
||||
|
||||
func extractOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, accountModels []dto.AgentAccountModel) []dto.AgentModelMetadata {
|
||||
result := make([]dto.AgentModelMetadata, 0, len(accountModels))
|
||||
configured := readOpenclawModelsConfig(conf)
|
||||
for _, item := range accountModels {
|
||||
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
metadata := dto.AgentModelMetadata{Model: item.ID, InputMode: "auto"}
|
||||
if configured != nil {
|
||||
for _, entry := range configured.Providers[providerID].Models {
|
||||
if entry.ID != inferred.ID {
|
||||
continue
|
||||
}
|
||||
metadata.ContextWindow = entry.ContextWindow
|
||||
metadata.MaxTokens = entry.MaxTokens
|
||||
if len(entry.Input) > 0 && !slices.Equal(entry.Input, inferred.Input) {
|
||||
if slices.Contains(entry.Input, "image") {
|
||||
metadata.InputMode = "image"
|
||||
} else {
|
||||
metadata.InputMode = "text"
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
result = append(result, metadata)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func applyOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, requested []dto.AgentModelMetadata) error {
|
||||
if len(requested) == 0 {
|
||||
return nil
|
||||
}
|
||||
configured := readOpenclawModelsConfig(conf)
|
||||
if configured == nil {
|
||||
return fmt.Errorf("model metadata is not supported for provider %s", account.Provider)
|
||||
}
|
||||
accountModels, err := loadAgentAccountModels(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
available := make(map[string]dto.AgentAccountModel, len(accountModels))
|
||||
for _, item := range accountModels {
|
||||
available[item.ID] = item
|
||||
}
|
||||
seen := make(map[string]struct{}, len(requested))
|
||||
for _, metadata := range requested {
|
||||
item, ok := available[metadata.Model]
|
||||
if !ok {
|
||||
return buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
if _, ok := seen[metadata.Model]; ok {
|
||||
return fmt.Errorf("duplicate model metadata: %s", metadata.Model)
|
||||
}
|
||||
seen[metadata.Model] = struct{}{}
|
||||
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provider := configured.Providers[providerID]
|
||||
found := false
|
||||
for index := range provider.Models {
|
||||
if provider.Models[index].ID != inferred.ID {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
provider.Models[index].ContextWindow = metadata.ContextWindow
|
||||
provider.Models[index].MaxTokens = metadata.MaxTokens
|
||||
switch metadata.InputMode {
|
||||
case "auto":
|
||||
provider.Models[index].Input = inferred.Input
|
||||
case "text":
|
||||
provider.Models[index].Input = []string{"text"}
|
||||
case "image":
|
||||
provider.Models[index].Input = []string{"text", "image"}
|
||||
default:
|
||||
return fmt.Errorf("unsupported model input mode: %s", metadata.InputMode)
|
||||
}
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
return buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
configured.Providers[providerID] = provider
|
||||
}
|
||||
modelsMap, err := structToMap(configured)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conf["models"] = modelsMap
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
|
||||
accountModels, err := loadAgentAccountModels(account)
|
||||
if err != nil {
|
||||
@@ -1030,7 +1186,7 @@ func prepareOpenclawInstallFiles(appInstall *model.AppInstall, account *model.Ag
|
||||
return fmt.Errorf("app install is required")
|
||||
}
|
||||
confDir := path.Join(appInstall.GetPath(), "data", "conf")
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil); err != nil {
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
dataDir := path.Join(appInstall.GetPath(), "data")
|
||||
@@ -1159,15 +1315,15 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto
|
||||
if len(requested) > 0 {
|
||||
return normalizeAgentAccountModels(account, requested)
|
||||
}
|
||||
meta, ok := providercatalog.Get(account.Provider)
|
||||
if !ok || len(meta.Models) == 0 {
|
||||
defaultModels := providercatalog.DefaultModels(account.Provider, account.APIType)
|
||||
if len(defaultModels) == 0 {
|
||||
if requiresInitialAgentAccountModels(account.Provider) {
|
||||
return nil, buserr.New("ErrAgentAccountModelsRequired")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
requested = make([]dto.AgentAccountModel, 0, len(meta.Models))
|
||||
for _, item := range meta.Models {
|
||||
requested = make([]dto.AgentAccountModel, 0, len(defaultModels))
|
||||
for _, item := range defaultModels {
|
||||
requested = append(requested, dto.AgentAccountModel{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
@@ -1327,7 +1483,7 @@ func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAcco
|
||||
|
||||
func requiresInitialAgentAccountModels(provider string) bool {
|
||||
switch provider {
|
||||
case "custom", "vllm", "ollama":
|
||||
case "custom", "vllm", "ollama", "llmman":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -1452,6 +1608,87 @@ func readInstallEnv(envStr string) map[string]interface{} {
|
||||
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) {
|
||||
fileOp := files.NewFileOp()
|
||||
if !fileOp.Stat(envPath) {
|
||||
|
||||
+434
-146
@@ -4,11 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
@@ -17,11 +14,13 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
|
||||
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/email"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
|
||||
)
|
||||
|
||||
type AlertService struct{}
|
||||
@@ -34,6 +33,28 @@ var communityAlertMethodTypeNames = map[string]string{
|
||||
constant.SMS: "SMS",
|
||||
}
|
||||
|
||||
var legacyAlertMethodTypeMap = map[string]string{
|
||||
"mail": constant.Email,
|
||||
constant.Email: constant.Email,
|
||||
constant.SMS: constant.SMS,
|
||||
constant.Bark: constant.Bark,
|
||||
constant.WeChat: constant.WeCom,
|
||||
constant.WeCom: constant.WeCom,
|
||||
constant.DingTalk: constant.DingTalk,
|
||||
constant.FeiShu: constant.FeiShu,
|
||||
constant.Custom: constant.Custom,
|
||||
}
|
||||
|
||||
var supportedAlertMethodTypes = map[string]struct{}{
|
||||
constant.Email: {},
|
||||
constant.SMS: {},
|
||||
constant.Bark: {},
|
||||
constant.WeCom: {},
|
||||
constant.DingTalk: {},
|
||||
constant.FeiShu: {},
|
||||
constant.Custom: {},
|
||||
}
|
||||
|
||||
type IAlertService interface {
|
||||
PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error)
|
||||
GetAlerts() ([]dto.AlertDTO, error)
|
||||
@@ -53,8 +74,10 @@ type IAlertService interface {
|
||||
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
|
||||
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
|
||||
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
|
||||
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
|
||||
DeleteAlertConfig(id uint) error
|
||||
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
||||
TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error)
|
||||
}
|
||||
|
||||
func NewIAlertService() IAlertService {
|
||||
@@ -82,7 +105,38 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
cronjobProjects := make(map[string]uint)
|
||||
var cronjobIDs []uint
|
||||
for _, item := range alerts {
|
||||
if alertUtil.GetCronJobType(item.Type) != "cronJob" {
|
||||
continue
|
||||
}
|
||||
if _, exists := cronjobProjects[item.Project]; exists {
|
||||
continue
|
||||
}
|
||||
id, parseErr := strconv.ParseUint(item.Project, 10, strconv.IntSize)
|
||||
if parseErr != nil || id == 0 {
|
||||
continue
|
||||
}
|
||||
cronjobProjects[item.Project] = uint(id)
|
||||
cronjobIDs = append(cronjobIDs, uint(id))
|
||||
}
|
||||
cronjobsByID := make(map[uint]model.Cronjob)
|
||||
if len(cronjobIDs) > 0 {
|
||||
cronjobs, err := cronjobRepo.List(repo.WithByIDs(cronjobIDs))
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
for _, cronjob := range cronjobs {
|
||||
cronjobsByID[cronjob.ID] = cronjob
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range alerts {
|
||||
var taskName string
|
||||
if cronjob, exists := cronjobsByID[cronjobProjects[item.Project]]; exists && cronjob.Type == item.Type {
|
||||
taskName = cronjob.Name
|
||||
}
|
||||
|
||||
result = append(result, dto.AlertDTO{
|
||||
ID: item.ID,
|
||||
@@ -92,6 +146,7 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
|
||||
Method: item.Method,
|
||||
Title: item.Title,
|
||||
Project: item.Project,
|
||||
TaskName: taskName,
|
||||
Status: item.Status,
|
||||
SendCount: item.SendCount,
|
||||
AdvancedParams: item.AdvancedParams,
|
||||
@@ -163,6 +218,16 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
advanced, err := prepareCronJobAlertParams(create.Type, "", create.AdvancedParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
create.AdvancedParams = advanced
|
||||
if create.Status != constant.AlertDisable {
|
||||
if err := a.validateCronJobAlertChannels(create.Type, advanced, create.Method); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
alertInfo.Status = constant.AlertEnable
|
||||
if err := copier.Copy(&alertInfo, &create); err != nil {
|
||||
return buserr.WithErr("ErrStructTransform", err)
|
||||
@@ -180,9 +245,28 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
|
||||
}
|
||||
|
||||
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
|
||||
if err := a.validateCommunityAlertMethod(req.Method); err != nil {
|
||||
if alertUtil.GetCronJobType(req.Type) == "cronJob" {
|
||||
previous, err := alertRepo.Get(repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.AdvancedParams, err = prepareCronJobAlertParams(req.Type, previous.AdvancedParams, req.AdvancedParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
methodTypes, err := a.validateAlertMethodReferences(req.Method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Status != constant.AlertDisable {
|
||||
if err := a.validateCronJobAlertChannels(req.Type, req.AdvancedParams, req.Method); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
upMap := make(map[string]interface{})
|
||||
upMap["id"] = req.ID
|
||||
@@ -240,7 +324,19 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
|
||||
if alertInfo.ID == 0 {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
err := alertRepo.Update(map[string]interface{}{"status": status}, repo.WithByID(alertInfo.ID))
|
||||
methodTypes, err := a.validateAlertMethodReferences(alertInfo.Method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status == constant.AlertEnable {
|
||||
if err := a.validateCronJobAlertChannels(alertInfo.Type, alertInfo.AdvancedParams, alertInfo.Method); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = alertRepo.Update(map[string]interface{}{"status": status}, repo.WithByID(alertInfo.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -257,122 +353,14 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
|
||||
}
|
||||
|
||||
func (a AlertService) GetDisks() ([]dto.DiskDTO, error) {
|
||||
var disks []dto.DiskDTO
|
||||
excludes := map[string]struct{}{
|
||||
"/mnt/cdrom": {}, "/boot": {}, "/boot/efi": {}, "/dev": {}, "/dev/shm": {},
|
||||
"/run/lock": {}, "/run": {}, "/run/shm": {}, "/run/user": {},
|
||||
infos := loadDiskInfo(true)
|
||||
disks := make([]dto.DiskDTO, 0, len(infos))
|
||||
for _, item := range infos {
|
||||
disks = append(disks, dto.DiskDTO(item))
|
||||
}
|
||||
stdout, err := executeDiskCommand()
|
||||
if err != nil {
|
||||
return disks, nil
|
||||
}
|
||||
|
||||
lines := strings.Split(stdout, "\n")
|
||||
var mounts []dto.AlertDiskInfo
|
||||
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 7 {
|
||||
continue
|
||||
}
|
||||
mountPoint := strings.Join(fields[6:], " ")
|
||||
if shouldExclude(fields, mountPoint, excludes) {
|
||||
continue
|
||||
}
|
||||
mounts = append(mounts, dto.AlertDiskInfo{Type: fields[1], Device: fields[0], Mount: mountPoint})
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
)
|
||||
wg.Add(len(mounts))
|
||||
for i := 0; i < len(mounts); i++ {
|
||||
go func(timeoutCh <-chan time.Time, mount dto.AlertDiskInfo) {
|
||||
defer wg.Done()
|
||||
|
||||
var itemData dto.DiskDTO
|
||||
itemData.Path = mount.Mount
|
||||
itemData.Type = mount.Type
|
||||
itemData.Device = mount.Device
|
||||
select {
|
||||
case <-timeoutCh:
|
||||
mu.Lock()
|
||||
disks = append(disks, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
|
||||
default:
|
||||
state, err := disk.Usage(mount.Mount)
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
disks = append(disks, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
|
||||
return
|
||||
}
|
||||
itemData.Total = state.Total
|
||||
itemData.Free = state.Free
|
||||
itemData.Used = state.Used
|
||||
itemData.UsedPercent = state.UsedPercent
|
||||
itemData.InodesTotal = state.InodesTotal
|
||||
itemData.InodesUsed = state.InodesUsed
|
||||
itemData.InodesFree = state.InodesFree
|
||||
itemData.InodesUsedPercent = state.InodesUsedPercent
|
||||
mu.Lock()
|
||||
disks = append(disks, itemData)
|
||||
mu.Unlock()
|
||||
}
|
||||
}(time.After(5*time.Second), mounts[i])
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
sort.Slice(disks, func(i, j int) bool {
|
||||
return disks[i].Path < disks[j].Path
|
||||
})
|
||||
return disks, nil
|
||||
}
|
||||
|
||||
func executeDiskCommand() (string, error) {
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||
stdout, err := cmdMgr.RunWithStdout("df", "-hT", "-P")
|
||||
if err != nil {
|
||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||
stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P")
|
||||
}
|
||||
if err != nil {
|
||||
return stdout, err
|
||||
}
|
||||
var lines []string
|
||||
for _, line := range strings.Split(stdout, "\n") {
|
||||
if !strings.Contains(line, "/") || strings.Contains(line, "tmpfs") || strings.Contains(line, "snap/core") || strings.Contains(line, "udev") {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
if len(lines) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
func shouldExclude(fields []string, mountPoint string, excludes map[string]struct{}) bool {
|
||||
if strings.HasPrefix(mountPoint, "/snap") || len(strings.Split(mountPoint, "/")) > 10 {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(fields[1]) == "tmpfs" {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(fields[2], "K") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(mountPoint, "docker") {
|
||||
return true
|
||||
}
|
||||
_, excluded := excludes[mountPoint]
|
||||
return excluded
|
||||
}
|
||||
|
||||
func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) {
|
||||
var (
|
||||
opts []repo.DBOption
|
||||
@@ -384,6 +372,9 @@ func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.Ale
|
||||
if search.Count != 0 {
|
||||
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"))
|
||||
|
||||
total, alerts, err := alertRepo.PageLog(search.Page, search.PageSize, opts...)
|
||||
@@ -409,6 +400,7 @@ func (a AlertService) parseAlertLog(item model.AlertLog) (dto.AlertLogDTO, error
|
||||
if err := unmarshalAlertInfo(item.AlertDetail, &alertDetail); err != nil {
|
||||
return dto.AlertLogDTO{}, err
|
||||
}
|
||||
alertDetail.Task = nil
|
||||
if err := unmarshalAlertInfo(item.AlertRule, &alertRule); err != nil {
|
||||
return dto.AlertLogDTO{}, err
|
||||
}
|
||||
@@ -491,7 +483,13 @@ func (a AlertService) GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertCon
|
||||
}
|
||||
opts = append(opts, repo.WithByStatus(constant.AlertEnable))
|
||||
configs, err := alertRepo.AlertConfigList(opts...)
|
||||
return configs, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := exposeCustomAlertConfigSecrets(configs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error) {
|
||||
@@ -502,13 +500,49 @@ func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []mode
|
||||
if len(req.ExcludeTypes) > 0 {
|
||||
opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes))
|
||||
}
|
||||
return alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...)
|
||||
total, configs, err := alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if err := exposeCustomAlertConfigSecrets(configs); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return total, configs, nil
|
||||
}
|
||||
|
||||
func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error {
|
||||
if req.Type == constant.Custom {
|
||||
if req.ID != 0 && req.Revision == nil {
|
||||
return repo.ErrAlertConfigRevisionRequired
|
||||
}
|
||||
return a.updateCustomAlertConfig(req, operator)
|
||||
}
|
||||
usesMutation, err := alertconfig.UsesMutation(req.Type, req.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.ID != 0 && usesMutation && req.Revision == nil {
|
||||
return repo.ErrAlertConfigRevisionRequired
|
||||
}
|
||||
var existing *model.AlertConfig
|
||||
if req.ID != 0 {
|
||||
stored, err := alertRepo.GetConfigById(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stored.Type != req.Type {
|
||||
return fmt.Errorf("alert config %d has type %s, not %s", req.ID, stored.Type, req.Type)
|
||||
}
|
||||
existing = &stored
|
||||
}
|
||||
if err := a.validateCommunityAlertConfigType(req.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
prepared, err := alertconfig.Prepare(req.Type, req.Config, req.Status, existing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Config = prepared
|
||||
if err := a.checkAlertConfigDisplayNameUnique(req); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -523,7 +557,7 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator stri
|
||||
upMap["status"] = req.Status
|
||||
upMap["config"] = req.Config
|
||||
upMap["update_user"] = operator
|
||||
if err := alertRepo.UpdateAlertConfig(upMap, repo.WithByID(req.ID)); err != nil {
|
||||
if err := alertRepo.UpdateAlertConfigWithRevision(upMap, req.Revision, repo.WithByID(req.ID)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
@@ -541,6 +575,99 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AlertService) updateCustomAlertConfig(req dto.AlertConfigUpdate, operator string) error {
|
||||
if err := validateAlertConfigStatus(req.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var existing *model.AlertConfig
|
||||
if req.ID != 0 {
|
||||
config, err := alertRepo.GetConfigById(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config.Type != constant.Custom {
|
||||
return fmt.Errorf("alert config %d is not a custom webhook", req.ID)
|
||||
}
|
||||
existing = &config
|
||||
}
|
||||
prepared, err := alertwebhook.Prepare(req.Config, req.Status, existing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
validatedReq := req
|
||||
validatedReq.Config = prepared.Config
|
||||
if err := a.checkAlertConfigDisplayNameUnique(validatedReq); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if existing != nil {
|
||||
return alertRepo.UpdateAlertConfigWithRevision(map[string]interface{}{
|
||||
"type": constant.Custom,
|
||||
"title": req.Title,
|
||||
"status": req.Status,
|
||||
"config": prepared.Config,
|
||||
"secret_config": prepared.SecretConfig,
|
||||
"update_user": operator,
|
||||
}, req.Revision, repo.WithByID(req.ID))
|
||||
}
|
||||
|
||||
return alertRepo.CreateAlertConfig(&model.AlertConfig{
|
||||
Type: constant.Custom,
|
||||
Title: req.Title,
|
||||
Status: req.Status,
|
||||
Config: prepared.Config,
|
||||
SecretConfig: prepared.SecretConfig,
|
||||
CreateUser: operator,
|
||||
UpdateUser: operator,
|
||||
})
|
||||
}
|
||||
|
||||
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error {
|
||||
if err := validateAlertConfigStatus(req.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
config, err := alertRepo.GetConfigById(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Status == constant.AlertEnable {
|
||||
if err := a.validateCommunityAlertConfigType(config.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
if config.Type == constant.Custom {
|
||||
if _, err := alertwebhook.Resolve(config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return alertRepo.UpdateAlertConfig(map[string]interface{}{
|
||||
"status": req.Status,
|
||||
"update_user": operator,
|
||||
}, repo.WithByID(req.ID))
|
||||
}
|
||||
|
||||
func validateAlertConfigStatus(status string) error {
|
||||
if status != constant.AlertEnable && status != constant.AlertDisable {
|
||||
return fmt.Errorf("alert config status must be Enable or Disable")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func exposeCustomAlertConfigSecrets(configs []model.AlertConfig) error {
|
||||
for index := range configs {
|
||||
if configs[index].Type != constant.Custom {
|
||||
continue
|
||||
}
|
||||
view, err := alertwebhook.PlainView(configs[index])
|
||||
if err != nil {
|
||||
return fmt.Errorf("build editable custom alert config %d: %w", configs[index].ID, err)
|
||||
}
|
||||
configs[index].Config = view
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate) error {
|
||||
if req.Type != constant.SMSConfig {
|
||||
return nil
|
||||
@@ -565,6 +692,9 @@ func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate)
|
||||
}
|
||||
|
||||
func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdate) error {
|
||||
if req.Type != constant.Custom && (global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn") {
|
||||
return nil
|
||||
}
|
||||
displayName := alertConfigDisplayName(req.Type, req.Config)
|
||||
if displayName == "" {
|
||||
return nil
|
||||
@@ -588,37 +718,67 @@ func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdat
|
||||
}
|
||||
|
||||
func (a AlertService) validateCommunityAlertMethod(method string) error {
|
||||
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(method) == "" {
|
||||
return nil
|
||||
methodTypes, err := a.validateAlertMethodReferences(method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.validateAlertMethodEntitlement(methodTypes)
|
||||
}
|
||||
|
||||
func (a AlertService) validateAlertMethodReferences(method string) ([]string, error) {
|
||||
if strings.TrimSpace(method) == "" {
|
||||
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||
}
|
||||
methodTypes := make([]string, 0)
|
||||
for _, item := range strings.Split(method, ",") {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
configType := ""
|
||||
if configID, err := strconv.ParseUint(item, 10, 64); err == nil {
|
||||
config, err := alertRepo.GetConfigById(uint(configID))
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := communityAlertMethodTypeNames[config.Type]; ok {
|
||||
return buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||
configType = config.Type
|
||||
} else {
|
||||
var ok bool
|
||||
configType, ok = legacyAlertMethodTypeMap[item]
|
||||
if !ok {
|
||||
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||
}
|
||||
}
|
||||
if _, ok := supportedAlertMethodTypes[configType]; !ok {
|
||||
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||
}
|
||||
methodTypes = append(methodTypes, configType)
|
||||
}
|
||||
if len(methodTypes) == 0 {
|
||||
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||
}
|
||||
return methodTypes, nil
|
||||
}
|
||||
|
||||
func (a AlertService) validateAlertMethodEntitlement(methodTypes []string) error {
|
||||
for _, configType := range methodTypes {
|
||||
if configType == constant.Custom {
|
||||
continue
|
||||
}
|
||||
if _, ok := communityAlertMethodTypeNames[item]; ok {
|
||||
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
|
||||
continue
|
||||
}
|
||||
if _, ok := communityAlertMethodTypeNames[configType]; ok {
|
||||
return buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AlertService) validateCommunityAlertConfigType(configType string) error {
|
||||
if configType == constant.Custom {
|
||||
return nil
|
||||
}
|
||||
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
|
||||
return nil
|
||||
}
|
||||
@@ -630,7 +790,7 @@ func (a AlertService) validateCommunityAlertConfigType(configType string) error
|
||||
|
||||
func alertConfigDisplayName(configType, configData string) string {
|
||||
switch configType {
|
||||
case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark, constant.SMS:
|
||||
case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark, constant.SMS, constant.Custom:
|
||||
var cfg struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
@@ -669,20 +829,24 @@ func (a AlertService) DeleteAlertConfig(id uint) error {
|
||||
}
|
||||
|
||||
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
||||
username := req.UserName
|
||||
if username == "" {
|
||||
username = req.Sender
|
||||
emailConfig, err := resolveEmailTestConfig(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
encodedDisplayName := mime.BEncoding.Encode("UTF-8", req.DisplayName)
|
||||
username := emailConfig.UserName
|
||||
if username == "" {
|
||||
username = emailConfig.Sender
|
||||
}
|
||||
encodedDisplayName := mime.BEncoding.Encode("UTF-8", emailConfig.DisplayName)
|
||||
cfg := email.SMTPConfig{
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Sender: req.Sender,
|
||||
Host: emailConfig.Host,
|
||||
Port: emailConfig.Port,
|
||||
Sender: emailConfig.Sender,
|
||||
Username: username,
|
||||
Password: req.Password,
|
||||
From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, req.Sender),
|
||||
Encryption: req.Encryption,
|
||||
Recipient: req.Recipient,
|
||||
Password: emailConfig.Password,
|
||||
From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, emailConfig.Sender),
|
||||
Encryption: emailConfig.Encryption,
|
||||
Recipient: emailConfig.Recipient,
|
||||
}
|
||||
|
||||
msg := email.EmailMessage{
|
||||
@@ -697,9 +861,94 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func resolveEmailTestConfig(req dto.AlertConfigTest) (dto.AlertEmailConfig, error) {
|
||||
emailConfig := dto.AlertEmailConfig{
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Sender: req.Sender,
|
||||
UserName: req.UserName,
|
||||
Password: req.Password,
|
||||
DisplayName: req.DisplayName,
|
||||
Encryption: req.Encryption,
|
||||
Recipient: req.Recipient,
|
||||
}
|
||||
if strings.TrimSpace(req.Config) != "" {
|
||||
configType := req.Type
|
||||
if configType == "" {
|
||||
configType = constant.EmailConfig
|
||||
}
|
||||
if configType != constant.EmailConfig {
|
||||
return dto.AlertEmailConfig{}, fmt.Errorf("alert config test type must be email")
|
||||
}
|
||||
var existing *model.AlertConfig
|
||||
if req.ID != 0 {
|
||||
stored, err := alertRepo.GetConfigById(req.ID)
|
||||
if err != nil {
|
||||
return dto.AlertEmailConfig{}, err
|
||||
}
|
||||
existing = &stored
|
||||
}
|
||||
prepared, err := alertconfig.Prepare(configType, req.Config, constant.AlertEnable, existing)
|
||||
if err != nil {
|
||||
return dto.AlertEmailConfig{}, err
|
||||
}
|
||||
if err := json.Unmarshal([]byte(prepared), &emailConfig); err != nil {
|
||||
return dto.AlertEmailConfig{}, fmt.Errorf("decode email alert config: %w", err)
|
||||
}
|
||||
}
|
||||
return emailConfig, nil
|
||||
}
|
||||
|
||||
func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error) {
|
||||
if req.Type != constant.Custom {
|
||||
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config test type must be custom")
|
||||
}
|
||||
var existing *model.AlertConfig
|
||||
if req.ID != 0 {
|
||||
config, err := alertRepo.GetConfigById(req.ID)
|
||||
if err != nil {
|
||||
return dto.AlertConfigTestResult{}, err
|
||||
}
|
||||
if config.Type != constant.Custom {
|
||||
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config %d is not a custom webhook", req.ID)
|
||||
}
|
||||
existing = &config
|
||||
}
|
||||
prepared, err := alertwebhook.Prepare(req.Config, constant.AlertEnable, existing)
|
||||
if err != nil {
|
||||
return dto.AlertConfigTestResult{}, err
|
||||
}
|
||||
resolved, err := alertwebhook.Resolve(model.AlertConfig{
|
||||
Type: constant.Custom,
|
||||
Config: prepared.Config,
|
||||
SecretConfig: prepared.SecretConfig,
|
||||
})
|
||||
if err != nil {
|
||||
return dto.AlertConfigTestResult{}, err
|
||||
}
|
||||
tester, ok := xpack.AlertProvider.(providers.CustomWebhookTester)
|
||||
if !ok {
|
||||
return dto.AlertConfigTestResult{
|
||||
Success: false,
|
||||
Message: providers.ErrCustomWebhookUnsupported.Error(),
|
||||
}, nil
|
||||
}
|
||||
return tester.TestCustomWebhook(resolved)
|
||||
}
|
||||
|
||||
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
|
||||
if err := a.validateCommunityAlertMethod(updateAlert.Method); err != nil {
|
||||
return err
|
||||
var methodTypes []string
|
||||
if updateAlert.SendCount != 0 || strings.TrimSpace(updateAlert.Method) != "" {
|
||||
var err error
|
||||
methodTypes, err = a.validateAlertMethodReferences(updateAlert.Method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if updateAlert.SendCount != 0 {
|
||||
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
upMap := make(map[string]interface{})
|
||||
var newStatus string
|
||||
@@ -718,6 +967,23 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
||||
alertRepo.WithByType(updateAlert.Type),
|
||||
alertRepo.WithByProject(updateAlert.Project),
|
||||
)
|
||||
advanced, err := prepareCronJobAlertParams(updateAlert.Type, alertInfo.AdvancedParams, updateAlert.AdvancedParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateAlert.AdvancedParams = advanced
|
||||
if alertUtil.GetCronJobType(updateAlert.Type) == "cronJob" {
|
||||
upMap["advanced_params"] = advanced
|
||||
}
|
||||
if newStatus == constant.AlertEnable {
|
||||
method := updateAlert.Method
|
||||
if method == "" {
|
||||
method = alertInfo.Method
|
||||
}
|
||||
if err := a.validateCronJobAlertChannels(updateAlert.Type, advanced, method); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if alertInfo.ID > 0 {
|
||||
shouldUpdate := false
|
||||
@@ -731,6 +997,9 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
||||
if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method {
|
||||
shouldUpdate = true
|
||||
}
|
||||
if val, ok := upMap["advanced_params"]; ok && val != alertInfo.AdvancedParams {
|
||||
shouldUpdate = true
|
||||
}
|
||||
|
||||
if shouldUpdate {
|
||||
if err := alertRepo.Update(
|
||||
@@ -752,3 +1021,22 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareCronJobAlertParams(alertType, previous, incoming string) (string, error) {
|
||||
if alertUtil.GetCronJobType(alertType) != "cronJob" {
|
||||
return incoming, nil
|
||||
}
|
||||
return alertUtil.MergeCronJobAlertParams(previous, incoming)
|
||||
}
|
||||
|
||||
func (a AlertService) validateCronJobAlertChannels(alertType, advanced, method string) error {
|
||||
if alertUtil.GetCronJobType(alertType) != "cronJob" {
|
||||
return nil
|
||||
}
|
||||
mode, err := alertUtil.CronJobAlertTriggerMode(advanced)
|
||||
if err != nil || mode != alertUtil.CronJobAlertSuccess {
|
||||
return err
|
||||
}
|
||||
_, err = a.validateAlertMethodReferences(method)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
@@ -29,9 +30,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ResourceAlertInterval = 30
|
||||
CheckIntervalSec = 3
|
||||
LoadCheckIntervalMin = 5
|
||||
ResourceAlertInterval = 30
|
||||
CheckIntervalSec = 3
|
||||
LoadCheckIntervalMin = 5
|
||||
sshIPLoginWindow = 30 * time.Minute
|
||||
sslAutoRenewAlertSkipDays = 31
|
||||
)
|
||||
|
||||
type AlertTaskHelper struct {
|
||||
@@ -512,10 +515,32 @@ func loadPanelLogin(alert dto.AlertDTO) {
|
||||
}
|
||||
|
||||
func loadSSHLogin(alert dto.AlertDTO) {
|
||||
count, isAlert, err := alertUtil.CountRecentFailedSSHLog(alert.Cycle, alert.Count)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Failed to count recent failed ssh login logs: %v", err)
|
||||
now := time.Now()
|
||||
failedWindow := time.Duration(alert.Cycle) * time.Minute
|
||||
loadWindow := failedWindow
|
||||
if loadWindow < sshIPLoginWindow {
|
||||
loadWindow = sshIPLoginWindow
|
||||
}
|
||||
location, err := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Failed to load timezone for ssh login logs: %v", err)
|
||||
location = time.Local
|
||||
}
|
||||
histories, err := loadSSHAlertHistories(defaultSSHLogDir, now.Add(-loadWindow), now, location)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Failed to load ssh login logs: %v", err)
|
||||
}
|
||||
interfaceAddrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
global.LOG.Warnf("Failed to load local IP addresses for ssh login alert: %v", err)
|
||||
}
|
||||
count, records := summarizeSSHLoginHistories(
|
||||
histories,
|
||||
now,
|
||||
failedWindow,
|
||||
sshSuccessLoginWhitelist(alert.AdvancedParams, interfaceAddrs),
|
||||
)
|
||||
isAlert := count >= int(alert.Count)
|
||||
if isAlert {
|
||||
params := []dto.Param{
|
||||
{
|
||||
@@ -531,12 +556,6 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
||||
}
|
||||
sendAlerts(alert, "sshLogin", strconv.Itoa(count), "sshLogin", params)
|
||||
}
|
||||
whitelist := strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n")
|
||||
records, err := alertUtil.FindRecentSuccessLoginNotInWhitelist(30, whitelist)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Failed to check recent failed ip ssh login logs: %v", err)
|
||||
}
|
||||
records = filterSSHLoginEntriesNotInWhitelist(records, whitelist)
|
||||
if len(records) > 0 {
|
||||
quota := strings.Join(records, "\n")
|
||||
params := []dto.Param{
|
||||
@@ -555,6 +574,19 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
||||
}
|
||||
}
|
||||
|
||||
func sshSuccessLoginWhitelist(configured string, interfaceAddrs []net.Addr) []string {
|
||||
whitelist := strings.Split(strings.TrimSpace(configured), "\n")
|
||||
whitelist = append(whitelist, "127.0.0.0/8", "::1")
|
||||
for _, addr := range interfaceAddrs {
|
||||
ipNet, ok := addr.(*net.IPNet)
|
||||
if !ok || ipNet.IP == nil || ipNet.IP.IsUnspecified() {
|
||||
continue
|
||||
}
|
||||
whitelist = append(whitelist, ipNet.IP.String())
|
||||
}
|
||||
return whitelist
|
||||
}
|
||||
|
||||
func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) []model.LoginLog {
|
||||
filtered := make([]model.LoginLog, 0, len(records))
|
||||
for _, record := range records {
|
||||
@@ -565,20 +597,6 @@ func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string)
|
||||
return filtered
|
||||
}
|
||||
|
||||
func filterSSHLoginEntriesNotInWhitelist(records []string, whitelist []string) []string {
|
||||
filtered := make([]string, 0, len(records))
|
||||
for _, record := range records {
|
||||
ip := record
|
||||
if idx := strings.Index(record, "-"); idx >= 0 {
|
||||
ip = record[:idx]
|
||||
}
|
||||
if !isIPInWhitelist(ip, whitelist) {
|
||||
filtered = append(filtered, record)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func isIPInWhitelist(ip string, whitelist []string) bool {
|
||||
targetIP := net.ParseIP(strings.TrimSpace(ip))
|
||||
if targetIP == nil {
|
||||
@@ -695,9 +713,10 @@ func sendAlertsByConfigId(alert dto.AlertDTO, alertType, quota, quotaType string
|
||||
|
||||
func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, method string) {
|
||||
typeMap := map[string]string{
|
||||
"mail": constant.Email,
|
||||
constant.Bark: constant.Bark,
|
||||
constant.SMS: constant.SMS,
|
||||
"mail": constant.Email,
|
||||
constant.Bark: constant.Bark,
|
||||
constant.SMS: constant.SMS,
|
||||
constant.Custom: constant.Custom,
|
||||
}
|
||||
configType, ok := typeMap[method]
|
||||
if !ok {
|
||||
@@ -785,7 +804,7 @@ func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params
|
||||
}
|
||||
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
|
||||
|
||||
case constant.WeCom, constant.DingTalk, constant.FeiShu:
|
||||
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Custom:
|
||||
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr)
|
||||
if !isValid {
|
||||
return
|
||||
@@ -798,12 +817,31 @@ func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params
|
||||
}
|
||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
|
||||
alertErr := xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo)
|
||||
queued := false
|
||||
var alertErr error
|
||||
if config.Type == constant.Custom {
|
||||
task := dto.AlertTaskMetadata{
|
||||
AlertID: alert.ID,
|
||||
Type: alertType,
|
||||
Quota: quota,
|
||||
QuotaType: quotaType,
|
||||
Method: methodStr,
|
||||
}
|
||||
result, deliveryErr := xpack.DeliverCustomWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo, task)
|
||||
queued, alertErr = result.Queued, deliveryErr
|
||||
if alertErr == nil && result.Queued {
|
||||
_, alertErr = alertUtil.RecordQueuedAlertTask(result.LogID, task)
|
||||
}
|
||||
} else {
|
||||
alertErr = xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo)
|
||||
}
|
||||
if alertErr != nil {
|
||||
global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, methodStr, alertErr)
|
||||
return
|
||||
}
|
||||
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
|
||||
if !queued {
|
||||
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -844,7 +882,7 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
|
||||
daysDiff := int(math.Ceil(
|
||||
ssl.ExpireDate.Sub(currentDate).Hours() / 24,
|
||||
))
|
||||
if daysDiff > 0 && int(cycle) >= daysDiff {
|
||||
if daysDiff > 0 && int(cycle) >= daysDiff && !shouldSuppressSSLExpiryAlert(ssl, daysDiff) {
|
||||
daysDiffMap[daysDiff] = append(daysDiffMap[daysDiff], ssl.PrimaryDomain)
|
||||
projectMap[ssl.ID] = append(projectMap[ssl.ID], ssl.ExpireDate)
|
||||
}
|
||||
@@ -852,6 +890,10 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
|
||||
return daysDiffMap, projectMap
|
||||
}
|
||||
|
||||
func shouldSuppressSSLExpiryAlert(ssl model.WebsiteSSL, remainingDays int) bool {
|
||||
return ssl.AutoRenew && remainingDays < sslAutoRenewAlertSkipDays
|
||||
}
|
||||
|
||||
func calculateWebsiteExpiryDays(websites []model.Website, cycle uint) (map[int][]string, map[uint][]time.Time) {
|
||||
currentDate := time.Now()
|
||||
daysDiffMap := make(map[int][]string)
|
||||
@@ -1024,50 +1066,37 @@ func processAllDisks(alert dto.AlertDTO) error {
|
||||
global.LOG.Errorf("error getting disk list, err: %v", err)
|
||||
return err
|
||||
}
|
||||
var errMsgs []string
|
||||
for _, item := range diskList {
|
||||
err := checkAndCreateDiskAlert(alert, item.Path)
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("disk path %s process failed: %v", item.Path, err)
|
||||
errMsgs = append(errMsgs, errMsg)
|
||||
global.LOG.Errorf("%s", errMsg)
|
||||
if item.Total == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(errMsgs) > 0 {
|
||||
return fmt.Errorf("batch process disks failed, error count: %d, details: %s", len(errMsgs), strings.Join(errMsgs, "; "))
|
||||
checkAndCreateDiskAlert(alert, item.Path, &disk.UsageStat{Used: item.Used, UsedPercent: item.UsedPercent})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func processSingleDisk(alert dto.AlertDTO) error {
|
||||
err := checkAndCreateDiskAlert(alert, alert.Project)
|
||||
usageStat, err := loadDiskUsageWithTimeout(alert.Project, true)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("%s", err.Error())
|
||||
global.LOG.Errorf("error getting disk usage for %s, err: %v", alert.Project, err)
|
||||
return err
|
||||
}
|
||||
checkAndCreateDiskAlert(alert, alert.Project, usageStat)
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string) error {
|
||||
usageStat, err := psutil.DISK.GetUsage(path, false)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("error getting disk usage for %s, err: %v", path, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func checkAndCreateDiskAlert(alert dto.AlertDTO, path string, usageStat *disk.UsageStat) {
|
||||
usedTotal, usedStr := calculateUsedTotal(alert.Cycle, usageStat)
|
||||
commonTotal := float64(alert.Count)
|
||||
if alert.Cycle == 1 {
|
||||
commonTotal *= 1024 * 1024 * 1024
|
||||
}
|
||||
if usedTotal < commonTotal {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
params := createAlertDiskParams(path, usedStr)
|
||||
sender := NewAlertSender(alert, alert.Project)
|
||||
sender.ResourceSend(path, params)
|
||||
return nil
|
||||
}
|
||||
|
||||
func calculateUsedTotal(cycle uint, usageStat *disk.UsageStat) (float64, string) {
|
||||
@@ -1097,3 +1126,55 @@ func calculateMinutesDifference(newDate time.Time) int {
|
||||
minutesDifference := int(now.Sub(newDate).Minutes())
|
||||
return minutesDifference
|
||||
}
|
||||
|
||||
func loadSSHAlertHistories(
|
||||
baseDir string,
|
||||
startTime, endTime time.Time,
|
||||
location *time.Location,
|
||||
) ([]dto.SSHHistory, error) {
|
||||
fileList, err := listSSHLogFiles(baseDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
histories []dto.SSHHistory
|
||||
loadErr error
|
||||
)
|
||||
for _, file := range fileList {
|
||||
items, err := loadSSHHistoriesFromFile(file.Name, "", "", startTime, endTime, file.Year, location)
|
||||
if err != nil {
|
||||
loadErr = errors.Join(loadErr, fmt.Errorf("load SSH log file %s: %w", file.Name, err))
|
||||
continue
|
||||
}
|
||||
histories = append(histories, items...)
|
||||
}
|
||||
return histories, loadErr
|
||||
}
|
||||
|
||||
func summarizeSSHLoginHistories(
|
||||
histories []dto.SSHHistory,
|
||||
now time.Time,
|
||||
failedWindow time.Duration,
|
||||
whitelist []string,
|
||||
) (int, []string) {
|
||||
failedStartTime := now.Add(-failedWindow)
|
||||
successStartTime := now.Add(-sshIPLoginWindow)
|
||||
failedCount := 0
|
||||
var abnormalLogins []string
|
||||
|
||||
for _, item := range histories {
|
||||
switch item.Status {
|
||||
case constant.StatusFailed:
|
||||
if isSSHLogWithinTimeRange(item.Date, failedStartTime, now) {
|
||||
failedCount++
|
||||
}
|
||||
case constant.StatusSuccess:
|
||||
if !isSSHLogWithinTimeRange(item.Date, successStartTime, now) || isIPInWhitelist(item.Address, whitelist) {
|
||||
continue
|
||||
}
|
||||
abnormalLogins = append(abnormalLogins, fmt.Sprintf("%s-%s", item.Address, item.Date.Format(constant.DateTimeLayout)))
|
||||
}
|
||||
}
|
||||
return failedCount, abnormalLogins
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func (s *AlertSender) sendByConfig(config model.AlertConfig, quota string, param
|
||||
} else {
|
||||
s.sendBarkWithConfig(config, quota, params)
|
||||
}
|
||||
case constant.WeCom, constant.DingTalk, constant.FeiShu:
|
||||
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Custom:
|
||||
if isResource {
|
||||
s.sendResourceWebhookWithConfig(config, quota, params)
|
||||
} else {
|
||||
@@ -86,7 +86,7 @@ func (s *AlertSender) sendByConfig(config model.AlertConfig, quota string, param
|
||||
|
||||
func (s *AlertSender) sendByLegacyMethod(method string, quota string, params []dto.Param, isResource bool) {
|
||||
alertRepo := repo.NewIAlertRepo()
|
||||
typeMap := map[string]string{"mail": constant.Email, constant.Bark: constant.Bark, constant.SMS: constant.SMS}
|
||||
typeMap := map[string]string{"mail": constant.Email, constant.Bark: constant.Bark, constant.SMS: constant.SMS, constant.Custom: constant.Custom}
|
||||
configType := method
|
||||
if mapped, ok := typeMap[method]; ok {
|
||||
configType = mapped
|
||||
@@ -308,12 +308,31 @@ func (s *AlertSender) sendWebhookWithConfig(config model.AlertConfig, quota stri
|
||||
}
|
||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
|
||||
err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
|
||||
queued := false
|
||||
var err error
|
||||
if config.Type == constant.Custom {
|
||||
task := dto.AlertTaskMetadata{
|
||||
AlertID: s.alert.ID,
|
||||
Type: s.alert.Type,
|
||||
Quota: quota,
|
||||
QuotaType: s.quotaType,
|
||||
Method: strconv.Itoa(int(config.ID)),
|
||||
}
|
||||
result, deliveryErr := xpack.DeliverCustomWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo, task)
|
||||
queued, err = result.Queued, deliveryErr
|
||||
if err == nil && result.Queued {
|
||||
_, err = alertUtil.RecordQueuedAlertTask(result.LogID, task)
|
||||
}
|
||||
} else {
|
||||
err = xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
|
||||
}
|
||||
if err != nil {
|
||||
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
|
||||
return
|
||||
}
|
||||
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
|
||||
if !queued {
|
||||
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
|
||||
@@ -334,11 +353,31 @@ func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, qu
|
||||
}
|
||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
|
||||
if err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo); err != nil {
|
||||
queued := false
|
||||
var err error
|
||||
if config.Type == constant.Custom {
|
||||
task := dto.AlertTaskMetadata{
|
||||
AlertID: s.alert.ID,
|
||||
Type: s.alert.Type,
|
||||
Quota: quota,
|
||||
QuotaType: s.quotaType,
|
||||
Method: strconv.Itoa(int(config.ID)),
|
||||
}
|
||||
result, deliveryErr := xpack.DeliverCustomWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo, task)
|
||||
queued, err = result.Queued, deliveryErr
|
||||
if err == nil && result.Queued {
|
||||
_, err = alertUtil.RecordQueuedAlertTask(result.LogID, task)
|
||||
}
|
||||
} else {
|
||||
err = xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
|
||||
}
|
||||
if err != nil {
|
||||
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
|
||||
return
|
||||
}
|
||||
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
|
||||
if !queued {
|
||||
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) {
|
||||
|
||||
+26
-14
@@ -223,6 +223,9 @@ func (a AppService) GetAppDetailByKey(appKey, version string) (response.AppDetai
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
if err = checkVllmVersionAccess(app.Key, version); err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
appDetail, err := appDetailRepo.GetFirst(appDetailRepo.WithAppId(app.ID), appDetailRepo.WithVersion(version))
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
@@ -241,14 +244,17 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
app, err := appRepo.GetFirst(repo.WithByID(detail.AppId))
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
if err = checkVllmVersionAccess(app.Key, detail.Version); err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
appDetailDTO.AppDetail = detail
|
||||
appDetailDTO.Enable = true
|
||||
|
||||
if appType == "runtime" {
|
||||
app, err := appRepo.GetFirst(repo.WithByID(appID))
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
|
||||
versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version)
|
||||
@@ -319,10 +325,6 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
||||
|
||||
appDetailDTO.HostMode = isHostModel(appDetailDTO.DockerCompose)
|
||||
|
||||
app, err := appRepo.GetFirst(repo.WithByID(detail.AppId))
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
if err := checkLimit(app); err != nil {
|
||||
appDetailDTO.Enable = false
|
||||
}
|
||||
@@ -374,6 +376,9 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = checkVllmVersionAccess(app.Key, appDetail.Version); err != nil {
|
||||
return
|
||||
}
|
||||
if DatabaseKeys[app.Key] > 0 {
|
||||
if existDatabases, _ := databaseRepo.GetList(repo.WithByName(req.Name)); len(existDatabases) > 0 {
|
||||
err = buserr.New("ErrRemoteExist")
|
||||
@@ -483,15 +488,17 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
index++
|
||||
}
|
||||
newServiceName := strings.ToLower(appInstall.Name)
|
||||
if app.Limit == 0 && newServiceName != serviceName && len(servicesMap) == 1 {
|
||||
if app.Limit == 0 && newServiceName != serviceName && len(servicesMap) == 1 && !req.KeepServiceName {
|
||||
servicesMap[newServiceName] = servicesMap[serviceName]
|
||||
delete(servicesMap, serviceName)
|
||||
serviceName = newServiceName
|
||||
}
|
||||
appInstall.ServiceName = serviceName
|
||||
|
||||
if err = addDockerComposeCommonParam(composeMap, appInstall.ServiceName, req.AppContainerConfig, req.Params); err != nil {
|
||||
return
|
||||
if !req.SkipComposeCommonConfig {
|
||||
if err = addDockerComposeCommonParam(composeMap, appInstall.ServiceName, req.AppContainerConfig, req.Params); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
var (
|
||||
composeByte []byte
|
||||
@@ -543,6 +550,11 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
|
||||
installTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskInstall, task.TaskScopeApp, req.TaskID, appInstall.ID)
|
||||
if err != nil {
|
||||
appInstall.Status = constant.StatusInstallErr
|
||||
appInstall.Message = err.Error()
|
||||
if saveErr := appInstallRepo.Save(context.Background(), appInstall); saveErr != nil {
|
||||
err = fmt.Errorf("%w; save failed install status: %v", err, saveErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -550,7 +562,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
return
|
||||
}
|
||||
|
||||
installApp := func(t *task.Task) error {
|
||||
installApp := func(t *task.Task) (err error) {
|
||||
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -559,7 +571,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
return err
|
||||
}
|
||||
}
|
||||
if executeScript {
|
||||
if executeScript || req.UseLifecycleScripts {
|
||||
if err = runScript(t, appInstall, "init"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -572,7 +584,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = upApp(t, appInstall, req.PullImage); err != nil {
|
||||
if err = upApp(t, appInstall, req.PullImage, req.UseLifecycleScripts); err != nil {
|
||||
return err
|
||||
}
|
||||
updateToolApp(appInstall)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -13,12 +14,14 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
@@ -252,6 +255,9 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
||||
return buserr.New("ErrInstallDirNotFound")
|
||||
}
|
||||
dockerComposePath := install.GetComposePath()
|
||||
if req.UseLifecycleScripts && (req.Operate == constant.Start || req.Operate == constant.Stop || req.Operate == constant.Restart) {
|
||||
return operateAppWithLifecycleScripts(install, req, nil)
|
||||
}
|
||||
switch req.Operate {
|
||||
case constant.Rebuild:
|
||||
return rebuildApp(install)
|
||||
@@ -275,12 +281,13 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
||||
return syncAppInstallStatus(&install, false)
|
||||
case constant.Delete:
|
||||
deleteReq := request.AppInstallDelete{
|
||||
Install: install,
|
||||
DeleteBackup: req.DeleteBackup,
|
||||
ForceDelete: req.ForceDelete,
|
||||
DeleteDB: req.DeleteDB,
|
||||
DeleteImage: req.DeleteImage,
|
||||
TaskID: req.TaskID,
|
||||
Install: install,
|
||||
DeleteBackup: req.DeleteBackup,
|
||||
ForceDelete: req.ForceDelete,
|
||||
DeleteDB: req.DeleteDB,
|
||||
DeleteImage: req.DeleteImage,
|
||||
TaskID: req.TaskID,
|
||||
UseLifecycleScripts: req.UseLifecycleScripts,
|
||||
}
|
||||
if err = deleteAppInstall(deleteReq); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
@@ -312,6 +319,75 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
||||
}
|
||||
}
|
||||
|
||||
func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppInstalledOperate, onFailure func(error)) error {
|
||||
taskType := task.TaskUpdate
|
||||
switch req.Operate {
|
||||
case constant.Start:
|
||||
install.Status = constant.StatusStarting
|
||||
case constant.Restart:
|
||||
taskType = task.TaskRestart
|
||||
install.Status = constant.StatusRestarting
|
||||
case constant.Stop:
|
||||
install.Status = constant.StatusWaiting
|
||||
default:
|
||||
return errors.New("lifecycle script operation not supported")
|
||||
}
|
||||
install.Message = ""
|
||||
if err := appInstallRepo.Save(context.Background(), &install); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID)
|
||||
if err != nil {
|
||||
install.Status = constant.StatusUpErr
|
||||
install.Message = err.Error()
|
||||
if saveErr := appInstallRepo.Save(context.Background(), &install); saveErr != nil {
|
||||
return fmt.Errorf("%w; save failed operation status: %v", err, saveErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
operation := string(req.Operate)
|
||||
operationTask.AddSubTaskWithOps(
|
||||
task.GetTaskName(install.Name, taskType, task.TaskScopeApp),
|
||||
func(t *task.Task) error {
|
||||
if err := runScript(t, &install, operation); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Operate == constant.Stop {
|
||||
install.Status = constant.StatusStopped
|
||||
install.Message = ""
|
||||
return appInstallRepo.Save(context.Background(), &install)
|
||||
}
|
||||
containerNames, err := getContainerNames(install)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(containerNames) == 0 {
|
||||
return buserr.WithName("ErrContainerNotFound", install.Name)
|
||||
}
|
||||
install.ContainerName = strings.Join(containerNames, ",")
|
||||
install.Status = constant.StatusRunning
|
||||
install.Message = ""
|
||||
return appInstallRepo.Save(context.Background(), &install)
|
||||
},
|
||||
nil,
|
||||
0,
|
||||
time.Hour,
|
||||
)
|
||||
go func() {
|
||||
if taskErr := operationTask.Execute(); taskErr != nil {
|
||||
if onFailure != nil {
|
||||
onFailure(taskErr)
|
||||
return
|
||||
}
|
||||
install.Status = constant.StatusUpErr
|
||||
install.Message = taskErr.Error()
|
||||
_ = appInstallRepo.Save(context.Background(), &install)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AppInstallService) UpdateAppConfig(req request.AppConfigUpdate) error {
|
||||
installed, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
|
||||
if err != nil {
|
||||
@@ -374,8 +450,10 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params); err != nil {
|
||||
return err
|
||||
if !req.SkipComposeCommonConfig {
|
||||
if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
composeByte, err := yaml.Marshal(composeMap)
|
||||
if err != nil {
|
||||
@@ -408,7 +486,7 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backupEnvMaps := oldEnvMaps
|
||||
backupEnvMaps := maps.Clone(oldEnvMaps)
|
||||
handleMap(req.Params, oldEnvMaps)
|
||||
paramByte, err := json.Marshal(oldEnvMaps)
|
||||
if err != nil {
|
||||
@@ -420,13 +498,32 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
_ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(installed.DockerCompose), constant.DirPerm)
|
||||
if err := rebuildApp(installed); err != nil {
|
||||
restoreConfig := func(operationErr error) {
|
||||
_ = env.Write(backupEnvMaps, envPath)
|
||||
_ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(backupDockerCompose), constant.DirPerm)
|
||||
failed := oldInstalled
|
||||
failed.Status = constant.StatusUpErr
|
||||
failed.Message = operationErr.Error()
|
||||
_ = appInstallRepo.Save(context.Background(), &failed)
|
||||
}
|
||||
if req.UseLifecycleScripts {
|
||||
err = operateAppWithLifecycleScripts(installed, request.AppInstalledOperate{
|
||||
InstallId: installed.ID,
|
||||
Operate: constant.Restart,
|
||||
TaskID: req.TaskID,
|
||||
UseLifecycleScripts: true,
|
||||
}, restoreConfig)
|
||||
} else {
|
||||
err = rebuildApp(installed)
|
||||
}
|
||||
if err != nil {
|
||||
restoreConfig(err)
|
||||
return err
|
||||
}
|
||||
installed.Status = constant.StatusRunning
|
||||
_ = appInstallRepo.Save(context.Background(), &installed)
|
||||
if !req.UseLifecycleScripts {
|
||||
installed.Status = constant.StatusRunning
|
||||
_ = appInstallRepo.Save(context.Background(), &installed)
|
||||
}
|
||||
|
||||
proxyChanged := hasAppInstallProxyPassChanged(&oldInstalled, &installed)
|
||||
currentProxy, currentProxyErr := getAppInstallProxyPass(&installed)
|
||||
@@ -475,11 +572,15 @@ func (a *AppInstallService) SyncAll(systemInit bool) error {
|
||||
return err
|
||||
}
|
||||
for _, i := range allList {
|
||||
if i.Status == constant.StatusInstalling || i.Status == constant.StatusUpgrading || i.Status == constant.StatusRebuilding || i.Status == constant.StatusUninstalling {
|
||||
if appInstallOperationInterruptedOnRestart(i.Status) {
|
||||
if systemInit {
|
||||
i.Status = constant.StatusError
|
||||
i.Message = "1Panel restart causes the task to terminate"
|
||||
_ = appInstallRepo.Save(context.Background(), &i)
|
||||
i.Status = appInstallOperationFailureStatus(i.Status)
|
||||
i.Message = constant.InterruptedMsg
|
||||
if err := appInstallRepo.Save(context.Background(), &i); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := syncAppInstallStatus(&i, false); err != nil {
|
||||
global.LOG.Errorf("sync install app[%s] error,mgs: %s", i.Name, err.Error())
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -583,6 +684,9 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
||||
return versions, err
|
||||
}
|
||||
for _, detail := range details {
|
||||
if !canAccessVllmVersion(app.Key, detail.Version) {
|
||||
continue
|
||||
}
|
||||
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
||||
if len(ignores) > 0 {
|
||||
continue
|
||||
@@ -836,8 +940,34 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
||||
}
|
||||
|
||||
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
||||
if appInstall.Status == constant.StatusInstalling || appInstall.Status == constant.StatusRebuilding || appInstall.Status == constant.StatusUpgrading || appInstall.Status == constant.StatusUninstalling {
|
||||
operation := ""
|
||||
switch appInstall.Status {
|
||||
case constant.StatusInstalling:
|
||||
operation = task.TaskInstall
|
||||
case constant.StatusUpgrading:
|
||||
operation = task.TaskUpgrade
|
||||
case constant.StatusRebuilding:
|
||||
operation = task.TaskBuild
|
||||
case constant.StatusUninstalling:
|
||||
operation = task.TaskUninstall
|
||||
case constant.StatusStarting, constant.StatusWaiting:
|
||||
operation = task.TaskUpdate
|
||||
case constant.StatusRestarting:
|
||||
operation = task.TaskRestart
|
||||
}
|
||||
if operation != "" {
|
||||
if err := reconcileAppInstallTaskFailure(appInstall, operation, global.DB, global.TaskDB); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
switch appInstall.Status {
|
||||
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
||||
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
|
||||
return nil
|
||||
case constant.StatusInstallErr, constant.StatusUpgradeErr, constant.StatusUpErr:
|
||||
if !force {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
cli, err := docker.NewClient()
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func appInstallOperationFailureStatus(status string) string {
|
||||
switch status {
|
||||
case constant.StatusInstalling:
|
||||
return constant.StatusInstallErr
|
||||
case constant.StatusUpgrading:
|
||||
return constant.StatusUpgradeErr
|
||||
case constant.StatusUninstalling:
|
||||
return constant.StatusError
|
||||
default:
|
||||
return constant.StatusUpErr
|
||||
}
|
||||
}
|
||||
|
||||
func appInstallOperationPending(status string) bool {
|
||||
switch status {
|
||||
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
||||
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func appInstallOperationInterruptedOnRestart(status string) bool {
|
||||
// ReStarting can also describe a container's Docker restart policy.
|
||||
return appInstallOperationPending(status) && status != constant.StatusRestarting
|
||||
}
|
||||
|
||||
func reconcileAppInstallTaskFailure(install *model.AppInstall, operation string, appDB, taskDB *gorm.DB) error {
|
||||
if !appInstallOperationPending(install.Status) {
|
||||
return nil
|
||||
}
|
||||
var operationTask model.Task
|
||||
err := taskDB.Where("type = ? AND resource_id = ? AND operate = ?", "App", install.ID, operation).
|
||||
Order("created_at DESC").First(&operationTask).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if operationTask.Status != constant.StatusFailed && operationTask.Status != constant.StatusCanceled {
|
||||
return nil
|
||||
}
|
||||
// A previous operation must not turn a newly queued retry into a failure.
|
||||
if operationTask.CreatedAt.Before(install.UpdatedAt) &&
|
||||
(operationTask.EndAt.IsZero() || operationTask.EndAt.Before(install.UpdatedAt)) {
|
||||
return nil
|
||||
}
|
||||
message := operationTask.ErrorMsg
|
||||
if message == "" {
|
||||
message = "the application operation task failed"
|
||||
}
|
||||
status := appInstallOperationFailureStatus(install.Status)
|
||||
result := appDB.Model(&model.AppInstall{}).
|
||||
Where("id = ? AND status = ? AND updated_at = ?", install.ID, install.Status, install.UpdatedAt).
|
||||
Updates(map[string]interface{}{"status": status, "message": message})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected > 0 {
|
||||
install.Status = status
|
||||
install.Message = message
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func operationStatusTestDB(t *testing.T) (*gorm.DB, *gorm.DB, model.AppInstall) {
|
||||
t.Helper()
|
||||
open := func(name string, schema interface{}) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), name)), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.AutoMigrate(schema); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
sqlDB, _ := db.DB()
|
||||
if sqlDB != nil {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
})
|
||||
return db
|
||||
}
|
||||
appDB := open("apps.db", &model.AppInstall{})
|
||||
taskDB := open("tasks.db", &model.Task{})
|
||||
install := model.AppInstall{
|
||||
Name: "gb10-ds-vision", AppId: 1, AppDetailId: 1,
|
||||
Version: "nvidia-gb10-dspark-0.1.1", ContainerName: "vllm-dspark-1", ServiceName: "vllm-dspark",
|
||||
Status: constant.StatusStarting, Env: `{"MODEL_DIR":"/models/vision"}`,
|
||||
}
|
||||
if err := appDB.Omit(clause.Associations).Create(&install).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return appDB, taskDB, install
|
||||
}
|
||||
|
||||
func TestAppInstallFailedTaskRecoversPendingState(t *testing.T) {
|
||||
for _, state := range []string{constant.StatusStarting, constant.StatusWaiting, constant.StatusRestarting, constant.StatusInstalling} {
|
||||
t.Run(state, func(t *testing.T) {
|
||||
appDB, taskDB, install := operationStatusTestDB(t)
|
||||
install.Status = state
|
||||
if err := appDB.Omit(clause.Associations).Save(&install).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
task := model.Task{ID: "failed", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
|
||||
Status: constant.StatusFailed, ErrorMsg: "model startup script failed", CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
|
||||
if err := taskDB.Create(&task).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := constant.StatusUpErr
|
||||
if state == constant.StatusInstalling {
|
||||
want = constant.StatusInstallErr
|
||||
}
|
||||
var stored model.AppInstall
|
||||
if err := appDB.First(&stored, install.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if install.Status != want || stored.Status != want || stored.Message != task.ErrorMsg || stored.Env != install.Env {
|
||||
t.Fatalf("recovered state = %s/%s, message %q, env %q", install.Status, stored.Status, stored.Message, stored.Env)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppInstallPendingStateKeepsActiveAndUnrelatedTasks(t *testing.T) {
|
||||
for _, taskStatus := range []string{constant.StatusExecuting, constant.StatusSuccess, "missing", "previous-failure", "other-resource"} {
|
||||
t.Run(taskStatus, func(t *testing.T) {
|
||||
appDB, taskDB, install := operationStatusTestDB(t)
|
||||
task := model.Task{ID: "task", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
|
||||
Status: taskStatus, CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
|
||||
if taskStatus == "previous-failure" {
|
||||
task.Status = constant.StatusFailed
|
||||
task.CreatedAt = install.UpdatedAt.Add(-2 * time.Hour)
|
||||
task.EndAt = install.UpdatedAt.Add(-time.Hour)
|
||||
}
|
||||
if taskStatus == "other-resource" {
|
||||
task.Status = constant.StatusFailed
|
||||
task.ResourceID++
|
||||
}
|
||||
if taskStatus != "missing" {
|
||||
if err := taskDB.Create(&task).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if install.Status != constant.StatusStarting {
|
||||
t.Fatalf("unrelated or active task changed pending state to %s", install.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppInstallRecoveryDoesNotOverwriteConcurrentRetry(t *testing.T) {
|
||||
appDB, taskDB, install := operationStatusTestDB(t)
|
||||
task := model.Task{ID: "failed", Type: "App", Operate: "TaskUpdate", ResourceID: install.ID,
|
||||
Status: constant.StatusCanceled, ErrorMsg: "interrupted", CreatedAt: install.UpdatedAt.Add(time.Second), EndAt: install.UpdatedAt.Add(2 * time.Second)}
|
||||
if err := taskDB.Create(&task).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := appDB.Callback().Update().Before("gorm:update").Register("new-operation", func(db *gorm.DB) {
|
||||
if err := db.Exec("UPDATE app_installs SET updated_at = ?, message = ? WHERE id = ?", install.UpdatedAt.Add(time.Hour), "new attempt", install.ID).Error; err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := reconcileAppInstallTaskFailure(&install, task.Operate, appDB, taskDB); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var stored model.AppInstall
|
||||
if err := appDB.First(&stored, install.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stored.Status != constant.StatusStarting || stored.Message != "new attempt" {
|
||||
t.Fatalf("recovery overwrote a newer operation: %s, %s", stored.Status, stored.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppInstallRestartRecoveryCoversLifecycleStates(t *testing.T) {
|
||||
for _, state := range []string{constant.StatusStarting, constant.StatusWaiting, constant.StatusInstalling} {
|
||||
if !appInstallOperationInterruptedOnRestart(state) {
|
||||
t.Errorf("restart recovery omitted %s", state)
|
||||
}
|
||||
}
|
||||
for _, state := range []string{constant.StatusRunning, constant.StatusStopped, constant.StatusRestarting} {
|
||||
if appInstallOperationInterruptedOnRestart(state) {
|
||||
t.Errorf("restart recovery changes a Docker runtime state: %s", state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,918 @@
|
||||
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
|
||||
appUpgradeCommitted
|
||||
)
|
||||
|
||||
var appUpgradeLocks sync.Map
|
||||
|
||||
const composeServiceLabel = "com.docker.compose.service"
|
||||
|
||||
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 err = checkVllmVersionAccess(install.App.Key, detail.Version); 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.UpWithoutBuild(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
|
||||
|
||||
containerNames, discoverErr := discoverUpgradeContainerNames(u.candidate, u.envContent)
|
||||
if discoverErr != nil {
|
||||
t.Logf("WARNING: discover upgraded application containers failed: %v", discoverErr)
|
||||
} else if len(containerNames) > 0 {
|
||||
u.candidate.ContainerName = strings.Join(containerNames, ",")
|
||||
} else {
|
||||
t.Log("WARNING: no containers found for the upgraded application")
|
||||
}
|
||||
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
|
||||
}
|
||||
// Upgrades deliberately keep the user's nginx.conf, so corrected gzip
|
||||
// defaults shipped with a new version would never reach existing
|
||||
// installations. Rewrite only an untouched factory configuration, and
|
||||
// never fail the upgrade over it.
|
||||
if gzipErr := upgradeStockNginxGzipConfig(u.candidate); gzipErr != nil {
|
||||
t.Logf("WARNING: update stock gzip configuration failed, keeping the current one: %v", gzipErr)
|
||||
}
|
||||
} else if err = appInstallRepo.Save(context.Background(), &u.candidate); err != nil {
|
||||
return err
|
||||
}
|
||||
if discoverErr == nil && len(containerNames) > 0 {
|
||||
if syncErr := syncAppInstallStatus(&u.candidate, true); syncErr != nil {
|
||||
t.Logf("WARNING: sync upgraded application status failed: %v", syncErr)
|
||||
}
|
||||
}
|
||||
u.phase = appUpgradeCommitted
|
||||
u.deleteOldImages(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
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),
|
||||
} {
|
||||
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.UpWithoutBuild(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.UpWithoutBuild(u.original.GetComposePath())
|
||||
return errors.Join(rollbackErr, err)
|
||||
}
|
||||
} else {
|
||||
if err := u.restoreManagedFiles(); err != nil {
|
||||
return errors.Join(rollbackErr, err)
|
||||
}
|
||||
if out, err := compose.UpWithoutBuild(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 {
|
||||
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 == "openlist" {
|
||||
// The upgrade script updates this too late for the pre-pull phase.
|
||||
image := "openlistteam/openlist:v" + strings.TrimPrefix(install.Version, "v")
|
||||
if preInstalled := params["PRE_INSTALLED"]; preInstalled != "" {
|
||||
image += "-" + preInstalled
|
||||
}
|
||||
params["OPENLIST_IMAGE"] = image
|
||||
envs["OPENLIST_IMAGE"] = image
|
||||
content, err := json.Marshal(envs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
install.Env = string(content)
|
||||
}
|
||||
if install.App.Key == constant.AppOpenresty {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func discoverUpgradeContainerNames(install model.AppInstall, envContent []byte) ([]string, error) {
|
||||
project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), envContent, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
containers, err := client.ContainerList(context.Background(), container.ListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(filters.Arg("label", composeWorkdirLabel+"="+install.GetPath())),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
containerNames := make([]string, 0, len(containers))
|
||||
for _, item := range containers {
|
||||
if _, ok := expectedServices[item.Labels[composeServiceLabel]]; ok && len(item.Names) > 0 {
|
||||
containerNames = append(containerNames, strings.TrimPrefix(item.Names[0], "/"))
|
||||
}
|
||||
}
|
||||
sort.Strings(containerNames)
|
||||
return containerNames, nil
|
||||
}
|
||||
+60
-424
@@ -9,7 +9,6 @@ import (
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -353,15 +352,33 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
if dir != nil {
|
||||
logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("App")
|
||||
t.Log(logStr)
|
||||
cleanupFailed := false
|
||||
|
||||
out, err := compose.Down(install.GetComposePath())
|
||||
if err != nil && !deleteReq.ForceDelete {
|
||||
return handleErr(install, err, out)
|
||||
if deleteReq.UseLifecycleScripts {
|
||||
if scriptErr := runScript(t, &install, "uninstall"); scriptErr != nil {
|
||||
cleanupFailed = true
|
||||
if !deleteReq.ForceDelete {
|
||||
return scriptErr
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out, downErr := compose.Down(install.GetComposePath())
|
||||
if downErr != nil {
|
||||
cleanupFailed = true
|
||||
if !deleteReq.ForceDelete {
|
||||
return handleErr(install, downErr, out)
|
||||
}
|
||||
}
|
||||
if scriptErr := runScript(t, &install, "uninstall"); scriptErr != nil {
|
||||
cleanupFailed = true
|
||||
if !deleteReq.ForceDelete {
|
||||
_, _ = compose.Up(install.GetComposePath())
|
||||
return scriptErr
|
||||
}
|
||||
}
|
||||
}
|
||||
t.LogSuccess(logStr)
|
||||
if err = runScript(t, &install, "uninstall"); err != nil {
|
||||
_, _ = compose.Up(install.GetComposePath())
|
||||
return err
|
||||
if !cleanupFailed {
|
||||
t.LogSuccess(logStr)
|
||||
}
|
||||
if deleteReq.DeleteImage {
|
||||
content, err := op.GetContent(install.GetEnvPath())
|
||||
@@ -461,8 +478,9 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
}
|
||||
uninstallTask.AddSubTask(task.GetTaskName(install.Name, task.TaskUninstall, task.TaskScopeApp), uninstall, nil)
|
||||
go func() {
|
||||
if err := uninstallTask.Execute(); err != nil && !deleteReq.ForceDelete {
|
||||
if err := uninstallTask.Execute(); err != nil {
|
||||
install.Status = constant.StatusError
|
||||
install.Message = err.Error()
|
||||
_ = appInstallRepo.Save(context.Background(), &install)
|
||||
}
|
||||
}()
|
||||
@@ -783,416 +801,6 @@ func buildNginx(parentTask *task.Task, nginxInstall model.AppInstall, catalogPat
|
||||
return commitNginxModuleBuilds(nginxInstall, previousModules, modules, false, catalogPath)
|
||||
}
|
||||
|
||||
func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
install, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
originalInstall := install
|
||||
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
|
||||
nginxUpgradeSnapshot *openrestyUpgradeSnapshot
|
||||
)
|
||||
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
|
||||
targetNginxCatalogPath := ""
|
||||
if install.App.Key == constant.AppOpenresty {
|
||||
nginxUpgradeSnapshot, err = createOpenrestyUpgradeSnapshot(install.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if install.App.Key == vllmAppKeyForUpgrade {
|
||||
envs := make(map[string]interface{})
|
||||
if err = json.Unmarshal([]byte(install.Env), &envs); err != nil {
|
||||
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(), nginxModuleBuildDir)
|
||||
detailBuildDir := path.Join(detailDir, nginxModuleBuildDir)
|
||||
if !fileOp.Stat(installBuildDir) {
|
||||
if err := fileOp.CreateDir(installBuildDir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := fileOp.DeleteDir(path.Join(installBuildDir, nginxModuleTmpDir)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fileOp.CopyDir(path.Join(detailBuildDir, nginxModuleTmpDir), installBuildDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fileOp.CopyFile(path.Join(detailBuildDir, "Dockerfile"), installBuildDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := syncNginxModuleBuilder(detailBuildDir, installBuildDir); err != nil {
|
||||
return err
|
||||
}
|
||||
targetCatalogSource := path.Join(detailBuildDir, nginxModuleCatalogFile)
|
||||
if !fileOp.Stat(targetCatalogSource) {
|
||||
return fmt.Errorf("target OpenResty module catalog not found: %s", targetCatalogSource)
|
||||
}
|
||||
targetNginxCatalogPath = path.Join(installBuildDir, nginxModuleCatalogPendingFile)
|
||||
if err := stageNginxModuleCatalog(targetCatalogSource, targetNginxCatalogPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fileOp.CopyFile(path.Join(detailBuildDir, "nginx.conf"), installBuildDir); err != nil {
|
||||
return err
|
||||
}
|
||||
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 install.App.Key == constant.AppOpenresty {
|
||||
modules, moduleErr := loadNginxModulesWithCatalog(install, targetNginxCatalogPath)
|
||||
if moduleErr != nil {
|
||||
return moduleErr
|
||||
}
|
||||
// Build dynamic modules for the target version before stopping the
|
||||
// current container. Static modules retain the full rebuild path.
|
||||
if !hasEnabledStaticNginxModules(modules) {
|
||||
previousModules := cloneNginxModules(modules)
|
||||
modules, moduleErr = buildDynamicNginxModules(install, modules, nil, false, "", targetNginxCatalogPath, t)
|
||||
if moduleErr != nil {
|
||||
return moduleErr
|
||||
}
|
||||
if moduleErr = saveNginxModulesWithCatalog(install, modules, targetNginxCatalogPath); moduleErr != nil {
|
||||
removeNginxModuleOutputsNotReferenced(install, modules, previousModules)
|
||||
return moduleErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if out, err := compose.Down(install.GetComposePath()); err != nil {
|
||||
if out != "" {
|
||||
upErr = errors.New(out)
|
||||
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, install, targetNginxCatalogPath); 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 install.App.Key == constant.AppOpenresty {
|
||||
if err = commitStaticNginxModuleBuilds(install, targetNginxCatalogPath, t); err != nil {
|
||||
return err
|
||||
}
|
||||
activeCatalogPath := path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleCatalogFile)
|
||||
if err = activateNginxModuleCatalogAndCommit(targetNginxCatalogPath, activeCatalogPath, func() error {
|
||||
return appInstallRepo.Save(context.Background(), &install)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = appInstallRepo.Save(context.Background(), &install); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if nginxUpgradeSnapshot != nil {
|
||||
nginxUpgradeSnapshot.Cleanup()
|
||||
nginxUpgradeSnapshot = nil
|
||||
}
|
||||
if req.DeleteImage {
|
||||
newEnvContent, err := fileOp.GetContent(install.GetEnvPath())
|
||||
if err != nil {
|
||||
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
||||
return nil
|
||||
}
|
||||
excludeImages, err := docker.GetImagesFromDockerCompose(newEnvContent, []byte(install.DockerCompose))
|
||||
if err != nil {
|
||||
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
||||
return nil
|
||||
}
|
||||
dockerCLi, err := docker.NewClient()
|
||||
if err != nil {
|
||||
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
||||
return nil
|
||||
}
|
||||
defer dockerCLi.Close()
|
||||
if err = deleteAppImagesByIDs(t, dockerCLi, oldImageIDs, excludeImages); err != nil {
|
||||
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
rollBackApp := func(t *task.Task) {
|
||||
if req.Backup {
|
||||
t.Log(i18n.GetWithName("AppRecover", install.Name))
|
||||
recoverErr := NewIBackupService().AppRecover(dto.CommonRecover{
|
||||
Name: install.App.Key, DetailName: install.Name, Type: "app", DownloadAccountID: 1, File: backupFile,
|
||||
})
|
||||
if recoverErr == nil {
|
||||
if nginxUpgradeSnapshot != nil {
|
||||
nginxUpgradeSnapshot.Cleanup()
|
||||
nginxUpgradeSnapshot = nil
|
||||
}
|
||||
t.LogSuccess(i18n.GetWithName("AppRecover", install.Name))
|
||||
return
|
||||
}
|
||||
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), recoverErr)
|
||||
if install.App.Key != constant.AppOpenresty {
|
||||
return
|
||||
}
|
||||
}
|
||||
if install.App.Key == constant.AppOpenresty && nginxUpgradeSnapshot != nil {
|
||||
if out, rollbackErr := compose.Down(install.GetComposePath()); rollbackErr != nil {
|
||||
if out != "" {
|
||||
rollbackErr = fmt.Errorf("%s: %w", out, rollbackErr)
|
||||
}
|
||||
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
|
||||
}
|
||||
if rollbackErr := nginxUpgradeSnapshot.Restore(); rollbackErr != nil {
|
||||
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
|
||||
return
|
||||
}
|
||||
nginxUpgradeSnapshot.Cleanup()
|
||||
nginxUpgradeSnapshot = nil
|
||||
if out, rollbackErr := compose.Up(originalInstall.GetComposePath()); rollbackErr != nil {
|
||||
if out != "" {
|
||||
rollbackErr = fmt.Errorf("%s: %w", out, rollbackErr)
|
||||
}
|
||||
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
|
||||
return
|
||||
}
|
||||
originalInstall.Status = constant.StatusRunning
|
||||
originalInstall.Message = ""
|
||||
if rollbackErr := appInstallRepo.Save(context.Background(), &originalInstall); rollbackErr != nil {
|
||||
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
|
||||
return
|
||||
}
|
||||
install = originalInstall
|
||||
t.LogSuccess(i18n.GetWithName("AppRecover", install.Name))
|
||||
return
|
||||
}
|
||||
if install.App.Key == constant.AppOpenresty {
|
||||
if rollbackErr := appInstallRepo.Save(context.Background(), &originalInstall); rollbackErr != nil {
|
||||
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
|
||||
return
|
||||
}
|
||||
install = originalInstall
|
||||
t.LogSuccess(i18n.GetWithName("AppRecover", install.Name))
|
||||
}
|
||||
}
|
||||
|
||||
upgradeTimeout := 1 * time.Hour
|
||||
if install.App.Key == constant.AppOpenresty {
|
||||
// Dynamic modules are built serially and each Docker build has its own
|
||||
// timeout. An outer deadline would start rollback while upgradeApp is
|
||||
// still mutating the installation because SubTask does not stop its
|
||||
// action goroutine on timeout.
|
||||
upgradeTimeout = 0
|
||||
}
|
||||
upgradeTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp), upgradeApp, rollBackApp, 0, upgradeTimeout)
|
||||
|
||||
upgradingInstall := install
|
||||
if err = appInstallRepo.Save(context.Background(), &upgradingInstall); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
if taskErr := upgradeTask.Execute(); taskErr != nil {
|
||||
existInstall, _ := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
|
||||
if existInstall.ID > 0 && existInstall.Status != constant.StatusRunning {
|
||||
existInstall.Status = constant.StatusUpgradeErr
|
||||
existInstall.Message = taskErr.Error()
|
||||
_ = appInstallRepo.Save(context.Background(), &existInstall)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func skipCheckStatus(service types.ServiceConfig) bool {
|
||||
for key := range service.Labels {
|
||||
if key == "skipStatusCheck" {
|
||||
@@ -1410,6 +1018,12 @@ func runScript(task *task.Task, appInstall *model.AppInstall, operate string) er
|
||||
scriptPath = path.Join(workDir, "scripts", "upgrade.sh")
|
||||
case "uninstall":
|
||||
scriptPath = path.Join(workDir, "scripts", "uninstall.sh")
|
||||
case "start":
|
||||
scriptPath = path.Join(workDir, "scripts", "start.sh")
|
||||
case "stop":
|
||||
scriptPath = path.Join(workDir, "scripts", "stop.sh")
|
||||
case "restart":
|
||||
scriptPath = path.Join(workDir, "scripts", "restart.sh")
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
if !fileOp.Stat(scriptPath) {
|
||||
@@ -1419,7 +1033,11 @@ func runScript(task *task.Task, appInstall *model.AppInstall, operate string) er
|
||||
logStr := i18n.GetWithName("ExecShell", operate)
|
||||
task.LogStart(logStr)
|
||||
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Minute), cmd.WithWorkDir(workDir))
|
||||
timeout := 10 * time.Minute
|
||||
if operate == "start" || operate == "restart" {
|
||||
timeout = time.Hour
|
||||
}
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(timeout), cmd.WithWorkDir(workDir), cmd.WithTask(*task))
|
||||
if err := cmdMgr.Run("bash", scriptPath); err != nil {
|
||||
task.LogFailedWithErr(logStr, err)
|
||||
return err
|
||||
@@ -1454,12 +1072,15 @@ func checkContainerNameIsExist(containerName, appDir string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func upApp(task *task.Task, appInstall *model.AppInstall, pullImages bool) error {
|
||||
func upApp(task *task.Task, appInstall *model.AppInstall, pullImages, useLifecycleScripts bool) error {
|
||||
upProject := func(appInstall *model.AppInstall) (err error) {
|
||||
var (
|
||||
out string
|
||||
errMsg string
|
||||
)
|
||||
if useLifecycleScripts {
|
||||
return runScript(task, appInstall, "start")
|
||||
}
|
||||
if pullImages && appInstall.App.Type != "php" {
|
||||
envByte, err := files.NewFileOp().GetContent(appInstall.GetEnvPath())
|
||||
if err != nil {
|
||||
@@ -1785,8 +1406,8 @@ func handleErr(install model.AppInstall, err error, out string) error {
|
||||
}
|
||||
|
||||
func doNotNeedSync(installed model.AppInstall) bool {
|
||||
return installed.Status == constant.StatusInstalling || installed.Status == constant.StatusRebuilding || installed.Status == constant.StatusUpgrading ||
|
||||
installed.Status == constant.StatusSyncing || installed.Status == constant.StatusUninstalling || installed.Status == constant.StatusInstallErr
|
||||
return appInstallOperationPending(installed.Status) || installed.Status == constant.StatusSyncing ||
|
||||
installed.Status == constant.StatusInstallErr || installed.Status == constant.StatusUpgradeErr || installed.Status == constant.StatusUpErr
|
||||
}
|
||||
|
||||
func synAppInstall(containers map[string]container.Summary, appInstall *model.AppInstall, force bool) {
|
||||
@@ -1880,6 +1501,11 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
||||
}
|
||||
|
||||
for _, installed := range appInstallList {
|
||||
if sync && appInstallOperationPending(installed.Status) {
|
||||
if err := syncAppInstallStatus(&installed, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if updated && ignoreUpdate(installed) {
|
||||
continue
|
||||
}
|
||||
@@ -2233,6 +1859,10 @@ func isHostModel(dockerCompose string) bool {
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -2244,6 +1874,9 @@ func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
|
||||
srcPath := path.Join(srcDir, entry.Name())
|
||||
dstPath := path.Join(dstDir, entry.Name())
|
||||
if !fileOp.Stat(dstPath) {
|
||||
if createdPaths != nil {
|
||||
*createdPaths = append(*createdPaths, dstPath)
|
||||
}
|
||||
if entry.IsDir() {
|
||||
if err := fileOp.CopyDir(srcPath, dstDir); err != nil {
|
||||
return err
|
||||
@@ -2258,7 +1891,7 @@ func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
if err := copyAppDetailMissing(fileOp, srcPath, dstPath); err != nil {
|
||||
if err := copyAppDetailMissingTracked(fileOp, srcPath, dstPath, createdPaths); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -2633,6 +2266,9 @@ func getAppVersions(key string, details []model.AppDetail) []string {
|
||||
hasLatest := false
|
||||
latestVersion := ""
|
||||
for _, detail := range details {
|
||||
if !canAccessVllmVersion(key, detail.Version) {
|
||||
continue
|
||||
}
|
||||
if key != "mssql" && strings.Contains(detail.Version, "latest") {
|
||||
hasLatest = true
|
||||
latestVersion = detail.Version
|
||||
|
||||
@@ -90,6 +90,34 @@ func (u *BackupService) AppBackup(req dto.CommonBackup) (*model.BackupRecord, er
|
||||
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 {
|
||||
app, err := appRepo.GetFirst(appRepo.WithKey(req.Name))
|
||||
if err != nil {
|
||||
@@ -203,7 +231,11 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_, _ = compose.Up(install.GetComposePath())
|
||||
if isRollback {
|
||||
_, _ = compose.UpWithoutBuild(install.GetComposePath())
|
||||
} else {
|
||||
_, _ = compose.Up(install.GetComposePath())
|
||||
}
|
||||
_ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", ""))
|
||||
}()
|
||||
|
||||
|
||||
@@ -582,6 +582,10 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
|
||||
if config.Image == "" {
|
||||
return fmt.Errorf("container image not found in backup file")
|
||||
}
|
||||
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(recoverCtx.inspectInfo.NetworkSettings, hostConfig)
|
||||
if err := normalizeContainerEndpointSettings(ctx, recoverCtx.client, networkConf, extraNetworks); err != nil {
|
||||
return err
|
||||
}
|
||||
if !checkImageExist(recoverCtx.client, config.Image) {
|
||||
if err := pullImages(taskItem, recoverCtx.client, config.Image); err != nil {
|
||||
return err
|
||||
@@ -596,7 +600,7 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
|
||||
return err
|
||||
}
|
||||
|
||||
createRes, err := createContainerWithOldNetworks(ctx, recoverCtx.client, config, hostConfig, recoverCtx.inspectInfo.NetworkSettings, recoverCtx.targetName)
|
||||
createRes, err := createContainerWithNetworks(ctx, recoverCtx.client, config, hostConfig, networkConf, extraNetworks, recoverCtx.targetName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -604,7 +608,7 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
|
||||
func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) error {
|
||||
if cli.NewVersionError(ctx, "1.44", "specify mac-address per network") != nil {
|
||||
removeEndpointMacAddresses(primary, extras)
|
||||
}
|
||||
@@ -619,11 +623,14 @@ func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client,
|
||||
}
|
||||
info, err := cli.NetworkInspect(ctx, netName, network.InspectOptions{})
|
||||
if err != nil {
|
||||
continue
|
||||
return fmt.Errorf("inspect network %s failed: %w", netName, err)
|
||||
}
|
||||
if err := validateContainerEndpointStaticIP(netName, info, endpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
removeUnsupportedEndpointStaticIP(netName, info, endpoint)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
|
||||
@@ -641,24 +648,28 @@ func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[st
|
||||
}
|
||||
}
|
||||
|
||||
func removeUnsupportedEndpointStaticIP(netName string, info network.Inspect, endpoint *network.EndpointSettings) {
|
||||
func validateContainerEndpointStaticIP(netName string, info network.Inspect, endpoint *network.EndpointSettings) error {
|
||||
if endpoint == nil || endpoint.IPAMConfig == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if isDefaultBridgeNetwork(netName, info) {
|
||||
endpoint.IPAMConfig = nil
|
||||
return
|
||||
ipam := endpoint.IPAMConfig
|
||||
if err := ipam.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid IP configuration for network %s: %w", netName, err)
|
||||
}
|
||||
if ipam.IPv4Address == "" && ipam.IPv6Address == "" {
|
||||
return nil
|
||||
}
|
||||
if netName == "host" || netName == "none" || isDefaultBridgeNetwork(netName, info) {
|
||||
return fmt.Errorf("network %s does not support static IP configuration", netName)
|
||||
}
|
||||
|
||||
if endpoint.IPAMConfig.IPv4Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv4Address, false) {
|
||||
endpoint.IPAMConfig.IPv4Address = ""
|
||||
if ipam.IPv4Address != "" && !networkSupportsStaticIP(info, ipam.IPv4Address, false) {
|
||||
return fmt.Errorf("static IPv4 address %s is not in a configured subnet of network %s", ipam.IPv4Address, netName)
|
||||
}
|
||||
if endpoint.IPAMConfig.IPv6Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv6Address, true) {
|
||||
endpoint.IPAMConfig.IPv6Address = ""
|
||||
}
|
||||
if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" && len(endpoint.IPAMConfig.LinkLocalIPs) == 0 {
|
||||
endpoint.IPAMConfig = nil
|
||||
if ipam.IPv6Address != "" && !networkSupportsStaticIP(info, ipam.IPv6Address, true) {
|
||||
return fmt.Errorf("static IPv6 address %s is not in a configured subnet of network %s", ipam.IPv6Address, netName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDefaultBridgeNetwork(netName string, info network.Inspect) bool {
|
||||
@@ -673,6 +684,7 @@ func networkSupportsStaticIP(info network.Inspect, ip string, isIPv6 bool) bool
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
addr = addr.Unmap()
|
||||
if addr.Is6() != isIPv6 {
|
||||
return false
|
||||
}
|
||||
@@ -813,11 +825,6 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
|
||||
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 {
|
||||
config.EndpointsConfig[name] = endpointSetting
|
||||
@@ -831,39 +838,6 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
|
||||
return config, extraNetworks
|
||||
}
|
||||
|
||||
const unsupportedUserSpecifiedIPAddress = "user specified IP address is supported only when connecting to networks with user configured subnets"
|
||||
|
||||
func clearUnsupportedDynamicEndpointIPAM(err error, endpoints map[string]*network.EndpointSettings, networkSettings *container.NetworkSettings) bool {
|
||||
if err == nil || !strings.Contains(err.Error(), unsupportedUserSpecifiedIPAddress) {
|
||||
return false
|
||||
}
|
||||
for name, endpoint := range endpoints {
|
||||
if !isDynamicContainerNetwork(networkSettings, name) || endpoint == nil || endpoint.IPAMConfig == nil {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(err.Error(), "network "+name+":") {
|
||||
endpoint.IPAMConfig = nil
|
||||
return true
|
||||
}
|
||||
}
|
||||
cleared := false
|
||||
for name, endpoint := range endpoints {
|
||||
if isDynamicContainerNetwork(networkSettings, name) && endpoint != nil && endpoint.IPAMConfig != nil {
|
||||
endpoint.IPAMConfig = nil
|
||||
cleared = true
|
||||
}
|
||||
}
|
||||
return cleared
|
||||
}
|
||||
|
||||
func isDynamicContainerNetwork(networkSettings *container.NetworkSettings, name string) bool {
|
||||
if networkSettings == nil || name == "bridge" {
|
||||
return false
|
||||
}
|
||||
endpoint := networkSettings.Networks[name]
|
||||
return endpoint != nil && endpoint.IPAMConfig == nil && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "")
|
||||
}
|
||||
|
||||
func cloneContainerConfig(config *container.Config) *container.Config {
|
||||
if config == nil {
|
||||
return &container.Config{}
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
@@ -39,6 +40,7 @@ func (u *BackupService) MysqlBackup(req dto.CommonBackup) error {
|
||||
TaskID: req.TaskID,
|
||||
Status: constant.StatusWaiting,
|
||||
Description: req.Description,
|
||||
Args: encodeBackupArgs(req.Args),
|
||||
}
|
||||
if err := backupRepo.CreateRecord(record); err != nil {
|
||||
global.LOG.Errorf("save backup record failed, err: %v", err)
|
||||
@@ -143,6 +145,14 @@ func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, 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)))
|
||||
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{
|
||||
Name: req.DetailName,
|
||||
Type: req.Type,
|
||||
@@ -150,6 +160,7 @@ func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback
|
||||
Format: dbInfo.Format,
|
||||
TargetDir: path.Dir(rollbackFile),
|
||||
FileName: path.Base(rollbackFile),
|
||||
Args: rollbackArgs,
|
||||
}); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
if !strings.HasSuffix(file, ".tar.gz") && !strings.HasSuffix(file, ".zip") {
|
||||
return file, nil
|
||||
|
||||
+72
-139
@@ -16,6 +16,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
@@ -71,6 +73,7 @@ type IContainerService interface {
|
||||
ComposeOperation(req dto.ComposeOperation) error
|
||||
TestCompose(req dto.ComposeCreate) (bool, error)
|
||||
ComposeUpdate(req dto.ComposeUpdate) error
|
||||
ComposePin(req dto.ComposePin) error
|
||||
ComposeLogClean(req dto.ComposeLogClean) error
|
||||
|
||||
ContainerCreate(req dto.ContainerOperate, inThread bool) error
|
||||
@@ -78,7 +81,7 @@ type IContainerService interface {
|
||||
ContainerUpgrade(req dto.ContainerUpgrade) error
|
||||
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
|
||||
ContainerListStats() ([]dto.ContainerListStats, error)
|
||||
ContainerItemStats(req dto.OperationWithName) (dto.ContainerItemStats, error)
|
||||
ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error)
|
||||
LoadResourceLimit() (*dto.ResourceLimit, error)
|
||||
ContainerRename(req dto.ContainerRename) error
|
||||
ContainerCommit(req dto.ContainerCommit) error
|
||||
@@ -245,15 +248,15 @@ func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
func (u *ContainerService) ContainerItemStats(req dto.OperationWithName) (dto.ContainerItemStats, error) {
|
||||
func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error) {
|
||||
var data dto.ContainerItemStats
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
defer client.Close()
|
||||
if req.Name != "system" {
|
||||
defer client.Close()
|
||||
containerInfo, _, err := client.ContainerInspectWithRaw(context.Background(), req.Name, true)
|
||||
containerInfo, _, err := client.ContainerInspectWithRaw(ctx, req.Name, true)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
@@ -262,7 +265,7 @@ func (u *ContainerService) ContainerItemStats(req dto.OperationWithName) (dto.Co
|
||||
return data, nil
|
||||
}
|
||||
|
||||
usage, err := client.DiskUsage(context.Background(), types.DiskUsageOptions{})
|
||||
usage, err := client.DiskUsage(ctx, types.DiskUsageOptions{})
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
@@ -380,11 +383,6 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
|
||||
}
|
||||
|
||||
func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
name := ""
|
||||
switch req.PruneType {
|
||||
case "container":
|
||||
@@ -405,6 +403,14 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
||||
}
|
||||
|
||||
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
|
||||
if err := t.TaskCtx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
pruneFilters := filters.NewArgs()
|
||||
if req.WithTagAll {
|
||||
pruneFilters.Add("dangling", "false")
|
||||
@@ -428,10 +434,7 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) error {
|
||||
}
|
||||
SpaceReclaimed = int(rep.SpaceReclaimed)
|
||||
case "network":
|
||||
_, err := client.NetworksPrune(context.Background(), pruneFilters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cleanUnusedNetworks(t, client)
|
||||
case "volume":
|
||||
versions, err := client.ServerVersion(context.Background())
|
||||
if err != nil {
|
||||
@@ -533,7 +536,9 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
normalizeContainerEndpointSettings(ctx, client, networkConf, nil)
|
||||
if err := normalizeContainerEndpointSettings(ctx, client, networkConf, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
|
||||
if err != nil {
|
||||
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
|
||||
@@ -643,14 +648,9 @@ func loadContainerNetworkInfo(name string, endpoint *network.EndpointSettings) d
|
||||
if endpoint.IPAMConfig != nil {
|
||||
item.LinkLocalIPs = append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...)
|
||||
}
|
||||
if name != "bridge" {
|
||||
if endpoint.IPAMConfig != nil {
|
||||
item.Ipv4 = endpoint.IPAMConfig.IPv4Address
|
||||
item.Ipv6 = endpoint.IPAMConfig.IPv6Address
|
||||
} else {
|
||||
item.Ipv4 = endpoint.IPAddress
|
||||
item.Ipv6 = endpoint.GlobalIPv6Address
|
||||
}
|
||||
if name != "bridge" && endpoint.IPAMConfig != nil {
|
||||
item.Ipv4 = endpoint.IPAMConfig.IPv4Address
|
||||
item.Ipv6 = endpoint.IPAMConfig.IPv6Address
|
||||
}
|
||||
return item
|
||||
}
|
||||
@@ -1677,30 +1677,42 @@ func checkImageLike(client *client.Client, imageName string) bool {
|
||||
|
||||
func pullImages(task *task.Task, client *client.Client, imageName string) error {
|
||||
dockerCli := docker.NewClientWithExist(client)
|
||||
repos, err := imageRepoRepo.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imageRepo := selectImageRepo(imageName, repos)
|
||||
if imageRepo == nil || !imageRepo.Auth {
|
||||
return dockerCli.PullImageWithProcess(task, imageName)
|
||||
}
|
||||
|
||||
options := image.PullOptions{}
|
||||
repos, _ := imageRepoRepo.List()
|
||||
if len(repos) != 0 {
|
||||
for _, repo := range repos {
|
||||
if strings.HasPrefix(imageName, repo.DownloadUrl) && repo.Auth {
|
||||
authConfig := registry.AuthConfig{
|
||||
Username: repo.Username,
|
||||
Password: repo.Password,
|
||||
}
|
||||
encodedJSON, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
|
||||
options.RegistryAuth = authStr
|
||||
}
|
||||
authConfig := registry.AuthConfig{
|
||||
Username: imageRepo.Username,
|
||||
Password: imageRepo.Password,
|
||||
}
|
||||
encodedJSON, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON)
|
||||
return dockerCli.PullImageWithProcessAndOptions(task, imageName, options)
|
||||
}
|
||||
|
||||
func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo {
|
||||
var selected *model.ImageRepo
|
||||
selectedURLLength := 0
|
||||
for i := range repos {
|
||||
downloadURL := strings.TrimRight(strings.TrimSpace(repos[i].DownloadUrl), "/")
|
||||
if downloadURL == "" || !strings.HasPrefix(imageName, downloadURL+"/") {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
hasAuth, authStr := loadAuthInfo(imageName)
|
||||
if hasAuth {
|
||||
options.RegistryAuth = authStr
|
||||
if len(downloadURL) > selectedURLLength {
|
||||
selected = &repos[i]
|
||||
selectedURLLength = len(downloadURL)
|
||||
}
|
||||
}
|
||||
return dockerCli.PullImageWithProcessAndOptions(task, imageName, options)
|
||||
return selected
|
||||
}
|
||||
|
||||
func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerListStats {
|
||||
@@ -1757,7 +1769,10 @@ func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error
|
||||
}
|
||||
for i := 0; i <= hostEnd-hostStart; i++ {
|
||||
bindItem := nat.PortBinding{HostPort: strconv.Itoa(hostStart + i), HostIP: port.HostIP}
|
||||
portMap[nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))] = []nat.PortBinding{bindItem}
|
||||
portKey := nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))
|
||||
if !slices.Contains(portMap[portKey], bindItem) {
|
||||
portMap[portKey] = append(portMap[portKey], bindItem)
|
||||
}
|
||||
}
|
||||
for i := hostStart; i <= hostEnd; i++ {
|
||||
if checkInUse && common.ScanPortWithIP(port.HostIP, i) {
|
||||
@@ -1775,7 +1790,10 @@ func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error
|
||||
return portMap, buserr.WithDetail("ErrPortInUsed", portItem, nil)
|
||||
}
|
||||
bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP}
|
||||
portMap[nat.Port(fmt.Sprintf("%s/%s", port.ContainerPort, port.Protocol))] = []nat.PortBinding{bindItem}
|
||||
portKey := nat.Port(fmt.Sprintf("%s/%s", port.ContainerPort, port.Protocol))
|
||||
if !slices.Contains(portMap[portKey], bindItem) {
|
||||
portMap[portKey] = append(portMap[portKey], bindItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
return portMap, nil
|
||||
@@ -1921,90 +1939,7 @@ func loadPortByInspect(id string, client *client.Client) ([]container.Port, erro
|
||||
return itemPorts, nil
|
||||
}
|
||||
func transPortToStr(ports []container.Port) []string {
|
||||
var (
|
||||
ipv4Ports []container.Port
|
||||
ipv6Ports []container.Port
|
||||
)
|
||||
for _, port := range ports {
|
||||
if strings.Contains(port.IP, ":") {
|
||||
ipv6Ports = append(ipv6Ports, port)
|
||||
} else {
|
||||
ipv4Ports = append(ipv4Ports, port)
|
||||
}
|
||||
}
|
||||
list1 := simplifyPort(ipv4Ports)
|
||||
list2 := simplifyPort(ipv6Ports)
|
||||
return append(list1, list2...)
|
||||
}
|
||||
func simplifyPort(ports []container.Port) []string {
|
||||
var datas []string
|
||||
if len(ports) == 0 {
|
||||
return datas
|
||||
}
|
||||
if len(ports) == 1 {
|
||||
ip := ""
|
||||
if len(ports[0].IP) != 0 {
|
||||
ip = ports[0].IP + ":"
|
||||
}
|
||||
itemPortStr := fmt.Sprintf("%s%v/%s", ip, ports[0].PrivatePort, ports[0].Type)
|
||||
if ports[0].PublicPort != 0 {
|
||||
itemPortStr = fmt.Sprintf("%s%v->%v/%s", ip, ports[0].PublicPort, ports[0].PrivatePort, ports[0].Type)
|
||||
}
|
||||
datas = append(datas, itemPortStr)
|
||||
return datas
|
||||
}
|
||||
|
||||
sort.Slice(ports, func(i, j int) bool {
|
||||
return ports[i].PrivatePort < ports[j].PrivatePort
|
||||
})
|
||||
start := ports[0]
|
||||
|
||||
for i := 1; i < len(ports); i++ {
|
||||
if ports[i].PrivatePort != ports[i-1].PrivatePort+1 || ports[i].IP != ports[i-1].IP || ports[i].PublicPort != ports[i-1].PublicPort+1 || ports[i].Type != ports[i-1].Type {
|
||||
if ports[i-1].PrivatePort == start.PrivatePort {
|
||||
itemPortStr := fmt.Sprintf("%s:%v/%s", start.IP, start.PrivatePort, start.Type)
|
||||
if start.PublicPort != 0 {
|
||||
itemPortStr = fmt.Sprintf("%s:%v->%v/%s", start.IP, start.PublicPort, start.PrivatePort, start.Type)
|
||||
}
|
||||
if len(start.IP) == 0 {
|
||||
itemPortStr = strings.TrimPrefix(itemPortStr, ":")
|
||||
}
|
||||
datas = append(datas, itemPortStr)
|
||||
} else {
|
||||
itemPortStr := fmt.Sprintf("%s:%v-%v/%s", start.IP, start.PrivatePort, ports[i-1].PrivatePort, start.Type)
|
||||
if start.PublicPort != 0 {
|
||||
itemPortStr = fmt.Sprintf("%s:%v-%v->%v-%v/%s", start.IP, start.PublicPort, ports[i-1].PublicPort, start.PrivatePort, ports[i-1].PrivatePort, start.Type)
|
||||
}
|
||||
if len(start.IP) == 0 {
|
||||
itemPortStr = strings.TrimPrefix(itemPortStr, ":")
|
||||
}
|
||||
datas = append(datas, itemPortStr)
|
||||
}
|
||||
start = ports[i]
|
||||
}
|
||||
if i == len(ports)-1 {
|
||||
if ports[i].PrivatePort == start.PrivatePort {
|
||||
itemPortStr := fmt.Sprintf("%s:%v/%s", start.IP, start.PrivatePort, start.Type)
|
||||
if start.PublicPort != 0 {
|
||||
itemPortStr = fmt.Sprintf("%s:%v->%v/%s", start.IP, start.PublicPort, start.PrivatePort, start.Type)
|
||||
}
|
||||
if len(start.IP) == 0 {
|
||||
itemPortStr = strings.TrimPrefix(itemPortStr, ":")
|
||||
}
|
||||
datas = append(datas, itemPortStr)
|
||||
} else {
|
||||
itemPortStr := fmt.Sprintf("%s:%v-%v/%s", start.IP, start.PrivatePort, ports[i].PrivatePort, start.Type)
|
||||
if start.PublicPort != 0 {
|
||||
itemPortStr = fmt.Sprintf("%s:%v-%v->%v-%v/%s", start.IP, start.PublicPort, ports[i].PublicPort, start.PrivatePort, ports[i].PrivatePort, start.Type)
|
||||
}
|
||||
if len(start.IP) == 0 {
|
||||
itemPortStr = strings.TrimPrefix(itemPortStr, ":")
|
||||
}
|
||||
datas = append(datas, itemPortStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return datas
|
||||
return docker.SimplifyPorts(ports)
|
||||
}
|
||||
|
||||
func loadComposeCount(client *client.Client) int {
|
||||
@@ -2025,6 +1960,9 @@ func loadComposeCount(client *client.Client) int {
|
||||
}
|
||||
}
|
||||
for _, compose := range composeCreatedByLocal {
|
||||
if len(compose.Path) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, has := composeMap[compose.Name]; !has {
|
||||
composeMap[compose.Name] = struct{}{}
|
||||
}
|
||||
@@ -2034,7 +1972,7 @@ func loadComposeCount(client *client.Client) int {
|
||||
}
|
||||
func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper {
|
||||
var exposedPorts []dto.PortHelper
|
||||
samePortMap := make(map[string]dto.PortHelper)
|
||||
seenPorts := make(map[dto.PortHelper]struct{})
|
||||
ports := transPortToStr(itemPorts)
|
||||
for _, item := range ports {
|
||||
itemStr := strings.Split(item, "->")
|
||||
@@ -2055,16 +1993,11 @@ func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper {
|
||||
}
|
||||
itemPort.ContainerPort = itemContainer[0]
|
||||
itemPort.Protocol = itemContainer[1]
|
||||
keyItem := fmt.Sprintf("%s->%s/%s", itemPort.HostPort, itemPort.ContainerPort, itemPort.Protocol)
|
||||
if val, ok := samePortMap[keyItem]; ok {
|
||||
val.HostIP = ""
|
||||
samePortMap[keyItem] = val
|
||||
} else {
|
||||
samePortMap[keyItem] = itemPort
|
||||
if _, exists := seenPorts[itemPort]; exists {
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, val := range samePortMap {
|
||||
exposedPorts = append(exposedPorts, val)
|
||||
seenPorts[itemPort] = struct{}{}
|
||||
exposedPorts = append(exposedPorts, itemPort)
|
||||
}
|
||||
return exposedPorts
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -23,8 +24,11 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||
"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/filters"
|
||||
"gopkg.in/yaml.v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const composeProjectLabel = "com.docker.compose.project"
|
||||
@@ -52,7 +56,15 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
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)
|
||||
for _, localItem := range composeCreatedByLocal {
|
||||
composeItemLocal := dto.ComposeInfo{
|
||||
@@ -136,6 +148,7 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
for key, value := range mergedMap {
|
||||
value.Name = key
|
||||
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
|
||||
value.IsPinned = pinnedByName[key]
|
||||
records = append(records, value)
|
||||
}
|
||||
if len(req.Info) != 0 {
|
||||
@@ -149,7 +162,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 {
|
||||
if records[i].IsPinned != records[j].IsPinned {
|
||||
return records[i].IsPinned
|
||||
}
|
||||
return records[i].CreatedAt > records[j].CreatedAt
|
||||
})
|
||||
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||
@@ -189,54 +216,56 @@ func composeFileExists(workdir, configFile string) bool {
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
composeItem, _ := composeRepo.GetRecord(repo.WithByName(req.Name))
|
||||
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()
|
||||
projectName, err := resolveComposeCreateProjectName(req)
|
||||
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
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if req.From == "path" {
|
||||
req.Name = path.Base(path.Dir(req.Path))
|
||||
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
||||
return err
|
||||
}
|
||||
req.Name = projectName
|
||||
recordName := strings.ToLower(req.Name)
|
||||
if err := saveComposeRecord(recordName, req.Path); err != nil {
|
||||
return fmt.Errorf("save compose record failed, err: %v", err)
|
||||
}
|
||||
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeCompose, req.TaskID, 1)
|
||||
if err != nil {
|
||||
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() {
|
||||
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)
|
||||
if err != nil {
|
||||
_, _ = compose.Down(req.Path)
|
||||
return err
|
||||
}
|
||||
_ = composeRepo.CreateRecord(&model.Compose{Name: strings.ToLower(req.Name), Path: req.Path})
|
||||
return nil
|
||||
return err
|
||||
}, nil)
|
||||
_ = taskItem.Execute()
|
||||
}()
|
||||
@@ -244,6 +273,254 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveComposeRecord(name, composePath string) error {
|
||||
record, err := composeRepo.GetRecord(repo.WithByName(name))
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
if record.ID == 0 {
|
||||
return composeRepo.CreateRecord(&model.Compose{Name: name, Path: composePath})
|
||||
}
|
||||
return composeRepo.UpdateRecord(name, map[string]interface{}{"path": composePath})
|
||||
}
|
||||
|
||||
func checkComposeRecordName(name string) error {
|
||||
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 {
|
||||
if len(req.Path) == 0 && req.Operation == "delete" {
|
||||
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
||||
@@ -267,15 +544,15 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
@@ -311,9 +588,9 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
||||
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)
|
||||
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 when handle compose up, err: %s", err)
|
||||
@@ -327,6 +604,20 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
||||
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 {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
@@ -389,15 +680,15 @@ func (u *ContainerService) LoadComposeEnv(name string) (string, error) {
|
||||
|
||||
func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
||||
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.MkdirAll(dir, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/docker-compose.yml", dir)
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, constant.FilePerm)
|
||||
file, err := os.OpenFile(composePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, constant.FilePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -405,14 +696,14 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
||||
write := bufio.NewWriter(file)
|
||||
_, _ = write.WriteString(string(req.File))
|
||||
write.Flush()
|
||||
req.Path = path
|
||||
req.Path = composePath
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeContainerForCompose(composeName, composePath string) error {
|
||||
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 nil
|
||||
@@ -437,7 +728,7 @@ func removeContainerForCompose(composeName, composePath string) error {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -447,7 +738,7 @@ func recreateCompose(content, path string) error {
|
||||
_, _ = write.WriteString(content)
|
||||
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 nil
|
||||
|
||||
@@ -2,16 +2,24 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/errdefs"
|
||||
)
|
||||
|
||||
var networkCleanupSlot = make(chan struct{}, 1)
|
||||
|
||||
func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
@@ -172,3 +180,114 @@ func (u *ContainerService) CreateNetwork(req dto.NetworkCreate) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanUnusedNetworks(t *task.Task, cli *client.Client) error {
|
||||
ctx := t.TaskCtx
|
||||
select {
|
||||
case networkCleanupSlot <- struct{}{}:
|
||||
defer func() { <-networkCleanupSlot }()
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
networks, err := cli.NetworkList(ctx, network.ListOptions{})
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
return err
|
||||
}
|
||||
deleted, skipped, failed := 0, 0, 0
|
||||
defer func() {
|
||||
t.Log(i18n.GetMsgWithMap("NetworkCleanupSummary", map[string]interface{}{
|
||||
"deleted": deleted, "skipped": skipped, "failed": failed,
|
||||
}))
|
||||
}()
|
||||
var used map[string]bool
|
||||
sort.Slice(networks, func(i, j int) bool { return networks[i].Name < networks[j].Name })
|
||||
for _, n := range networks {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
switch n.Name {
|
||||
case "none", "host", "bridge", "1panel-network":
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
if n.Scope != "local" || n.Ingress || n.ConfigOnly {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
values := map[string]interface{}{"name": n.Name, "id": n.ID}
|
||||
if used == nil {
|
||||
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
return err
|
||||
}
|
||||
used = make(map[string]bool)
|
||||
for _, c := range containers {
|
||||
if c.NetworkSettings == nil {
|
||||
continue
|
||||
}
|
||||
for name, endpoint := range c.NetworkSettings.Networks {
|
||||
used[name] = true
|
||||
if endpoint != nil {
|
||||
used[endpoint.NetworkID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if used[n.Name] || used[n.ID] {
|
||||
skipped++
|
||||
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
|
||||
continue
|
||||
}
|
||||
inspected, err := cli.NetworkInspect(ctx, n.ID, network.InspectOptions{})
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if errdefs.IsNotFound(err) {
|
||||
skipped++
|
||||
} else {
|
||||
failed++
|
||||
t.Logf("Failed to inspect network [%s] (%s): %v", n.Name, n.ID, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(inspected.Containers) > 0 {
|
||||
skipped++
|
||||
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
|
||||
continue
|
||||
}
|
||||
if err := cli.NetworkRemove(ctx, n.ID); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
switch {
|
||||
case errdefs.IsNotFound(err):
|
||||
skipped++
|
||||
case errdefs.IsConflict(err):
|
||||
skipped++
|
||||
t.Log(i18n.GetMsgWithMap("NetworkCleanupConnected", values))
|
||||
default:
|
||||
failed++
|
||||
t.Logf("Failed to remove network [%s] (%s): %v", n.Name, n.ID, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
deleted++
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if failed > 0 {
|
||||
return errors.New(i18n.GetMsgByKey("NetworkCleanupPartialFailure"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,6 +20,7 @@ import (
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
@@ -64,13 +67,13 @@ func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
normalizeContainerEndpointSettings(ctx, client, networkConf, nil)
|
||||
if err := normalizeContainerEndpointSettings(ctx, client, networkConf, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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))
|
||||
return client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
|
||||
}, config.Tty, t)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update container failed, err: %v", err)
|
||||
}
|
||||
@@ -135,9 +138,15 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
|
||||
config.Image = req.Image
|
||||
hostConf := cloneContainerHostConfig(oldContainer.HostConfig)
|
||||
preserveContainerVolumeMounts(hostConf, oldContainer.Mounts)
|
||||
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(oldContainer.NetworkSettings, hostConf)
|
||||
if err := normalizeContainerEndpointSettings(ctx, client, networkConf, extraNetworks); err != nil {
|
||||
upgradeErr := fmt.Errorf("prepare networks for container %s failed: %w", item, err)
|
||||
upgradeErrors = append(upgradeErrors, upgradeErr)
|
||||
return upgradeErr
|
||||
}
|
||||
cleanupErr, err := switchContainer(ctx, client, item, oldContainer, func() (container.CreateResponse, error) {
|
||||
return createContainerWithOldNetworks(ctx, client, config, hostConf, oldContainer.NetworkSettings, item)
|
||||
}, newContainerSwitchTaskLogger(t))
|
||||
return createContainerWithNetworks(ctx, client, config, hostConf, networkConf, extraNetworks, item)
|
||||
}, config.Tty, t)
|
||||
if err != nil {
|
||||
upgradeErr := fmt.Errorf("upgrade container %s failed: %w", item, err)
|
||||
upgradeErrors = append(upgradeErrors, upgradeErr)
|
||||
@@ -166,10 +175,15 @@ type containerSwitchClient interface {
|
||||
ContainerStart(context.Context, string, container.StartOptions) error
|
||||
ContainerRemove(context.Context, string, container.RemoveOptions) error
|
||||
ContainerInspect(context.Context, string) (container.InspectResponse, error)
|
||||
ContainerLogs(context.Context, string, container.LogsOptions) (io.ReadCloser, error)
|
||||
NetworkConnect(context.Context, string, string, *network.EndpointSettings) error
|
||||
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
|
||||
@@ -234,22 +248,18 @@ func (l *containerOperationMutex) lock(names ...string) func() {
|
||||
}
|
||||
|
||||
type containerNetworkAttachment struct {
|
||||
name string
|
||||
endpoint *network.EndpointSettings
|
||||
isDynamic bool
|
||||
name string
|
||||
endpoint *network.EndpointSettings
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
type containerSwitchLogger interface {
|
||||
LogWithStatus(string, error)
|
||||
Log(string)
|
||||
}
|
||||
|
||||
func logContainerSwitchStep(logger containerSwitchLogFunc, messageKey, containerName string, err error) {
|
||||
func logContainerSwitchStep(logger containerSwitchLogger, messageKey, containerName string, err error) {
|
||||
if logger != nil {
|
||||
logger(messageKey, containerName, err)
|
||||
logger.LogWithStatus(i18n.GetWithName(messageKey, containerName), err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +270,8 @@ func switchContainer(
|
||||
name string,
|
||||
oldContainer container.InspectResponse,
|
||||
createNew func() (container.CreateResponse, error),
|
||||
logger containerSwitchLogFunc,
|
||||
tty bool,
|
||||
logger containerSwitchLogger,
|
||||
) (cleanupErr error, err error) {
|
||||
if oldContainer.ID == "" {
|
||||
return nil, fmt.Errorf("original container ID is empty")
|
||||
@@ -310,6 +321,7 @@ func switchContainer(
|
||||
}
|
||||
if err := cli.ContainerStart(ctx, created.ID, container.StartOptions{}); err != nil {
|
||||
logContainerSwitchStep(logger, "ContainerStartReplacement", name, err)
|
||||
logContainerStartupLogs(ctx, cli, created.ID, name, tty, logger)
|
||||
rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger)
|
||||
return nil, errors.Join(fmt.Errorf("start new container failed: %w", err), rollbackErr)
|
||||
}
|
||||
@@ -317,6 +329,7 @@ func switchContainer(
|
||||
if wasRunning {
|
||||
if err := waitContainerReady(ctx, cli, created.ID); err != nil {
|
||||
logContainerSwitchStep(logger, "ContainerWaitReplacement", name, err)
|
||||
logContainerStartupLogs(ctx, cli, created.ID, name, tty, logger)
|
||||
rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger)
|
||||
return nil, errors.Join(fmt.Errorf("new container readiness check failed: %w", err), rollbackErr)
|
||||
}
|
||||
@@ -333,9 +346,49 @@ const (
|
||||
containerStartPollInterval = time.Second
|
||||
containerHealthCheckMinWait = 30 * time.Second
|
||||
containerHealthCheckMaxWait = 10 * time.Minute
|
||||
containerDiagnosticLogTail = "200"
|
||||
)
|
||||
|
||||
func waitContainerReady(ctx context.Context, cli containerSwitchClient, containerID string) error {
|
||||
func logContainerStartupLogs(ctx context.Context, cli containerSwitchClient, containerID, name string, tty bool, logger containerSwitchLogger) {
|
||||
if logger == nil {
|
||||
return
|
||||
}
|
||||
logger.Log(fmt.Sprintf("========== %s ==========", i18n.GetWithName("ContainerStartupDiagnostic", name)))
|
||||
diagnosticCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
reader, err := cli.ContainerLogs(diagnosticCtx, containerID, container.LogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Timestamps: true,
|
||||
Tail: containerDiagnosticLogTail,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Log(i18n.GetWithNameAndErr("ContainerDiagnosticLogsFailed", name, err))
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
var output bytes.Buffer
|
||||
if tty {
|
||||
_, err = io.Copy(&output, reader)
|
||||
} else {
|
||||
_, err = stdcopy.StdCopy(&output, &output, reader)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Log(i18n.GetWithNameAndErr("ContainerDiagnosticLogsFailed", name, err))
|
||||
return
|
||||
}
|
||||
logs := strings.TrimSpace(output.String())
|
||||
logger.Log(fmt.Sprintf("---------- %s ----------", i18n.GetMsgByKey("ContainerRecentLogs")))
|
||||
if logs == "" {
|
||||
logger.Log(i18n.GetMsgByKey("ContainerDiagnosticLogsEmpty"))
|
||||
return
|
||||
}
|
||||
logger.Log(logs)
|
||||
}
|
||||
|
||||
func waitContainerReady(ctx context.Context, cli containerInspectClient, containerID string) error {
|
||||
info, err := cli.ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -347,14 +400,15 @@ func waitContainerReady(ctx context.Context, cli containerSwitchClient, containe
|
||||
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 != 0 {
|
||||
return fmt.Errorf("container restarted %d times during startup", info.RestartCount)
|
||||
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")
|
||||
@@ -382,9 +436,10 @@ func waitContainerReady(ctx context.Context, cli containerSwitchClient, containe
|
||||
}
|
||||
}
|
||||
|
||||
func waitContainerStable(ctx context.Context, cli containerSwitchClient, containerID string, initial container.InspectResponse) error {
|
||||
func waitContainerStable(ctx context.Context, cli containerInspectClient, containerID string, initial container.InspectResponse) error {
|
||||
startedAt := initial.State.StartedAt
|
||||
if err := checkContainerStableState(initial, startedAt); err != nil {
|
||||
restartCount := initial.RestartCount
|
||||
if err := checkContainerStableState(initial, startedAt, restartCount); err != nil {
|
||||
return err
|
||||
}
|
||||
deadline := time.NewTimer(containerStartStabilization)
|
||||
@@ -400,25 +455,25 @@ func waitContainerStable(ctx context.Context, cli containerSwitchClient, contain
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return checkContainerStableState(info, startedAt)
|
||||
return checkContainerStableState(info, startedAt, restartCount)
|
||||
case <-ticker.C:
|
||||
info, err := cli.ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkContainerStableState(info, startedAt); err != nil {
|
||||
if err := checkContainerStableState(info, startedAt, restartCount); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkContainerStableState(info container.InspectResponse, startedAt string) error {
|
||||
func checkContainerStableState(info container.InspectResponse, startedAt string, restartCount int) error {
|
||||
if err := checkContainerRunningState(info); err != nil {
|
||||
return err
|
||||
}
|
||||
if info.State.Restarting || info.RestartCount != 0 {
|
||||
return fmt.Errorf("container restarted %d times during startup", info.RestartCount)
|
||||
if 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")
|
||||
@@ -532,13 +587,13 @@ func disconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitc
|
||||
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 {
|
||||
if name != "bridge" && endpoint != nil {
|
||||
endpoints[name] = endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
for name, endpoint := range extras {
|
||||
if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil {
|
||||
if name != "bridge" && endpoint != nil {
|
||||
endpoints[name] = endpoint
|
||||
}
|
||||
}
|
||||
@@ -554,9 +609,8 @@ func disconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitc
|
||||
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),
|
||||
name: name,
|
||||
endpoint: endpoints[name],
|
||||
})
|
||||
}
|
||||
return disconnected, nil
|
||||
@@ -566,10 +620,6 @@ func reconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitch
|
||||
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))
|
||||
}
|
||||
@@ -577,7 +627,7 @@ func reconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitch
|
||||
return reconnectErr
|
||||
}
|
||||
|
||||
func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, oldContainerID, originalName string, wasRunning bool, newContainer string, disconnectedNetworks []containerNetworkAttachment, logger containerSwitchLogFunc) error {
|
||||
func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, oldContainerID, originalName string, wasRunning bool, newContainer string, disconnectedNetworks []containerNetworkAttachment, logger containerSwitchLogger) error {
|
||||
var rollbackErr error
|
||||
backupName := containerSwitchBackupName(oldContainerID)
|
||||
if newContainer != "" {
|
||||
@@ -602,7 +652,7 @@ func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, ol
|
||||
reconnectErr := reconnectOriginalContainerNetworks(ctx, cli, oldContainerID, disconnectedNetworks)
|
||||
logContainerSwitchStep(logger, "ContainerRollbackReconnectOld", currentName, reconnectErr)
|
||||
rollbackErr = errors.Join(rollbackErr, reconnectErr)
|
||||
if wasRunning {
|
||||
if wasRunning && reconnectErr == nil {
|
||||
restartErr := restartOriginalContainer(ctx, cli, oldContainerID)
|
||||
logContainerSwitchStep(logger, "ContainerRollbackRestartOld", currentName, restartErr)
|
||||
rollbackErr = errors.Join(rollbackErr, restartErr)
|
||||
@@ -610,17 +660,8 @@ func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, ol
|
||||
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)
|
||||
func createContainerWithNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkConf *network.NetworkingConfig, extraNetworks map[string]*network.EndpointSettings, name string) (container.CreateResponse, error) {
|
||||
created, err := client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name)
|
||||
if err != nil {
|
||||
return created, err
|
||||
}
|
||||
@@ -632,9 +673,6 @@ func createContainerWithOldNetworks(ctx context.Context, client *client.Client,
|
||||
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
|
||||
@@ -642,16 +680,3 @@ func createContainerWithOldNetworks(ctx context.Context, client *client.Client,
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/pkg/errors"
|
||||
@@ -75,6 +76,7 @@ func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interfac
|
||||
EntryID: cronjob.ID,
|
||||
}
|
||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable))
|
||||
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
|
||||
if alertInfo.SendCount != 0 {
|
||||
item.AlertCount = alertInfo.SendCount
|
||||
} else {
|
||||
@@ -98,9 +100,11 @@ func (u *CronjobService) LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, err
|
||||
AlertType: cronjob.Type,
|
||||
EntryID: cronjob.ID,
|
||||
}
|
||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable))
|
||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))))
|
||||
item.AlertMethod = alertInfo.Method
|
||||
if alertInfo.SendCount != 0 {
|
||||
item.AlertTitle = alertInfo.Title
|
||||
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
|
||||
if alertInfo.Status == constant.AlertEnable {
|
||||
item.AlertCount = alertInfo.SendCount
|
||||
} else {
|
||||
item.AlertCount = 0
|
||||
@@ -195,11 +199,12 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
|
||||
}
|
||||
}
|
||||
item.SourceAccounts, item.DownloadAccount, _ = loadBackupNamesByID(cronjob.SourceAccountIDs, cronjob.DownloadAccountID)
|
||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))), repo.WithByStatus(constant.AlertEnable))
|
||||
if alertInfo.SendCount != 0 {
|
||||
alertInfo, _ := alertRepo.Get(alertRepo.WithByType(cronjob.Type), alertRepo.WithByProject(strconv.Itoa(int(cronjob.ID))))
|
||||
item.AlertTitle = alertInfo.Title
|
||||
item.AlertMethod = alertInfo.Method
|
||||
item.AlertTriggerMode, _ = alertUtil.CronJobAlertTriggerMode(alertInfo.AdvancedParams)
|
||||
if alertInfo.Status == constant.AlertEnable {
|
||||
item.AlertCount = alertInfo.SendCount
|
||||
item.AlertTitle = alertInfo.Title
|
||||
item.AlertMethod = alertInfo.Method
|
||||
} else {
|
||||
item.AlertCount = 0
|
||||
}
|
||||
@@ -213,6 +218,17 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
|
||||
}
|
||||
|
||||
func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
|
||||
for _, item := range req {
|
||||
advanced, err := cronJobAlertAdvancedParams(item.AlertTriggerMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if item.AlertCount != 0 {
|
||||
if err := (AlertService{}).validateCronJobAlertChannels(item.Type, advanced, item.AlertMethod); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, item := range req {
|
||||
cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name))
|
||||
if cronjobItem.ID != 0 {
|
||||
@@ -395,17 +411,27 @@ func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
|
||||
} else {
|
||||
cronjob.Status = constant.StatusDisable
|
||||
}
|
||||
_ = cronjobRepo.Create(&cronjob)
|
||||
if item.AlertCount != 0 && item.AlertTitle != "" && item.AlertMethod != "" {
|
||||
if err := cronjobRepo.Create(&cronjob); err != nil {
|
||||
return err
|
||||
}
|
||||
if item.AlertTitle != "" && item.AlertMethod != "" {
|
||||
advanced, _ := cronJobAlertAdvancedParams(item.AlertTriggerMode)
|
||||
status := constant.AlertEnable
|
||||
if item.AlertCount == 0 {
|
||||
status = constant.AlertDisable
|
||||
}
|
||||
createAlert := dto.AlertCreate{
|
||||
Title: item.AlertTitle,
|
||||
SendCount: item.AlertCount,
|
||||
Method: item.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronjob.ID)),
|
||||
Status: constant.AlertEnable,
|
||||
Title: item.AlertTitle,
|
||||
SendCount: item.AlertCount,
|
||||
Method: item.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronjob.ID)),
|
||||
Status: status,
|
||||
AdvancedParams: advanced,
|
||||
}
|
||||
if err := NewIAlertService().CreateAlert(createAlert, operator); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = NewIAlertService().CreateAlert(createAlert, operator)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -538,11 +564,14 @@ func (u *CronjobService) CleanRecord(req dto.CronjobClean) error {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
}
|
||||
if err := cronjobRepo.DeleteRecord(cronjobRepo.WithByJobID(int(req.CronjobID))); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -559,6 +588,15 @@ func (u *CronjobService) HandleOnce(id uint) error {
|
||||
}
|
||||
|
||||
func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
|
||||
advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.AlertCount != 0 {
|
||||
if err := (AlertService{}).validateCronJobAlertChannels(req.Type, advanced, req.AlertMethod); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
cronjob, _ := cronjobRepo.Get(repo.WithByName(req.Name))
|
||||
if cronjob.ID != 0 {
|
||||
return buserr.New("ErrRecordExist")
|
||||
@@ -600,12 +638,13 @@ func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
|
||||
}
|
||||
if req.AlertCount != 0 && req.AlertTitle != "" && req.AlertMethod != "" {
|
||||
createAlert := dto.AlertCreate{
|
||||
Title: req.AlertTitle,
|
||||
SendCount: req.AlertCount,
|
||||
Method: req.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronjob.ID)),
|
||||
Status: constant.AlertEnable,
|
||||
Title: req.AlertTitle,
|
||||
SendCount: req.AlertCount,
|
||||
Method: req.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronjob.ID)),
|
||||
Status: constant.AlertEnable,
|
||||
AdvancedParams: advanced,
|
||||
}
|
||||
err := NewIAlertService().CreateAlert(createAlert, operator)
|
||||
if err != nil {
|
||||
@@ -679,6 +718,10 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
|
||||
}
|
||||
|
||||
func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string) error {
|
||||
advanced, err := cronJobAlertAdvancedParams(req.AlertTriggerMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var cronjob model.Cronjob
|
||||
if err := copier.Copy(&cronjob, &req); err != nil {
|
||||
return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||
@@ -694,6 +737,20 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
|
||||
if err != nil {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
if req.AlertCount != 0 {
|
||||
previous, _ := alertRepo.Get(alertRepo.WithByType(cronModel.Type), alertRepo.WithByProject(strconv.Itoa(int(id))))
|
||||
merged, err := prepareCronJobAlertParams(cronModel.Type, previous.AdvancedParams, advanced)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
method := req.AlertMethod
|
||||
if method == "" {
|
||||
method = previous.Method
|
||||
}
|
||||
if err := (AlertService{}).validateCronJobAlertChannels(cronModel.Type, merged, method); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
upMap := make(map[string]interface{})
|
||||
cronjob.EntryIDs = cronModel.EntryIDs
|
||||
cronjob.Type = cronModel.Type
|
||||
@@ -750,11 +807,12 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
|
||||
return err
|
||||
}
|
||||
updateAlert := dto.AlertCreate{
|
||||
Title: req.AlertTitle,
|
||||
SendCount: req.AlertCount,
|
||||
Method: req.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronModel.ID)),
|
||||
Title: req.AlertTitle,
|
||||
SendCount: req.AlertCount,
|
||||
Method: req.AlertMethod,
|
||||
Type: cronjob.Type,
|
||||
Project: strconv.Itoa(int(cronModel.ID)),
|
||||
AdvancedParams: advanced,
|
||||
}
|
||||
err = NewIAlertService().ExternalUpdateAlert(updateAlert, operator)
|
||||
if err != nil {
|
||||
@@ -763,6 +821,17 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string
|
||||
return nil
|
||||
}
|
||||
|
||||
func cronJobAlertAdvancedParams(mode string) (string, error) {
|
||||
if mode == "" {
|
||||
return "", nil
|
||||
}
|
||||
data, err := json.Marshal(map[string]string{"alertTriggerMode": mode})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return alertUtil.MergeCronJobAlertParams("", string(data))
|
||||
}
|
||||
|
||||
func (u *CronjobService) UpdateStatus(id uint, status string) error {
|
||||
cronjob, _ := cronjobRepo.Get(repo.WithByID(id))
|
||||
if cronjob.ID == 0 {
|
||||
|
||||
@@ -163,6 +163,7 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
|
||||
record.Name = dbInfo.Database
|
||||
record.DetailName = dbInfo.Name
|
||||
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))
|
||||
switch dbInfo.DBType {
|
||||
@@ -411,6 +412,7 @@ func addSkipTask(source string, taskItem *task.Task) {
|
||||
taskItem.Log(i18n.GetMsgByKey("NoSuchResource"))
|
||||
return nil
|
||||
}, nil)
|
||||
taskItem.SubTasks[len(taskItem.SubTasks)-1].StepAlias = cronJobSkippedStep
|
||||
}
|
||||
|
||||
func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ntp"
|
||||
@@ -56,10 +58,11 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
|
||||
_ = taskRepo.Save(context.Background(), taskItem.Task)
|
||||
}
|
||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||
handleCronJobAlert(cronjob)
|
||||
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
|
||||
return
|
||||
}
|
||||
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
|
||||
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil))
|
||||
}()
|
||||
return
|
||||
}
|
||||
@@ -70,19 +73,20 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
|
||||
record.TaskID = ""
|
||||
}
|
||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||
handleCronJobAlert(cronjob)
|
||||
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
taskItem, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
|
||||
if len(taskItem.ID) == 0 {
|
||||
storedTask, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID))
|
||||
if len(storedTask.ID) == 0 {
|
||||
record.TaskID = ""
|
||||
}
|
||||
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
|
||||
handleCronJobAlert(cronjob)
|
||||
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, err))
|
||||
} else {
|
||||
cronjobRepo.EndRecords(record, constant.StatusSuccess, "", record.Records)
|
||||
handleCronJobAlert(cronjob, cronJobAlertResult(taskItem, nil))
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -482,8 +486,33 @@ func hasBackup(cronjobType string) bool {
|
||||
return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log" || cronjobType == "cutWebsiteLog"
|
||||
}
|
||||
|
||||
func handleCronJobAlert(cronjob *model.Cronjob) {
|
||||
const cronJobSkippedStep = "cronjob-skipped"
|
||||
|
||||
func cronJobAlertResult(taskItem *task.Task, err error) string {
|
||||
if errors.Is(err, context.Canceled) || taskItem.Task.Status == constant.StatusCanceled ||
|
||||
(taskItem.TaskCtx != nil && taskItem.TaskCtx.Err() != nil) {
|
||||
return ""
|
||||
}
|
||||
if err != nil {
|
||||
return alertUtil.CronJobAlertFailed
|
||||
}
|
||||
if taskItem.Task.Status != constant.StatusSuccess {
|
||||
return ""
|
||||
}
|
||||
for _, subTask := range taskItem.SubTasks {
|
||||
if subTask.StepAlias != cronJobSkippedStep {
|
||||
return alertUtil.CronJobAlertSuccess
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func handleCronJobAlert(cronjob *model.Cronjob, result string) {
|
||||
if result == "" {
|
||||
return
|
||||
}
|
||||
pushAlert := dto.PushAlert{
|
||||
Result: result,
|
||||
TaskName: cronjob.Name,
|
||||
AlertType: cronjob.Type,
|
||||
EntryID: cronjob.ID,
|
||||
|
||||
+168
-110
@@ -18,8 +18,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
||||
@@ -184,10 +183,30 @@ func (u *DashboardService) LoadBaseInfo(ioOption string, netOption string) (*dto
|
||||
|
||||
func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent {
|
||||
var currentInfo dto.DashboardCurrent
|
||||
hostInfo, _ := psutil.HOST.GetHostInfo(false)
|
||||
currentInfo.Uptime = hostInfo.Uptime
|
||||
currentInfo.TimeSinceUptime = time.Unix(int64(hostInfo.BootTime), 0).Format(constant.DateTimeLayout)
|
||||
currentInfo.RunningTime = loadRunningTime(hostInfo.Uptime)
|
||||
shotTime := time.Now()
|
||||
hostInfo, err := psutil.HOST.GetHostInfo(false)
|
||||
if err != nil {
|
||||
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.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false)
|
||||
|
||||
@@ -223,9 +242,8 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
|
||||
currentInfo.SwapMemoryUsed = swapInfo.Used
|
||||
currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent
|
||||
|
||||
currentInfo.DiskData = loadDiskInfo()
|
||||
currentInfo.GPUData = loadGPUInfo()
|
||||
currentInfo.XPUData = loadXpuInfo()
|
||||
currentInfo.DiskData = loadDiskInfo(false)
|
||||
currentInfo.GPUData, currentInfo.NPUData, currentInfo.XPUData = loadAcceleratorInfo()
|
||||
|
||||
if ioOption == "all" {
|
||||
diskInfo, _ := disk.IOCounters()
|
||||
@@ -263,7 +281,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
|
||||
}
|
||||
}
|
||||
|
||||
currentInfo.ShotTime = time.Now()
|
||||
currentInfo.ShotTime = shotTime
|
||||
return ¤tInfo
|
||||
}
|
||||
|
||||
@@ -437,24 +455,9 @@ type diskInfo struct {
|
||||
Device string
|
||||
}
|
||||
|
||||
func loadDiskInfo() []dto.DiskInfo {
|
||||
func loadDiskInfo(forceRefresh bool) []dto.DiskInfo {
|
||||
var datas []dto.DiskInfo
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||
format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
|
||||
stdout, err := cmdMgr.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||
stdout, err = cmdMgr2.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
return datas
|
||||
}
|
||||
}
|
||||
stdout := loadDiskMounts()
|
||||
lines := strings.Split(stdout, "\n")
|
||||
|
||||
var mounts []diskInfo
|
||||
@@ -502,43 +505,22 @@ func loadDiskInfo() []dto.DiskInfo {
|
||||
itemData.Type = mount.Type
|
||||
itemData.Device = mount.Device
|
||||
|
||||
type diskResult struct {
|
||||
state *disk.UsageStat
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan diskResult, 1)
|
||||
|
||||
go func() {
|
||||
state, err := psutil.DISK.GetUsage(mount.Mount, false)
|
||||
resultCh <- diskResult{state: state, err: err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount)
|
||||
case result := <-resultCh:
|
||||
if result.err != nil {
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, result.err)
|
||||
return
|
||||
}
|
||||
itemData.Total = result.state.Total
|
||||
itemData.Free = result.state.Free
|
||||
itemData.Used = result.state.Used
|
||||
itemData.UsedPercent = result.state.UsedPercent
|
||||
itemData.InodesTotal = result.state.InodesTotal
|
||||
itemData.InodesUsed = result.state.InodesUsed
|
||||
itemData.InodesFree = result.state.InodesFree
|
||||
itemData.InodesUsedPercent = result.state.InodesUsedPercent
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
state, err := loadDiskUsageWithTimeout(mount.Mount, forceRefresh)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err)
|
||||
} else {
|
||||
itemData.Total = state.Total
|
||||
itemData.Free = state.Free
|
||||
itemData.Used = state.Used
|
||||
itemData.UsedPercent = state.UsedPercent
|
||||
itemData.InodesTotal = state.InodesTotal
|
||||
itemData.InodesUsed = state.InodesUsed
|
||||
itemData.InodesFree = state.InodesFree
|
||||
itemData.InodesUsedPercent = state.InodesUsedPercent
|
||||
}
|
||||
mu.Lock()
|
||||
datas = append(datas, itemData)
|
||||
mu.Unlock()
|
||||
}(mounts[i])
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -549,32 +531,134 @@ func loadDiskInfo() []dto.DiskInfo {
|
||||
return datas
|
||||
}
|
||||
|
||||
func loadGPUInfo() []dto.GPUInfo {
|
||||
ok, client := gpu.New()
|
||||
var list []interface{}
|
||||
if ok {
|
||||
info, err := client.LoadGpuInfo()
|
||||
if err != nil || len(info.GPUs) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, item := range info.GPUs {
|
||||
list = append(list, item)
|
||||
}
|
||||
var diskMountsMu sync.Mutex
|
||||
|
||||
func loadDiskMounts() string {
|
||||
if !diskMountsMu.TryLock() {
|
||||
return ""
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
resultCh := make(chan string, 1)
|
||||
go func() {
|
||||
var stdout string
|
||||
defer func() {
|
||||
diskMountsMu.Unlock()
|
||||
resultCh <- stdout
|
||||
}()
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
|
||||
format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
|
||||
output, err := cmdMgr.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info with df -hT -P failed, err: %v", err)
|
||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||
output, err = cmdMgr2.RunPipe(
|
||||
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||
)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load disk info with df -lhT -P failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stdout = output
|
||||
}()
|
||||
timer := time.NewTimer(3 * time.Second)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case stdout := <-resultCh:
|
||||
return stdout
|
||||
case <-timer.C:
|
||||
global.LOG.Error("load disk mounts timed out; df collection is still running")
|
||||
return ""
|
||||
}
|
||||
var data []dto.GPUInfo
|
||||
for _, gpu := range list {
|
||||
var dataItem dto.GPUInfo
|
||||
if err := copier.Copy(&dataItem, &gpu); err != nil {
|
||||
}
|
||||
|
||||
func loadDiskUsageWithTimeout(path string, forceRefresh bool) (*disk.UsageStat, error) {
|
||||
type diskResult struct {
|
||||
state *disk.UsageStat
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan diskResult, 1)
|
||||
go func() {
|
||||
state, err := psutil.DISK.GetUsage(path, forceRefresh)
|
||||
resultCh <- diskResult{state: state, err: err}
|
||||
}()
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
return nil, fmt.Errorf("load disk usage from %s: timeout", path)
|
||||
case result := <-resultCh:
|
||||
return result.state, result.err
|
||||
}
|
||||
}
|
||||
|
||||
func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
||||
ok, client := accelerator.New()
|
||||
if !ok {
|
||||
return nil, nil, nil
|
||||
}
|
||||
snapshot, err := client.Collect(context.Background())
|
||||
if err != nil || len(snapshot.Devices) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
if warning := snapshot.Warning(); warning != nil {
|
||||
global.LOG.Warnf("load accelerator dashboard data partially failed, err: %v", warning)
|
||||
}
|
||||
|
||||
var (
|
||||
gpuData []dto.GPUInfo
|
||||
npuData []dto.NPUInfo
|
||||
xpuData []dto.XPUInfo
|
||||
)
|
||||
for _, device := range snapshot.Devices {
|
||||
if device.ParentID != "" {
|
||||
continue
|
||||
}
|
||||
dataItem.PowerUsage = dataItem.PowerDraw + " / " + dataItem.MaxPowerLimit
|
||||
dataItem.MemoryUsage = dataItem.MemUsed + " / " + dataItem.MemTotal
|
||||
data = append(data, dataItem)
|
||||
switch device.Kind {
|
||||
case accelerator.KindGPU:
|
||||
if device.GPU == nil {
|
||||
continue
|
||||
}
|
||||
var dataItem dto.GPUInfo
|
||||
if err := copier.Copy(&dataItem, device.GPU); err != nil {
|
||||
continue
|
||||
}
|
||||
dataItem.MaxPowerLimit = device.GPU.PowerLimit
|
||||
dataItem.PowerUsage = dataItem.PowerDraw
|
||||
if dataItem.MaxPowerLimit != "" {
|
||||
dataItem.PowerUsage += " / " + dataItem.MaxPowerLimit
|
||||
}
|
||||
dataItem.MemoryUsage = dataItem.MemUsed + " / " + dataItem.MemTotal
|
||||
gpuData = append(gpuData, dataItem)
|
||||
case accelerator.KindNPU:
|
||||
if device.NPU == nil {
|
||||
continue
|
||||
}
|
||||
var dataItem dto.NPUInfo
|
||||
if err := copier.Copy(&dataItem, device.NPU); err != nil {
|
||||
continue
|
||||
}
|
||||
npuData = append(npuData, dataItem)
|
||||
case accelerator.KindXPU:
|
||||
if device.XPU == nil {
|
||||
continue
|
||||
}
|
||||
xpuData = append(xpuData, dto.XPUInfo{
|
||||
DeviceID: device.Index,
|
||||
DeviceName: device.Name,
|
||||
PciBdfAddress: device.BusID,
|
||||
Memory: device.XPU.Basic.Memory,
|
||||
Temperature: device.Metrics.Temperature.Display,
|
||||
GPUUtil: device.Metrics.Utilization.Display,
|
||||
MemoryUsed: device.Metrics.MemoryUsed.Display,
|
||||
Power: device.Metrics.Power.Display,
|
||||
MemoryUtil: device.Metrics.MemoryUtil.Display,
|
||||
})
|
||||
}
|
||||
}
|
||||
return data
|
||||
return gpuData, npuData, xpuData
|
||||
}
|
||||
|
||||
type AppLauncher struct {
|
||||
@@ -590,32 +674,6 @@ func ArryContains(arr []string, element string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func loadXpuInfo() []dto.XPUInfo {
|
||||
var list []interface{}
|
||||
ok, xpuClient := xpu.New()
|
||||
if ok {
|
||||
xpus, err := xpuClient.LoadDashData()
|
||||
if err != nil || len(xpus) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, item := range xpus {
|
||||
list = append(list, item)
|
||||
}
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
var data []dto.XPUInfo
|
||||
for _, gpu := range list {
|
||||
var dataItem dto.XPUInfo
|
||||
if err := copier.Copy(&dataItem, &gpu); err != nil {
|
||||
continue
|
||||
}
|
||||
data = append(data, dataItem)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func loadOutboundIP() string {
|
||||
conn, err := network.Dial("udp", "8.8.8.8:80")
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
@@ -17,12 +23,15 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
type RedisService struct{}
|
||||
|
||||
// The CLI container is shared by all remote Redis databases on this node.
|
||||
var redisCliInstallMutex sync.Mutex
|
||||
|
||||
type IRedisService interface {
|
||||
UpdateConf(req dto.RedisConfUpdate) error
|
||||
UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
|
||||
@@ -33,7 +42,7 @@ type IRedisService interface {
|
||||
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
|
||||
|
||||
CheckHasCli() bool
|
||||
InstallCli() error
|
||||
InstallCli(req dto.RedisCliInstall) (string, error)
|
||||
}
|
||||
|
||||
func NewIRedisService() IRedisService {
|
||||
@@ -61,30 +70,65 @@ func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error {
|
||||
}
|
||||
|
||||
func (u *RedisService) CheckHasCli() bool {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer client.Close()
|
||||
containerLists, err := client.ContainerList(context.Background(), container.ListOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, item := range containerLists {
|
||||
if strings.ReplaceAll(item.Names[0], "/", "") == "1Panel-redis-cli-tools" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
installed, _ := u.checkCliInstalled()
|
||||
return installed
|
||||
}
|
||||
|
||||
func (u *RedisService) InstallCli() error {
|
||||
func (u *RedisService) checkCliInstalled() (bool, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer client.Close()
|
||||
info, err := client.ContainerInspect(context.Background(), "1Panel-redis-cli-tools")
|
||||
if dockerclient.IsErrNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return info.ContainerJSONBase != nil && info.State != nil && info.State.Running, nil
|
||||
}
|
||||
|
||||
func (u *RedisService) InstallCli(req dto.RedisCliInstall) (string, error) {
|
||||
if !redisCliInstallMutex.TryLock() {
|
||||
return "", buserr.New("TaskIsExecuting")
|
||||
}
|
||||
defer redisCliInstallMutex.Unlock()
|
||||
item := dto.ContainerOperate{
|
||||
TaskID: req.TaskID,
|
||||
Name: "1Panel-redis-cli-tools",
|
||||
Image: "redis:7.4.4",
|
||||
Networks: []dto.ContainerNetwork{{Network: "1panel-network"}},
|
||||
}
|
||||
return NewIContainerService().ContainerCreate(item, false)
|
||||
running, err := taskRepo.GetFirst(
|
||||
repo.WithByName(task.GetTaskName(item.Name, task.TaskCreate, task.TaskScopeContainer)),
|
||||
repo.WithByType(task.TaskScopeContainer),
|
||||
taskRepo.WithByStatus(constant.StatusExecuting),
|
||||
)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", err
|
||||
}
|
||||
if running.ID != "" {
|
||||
return running.ID, nil
|
||||
}
|
||||
installed, err := u.checkCliInstalled()
|
||||
if err != nil || installed {
|
||||
return "", err
|
||||
}
|
||||
if item.TaskID == "" {
|
||||
item.TaskID = uuid.NewString()
|
||||
}
|
||||
if _, err := taskRepo.GetFirst(taskRepo.WithByID(item.TaskID)); !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "", buserr.New("TaskIsExecuting")
|
||||
}
|
||||
if err := NewIContainerService().ContainerCreate(item, true); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return item.TaskID, nil
|
||||
}
|
||||
|
||||
func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
|
||||
|
||||
@@ -73,7 +73,7 @@ func (u *DeviceService) LoadBaseInfo() (dto.DeviceBaseInfo, error) {
|
||||
if baseInfo.SwapMemoryTotal != 0 {
|
||||
baseInfo.SwapDetails = loadSwap()
|
||||
}
|
||||
disks := loadDiskInfo()
|
||||
disks := loadDiskInfo(false)
|
||||
for _, item := range disks {
|
||||
baseInfo.MaxSize += item.Free
|
||||
}
|
||||
|
||||
@@ -28,10 +28,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
rollbackPath = "1panel/tmp"
|
||||
upgradePath = "1panel/tmp/upgrade"
|
||||
uploadPath = "1panel/uploads"
|
||||
downloadPath = "1panel/download"
|
||||
rollbackPath = "1panel/tmp"
|
||||
communityRestorePath = "1panel/tmp/community-restore"
|
||||
upgradePath = "1panel/tmp/upgrade"
|
||||
uploadPath = "1panel/uploads"
|
||||
downloadPath = "1panel/download"
|
||||
)
|
||||
|
||||
func (u *DeviceService) Scan() dto.CleanData {
|
||||
@@ -58,7 +59,7 @@ func (u *DeviceService) Scan() dto.CleanData {
|
||||
|
||||
SystemClean.BackupClean = loadBackupTree(fileOp)
|
||||
|
||||
rollBackTree := loadRollBackTree(fileOp)
|
||||
rollBackTree := loadRollBackTree()
|
||||
rollbackSize := uint64(0)
|
||||
for _, rollback := range rollBackTree {
|
||||
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, "database"))
|
||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website"))
|
||||
dropFileOrDir(path.Join(global.Dir.BaseDir, communityRestorePath))
|
||||
case "rollback_app":
|
||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app", item.Name))
|
||||
case "rollback_database":
|
||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database", item.Name))
|
||||
case "rollback_website":
|
||||
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":
|
||||
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, "website"), 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)
|
||||
oldUpgradeFiles, _ := os.ReadDir(upgrades)
|
||||
@@ -606,20 +611,21 @@ func isExactPathMatch(path string, excludePaths []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func loadRollBackTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||
func loadRollBackTree() []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, "website"), "rollback_website", fileOp)
|
||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "database"), "rollback_database", 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")
|
||||
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
|
||||
}
|
||||
|
||||
func loadUploadTree(fileOp fileUtils.FileOp) []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, "website"), "upload_website", fileOp)
|
||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "database"), "upload_database", 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")
|
||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "database"), "upload_database")
|
||||
|
||||
path5 := path.Join(global.Dir.BaseDir, uploadPath)
|
||||
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 {
|
||||
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, "website"), "download_website", fileOp)
|
||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "database"), "download_database", 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")
|
||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "database"), "download_database")
|
||||
|
||||
path5 := path.Join(global.Dir.BaseDir, downloadPath)
|
||||
uploadTreeData := loadTreeWithAllFile(true, path5, "download", path5, fileOp)
|
||||
@@ -814,16 +820,59 @@ func loadContainerTree() []dto.CleanTree {
|
||||
return treeData
|
||||
}
|
||||
|
||||
func loadTreeWithCheck(treeData []dto.CleanTree, pathItem, treeType string, fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||
size, _ := fileOp.GetDirSize(pathItem)
|
||||
if size == 0 {
|
||||
func loadTreeWithCheck(treeData []dto.CleanTree, pathItem, treeType string) []dto.CleanTree {
|
||||
list, size := loadTreeWithFileSize(true, pathItem, treeType, pathItem)
|
||||
if len(list) == 0 || size == 0 {
|
||||
return treeData
|
||||
}
|
||||
list := loadTreeWithAllFile(true, pathItem, treeType, pathItem, fileOp)
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: treeType, Size: uint64(size), IsCheck: size > 0, Children: list, Type: treeType, IsRecommend: true, CanDelete: false})
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: treeType, Size: size, IsCheck: size > 0, Children: list, Type: treeType, IsRecommend: true, CanDelete: false})
|
||||
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 {
|
||||
var lists []dto.CleanTree
|
||||
files, err := os.ReadDir(pathItem)
|
||||
|
||||
@@ -61,18 +61,21 @@ func parseDevice(dev LsblkDevice) []response.DiskBasicInfo {
|
||||
|
||||
var used, avail, totalSize string
|
||||
var usePercent int
|
||||
isMounted := mountPoint != ""
|
||||
isMounted := mountPoint != "" && mountPoint != "-"
|
||||
isSystem := false
|
||||
|
||||
if dev.Fstype == "LVM2_member" && len(dev.Children) > 0 {
|
||||
for _, child := range dev.Children {
|
||||
if child.Type == "lvm" && child.Mountpoint != "" {
|
||||
devicePath := "/dev/mapper/" + child.Name
|
||||
totalSize, used, avail, usePercent, _ := getDiskUsageInfo(devicePath)
|
||||
if child.Type == "lvm" && child.Mountpoint != "" && child.Mountpoint != "-" {
|
||||
totalSize, used, avail, usePercent, _ := getDiskUsageInfo(child.Mountpoint)
|
||||
childSize := child.Size
|
||||
if totalSize != "" {
|
||||
childSize = totalSize
|
||||
}
|
||||
|
||||
childInfo := response.DiskBasicInfo{
|
||||
Device: dev.Name,
|
||||
Size: totalSize,
|
||||
Size: childSize,
|
||||
Model: dev.Model,
|
||||
DiskType: diskType,
|
||||
Filesystem: child.Fstype,
|
||||
@@ -91,8 +94,7 @@ func parseDevice(dev LsblkDevice) []response.DiskBasicInfo {
|
||||
return list
|
||||
} else if isMounted {
|
||||
isSystem = isSystemDisk(mountPoint)
|
||||
devicePath := "/dev/" + dev.Name
|
||||
totalSize, used, avail, usePercent, _ = getDiskUsageInfo(devicePath)
|
||||
totalSize, used, avail, usePercent, _ = getDiskUsageInfo(mountPoint)
|
||||
if totalSize != "" {
|
||||
size = totalSize
|
||||
}
|
||||
@@ -229,9 +231,14 @@ func parseLsblkOutput(output string) ([]response.DiskBasicInfo, error) {
|
||||
size := fields["SIZE"]
|
||||
|
||||
if diskType == "lvm" {
|
||||
total, used, avail, usePercent, _ := getDiskUsageInfo("/dev/mapper/" + name)
|
||||
if total != "" && fsType != "" {
|
||||
size = total
|
||||
var total, used, avail string
|
||||
var usePercent int
|
||||
isMounted := mountPoint != "" && mountPoint != "-"
|
||||
if isMounted {
|
||||
total, used, avail, usePercent, _ = getDiskUsageInfo(mountPoint)
|
||||
if total != "" && fsType != "" {
|
||||
size = total
|
||||
}
|
||||
}
|
||||
|
||||
lvmInfo := response.DiskBasicInfo{
|
||||
@@ -246,7 +253,7 @@ func parseLsblkOutput(output string) ([]response.DiskBasicInfo, error) {
|
||||
Avail: avail,
|
||||
UsePercent: usePercent,
|
||||
MountPoint: mountPoint,
|
||||
IsMounted: mountPoint != "" && mountPoint != "-",
|
||||
IsMounted: isMounted,
|
||||
Serial: fields["SERIAL"],
|
||||
}
|
||||
lvmMap[name] = lvmInfo
|
||||
@@ -269,8 +276,8 @@ func parseLsblkOutput(output string) ([]response.DiskBasicInfo, error) {
|
||||
used, avail, totalSize string
|
||||
usePercent int
|
||||
)
|
||||
if mountPoint != "" {
|
||||
totalSize, used, avail, usePercent, _ = getDiskUsageInfo("/dev/" + name)
|
||||
if mountPoint != "" && mountPoint != "-" {
|
||||
totalSize, used, avail, usePercent, _ = getDiskUsageInfo(mountPoint)
|
||||
if totalSize != "" {
|
||||
size = totalSize
|
||||
}
|
||||
@@ -387,26 +394,39 @@ func getParentDevice(device string) string {
|
||||
return device
|
||||
}
|
||||
|
||||
func getDiskUsageInfo(device string) (size, used, avail string, usePercent int, err error) {
|
||||
output, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("df", "-h", device)
|
||||
func getDiskUsageInfo(mountPoint string) (size, used, avail string, usePercent int, err error) {
|
||||
// Query by mount point instead of a reconstructed device path. The mount table may record
|
||||
// a different device alias such as /dev/root.
|
||||
output, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("df", "-h", "-P", mountPoint)
|
||||
if err != nil {
|
||||
return "", "", "", 0, nil
|
||||
}
|
||||
return parseDiskUsageOutput(output)
|
||||
}
|
||||
|
||||
func parseDiskUsageOutput(output string) (size, used, avail string, usePercent int, err error) {
|
||||
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||
if len(lines) > 1 {
|
||||
output = lines[len(lines)-1]
|
||||
for i := len(lines) - 1; i >= 0; i-- {
|
||||
fields := strings.Fields(lines[i])
|
||||
for index, field := range fields {
|
||||
if index < 3 || !strings.HasSuffix(field, "%") {
|
||||
continue
|
||||
}
|
||||
percent, parseErr := strconv.Atoi(strings.TrimSuffix(field, "%"))
|
||||
if parseErr != nil {
|
||||
continue
|
||||
}
|
||||
return fields[index-3], fields[index-2], fields[index-1], percent, nil
|
||||
}
|
||||
for index, field := range fields {
|
||||
if index < 3 || index+1 >= len(fields) || field != "-" || !strings.HasPrefix(fields[index+1], "/") {
|
||||
continue
|
||||
}
|
||||
return fields[index-3], fields[index-2], fields[index-1], 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
fields := strings.Fields(output)
|
||||
if len(fields) >= 5 {
|
||||
size = fields[1]
|
||||
used = fields[2]
|
||||
avail = fields[3]
|
||||
usePercentStr := strings.TrimSuffix(fields[4], "%")
|
||||
usePercent, _ = strconv.Atoi(usePercentStr)
|
||||
}
|
||||
|
||||
return size, used, avail, usePercent, nil
|
||||
return "", "", "", 0, nil
|
||||
}
|
||||
|
||||
func formatDisk(req dto.DiskFormatRequest) error {
|
||||
|
||||
+123
-1
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -14,14 +15,20 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
)
|
||||
|
||||
const dockerNftablesMinVersion = "29.0.0"
|
||||
|
||||
type DockerService struct{}
|
||||
|
||||
type IDockerService interface {
|
||||
UpdateConf(req dto.SettingUpdate, withRestart bool) error
|
||||
UpdateFirewallBackend(backend string) error
|
||||
UpdateLogOption(req dto.LogOption) error
|
||||
UpdateIpv6Option(req dto.Ipv6Option) error
|
||||
UpdateConfByFile(info dto.DaemonJsonUpdateByFile) error
|
||||
@@ -30,6 +37,115 @@ type IDockerService interface {
|
||||
OperateDocker(req dto.DockerOperation) error
|
||||
}
|
||||
|
||||
func loadDockerEngineVersion(ctx context.Context) string {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err == nil {
|
||||
defer client.Close()
|
||||
if version, versionErr := client.ServerVersion(ctx); versionErr == nil && version.Version != "" {
|
||||
return version.Version
|
||||
}
|
||||
}
|
||||
if !cmd.Which("dockerd") {
|
||||
return ""
|
||||
}
|
||||
stdout, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("dockerd", "--version")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(stdout)
|
||||
}
|
||||
|
||||
func dockerNftablesSupported(version string) bool {
|
||||
return version != "" && common.CompareAppVersion(version, dockerNftablesMinVersion)
|
||||
}
|
||||
|
||||
func applyDockerFirewallBackendConfig(daemonMap map[string]interface{}, backend, version string) error {
|
||||
switch backend {
|
||||
case constant.FirewallProviderNftables:
|
||||
if !dockerNftablesSupported(version) {
|
||||
return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion)
|
||||
}
|
||||
daemonMap["experimental"] = true
|
||||
daemonMap["firewall-backend"] = constant.FirewallProviderNftables
|
||||
case constant.FirewallProviderIptables:
|
||||
if dockerNftablesSupported(version) {
|
||||
daemonMap["firewall-backend"] = constant.FirewallProviderIptables
|
||||
} else {
|
||||
delete(daemonMap, "firewall-backend")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported Docker firewall backend %q", backend)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *DockerService) UpdateFirewallBackend(backend string) error {
|
||||
version := loadDockerEngineVersion(context.Background())
|
||||
if backend == constant.FirewallProviderNftables && !dockerNftablesSupported(version) {
|
||||
return fmt.Errorf("Docker Engine %s or later is required for the nftables firewall backend", dockerNftablesMinVersion)
|
||||
}
|
||||
if backend == constant.FirewallProviderNftables {
|
||||
if err := dockerfirewall.CheckIPv4Forwarding(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
original, readErr := os.ReadFile(constant.DaemonJsonPath)
|
||||
existed := readErr == nil
|
||||
if readErr != nil && !os.IsNotExist(readErr) {
|
||||
return readErr
|
||||
}
|
||||
daemonMap := make(map[string]interface{})
|
||||
if len(bytes.TrimSpace(original)) > 0 {
|
||||
if err := json.Unmarshal(original, &daemonMap); err != nil {
|
||||
return fmt.Errorf("failed to parse Docker configuration: %w", err)
|
||||
}
|
||||
}
|
||||
if err := applyDockerFirewallBackendConfig(daemonMap, backend, version); err != nil {
|
||||
return err
|
||||
}
|
||||
updated, err := json.MarshalIndent(daemonMap, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existed && bytes.Equal(bytes.TrimSpace(original), bytes.TrimSpace(updated)) {
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(path.Dir(constant.DaemonJsonPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(constant.DaemonJsonPath, updated, 0640); err != nil {
|
||||
return err
|
||||
}
|
||||
restore := func() error {
|
||||
if existed {
|
||||
return os.WriteFile(constant.DaemonJsonPath, original, 0640)
|
||||
}
|
||||
err := os.Remove(constant.DaemonJsonPath)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
if restoreErr := restore(); restoreErr != nil {
|
||||
return fmt.Errorf("%v; failed to restore Docker configuration: %w", err, restoreErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := controller.HandleRestart("docker"); err != nil {
|
||||
cause := fmt.Errorf("failed to restart Docker: %w", err)
|
||||
if restoreErr := restore(); restoreErr != nil {
|
||||
return fmt.Errorf("%v; failed to restore Docker configuration: %w", cause, restoreErr)
|
||||
}
|
||||
if restoreRestartErr := controller.HandleRestart("docker"); restoreRestartErr != nil {
|
||||
return fmt.Errorf("%v; the previous configuration was restored but Docker could not be restarted: %w", cause, restoreRestartErr)
|
||||
}
|
||||
return cause
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewIDockerService() IDockerService {
|
||||
return &DockerService{}
|
||||
}
|
||||
@@ -167,7 +283,10 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate, withRestart bool) erro
|
||||
delete(daemonMap, "ipv6")
|
||||
delete(daemonMap, "fixed-cidr-v6")
|
||||
delete(daemonMap, "ip6tables")
|
||||
delete(daemonMap, "experimental")
|
||||
backend, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||
if !strings.EqualFold(strings.TrimSpace(backend), constant.FirewallProviderNftables) {
|
||||
delete(daemonMap, "experimental")
|
||||
}
|
||||
}
|
||||
case "LogOption":
|
||||
if req.Value == "disable" {
|
||||
@@ -368,6 +487,9 @@ func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
|
||||
if err := controller.Handle(req.Operation, service); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Operation == "start" || req.Operation == "restart" {
|
||||
return RestoreDockerPortGuard(context.Background())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+13
-10
@@ -32,20 +32,23 @@ var (
|
||||
scriptRepo = repo.NewIScriptRepo()
|
||||
cronjobRepo = repo.NewICronjobRepo()
|
||||
|
||||
hostRepo = repo.NewIHostRepo()
|
||||
ftpRepo = repo.NewIFtpRepo()
|
||||
clamRepo = repo.NewIClamRepo()
|
||||
monitorRepo = repo.NewIMonitorRepo()
|
||||
hostRepo = repo.NewIHostRepo()
|
||||
ftpRepo = repo.NewIFtpRepo()
|
||||
clamRepo = repo.NewIClamRepo()
|
||||
monitorRepo = repo.NewIMonitorRepo()
|
||||
vllmMonitorRepo = &repo.VLLMMonitorRepo{}
|
||||
|
||||
settingRepo = repo.NewISettingRepo()
|
||||
backupRepo = repo.NewIBackupRepo()
|
||||
|
||||
websiteRepo = repo.NewIWebsiteRepo()
|
||||
websiteDomainRepo = repo.NewIWebsiteDomainRepo()
|
||||
websiteDnsRepo = repo.NewIWebsiteDnsAccountRepo()
|
||||
websiteSSLRepo = repo.NewISSLRepo()
|
||||
websiteAcmeRepo = repo.NewIAcmeAccountRepo()
|
||||
websiteCARepo = repo.NewIWebsiteCARepo()
|
||||
websiteRepo = repo.NewIWebsiteRepo()
|
||||
websiteDomainRepo = repo.NewIWebsiteDomainRepo()
|
||||
websiteDnsRepo = repo.NewIWebsiteDnsAccountRepo()
|
||||
websiteSSLRepo = repo.NewISSLRepo()
|
||||
websiteAcmeRepo = repo.NewIAcmeAccountRepo()
|
||||
websiteCARepo = repo.NewIWebsiteCARepo()
|
||||
websiteTemplateRepo = repo.NewIWebsiteTemplateRepo()
|
||||
websiteTemplateOutputRepo = repo.NewIWebsiteTemplateOutputRepo()
|
||||
|
||||
snapshotRepo = repo.NewISnapshotRepo()
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/toolbox"
|
||||
)
|
||||
|
||||
@@ -109,7 +108,7 @@ func (u *Fail2BanService) UpdateConf(req dto.Fail2BanUpdate) error {
|
||||
if req.Value == "firewallcmd-ipset" {
|
||||
itemName = "firewalld"
|
||||
}
|
||||
client, err := firewall.NewFirewallClient()
|
||||
client, err := NewSelectedSystemFirewallClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+150
-28
@@ -16,6 +16,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -51,6 +52,8 @@ type FileService struct {
|
||||
|
||||
const fileHistorySnapshotMaxSize = 10 * 1024 * 1024
|
||||
|
||||
var fileTransferLocks = newFileTransferLocks()
|
||||
|
||||
type IFileService interface {
|
||||
GetFileList(op request.FileOption) (response.FileInfo, error)
|
||||
SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error)
|
||||
@@ -71,6 +74,7 @@ type IFileService interface {
|
||||
ChangeName(req request.FileRename) error
|
||||
Wget(w request.FileWget) (string, error)
|
||||
MvFile(m request.FileMove) error
|
||||
StopMvFile(taskID string) error
|
||||
ChangeOwner(req request.FileRoleUpdate) error
|
||||
ChangeMode(op request.FileCreate) error
|
||||
BatchChangeModeAndOwner(op request.FileRoleReq) error
|
||||
@@ -155,6 +159,10 @@ func (f *FileService) SearchUploadWithPage(req request.SearchUploadWithPage) (in
|
||||
})
|
||||
}
|
||||
|
||||
sort.SliceStable(files, func(i, j int) bool {
|
||||
return files[i].CreatedAt > files[j].CreatedAt
|
||||
})
|
||||
|
||||
total, start, end := len(files), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||
if start > total {
|
||||
backData = make([]response.UploadInfo, 0)
|
||||
@@ -431,13 +439,15 @@ func (f *FileService) Compress(c request.FileCompress) error {
|
||||
if err := preflightCompressTool(files.CompressType(c.Type)); err != nil {
|
||||
return err
|
||||
}
|
||||
taskItem, err := task.NewTask(c.Name, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||
taskName := i18n.GetMsgWithMap("FileTaskCompress", map[string]interface{}{"dst": strconv.Quote(filepath.Join(c.Dst, c.Name))})
|
||||
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
taskItem.AddSubTask(c.Name, func(t *task.Task) error {
|
||||
t.LogStart(c.Name)
|
||||
taskItem.AddSubTask(taskName, func(t *task.Task) error {
|
||||
logFileTaskSources(t, c.Files)
|
||||
t.Log(i18n.GetMsgWithMap("FileTaskFormat", map[string]interface{}{"format": strconv.Quote(c.Type)}))
|
||||
compressType := files.CompressType(c.Type)
|
||||
dstFile := filepath.Join(c.Dst, c.Name)
|
||||
success := false
|
||||
@@ -466,7 +476,7 @@ func (f *FileService) Compress(c request.FileCompress) error {
|
||||
|
||||
func preflightCompressTool(compressType files.CompressType) error {
|
||||
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)
|
||||
return err
|
||||
default:
|
||||
@@ -476,7 +486,7 @@ func preflightCompressTool(compressType files.CompressType) error {
|
||||
|
||||
func preflightDecompressTool(decompressType files.CompressType) error {
|
||||
switch decompressType {
|
||||
case files.Rar, files.X7z:
|
||||
case files.Rar:
|
||||
_, err := files.NewExtractShellArchiver(decompressType)
|
||||
return err
|
||||
default:
|
||||
@@ -508,13 +518,15 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
|
||||
if err := preflightDecompressTool(files.CompressType(c.Type)); err != nil {
|
||||
return err
|
||||
}
|
||||
taskItem, err := task.NewTask(c.Path, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||
taskName := i18n.GetMsgWithMap("FileTaskDecompress", map[string]interface{}{"dst": strconv.Quote(c.Dst)})
|
||||
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
taskItem.AddSubTask(c.Path, func(t *task.Task) error {
|
||||
t.LogStart(c.Path)
|
||||
taskItem.AddSubTask(taskName, func(t *task.Task) error {
|
||||
logFileTaskSources(t, []string{c.Path})
|
||||
t.Log(i18n.GetMsgWithMap("FileTaskFormat", map[string]interface{}{"format": strconv.Quote(c.Type)}))
|
||||
dstExisted := fo.Stat(c.Dst)
|
||||
parentDir := filepath.Dir(c.Dst)
|
||||
if !fo.Stat(parentDir) {
|
||||
@@ -533,7 +545,10 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
|
||||
_ = 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
|
||||
}
|
||||
if err := fo.CreateDir(c.Dst, constant.DirPerm); err != nil {
|
||||
@@ -551,19 +566,42 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
|
||||
}
|
||||
|
||||
func copyDecompressTree(ctx context.Context, srcDir, dstDir string) error {
|
||||
state := decompressCopyState{hardlinks: make(map[decompressFileIdentity]string)}
|
||||
entries, err := os.ReadDir(srcDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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 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) {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -605,13 +643,16 @@ func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr e
|
||||
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(dstPath, info.Mode().Perm()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
entries, err := os.ReadDir(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -634,6 +675,15 @@ func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr e
|
||||
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -658,6 +708,12 @@ func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr e
|
||||
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())
|
||||
}
|
||||
@@ -667,7 +723,7 @@ func applyDecompressOwnership(srcPath, dstPath string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stat, ok := info.Sys().(*unix.Stat_t)
|
||||
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -844,6 +900,7 @@ func (f *FileService) Wget(w request.FileWget) (string, error) {
|
||||
key := "file-wget-" + common.GetUuid()
|
||||
options := files.DownloadOptions{
|
||||
IgnoreCertificate: w.IgnoreCertificate,
|
||||
UseServerFilename: w.UseServerFilename,
|
||||
}
|
||||
if w.UseProxy {
|
||||
systemProxy, err := NewISettingService().GetSystemProxy()
|
||||
@@ -861,19 +918,79 @@ func (f *FileService) Wget(w request.FileWget) (string, error) {
|
||||
return key, fo.DownloadFileWithProcess(w.Url, filepath.Join(w.Path, w.Name), key, options)
|
||||
}
|
||||
|
||||
func logFileTaskSources(t *task.Task, sources []string) {
|
||||
for _, source := range sources {
|
||||
t.Log(i18n.GetMsgWithMap("FileTaskSource", map[string]interface{}{"path": strconv.Quote(source)}))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileService) MvFile(m request.FileMove) error {
|
||||
fo := files.NewFileOp()
|
||||
if err := validateFileMove(fo, m); err != nil {
|
||||
return err
|
||||
}
|
||||
if m.TaskID == "" {
|
||||
m.TaskID = common.GetUuid()
|
||||
}
|
||||
if !fileTransferLocks.Acquire(m.TaskID, getFileTransferPaths(m)) {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
nameKey := "FileTaskCopy"
|
||||
if m.Type == "cut" {
|
||||
nameKey = "FileTaskMove"
|
||||
}
|
||||
taskName := i18n.GetMsgWithMap(nameKey, map[string]interface{}{"dst": strconv.Quote(m.NewPath)})
|
||||
taskItem, err := task.NewTask(taskName, task.TaskExec, task.TaskScopeTask, m.TaskID, 1)
|
||||
if err != nil {
|
||||
fileTransferLocks.Release(m.TaskID)
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
defer fileTransferLocks.Release(m.TaskID)
|
||||
taskItem.AddSubTaskWithOps(taskName, func(t *task.Task) error {
|
||||
logFileTaskSources(t, m.OldPaths)
|
||||
logFileTaskSources(t, m.CoverPaths)
|
||||
if m.Name != "" {
|
||||
t.Log(i18n.GetMsgWithMap("FileTaskRename", map[string]interface{}{"name": strconv.Quote(m.Name)}))
|
||||
}
|
||||
err := f.moveFileWithContext(t.TaskCtx, m)
|
||||
if err != nil && t.TaskCtx.Err() != nil {
|
||||
return t.TaskCtx.Err()
|
||||
}
|
||||
return err
|
||||
}, nil, 0, 0)
|
||||
_ = taskItem.Execute()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileService) StopMvFile(taskID string) error {
|
||||
if cancel, ok := global.LoadTaskCancel(taskID); ok {
|
||||
cancel()
|
||||
return nil
|
||||
}
|
||||
return buserr.New("TaskNotFound")
|
||||
}
|
||||
|
||||
func validateFileMove(fo files.FileOp, m request.FileMove) error {
|
||||
if !fo.Stat(m.NewPath) {
|
||||
return buserr.New("ErrPathNotFound")
|
||||
}
|
||||
for _, oldPath := range m.OldPaths {
|
||||
for _, oldPath := range append(append([]string{}, m.OldPaths...), m.CoverPaths...) {
|
||||
if !fo.Stat(oldPath) {
|
||||
return buserr.WithName("ErrFileNotFound", oldPath)
|
||||
}
|
||||
if oldPath == m.NewPath || strings.Contains(m.NewPath, filepath.Clean(oldPath)+"/") {
|
||||
oldPath = filepath.Clean(oldPath)
|
||||
newPath := filepath.Clean(m.NewPath)
|
||||
if oldPath == newPath || strings.HasPrefix(newPath, oldPath+string(filepath.Separator)) {
|
||||
return buserr.New("ErrMovePathFailed")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileService) moveFileWithContext(ctx context.Context, m request.FileMove) error {
|
||||
fo := files.NewFileOp()
|
||||
type moveSnapshot struct {
|
||||
path string
|
||||
content []byte
|
||||
@@ -889,13 +1006,25 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
||||
}
|
||||
if len(m.CoverPaths) > 0 {
|
||||
for _, src := range m.CoverPaths {
|
||||
if err := fo.CopyAndReName(src, m.NewPath, "", true); err != nil {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fo.CopyAndReNameWithContext(ctx, src, m.NewPath, "", true); err != nil {
|
||||
errs = append(errs, err)
|
||||
global.LOG.Errorf("cut copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
|
||||
continue
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fo.DeleteDir(src); err != nil {
|
||||
removeErr := fmt.Errorf("remove merged source [%s] failed: %w", src, err)
|
||||
errs = append(errs, removeErr)
|
||||
global.LOG.Errorf("%s", removeErr.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := fo.Cut(m.OldPaths, m.NewPath, m.Name, m.Cover); err != nil {
|
||||
if err := fo.CutWithContext(ctx, m.OldPaths, m.NewPath, m.Name, m.Cover); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, snapshot := range snapshots {
|
||||
@@ -906,18 +1035,18 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return aggregateFileMoveErrors(errs)
|
||||
}
|
||||
if m.Type == "copy" {
|
||||
for _, src := range m.OldPaths {
|
||||
if err := fo.CopyAndReName(src, m.NewPath, m.Name, m.Cover); err != nil {
|
||||
if err := fo.CopyAndReNameWithContext(ctx, src, m.NewPath, m.Name, m.Cover); err != nil {
|
||||
errs = append(errs, err)
|
||||
global.LOG.Errorf("copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
|
||||
}
|
||||
}
|
||||
if len(m.CoverPaths) > 0 {
|
||||
for _, src := range m.CoverPaths {
|
||||
if err := fo.CopyAndReName(src, m.NewPath, "", true); err != nil {
|
||||
if err := fo.CopyAndReNameWithContext(ctx, src, m.NewPath, "", true); err != nil {
|
||||
errs = append(errs, err)
|
||||
global.LOG.Errorf("copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
|
||||
}
|
||||
@@ -925,14 +1054,7 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
||||
}
|
||||
}
|
||||
|
||||
var errString string
|
||||
for _, err := range errs {
|
||||
errString += err.Error() + "\n"
|
||||
}
|
||||
if errString != "" {
|
||||
return errors.New(errString)
|
||||
}
|
||||
return nil
|
||||
return aggregateFileMoveErrors(errs)
|
||||
}
|
||||
|
||||
func readEditableFileHistoryContent(filePath string) ([]byte, os.FileMode, bool) {
|
||||
@@ -1188,7 +1310,7 @@ func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.Exi
|
||||
}
|
||||
|
||||
func (f *FileService) GetHostMount() []dto.DiskInfo {
|
||||
return loadDiskInfo()
|
||||
return loadDiskInfo(false)
|
||||
}
|
||||
|
||||
func (f *FileService) GetUsersAndGroups() (*response.UserGroupResponse, error) {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
)
|
||||
|
||||
type fileTransferLockSet struct {
|
||||
mu sync.Mutex
|
||||
paths map[string][]string
|
||||
}
|
||||
|
||||
func newFileTransferLocks() *fileTransferLockSet {
|
||||
return &fileTransferLockSet{paths: make(map[string][]string)}
|
||||
}
|
||||
|
||||
func (s *fileTransferLockSet) Acquire(taskID string, transferPaths []string) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for _, activePaths := range s.paths {
|
||||
for _, activePath := range activePaths {
|
||||
for _, transferPath := range transferPaths {
|
||||
if fileTransferPathsOverlap(activePath, transferPath) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.paths[taskID] = transferPaths
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *fileTransferLockSet) Release(taskID string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
delete(s.paths, taskID)
|
||||
}
|
||||
|
||||
func getFileTransferPaths(req request.FileMove) []string {
|
||||
paths := make([]string, 0, 1+len(req.OldPaths)+len(req.CoverPaths))
|
||||
paths = append(paths, req.NewPath)
|
||||
paths = append(paths, req.OldPaths...)
|
||||
paths = append(paths, req.CoverPaths...)
|
||||
|
||||
unique := make(map[string]struct{}, len(paths))
|
||||
result := make([]string, 0, len(paths))
|
||||
for _, item := range paths {
|
||||
item = filepath.Clean(item)
|
||||
if _, ok := unique[item]; ok {
|
||||
continue
|
||||
}
|
||||
unique[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func fileTransferPathsOverlap(first, second string) bool {
|
||||
return first == second || strings.HasPrefix(first, second+string(filepath.Separator)) || strings.HasPrefix(second, first+string(filepath.Separator))
|
||||
}
|
||||
|
||||
func aggregateFileMoveErrors(errs []error) error {
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
var errString strings.Builder
|
||||
for _, err := range errs {
|
||||
errString.WriteString(err.Error())
|
||||
errString.WriteByte('\n')
|
||||
}
|
||||
return errors.New(errString.String())
|
||||
}
|
||||
+1213
-646
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,490 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
dockerGuardComposeProjectLabel = "com.docker.compose.project"
|
||||
dockerGuardComposeCreatedBy = "createdBy"
|
||||
dockerTrafficPathForward = "forward"
|
||||
dockerTrafficPathInput = "input"
|
||||
dockerTrafficPathUnknown = "unknown"
|
||||
dockerManagementContainerGuard = "container_guard"
|
||||
dockerManagementHostFirewall = "host_firewall"
|
||||
dockerManagementNeedsDiagnosis = "needs_diagnosis"
|
||||
dockerReasonNATInspectFailed = "nat_inspect_failed"
|
||||
dockerReasonNATChainUnreachable = "nat_chain_unreachable"
|
||||
dockerReasonProxyInspectFailed = "proxy_inspect_failed"
|
||||
dockerReasonNoMatchingPath = "no_matching_path"
|
||||
)
|
||||
|
||||
type DockerPortGuardService struct {
|
||||
runtime dockerfirewall.Runtime
|
||||
runtimeForBackend func(context.Context, string) dockerfirewall.Runtime
|
||||
client func() (*client.Client, error)
|
||||
version func(string) string
|
||||
}
|
||||
|
||||
var dockerPortGuardServiceMu sync.Mutex
|
||||
|
||||
type IDockerPortGuardService interface {
|
||||
LoadOverview(context.Context) (dto.DockerPortGuardList, error)
|
||||
ExportBackup(context.Context, filter.Provider) (dto.FirewallSubsystemBackup, error)
|
||||
LoadPublishedPorts(context.Context) ([]dto.DockerPortGuardContainer, error)
|
||||
Operate(context.Context, dto.DockerPortGuardOperation) error
|
||||
QueueInitialization(dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error)
|
||||
DeletePolicies(dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error)
|
||||
UpsertPolicies(dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error)
|
||||
Restore(context.Context) error
|
||||
}
|
||||
|
||||
func NewIDockerPortGuardService() IDockerPortGuardService {
|
||||
return newDockerPortGuardService()
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPortGuardList, error) {
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return dto.DockerPortGuardList{}, err
|
||||
}
|
||||
backend := selectedDockerFirewallBackend("")
|
||||
inventory, err := s.guardRuntime(ctx, backend).ListPolicies()
|
||||
if err != nil {
|
||||
return dto.DockerPortGuardList{}, err
|
||||
}
|
||||
policies := dockerGuardInventoryEndpoints(inventory)
|
||||
unavailable := func() dto.DockerPortGuardList {
|
||||
backend := selectedDockerFirewallBackend("")
|
||||
base := s.runtimeStatus(s.guardRuntime(ctx, backend), backend, len(families) > 1)
|
||||
base.Message = i18n.Get("ErrDockerFailed")
|
||||
return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: policies}
|
||||
}
|
||||
cli, err := s.client()
|
||||
if err != nil {
|
||||
return unavailable(), nil
|
||||
}
|
||||
defer cli.Close()
|
||||
info, err := cli.Info(ctx)
|
||||
if err != nil {
|
||||
return unavailable(), nil
|
||||
}
|
||||
detectedBackend := dockerFirewallBackend(info)
|
||||
backend = selectedDockerFirewallBackend(detectedBackend)
|
||||
base := s.runtimeStatus(s.guardRuntime(ctx, backend), backend, len(families) > 1)
|
||||
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
|
||||
if err != nil {
|
||||
return dto.DockerPortGuardList{}, err
|
||||
}
|
||||
annotateDockerEndpointManagement(ctx, endpoints, detectedBackend)
|
||||
endpoints, orphanPolicies := matchDockerGuardPolicies(base, policies, endpoints)
|
||||
sort.Slice(endpoints, func(i, j int) bool {
|
||||
return fmt.Sprintf("%s|%s|%d|%s", endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol) < fmt.Sprintf("%s|%s|%d|%s", endpoints[j].Family, endpoints[j].HostIP, endpoints[j].HostPort, endpoints[j].Protocol)
|
||||
})
|
||||
sort.Slice(orphanPolicies, func(i, j int) bool {
|
||||
return fmt.Sprintf("%s|%s|%d|%s", orphanPolicies[i].Family, orphanPolicies[i].HostIP, orphanPolicies[i].HostPort, orphanPolicies[i].Protocol) < fmt.Sprintf("%s|%s|%d|%s", orphanPolicies[j].Family, orphanPolicies[j].HostIP, orphanPolicies[j].HostPort, orphanPolicies[j].Protocol)
|
||||
})
|
||||
return dto.DockerPortGuardList{Base: base, Containers: groupDockerGuardContainers(endpoints), OrphanPolicies: orphanPolicies}, nil
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) ExportBackup(ctx context.Context, provider filter.Provider) (dto.FirewallSubsystemBackup, error) {
|
||||
backend := string(provider)
|
||||
if backend == "" {
|
||||
backend = selectedDockerFirewallBackend("")
|
||||
}
|
||||
if backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables {
|
||||
return dto.FirewallSubsystemBackup{}, filter.ErrInvalidRule
|
||||
}
|
||||
inventory, err := s.guardRuntime(ctx, backend).ListPolicies()
|
||||
if err != nil {
|
||||
return dto.FirewallSubsystemBackup{}, err
|
||||
}
|
||||
if inventory.Policies == nil {
|
||||
inventory.Policies = []dockerfirewall.Policy{}
|
||||
}
|
||||
return dto.FirewallSubsystemBackup{Subsystem: "docker", Provider: filter.Provider(backend), Docker: &inventory}, nil
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.DockerPortGuardContainer, error) {
|
||||
cli, err := s.client()
|
||||
if err != nil {
|
||||
return nil, buserr.WithDetail("ErrDockerFailed", err.Error(), err)
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
if socketPath, local := strings.CutPrefix(cli.DaemonHost(), "unix://"); local {
|
||||
if _, statErr := os.Stat(socketPath); errors.Is(statErr, os.ErrNotExist) {
|
||||
return []dto.DockerPortGuardContainer{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
endpoints, err := discoverDockerEndpoints(ctx, cli, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
backend := selectedDockerFirewallBackend("")
|
||||
if info, infoErr := cli.Info(ctx); infoErr == nil {
|
||||
backend = dockerFirewallBackend(info)
|
||||
}
|
||||
annotateDockerEndpointManagement(ctx, endpoints, backend)
|
||||
return groupDockerGuardContainers(endpoints), nil
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.DockerPortGuardOperation) error {
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
switch request.Operation {
|
||||
case "initialize":
|
||||
return s.initialize(ctx, request, nil)
|
||||
case "bind":
|
||||
runtime, _, err := s.runtimeForDocker(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := errors.Join(runtime.Bind(families...), ctx.Err()); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
|
||||
case "unbind":
|
||||
var err error
|
||||
if s.runtime != nil {
|
||||
err = s.runtime.Unbind()
|
||||
} else {
|
||||
err = errors.Join(dockerfirewall.NewIptables(ctx).Unbind(), dockerfirewall.NewNftables(ctx).Unbind())
|
||||
}
|
||||
if err = errors.Join(err, ctx.Err()); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
|
||||
default:
|
||||
return fmt.Errorf("unsupported Docker port guard operation: %s", request.Operation)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) QueueInitialization(request dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error) {
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
if request.Operation != "initialize" {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule
|
||||
}
|
||||
if request.BackupFile != "" {
|
||||
if _, err := readFirewallSubsystemBackup(request.BackupFile, "docker"); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
}
|
||||
return queueFirewallRuleTask(firewallTaskDocker, task.TaskExec, request.TaskID, []string{firewallTaskDocker}, func(t *task.Task) error {
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
return s.initialize(t.TaskCtx, request, t)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) {
|
||||
uuids, err := normalizeDockerFirewallUUIDs(request.UUIDs)
|
||||
if err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
return queueFirewallRuleTask(firewallTaskDocker, task.TaskDelete, "", uuids, func(t *task.Task) error {
|
||||
ctx := t.TaskCtx
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
runtime, backend, err := s.runtimeForDocker(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wanted := make(map[string]bool, len(uuids))
|
||||
for _, id := range uuids {
|
||||
wanted[id] = true
|
||||
}
|
||||
remaining := make([]dockerfirewall.Policy, 0, len(inventory.Policies))
|
||||
for _, policy := range inventory.Policies {
|
||||
if wanted[policy.UUID] {
|
||||
delete(wanted, policy.UUID)
|
||||
} else {
|
||||
remaining = append(remaining, policy)
|
||||
}
|
||||
}
|
||||
if len(wanted) > 0 {
|
||||
return filter.ErrRuleStale
|
||||
}
|
||||
return applyDockerPolicies(ctx, runtime, backend, inventory, remaining)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error) {
|
||||
if len(request.Policies) > filter.MaxAtomicExpansion {
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
|
||||
}
|
||||
labels := make([]string, len(request.Policies))
|
||||
policies := make([]dockerfirewall.Policy, 0, len(request.Policies))
|
||||
endpoints := make([]dto.DockerPortGuardEndpointIdentity, 0, len(request.Policies))
|
||||
count := 0
|
||||
for i, policy := range request.Policies {
|
||||
labels[i] = fmt.Sprintf("[%d/%d] %s %s %s:%d %s", i+1, len(request.Policies), policy.Family, policy.Protocol, policy.HostIP, policy.HostPort, policy.Mode)
|
||||
normalized, err := normalizeDockerFirewallPolicy(dockerfirewall.Policy{
|
||||
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort,
|
||||
Protocol: policy.Protocol, Mode: policy.Mode, Sources: policy.Sources,
|
||||
})
|
||||
if err != nil {
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("%s: %w", labels[i], err)
|
||||
}
|
||||
if normalized.Mode == dockerfirewall.ModeAll || normalized.Mode == dockerfirewall.ModeAcceptAll {
|
||||
count++
|
||||
} else {
|
||||
count += len(normalized.Sources)
|
||||
if normalized.Mode == dockerfirewall.ModeAllow {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count > filter.MaxAtomicExpansion {
|
||||
return dto.FilterChainOperationResponse{}, fmt.Errorf("create or import at most %d rules per batch (after expansion)", filter.MaxAtomicExpansion)
|
||||
}
|
||||
normalized.UUID = uuid.NewString()
|
||||
policies = append(policies, normalized)
|
||||
}
|
||||
return queueFirewallRuleTask(firewallTaskDocker, task.TaskUpdate, "", labels, func(t *task.Task) error {
|
||||
ctx := t.TaskCtx
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
runtime, backend, err := s.runtimeForDocker(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current := append([]dockerfirewall.Policy(nil), inventory.Policies...)
|
||||
if request.Import {
|
||||
backup := dto.FirewallSubsystemBackup{Provider: filter.Provider(backend), Docker: &dockerfirewall.PolicyInventory{Policies: policies}}
|
||||
merged, err := mergeDockerBackup(inventory, backup, backend, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current = merged.Policies
|
||||
}
|
||||
byEndpoint := make(map[string]int, len(current))
|
||||
for i, policy := range current {
|
||||
byEndpoint[dockerPolicyEndpointKey(policy)] = i
|
||||
}
|
||||
for i := range policies {
|
||||
key := dockerPolicyEndpointKey(policies[i])
|
||||
index, exists := byEndpoint[key]
|
||||
if request.Import {
|
||||
if !exists || current[index].UUID != policies[i].UUID {
|
||||
labels[i] = ""
|
||||
continue
|
||||
}
|
||||
} else if exists {
|
||||
policies[i].UUID = current[index].UUID
|
||||
current[index] = policies[i]
|
||||
} else {
|
||||
byEndpoint[key] = len(current)
|
||||
current = append(current, policies[i])
|
||||
}
|
||||
endpoints = append(endpoints, dto.DockerPortGuardEndpointIdentity{
|
||||
Family: policies[i].Family, HostIP: policies[i].HostIP, HostPort: policies[i].HostPort, Protocol: policies[i].Protocol,
|
||||
})
|
||||
}
|
||||
if len(endpoints) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := s.rejectHostInputDockerGuardEndpoints(ctx, endpoints); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := applyDockerPolicies(ctx, runtime, backend, inventory, current); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) Restore(ctx context.Context) error {
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
enabled, err := dockerPortGuardPersistedEnabled()
|
||||
if err != nil || !enabled {
|
||||
return err
|
||||
}
|
||||
runtime, backend, err := s.runtimeForDocker(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backup, err := readFirewallSubsystemBackup("docker-"+backend+".rules", "docker")
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !slices.Contains(families, constant.FirewallFamilyIPv6) {
|
||||
before := len(backup.Docker.Policies)
|
||||
backup.Docker.Policies = slices.DeleteFunc(backup.Docker.Policies, func(policy dockerfirewall.Policy) bool { return policy.Family == constant.FirewallFamilyIPv6 })
|
||||
logFirewallIPv6Skipped(nil, "Docker startup", before-len(backup.Docker.Policies))
|
||||
}
|
||||
missing := make(map[string]bool)
|
||||
needsBind := false
|
||||
for _, family := range families {
|
||||
status := runtime.Status(family)
|
||||
if status.Reason == dockerfirewall.ReasonInspectFailed {
|
||||
return fmt.Errorf("inspect Docker guard %s failed", family)
|
||||
}
|
||||
if status.Reason == dockerfirewall.ReasonCommandMissing {
|
||||
continue
|
||||
}
|
||||
missing[family] = !status.Initialized
|
||||
needsBind = needsBind || (status.Initialized && !status.Effective)
|
||||
}
|
||||
if !missing[dockerfirewall.FamilyIPv4] && !missing[dockerfirewall.FamilyIPv6] {
|
||||
if needsBind {
|
||||
return runtime.Bind(families...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
inventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, policy := range inventory.Policies {
|
||||
missing[policy.Family] = false
|
||||
}
|
||||
if inventory.RuleOrders == nil {
|
||||
inventory.RuleOrders = make(map[string][]int64)
|
||||
}
|
||||
for _, policy := range backup.Docker.Policies {
|
||||
if !missing[policy.Family] {
|
||||
continue
|
||||
}
|
||||
inventory.Policies = append(inventory.Policies, policy)
|
||||
key := policy.Family + "\x00" + policy.UUID
|
||||
inventory.RuleOrders[key] = backup.Docker.RuleOrders[key]
|
||||
}
|
||||
return runtime.Initialize(inventory.Policies, inventory, families...)
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) runtimeStatus(runtime dockerfirewall.Runtime, backend string, ipv6Enabled bool) dto.DockerPortGuardBase {
|
||||
ipv4 := runtime.Status(dockerfirewall.FamilyIPv4)
|
||||
var ipv6 dockerfirewall.FamilyStatus
|
||||
if ipv6Enabled {
|
||||
ipv6 = runtime.Status(dockerfirewall.FamilyIPv6)
|
||||
}
|
||||
version := "-"
|
||||
if s.version != nil {
|
||||
version = s.version(backend)
|
||||
}
|
||||
name := "iptables-docker"
|
||||
if strings.EqualFold(strings.TrimSpace(backend), constant.FirewallProviderNftables) {
|
||||
name = "nftables-docker"
|
||||
}
|
||||
return dto.DockerPortGuardBase{
|
||||
IPv6Enabled: ipv6Enabled,
|
||||
Name: name,
|
||||
Version: version,
|
||||
Backend: backend,
|
||||
IsExist: ipv4.Reason != dockerfirewall.ReasonCommandMissing || (ipv6Enabled && ipv6.Reason != dockerfirewall.ReasonCommandMissing),
|
||||
Initialized: ipv4.Initialized || ipv6.Initialized,
|
||||
Bound: ipv4.Bound || ipv6.Bound,
|
||||
IPv4: dto.DockerPortGuardFamilyStatus{Partial: ipv4.Partial, State: ipv4.State, Reason: ipv4.Reason, Initialized: ipv4.Initialized, Bound: ipv4.Bound, Effective: ipv4.Effective},
|
||||
IPv6: dto.DockerPortGuardFamilyStatus{Partial: ipv6.Partial, State: ipv6.State, Reason: ipv6.Reason, Initialized: ipv6.Initialized, Bound: ipv6.Bound, Effective: ipv6.Effective},
|
||||
}
|
||||
}
|
||||
|
||||
func matchDockerGuardPolicies(base dto.DockerPortGuardBase, policies []dto.DockerPortGuardEndpoint, endpoints []dto.DockerPortGuardEndpoint) ([]dto.DockerPortGuardEndpoint, []dto.DockerPortGuardEndpoint) {
|
||||
matched := make(map[int]bool, len(policies))
|
||||
byEndpoint := make(map[string]int, len(policies))
|
||||
for index, policy := range policies {
|
||||
key := strings.Join([]string{policy.Family, policy.HostIP, strconv.Itoa(int(policy.HostPort)), policy.Protocol}, "\x00")
|
||||
if _, exists := byEndpoint[key]; !exists {
|
||||
byEndpoint[key] = index
|
||||
}
|
||||
}
|
||||
for i := range endpoints {
|
||||
key := strings.Join([]string{endpoints[i].Family, endpoints[i].HostIP, strconv.Itoa(int(endpoints[i].HostPort)), endpoints[i].Protocol}, "\x00")
|
||||
index, exists := byEndpoint[key]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
policy := policies[index]
|
||||
endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.PolicyUUID, policy.Mode, policy.Sources
|
||||
endpoints[i].Effective = endpoints[i].ManagementTarget == dockerManagementContainerGuard && ((policy.Family == dockerfirewall.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == dockerfirewall.FamilyIPv6 && base.IPv6.Effective))
|
||||
matched[index] = true
|
||||
}
|
||||
orphans := make([]dto.DockerPortGuardEndpoint, 0)
|
||||
for i, policy := range policies {
|
||||
if !matched[i] {
|
||||
orphans = append(orphans, policy)
|
||||
}
|
||||
}
|
||||
return endpoints, orphans
|
||||
}
|
||||
|
||||
func (s *DockerPortGuardService) rejectHostInputDockerGuardEndpoints(ctx context.Context, requested []dto.DockerPortGuardEndpointIdentity) error {
|
||||
if s.client == nil || len(requested) == 0 {
|
||||
return ctx.Err()
|
||||
}
|
||||
cli, err := s.client()
|
||||
if err != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
defer cli.Close()
|
||||
info, err := cli.Info(ctx)
|
||||
if err != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
|
||||
if err != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
annotateDockerEndpointManagement(ctx, endpoints, dockerFirewallBackend(info))
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
targets := make(map[string]string, len(endpoints))
|
||||
for _, endpoint := range endpoints {
|
||||
targets[fmt.Sprintf("%s|%s|%d|%s", endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)] = endpoint.ManagementTarget
|
||||
}
|
||||
for _, endpoint := range requested {
|
||||
target := targets[fmt.Sprintf("%s|%s|%d|%s", endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)]
|
||||
if target == dockerManagementHostFirewall {
|
||||
return buserr.WithDetail("ErrInvalidParams", "endpoint traffic is handled by the host input firewall", nil)
|
||||
}
|
||||
if target == dockerManagementNeedsDiagnosis {
|
||||
return buserr.WithDetail("ErrInvalidParams", "endpoint traffic management target requires diagnosis", nil)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
||||
)
|
||||
|
||||
const fail2BanRestoreWithFirewallMarker = "/run/1panel_fail2ban_restore_with_firewall"
|
||||
|
||||
type firewallFail2BanState struct {
|
||||
markerPath string
|
||||
isExist func(string) bool
|
||||
isActive func(string) bool
|
||||
start func(string) error
|
||||
}
|
||||
|
||||
func newFirewallFail2BanState() *firewallFail2BanState {
|
||||
return &firewallFail2BanState{
|
||||
markerPath: fail2BanRestoreWithFirewallMarker,
|
||||
isExist: func(serviceName string) bool {
|
||||
exists, err := controller.CheckExist(serviceName)
|
||||
if err != nil {
|
||||
global.LOG.Warnf("check %s installation before stopping the firewall failed: %v", serviceName, err)
|
||||
}
|
||||
return exists
|
||||
},
|
||||
isActive: func(serviceName string) bool {
|
||||
active, err := controller.CheckActive(serviceName)
|
||||
if err != nil {
|
||||
global.LOG.Warnf("check %s status before stopping the firewall failed: %v", serviceName, err)
|
||||
}
|
||||
return active
|
||||
},
|
||||
start: controller.HandleStart,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *firewallFail2BanState) rememberBeforeFirewallStop() error {
|
||||
if !s.isExist("fail2ban.service") {
|
||||
return nil
|
||||
}
|
||||
if !s.isActive("fail2ban.service") {
|
||||
return nil
|
||||
}
|
||||
return s.markForRestore()
|
||||
}
|
||||
|
||||
func (s *firewallFail2BanState) markForRestore() error {
|
||||
if err := os.WriteFile(s.markerPath, nil, 0600); err != nil {
|
||||
return fmt.Errorf("mark Fail2Ban for restoration with the firewall: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *firewallFail2BanState) restoreAfterFirewallStart() error {
|
||||
_, err := os.Stat(s.markerPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("load Fail2Ban restore marker after starting the firewall: %w", err)
|
||||
}
|
||||
|
||||
if err := s.start("fail2ban.service"); err != nil {
|
||||
return fmt.Errorf("restore Fail2Ban after starting the firewall: %w", err)
|
||||
}
|
||||
return s.clearRestoreMarker()
|
||||
}
|
||||
|
||||
func (s *firewallFail2BanState) clearRestoreMarker() error {
|
||||
if err := os.Remove(s.markerPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("clear Fail2Ban firewall restore status: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,175 +1,688 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
fireClient "github.com/1Panel-dev/1Panel/agent/utils/firewall/client"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client/iptables"
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/iptables_helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/nftables_helper"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type firewallPortWhitelist struct {
|
||||
Port string
|
||||
Protocol string
|
||||
type IFirewallSettingService interface {
|
||||
CreatePortWhitelist(context.Context, dto.FirewallPortWhitelistCreate) error
|
||||
UpdatePortWhitelist(context.Context, dto.FirewallPortWhitelistUpdate) error
|
||||
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error
|
||||
Load(context.Context) (dto.FirewallSettings, error)
|
||||
Operate(context.Context, dto.FirewallBackendOperation) error
|
||||
OperateFamily(dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error)
|
||||
OperateIPv6(dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error)
|
||||
}
|
||||
|
||||
func loadConfiguredFirewallPortWhiteList() ([]firewallPortWhitelist, error) {
|
||||
value, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList)
|
||||
if err != nil {
|
||||
value = constant.FirewallPortWhiteListValue
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return parseFirewallPortWhiteList(value)
|
||||
}
|
||||
type FirewallSettingService struct{}
|
||||
|
||||
func loadFirewallPortWhiteList() ([]firewallPortWhitelist, error) {
|
||||
portWhiteList, err := loadConfiguredFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requiredPorts, err := loadRequiredFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return normalizeFirewallPortWhiteList(append(portWhiteList, requiredPorts...)), nil
|
||||
}
|
||||
var firewallWhitelistMu sync.Mutex
|
||||
|
||||
func loadRequiredFirewallPortWhiteList() ([]firewallPortWhitelist, error) {
|
||||
panelPort := LoadPanelPort()
|
||||
if panelPort == "" {
|
||||
return nil, fmt.Errorf("find 1panel service port failed")
|
||||
}
|
||||
return normalizeFirewallPortWhiteList([]firewallPortWhitelist{
|
||||
{Port: panelPort, Protocol: "tcp"},
|
||||
{Port: loadSSHPort(), Protocol: "tcp"},
|
||||
}), nil
|
||||
}
|
||||
|
||||
func parseFirewallPortWhiteList(value string) ([]firewallPortWhitelist, error) {
|
||||
items := strings.FieldsFunc(value, func(r rune) bool {
|
||||
return r == ',' || r == '\n' || r == ';' || r == ' '
|
||||
})
|
||||
ports := make([]firewallPortWhitelist, 0, len(items))
|
||||
exists := make(map[string]struct{})
|
||||
for _, item := range items {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) (result error) {
|
||||
firewallWhitelistMu.Lock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer func() {
|
||||
firewallRuleMutationMu.Unlock()
|
||||
firewallWhitelistMu.Unlock()
|
||||
if result == nil {
|
||||
result = newFirewallService().SyncPortWhitelist(ctx)
|
||||
}
|
||||
port, protocol, ok := strings.Cut(item, "/")
|
||||
if !ok {
|
||||
protocol = "tcp"
|
||||
}
|
||||
port = strings.TrimSpace(port)
|
||||
protocol = strings.ToLower(strings.TrimSpace(protocol))
|
||||
if protocol != "tcp" && protocol != "udp" {
|
||||
return nil, fmt.Errorf("invalid firewall port whitelist protocol: %s", item)
|
||||
}
|
||||
portNum, err := strconv.Atoi(port)
|
||||
if err != nil || portNum < 1 || portNum > 65535 {
|
||||
return nil, fmt.Errorf("invalid firewall port whitelist: %s", item)
|
||||
}
|
||||
key := fmt.Sprintf("%d/%s", portNum, protocol)
|
||||
if _, ok := exists[key]; ok {
|
||||
continue
|
||||
}
|
||||
exists[key] = struct{}{}
|
||||
ports = append(ports, firewallPortWhitelist{Port: strconv.Itoa(portNum), Protocol: protocol})
|
||||
}
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
func normalizeFirewallPortWhiteList(portWhiteList []firewallPortWhitelist) []firewallPortWhitelist {
|
||||
ports := make([]firewallPortWhitelist, 0, len(portWhiteList))
|
||||
exists := make(map[string]struct{})
|
||||
for _, item := range portWhiteList {
|
||||
if item.Port == "" {
|
||||
continue
|
||||
}
|
||||
key := fmt.Sprintf("%s/%s", item.Port, item.Protocol)
|
||||
if _, ok := exists[key]; ok {
|
||||
continue
|
||||
}
|
||||
exists[key] = struct{}{}
|
||||
ports = append(ports, item)
|
||||
}
|
||||
return ports
|
||||
}
|
||||
|
||||
func syncFirewallPortWhiteListAfterUpdate(oldValue string) error {
|
||||
client, err := firewall.NewFirewallClient()
|
||||
}()
|
||||
current, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if client.Name() == "iptables" {
|
||||
isInit, _ := iptables.LoadInitStatus("iptables", "base")
|
||||
if !isInit {
|
||||
return nil
|
||||
current = append(current, request.Rule)
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) (result error) {
|
||||
firewallWhitelistMu.Lock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer func() {
|
||||
firewallRuleMutationMu.Unlock()
|
||||
firewallWhitelistMu.Unlock()
|
||||
if result == nil {
|
||||
result = newFirewallService().SyncPortWhitelist(ctx)
|
||||
}
|
||||
oldPortWhiteList, err := parseFirewallPortWhiteList(oldValue)
|
||||
}()
|
||||
current, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index, err := findPortWhitelistRule(current, request.OldRule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current[index] = request.Rule
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error {
|
||||
firewallWhitelistMu.Lock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer func() {
|
||||
firewallRuleMutationMu.Unlock()
|
||||
firewallWhitelistMu.Unlock()
|
||||
}()
|
||||
current, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Rule == nil {
|
||||
return filter.ErrInvalidRule
|
||||
}
|
||||
index, err := findPortWhitelistRule(current, *request.Rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current = slices.Delete(current, index, index+1)
|
||||
current, err = firewall.ValidatePortWhitelist(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateFirewallWhitelistFamilies(current); err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, string(value))
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return dto.FirewallSettings{}, err
|
||||
}
|
||||
result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus(), IPv6Enabled: slices.Contains(families, constant.FirewallFamilyIPv6)}
|
||||
|
||||
installed := make(map[string]bool)
|
||||
for _, name := range lifecycle.InstalledProviders() {
|
||||
installed[name] = true
|
||||
}
|
||||
systemBackend, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
||||
result.System.Selected = strings.TrimSpace(systemBackend)
|
||||
if result.System.Selected == "" {
|
||||
if client, err := lifecycle.NewClient(""); err == nil {
|
||||
result.System.Selected = client.Name()
|
||||
}
|
||||
}
|
||||
result.System.Current = result.System.Selected
|
||||
for _, name := range []string{
|
||||
constant.FirewallProviderFirewalld,
|
||||
constant.FirewallProviderUFW,
|
||||
constant.FirewallProviderIptables,
|
||||
constant.FirewallProviderNftables,
|
||||
} {
|
||||
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
|
||||
if option.Installed && name == result.System.Selected {
|
||||
client, err := lifecycle.NewClient(name)
|
||||
if err != nil {
|
||||
option.Message = err.Error()
|
||||
} else if name == constant.FirewallProviderIptables || name == constant.FirewallProviderNftables {
|
||||
overview, err := loadSystemFirewallOverview(name, "base", families)
|
||||
if err != nil {
|
||||
option.Message = err.Error()
|
||||
}
|
||||
option.Initialized, option.Bound = overview.IsInit, overview.IsBind
|
||||
option.IPv4, option.IPv6 = overview.IPv4, overview.IPv6
|
||||
} else if option.Active, err = client.Status(); err != nil {
|
||||
option.Message = err.Error()
|
||||
}
|
||||
}
|
||||
if name == result.System.Selected && name == constant.FirewallProviderIptables {
|
||||
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil {
|
||||
option.Implementation = commands.IPv4
|
||||
}
|
||||
}
|
||||
result.System.Options = append(result.System.Options, option)
|
||||
}
|
||||
|
||||
forwardingBackend, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||
result.Forwarding.Selected = strings.TrimSpace(forwardingBackend)
|
||||
if result.Forwarding.Selected == "" {
|
||||
result.Forwarding.Selected = constant.FirewallProviderIptables
|
||||
}
|
||||
result.Forwarding.Current = result.Forwarding.Selected
|
||||
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
|
||||
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
|
||||
if option.Installed && name == result.Forwarding.Selected {
|
||||
manager, err := newForwardingAdapterFor(ctx, name)
|
||||
if err != nil {
|
||||
option.Message = err.Error()
|
||||
} else {
|
||||
status, statusErr := loadForwardingFirewallOverview(manager, families)
|
||||
option.IPv4, option.IPv6 = status.IPv4, status.IPv6
|
||||
if statusErr != nil {
|
||||
option.Message = statusErr.Error()
|
||||
} else {
|
||||
option.Initialized, option.Bound = status.IsInit, status.IsBind
|
||||
}
|
||||
if result.IPv6Enabled && name == constant.FirewallProviderIptables && !option.IPv6.Available {
|
||||
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil && !commands.IPv6Available() {
|
||||
option.IPv6.Reason = dockerfirewall.ReasonCommandMissing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if name == result.Forwarding.Selected && name == constant.FirewallProviderIptables {
|
||||
if commands, err := lifecycle.ResolveIptablesCommands(); err == nil {
|
||||
option.Implementation = commands.IPv4
|
||||
}
|
||||
}
|
||||
result.Forwarding.Options = append(result.Forwarding.Options, option)
|
||||
}
|
||||
|
||||
dockerInstalled := cmd.Which("docker")
|
||||
dockerVersion := ""
|
||||
if dockerInstalled {
|
||||
dockerVersion = loadDockerEngineVersion(ctx)
|
||||
}
|
||||
dockerBackend, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||
dockerBackend = strings.ToLower(strings.TrimSpace(dockerBackend))
|
||||
if dockerBackend == constant.FirewallProviderIptables || dockerBackend == constant.FirewallProviderNftables {
|
||||
result.Docker.Selected = dockerBackend
|
||||
}
|
||||
result.Docker.Current = result.Docker.Selected
|
||||
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
|
||||
option := dto.FirewallBackendOption{
|
||||
Name: name, Installed: installed[name], Supported: dockerInstalled,
|
||||
Active: dockerInstalled && installed[name] && result.Docker.Selected == name,
|
||||
}
|
||||
if name == constant.FirewallProviderNftables && dockerInstalled && !dockerNftablesSupported(dockerVersion) {
|
||||
option.Supported = false
|
||||
option.SupportReason = "docker_version_unsupported"
|
||||
option.Active = false
|
||||
}
|
||||
if option.Active {
|
||||
guard := newDockerFirewallRuntime(ctx, name)
|
||||
for _, family := range families {
|
||||
status := guard.Status(family)
|
||||
option.Initialized = option.Initialized || status.Initialized
|
||||
option.Bound = option.Bound || status.Bound
|
||||
info := dto.FirewallBackendFamilyStatus{
|
||||
Available: status.Reason != dockerfirewall.ReasonCommandMissing,
|
||||
Initialized: status.Initialized, Bound: status.Bound, Reason: status.Reason,
|
||||
}
|
||||
if family == constant.FirewallFamilyIPv4 {
|
||||
option.IPv4 = info
|
||||
} else {
|
||||
option.IPv6 = info
|
||||
}
|
||||
}
|
||||
}
|
||||
result.Docker.Options = append(result.Docker.Options, option)
|
||||
}
|
||||
result.PortWhitelist, err = loadPortWhitelistSetting()
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.PanelPort = LoadPanelPort()
|
||||
sshPort, sshErr := loadSSHWhitelistPortFrom(sshPath)
|
||||
if sshErr != nil {
|
||||
global.LOG.Warnf("load SSH port for firewall settings: %v", sshErr)
|
||||
} else {
|
||||
result.SSHPort = sshPort
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||
if request.Operation == "cleanup" {
|
||||
_, err := newFirewallService().Reset(ctx, dto.FirewallRuleReset{Subsystem: request.Subsystem, Provider: filter.Provider(request.Backend)})
|
||||
return err
|
||||
}
|
||||
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer firewallLifecycleTaskMu.Unlock()
|
||||
if request.Subsystem != "system" && request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
|
||||
return fmt.Errorf("%s only supports iptables or nftables", request.Subsystem)
|
||||
}
|
||||
if request.Subsystem == "system" && (request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables) && request.Operation != "select" {
|
||||
return fmt.Errorf("%s does not support initialization or cleanup", request.Backend)
|
||||
}
|
||||
switch request.Subsystem {
|
||||
case "system":
|
||||
if err := s.operateSystem(request); err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "initialize" {
|
||||
service := newFirewallService()
|
||||
whitelistErr := service.SyncPortWhitelist(ctx)
|
||||
return whitelistErr
|
||||
}
|
||||
return nil
|
||||
case "forwarding":
|
||||
return s.operateForwarding(ctx, request)
|
||||
case "docker":
|
||||
return s.operateDocker(ctx, request)
|
||||
default:
|
||||
return fmt.Errorf("unsupported firewall subsystem %q", request.Subsystem)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) OperateFamily(request dto.FirewallFamilyOperation) (dto.FilterChainOperationResponse, error) {
|
||||
if request.Family != constant.FirewallFamilyIPv4 && request.Family != constant.FirewallFamilyIPv6 {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope
|
||||
}
|
||||
if request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrUnsupportedScope
|
||||
}
|
||||
if request.Operation != "initialize" && request.Operation != "repair" && request.Operation != "bind" {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrRuleOperation
|
||||
}
|
||||
subsystem := ""
|
||||
switch request.Subsystem {
|
||||
case "system":
|
||||
subsystem = firewallTaskHost
|
||||
case "forwarding":
|
||||
subsystem = firewallTaskForwarding
|
||||
case "docker":
|
||||
subsystem = firewallTaskDocker
|
||||
default:
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrInvalidScope
|
||||
}
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
return queueFirewallRuleTask(subsystem, task.TaskExec, "", nil, func(t *task.Task) error {
|
||||
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer firewallLifecycleTaskMu.Unlock()
|
||||
t.Logf("backend=%s family=%s operation=%s", request.Backend, request.Family, request.Operation)
|
||||
families, err := loadFirewallFamilies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return syncIptablesFirewallPortWhiteList(true, oldPortWhiteList)
|
||||
}
|
||||
if !slices.Contains(families, request.Family) {
|
||||
return fmt.Errorf("IPv6 firewall support is disabled")
|
||||
}
|
||||
initialize := request.Operation != "bind"
|
||||
switch request.Subsystem {
|
||||
case "system":
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
if err := newFirewallService().checkSelectedProvider(t.TaskCtx, filter.Provider(request.Backend)); err != nil {
|
||||
return err
|
||||
}
|
||||
ports, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
required, err := firewall.RequiredPortWhitelist(ports)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
firewallRuleMutationMu.Lock()
|
||||
if request.Backend == constant.FirewallProviderIptables {
|
||||
err = iptables_helper.OperateFamily(request.Family, initialize, required)
|
||||
} else {
|
||||
err = nftables_helper.OperateFamily(filter.Family(request.Family), initialize, required)
|
||||
}
|
||||
firewallRuleMutationMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if initialize {
|
||||
if err := newFirewallService().applyPortWhitelist(t.TaskCtx, ports, nil, filter.Family(request.Family)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return settingRepo.UpdateOrCreate("IptablesStatus", constant.StatusEnable)
|
||||
case "forwarding":
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
manager, err := newForwardingAdapter(t.TaskCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if manager.Name() != request.Backend {
|
||||
return filter.ErrProviderUnavailable
|
||||
}
|
||||
if err := manager.OperateFamily(request.Family, initialize); err != nil {
|
||||
return err
|
||||
}
|
||||
rules, err := manager.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := persistForwardingRules(manager, rules); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
|
||||
default:
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
runtime, backend, err := newDockerPortGuardService().runtimeForDocker(t.TaskCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if backend != request.Backend {
|
||||
return filter.ErrProviderUnavailable
|
||||
}
|
||||
if err := runtime.OperateFamily(request.Family, initialize); err != nil {
|
||||
return err
|
||||
}
|
||||
inventory, err := runtime.ListPolicies()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := persistDockerRules(backend, inventory); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
isActive, _ := client.Status()
|
||||
if !isActive {
|
||||
func (s *FirewallSettingService) OperateIPv6(request dto.FirewallIPv6Operation) (dto.FilterChainOperationResponse, error) {
|
||||
if request.Status != constant.StatusEnable && request.Status != constant.StatusDisable {
|
||||
return dto.FilterChainOperationResponse{}, filter.ErrInvalidRule
|
||||
}
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||
return dto.FilterChainOperationResponse{}, err
|
||||
}
|
||||
return queueFirewallRuleTask(firewallTaskHost, task.TaskExec, "", nil, func(t *task.Task) error {
|
||||
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer firewallLifecycleTaskMu.Unlock()
|
||||
firewallWhitelistMu.Lock()
|
||||
defer firewallWhitelistMu.Unlock()
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
forwardingMutationMu.Lock()
|
||||
defer forwardingMutationMu.Unlock()
|
||||
dockerPortGuardServiceMu.Lock()
|
||||
defer dockerPortGuardServiceMu.Unlock()
|
||||
if request.Status == constant.StatusEnable {
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallIPv6SupportKey, request.Status)
|
||||
}
|
||||
ports, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(ipv4PortWhitelist(ports))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
installed := lifecycle.InstalledProviders()
|
||||
for _, selection := range []struct{ subsystem, key string }{
|
||||
{"system", constant.FirewallSystemBackendKey},
|
||||
{"forwarding", constant.FirewallForwardingBackendKey},
|
||||
{"docker", constant.FirewallDockerBackendKey},
|
||||
} {
|
||||
backend, err := settingRepo.GetValueByKey(selection.key)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
backend = strings.ToLower(strings.TrimSpace(backend))
|
||||
if backend == "" {
|
||||
if selection.subsystem == "system" {
|
||||
if len(installed) == 0 {
|
||||
continue
|
||||
}
|
||||
client, err := NewSelectedSystemFirewallClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backend = client.Name()
|
||||
} else {
|
||||
backend = constant.FirewallProviderIptables
|
||||
}
|
||||
}
|
||||
if !slices.Contains(installed, backend) || (backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables) {
|
||||
continue
|
||||
}
|
||||
t.Logf("disable IPv6 firewall bindings: subsystem=%s backend=%s", selection.subsystem, backend)
|
||||
switch selection.subsystem {
|
||||
case "system":
|
||||
if backend == constant.FirewallProviderIptables {
|
||||
err = iptables_helper.UnbindIPv6BaseChains()
|
||||
} else {
|
||||
err = nftables_helper.SetTableDormant(t.TaskCtx, "ip6", nftables_helper.TableName)
|
||||
if err == nil {
|
||||
err = nftables_helper.PersistRuleset(t.TaskCtx)
|
||||
}
|
||||
}
|
||||
case "forwarding":
|
||||
var manager forwarding.Adapter
|
||||
manager, err = newForwardingAdapterFor(t.TaskCtx, backend)
|
||||
if err == nil {
|
||||
err = manager.UnbindFamily(constant.FirewallFamilyIPv6)
|
||||
}
|
||||
case "docker":
|
||||
err = newDockerFirewallRuntime(t.TaskCtx, backend).Unbind(constant.FirewallFamilyIPv6)
|
||||
}
|
||||
if err != nil && !errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := t.TaskCtx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return settingRepo.UpdateValues(map[string]string{constant.FirewallIPv6SupportKey: request.Status, constant.FirewallPortWhiteList: string(value)})
|
||||
})
|
||||
}
|
||||
|
||||
func NewIFirewallSettingService() IFirewallSettingService {
|
||||
return &FirewallSettingService{}
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperation) error {
|
||||
firewallRuleMutationMu.Lock()
|
||||
defer firewallRuleMutationMu.Unlock()
|
||||
if _, err := lifecycle.NewClient(request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
||||
if previous == "" {
|
||||
if client, err := lifecycle.NewClient(""); err == nil {
|
||||
previous = client.Name()
|
||||
}
|
||||
}
|
||||
if request.Operation == "select" && previous != "" && previous != request.Backend {
|
||||
initialized, err := systemFirewallBackendInitialized(previous)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if initialized {
|
||||
return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": previous, "target": request.Backend}, nil)
|
||||
}
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
rollback := func(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
_ = settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, previous)
|
||||
return err
|
||||
}
|
||||
if request.Operation == "select" {
|
||||
return nil
|
||||
}
|
||||
portWhiteList, err := loadFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
initErr := newFirewallService().operateFilterChainBaseLocked(request.Backend, dto.FilterChainOperation{
|
||||
Name: constant.FirewallBasicChain, Operate: string(firewall.BaseOperationInit),
|
||||
})
|
||||
if initErr != nil {
|
||||
return rollback(initErr)
|
||||
}
|
||||
oldPortWhiteList, err := parseFirewallPortWhiteList(oldValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requiredPorts, err := loadRequiredFirewallPortWhiteList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oldPortWhiteList = normalizeFirewallPortWhiteList(append(oldPortWhiteList, requiredPorts...))
|
||||
return syncFirewallClientPortWhiteList(client, oldPortWhiteList, portWhiteList)
|
||||
return settingRepo.UpdateOrCreate(constant.FirewallFilterInitializedKey, constant.StatusEnable)
|
||||
}
|
||||
|
||||
func syncFirewallClientPortWhiteList(client firewall.FirewallClient, oldPortWhiteList, portWhiteList []firewallPortWhitelist) error {
|
||||
oldPorts := firewallPortWhiteListMap(oldPortWhiteList)
|
||||
newPorts := firewallPortWhiteListMap(portWhiteList)
|
||||
for _, item := range oldPortWhiteList {
|
||||
key := firewallPortWhiteListKey(item)
|
||||
if _, ok := newPorts[key]; ok {
|
||||
continue
|
||||
func systemFirewallBackendInitialized(backend string) (bool, error) {
|
||||
client, err := lifecycle.NewClient(backend)
|
||||
if err != nil {
|
||||
if errors.Is(err, lifecycle.ErrNotInstalled) {
|
||||
return false, nil
|
||||
}
|
||||
if err := client.Port(fireClient.FireInfo{Port: item.Port, Protocol: item.Protocol, Strategy: "accept"}, "remove"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if backend == constant.FirewallProviderIptables || backend == constant.FirewallProviderNftables {
|
||||
for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} {
|
||||
initialized, _, err := loadSystemFirewallFamilyStatus(backend, family)
|
||||
if family == constant.FirewallFamilyIPv6 && errors.Is(err, filter.ErrFamilyUnavailable) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if initialized {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
return client.Status()
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) operateForwarding(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||
if _, err := newForwardingAdapterFor(ctx, request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||
if request.Operation == "select" {
|
||||
current := previous
|
||||
if current == "" {
|
||||
detected, err := newForwardingAdapter(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current = detected.Name()
|
||||
}
|
||||
initialized, err := forwardingBackendInitialized(ctx, current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current != request.Backend && initialized {
|
||||
return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": current, "target": request.Backend}, nil)
|
||||
}
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "initialize" {
|
||||
return newForwardingService().Enable(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func forwardingBackendInitialized(ctx context.Context, backend string) (bool, error) {
|
||||
manager, err := newForwardingAdapterFor(ctx, backend)
|
||||
if err != nil {
|
||||
if errors.Is(err, lifecycle.ErrNotInstalled) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
for _, family := range []string{constant.FirewallFamilyIPv4, constant.FirewallFamilyIPv6} {
|
||||
initialized, _, err := manager.FamilyStatus(family)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if initialized {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||
|
||||
previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||
if request.Operation == "select" {
|
||||
current := previous
|
||||
if current == "" {
|
||||
current = constant.FirewallProviderNftables
|
||||
if request.Backend == constant.FirewallProviderNftables {
|
||||
current = constant.FirewallProviderIptables
|
||||
}
|
||||
}
|
||||
initialized, err := dockerGuardBackendInitialized(ctx, current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current != request.Backend && initialized {
|
||||
return buserr.WithMap("ErrFirewallBackendCleanupRequired", map[string]interface{}{"current": current, "target": request.Backend}, nil)
|
||||
}
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, request.Backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Operation == "select" {
|
||||
if err := (&DockerService{}).UpdateFirewallBackend(request.Backend); err != nil {
|
||||
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, item := range portWhiteList {
|
||||
key := firewallPortWhiteListKey(item)
|
||||
if _, ok := oldPorts[key]; ok {
|
||||
continue
|
||||
}
|
||||
if err := client.Port(fireClient.FireInfo{Port: item.Port, Protocol: item.Protocol, Strategy: "accept"}, "add"); err != nil {
|
||||
if request.Operation == "initialize" {
|
||||
if err := newDockerPortGuardService().Operate(ctx, dto.DockerPortGuardOperation{Operation: "initialize"}); err != nil {
|
||||
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return client.Reload()
|
||||
return nil
|
||||
}
|
||||
|
||||
func firewallPortWhiteListMap(portWhiteList []firewallPortWhitelist) map[string]struct{} {
|
||||
ports := make(map[string]struct{})
|
||||
for _, item := range portWhiteList {
|
||||
ports[firewallPortWhiteListKey(item)] = struct{}{}
|
||||
func dockerGuardBackendInitialized(ctx context.Context, backend string) (bool, error) {
|
||||
guard := newDockerFirewallRuntime(ctx, backend)
|
||||
for _, family := range []string{dockerfirewall.FamilyIPv4, dockerfirewall.FamilyIPv6} {
|
||||
initialized, err := guard.Initialized(family)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if initialized {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return ports
|
||||
}
|
||||
|
||||
func firewallPortWhiteListKey(item firewallPortWhitelist) string {
|
||||
return item.Port + "/" + item.Protocol
|
||||
return false, nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user