mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-11 00:00:30 +00:00
Compare commits
180
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
680125d5b7 | ||
|
|
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>
|
<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">
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -12,7 +9,6 @@
|
|||||||
|
|
||||||
<p align="center">
|
<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://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://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/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>
|
<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?
|
## 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.
|
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.
|
||||||
👉 Watch the [2-minute introduction](https://www.youtube.com/watch?v=Jl_wqp-XA08)
|
- **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?
|
## Why 1Panel?
|
||||||
|
|
||||||
| | 1Panel | cPanel / Plesk | aaPanel | Webmin |
|
| | 1Panel | cPanel / Plesk | aaPanel | Webmin |
|
||||||
|--|--------|----------------|---------|--------|
|
|--|--------|----------------|---------|--------|
|
||||||
| Free & open source | ✅ | ❌ | Partial | ✅ |
|
| Free & open source | ✅ | ❌ | Partial | ✅ |
|
||||||
| Native AI agent runtime | ✅ | ❌ | ❌ | ❌ |
|
| AI management | ✅ | ❌ | ❌ | ❌ |
|
||||||
| One-click app marketplace | ✅ 165+ apps | ❌ | ✅ | ❌ |
|
| One-click app marketplace | ✅ 165+ apps | ❌ | ✅ | ❌ |
|
||||||
| Modern UI (post-2020) | ✅ | ❌ | Partial | ❌ |
|
| Modern UI (post-2020) | ✅ | ❌ | Partial | ❌ |
|
||||||
| Docker / container management | ✅ | ❌ | ❌ | ❌ |
|
| Docker / container management | ✅ | ❌ | ❌ | ❌ |
|
||||||
| Active development | ✅ | ✅ | ✅ | Slow |
|
| 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
|
## Quick Start
|
||||||
|
|
||||||
> **Requirements:** Linux VPS (Debian / Ubuntu / CentOS / Rocky), 1 GB RAM, internet access.
|
Prepare your Linux server and run the following script:
|
||||||
> Takes ~60 seconds.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash -c "$(curl -sSL https://resource.1panel.pro/v2/quick_start.sh)"
|
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
|
## 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 |
|
| Feature | OSS | Pro | Ent |
|
||||||
|---------|:---:|:---:|
|
|---------|:---:|:---:|:---:|
|
||||||
| One-click app installs | ✅ | ✅ |
|
| One-click app installs | ✅ | ✅ | ✅ |
|
||||||
| AI agents (OpenClaw) | 1 agent | Unlimited |
|
| AI agents (OpenClaw) | 5 agent | Unlimited | ✅ |
|
||||||
| WAF & advanced security | Basic | ✅ |
|
| WAF & advanced security | Basic | ✅ | ✅ |
|
||||||
| Website tamper protection | ❌ | ✅ |
|
| Website tamper protection | ❌ | ✅ | ✅ |
|
||||||
| Website uptime monitoring | ❌ | ✅ |
|
| Website uptime monitoring | ❌ | ✅ | ✅ |
|
||||||
| Multi-node management | ❌ | ✅ |
|
| Multi-node management | ❌ | ✅ | ✅ |
|
||||||
| Custom logo & theme | ❌ | ✅ |
|
| Custom logo & theme | ❌ | ✅ | ✅ |
|
||||||
| Priority support | ❌ | ✅ |
|
| KVM Web UI | ❌ | ❌ | ✅ |
|
||||||
|
| AI Gateway | ❌ | ❌ | ✅ |
|
||||||
**From $80/year.** [Compare plans & start 30-day free trial →](https://1panel.pro/pricing)
|
| Priority support | ❌ | ❌ | ✅ |
|
||||||
|
|
||||||
## Star History
|
|
||||||
|
|
||||||
[](https://star-history.com/#1Panel-dev/1Panel&Date)
|
|
||||||
|
|
||||||
## Community & Support
|
## Community & Support
|
||||||
|
|
||||||
|
|||||||
@@ -1378,6 +1378,88 @@ func (b *BaseApi) UninstallAgentSkill(c *gin.Context) {
|
|||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary List OpenClaw plugins
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentPluginsReq true "request"
|
||||||
|
// @Success 200 {array} dto.AgentPluginItem
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/plugins/list [post]
|
||||||
|
func (b *BaseApi) ListAgentPlugins(c *gin.Context) {
|
||||||
|
var req dto.AgentPluginsReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := agentService.ListPlugins(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary Search OpenClaw plugins
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentPluginSearchReq true "request"
|
||||||
|
// @Success 200 {array} dto.AgentPluginSearchItem
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/plugins/search [post]
|
||||||
|
func (b *BaseApi) SearchAgentPlugins(c *gin.Context) {
|
||||||
|
var req dto.AgentPluginSearchReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := agentService.SearchPlugins(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary Install an OpenClaw marketplace plugin
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentPluginMarketInstallReq true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/plugins/install [post]
|
||||||
|
func (b *BaseApi) InstallAgentMarketPlugin(c *gin.Context) {
|
||||||
|
var req dto.AgentPluginMarketInstallReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := agentService.InstallMarketPlugin(req); err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary Operate an OpenClaw plugin
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentPluginOperateReq true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/plugins/operate [post]
|
||||||
|
func (b *BaseApi) OperateAgentPlugin(c *gin.Context) {
|
||||||
|
var req dto.AgentPluginOperateReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := agentService.OperatePlugin(req); err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Login Agent Weixin channel
|
// @Summary Login Agent Weixin channel
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ package v2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -294,6 +297,34 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := alertService.UpdateAlertConfig(req, loadAuditUser(c)); err != nil {
|
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)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -346,6 +377,15 @@ func (b *BaseApi) TestAlertConfig(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
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)
|
flag, err := alertService.TestAlertConfig(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
|
|||||||
@@ -439,7 +439,7 @@ func (b *BaseApi) ContainerItemStats(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := containerService.ContainerItemStats(req)
|
data, err := containerService.ContainerItemStats(c.Request.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -881,6 +881,26 @@ func (b *BaseApi) ComposeUpdate(c *gin.Context) {
|
|||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags Container Compose
|
||||||
|
// @Summary Pin compose
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.ComposePin true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /containers/compose/pin [post]
|
||||||
|
func (b *BaseApi) ComposePin(c *gin.Context) {
|
||||||
|
var req dto.ComposePin
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := containerService.ComposePin(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags Container Compose
|
// @Tags Container Compose
|
||||||
// @Summary Load compose environment variables
|
// @Summary Load compose environment variables
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ var (
|
|||||||
fileShareService = service.NewIFileShareService()
|
fileShareService = service.NewIFileShareService()
|
||||||
sshService = service.NewISSHService()
|
sshService = service.NewISSHService()
|
||||||
firewallService = service.NewIFirewallService()
|
firewallService = service.NewIFirewallService()
|
||||||
iptablesService = service.NewIIptablesService()
|
firewallSettingService = service.NewIFirewallSettingService()
|
||||||
|
forwardingService = service.NewIForwardingService()
|
||||||
|
dockerPortGuardService = service.NewIDockerPortGuardService()
|
||||||
monitorService = service.NewIMonitorService()
|
monitorService = service.NewIMonitorService()
|
||||||
systemService = service.NewISystemService()
|
systemService = service.NewISystemService()
|
||||||
runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService()
|
runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService()
|
||||||
@@ -60,6 +62,7 @@ var (
|
|||||||
websiteDnsAccountService = service.NewIWebsiteDnsAccountService()
|
websiteDnsAccountService = service.NewIWebsiteDnsAccountService()
|
||||||
websiteSSLService = service.NewIWebsiteSSLService()
|
websiteSSLService = service.NewIWebsiteSSLService()
|
||||||
websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService()
|
websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService()
|
||||||
|
websiteTemplateService = service.NewIWebsiteTemplateService()
|
||||||
|
|
||||||
nginxService = service.NewINginxService()
|
nginxService = service.NewINginxService()
|
||||||
|
|
||||||
|
|||||||
+470
-23
@@ -35,6 +35,81 @@ var cancelledChunkUploads = struct {
|
|||||||
ids map[string]struct{}
|
ids map[string]struct{}
|
||||||
}{ids: make(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
|
// @Tags File
|
||||||
// @Summary List files
|
// @Summary List files
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -474,11 +549,7 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dstInfo, statErr := os.Stat(dstFilename)
|
dstInfo, statErr := os.Stat(dstFilename)
|
||||||
if overwrite {
|
err = finalizeUploadedFile(tmpFilename, dstFilename, overwrite)
|
||||||
_ = os.Remove(dstFilename)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Rename(tmpFilename, dstFilename)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = os.Remove(tmpFilename)
|
_ = os.Remove(tmpFilename)
|
||||||
e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
|
e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
|
||||||
@@ -613,10 +684,35 @@ func (b *BaseApi) StopWget(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
files.CancelDownload(req.Key)
|
if err := files.CancelDownload(req.Key); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
helper.Success(c)
|
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
|
// @Tags File
|
||||||
// @Summary Move file
|
// @Summary Move file
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -638,6 +734,26 @@ func (b *BaseApi) MoveFile(c *gin.Context) {
|
|||||||
helper.Success(c)
|
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
|
// @Tags File
|
||||||
// @Summary Download file
|
// @Summary Download file
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -792,6 +908,289 @@ func (b *BaseApi) DepthDirSize(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, res)
|
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 {
|
func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int, overwrite bool) error {
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = os.RemoveAll(fileDir)
|
_ = os.RemoveAll(fileDir)
|
||||||
@@ -871,6 +1270,10 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
|||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if chunkCount <= 0 || chunkIndex < 0 || chunkIndex >= chunkCount {
|
||||||
|
helper.BadRequest(c, errors.New("invalid chunk index"))
|
||||||
|
return
|
||||||
|
}
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
tmpDir := path.Join(global.Dir.TmpDir, "upload")
|
tmpDir := path.Join(global.Dir.TmpDir, "upload")
|
||||||
if !fileOp.Stat(tmpDir) {
|
if !fileOp.Stat(tmpDir) {
|
||||||
@@ -885,20 +1288,25 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
uploadID := strings.TrimSpace(c.PostForm("uploadID"))
|
uploadID := strings.TrimSpace(c.PostForm("uploadID"))
|
||||||
|
resumable := c.PostForm("fileSize") != "" || c.PostForm("offset") != ""
|
||||||
cancellable := uploadID != ""
|
cancellable := uploadID != ""
|
||||||
if cancellable && (filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`)) {
|
if cancellable && (filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`)) {
|
||||||
helper.BadRequest(c, errors.New("invalid upload ID"))
|
helper.BadRequest(c, errors.New("invalid upload ID"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if resumable && !cancellable {
|
||||||
|
helper.BadRequest(c, errors.New("upload ID is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
if !cancellable {
|
if !cancellable {
|
||||||
uploadID = filename
|
uploadID = filename
|
||||||
}
|
}
|
||||||
fileDir := filepath.Join(tmpDir, uploadID)
|
fileDir := filepath.Join(tmpDir, uploadID)
|
||||||
if cancellable && chunkUploadCancelled(uploadID) {
|
if cancellable && chunkUploadCancelled(uploadID) {
|
||||||
helper.BadRequest(c, errors.New("upload cancelled"))
|
helper.BadRequest(c, errChunkUploadCancelled)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if chunkIndex == 0 {
|
if !resumable && chunkIndex == 0 {
|
||||||
if fileOp.Stat(fileDir) {
|
if fileOp.Stat(fileDir) {
|
||||||
_ = fileOp.DeleteDir(fileDir)
|
_ = fileOp.DeleteDir(fileDir)
|
||||||
}
|
}
|
||||||
@@ -907,32 +1315,67 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
|||||||
filePath := filepath.Join(fileDir, filename)
|
filePath := filepath.Join(fileDir, filename)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if !resumable && err != nil {
|
||||||
_ = os.RemoveAll(fileDir)
|
_ = os.RemoveAll(fileDir)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
var (
|
chunkData, err := io.ReadAll(uploadFile)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cancellable && chunkUploadCancelled(uploadID) {
|
if cancellable && chunkUploadCancelled(uploadID) {
|
||||||
err = errors.New("upload cancelled")
|
err = errChunkUploadCancelled
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
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))
|
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
|
||||||
err = os.WriteFile(chunkPath, chunkData, constant.DirPerm)
|
err = os.WriteFile(chunkPath, chunkData, constant.DirPerm)
|
||||||
@@ -985,6 +1428,10 @@ func (b *BaseApi) StopChunkUpload(c *gin.Context) {
|
|||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := removeActiveResumableUploadPart(uploadID); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+636
-246
@@ -1,26 +1,53 @@
|
|||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||||
"github.com/gin-gonic/gin"
|
"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
|
// @Tags Firewall
|
||||||
// @Summary Load firewall base info
|
// @Summary Load firewall base info
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.OperationWithName true "request"
|
// @Param request body dto.OperationWithName true "request"
|
||||||
// @Success 200 {object} dto.FirewallBaseInfo
|
// @Success 200 {object} dto.FirewallSubsystemStatus
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/base [post]
|
// @Router /hosts/firewall/base [post]
|
||||||
func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
||||||
var req dto.OperationWithName
|
var request dto.OperationWithName
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := firewallService.LoadBaseInfo(req.Name)
|
data, err := firewallService.LoadBaseInfo(request.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -29,311 +56,674 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, data)
|
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
|
// @Tags Firewall
|
||||||
// @Summary Operate firewall
|
// @Summary Operate firewall
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallOperation true "request"
|
// @Param request body dto.FirewallLifecycleOperation true "request"
|
||||||
// @Success 200
|
// @Success 200 {object} dto.FirewallLifecycleOperationResponse
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/operate [post]
|
// @Router /hosts/firewall/operate [post]
|
||||||
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] 防火墙","formatEN":"[operation] firewall"}
|
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] 防火墙","formatEN":"[operation] firewall"}
|
||||||
func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
||||||
var req dto.FirewallOperation
|
var request dto.FirewallLifecycleOperation
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := firewallService.OperateFirewall(req); err != nil {
|
result, err := firewallService.QueueFirewallOperation(request)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
helper.SuccessWithData(c, dto.PageResult{
|
helper.SuccessWithData(c, result)
|
||||||
Items: list,
|
|
||||||
Total: total,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Operate iptables filter rule
|
// @Summary Load forwarding base info
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.IptablesRuleOp true "request"
|
// @Success 200 {object} dto.FirewallSubsystemStatus
|
||||||
// @Success 200
|
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/filter/rule/operate [post]
|
// @Router /hosts/firewall/forward/base [post]
|
||||||
// @x-panel-log {"bodyKeys":["operation","chain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] filter规则到 [chain]","formatEN":"[operation] filter rule to [chain]"}
|
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) {
|
||||||
func (b *BaseApi) OperateFilterRule(c *gin.Context) {
|
data, err := forwardingService.LoadBaseInfo()
|
||||||
var req dto.IptablesRuleOp
|
if err != nil {
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
helper.InternalServer(c, err)
|
||||||
return
|
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(request)
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
helper.Success(c)
|
helper.SuccessWithData(c, dto.PageResult{Items: items, Total: total})
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Batch operate iptables filter rules
|
// @Summary Operate forwarding rules
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.IptablesBatchOperate true "request"
|
// @Param request body dto.ForwardRuleOperate true "request"
|
||||||
// @Success 200
|
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/filter/rule/batch [post]
|
// @Router /hosts/firewall/forward/operate [post]
|
||||||
func (b *BaseApi) BatchOperateFilterRule(c *gin.Context) {
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新端口转发规则","formatEN":"update port forward rules"}
|
||||||
var req dto.IptablesBatchOperate
|
func (b *BaseApi) OperateForwardingRules(c *gin.Context) {
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
var request dto.ForwardRuleOperate
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := iptablesService.BatchOperate(req); err != nil {
|
result, err := forwardingService.OperateRules(request)
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
helper.Success(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Apply/Unload/Init iptables filter
|
// @Summary Enable forwarding
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.IptablesOp true "request"
|
// @Param request body dto.FirewallInitializationTask true "request"
|
||||||
// @Success 200
|
// @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 ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/filter/operate [post]
|
// @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) {
|
func (b *BaseApi) OperateFilterChain(c *gin.Context) {
|
||||||
var req dto.IptablesOp
|
var request dto.FilterChainOperation
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
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)
|
helper.InternalServer(c, err)
|
||||||
return
|
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 Adopt an external firewall rule
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallRuleAdopt true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Failure 400 {object} dto.Response
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/rules/adopt [post]
|
||||||
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"纳管防火墙规则","formatEN":"adopt firewall rule"}
|
||||||
|
func (b *BaseApi) AdoptFirewallRule(c *gin.Context) {
|
||||||
|
var request dto.FirewallRuleAdopt
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := firewallService.Adopt(c.Request.Context(), request); err != nil {
|
||||||
|
handleFirewallRuleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary load chain status with name
|
// @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
|
// @Accept json
|
||||||
// @Param request body dto.OperationWithName true "request"
|
// @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 Preview firewall rule synchronization
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallRuleSyncRequest true "request"
|
||||||
|
// @Success 200 {object} dto.FirewallRuleSyncPreview
|
||||||
|
// @Failure 400 {object} dto.Response
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/rules/sync/preview [post]
|
||||||
|
func (b *BaseApi) PreviewFirewallRuleSync(c *gin.Context) {
|
||||||
|
var request dto.FirewallRuleSyncRequest
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := firewallService.PreviewRuleSync(c.Request.Context(), c.ClientIP(), request)
|
||||||
|
if err != nil {
|
||||||
|
handleFirewallRuleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Load the currently executing firewall rule synchronization task
|
||||||
|
// @Success 200 {object} dto.FirewallRuleSyncTask
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/rules/sync/task [get]
|
||||||
|
func (b *BaseApi) LoadFirewallRuleSyncTask(c *gin.Context) {
|
||||||
|
result, err := firewallService.CurrentRuleSyncTask()
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Synchronize firewall rules to a target backend
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallRuleSyncRequest true "request"
|
||||||
|
// @Success 200 {object} dto.FirewallRuleSyncResult
|
||||||
|
// @Failure 400 {object} dto.Response
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/rules/sync [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["subsystem","sourceProvider","targetProvider"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 [subsystem] 防火墙规则到 [targetProvider]","formatEN":"sync [subsystem] firewall rules to [targetProvider]"}
|
||||||
|
func (b *BaseApi) SyncFirewallRules(c *gin.Context) {
|
||||||
|
var request dto.FirewallRuleSyncRequest
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := firewallService.SyncRules(c.Request.Context(), c.ClientIP(), request)
|
||||||
|
if err != nil {
|
||||||
|
handleFirewallRuleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Queue firewall rule deletion
|
||||||
|
// @Description Deletes managed rules by UUID or unprotected before-chain rules by 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 managed unified firewall v2 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":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [uuid]","formatEN":"update firewall rule [uuid]"}
|
||||||
|
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||||
|
var request dto.FirewallRuleUpdate
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !normalizeFirewallRuleUUID(c, &request.UUID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := firewallService.Update(c.Request.Context(), c.ClientIP(), request); err != nil {
|
||||||
|
handleFirewallRuleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Reorder a managed unified firewall v2 rule
|
||||||
|
// @Accept json
|
||||||
|
// @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":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [uuid]","formatEN":"reorder firewall rule [uuid]"}
|
||||||
|
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
|
||||||
|
var request dto.FirewallRuleReorder
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !normalizeFirewallRuleUUID(c, &request.UUID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := firewallService.Reorder(c.Request.Context(), c.ClientIP(), request); err != nil {
|
||||||
|
handleFirewallRuleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeFirewallRuleUUID(c *gin.Context, value *string) bool {
|
||||||
|
if value == nil {
|
||||||
|
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
*value = strings.TrimSpace(*value)
|
||||||
|
if *value == "" {
|
||||||
|
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleFirewallRuleError(c *gin.Context, err error) {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, filter.ErrProtectedRule):
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
|
||||||
|
case errors.Is(err, filter.ErrRuleStale):
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err)
|
||||||
|
case errors.Is(err, repo.ErrFirewallRuleRevisionConflict):
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_REVISION_CONFLICT", "ErrInvalidParams", err)
|
||||||
|
case errors.Is(err, filter.ErrManagedScopeChange):
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrFirewallRuleScopeChange", err)
|
||||||
|
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
|
||||||
|
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||||
|
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation), errors.Is(err, filter.ErrRuleConflict),
|
||||||
|
errors.Is(err, repo.ErrFirewallPersistenceInvalid):
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||||
|
case errors.Is(err, filter.ErrVerificationFailed):
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err)
|
||||||
|
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 only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallPortWhitelistCreate true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/filter/chain/status [post]
|
// @Router /hosts/firewall/settings/whitelist [post]
|
||||||
func (b *BaseApi) LoadChainStatus(c *gin.Context) {
|
// @x-panel-log {"bodyKeys":["rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建防火墙端口白名单","formatEN":"create firewall port whitelist"}
|
||||||
var req dto.OperationWithName
|
func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) {
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
var request dto.FirewallPortWhitelistCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
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 only. Missing rules are added on startup, restart, initialization, or synchronization; 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 Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing 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 {
|
||||||
|
if errors.Is(err, service.ErrFirewallBackendCleanupRequired) {
|
||||||
|
helper.ErrorWithBusinessCode(
|
||||||
|
c,
|
||||||
|
http.StatusConflict,
|
||||||
|
"FW_BACKEND_CLEANUP_REQUIRED",
|
||||||
|
"ErrInvalidParams",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
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 Sync Docker port guard rules
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/docker/sync [post]
|
||||||
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Docker 端口防护规则","formatEN":"sync Docker port guard rules"}
|
||||||
|
func (b *BaseApi) SyncDockerPortGuard(c *gin.Context) {
|
||||||
|
if err := dockerPortGuardService.Reconcile(c.Request.Context()); err != nil {
|
||||||
|
handleDockerPortGuardError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Operate Docker port guard
|
||||||
|
// @Accept json
|
||||||
|
// @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) {
|
||||||
|
if errors.Is(err, service.ErrDockerIptablesChainUnavailable) {
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE", "ErrDockerIptablesChainUnavailable", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errors.Is(err, service.ErrDockerNftablesChainUnavailable) {
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_NFTABLES_CHAIN_UNAVAILABLE", "ErrDockerNftablesChainUnavailable", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errors.Is(err, service.ErrDockerGuardInvalid) {
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID", "ErrInvalidParams", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errors.Is(err, service.ErrDockerUnavailable) {
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE", "ErrDockerFailed", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-16
@@ -3,9 +3,8 @@ package v2
|
|||||||
import (
|
import (
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu/common"
|
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,27 +16,20 @@ import (
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /ai/gpu/load [get]
|
// @Router /ai/gpu/load [get]
|
||||||
func (b *BaseApi) LoadGpuInfo(c *gin.Context) {
|
func (b *BaseApi) LoadGpuInfo(c *gin.Context) {
|
||||||
ok, client := gpu.New()
|
ok, client := accelerator.New()
|
||||||
if ok {
|
if ok {
|
||||||
info, err := client.LoadGpuInfo()
|
snapshot, err := client.Collect(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, info)
|
if warning := snapshot.Warning(); warning != nil {
|
||||||
return
|
global.LOG.Warnf("load realtime accelerator data partially failed, err: %v", warning)
|
||||||
}
|
|
||||||
xpuOK, xpuClient := xpu.New()
|
|
||||||
if xpuOK {
|
|
||||||
info, err := xpuClient.LoadGpuInfo()
|
|
||||||
if err != nil {
|
|
||||||
helper.BadRequest(c, err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, info)
|
helper.SuccessWithData(c, &snapshot.Info)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, &common.GpuInfo{})
|
helper.SuccessWithData(c, &accelerator.Info{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
|
|||||||
@@ -30,6 +30,26 @@ func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
|||||||
ctx.Abort()
|
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) {
|
func InternalServer(ctx *gin.Context, err error) {
|
||||||
ErrorWithDetail(ctx, http.StatusInternalServerError, "ErrInternalServer", err)
|
ErrorWithDetail(ctx, http.StatusInternalServerError, "ErrInternalServer", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+143
-31
@@ -1,11 +1,14 @@
|
|||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
@@ -19,29 +22,35 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
gossh "golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @Tags Terminal
|
// @Tags Terminal
|
||||||
// @Summary Ws local terminal
|
// @Summary Ws local terminal
|
||||||
// @Param command query string false "command"
|
// @Param command query string false "command"
|
||||||
|
// @Param session query string false "session id to reattach"
|
||||||
|
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/terminal/local [get]
|
// @Router /hosts/terminal/local [get]
|
||||||
func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
|
func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
|
||||||
b.runSSHSession(c, loadLocalConn, c.DefaultQuery("command", ""))
|
b.runSSHSession(c, "local", loadLocalConn, c.DefaultQuery("command", ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Terminal
|
// @Tags Terminal
|
||||||
// @Summary Ws host SSH
|
// @Summary Ws host SSH
|
||||||
// @Param id query integer false "id"
|
// @Param id query integer false "id"
|
||||||
// @Param command query string false "command"
|
// @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
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/terminal/ssh [get]
|
// @Router /hosts/terminal/ssh [get]
|
||||||
func (b *BaseApi) WsHostSSH(c *gin.Context) {
|
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"))
|
hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0"))
|
||||||
if hostID <= 0 {
|
if hostID <= 0 {
|
||||||
return nil, errors.New("missing host id")
|
return nil, errors.New("missing host id")
|
||||||
@@ -65,26 +74,33 @@ func (b *BaseApi) WsContainerTerminal(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer wsConn.Close()
|
defer wsConn.Close()
|
||||||
|
identity, ok := loadTerminalIdentity(c)
|
||||||
slave, err := loadContainerTerminalCommand(c)
|
if !ok {
|
||||||
if wshandleError(wsConn, err) {
|
_ = wshandleError(wsConn, errors.New("missing terminal identity"))
|
||||||
return
|
|
||||||
}
|
|
||||||
defer slave.Close()
|
|
||||||
|
|
||||||
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
|
|
||||||
if wshandleError(wsConn, err) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
quitChan := make(chan bool, 3)
|
opts := terminal.SessionOptions{
|
||||||
tty.Start(quitChan)
|
Identity: identity,
|
||||||
go slave.Wait(quitChan)
|
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
|
func containerTerminalTarget(c *gin.Context) string {
|
||||||
|
query := c.Request.URL.Query()
|
||||||
global.LOG.Info("websocket finished")
|
for _, key := range []string{"cols", "rows", "session", "terminalRevalidate"} {
|
||||||
closeTerminalConn(wsConn)
|
query.Del(key)
|
||||||
|
}
|
||||||
|
sum := sha256.Sum256([]byte(query.Encode()))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
|
func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
|
||||||
@@ -115,32 +131,128 @@ func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
|
|||||||
return wsConn, cols, rows, true
|
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)
|
wsConn, cols, rows, ok := prepareTerminalSession(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer wsConn.Close()
|
defer wsConn.Close()
|
||||||
|
identity, ok := loadTerminalIdentity(c)
|
||||||
client, clientErr := connect()
|
if !ok {
|
||||||
if wshandleError(wsConn, errors.WithMessage(clientErr, "failed to set up the connection. Please check the host information")) {
|
_ = wshandleError(wsConn, errors.New("missing terminal identity"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, command)
|
hostID := 0
|
||||||
if wshandleError(wsConn, err) {
|
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
|
return
|
||||||
}
|
}
|
||||||
defer sws.Close()
|
helper.SuccessWithData(c, terminal.List(identity))
|
||||||
|
}
|
||||||
|
|
||||||
quitChan := make(chan bool, 3)
|
// @Tags Terminal
|
||||||
sws.Start(quitChan)
|
// @Summary Close a terminal session
|
||||||
go sws.Wait(quitChan)
|
// @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)),
|
||||||
|
}
|
||||||
|
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) {
|
func closeTerminalConn(wsConn *websocket.Conn) {
|
||||||
|
|||||||
@@ -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 {
|
type AgentModelConfigUpdateReq struct {
|
||||||
AgentID uint `json:"agentId" validate:"required"`
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
AccountID uint `json:"accountId" validate:"required"`
|
AccountID uint `json:"accountId" validate:"required"`
|
||||||
Model string `json:"model" validate:"required"`
|
Model string `json:"model" validate:"required"`
|
||||||
Fallbacks []string `json:"fallbacks"`
|
Fallbacks []string `json:"fallbacks"`
|
||||||
|
Metadata []AgentModelMetadata `json:"metadata" validate:"dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentModelConfig struct {
|
type AgentModelConfig struct {
|
||||||
AccountID uint `json:"accountId"`
|
AccountID uint `json:"accountId"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Fallbacks []string `json:"fallbacks"`
|
Fallbacks []string `json:"fallbacks"`
|
||||||
|
Metadata []AgentModelMetadata `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentModelMetadata struct {
|
||||||
|
Model string `json:"model" validate:"required"`
|
||||||
|
InputMode string `json:"inputMode" validate:"required,oneof=auto text image"`
|
||||||
|
ContextWindow int `json:"contextWindow" validate:"min=0"`
|
||||||
|
MaxTokens int `json:"maxTokens" validate:"min=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentHermesChatSessionItem struct {
|
type AgentHermesChatSessionItem struct {
|
||||||
@@ -322,29 +331,31 @@ type AgentAccountModelDeleteReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountCreateReq struct {
|
type AgentAccountCreateReq struct {
|
||||||
Provider string `json:"provider" validate:"required"`
|
Provider string `json:"provider" validate:"required"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
APIKey string `json:"apiKey" validate:"required"`
|
APIKey string `json:"apiKey" validate:"required"`
|
||||||
RememberAPIKey bool `json:"rememberApiKey"`
|
RememberAPIKey bool `json:"rememberApiKey"`
|
||||||
BaseURL string `json:"baseURL"`
|
BaseURL string `json:"baseURL"`
|
||||||
Models []AgentAccountModel `json:"models"`
|
Models []AgentAccountModel `json:"models"`
|
||||||
APIType string `json:"apiType" validate:"required"`
|
APIType string `json:"apiType" validate:"required"`
|
||||||
AuthMode string `json:"authMode"`
|
AuthMode string `json:"authMode"`
|
||||||
VerifyModel string `json:"verifyModel"`
|
VerifyModel string `json:"verifyModel"`
|
||||||
Remark string `json:"remark"`
|
ValidateAvailability *bool `json:"validateAvailability"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountUpdateReq struct {
|
type AgentAccountUpdateReq struct {
|
||||||
ID uint `json:"id" validate:"required"`
|
ID uint `json:"id" validate:"required"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
APIKey string `json:"apiKey" validate:"required"`
|
APIKey string `json:"apiKey" validate:"required"`
|
||||||
RememberAPIKey bool `json:"rememberApiKey"`
|
RememberAPIKey bool `json:"rememberApiKey"`
|
||||||
BaseURL string `json:"baseURL"`
|
BaseURL string `json:"baseURL"`
|
||||||
APIType string `json:"apiType" validate:"required"`
|
APIType string `json:"apiType" validate:"required"`
|
||||||
AuthMode string `json:"authMode"`
|
AuthMode string `json:"authMode"`
|
||||||
VerifyModel string `json:"verifyModel"`
|
VerifyModel string `json:"verifyModel"`
|
||||||
Remark string `json:"remark"`
|
ValidateAvailability *bool `json:"validateAvailability"`
|
||||||
SyncAgents bool `json:"syncAgents"`
|
Remark string `json:"remark"`
|
||||||
|
SyncAgents bool `json:"syncAgents"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountVerifyReq struct {
|
type AgentAccountVerifyReq struct {
|
||||||
@@ -363,6 +374,8 @@ type AgentAccountDeleteReq struct {
|
|||||||
type AgentAccountSearch struct {
|
type AgentAccountSearch struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
|
APIType string `json:"apiType"`
|
||||||
|
TextOnly bool `json:"textOnly"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,11 +407,13 @@ type ProviderModelInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProviderAPIInfo struct {
|
type ProviderAPIInfo struct {
|
||||||
APIType string `json:"apiType"`
|
APIType string `json:"apiType"`
|
||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
EditableBaseURL bool `json:"editableBaseUrl"`
|
EditableBaseURL bool `json:"editableBaseUrl"`
|
||||||
DefaultAuthMode string `json:"defaultAuthMode"`
|
SupportsModelDiscovery bool `json:"supportsModelDiscovery"`
|
||||||
AuthModes []string `json:"authModes"`
|
DefaultAuthMode string `json:"defaultAuthMode"`
|
||||||
|
AuthModes []string `json:"authModes"`
|
||||||
|
Models []ProviderModelInfo `json:"models"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderInfo struct {
|
type ProviderInfo struct {
|
||||||
@@ -598,6 +613,52 @@ type AgentPluginStatus struct {
|
|||||||
Upgradable bool `json:"upgradable"`
|
Upgradable bool `json:"upgradable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AgentPluginsReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginSearchReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
Keyword string `json:"keyword" validate:"required,max=100"`
|
||||||
|
Limit int `json:"limit" validate:"omitempty,min=1,max=100"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginMarketInstallReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
Package string `json:"package" validate:"required,max=200"`
|
||||||
|
Version string `json:"version" validate:"required,max=100"`
|
||||||
|
TaskID string `json:"taskID" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginOperateReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
PluginID string `json:"pluginId" validate:"required,max=200"`
|
||||||
|
Operate string `json:"operate" validate:"required,oneof=enable disable update uninstall"`
|
||||||
|
TaskID string `json:"taskID" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginSearchItem struct {
|
||||||
|
Package string `json:"package"`
|
||||||
|
PluginID string `json:"pluginId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
VerificationTier string `json:"verificationTier"`
|
||||||
|
Categories []string `json:"categories"`
|
||||||
|
Official bool `json:"official"`
|
||||||
|
Downloads int64 `json:"downloads"`
|
||||||
|
Score float64 `json:"score"`
|
||||||
|
}
|
||||||
|
|
||||||
type AgentDiscordConfigUpdateReq struct {
|
type AgentDiscordConfigUpdateReq struct {
|
||||||
AgentID uint `json:"agentId" validate:"required"`
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
@@ -678,9 +739,9 @@ type AgentSecurityConfig struct {
|
|||||||
|
|
||||||
type AgentOtherConfigUpdateReq struct {
|
type AgentOtherConfigUpdateReq struct {
|
||||||
AgentID uint `json:"agentId" validate:"required"`
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
UserTimezone string `json:"userTimezone" validate:"required"`
|
UserTimezone string `json:"userTimezone"`
|
||||||
BrowserEnabled bool `json:"browserEnabled"`
|
BrowserEnabled bool `json:"browserEnabled"`
|
||||||
NPMRegistry string `json:"npmRegistry" validate:"required"`
|
NPMRegistry string `json:"npmRegistry"`
|
||||||
DashboardUsername string `json:"dashboardUsername"`
|
DashboardUsername string `json:"dashboardUsername"`
|
||||||
DashboardPassword string `json:"dashboardPassword"`
|
DashboardPassword string `json:"dashboardPassword"`
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-18
@@ -113,8 +113,10 @@ type DiskDTO struct {
|
|||||||
|
|
||||||
type AlertLogSearch struct {
|
type AlertLogSearch struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
Count uint `json:"count"`
|
Count uint `json:"count"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
StartTime time.Time `json:"startTime"`
|
||||||
|
EndTime time.Time `json:"endTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertLogDTO struct {
|
type AlertLogDTO struct {
|
||||||
@@ -149,16 +151,25 @@ type AlertLog struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AlertDetail struct {
|
type AlertDetail struct {
|
||||||
LicenseId string `json:"licenseId"`
|
LicenseId string `json:"licenseId"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
SubType string `json:"subType"`
|
SubType string `json:"subType"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
LicenseCode string `json:"licenseCode"`
|
LicenseCode string `json:"licenseCode"`
|
||||||
DeviceId string `json:"deviceId"`
|
DeviceId string `json:"deviceId"`
|
||||||
Project string `json:"project"`
|
Project string `json:"project"`
|
||||||
Params []Param `json:"params"`
|
Params []Param `json:"params"`
|
||||||
Phone string `json:"phone"`
|
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 {
|
type AlertRule struct {
|
||||||
@@ -293,15 +304,19 @@ type OfflineQueryRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AlertConfigUpdate struct {
|
type AlertConfigUpdate struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Config string `json:"config"`
|
Config string `json:"config"`
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
|
Revision *time.Time `json:"revision"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertConfigTest struct {
|
type AlertConfigTest struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Config string `json:"config"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Sender string `json:"sender"`
|
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 {
|
type SearchWithPage struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
Info string `json:"info"`
|
Info string `json:"info"`
|
||||||
|
ExcludeAppStore bool `json:"excludeAppStore"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchPageWithType struct {
|
type SearchPageWithType struct {
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ type PageResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Response struct {
|
type Response struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
Message string `json:"message"`
|
ErrorCode string `json:"errorCode,omitempty"`
|
||||||
Data interface{} `json:"data"`
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
|
|||||||
@@ -300,6 +300,7 @@ type ComposeInfo struct {
|
|||||||
ConfigFile string `json:"configFile"`
|
ConfigFile string `json:"configFile"`
|
||||||
Workdir string `json:"workdir"`
|
Workdir string `json:"workdir"`
|
||||||
ComposeFileExists bool `json:"composeFileExists"`
|
ComposeFileExists bool `json:"composeFileExists"`
|
||||||
|
IsPinned bool `json:"isPinned"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Containers []ComposeContainer `json:"containers"`
|
Containers []ComposeContainer `json:"containers"`
|
||||||
Env string `json:"env"`
|
Env string `json:"env"`
|
||||||
@@ -314,6 +315,7 @@ type ComposeContainer struct {
|
|||||||
type ComposeCreate struct {
|
type ComposeCreate struct {
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
DirName string `json:"dirName"`
|
||||||
From string `json:"from" validate:"required,oneof=edit path template"`
|
From string `json:"from" validate:"required,oneof=edit path template"`
|
||||||
File string `json:"file"`
|
File string `json:"file"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
@@ -337,6 +339,10 @@ type ComposeUpdate struct {
|
|||||||
Env string `json:"env"`
|
Env string `json:"env"`
|
||||||
ForcePull bool `json:"forcePull"`
|
ForcePull bool `json:"forcePull"`
|
||||||
}
|
}
|
||||||
|
type ComposePin struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
IsPinned bool `json:"isPinned"`
|
||||||
|
}
|
||||||
type ComposeLogClean struct {
|
type ComposeLogClean struct {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Path string `json:"path" validate:"required"`
|
Path string `json:"path" validate:"required"`
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ type SearchRecord struct {
|
|||||||
|
|
||||||
type Record struct {
|
type Record struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
|
CronjobID uint `json:"cronjobID"`
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
StartTime string `json:"startTime"`
|
StartTime string `json:"startTime"`
|
||||||
Records string `json:"records"`
|
Records string `json:"records"`
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ type DashboardCurrent struct {
|
|||||||
NetBytesRecv uint64 `json:"netBytesRecv"`
|
NetBytesRecv uint64 `json:"netBytesRecv"`
|
||||||
|
|
||||||
GPUData []GPUInfo `json:"gpuData"`
|
GPUData []GPUInfo `json:"gpuData"`
|
||||||
|
NPUData []NPUInfo `json:"npuData"`
|
||||||
XPUData []XPUInfo `json:"xpuData"`
|
XPUData []XPUInfo `json:"xpuData"`
|
||||||
|
|
||||||
TopCPUItems []Process `json:"topCPUItems"`
|
TopCPUItems []Process `json:"topCPUItems"`
|
||||||
@@ -156,8 +157,12 @@ type DiskInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GPUInfo struct {
|
type GPUInfo struct {
|
||||||
|
Type string `json:"type"`
|
||||||
Index uint `json:"index"`
|
Index uint `json:"index"`
|
||||||
|
NPUIndex uint `json:"npuIndex"`
|
||||||
|
ChipIndex uint `json:"chipIndex"`
|
||||||
ProductName string `json:"productName"`
|
ProductName string `json:"productName"`
|
||||||
|
BusID string `json:"busID"`
|
||||||
GPUUtil string `json:"gpuUtil"`
|
GPUUtil string `json:"gpuUtil"`
|
||||||
Temperature string `json:"temperature"`
|
Temperature string `json:"temperature"`
|
||||||
PerformanceState string `json:"performanceState"`
|
PerformanceState string `json:"performanceState"`
|
||||||
@@ -170,6 +175,27 @@ type GPUInfo struct {
|
|||||||
FanSpeed string `json:"fanSpeed"`
|
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 {
|
type AppLauncher struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -202,11 +228,13 @@ type LauncherOption struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type XPUInfo struct {
|
type XPUInfo struct {
|
||||||
DeviceID int `json:"deviceID"`
|
DeviceID int `json:"deviceID"`
|
||||||
DeviceName string `json:"deviceName"`
|
DeviceName string `json:"deviceName"`
|
||||||
Memory string `json:"memory"`
|
PciBdfAddress string `json:"pciBdfAddress"`
|
||||||
Temperature string `json:"temperature"`
|
Memory string `json:"memory"`
|
||||||
MemoryUsed string `json:"memoryUsed"`
|
Temperature string `json:"temperature"`
|
||||||
Power string `json:"power"`
|
GPUUtil string `json:"gpuUtil"`
|
||||||
MemoryUtil string `json:"memoryUtil"`
|
MemoryUsed string `json:"memoryUsed"`
|
||||||
|
Power string `json:"power"`
|
||||||
|
MemoryUtil string `json:"memoryUtil"`
|
||||||
}
|
}
|
||||||
|
|||||||
+356
-84
@@ -1,113 +1,385 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
type FirewallBaseInfo struct {
|
import (
|
||||||
Name string `json:"name"`
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||||
IsExist bool `json:"isExist"`
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||||
IsActive bool `json:"isActive"`
|
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
|
||||||
IsInit bool `json:"isInit"`
|
)
|
||||||
IsBind bool `json:"isBind"`
|
|
||||||
Version string `json:"version"`
|
type FirewallSubsystemStatus struct {
|
||||||
PingStatus string `json:"pingStatus"`
|
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"`
|
||||||
|
SyncError string `json:"syncError,omitempty"`
|
||||||
|
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
|
||||||
|
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
||||||
|
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuleSearch struct {
|
type FirewallLifecycleOperation struct {
|
||||||
PageInfo
|
|
||||||
Info string `json:"info"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Strategy string `json:"strategy"`
|
|
||||||
Type string `json:"type" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallOperation struct {
|
|
||||||
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
|
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
|
||||||
WithDockerRestart bool `json:"withDockerRestart"`
|
WithDockerRestart bool `json:"withDockerRestart"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PortRuleOperate struct {
|
type FirewallLifecycleOperationResponse struct {
|
||||||
ID uint `json:"id"`
|
TaskID string `json:"taskID,omitempty"`
|
||||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
Queued bool `json:"queued"`
|
||||||
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 ForwardRuleOperate struct {
|
type FirewallBackendOption struct {
|
||||||
ForceDelete bool `json:"forceDelete"`
|
Name string `json:"name"`
|
||||||
Rules []struct {
|
Installed bool `json:"installed"`
|
||||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
Active bool `json:"active"`
|
||||||
Num string `json:"num"`
|
Initialized bool `json:"initialized"`
|
||||||
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
|
Bound bool `json:"bound"`
|
||||||
Interface string `json:"interface"`
|
Supported bool `json:"supported"`
|
||||||
Port string `json:"port" validate:"required"`
|
SupportReason string `json:"supportReason,omitempty"`
|
||||||
TargetIP string `json:"targetIP"`
|
Implementation string `json:"implementation,omitempty"`
|
||||||
TargetPort string `json:"targetPort" validate:"required"`
|
Message string `json:"message,omitempty"`
|
||||||
} `json:"rules"`
|
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
||||||
|
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateFirewallDescription struct {
|
type FirewallBackendFamilyStatus struct {
|
||||||
Type string `json:"type"`
|
Available bool `json:"available"`
|
||||||
Chain string `json:"chain"`
|
Initialized bool `json:"initialized"`
|
||||||
SrcIP string `json:"srcIP"`
|
Bound bool `json:"bound"`
|
||||||
DstIP string `json:"dstIP"`
|
Reason string `json:"reason,omitempty"`
|
||||||
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 AddrRuleOperate struct {
|
type FirewallBackendGroup struct {
|
||||||
ID uint `json:"id"`
|
Selected string `json:"selected"`
|
||||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
Current string `json:"current,omitempty"`
|
||||||
Address string `json:"address" validate:"required"`
|
Options []FirewallBackendOption `json:"options"`
|
||||||
Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
|
|
||||||
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PortRuleUpdate struct {
|
type FirewallSettings struct {
|
||||||
OldRule PortRuleOperate `json:"oldRule"`
|
System FirewallBackendGroup `json:"system"`
|
||||||
NewRule PortRuleOperate `json:"newRule"`
|
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 {
|
type FirewallPortWhitelistCreate struct {
|
||||||
OldRule AddrRuleOperate `json:"oldRule"`
|
Rule filter.PortWhitelist `json:"rule" validate:"required"`
|
||||||
NewRule AddrRuleOperate `json:"newRule"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BatchRuleOperate struct {
|
type FirewallPortWhitelistUpdate struct {
|
||||||
Type string `json:"type" validate:"required"`
|
OldRule filter.PortWhitelist `json:"oldRule" validate:"required"`
|
||||||
Rules []PortRuleOperate `json:"rules"`
|
Rule filter.PortWhitelist `json:"rule" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type IptablesOp struct {
|
type FirewallPortWhitelistDelete struct {
|
||||||
Name string `json:"name" validate:"required,oneof=1PANEL_INPUT 1PANEL_OUTPUT 1PANEL_BASIC"`
|
Rule *filter.PortWhitelist `json:"rule" validate:"required"`
|
||||||
Operate string `json:"operate" validate:"required,oneof=init-base init-forward init-advance bind-base unbind-base bind unbind"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type IptablesRuleOp struct {
|
type FirewallBackendOperation struct {
|
||||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
Subsystem string `json:"subsystem" validate:"required,oneof=system forwarding docker"`
|
||||||
ID uint `json:"id"`
|
Backend string `json:"backend" validate:"required,oneof=firewalld ufw iptables nftables"`
|
||||||
Chain string `json:"chain" validate:"required,oneof=1PANEL_BASIC 1PANEL_BASIC_BEFORE 1PANEL_INPUT 1PANEL_OUTPUT"`
|
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"`
|
||||||
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 IptablesBatchOperate struct {
|
type FilterChainOperation struct {
|
||||||
Rules []IptablesRuleOp `json:"rules"`
|
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 IptablesChainStatus struct {
|
type FilterChainOperationResponse struct {
|
||||||
IsBind bool `json:"isBind"`
|
TaskID string `json:"taskID"`
|
||||||
DefaultStrategy string `json:"defaultStrategy"`
|
Queued bool `json:"queued"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallInitializationTask struct {
|
||||||
|
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"`
|
||||||
|
ManagedTotal int64 `json:"managedTotal"`
|
||||||
|
Items []filter.InventoryItem `json:"items"`
|
||||||
|
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleResetResponse struct {
|
||||||
|
Removed int `json:"removed"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleReset struct {
|
||||||
|
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
||||||
|
WithDockerRestart bool `json:"withDockerRestart"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleInventory struct {
|
||||||
|
Refresh bool `json:"refresh,omitempty"`
|
||||||
|
PageInfo
|
||||||
|
Scope filter.Scope `json:"scope,omitempty"`
|
||||||
|
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
|
||||||
|
All bool `json:"all,omitempty"`
|
||||||
|
Info string `json:"info"`
|
||||||
|
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
|
||||||
|
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
|
||||||
|
States []filter.InventoryState `json:"states,omitempty" validate:"omitempty,dive,oneof=managed adopted external drifted protected"`
|
||||||
|
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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"`
|
||||||
|
NativeAction string `json:"nativeAction,omitempty"`
|
||||||
|
ReadOnly bool `json:"readOnly,omitempty"`
|
||||||
|
Sources []string `json:"sources"`
|
||||||
|
Effective bool `json:"effective"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
TrafficPath string `json:"trafficPath"`
|
||||||
|
ManagementTarget string `json:"managementTarget"`
|
||||||
|
ManagementReason string `json:"managementReason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
Sources []string `json:"sources" validate:"dive,required,max=64"`
|
||||||
|
Description string `json:"description" validate:"max=256"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerPortGuardOperation struct {
|
||||||
|
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
|
||||||
|
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleAdopt struct {
|
||||||
|
Scope filter.Scope `json:"scope" validate:"required"`
|
||||||
|
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleCreateItem struct {
|
||||||
|
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
||||||
|
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
|
||||||
|
SourceID string `json:"sourceID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleCreate struct {
|
||||||
|
Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,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 FirewallRuleSyncRequest struct {
|
||||||
|
Subsystem string `json:"subsystem" validate:"omitempty,oneof=system forwarding docker"`
|
||||||
|
SourceProvider filter.Provider `json:"sourceProvider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
||||||
|
TargetProvider filter.Provider `json:"targetProvider" validate:"required,oneof=firewalld ufw iptables nftables"`
|
||||||
|
ResetSource bool `json:"resetSource"`
|
||||||
|
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleSyncItem struct {
|
||||||
|
SourceUUID string `json:"sourceUUID"`
|
||||||
|
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
||||||
|
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
|
||||||
|
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
|
||||||
|
Status firewallsync.Status `json:"status"`
|
||||||
|
ReasonCode firewallsync.ReasonCode `json:"reasonCode,omitempty"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleSyncPreview struct {
|
||||||
|
Subsystem string `json:"subsystem"`
|
||||||
|
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
|
||||||
|
TargetProvider filter.Provider `json:"targetProvider"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Ready int `json:"ready"`
|
||||||
|
Existing int `json:"existing"`
|
||||||
|
Removed int `json:"removed"`
|
||||||
|
Blocked int `json:"blocked"`
|
||||||
|
Items []FirewallRuleSyncItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleSyncResult struct {
|
||||||
|
Subsystem string `json:"subsystem"`
|
||||||
|
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
|
||||||
|
TargetProvider filter.Provider `json:"targetProvider"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Succeeded int `json:"succeeded"`
|
||||||
|
Skipped int `json:"skipped"`
|
||||||
|
Removed int `json:"removed"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
Errors []FirewallRuleSyncFailure `json:"errors,omitempty"`
|
||||||
|
TaskID string `json:"taskID,omitempty"`
|
||||||
|
Queued bool `json:"queued,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleSyncTask struct {
|
||||||
|
TaskID string `json:"taskID,omitempty"`
|
||||||
|
Executing bool `json:"executing"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleSyncFailure struct {
|
||||||
|
SourceUUID string `json:"sourceUUID"`
|
||||||
|
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
||||||
|
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
|
||||||
|
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleDelete struct {
|
||||||
|
UUIDs []string `json:"uuids" validate:"omitempty,dive,required,max=64"`
|
||||||
|
BeforeRules []FirewallRuleDeleteTarget `json:"beforeRules,omitempty" validate:"omitempty,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleDeleteTarget struct {
|
||||||
|
Scope filter.Scope `json:"scope" validate:"required"`
|
||||||
|
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleDeleteResponse struct {
|
||||||
|
TaskID string `json:"taskID,omitempty"`
|
||||||
|
Queued bool `json:"queued,omitempty"`
|
||||||
|
Succeeded int `json:"succeeded"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
Errors []FirewallRuleDeleteFailure `json:"errors,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleDeleteFailure struct {
|
||||||
|
Index int `json:"index"`
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleUpdate struct {
|
||||||
|
UUID string `json:"uuid" validate:"required,max=64"`
|
||||||
|
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 {
|
||||||
|
UUID string `json:"uuid" validate:"required,max=64"`
|
||||||
|
TargetPosition *int64 `json:"targetPosition"`
|
||||||
|
Priority *int `json:"priority"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *FirewallRuleSyncPreview) Add(item FirewallRuleSyncItem) {
|
||||||
|
p.Items = append(p.Items, item)
|
||||||
|
switch item.Status {
|
||||||
|
case firewallsync.StatusReady:
|
||||||
|
p.Ready++
|
||||||
|
p.Total++
|
||||||
|
case firewallsync.StatusExisting:
|
||||||
|
p.Existing++
|
||||||
|
p.Total++
|
||||||
|
case firewallsync.StatusRemove:
|
||||||
|
p.Removed++
|
||||||
|
case firewallsync.StatusBlocked:
|
||||||
|
p.Blocked++
|
||||||
|
if item.ReasonCode != firewallsync.ReasonReadOnlyRule {
|
||||||
|
p.Total++
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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"`
|
||||||
|
|
||||||
|
IsDesired bool `json:"isDesired"`
|
||||||
|
IsRuntime bool `json:"isRuntime"`
|
||||||
|
SyncStatus string `json:"syncStatus"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardRuleOperate struct {
|
||||||
|
ForceDelete bool `json:"forceDelete"`
|
||||||
|
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
@@ -45,10 +45,12 @@ type MonitorGPUOptions struct {
|
|||||||
}
|
}
|
||||||
type GPUChartHide struct {
|
type GPUChartHide struct {
|
||||||
ProductName string `json:"productName"`
|
ProductName string `json:"productName"`
|
||||||
|
Type string `json:"type"`
|
||||||
Process bool `json:"process"`
|
Process bool `json:"process"`
|
||||||
GPU bool `json:"gpu"`
|
GPU bool `json:"gpu"`
|
||||||
Memory bool `json:"memory"`
|
Memory bool `json:"memory"`
|
||||||
Power bool `json:"power"`
|
Power bool `json:"power"`
|
||||||
|
PowerLimit bool `json:"powerLimit"`
|
||||||
Temperature bool `json:"temperature"`
|
Temperature bool `json:"temperature"`
|
||||||
Speed bool `json:"speed"`
|
Speed bool `json:"speed"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,13 +51,19 @@ const (
|
|||||||
CACHE NginxKey = "cache"
|
CACHE NginxKey = "cache"
|
||||||
HttpPer NginxKey = "http-per"
|
HttpPer NginxKey = "http-per"
|
||||||
ProxyCache NginxKey = "proxy-cache"
|
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{
|
var ScopeKeyMap = map[NginxKey][]string{
|
||||||
Index: {"index"},
|
Index: {"index"},
|
||||||
LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"},
|
LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"},
|
||||||
SSL: {"ssl_certificate", "ssl_certificate_key"},
|
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 {
|
var StaticFileKeyMap = map[NginxKey]struct {
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ type AppContainerConfig struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
SpecifyIP string `json:"specifyIP"`
|
SpecifyIP string `json:"specifyIP"`
|
||||||
RestartPolicy string `json:"restartPolicy" validate:"omitempty,oneof=always unless-stopped no on-failure"`
|
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 {
|
type AppInstalledSearch struct {
|
||||||
@@ -92,6 +96,8 @@ type AppInstalledOperate struct {
|
|||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
DeleteImage bool `json:"deleteImage"`
|
DeleteImage bool `json:"deleteImage"`
|
||||||
Favorite bool `json:"favorite"`
|
Favorite bool `json:"favorite"`
|
||||||
|
|
||||||
|
UseLifecycleScripts bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppInstallUpgrade struct {
|
type AppInstallUpgrade struct {
|
||||||
@@ -111,11 +117,14 @@ type AppInstallDelete struct {
|
|||||||
DeleteDB bool `json:"deleteDB"`
|
DeleteDB bool `json:"deleteDB"`
|
||||||
DeleteImage bool `json:"deleteImage"`
|
DeleteImage bool `json:"deleteImage"`
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
|
|
||||||
|
UseLifecycleScripts bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppInstalledUpdate struct {
|
type AppInstalledUpdate struct {
|
||||||
InstallId uint `json:"installId" validate:"required"`
|
InstallId uint `json:"installId" validate:"required"`
|
||||||
Params map[string]interface{} `json:"params" validate:"required"`
|
Params map[string]interface{} `json:"params" validate:"required"`
|
||||||
|
TaskID string `json:"-"`
|
||||||
AppContainerConfig
|
AppContainerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ type FileWget struct {
|
|||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
IgnoreCertificate bool `json:"ignoreCertificate"`
|
IgnoreCertificate bool `json:"ignoreCertificate"`
|
||||||
UseProxy bool `json:"useProxy"`
|
UseProxy bool `json:"useProxy"`
|
||||||
|
UseServerFilename bool `json:"useServerFilename"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileMove struct {
|
type FileMove struct {
|
||||||
@@ -131,6 +132,11 @@ type FileMove struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Cover bool `json:"cover"`
|
Cover bool `json:"cover"`
|
||||||
CoverPaths []string `json:"coverPaths"`
|
CoverPaths []string `json:"coverPaths"`
|
||||||
|
TaskID string `json:"taskID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileMoveStopReq struct {
|
||||||
|
TaskID string `json:"taskID" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileDownload struct {
|
type FileDownload struct {
|
||||||
@@ -153,6 +159,10 @@ type FileProcessReq struct {
|
|||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FileProcessRemoveReq struct {
|
||||||
|
Keys []string `json:"keys" validate:"required,min=1,max=1000"`
|
||||||
|
}
|
||||||
|
|
||||||
type FileRoleUpdate struct {
|
type FileRoleUpdate struct {
|
||||||
Path string `json:"path" validate:"required"`
|
Path string `json:"path" validate:"required"`
|
||||||
User string `json:"user" validate:"required"`
|
User string `json:"user" validate:"required"`
|
||||||
|
|||||||
@@ -66,14 +66,15 @@ type RuntimeDelete struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeUpdate struct {
|
type RuntimeUpdate struct {
|
||||||
Name string `json:"name"`
|
AppDetailID uint `json:"appDetailId"`
|
||||||
ID uint `json:"id"`
|
Name string `json:"name"`
|
||||||
Image string `json:"image"`
|
ID uint `json:"id"`
|
||||||
Version string `json:"version"`
|
Image string `json:"image"`
|
||||||
Rebuild bool `json:"rebuild"`
|
Version string `json:"version"`
|
||||||
Source string `json:"source"`
|
Rebuild bool `json:"rebuild"`
|
||||||
CodeDir string `json:"codeDir"`
|
Source string `json:"source"`
|
||||||
Remark string `json:"remark"`
|
CodeDir string `json:"codeDir"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
|
||||||
Params map[string]interface{} `json:"params"`
|
Params map[string]interface{} `json:"params"`
|
||||||
NodeConfig
|
NodeConfig
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ type WebsiteCreate struct {
|
|||||||
|
|
||||||
SiteDir string `json:"siteDir"`
|
SiteDir string `json:"siteDir"`
|
||||||
|
|
||||||
|
TemplateOutputID uint `json:"templateOutputID"`
|
||||||
|
|
||||||
RuntimeConfig
|
RuntimeConfig
|
||||||
FtpConfig
|
FtpConfig
|
||||||
DataBaseConfig
|
DataBaseConfig
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebsiteTemplateSearch struct {
|
||||||
|
dto.PageInfo
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateCreate struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Type string `json:"type" validate:"required,oneof=single multi"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
Variables string `json:"variables"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateUpdate struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Type string `json:"type" validate:"required,oneof=single multi"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
Variables string `json:"variables"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateOutputSearch struct {
|
||||||
|
dto.PageInfo
|
||||||
|
TemplateID uint `json:"templateID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateOutputCreate struct {
|
||||||
|
TemplateID uint `json:"templateID" validate:"required"`
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
VariableValues map[string]string `json:"variableValues"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsitePreviewReq struct {
|
||||||
|
TemplateID uint `json:"templateID" validate:"required"`
|
||||||
|
VariableValues map[string]string `json:"variableValues"`
|
||||||
|
}
|
||||||
@@ -17,6 +17,17 @@ type NginxParam struct {
|
|||||||
Params []string `json:"params"`
|
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 {
|
type NginxAuthRes struct {
|
||||||
Enable bool `json:"enable"`
|
Enable bool `json:"enable"`
|
||||||
Items []dto.NginxAuth `json:"items"`
|
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 {
|
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"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
type Alert struct {
|
type Alert struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
|
|
||||||
@@ -18,10 +25,11 @@ type Alert struct {
|
|||||||
|
|
||||||
type AlertTask struct {
|
type AlertTask struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
||||||
Quota string `gorm:"type:varchar(64)" json:"quota"`
|
Quota string `gorm:"type:varchar(64)" json:"quota"`
|
||||||
QuotaType string `gorm:"type:varchar(64)" json:"quotaType"`
|
QuotaType string `gorm:"type:varchar(64)" json:"quotaType"`
|
||||||
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
|
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
|
||||||
|
DeliveryLogID *uint `gorm:"uniqueIndex" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertLog struct {
|
type AlertLog struct {
|
||||||
@@ -41,12 +49,21 @@ type AlertLog struct {
|
|||||||
|
|
||||||
type AlertConfig struct {
|
type AlertConfig struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
UID string `gorm:"type:varchar(64);not null;uniqueIndex" json:"uid"`
|
||||||
Title string `gorm:"type:varchar(64);not null" json:"title"`
|
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
||||||
Status string `gorm:"type:varchar(64);not null" json:"status"`
|
Title string `gorm:"type:varchar(64);not null" json:"title"`
|
||||||
Config string `gorm:"type:varchar(256);not null" json:"config"`
|
Status string `gorm:"type:varchar(64);not null" json:"status"`
|
||||||
CreateUser string `gorm:"type:varchar(256)" json:"createUser"`
|
Config string `gorm:"type:text;not null" json:"config"`
|
||||||
UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"`
|
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 {
|
type LoginLog struct {
|
||||||
|
|||||||
@@ -31,4 +31,5 @@ type BackupRecord struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
Args string `gorm:"not null;default:''" json:"args"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type ComposeTemplate struct {
|
|||||||
type Compose struct {
|
type Compose struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
|
IsPinned bool `json:"isPinned"`
|
||||||
}
|
}
|
||||||
|
|||||||
+217
-13
@@ -1,18 +1,222 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
type Firewall struct {
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||||
|
)
|
||||||
|
|
||||||
|
const FirewallRuleSequenceStep int64 = 1 << 32
|
||||||
|
|
||||||
|
type DockerPortGuardPolicy struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
|
|
||||||
Type string `json:"type"`
|
UUID string `gorm:"size:64;not null;uniqueIndex" json:"uuid"`
|
||||||
Port string `json:"port"` // Deprecated
|
ReadOnly bool `gorm:"not null;default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"`
|
||||||
Address string `json:"address"` // Deprecated
|
Family string `gorm:"size:16;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"family"`
|
||||||
|
HostIP string `gorm:"size:64;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"`
|
||||||
Chain string `json:"chain"`
|
HostPort uint16 `gorm:"not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"`
|
||||||
Protocol string `json:"protocol"`
|
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"`
|
||||||
SrcIP string `json:"srcIP"`
|
Mode string `gorm:"size:32;not null" json:"mode"`
|
||||||
SrcPort string `json:"srcPort"`
|
Sources string `gorm:"type:text" json:"-"`
|
||||||
DstIP string `json:"dstIP"`
|
Description string `gorm:"type:text" json:"description"`
|
||||||
DstPort string `json:"dstPort"`
|
NativeAction string `gorm:"size:32;not null;default:''" json:"-"`
|
||||||
Strategy string `gorm:"not null" json:"strategy"`
|
NativeRules string `gorm:"type:text" json:"-"`
|
||||||
Description string `json:"description"`
|
Sequence int64 `gorm:"not null;default:0" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardingRule struct {
|
||||||
|
BaseModel
|
||||||
|
|
||||||
|
Family string `gorm:"size:16;not null;uniqueIndex:idx_forwarding_rule_identity" json:"family"`
|
||||||
|
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_forwarding_rule_identity" json:"protocol"`
|
||||||
|
Port string `gorm:"size:32;not null;uniqueIndex:idx_forwarding_rule_identity" json:"port"`
|
||||||
|
TargetIP string `gorm:"size:64;not null;uniqueIndex:idx_forwarding_rule_identity" json:"targetIP"`
|
||||||
|
TargetPort string `gorm:"size:32;not null;uniqueIndex:idx_forwarding_rule_identity" json:"targetPort"`
|
||||||
|
Interface string `gorm:"size:32;not null;default:'';uniqueIndex:idx_forwarding_rule_identity" json:"interface"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRule struct {
|
||||||
|
UUID string `gorm:"size:64;primaryKey" json:"uuid"`
|
||||||
|
Family string `gorm:"size:16;not null" json:"family"`
|
||||||
|
|
||||||
|
Protocol string `gorm:"size:32;not null" json:"protocol"`
|
||||||
|
SourceAddress string `gorm:"size:255" json:"sourceAddress"`
|
||||||
|
SourcePort string `gorm:"size:64" json:"sourcePort"`
|
||||||
|
DestinationAddress string `gorm:"size:255" json:"destinationAddress"`
|
||||||
|
DestinationPort string `gorm:"size:64" json:"destinationPort"`
|
||||||
|
Interface string `gorm:"size:128" json:"interface"`
|
||||||
|
ConnectionStates string `gorm:"type:text" json:"connectionStates"`
|
||||||
|
Action string `gorm:"size:32;not null" json:"action"`
|
||||||
|
Description string `gorm:"type:text" json:"description"`
|
||||||
|
CompatibilityError string `gorm:"type:text" json:"compatibilityError,omitempty"`
|
||||||
|
Priority *int `json:"priority,omitempty"`
|
||||||
|
Sequence *int64 `gorm:"index" json:"sequence,omitempty"`
|
||||||
|
|
||||||
|
Origin string `gorm:"size:32;not null" json:"origin"`
|
||||||
|
Owner string `gorm:"size:320;not null" json:"owner"`
|
||||||
|
Revision uint `gorm:"not null;default:1" json:"revision"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func FirewallRuleOwner(sourceKind, sourceID string) string {
|
||||||
|
sourceKind = strings.TrimSpace(sourceKind)
|
||||||
|
sourceID = strings.TrimSpace(sourceID)
|
||||||
|
if sourceID == "" {
|
||||||
|
return sourceKind
|
||||||
|
}
|
||||||
|
return sourceKind + ":" + sourceID
|
||||||
|
}
|
||||||
|
|
||||||
|
func FirewallRuleFromDomain(rule filter.FirewallRule) (FirewallRule, error) {
|
||||||
|
normalized, err := filter.NormalizeRule(rule)
|
||||||
|
if err != nil {
|
||||||
|
return FirewallRule{}, err
|
||||||
|
}
|
||||||
|
switch normalized.NativeKind {
|
||||||
|
case "", filter.NativeKindRule, filter.NativeKindZonePort, filter.NativeKindRichRule, filter.NativeKindUFWRule:
|
||||||
|
default:
|
||||||
|
return FirewallRule{}, fmt.Errorf("%w: native rule %q cannot be stored as a provider-neutral policy", filter.ErrUnsupportedScope, normalized.NativeKind)
|
||||||
|
}
|
||||||
|
record := FirewallRule{
|
||||||
|
Family: string(normalized.Scope.Family),
|
||||||
|
Protocol: normalized.Protocol,
|
||||||
|
SourceAddress: normalized.SourceAddress,
|
||||||
|
SourcePort: normalized.SourcePort,
|
||||||
|
DestinationAddress: normalized.DestinationAddress,
|
||||||
|
DestinationPort: normalized.DestinationPort,
|
||||||
|
Interface: normalized.Interface,
|
||||||
|
ConnectionStates: strings.Join(normalized.ConnectionStates, ","),
|
||||||
|
Action: string(normalized.Action),
|
||||||
|
Description: normalized.Description,
|
||||||
|
}
|
||||||
|
if normalized.Scope.Provider == filter.ProviderFirewalld {
|
||||||
|
record.Priority = normalized.Priority
|
||||||
|
}
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rule FirewallRule) PolicyKey() string {
|
||||||
|
payload, _ := json.Marshal(struct {
|
||||||
|
Family string `json:"family"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
SourceAddress string `json:"sourceAddress,omitempty"`
|
||||||
|
SourcePort string `json:"sourcePort,omitempty"`
|
||||||
|
DestinationAddress string `json:"destinationAddress,omitempty"`
|
||||||
|
DestinationPort string `json:"destinationPort,omitempty"`
|
||||||
|
Interface string `json:"interface,omitempty"`
|
||||||
|
ConnectionStates string `json:"connectionStates,omitempty"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
}{
|
||||||
|
Family: rule.Family, Protocol: rule.Protocol,
|
||||||
|
SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort,
|
||||||
|
DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort,
|
||||||
|
Interface: rule.Interface, ConnectionStates: rule.ConnectionStates, Action: rule.Action,
|
||||||
|
})
|
||||||
|
sum := sha256.Sum256(payload)
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rule FirewallRule) RulesForProvider(provider filter.Provider) ([]filter.FirewallRule, error) {
|
||||||
|
if rule.CompatibilityError != "" {
|
||||||
|
return nil, fmt.Errorf("%w: %s", filter.ErrUnsupportedScope, rule.CompatibilityError)
|
||||||
|
}
|
||||||
|
connectionStates := make([]string, 0)
|
||||||
|
if rule.ConnectionStates != "" {
|
||||||
|
connectionStates = strings.Split(rule.ConnectionStates, ",")
|
||||||
|
}
|
||||||
|
base := filter.FirewallRule{
|
||||||
|
Protocol: rule.Protocol, SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort,
|
||||||
|
DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort,
|
||||||
|
Interface: rule.Interface, ConnectionStates: connectionStates,
|
||||||
|
Action: filter.Action(rule.Action), Description: rule.Description,
|
||||||
|
}
|
||||||
|
if provider != filter.ProviderUFW && strings.EqualFold(strings.TrimSpace(base.Protocol), "all") &&
|
||||||
|
strings.TrimSpace(base.SourcePort) == "" && strings.TrimSpace(base.DestinationPort) != "" {
|
||||||
|
base.Protocol = "tcp/udp"
|
||||||
|
}
|
||||||
|
if provider == filter.ProviderFirewalld {
|
||||||
|
base.Priority = rule.Priority
|
||||||
|
}
|
||||||
|
families := []filter.Family{filter.Family(rule.Family)}
|
||||||
|
if provider != filter.ProviderFirewalld && families[0] == filter.FamilyInet {
|
||||||
|
hasIPv4, hasIPv6 := ruleAddressFamilies(base)
|
||||||
|
switch {
|
||||||
|
case hasIPv4 && hasIPv6:
|
||||||
|
return nil, fmt.Errorf("%w: inet policy contains both IPv4 and IPv6 addresses", filter.ErrUnsupportedScope)
|
||||||
|
case hasIPv6 || strings.EqualFold(base.Protocol, "icmpv6"):
|
||||||
|
families = []filter.Family{filter.FamilyIPv6}
|
||||||
|
case hasIPv4:
|
||||||
|
families = []filter.Family{filter.FamilyIPv4}
|
||||||
|
default:
|
||||||
|
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result := make([]filter.FirewallRule, 0, len(families))
|
||||||
|
for _, family := range families {
|
||||||
|
compiled := base
|
||||||
|
compiled.Scope = filter.Scope{Provider: provider, Family: family, Direction: filter.DirectionInput}
|
||||||
|
switch provider {
|
||||||
|
case filter.ProviderIptables, filter.ProviderNftables:
|
||||||
|
compiled.Scope.Table, compiled.Scope.Chain = "filter", filter.IptablesInputChain
|
||||||
|
case filter.ProviderFirewalld:
|
||||||
|
compiled.Scope.Zone = filter.FirewalldInputZone
|
||||||
|
case filter.ProviderUFW:
|
||||||
|
compiled.Scope.Chain = filter.UFWInputChain
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%w: unsupported firewall provider %q", filter.ErrProviderUnavailable, provider)
|
||||||
|
}
|
||||||
|
expanded, err := filter.ExpandAtomicRules(compiled)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result = append(result, expanded...)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SortFirewallRules(rules []FirewallRule, provider filter.Provider) {
|
||||||
|
sort.SliceStable(rules, func(i, j int) bool {
|
||||||
|
left, right := rules[i], rules[j]
|
||||||
|
if provider == filter.ProviderFirewalld {
|
||||||
|
switch {
|
||||||
|
case left.Priority == nil && right.Priority != nil:
|
||||||
|
return false
|
||||||
|
case left.Priority != nil && right.Priority == nil:
|
||||||
|
return true
|
||||||
|
case left.Priority != nil && right.Priority != nil && *left.Priority != *right.Priority:
|
||||||
|
return *left.Priority < *right.Priority
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch {
|
||||||
|
case left.Sequence == nil && right.Sequence != nil:
|
||||||
|
return false
|
||||||
|
case left.Sequence != nil && right.Sequence == nil:
|
||||||
|
return true
|
||||||
|
case left.Sequence != nil && right.Sequence != nil && *left.Sequence != *right.Sequence:
|
||||||
|
return *left.Sequence < *right.Sequence
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return left.UUID < right.UUID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ruleAddressFamilies(rule filter.FirewallRule) (bool, bool) {
|
||||||
|
hasIPv4, hasIPv6 := false, false
|
||||||
|
for _, address := range []string{rule.SourceAddress, rule.DestinationAddress} {
|
||||||
|
address = strings.TrimSpace(address)
|
||||||
|
if address == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(address, ":") {
|
||||||
|
hasIPv6 = true
|
||||||
|
} else {
|
||||||
|
hasIPv4 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasIPv4, hasIPv6
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ type Ftp struct {
|
|||||||
Status string `gorm:"not null" json:"status"`
|
Status string `gorm:"not null" json:"status"`
|
||||||
Path string `gorm:"not null" json:"path"`
|
Path string `gorm:"not null" json:"path"`
|
||||||
Description string `gorm:"not null" json:"description"`
|
Description string `gorm:"not null" json:"description"`
|
||||||
|
UID uint `gorm:"column:uid;not null;default:1000" json:"-"`
|
||||||
|
GID uint `gorm:"column:gid;not null;default:1000" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
APIType string
|
||||||
BaseURL string
|
BaseURL string
|
||||||
EditableBaseURL bool
|
EditableBaseURL bool
|
||||||
|
DiscoverModels bool
|
||||||
DefaultAuthMode string
|
DefaultAuthMode string
|
||||||
AuthModes []string
|
AuthModes []string
|
||||||
|
Models []Model
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -27,6 +29,7 @@ type Model struct {
|
|||||||
type Meta struct {
|
type Meta struct {
|
||||||
Key string
|
Key string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
|
DisplayNameKey string
|
||||||
Sort uint
|
Sort uint
|
||||||
DefaultAPIType string
|
DefaultAPIType string
|
||||||
APIConfigs []APIConfig
|
APIConfigs []APIConfig
|
||||||
@@ -37,26 +40,36 @@ type Meta struct {
|
|||||||
var catalog = map[string]Meta{
|
var catalog = map[string]Meta{
|
||||||
"custom": {
|
"custom": {
|
||||||
Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY",
|
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": {
|
"ollama": {
|
||||||
Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses",
|
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": {
|
"vllm": {
|
||||||
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY",
|
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": {
|
"deepseek": {
|
||||||
Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY",
|
Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY",
|
||||||
APIConfigs: []APIConfig{
|
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),
|
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
|
||||||
},
|
},
|
||||||
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
||||||
},
|
},
|
||||||
"bailian-coding-plan": {
|
"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{
|
APIConfigs: []APIConfig{
|
||||||
{APIType: "openai-completions", BaseURL: "https://coding.dashscope.aliyuncs.com/v1"},
|
{APIType: "openai-completions", BaseURL: "https://coding.dashscope.aliyuncs.com/v1"},
|
||||||
anthropicAPIConfig("https://coding.dashscope.aliyuncs.com/apps/anthropic", AuthModeBearer),
|
anthropicAPIConfig("https://coding.dashscope.aliyuncs.com/apps/anthropic", AuthModeBearer),
|
||||||
@@ -75,7 +88,7 @@ var catalog = map[string]Meta{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"ark-coding-plan": {
|
"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{
|
APIConfigs: []APIConfig{
|
||||||
{APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3"},
|
{APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3"},
|
||||||
anthropicAPIConfig("https://ark.cn-beijing.volces.com/api/coding", AuthModeBearer),
|
anthropicAPIConfig("https://ark.cn-beijing.volces.com/api/coding", AuthModeBearer),
|
||||||
@@ -88,14 +101,18 @@ var catalog = map[string]Meta{
|
|||||||
},
|
},
|
||||||
"zai": {
|
"zai": {
|
||||||
Key: "zai", DisplayName: "Z.ai", Sort: 40, DefaultAPIType: "openai-completions", EnvKey: "ZAI_API_KEY",
|
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}},
|
APIConfigs: []APIConfig{
|
||||||
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"}},
|
{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": {
|
"minimax": {
|
||||||
Key: "minimax", DisplayName: "MiniMax (CN)", Sort: 45, DefaultAPIType: "anthropic-messages", EnvKey: "MINIMAX_API_KEY",
|
Key: "minimax", DisplayName: "MiniMax (CN)", Sort: 45, DefaultAPIType: "anthropic-messages", EnvKey: "MINIMAX_API_KEY",
|
||||||
APIConfigs: []APIConfig{
|
APIConfigs: []APIConfig{
|
||||||
anthropicAPIConfig("https://api.minimaxi.com/anthropic", AuthModeXAPIKey, AuthModeBearer),
|
anthropicAPIConfig("https://api.minimaxi.com/anthropic", AuthModeXAPIKey, AuthModeBearer),
|
||||||
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
|
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
|
||||||
|
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"},
|
||||||
},
|
},
|
||||||
Models: []Model{{ID: "MiniMax-M3", Name: "MiniMax M3"}, {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}},
|
Models: []Model{{ID: "MiniMax-M3", 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",
|
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
|
||||||
APIConfigs: []APIConfig{
|
APIConfigs: []APIConfig{
|
||||||
{APIType: "openai-completions", BaseURL: "https://api.xiaomimimo.com/v1"},
|
{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),
|
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": {
|
"kimi": {
|
||||||
Key: "kimi", DisplayName: "Kimi (CN)", Sort: 50, DefaultAPIType: "openai-completions", EnvKey: "KIMI_API_KEY",
|
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{
|
APIConfigs: []APIConfig{
|
||||||
{APIType: "openai-responses", BaseURL: "https://api.openai.com/v1"},
|
{APIType: "openai-responses", BaseURL: "https://api.openai.com/v1"},
|
||||||
{APIType: "openai-completions", 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"}},
|
Models: []Model{{ID: "gpt-5.4", Name: "gpt-5.4"}, {ID: "gpt-5.4-pro", Name: "gpt-5.4-pro"}, {ID: "gpt-5.4-mini", Name: "gpt-5.4-mini"}, {ID: "gpt-5.4-nano", Name: "gpt-5.4-nano"}},
|
||||||
},
|
},
|
||||||
"openrouter": {
|
"openrouter": {
|
||||||
Key: "openrouter", DisplayName: "OpenRouter", Sort: 56, DefaultAPIType: "openai-completions", EnvKey: "OPENROUTER_API_KEY",
|
Key: "openrouter", DisplayName: "OpenRouter", Sort: 56, DefaultAPIType: "openai-completions", EnvKey: "OPENROUTER_API_KEY",
|
||||||
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://openrouter.ai/api/v1"}},
|
APIConfigs: []APIConfig{
|
||||||
Models: []Model{{ID: "openrouter/free", Name: "openrouter/free"}, {ID: "openrouter/auto", Name: "openrouter/auto"}},
|
{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": {
|
"anthropic": {
|
||||||
Key: "anthropic", DisplayName: "Anthropic", Sort: 60, DefaultAPIType: "anthropic-messages", EnvKey: "ANTHROPIC_API_KEY",
|
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"}},
|
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"}},
|
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))
|
configs := make([]APIConfig, 0, len(apiTypes))
|
||||||
for _, apiType := range apiTypes {
|
for _, apiType := range apiTypes {
|
||||||
if apiType == "anthropic-messages" {
|
if apiType == "anthropic-messages" {
|
||||||
@@ -156,7 +235,11 @@ func editableAPIConfigs(apiTypes ...string) []APIConfig {
|
|||||||
configs = append(configs, config)
|
configs = append(configs, config)
|
||||||
continue
|
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
|
return configs
|
||||||
}
|
}
|
||||||
@@ -203,12 +286,38 @@ func FindAPIConfig(key, apiType string) (APIConfig, bool) {
|
|||||||
}
|
}
|
||||||
for _, config := range meta.APIConfigs {
|
for _, config := range meta.APIConfigs {
|
||||||
if config.APIType == target {
|
if config.APIType == target {
|
||||||
|
config.AuthModes = append([]string(nil), config.AuthModes...)
|
||||||
|
config.Models = append([]Model(nil), config.Models...)
|
||||||
return config, true
|
return config, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return APIConfig{}, false
|
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) {
|
func ResolveAuthMode(provider, apiType, requested string) (string, error) {
|
||||||
config, ok := FindAPIConfig(provider, apiType)
|
config, ok := FindAPIConfig(provider, apiType)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -264,6 +373,9 @@ func ResolveBaseURL(key, apiType, requested string) (string, error) {
|
|||||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||||
return "", fmt.Errorf("invalid base url")
|
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.Path = normalizeEndpointPath(config.APIType, parsed.Path)
|
||||||
parsed.RawQuery = ""
|
parsed.RawQuery = ""
|
||||||
parsed.Fragment = ""
|
parsed.Fragment = ""
|
||||||
@@ -271,6 +383,9 @@ func ResolveBaseURL(key, apiType, requested string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func normalizeEndpointPath(apiType, value string) string {
|
func normalizeEndpointPath(apiType, value string) string {
|
||||||
|
if IsImageAPIType(apiType) {
|
||||||
|
return strings.TrimRight(value, "/")
|
||||||
|
}
|
||||||
path := strings.TrimRight(value, "/")
|
path := strings.TrimRight(value, "/")
|
||||||
suffixes := []string{}
|
suffixes := []string{}
|
||||||
switch apiType {
|
switch apiType {
|
||||||
@@ -280,6 +395,8 @@ func normalizeEndpointPath(apiType, value string) string {
|
|||||||
suffixes = []string{"/responses"}
|
suffixes = []string{"/responses"}
|
||||||
case "anthropic-messages":
|
case "anthropic-messages":
|
||||||
suffixes = []string{"/v1/messages", "/messages"}
|
suffixes = []string{"/v1/messages", "/messages"}
|
||||||
|
case "openai-embeddings":
|
||||||
|
suffixes = []string{"/v1/embeddings", "/embeddings"}
|
||||||
}
|
}
|
||||||
for _, suffix := range suffixes {
|
for _, suffix := range suffixes {
|
||||||
if strings.HasSuffix(strings.ToLower(path), suffix) {
|
if strings.HasSuffix(strings.ToLower(path), suffix) {
|
||||||
@@ -289,6 +406,19 @@ func normalizeEndpointPath(apiType, value string) string {
|
|||||||
return path
|
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 {
|
func EnvKey(key string) string {
|
||||||
meta, ok := catalog[key]
|
meta, ok := catalog[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -305,6 +435,14 @@ func DisplayName(key string) string {
|
|||||||
return meta.DisplayName
|
return meta.DisplayName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DisplayNameKey(key string) string {
|
||||||
|
meta, ok := catalog[key]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return meta.DisplayNameKey
|
||||||
|
}
|
||||||
|
|
||||||
func NormalizeModelID(provider, modelID string) string {
|
func NormalizeModelID(provider, modelID string) string {
|
||||||
target := strings.TrimLeft(strings.TrimSpace(modelID), "/")
|
target := strings.TrimLeft(strings.TrimSpace(modelID), "/")
|
||||||
for _, prefix := range legacyModelPrefixes[provider] {
|
for _, prefix := range legacyModelPrefixes[provider] {
|
||||||
@@ -324,6 +462,7 @@ var legacyModelPrefixes = map[string][]string{
|
|||||||
"custom": {"custom"},
|
"custom": {"custom"},
|
||||||
"vllm": {"custom"},
|
"vllm": {"custom"},
|
||||||
"ollama": {"ollama"},
|
"ollama": {"ollama"},
|
||||||
|
"llmman": {"llmman"},
|
||||||
"deepseek": {"deepseek"},
|
"deepseek": {"deepseek"},
|
||||||
"bailian-coding-plan": {"bailian-coding-plan"},
|
"bailian-coding-plan": {"bailian-coding-plan"},
|
||||||
"ark-coding-plan": {"ark-coding-plan"},
|
"ark-coding-plan": {"ark-coding-plan"},
|
||||||
@@ -344,6 +483,7 @@ func cloneMeta(meta Meta) Meta {
|
|||||||
for index, config := range meta.APIConfigs {
|
for index, config := range meta.APIConfigs {
|
||||||
clone.APIConfigs[index] = config
|
clone.APIConfigs[index] = config
|
||||||
clone.APIConfigs[index].AuthModes = append([]string(nil), config.AuthModes...)
|
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...)
|
clone.Models = append([]Model(nil), meta.Models...)
|
||||||
return clone
|
return clone
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,11 +55,20 @@ func buildModelDiscoveryURL(baseURL string) string {
|
|||||||
base = normalizeEndpointPath(apiType, base)
|
base = normalizeEndpointPath(apiType, base)
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case strings.HasSuffix(base, "/v1/models"):
|
case strings.HasSuffix(base, "/models"):
|
||||||
return base
|
return base
|
||||||
case strings.HasSuffix(base, "/v1"):
|
case hasAPIVersionSuffix(base):
|
||||||
return base + "/models"
|
return base + "/models"
|
||||||
default:
|
default:
|
||||||
return base + "/v1/models"
|
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 {
|
if _, ok := FindAPIConfig(provider, resolvedAPIType); !ok {
|
||||||
resolvedAPIType = DefaultAPIType(provider)
|
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)
|
resolvedAuthMode, err := ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -40,8 +43,8 @@ func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL,
|
|||||||
providerKey = "moonshot"
|
providerKey = "moonshot"
|
||||||
resolvedAPIType = "openai-completions"
|
resolvedAPIType = "openai-completions"
|
||||||
usesBearer = false
|
usesBearer = false
|
||||||
case "ollama":
|
case "ollama", "llmman":
|
||||||
apiKey = "ollama"
|
apiKey = provider
|
||||||
usesBearer = false
|
usesBearer = false
|
||||||
case "openai", "openrouter", "anthropic":
|
case "openai", "openrouter", "anthropic":
|
||||||
preserveQualifiedModel = strings.Contains(modelName, "/")
|
preserveQualifiedModel = strings.Contains(modelName, "/")
|
||||||
|
|||||||
@@ -27,11 +27,14 @@ type verifyErrorResponse struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultVerifyTimeout = 30 * time.Second
|
const (
|
||||||
|
defaultVerifyTimeout = 30 * time.Second
|
||||||
|
defaultVerifyMaxTokens = 16
|
||||||
|
)
|
||||||
|
|
||||||
func SkipVerification(provider string) bool {
|
func SkipVerification(provider string) bool {
|
||||||
switch provider {
|
switch provider {
|
||||||
case "vllm", "ollama", "kimi-coding":
|
case "vllm", "ollama", "llmman", "kimi-coding":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
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 {
|
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"}
|
headers := map[string]string{"Content-Type": "application/json"}
|
||||||
request := VerifyRequest{Method: http.MethodPost, Headers: headers}
|
request := VerifyRequest{Method: http.MethodPost, Headers: headers}
|
||||||
|
|
||||||
@@ -78,6 +84,32 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch apiType {
|
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":
|
case "anthropic-messages":
|
||||||
request.URL = baseURL + "/v1/messages"
|
request.URL = baseURL + "/v1/messages"
|
||||||
if authMode == AuthModeBearer {
|
if authMode == AuthModeBearer {
|
||||||
@@ -87,25 +119,43 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
|||||||
}
|
}
|
||||||
headers["anthropic-version"] = "2023-06-01"
|
headers["anthropic-version"] = "2023-06-01"
|
||||||
request.Body = mustJSON(map[string]interface{}{
|
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"}}}},
|
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
|
||||||
})
|
})
|
||||||
case "openai-responses":
|
case "openai-responses":
|
||||||
request.URL = baseURL + "/responses"
|
request.URL = baseURL + "/responses"
|
||||||
headers["Authorization"] = "Bearer " + apiKey
|
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:
|
default:
|
||||||
request.URL = baseURL + "/chat/completions"
|
request.URL = baseURL + "/chat/completions"
|
||||||
if provider != "ollama" || strings.TrimSpace(apiKey) != "" {
|
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
|
||||||
headers["Authorization"] = "Bearer " + apiKey
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
}
|
}
|
||||||
request.Body = mustJSON(map[string]interface{}{
|
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
|
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 {
|
func verifyHTTPError(statusCode int, body []byte) string {
|
||||||
message := strings.TrimSpace(string(body))
|
message := strings.TrimSpace(string(body))
|
||||||
var payload verifyErrorResponse
|
var payload verifyErrorResponse
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type IAgentAccountRepo interface {
|
|||||||
Save(account *model.AgentAccount) error
|
Save(account *model.AgentAccount) error
|
||||||
DeleteByID(id uint) error
|
DeleteByID(id uint) error
|
||||||
List(opts ...DBOption) ([]model.AgentAccount, error)
|
List(opts ...DBOption) ([]model.AgentAccount, error)
|
||||||
CountByProviders(providers []string) (map[string]int64, error)
|
CountTextByProviders(providers []string) (map[string]int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAgentAccountRepo() IAgentAccountRepo {
|
func NewIAgentAccountRepo() IAgentAccountRepo {
|
||||||
@@ -67,7 +67,7 @@ func (a AgentAccountRepo) List(opts ...DBOption) ([]model.AgentAccount, error) {
|
|||||||
return accounts, nil
|
return accounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentAccountRepo) CountByProviders(providers []string) (map[string]int64, error) {
|
func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]int64, error) {
|
||||||
normalizedProviders := normalizeProviders(providers)
|
normalizedProviders := normalizeProviders(providers)
|
||||||
counts := make(map[string]int64, len(normalizedProviders))
|
counts := make(map[string]int64, len(normalizedProviders))
|
||||||
for _, provider := range normalizedProviders {
|
for _, provider := range normalizedProviders {
|
||||||
@@ -86,6 +86,7 @@ func (a AgentAccountRepo) CountByProviders(providers []string) (map[string]int64
|
|||||||
Model(&model.AgentAccount{}).
|
Model(&model.AgentAccount{}).
|
||||||
Select("provider, COUNT(*) as count").
|
Select("provider, COUNT(*) as count").
|
||||||
Where("provider IN ?", normalizedProviders).
|
Where("provider IN ?", normalizedProviders).
|
||||||
|
Scopes(WithTextAPIType()).
|
||||||
Group("provider").
|
Group("provider").
|
||||||
Scan(&rows).Error; err != nil {
|
Scan(&rows).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
+217
-9
@@ -1,20 +1,30 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/google/uuid"
|
||||||
"google.golang.org/genproto/googleapis/type/date"
|
"google.golang.org/genproto/googleapis/type/date"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"strconv"
|
"gorm.io/gorm/clause"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AlertRepo struct{}
|
type AlertRepo struct{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrAlertConfigRevisionConflict = errors.New("alert config revision conflict")
|
||||||
|
ErrAlertConfigRevisionRequired = errors.New("alert config revision is required")
|
||||||
|
)
|
||||||
|
|
||||||
type IAlertRepo interface {
|
type IAlertRepo interface {
|
||||||
WithByType(alertType string) DBOption
|
WithByType(alertType string) DBOption
|
||||||
WithByStatusIn(status []string) DBOption
|
WithByStatusIn(status []string) DBOption
|
||||||
@@ -24,6 +34,7 @@ type IAlertRepo interface {
|
|||||||
WithByCreateAt(date *date.Date) DBOption
|
WithByCreateAt(date *date.Date) DBOption
|
||||||
WithByLicenseId(licenseId string) DBOption
|
WithByLicenseId(licenseId string) DBOption
|
||||||
WithByRecordId(recordId uint) DBOption
|
WithByRecordId(recordId uint) DBOption
|
||||||
|
WithByDeliveryLogID(logID uint) DBOption
|
||||||
WithByAlertMethodContainsConfigID(id uint) DBOption
|
WithByAlertMethodContainsConfigID(id uint) DBOption
|
||||||
WithByMethodConfigIDs(ids []uint) DBOption
|
WithByMethodConfigIDs(ids []uint) DBOption
|
||||||
|
|
||||||
@@ -45,6 +56,8 @@ type IAlertRepo interface {
|
|||||||
CleanAlertLogs() error
|
CleanAlertLogs() error
|
||||||
|
|
||||||
CreateAlertTask(alertTaskBase *model.AlertTask) 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
|
DeleteAlertTask(opts ...DBOption) error
|
||||||
GetAlertTask(opts ...DBOption) (model.AlertTask, error)
|
GetAlertTask(opts ...DBOption) (model.AlertTask, error)
|
||||||
LoadTaskCount(alertType string, project string, method string) (uint, uint, error)
|
LoadTaskCount(alertType string, project string, method string) (uint, uint, error)
|
||||||
@@ -55,6 +68,7 @@ type IAlertRepo interface {
|
|||||||
GetConfigById(id uint) (model.AlertConfig, error)
|
GetConfigById(id uint) (model.AlertConfig, error)
|
||||||
AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error)
|
AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error)
|
||||||
UpdateAlertConfig(maps map[string]interface{}, opts ...DBOption) 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
|
CreateAlertConfig(config *model.AlertConfig) error
|
||||||
DeleteAlertConfig(opts ...DBOption) error
|
DeleteAlertConfig(opts ...DBOption) error
|
||||||
|
|
||||||
@@ -223,13 +237,78 @@ func (a *AlertRepo) DeleteLog(opts ...DBOption) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *AlertRepo) CleanAlertLogs() 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 {
|
func (a *AlertRepo) CreateAlertTask(alertTaskBase *model.AlertTask) error {
|
||||||
return global.AlertDB.Model(&model.AlertTask{}).Create(&alertTaskBase).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 {
|
func (a *AlertRepo) DeleteAlertTask(opts ...DBOption) error {
|
||||||
db, _ := getAlertDB(opts...)
|
db, _ := getAlertDB(opts...)
|
||||||
return db.Delete(&model.AlertTask{}).Error
|
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
|
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 {
|
func (a *AlertRepo) CreateAlertConfig(config *model.AlertConfig) error {
|
||||||
|
ensureAlertConfigUID(config)
|
||||||
return global.AlertDB.Model(&model.AlertConfig{}).Create(config).Error
|
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) {
|
func (a *AlertRepo) PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error) {
|
||||||
var configs []model.AlertConfig
|
var configs []model.AlertConfig
|
||||||
db := global.AlertDB.Model(&model.AlertConfig{})
|
db := global.AlertDB.Model(&model.AlertConfig{})
|
||||||
@@ -378,26 +479,44 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
|||||||
return err
|
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)
|
oldConfigByType := make(map[string][]model.AlertConfig)
|
||||||
oldConfigByKey := make(map[string][]model.AlertConfig)
|
oldConfigByKey := make(map[string][]model.AlertConfig)
|
||||||
consumedConfigIDs := make(map[uint]struct{})
|
consumedConfigIDs := make(map[uint]struct{})
|
||||||
for _, item := range oldConfigs {
|
for _, item := range oldConfigs {
|
||||||
|
if strings.TrimSpace(item.UID) != "" {
|
||||||
|
oldConfigByUID[item.UID] = item
|
||||||
|
}
|
||||||
if singletonTypes[item.Type] {
|
if singletonTypes[item.Type] {
|
||||||
oldConfigMap[item.Type] = item.ID
|
oldConfigMap[item.Type] = item
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
oldConfigByType[item.Type] = append(oldConfigByType[item.Type], item)
|
oldConfigByType[item.Type] = append(oldConfigByType[item.Type], item)
|
||||||
oldConfigByKey[alertConfigSyncKey(item)] = append(oldConfigByKey[alertConfigSyncKey(item)], item)
|
oldConfigByKey[alertConfigSyncKey(item)] = append(oldConfigByKey[alertConfigSyncKey(item)], item)
|
||||||
}
|
}
|
||||||
for _, item := range data {
|
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 singletonTypes[item.Type] {
|
||||||
if val, ok := oldConfigMap[item.Type]; ok {
|
if matched, ok := oldConfigMap[item.Type]; ok {
|
||||||
item.ID = val
|
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
delete(oldConfigMap, item.Type)
|
delete(oldConfigMap, item.Type)
|
||||||
consumedConfigIDs[item.ID] = struct{}{}
|
consumedConfigIDs[item.ID] = struct{}{}
|
||||||
} else {
|
} else {
|
||||||
item.ID = 0
|
item.ID = 0
|
||||||
|
ensureAlertConfigUID(&item)
|
||||||
|
if err := validateAlertConfigSyncSecret(&item); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if item.ID == 0 {
|
if item.ID == 0 {
|
||||||
if err := tx.Create(&item).Error; err != nil {
|
if err := tx.Create(&item).Error; err != nil {
|
||||||
@@ -411,9 +530,31 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
|||||||
continue
|
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)
|
key := alertConfigSyncKey(item)
|
||||||
if matched, ok := popAlertConfigByKey(oldConfigByKey, key); ok {
|
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{}{}
|
consumedConfigIDs[item.ID] = struct{}{}
|
||||||
if err := tx.Save(&item).Error; err != nil {
|
if err := tx.Save(&item).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
@@ -424,7 +565,12 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if matched, ok := popUnusedAlertConfigByType(oldConfigByType, usedConfigIDs, item.Type); ok {
|
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{}{}
|
consumedConfigIDs[item.ID] = struct{}{}
|
||||||
if err := tx.Save(&item).Error; err != nil {
|
if err := tx.Save(&item).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
@@ -434,6 +580,11 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
item.ID = 0
|
item.ID = 0
|
||||||
|
ensureAlertConfigUID(&item)
|
||||||
|
if err := validateAlertConfigSyncSecret(&item); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := tx.Create(&item).Error; err != nil {
|
if err := tx.Create(&item).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return err
|
return err
|
||||||
@@ -458,6 +609,63 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
|||||||
return nil
|
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) {
|
func loadUsedAlertConfigIDs(tx *gorm.DB) (map[uint]struct{}, error) {
|
||||||
var alerts []model.Alert
|
var alerts []model.Alert
|
||||||
if err := tx.Select("method").Find(&alerts).Error; err != nil {
|
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 {
|
func WithByAddr(addr string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
return g.Where("addr = ?", addr)
|
return g.Where("addr = ?", addr)
|
||||||
@@ -94,6 +100,18 @@ func WithByProvider(provider string) DBOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithByAPIType(apiType string) DBOption {
|
||||||
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
|
return g.Where("api_type = ?", apiType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTextAPIType() DBOption {
|
||||||
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
|
return g.Where("api_type NOT LIKE ? AND api_type <> ?", "%-images", "openai-embeddings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithByModel(model string) DBOption {
|
func WithByModel(model string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
if len(model) == 0 {
|
if len(model) == 0 {
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IDockerPortGuardRepo interface {
|
||||||
|
ListManaged(context.Context) ([]model.DockerPortGuardPolicy, error)
|
||||||
|
ListRuntimeReadOnly(context.Context) ([]model.DockerPortGuardPolicy, error)
|
||||||
|
DeleteBatch(context.Context, []string) error
|
||||||
|
UpsertBatch(context.Context, []model.DockerPortGuardPolicy) error
|
||||||
|
ReplaceRuntimeReadOnly(context.Context, []model.DockerPortGuardPolicy) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerPortGuardRepo struct{}
|
||||||
|
|
||||||
|
func NewIDockerPortGuardRepo() IDockerPortGuardRepo { return &DockerPortGuardRepo{} }
|
||||||
|
|
||||||
|
func (r *DockerPortGuardRepo) ListManaged(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
|
||||||
|
var policies []model.DockerPortGuardPolicy
|
||||||
|
err := global.DB.WithContext(ctx).
|
||||||
|
Where("read_only = ?", false).
|
||||||
|
Order("family, host_ip, host_port, protocol").
|
||||||
|
Find(&policies).Error
|
||||||
|
return policies, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DockerPortGuardRepo) ListRuntimeReadOnly(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
|
||||||
|
var policies []model.DockerPortGuardPolicy
|
||||||
|
err := global.DB.WithContext(ctx).
|
||||||
|
Where("read_only = ?", true).
|
||||||
|
Order("family, sequence, host_ip, host_port, protocol").
|
||||||
|
Find(&policies).Error
|
||||||
|
return policies, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DockerPortGuardRepo) DeleteBatch(ctx context.Context, uuids []string) error {
|
||||||
|
return global.DB.WithContext(ctx).
|
||||||
|
Where("read_only = ? AND uuid IN ?", false, uuids).
|
||||||
|
Delete(&model.DockerPortGuardPolicy{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DockerPortGuardRepo) UpsertBatch(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
|
||||||
|
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
for i := range policies {
|
||||||
|
policies[i].ReadOnly = false
|
||||||
|
if err := tx.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "read_only"}, {Name: "family"}, {Name: "host_ip"}, {Name: "host_port"}, {Name: "protocol"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{"mode", "sources", "description", "updated_at"}),
|
||||||
|
}).Create(&policies[i]).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DockerPortGuardRepo) ReplaceRuntimeReadOnly(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
|
||||||
|
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Where("read_only = ?", true).
|
||||||
|
Delete(&model.DockerPortGuardPolicy{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(policies) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i := range policies {
|
||||||
|
policies[i].ReadOnly = true
|
||||||
|
}
|
||||||
|
return tx.Create(&policies).Error
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrFirewallRuleRevisionConflict = errors.New("firewall rule revision conflict")
|
||||||
|
ErrFirewallPersistenceInvalid = errors.New("invalid firewall persistence record")
|
||||||
|
)
|
||||||
|
|
||||||
|
type IFirewallRuleRepo interface {
|
||||||
|
Create(context.Context, *model.FirewallRule) error
|
||||||
|
GetByUUID(context.Context, string) (model.FirewallRule, error)
|
||||||
|
List(context.Context, ...DBOption) ([]model.FirewallRule, error)
|
||||||
|
UpdateWithRevision(context.Context, string, uint, map[string]interface{}) error
|
||||||
|
DeleteWithRevision(context.Context, string, uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleRepo struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIFirewallRuleRepo() IFirewallRuleRepo {
|
||||||
|
return &FirewallRuleRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFirewallRuleRepo(db *gorm.DB) *FirewallRuleRepo {
|
||||||
|
return &FirewallRuleRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FirewallRuleRepo) Create(ctx context.Context, rule *model.FirewallRule) error {
|
||||||
|
if err := prepareFirewallRule(rule); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return r.dbFor(ctx).Create(rule).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FirewallRuleRepo) GetByUUID(ctx context.Context, ruleUUID string) (model.FirewallRule, error) {
|
||||||
|
var rule model.FirewallRule
|
||||||
|
err := r.dbFor(ctx).Where("uuid = ?", ruleUUID).First(&rule).Error
|
||||||
|
return rule, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FirewallRuleRepo) List(ctx context.Context, opts ...DBOption) ([]model.FirewallRule, error) {
|
||||||
|
var rules []model.FirewallRule
|
||||||
|
db := r.dbFor(ctx).Model(&model.FirewallRule{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
return rules, db.Find(&rules).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FirewallRuleRepo) UpdateWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint, updates map[string]interface{}) error {
|
||||||
|
updates = sanitizeRuleUpdates(updates)
|
||||||
|
updates["revision"] = gorm.Expr("revision + 1")
|
||||||
|
result := r.dbFor(ctx).Model(&model.FirewallRule{}).
|
||||||
|
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
|
||||||
|
Updates(updates)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return ErrFirewallRuleRevisionConflict
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FirewallRuleRepo) DeleteWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint) error {
|
||||||
|
result := r.dbFor(ctx).
|
||||||
|
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
|
||||||
|
Delete(&model.FirewallRule{})
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return ErrFirewallRuleRevisionConflict
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FirewallRuleRepo) dbFor(ctx context.Context) *gorm.DB {
|
||||||
|
return firewallDB(ctx, r.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func firewallDB(ctx context.Context, fallback *gorm.DB) *gorm.DB {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
if tx, ok := ctx.Value(constant.DB).(*gorm.DB); ok && tx != nil {
|
||||||
|
return tx.WithContext(ctx)
|
||||||
|
}
|
||||||
|
if fallback == nil {
|
||||||
|
fallback = global.DB
|
||||||
|
}
|
||||||
|
return fallback.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareFirewallRule(rule *model.FirewallRule) error {
|
||||||
|
if rule == nil {
|
||||||
|
return fmt.Errorf("%w: rule is nil", ErrFirewallPersistenceInvalid)
|
||||||
|
}
|
||||||
|
if rule.Family == "" || rule.Protocol == "" || rule.Action == "" {
|
||||||
|
return fmt.Errorf("%w: atomic rule identity fields are required", ErrFirewallPersistenceInvalid)
|
||||||
|
}
|
||||||
|
if rule.UUID == "" {
|
||||||
|
rule.UUID = uuid.NewString()
|
||||||
|
}
|
||||||
|
if rule.Revision == 0 {
|
||||||
|
rule.Revision = 1
|
||||||
|
}
|
||||||
|
if rule.Origin == "" {
|
||||||
|
rule.Origin = constant.FirewallRuleOriginCreated
|
||||||
|
}
|
||||||
|
if rule.Owner == "" {
|
||||||
|
rule.Owner = constant.FirewallRuleSourceUser
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeRuleUpdates(updates map[string]interface{}) map[string]interface{} {
|
||||||
|
result := cloneUpdates(updates)
|
||||||
|
delete(result, "id")
|
||||||
|
delete(result, "uuid")
|
||||||
|
delete(result, "revision")
|
||||||
|
delete(result, "created_at")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneUpdates(updates map[string]interface{}) map[string]interface{} {
|
||||||
|
result := make(map[string]interface{}, len(updates)+1)
|
||||||
|
for key, value := range updates {
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IForwardingRuleRepo interface {
|
||||||
|
List(context.Context) ([]model.ForwardingRule, error)
|
||||||
|
ReplaceAll(context.Context, []model.ForwardingRule) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardingRuleRepo struct{}
|
||||||
|
|
||||||
|
func NewIForwardingRuleRepo() IForwardingRuleRepo { return &ForwardingRuleRepo{} }
|
||||||
|
|
||||||
|
func (r *ForwardingRuleRepo) List(ctx context.Context) ([]model.ForwardingRule, error) {
|
||||||
|
var rules []model.ForwardingRule
|
||||||
|
err := global.DB.WithContext(ctx).Order("id ASC").Find(&rules).Error
|
||||||
|
return rules, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ForwardingRuleRepo) ReplaceAll(ctx context.Context, rules []model.ForwardingRule) error {
|
||||||
|
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.ForwardingRule{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(rules) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tx.Create(&rules).Error
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -22,11 +22,6 @@ type IHostRepo interface {
|
|||||||
WithByPort(port uint) DBOption
|
WithByPort(port uint) DBOption
|
||||||
WithByUser(user string) 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
|
SyncCert(data []model.RootCert) error
|
||||||
GetCert(opts ...DBOption) (model.RootCert, error)
|
GetCert(opts ...DBOption) (model.RootCert, error)
|
||||||
PageCert(limit, offset int, opts ...DBOption) (int64, []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
|
SaveCert(cert *model.RootCert) error
|
||||||
UpdateCert(id uint, vars map[string]interface{}) error
|
UpdateCert(id uint, vars map[string]interface{}) error
|
||||||
DeleteCert(opts ...DBOption) error
|
DeleteCert(opts ...DBOption) error
|
||||||
|
|
||||||
WithByChain(chain string) DBOption
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIHostRepo() IHostRepo {
|
func NewIHostRepo() IHostRepo {
|
||||||
@@ -116,65 +109,6 @@ func (h *HostRepo) Delete(opts ...DBOption) error {
|
|||||||
return db.Delete(&model.Host{}).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) {
|
func (u *HostRepo) GetCert(opts ...DBOption) (model.RootCert, error) {
|
||||||
var cert model.RootCert
|
var cert model.RootCert
|
||||||
db := global.DB
|
db := global.DB
|
||||||
@@ -253,9 +187,3 @@ func (u *HostRepo) SyncCert(data []model.RootCert) error {
|
|||||||
tx.Commit()
|
tx.Commit()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *HostRepo) WithByChain(chain string) DBOption {
|
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
|
||||||
return g.Where("chain = ?", chain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
|
|||||||
return global.MonitorDB.Create(&model).Error
|
return global.MonitorDB.Create(&model).Error
|
||||||
}
|
}
|
||||||
func (s *MonitorRepo) BatchCreateMonitorGPU(list []model.MonitorGPU) error {
|
func (s *MonitorRepo) BatchCreateMonitorGPU(list []model.MonitorGPU) error {
|
||||||
|
if len(list) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return global.GPUMonitorDB.CreateInBatches(&list, len(list)).Error
|
return global.GPUMonitorDB.CreateInBatches(&list, len(list)).Error
|
||||||
}
|
}
|
||||||
func (u *MonitorRepo) BatchCreateMonitorIO(ioList []model.MonitorIO) error {
|
func (u *MonitorRepo) BatchCreateMonitorIO(ioList []model.MonitorIO) 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
|
UpgradePlugin(req dto.AgentPluginUpgradeReq) error
|
||||||
UninstallPlugin(req dto.AgentPluginUninstallReq) error
|
UninstallPlugin(req dto.AgentPluginUninstallReq) error
|
||||||
CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error)
|
CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error)
|
||||||
|
ListPlugins(req dto.AgentPluginsReq) ([]dto.AgentPluginItem, error)
|
||||||
|
SearchPlugins(req dto.AgentPluginSearchReq) ([]dto.AgentPluginSearchItem, error)
|
||||||
|
InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) error
|
||||||
|
OperatePlugin(req dto.AgentPluginOperateReq) error
|
||||||
ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
|
ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +181,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
var allowedOrigins []string
|
var allowedOrigins []string
|
||||||
var account *model.AgentAccount
|
var account *model.AgentAccount
|
||||||
var installHooks *appInstallHooks
|
var installHooks *appInstallHooks
|
||||||
var hermesAuth hermesDashboardAuth
|
var dashboardAuth agentDashboardAuth
|
||||||
|
|
||||||
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
|
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
|
||||||
if req.AccountID == 0 {
|
if req.AccountID == 0 {
|
||||||
@@ -222,15 +226,17 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if agentType == constant.AppHermesAgent {
|
} else if agentType == constant.AppHermesAgent {
|
||||||
hermesAuth = normalizeHermesDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||||
installHooks = &appInstallHooks{
|
installHooks = &appInstallHooks{
|
||||||
AfterCopyData: func(appInstall *model.AppInstall) error {
|
AfterCopyData: func(appInstall *model.AppInstall) error {
|
||||||
if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil {
|
if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeHermesDashboardAuthEnv(path.Join(appInstall.GetPath(), ".env"), hermesAuth, false)
|
return writeAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType, dashboardAuth, false)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
} else if agentType == constant.AppCopaw {
|
||||||
|
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
@@ -250,9 +256,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
params["API_KEY"] = apiKey
|
params["API_KEY"] = apiKey
|
||||||
params["OPENCLAW_GATEWAY_TOKEN"] = token
|
params["OPENCLAW_GATEWAY_TOKEN"] = token
|
||||||
}
|
}
|
||||||
if agentType == constant.AppHermesAgent {
|
if usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType); ok {
|
||||||
params[hermesDashboardUsernameEnvKey] = hermesAuth.Username
|
params[usernameKey] = dashboardAuth.Username
|
||||||
params[hermesDashboardPasswordEnvKey] = hermesAuth.Password
|
params[passwordKey] = dashboardAuth.Password
|
||||||
|
if agentType == constant.AppCopaw {
|
||||||
|
params[qwenPawAuthEnabledEnvKey] = "true"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
|
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
|
||||||
@@ -927,6 +936,7 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig,
|
|||||||
AccountID: agent.AccountID,
|
AccountID: agent.AccountID,
|
||||||
Model: model,
|
Model: model,
|
||||||
Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
|
Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
|
||||||
|
Metadata: extractOpenclawModelMetadata(conf, account, models),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -958,7 +968,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
|||||||
if agent.AgentType != constant.AppOpenclaw {
|
if agent.AgentType != constant.AppOpenclaw {
|
||||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||||
}
|
}
|
||||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil {
|
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks, req.Metadata); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -984,19 +994,28 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
|
|||||||
}
|
}
|
||||||
apiTypes := make([]dto.ProviderAPIInfo, 0, len(def.APIConfigs))
|
apiTypes := make([]dto.ProviderAPIInfo, 0, len(def.APIConfigs))
|
||||||
for _, item := range 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{
|
apiTypes = append(apiTypes, dto.ProviderAPIInfo{
|
||||||
APIType: item.APIType,
|
APIType: item.APIType,
|
||||||
BaseURL: item.BaseURL,
|
BaseURL: item.BaseURL,
|
||||||
EditableBaseURL: item.EditableBaseURL,
|
EditableBaseURL: item.EditableBaseURL,
|
||||||
DefaultAuthMode: item.DefaultAuthMode,
|
SupportsModelDiscovery: item.DiscoverModels,
|
||||||
AuthModes: item.AuthModes,
|
DefaultAuthMode: item.DefaultAuthMode,
|
||||||
|
AuthModes: item.AuthModes,
|
||||||
|
Models: apiModels,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
baseURL, _ := providercatalog.DefaultBaseURL(key)
|
baseURL, _ := providercatalog.DefaultBaseURL(key)
|
||||||
providers = append(providers, dto.ProviderInfo{
|
providers = append(providers, dto.ProviderInfo{
|
||||||
Sort: def.Sort,
|
Sort: def.Sort,
|
||||||
Provider: key,
|
Provider: key,
|
||||||
DisplayName: def.DisplayName,
|
DisplayName: localizedAgentProviderName(key),
|
||||||
BaseURL: baseURL,
|
BaseURL: baseURL,
|
||||||
DefaultAPIType: def.DefaultAPIType,
|
DefaultAPIType: def.DefaultAPIType,
|
||||||
APITypes: apiTypes,
|
APITypes: apiTypes,
|
||||||
@@ -1016,7 +1035,7 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
|||||||
if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil {
|
if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1024,7 +1043,8 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1062,6 +1082,9 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if req.APIType != account.APIType {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "API type cannot be changed", nil)
|
||||||
|
}
|
||||||
provider := account.Provider
|
provider := account.Provider
|
||||||
if err := ensureAgentAccountNameAvailable(provider, req.Name, account.ID); err != nil {
|
if err := ensureAgentAccountNameAvailable(provider, req.Name, account.ID); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1078,7 +1101,8 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1110,6 +1134,12 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
|||||||
if strings.TrimSpace(req.Provider) != "" {
|
if strings.TrimSpace(req.Provider) != "" {
|
||||||
opts = append(opts, repo.WithByProvider(req.Provider))
|
opts = append(opts, repo.WithByProvider(req.Provider))
|
||||||
}
|
}
|
||||||
|
if apiType := strings.TrimSpace(req.APIType); apiType != "" {
|
||||||
|
opts = append(opts, repo.WithByAPIType(apiType))
|
||||||
|
}
|
||||||
|
if req.TextOnly {
|
||||||
|
opts = append(opts, repo.WithTextAPIType())
|
||||||
|
}
|
||||||
if strings.TrimSpace(req.Name) != "" {
|
if strings.TrimSpace(req.Name) != "" {
|
||||||
opts = append(opts, repo.WithByLikeName(req.Name))
|
opts = append(opts, repo.WithByLikeName(req.Name))
|
||||||
}
|
}
|
||||||
@@ -1127,7 +1157,7 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
|||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
MasterAccountID: item.MasterAccountID,
|
MasterAccountID: item.MasterAccountID,
|
||||||
Provider: item.Provider,
|
Provider: item.Provider,
|
||||||
ProviderName: providercatalog.DisplayName(item.Provider),
|
ProviderName: localizedAgentProviderName(item.Provider),
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
APIKey: apiKey,
|
APIKey: apiKey,
|
||||||
RememberAPIKey: item.RememberAPIKey,
|
RememberAPIKey: item.RememberAPIKey,
|
||||||
@@ -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) {
|
func (a AgentService) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) {
|
||||||
return agentAccountRepo.CountByProviders(req.Providers)
|
return agentAccountRepo.CountTextByProviders(req.Providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) {
|
func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) {
|
||||||
@@ -1178,7 +1208,8 @@ func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.Agen
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) {
|
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")
|
return nil, buserr.New("ErrAgentAccountModelsRequired")
|
||||||
}
|
}
|
||||||
baseURL, err := providercatalog.ResolveBaseURL(req.Provider, req.APIType, req.BaseURL)
|
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 {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1410,7 +1441,7 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
auth := readHermesDashboardAuthFromInstall(install)
|
auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
|
||||||
return &dto.AgentOtherConfig{
|
return &dto.AgentOtherConfig{
|
||||||
UserTimezone: cfg.Timezone,
|
UserTimezone: cfg.Timezone,
|
||||||
BrowserEnabled: true,
|
BrowserEnabled: true,
|
||||||
@@ -1419,6 +1450,13 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
|
|||||||
DashboardPassword: auth.Password,
|
DashboardPassword: auth.Password,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
if agent.AgentType == constant.AppCopaw {
|
||||||
|
auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
|
||||||
|
return &dto.AgentOtherConfig{
|
||||||
|
DashboardUsername: auth.Username,
|
||||||
|
DashboardPassword: auth.Password,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1437,16 +1475,19 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if agent.AgentType == constant.AppHermesAgent {
|
if agent.AgentType == constant.AppHermesAgent {
|
||||||
|
if strings.TrimSpace(req.UserTimezone) == "" {
|
||||||
|
return buserr.New("ErrInvalidParams")
|
||||||
|
}
|
||||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
|
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
previousAuth := readHermesDashboardAuthFromInstall(install)
|
previousAuth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
|
||||||
nextAuth := normalizeHermesDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
nextAuth := normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||||
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
|
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := writeHermesDashboardAuthEnv(path.Join(install.GetPath(), ".env"), nextAuth, true); err != nil {
|
if err := writeAgentDashboardAuthEnv(install.GetEnvPath(), agent.AgentType, nextAuth, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
operate := constant.Restart
|
operate := constant.Restart
|
||||||
@@ -1458,6 +1499,12 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
|||||||
Operate: operate,
|
Operate: operate,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if agent.AgentType == constant.AppCopaw {
|
||||||
|
return updateQwenPawDashboardAuth(install, normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.UserTimezone) == "" || strings.TrimSpace(req.NPMRegistry) == "" {
|
||||||
|
return buserr.New("ErrInvalidParams")
|
||||||
|
}
|
||||||
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1638,7 +1685,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
|
fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
|
||||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil {
|
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case constant.AppHermesAgent:
|
case constant.AppHermesAgent:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -1320,6 +1321,10 @@ func appendPluginAllow(conf map[string]interface{}, pluginID string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID string) error {
|
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)
|
workdir := path.Join(openclawPluginPackageTmpDir, pluginID)
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = mgr.Run("docker", "exec", containerName, "rm", "-rf", workdir)
|
_ = mgr.Run("docker", "exec", containerName, "rm", "-rf", workdir)
|
||||||
@@ -1341,7 +1346,19 @@ func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID
|
|||||||
if pkgPath == "" {
|
if pkgPath == "" {
|
||||||
return fmt.Errorf("openclaw plugin package not found")
|
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 {
|
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 hermesWorkspaceDir = "/opt/data/workspace"
|
||||||
const hermesExecutablePath = "/opt/hermes/.venv/bin/hermes"
|
const hermesExecutablePath = "/opt/hermes/.venv/bin/hermes"
|
||||||
const hermesDashboardUsernameEnvKey = "HERMES_DASHBOARD_USERNAME"
|
|
||||||
const hermesDashboardPasswordEnvKey = "HERMES_DASHBOARD_PASSWORD"
|
|
||||||
|
|
||||||
type hermesDashboardAuth struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
type hermesConfig struct {
|
type hermesConfig struct {
|
||||||
Model hermesModelConfig `yaml:"model"`
|
Model hermesModelConfig `yaml:"model"`
|
||||||
@@ -117,52 +110,6 @@ func prepareHermesInstallFiles(appInstall *model.AppInstall, account *model.Agen
|
|||||||
return files.NewFileOp().ChownR(dataDir, "1000", "1000", true)
|
return files.NewFileOp().ChownR(dataDir, "1000", "1000", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeHermesDashboardAuth(username, password string) hermesDashboardAuth {
|
|
||||||
auth := hermesDashboardAuth{
|
|
||||||
Username: strings.TrimSpace(username),
|
|
||||||
Password: strings.TrimSpace(password),
|
|
||||||
}
|
|
||||||
if auth.Username == "" {
|
|
||||||
auth.Username = "admin"
|
|
||||||
}
|
|
||||||
if auth.Password == "" {
|
|
||||||
auth.Password = common.RandStr(8)
|
|
||||||
}
|
|
||||||
return auth
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeHermesDashboardAuthEnv(envPath string, auth hermesDashboardAuth, overwrite bool) error {
|
|
||||||
return upsertAgentEnv(envPath, map[string]string{
|
|
||||||
hermesDashboardUsernameEnvKey: auth.Username,
|
|
||||||
hermesDashboardPasswordEnvKey: auth.Password,
|
|
||||||
}, []string{
|
|
||||||
hermesDashboardUsernameEnvKey,
|
|
||||||
hermesDashboardPasswordEnvKey,
|
|
||||||
}, overwrite)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readHermesDashboardAuthEnv(envPath string) (hermesDashboardAuth, error) {
|
|
||||||
envMap, err := readAgentEnvMap(envPath)
|
|
||||||
if err != nil {
|
|
||||||
return hermesDashboardAuth{}, err
|
|
||||||
}
|
|
||||||
return hermesDashboardAuth{
|
|
||||||
Username: strings.TrimSpace(envMap[hermesDashboardUsernameEnvKey]),
|
|
||||||
Password: strings.TrimSpace(envMap[hermesDashboardPasswordEnvKey]),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readHermesDashboardAuthFromInstall(appInstall *model.AppInstall) hermesDashboardAuth {
|
|
||||||
if appInstall == nil || appInstall.ID == 0 {
|
|
||||||
return hermesDashboardAuth{}
|
|
||||||
}
|
|
||||||
auth, err := readHermesDashboardAuthEnv(path.Join(appInstall.GetPath(), ".env"))
|
|
||||||
if err != nil {
|
|
||||||
return hermesDashboardAuth{}
|
|
||||||
}
|
|
||||||
return auth
|
|
||||||
}
|
|
||||||
|
|
||||||
func readHermesConfig(configPath string) (*hermesConfig, error) {
|
func readHermesConfig(configPath string) (*hermesConfig, error) {
|
||||||
content, err := files.NewFileOp().GetContent(configPath)
|
content, err := files.NewFileOp().GetContent(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -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"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -21,6 +22,7 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
|
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||||
@@ -96,7 +98,7 @@ func ensureContainerRunning(containerName string) error {
|
|||||||
return nil
|
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)
|
resolvedAPIKey := strings.TrimSpace(apiKey)
|
||||||
resolvedAPIType := strings.TrimSpace(apiType)
|
resolvedAPIType := strings.TrimSpace(apiType)
|
||||||
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||||
@@ -114,7 +116,8 @@ func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, mode
|
|||||||
if modelID == "" {
|
if modelID == "" {
|
||||||
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
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 {
|
if err := providercatalog.VerifyAccount(provider, resolvedAPIType, resolvedAuthMode, resolvedBaseURL, resolvedAPIKey, modelID); err != nil {
|
||||||
return resolvedAgentAccountInput{}, err
|
return resolvedAgentAccountInput{}, err
|
||||||
}
|
}
|
||||||
@@ -386,7 +389,7 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
|||||||
Remark: agent.Remark,
|
Remark: agent.Remark,
|
||||||
AgentType: agentType,
|
AgentType: agentType,
|
||||||
Provider: agent.Provider,
|
Provider: agent.Provider,
|
||||||
ProviderName: providercatalog.DisplayName(agent.Provider),
|
ProviderName: localizedAgentProviderName(agent.Provider),
|
||||||
Model: agent.Model,
|
Model: agent.Model,
|
||||||
APIType: agent.APIType,
|
APIType: agent.APIType,
|
||||||
BaseURL: agent.BaseURL,
|
BaseURL: agent.BaseURL,
|
||||||
@@ -420,8 +423,8 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
|||||||
item.BridgePort = toInt(bridge)
|
item.BridgePort = toInt(bridge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if agentType == constant.AppHermesAgent {
|
if _, _, ok := agentDashboardAuthEnvKeys(agentType); ok {
|
||||||
auth := readHermesDashboardAuthFromInstall(appInstall)
|
auth := readAgentDashboardAuthFromInstall(appInstall, agentType)
|
||||||
item.DashboardUsername = auth.Username
|
item.DashboardUsername = auth.Username
|
||||||
item.DashboardPassword = auth.Password
|
item.DashboardPassword = auth.Password
|
||||||
}
|
}
|
||||||
@@ -429,6 +432,15 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
|||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func localizedAgentProviderName(provider string) string {
|
||||||
|
if key := providercatalog.DisplayNameKey(provider); key != "" {
|
||||||
|
if name := strings.TrimSpace(i18n.GetMsgByKey(key)); name != "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return providercatalog.DisplayName(provider)
|
||||||
|
}
|
||||||
|
|
||||||
func isAgentAppKey(appKey string) bool {
|
func isAgentAppKey(appKey string) bool {
|
||||||
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
||||||
}
|
}
|
||||||
@@ -726,9 +738,11 @@ type modelProvider struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type modelEntry struct {
|
type modelEntry struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Input []string `json:"input,omitempty"`
|
Input []string `json:"input,omitempty"`
|
||||||
|
ContextWindow int `json:"contextWindow,omitempty"`
|
||||||
|
MaxTokens int `json:"maxTokens,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func requiresOpenclawProviderModels(provider string) bool {
|
func requiresOpenclawProviderModels(provider string) bool {
|
||||||
@@ -756,7 +770,7 @@ type browserConfig struct {
|
|||||||
DefaultProfile string `json:"defaultProfile"`
|
DefaultProfile string `json:"defaultProfile"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string) error {
|
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string, metadata []dto.AgentModelMetadata) error {
|
||||||
if strings.TrimSpace(confDir) == "" {
|
if strings.TrimSpace(confDir) == "" {
|
||||||
return fmt.Errorf("config dir is required")
|
return fmt.Errorf("config dir is required")
|
||||||
}
|
}
|
||||||
@@ -841,6 +855,7 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
|||||||
}
|
}
|
||||||
conf = initial
|
conf = initial
|
||||||
} else {
|
} else {
|
||||||
|
preserveOpenclawModelMetadata(conf, cfg.Models)
|
||||||
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
|
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -895,6 +910,9 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
|||||||
if allowedOrigins != nil {
|
if allowedOrigins != nil {
|
||||||
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
|
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
|
||||||
}
|
}
|
||||||
|
if err := applyOpenclawModelMetadata(conf, account, metadata); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
|
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -909,6 +927,144 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
|||||||
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order)
|
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readOpenclawModelsConfig(conf map[string]interface{}) *modelsConfig {
|
||||||
|
raw, ok := conf["models"]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
payload, err := json.Marshal(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var models modelsConfig
|
||||||
|
if err := json.Unmarshal(payload, &models); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &models
|
||||||
|
}
|
||||||
|
|
||||||
|
func preserveOpenclawModelMetadata(conf map[string]interface{}, next *modelsConfig) {
|
||||||
|
current := readOpenclawModelsConfig(conf)
|
||||||
|
if current == nil || next == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for providerID, nextProvider := range next.Providers {
|
||||||
|
currentProvider, ok := current.Providers[providerID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byID := make(map[string]modelEntry, len(currentProvider.Models))
|
||||||
|
for _, entry := range currentProvider.Models {
|
||||||
|
byID[entry.ID] = entry
|
||||||
|
}
|
||||||
|
for index := range nextProvider.Models {
|
||||||
|
currentEntry, ok := byID[nextProvider.Models[index].ID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nextProvider.Models[index].Input = currentEntry.Input
|
||||||
|
nextProvider.Models[index].ContextWindow = currentEntry.ContextWindow
|
||||||
|
nextProvider.Models[index].MaxTokens = currentEntry.MaxTokens
|
||||||
|
}
|
||||||
|
next.Providers[providerID] = nextProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, accountModels []dto.AgentAccountModel) []dto.AgentModelMetadata {
|
||||||
|
result := make([]dto.AgentModelMetadata, 0, len(accountModels))
|
||||||
|
configured := readOpenclawModelsConfig(conf)
|
||||||
|
for _, item := range accountModels {
|
||||||
|
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
metadata := dto.AgentModelMetadata{Model: item.ID, InputMode: "auto"}
|
||||||
|
if configured != nil {
|
||||||
|
for _, entry := range configured.Providers[providerID].Models {
|
||||||
|
if entry.ID != inferred.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
metadata.ContextWindow = entry.ContextWindow
|
||||||
|
metadata.MaxTokens = entry.MaxTokens
|
||||||
|
if len(entry.Input) > 0 && !slices.Equal(entry.Input, inferred.Input) {
|
||||||
|
if slices.Contains(entry.Input, "image") {
|
||||||
|
metadata.InputMode = "image"
|
||||||
|
} else {
|
||||||
|
metadata.InputMode = "text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, metadata)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, requested []dto.AgentModelMetadata) error {
|
||||||
|
if len(requested) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
configured := readOpenclawModelsConfig(conf)
|
||||||
|
if configured == nil {
|
||||||
|
return fmt.Errorf("model metadata is not supported for provider %s", account.Provider)
|
||||||
|
}
|
||||||
|
accountModels, err := loadAgentAccountModels(account)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
available := make(map[string]dto.AgentAccountModel, len(accountModels))
|
||||||
|
for _, item := range accountModels {
|
||||||
|
available[item.ID] = item
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{}, len(requested))
|
||||||
|
for _, metadata := range requested {
|
||||||
|
item, ok := available[metadata.Model]
|
||||||
|
if !ok {
|
||||||
|
return buserr.New("ErrAgentModelNotInAccount")
|
||||||
|
}
|
||||||
|
if _, ok := seen[metadata.Model]; ok {
|
||||||
|
return fmt.Errorf("duplicate model metadata: %s", metadata.Model)
|
||||||
|
}
|
||||||
|
seen[metadata.Model] = struct{}{}
|
||||||
|
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
provider := configured.Providers[providerID]
|
||||||
|
found := false
|
||||||
|
for index := range provider.Models {
|
||||||
|
if provider.Models[index].ID != inferred.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
provider.Models[index].ContextWindow = metadata.ContextWindow
|
||||||
|
provider.Models[index].MaxTokens = metadata.MaxTokens
|
||||||
|
switch metadata.InputMode {
|
||||||
|
case "auto":
|
||||||
|
provider.Models[index].Input = inferred.Input
|
||||||
|
case "text":
|
||||||
|
provider.Models[index].Input = []string{"text"}
|
||||||
|
case "image":
|
||||||
|
provider.Models[index].Input = []string{"text", "image"}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported model input mode: %s", metadata.InputMode)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return buserr.New("ErrAgentModelNotInAccount")
|
||||||
|
}
|
||||||
|
configured.Providers[providerID] = provider
|
||||||
|
}
|
||||||
|
modelsMap, err := structToMap(configured)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conf["models"] = modelsMap
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
|
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
|
||||||
accountModels, err := loadAgentAccountModels(account)
|
accountModels, err := loadAgentAccountModels(account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1030,7 +1186,7 @@ func prepareOpenclawInstallFiles(appInstall *model.AppInstall, account *model.Ag
|
|||||||
return fmt.Errorf("app install is required")
|
return fmt.Errorf("app install is required")
|
||||||
}
|
}
|
||||||
confDir := path.Join(appInstall.GetPath(), "data", "conf")
|
confDir := path.Join(appInstall.GetPath(), "data", "conf")
|
||||||
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil); err != nil {
|
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dataDir := path.Join(appInstall.GetPath(), "data")
|
dataDir := path.Join(appInstall.GetPath(), "data")
|
||||||
@@ -1159,15 +1315,15 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto
|
|||||||
if len(requested) > 0 {
|
if len(requested) > 0 {
|
||||||
return normalizeAgentAccountModels(account, requested)
|
return normalizeAgentAccountModels(account, requested)
|
||||||
}
|
}
|
||||||
meta, ok := providercatalog.Get(account.Provider)
|
defaultModels := providercatalog.DefaultModels(account.Provider, account.APIType)
|
||||||
if !ok || len(meta.Models) == 0 {
|
if len(defaultModels) == 0 {
|
||||||
if requiresInitialAgentAccountModels(account.Provider) {
|
if requiresInitialAgentAccountModels(account.Provider) {
|
||||||
return nil, buserr.New("ErrAgentAccountModelsRequired")
|
return nil, buserr.New("ErrAgentAccountModelsRequired")
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
requested = make([]dto.AgentAccountModel, 0, len(meta.Models))
|
requested = make([]dto.AgentAccountModel, 0, len(defaultModels))
|
||||||
for _, item := range meta.Models {
|
for _, item := range defaultModels {
|
||||||
requested = append(requested, dto.AgentAccountModel{
|
requested = append(requested, dto.AgentAccountModel{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
@@ -1327,7 +1483,7 @@ func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAcco
|
|||||||
|
|
||||||
func requiresInitialAgentAccountModels(provider string) bool {
|
func requiresInitialAgentAccountModels(provider string) bool {
|
||||||
switch provider {
|
switch provider {
|
||||||
case "custom", "vllm", "ollama":
|
case "custom", "vllm", "ollama", "llmman":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@@ -1452,6 +1608,87 @@ func readInstallEnv(envStr string) map[string]interface{} {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
hermesDashboardUsernameEnvKey = "HERMES_DASHBOARD_USERNAME"
|
||||||
|
hermesDashboardPasswordEnvKey = "HERMES_DASHBOARD_PASSWORD"
|
||||||
|
qwenPawAuthEnabledEnvKey = "QWENPAW_AUTH_ENABLED"
|
||||||
|
qwenPawAuthUsernameEnvKey = "QWENPAW_AUTH_USERNAME"
|
||||||
|
qwenPawAuthPasswordEnvKey = "QWENPAW_AUTH_PASSWORD"
|
||||||
|
)
|
||||||
|
|
||||||
|
type agentDashboardAuth struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAgentDashboardAuth(username, password string) agentDashboardAuth {
|
||||||
|
auth := agentDashboardAuth{
|
||||||
|
Username: strings.TrimSpace(username),
|
||||||
|
Password: strings.TrimSpace(password),
|
||||||
|
}
|
||||||
|
if auth.Username == "" {
|
||||||
|
auth.Username = "admin"
|
||||||
|
}
|
||||||
|
if auth.Password == "" {
|
||||||
|
auth.Password = common.RandStr(8)
|
||||||
|
}
|
||||||
|
return auth
|
||||||
|
}
|
||||||
|
|
||||||
|
func agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
|
||||||
|
switch agentType {
|
||||||
|
case constant.AppHermesAgent:
|
||||||
|
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
|
||||||
|
case constant.AppCopaw:
|
||||||
|
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
|
||||||
|
default:
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAgentDashboardAuthEnv(envPath, agentType string, auth agentDashboardAuth, overwrite bool) error {
|
||||||
|
usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("dashboard auth is not supported for %s", agentType)
|
||||||
|
}
|
||||||
|
values := map[string]string{
|
||||||
|
usernameKey: auth.Username,
|
||||||
|
passwordKey: auth.Password,
|
||||||
|
}
|
||||||
|
order := []string{usernameKey, passwordKey}
|
||||||
|
if agentType == constant.AppCopaw {
|
||||||
|
values[qwenPawAuthEnabledEnvKey] = "true"
|
||||||
|
order = append([]string{qwenPawAuthEnabledEnvKey}, order...)
|
||||||
|
}
|
||||||
|
return upsertAgentEnv(envPath, values, order, overwrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAgentDashboardAuthEnv(envPath, agentType string) (agentDashboardAuth, error) {
|
||||||
|
usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType)
|
||||||
|
if !ok {
|
||||||
|
return agentDashboardAuth{}, fmt.Errorf("dashboard auth is not supported for %s", agentType)
|
||||||
|
}
|
||||||
|
envMap, err := readAgentEnvMap(envPath)
|
||||||
|
if err != nil {
|
||||||
|
return agentDashboardAuth{}, err
|
||||||
|
}
|
||||||
|
return agentDashboardAuth{
|
||||||
|
Username: strings.TrimSpace(envMap[usernameKey]),
|
||||||
|
Password: strings.TrimSpace(envMap[passwordKey]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAgentDashboardAuthFromInstall(appInstall *model.AppInstall, agentType string) agentDashboardAuth {
|
||||||
|
if appInstall == nil || appInstall.ID == 0 {
|
||||||
|
return agentDashboardAuth{}
|
||||||
|
}
|
||||||
|
auth, err := readAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType)
|
||||||
|
if err != nil {
|
||||||
|
return agentDashboardAuth{}
|
||||||
|
}
|
||||||
|
return auth
|
||||||
|
}
|
||||||
|
|
||||||
func readAgentEnvMap(envPath string) (map[string]string, error) {
|
func readAgentEnvMap(envPath string) (map[string]string, error) {
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
if !fileOp.Stat(envPath) {
|
if !fileOp.Stat(envPath) {
|
||||||
|
|||||||
+333
-30
@@ -17,10 +17,13 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
|
||||||
|
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/email"
|
"github.com/1Panel-dev/1Panel/agent/utils/email"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
|
||||||
"github.com/shirou/gopsutil/v4/disk"
|
"github.com/shirou/gopsutil/v4/disk"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,6 +37,28 @@ var communityAlertMethodTypeNames = map[string]string{
|
|||||||
constant.SMS: "SMS",
|
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 {
|
type IAlertService interface {
|
||||||
PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error)
|
PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error)
|
||||||
GetAlerts() ([]dto.AlertDTO, error)
|
GetAlerts() ([]dto.AlertDTO, error)
|
||||||
@@ -53,8 +78,10 @@ type IAlertService interface {
|
|||||||
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
|
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
|
||||||
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
|
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
|
||||||
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
|
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
|
||||||
|
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
|
||||||
DeleteAlertConfig(id uint) error
|
DeleteAlertConfig(id uint) error
|
||||||
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
||||||
|
TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAlertService() IAlertService {
|
func NewIAlertService() IAlertService {
|
||||||
@@ -180,9 +207,15 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
|
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
|
||||||
if err := a.validateCommunityAlertMethod(req.Method); err != nil {
|
methodTypes, err := a.validateAlertMethodReferences(req.Method)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if req.Status != constant.AlertDisable {
|
||||||
|
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
upMap := make(map[string]interface{})
|
upMap := make(map[string]interface{})
|
||||||
upMap["id"] = req.ID
|
upMap["id"] = req.ID
|
||||||
@@ -240,7 +273,16 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
|
|||||||
if alertInfo.ID == 0 {
|
if alertInfo.ID == 0 {
|
||||||
return buserr.New("ErrRecordNotFound")
|
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.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = alertRepo.Update(map[string]interface{}{"status": status}, repo.WithByID(alertInfo.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -340,7 +382,7 @@ func executeDiskCommand() (string, error) {
|
|||||||
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
|
||||||
stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P")
|
stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil && strings.TrimSpace(stdout) == "" {
|
||||||
return stdout, err
|
return stdout, err
|
||||||
}
|
}
|
||||||
var lines []string
|
var lines []string
|
||||||
@@ -384,6 +426,9 @@ func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.Ale
|
|||||||
if search.Count != 0 {
|
if search.Count != 0 {
|
||||||
opts = append(opts, alertRepo.WithByCount(search.Count))
|
opts = append(opts, alertRepo.WithByCount(search.Count))
|
||||||
}
|
}
|
||||||
|
if !search.StartTime.IsZero() && !search.EndTime.IsZero() {
|
||||||
|
opts = append(opts, repo.WithByCreatedAt(search.StartTime, search.EndTime))
|
||||||
|
}
|
||||||
opts = append(opts, repo.WithOrderDesc("created_at"))
|
opts = append(opts, repo.WithOrderDesc("created_at"))
|
||||||
|
|
||||||
total, alerts, err := alertRepo.PageLog(search.Page, search.PageSize, opts...)
|
total, alerts, err := alertRepo.PageLog(search.Page, search.PageSize, opts...)
|
||||||
@@ -409,6 +454,7 @@ func (a AlertService) parseAlertLog(item model.AlertLog) (dto.AlertLogDTO, error
|
|||||||
if err := unmarshalAlertInfo(item.AlertDetail, &alertDetail); err != nil {
|
if err := unmarshalAlertInfo(item.AlertDetail, &alertDetail); err != nil {
|
||||||
return dto.AlertLogDTO{}, err
|
return dto.AlertLogDTO{}, err
|
||||||
}
|
}
|
||||||
|
alertDetail.Task = nil
|
||||||
if err := unmarshalAlertInfo(item.AlertRule, &alertRule); err != nil {
|
if err := unmarshalAlertInfo(item.AlertRule, &alertRule); err != nil {
|
||||||
return dto.AlertLogDTO{}, err
|
return dto.AlertLogDTO{}, err
|
||||||
}
|
}
|
||||||
@@ -491,7 +537,13 @@ func (a AlertService) GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertCon
|
|||||||
}
|
}
|
||||||
opts = append(opts, repo.WithByStatus(constant.AlertEnable))
|
opts = append(opts, repo.WithByStatus(constant.AlertEnable))
|
||||||
configs, err := alertRepo.AlertConfigList(opts...)
|
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) {
|
func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error) {
|
||||||
@@ -502,13 +554,49 @@ func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []mode
|
|||||||
if len(req.ExcludeTypes) > 0 {
|
if len(req.ExcludeTypes) > 0 {
|
||||||
opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes))
|
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 {
|
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 {
|
if err := a.validateCommunityAlertConfigType(req.Type); err != nil {
|
||||||
return err
|
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 {
|
if err := a.checkAlertConfigDisplayNameUnique(req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -523,7 +611,7 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator stri
|
|||||||
upMap["status"] = req.Status
|
upMap["status"] = req.Status
|
||||||
upMap["config"] = req.Config
|
upMap["config"] = req.Config
|
||||||
upMap["update_user"] = operator
|
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
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -541,6 +629,99 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator stri
|
|||||||
return nil
|
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 {
|
func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate) error {
|
||||||
if req.Type != constant.SMSConfig {
|
if req.Type != constant.SMSConfig {
|
||||||
return nil
|
return nil
|
||||||
@@ -565,6 +746,9 @@ func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdate) error {
|
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)
|
displayName := alertConfigDisplayName(req.Type, req.Config)
|
||||||
if displayName == "" {
|
if displayName == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -588,37 +772,67 @@ func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdat
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) validateCommunityAlertMethod(method string) error {
|
func (a AlertService) validateCommunityAlertMethod(method string) error {
|
||||||
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
|
methodTypes, err := a.validateAlertMethodReferences(method)
|
||||||
return nil
|
if err != nil {
|
||||||
}
|
return err
|
||||||
if strings.TrimSpace(method) == "" {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
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, ",") {
|
for _, item := range strings.Split(method, ",") {
|
||||||
item = strings.TrimSpace(item)
|
item = strings.TrimSpace(item)
|
||||||
if item == "" {
|
if item == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
configType := ""
|
||||||
if configID, err := strconv.ParseUint(item, 10, 64); err == nil {
|
if configID, err := strconv.ParseUint(item, 10, 64); err == nil {
|
||||||
config, err := alertRepo.GetConfigById(uint(configID))
|
config, err := alertRepo.GetConfigById(uint(configID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, ok := communityAlertMethodTypeNames[config.Type]; ok {
|
configType = config.Type
|
||||||
return buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
} 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
|
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 buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) validateCommunityAlertConfigType(configType string) error {
|
func (a AlertService) validateCommunityAlertConfigType(configType string) error {
|
||||||
|
if configType == constant.Custom {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
|
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -630,7 +844,7 @@ func (a AlertService) validateCommunityAlertConfigType(configType string) error
|
|||||||
|
|
||||||
func alertConfigDisplayName(configType, configData string) string {
|
func alertConfigDisplayName(configType, configData string) string {
|
||||||
switch configType {
|
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 {
|
var cfg struct {
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
}
|
}
|
||||||
@@ -669,20 +883,24 @@ func (a AlertService) DeleteAlertConfig(id uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
||||||
username := req.UserName
|
emailConfig, err := resolveEmailTestConfig(req)
|
||||||
if username == "" {
|
if err != nil {
|
||||||
username = req.Sender
|
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{
|
cfg := email.SMTPConfig{
|
||||||
Host: req.Host,
|
Host: emailConfig.Host,
|
||||||
Port: req.Port,
|
Port: emailConfig.Port,
|
||||||
Sender: req.Sender,
|
Sender: emailConfig.Sender,
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: req.Password,
|
Password: emailConfig.Password,
|
||||||
From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, req.Sender),
|
From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, emailConfig.Sender),
|
||||||
Encryption: req.Encryption,
|
Encryption: emailConfig.Encryption,
|
||||||
Recipient: req.Recipient,
|
Recipient: emailConfig.Recipient,
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := email.EmailMessage{
|
msg := email.EmailMessage{
|
||||||
@@ -697,9 +915,94 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
|||||||
return true, nil
|
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 {
|
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
|
||||||
if err := a.validateCommunityAlertMethod(updateAlert.Method); err != nil {
|
var methodTypes []string
|
||||||
return err
|
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{})
|
upMap := make(map[string]interface{})
|
||||||
var newStatus string
|
var newStatus string
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
@@ -32,6 +33,7 @@ const (
|
|||||||
ResourceAlertInterval = 30
|
ResourceAlertInterval = 30
|
||||||
CheckIntervalSec = 3
|
CheckIntervalSec = 3
|
||||||
LoadCheckIntervalMin = 5
|
LoadCheckIntervalMin = 5
|
||||||
|
sshIPLoginWindow = 30 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
type AlertTaskHelper struct {
|
type AlertTaskHelper struct {
|
||||||
@@ -512,10 +514,28 @@ func loadPanelLogin(alert dto.AlertDTO) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadSSHLogin(alert dto.AlertDTO) {
|
func loadSSHLogin(alert dto.AlertDTO) {
|
||||||
count, isAlert, err := alertUtil.CountRecentFailedSSHLog(alert.Cycle, alert.Count)
|
now := time.Now()
|
||||||
if err != nil {
|
failedWindow := time.Duration(alert.Cycle) * time.Minute
|
||||||
global.LOG.Errorf("Failed to count recent failed ssh login logs: %v", err)
|
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)
|
||||||
|
}
|
||||||
|
count, records := summarizeSSHLoginHistories(
|
||||||
|
histories,
|
||||||
|
now,
|
||||||
|
failedWindow,
|
||||||
|
strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n"),
|
||||||
|
)
|
||||||
|
isAlert := count >= int(alert.Count)
|
||||||
if isAlert {
|
if isAlert {
|
||||||
params := []dto.Param{
|
params := []dto.Param{
|
||||||
{
|
{
|
||||||
@@ -531,12 +551,6 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
|||||||
}
|
}
|
||||||
sendAlerts(alert, "sshLogin", strconv.Itoa(count), "sshLogin", params)
|
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 {
|
if len(records) > 0 {
|
||||||
quota := strings.Join(records, "\n")
|
quota := strings.Join(records, "\n")
|
||||||
params := []dto.Param{
|
params := []dto.Param{
|
||||||
@@ -565,20 +579,6 @@ func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string)
|
|||||||
return filtered
|
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 {
|
func isIPInWhitelist(ip string, whitelist []string) bool {
|
||||||
targetIP := net.ParseIP(strings.TrimSpace(ip))
|
targetIP := net.ParseIP(strings.TrimSpace(ip))
|
||||||
if targetIP == nil {
|
if targetIP == nil {
|
||||||
@@ -695,9 +695,10 @@ func sendAlertsByConfigId(alert dto.AlertDTO, alertType, quota, quotaType string
|
|||||||
|
|
||||||
func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, method string) {
|
func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, method string) {
|
||||||
typeMap := map[string]string{
|
typeMap := map[string]string{
|
||||||
"mail": constant.Email,
|
"mail": constant.Email,
|
||||||
constant.Bark: constant.Bark,
|
constant.Bark: constant.Bark,
|
||||||
constant.SMS: constant.SMS,
|
constant.SMS: constant.SMS,
|
||||||
|
constant.Custom: constant.Custom,
|
||||||
}
|
}
|
||||||
configType, ok := typeMap[method]
|
configType, ok := typeMap[method]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -785,7 +786,7 @@ func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params
|
|||||||
}
|
}
|
||||||
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
|
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)
|
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr)
|
||||||
if !isValid {
|
if !isValid {
|
||||||
return
|
return
|
||||||
@@ -798,12 +799,31 @@ func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params
|
|||||||
}
|
}
|
||||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||||
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
|
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 {
|
if alertErr != nil {
|
||||||
global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, methodStr, alertErr)
|
global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, methodStr, alertErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
|
if !queued {
|
||||||
|
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1097,3 +1117,55 @@ func calculateMinutesDifference(newDate time.Time) int {
|
|||||||
minutesDifference := int(now.Sub(newDate).Minutes())
|
minutesDifference := int(now.Sub(newDate).Minutes())
|
||||||
return minutesDifference
|
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 {
|
} else {
|
||||||
s.sendBarkWithConfig(config, quota, params)
|
s.sendBarkWithConfig(config, quota, params)
|
||||||
}
|
}
|
||||||
case constant.WeCom, constant.DingTalk, constant.FeiShu:
|
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Custom:
|
||||||
if isResource {
|
if isResource {
|
||||||
s.sendResourceWebhookWithConfig(config, quota, params)
|
s.sendResourceWebhookWithConfig(config, quota, params)
|
||||||
} else {
|
} 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) {
|
func (s *AlertSender) sendByLegacyMethod(method string, quota string, params []dto.Param, isResource bool) {
|
||||||
alertRepo := repo.NewIAlertRepo()
|
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
|
configType := method
|
||||||
if mapped, ok := typeMap[method]; ok {
|
if mapped, ok := typeMap[method]; ok {
|
||||||
configType = mapped
|
configType = mapped
|
||||||
@@ -308,12 +308,31 @@ func (s *AlertSender) sendWebhookWithConfig(config model.AlertConfig, quota stri
|
|||||||
}
|
}
|
||||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||||
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
|
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 {
|
if err != nil {
|
||||||
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
|
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
|
||||||
return
|
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) {
|
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()
|
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||||
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
|
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)
|
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
|
||||||
return
|
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) {
|
func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) {
|
||||||
|
|||||||
+20
-13
@@ -223,6 +223,9 @@ func (a AppService) GetAppDetailByKey(appKey, version string) (response.AppDetai
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return appDetailDTO, err
|
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))
|
appDetail, err := appDetailRepo.GetFirst(appDetailRepo.WithAppId(app.ID), appDetailRepo.WithVersion(version))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return appDetailDTO, err
|
return appDetailDTO, err
|
||||||
@@ -241,14 +244,17 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return appDetailDTO, err
|
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.AppDetail = detail
|
||||||
appDetailDTO.Enable = true
|
appDetailDTO.Enable = true
|
||||||
|
|
||||||
if appType == "runtime" {
|
if appType == "runtime" {
|
||||||
app, err := appRepo.GetFirst(repo.WithByID(appID))
|
|
||||||
if err != nil {
|
|
||||||
return appDetailDTO, err
|
|
||||||
}
|
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
|
|
||||||
versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version)
|
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)
|
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 {
|
if err := checkLimit(app); err != nil {
|
||||||
appDetailDTO.Enable = false
|
appDetailDTO.Enable = false
|
||||||
}
|
}
|
||||||
@@ -374,6 +376,9 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err = checkVllmVersionAccess(app.Key, appDetail.Version); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if DatabaseKeys[app.Key] > 0 {
|
if DatabaseKeys[app.Key] > 0 {
|
||||||
if existDatabases, _ := databaseRepo.GetList(repo.WithByName(req.Name)); len(existDatabases) > 0 {
|
if existDatabases, _ := databaseRepo.GetList(repo.WithByName(req.Name)); len(existDatabases) > 0 {
|
||||||
err = buserr.New("ErrRemoteExist")
|
err = buserr.New("ErrRemoteExist")
|
||||||
@@ -483,15 +488,17 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
newServiceName := strings.ToLower(appInstall.Name)
|
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]
|
servicesMap[newServiceName] = servicesMap[serviceName]
|
||||||
delete(servicesMap, serviceName)
|
delete(servicesMap, serviceName)
|
||||||
serviceName = newServiceName
|
serviceName = newServiceName
|
||||||
}
|
}
|
||||||
appInstall.ServiceName = serviceName
|
appInstall.ServiceName = serviceName
|
||||||
|
|
||||||
if err = addDockerComposeCommonParam(composeMap, appInstall.ServiceName, req.AppContainerConfig, req.Params); err != nil {
|
if !req.SkipComposeCommonConfig {
|
||||||
return
|
if err = addDockerComposeCommonParam(composeMap, appInstall.ServiceName, req.AppContainerConfig, req.Params); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
composeByte []byte
|
composeByte []byte
|
||||||
@@ -559,7 +566,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if executeScript {
|
if executeScript || req.UseLifecycleScripts {
|
||||||
if err = runScript(t, appInstall, "init"); err != nil {
|
if err = runScript(t, appInstall, "init"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -572,7 +579,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = upApp(t, appInstall, req.PullImage); err != nil {
|
if err = upApp(t, appInstall, req.PullImage, req.UseLifecycleScripts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
updateToolApp(appInstall)
|
updateToolApp(appInstall)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -13,12 +14,14 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
"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/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
@@ -252,6 +255,9 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
|||||||
return buserr.New("ErrInstallDirNotFound")
|
return buserr.New("ErrInstallDirNotFound")
|
||||||
}
|
}
|
||||||
dockerComposePath := install.GetComposePath()
|
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 {
|
switch req.Operate {
|
||||||
case constant.Rebuild:
|
case constant.Rebuild:
|
||||||
return rebuildApp(install)
|
return rebuildApp(install)
|
||||||
@@ -275,12 +281,13 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
|||||||
return syncAppInstallStatus(&install, false)
|
return syncAppInstallStatus(&install, false)
|
||||||
case constant.Delete:
|
case constant.Delete:
|
||||||
deleteReq := request.AppInstallDelete{
|
deleteReq := request.AppInstallDelete{
|
||||||
Install: install,
|
Install: install,
|
||||||
DeleteBackup: req.DeleteBackup,
|
DeleteBackup: req.DeleteBackup,
|
||||||
ForceDelete: req.ForceDelete,
|
ForceDelete: req.ForceDelete,
|
||||||
DeleteDB: req.DeleteDB,
|
DeleteDB: req.DeleteDB,
|
||||||
DeleteImage: req.DeleteImage,
|
DeleteImage: req.DeleteImage,
|
||||||
TaskID: req.TaskID,
|
TaskID: req.TaskID,
|
||||||
|
UseLifecycleScripts: req.UseLifecycleScripts,
|
||||||
}
|
}
|
||||||
if err = deleteAppInstall(deleteReq); err != nil && !req.ForceDelete {
|
if err = deleteAppInstall(deleteReq); err != nil && !req.ForceDelete {
|
||||||
return err
|
return err
|
||||||
@@ -312,6 +319,70 @@ 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 {
|
||||||
|
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 {
|
func (a *AppInstallService) UpdateAppConfig(req request.AppConfigUpdate) error {
|
||||||
installed, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
|
installed, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -374,8 +445,10 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params); err != nil {
|
if !req.SkipComposeCommonConfig {
|
||||||
return err
|
if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
composeByte, err := yaml.Marshal(composeMap)
|
composeByte, err := yaml.Marshal(composeMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -408,7 +481,7 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
backupEnvMaps := oldEnvMaps
|
backupEnvMaps := maps.Clone(oldEnvMaps)
|
||||||
handleMap(req.Params, oldEnvMaps)
|
handleMap(req.Params, oldEnvMaps)
|
||||||
paramByte, err := json.Marshal(oldEnvMaps)
|
paramByte, err := json.Marshal(oldEnvMaps)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -420,13 +493,32 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
|
|||||||
}
|
}
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
_ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(installed.DockerCompose), constant.DirPerm)
|
_ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(installed.DockerCompose), constant.DirPerm)
|
||||||
if err := rebuildApp(installed); err != nil {
|
restoreConfig := func(operationErr error) {
|
||||||
_ = env.Write(backupEnvMaps, envPath)
|
_ = env.Write(backupEnvMaps, envPath)
|
||||||
_ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(backupDockerCompose), constant.DirPerm)
|
_ = 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
|
return err
|
||||||
}
|
}
|
||||||
installed.Status = constant.StatusRunning
|
if !req.UseLifecycleScripts {
|
||||||
_ = appInstallRepo.Save(context.Background(), &installed)
|
installed.Status = constant.StatusRunning
|
||||||
|
_ = appInstallRepo.Save(context.Background(), &installed)
|
||||||
|
}
|
||||||
|
|
||||||
proxyChanged := hasAppInstallProxyPassChanged(&oldInstalled, &installed)
|
proxyChanged := hasAppInstallProxyPassChanged(&oldInstalled, &installed)
|
||||||
currentProxy, currentProxyErr := getAppInstallProxyPass(&installed)
|
currentProxy, currentProxyErr := getAppInstallProxyPass(&installed)
|
||||||
@@ -583,6 +675,9 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
|
|||||||
return versions, err
|
return versions, err
|
||||||
}
|
}
|
||||||
for _, detail := range details {
|
for _, detail := range details {
|
||||||
|
if !canAccessVllmVersion(app.Key, detail.Version) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version"))
|
||||||
if len(ignores) > 0 {
|
if len(ignores) > 0 {
|
||||||
continue
|
continue
|
||||||
@@ -836,7 +931,9 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
||||||
if appInstall.Status == constant.StatusInstalling || appInstall.Status == constant.StatusRebuilding || appInstall.Status == constant.StatusUpgrading || appInstall.Status == constant.StatusUninstalling {
|
switch appInstall.Status {
|
||||||
|
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
|
||||||
|
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
cli, err := docker.NewClient()
|
cli, err := docker.NewClient()
|
||||||
|
|||||||
@@ -0,0 +1,951 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type appUpgradePhase int
|
||||||
|
|
||||||
|
const (
|
||||||
|
appUpgradePreparing appUpgradePhase = iota
|
||||||
|
appUpgradePrepared
|
||||||
|
appUpgradeStopped
|
||||||
|
appUpgradeBackedUp
|
||||||
|
appUpgradeDown
|
||||||
|
appUpgradeMutated
|
||||||
|
appUpgradeStarted
|
||||||
|
appUpgradeReady
|
||||||
|
appUpgradeCommitted
|
||||||
|
)
|
||||||
|
|
||||||
|
const composeServiceLabel = "com.docker.compose.service"
|
||||||
|
|
||||||
|
var appUpgradeLocks sync.Map
|
||||||
|
|
||||||
|
type appUpgradeSnapshot interface {
|
||||||
|
Restore() error
|
||||||
|
Cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
type upgradeFileSnapshot struct {
|
||||||
|
installPath string
|
||||||
|
backupPath string
|
||||||
|
paths []string
|
||||||
|
existing map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type appUpgradeContext struct {
|
||||||
|
req request.AppInstallUpgrade
|
||||||
|
original model.AppInstall
|
||||||
|
candidate model.AppInstall
|
||||||
|
detail model.AppDetail
|
||||||
|
|
||||||
|
phase appUpgradePhase
|
||||||
|
stopAttempted bool
|
||||||
|
downAttempted bool
|
||||||
|
rollbackErr error
|
||||||
|
|
||||||
|
detailDir string
|
||||||
|
stageDir string
|
||||||
|
envContent []byte
|
||||||
|
oldEnvContent []byte
|
||||||
|
oldDockerCompose string
|
||||||
|
oldImageIDs []appImageID
|
||||||
|
backupFile string
|
||||||
|
snapshot appUpgradeSnapshot
|
||||||
|
createdPaths []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||||
|
install, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if install.Status == constant.StatusUpgrading {
|
||||||
|
return buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, install.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, loaded := appUpgradeLocks.LoadOrStore(install.ID, struct{}{}); loaded {
|
||||||
|
return buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
releaseLock := true
|
||||||
|
defer func() {
|
||||||
|
if releaseLock {
|
||||||
|
appUpgradeLocks.Delete(install.ID)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if 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
|
||||||
|
|
||||||
|
t.LogStart(i18n.GetMsgByKey("UpgradeWaitReady"))
|
||||||
|
containerNames, err := waitAppContainersReady(context.Background(), u.candidate)
|
||||||
|
if err != nil {
|
||||||
|
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeWaitReady"), err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.LogSuccess(i18n.GetMsgByKey("UpgradeWaitReady"))
|
||||||
|
u.phase = appUpgradeReady
|
||||||
|
u.candidate.ContainerName = strings.Join(containerNames, ",")
|
||||||
|
u.candidate.Status = constant.StatusRunning
|
||||||
|
u.candidate.Message = ""
|
||||||
|
|
||||||
|
if u.candidate.App.Key == constant.AppOpenresty {
|
||||||
|
liveCatalogPath := path.Join(u.candidate.GetPath(), nginxModuleBuildDir, nginxModuleCatalogPendingFile)
|
||||||
|
if err = commitStaticNginxModuleBuilds(u.candidate, liveCatalogPath, t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
activeCatalogPath := path.Join(u.candidate.GetPath(), nginxModuleBuildDir, nginxModuleCatalogFile)
|
||||||
|
if err = activateNginxModuleCatalogAndCommit(liveCatalogPath, activeCatalogPath, func() error {
|
||||||
|
return appInstallRepo.Save(context.Background(), &u.candidate)
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
if _, err := waitAppContainersReady(context.Background(), u.original); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
restored := u.original
|
||||||
|
if err := appInstallRepo.Save(context.Background(), &restored); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) restoreManagedFiles() error {
|
||||||
|
var restoreErr error
|
||||||
|
if u.snapshot != nil {
|
||||||
|
restoreErr = u.snapshot.Restore()
|
||||||
|
}
|
||||||
|
for index := len(u.createdPaths) - 1; index >= 0; index-- {
|
||||||
|
if err := os.RemoveAll(u.createdPaths[index]); err != nil {
|
||||||
|
restoreErr = errors.Join(restoreErr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return restoreErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) deleteOldImages(t *task.Task) {
|
||||||
|
if !u.req.DeleteImage {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
excludeImages, err := docker.GetImagesFromDockerCompose(u.envContent, []byte(u.candidate.DockerCompose))
|
||||||
|
if err != nil {
|
||||||
|
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dockerClient, err := docker.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dockerClient.Close()
|
||||||
|
if err = deleteAppImagesByIDs(t, dockerClient, u.oldImageIDs, excludeImages); err != nil {
|
||||||
|
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) cleanup() {
|
||||||
|
if u.snapshot != nil {
|
||||||
|
u.snapshot.Cleanup()
|
||||||
|
}
|
||||||
|
if u.stageDir != "" {
|
||||||
|
_ = os.RemoveAll(u.stageDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type upgradeImageClient interface {
|
||||||
|
PullImageWithProcess(*task.Task, string) error
|
||||||
|
ImageExists(string) (bool, error)
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareUpgradeImages(t *task.Task, images []string, pull bool) error {
|
||||||
|
dockerClient, err := docker.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return prepareUpgradeImagesWithClient(t, dockerClient, images, pull)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareUpgradeImagesWithClient(t *task.Task, dockerClient upgradeImageClient, images []string, pull bool) error {
|
||||||
|
defer dockerClient.Close()
|
||||||
|
seen := make(map[string]struct{}, len(images))
|
||||||
|
for _, image := range images {
|
||||||
|
image = strings.TrimSpace(image)
|
||||||
|
if image == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[image]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[image] = struct{}{}
|
||||||
|
if pull {
|
||||||
|
if t != nil {
|
||||||
|
t.Log(i18n.GetWithName("PullImageStart", image))
|
||||||
|
}
|
||||||
|
if pullErr := dockerClient.PullImageWithProcess(t, image); pullErr != nil {
|
||||||
|
if exists, _ := dockerClient.ImageExists(image); exists {
|
||||||
|
if t != nil {
|
||||||
|
t.Log(i18n.GetMsgByKey("UseExistImage"))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return buserr.WithNameAndErr("ErrDockerPullImage", "", pullErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exists, inspectErr := dockerClient.ImageExists(image)
|
||||||
|
if inspectErr != nil || !exists {
|
||||||
|
return buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s is not available locally: %v", image, inspectErr))
|
||||||
|
}
|
||||||
|
if pull && t != nil {
|
||||||
|
t.LogSuccess(i18n.GetMsgByKey("PullImage"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyUpgradeImages(images []string) error {
|
||||||
|
dockerClient, err := docker.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dockerClient.Close()
|
||||||
|
for _, image := range images {
|
||||||
|
exists, inspectErr := dockerClient.ImageExists(image)
|
||||||
|
if inspectErr != nil || !exists {
|
||||||
|
return buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s is not available locally: %v", image, inspectErr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderUpgradeEnv(install *model.AppInstall, original []byte) ([]byte, error) {
|
||||||
|
originalEnv := make(map[string]string)
|
||||||
|
if len(original) > 0 {
|
||||||
|
var err error
|
||||||
|
originalEnv, err = godotenv.UnmarshalBytes(original)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
params := make(map[string]string, len(originalEnv))
|
||||||
|
maps.Copy(params, originalEnv)
|
||||||
|
envs := make(map[string]interface{})
|
||||||
|
if err := json.Unmarshal([]byte(install.Env), &envs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
handleMap(envs, params)
|
||||||
|
if install.App.Key == "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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type appContainerReadinessClient interface {
|
||||||
|
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
|
||||||
|
ContainerInspect(context.Context, string) (container.InspectResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitAppContainersReady(ctx context.Context, install model.AppInstall) ([]string, error) {
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
return waitAppContainersReadyWithClient(ctx, client, install)
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitAppContainersReadyWithClient(ctx context.Context, client appContainerReadinessClient, install model.AppInstall) ([]string, error) {
|
||||||
|
envContent, err := os.ReadFile(install.GetEnvPath())
|
||||||
|
if err != nil {
|
||||||
|
envContent, err = renderUpgradeEnv(&install, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), envContent, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
expectedServices := make(map[string]struct{})
|
||||||
|
for _, service := range project.Services {
|
||||||
|
if !skipCheckStatus(service) {
|
||||||
|
expectedServices[service.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(expectedServices) == 0 {
|
||||||
|
return strings.Split(install.ContainerName, ","), nil
|
||||||
|
}
|
||||||
|
options := container.ListOptions{
|
||||||
|
All: true,
|
||||||
|
Filters: filters.NewArgs(
|
||||||
|
filters.Arg("label", composeWorkdirLabel+"="+install.GetPath()),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
containers, err := client.ContainerList(ctx, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
foundServices := make(map[string]bool, len(expectedServices))
|
||||||
|
containerNames := make([]string, 0, len(containers))
|
||||||
|
for _, item := range containers {
|
||||||
|
serviceName := item.Labels[composeServiceLabel]
|
||||||
|
if _, ok := expectedServices[serviceName]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = waitContainerReady(ctx, client, item.ID); err != nil {
|
||||||
|
return nil, fmt.Errorf("container %s is not ready: %w", serviceName, err)
|
||||||
|
}
|
||||||
|
foundServices[serviceName] = true
|
||||||
|
if len(item.Names) > 0 {
|
||||||
|
containerNames = append(containerNames, strings.TrimPrefix(item.Names[0], "/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for serviceName := range expectedServices {
|
||||||
|
if !foundServices[serviceName] {
|
||||||
|
return nil, fmt.Errorf("container for service %s was not created", serviceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(containerNames)
|
||||||
|
return containerNames, nil
|
||||||
|
}
|
||||||
+41
-422
@@ -9,7 +9,6 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -354,15 +353,21 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
|||||||
logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("App")
|
logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("App")
|
||||||
t.Log(logStr)
|
t.Log(logStr)
|
||||||
|
|
||||||
out, err := compose.Down(install.GetComposePath())
|
if deleteReq.UseLifecycleScripts {
|
||||||
if err != nil && !deleteReq.ForceDelete {
|
if err = runScript(t, &install, "uninstall"); err != nil {
|
||||||
return handleErr(install, err, out)
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out, err := compose.Down(install.GetComposePath())
|
||||||
|
if err != nil && !deleteReq.ForceDelete {
|
||||||
|
return handleErr(install, err, out)
|
||||||
|
}
|
||||||
|
if err = runScript(t, &install, "uninstall"); err != nil {
|
||||||
|
_, _ = compose.Up(install.GetComposePath())
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
t.LogSuccess(logStr)
|
t.LogSuccess(logStr)
|
||||||
if err = runScript(t, &install, "uninstall"); err != nil {
|
|
||||||
_, _ = compose.Up(install.GetComposePath())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if deleteReq.DeleteImage {
|
if deleteReq.DeleteImage {
|
||||||
content, err := op.GetContent(install.GetEnvPath())
|
content, err := op.GetContent(install.GetEnvPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -783,416 +788,6 @@ func buildNginx(parentTask *task.Task, nginxInstall model.AppInstall, catalogPat
|
|||||||
return commitNginxModuleBuilds(nginxInstall, previousModules, modules, false, catalogPath)
|
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 {
|
func skipCheckStatus(service types.ServiceConfig) bool {
|
||||||
for key := range service.Labels {
|
for key := range service.Labels {
|
||||||
if key == "skipStatusCheck" {
|
if key == "skipStatusCheck" {
|
||||||
@@ -1410,6 +1005,12 @@ func runScript(task *task.Task, appInstall *model.AppInstall, operate string) er
|
|||||||
scriptPath = path.Join(workDir, "scripts", "upgrade.sh")
|
scriptPath = path.Join(workDir, "scripts", "upgrade.sh")
|
||||||
case "uninstall":
|
case "uninstall":
|
||||||
scriptPath = path.Join(workDir, "scripts", "uninstall.sh")
|
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()
|
fileOp := files.NewFileOp()
|
||||||
if !fileOp.Stat(scriptPath) {
|
if !fileOp.Stat(scriptPath) {
|
||||||
@@ -1419,7 +1020,11 @@ func runScript(task *task.Task, appInstall *model.AppInstall, operate string) er
|
|||||||
logStr := i18n.GetWithName("ExecShell", operate)
|
logStr := i18n.GetWithName("ExecShell", operate)
|
||||||
task.LogStart(logStr)
|
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 {
|
if err := cmdMgr.Run("bash", scriptPath); err != nil {
|
||||||
task.LogFailedWithErr(logStr, err)
|
task.LogFailedWithErr(logStr, err)
|
||||||
return err
|
return err
|
||||||
@@ -1454,12 +1059,15 @@ func checkContainerNameIsExist(containerName, appDir string) (bool, error) {
|
|||||||
return false, nil
|
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) {
|
upProject := func(appInstall *model.AppInstall) (err error) {
|
||||||
var (
|
var (
|
||||||
out string
|
out string
|
||||||
errMsg string
|
errMsg string
|
||||||
)
|
)
|
||||||
|
if useLifecycleScripts {
|
||||||
|
return runScript(task, appInstall, "start")
|
||||||
|
}
|
||||||
if pullImages && appInstall.App.Type != "php" {
|
if pullImages && appInstall.App.Type != "php" {
|
||||||
envByte, err := files.NewFileOp().GetContent(appInstall.GetEnvPath())
|
envByte, err := files.NewFileOp().GetContent(appInstall.GetEnvPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1786,7 +1394,8 @@ func handleErr(install model.AppInstall, err error, out string) error {
|
|||||||
|
|
||||||
func doNotNeedSync(installed model.AppInstall) bool {
|
func doNotNeedSync(installed model.AppInstall) bool {
|
||||||
return installed.Status == constant.StatusInstalling || installed.Status == constant.StatusRebuilding || installed.Status == constant.StatusUpgrading ||
|
return installed.Status == constant.StatusInstalling || installed.Status == constant.StatusRebuilding || installed.Status == constant.StatusUpgrading ||
|
||||||
installed.Status == constant.StatusSyncing || installed.Status == constant.StatusUninstalling || installed.Status == constant.StatusInstallErr
|
installed.Status == constant.StatusSyncing || installed.Status == constant.StatusUninstalling || installed.Status == constant.StatusInstallErr ||
|
||||||
|
installed.Status == constant.StatusStarting || installed.Status == constant.StatusRestarting || installed.Status == constant.StatusWaiting
|
||||||
}
|
}
|
||||||
|
|
||||||
func synAppInstall(containers map[string]container.Summary, appInstall *model.AppInstall, force bool) {
|
func synAppInstall(containers map[string]container.Summary, appInstall *model.AppInstall, force bool) {
|
||||||
@@ -2233,6 +1842,10 @@ func isHostModel(dockerCompose string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
|
func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
|
||||||
|
return copyAppDetailMissingTracked(fileOp, srcDir, dstDir, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyAppDetailMissingTracked(fileOp files.FileOp, srcDir, dstDir string, createdPaths *[]string) error {
|
||||||
entries, err := os.ReadDir(srcDir)
|
entries, err := os.ReadDir(srcDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -2244,6 +1857,9 @@ func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
|
|||||||
srcPath := path.Join(srcDir, entry.Name())
|
srcPath := path.Join(srcDir, entry.Name())
|
||||||
dstPath := path.Join(dstDir, entry.Name())
|
dstPath := path.Join(dstDir, entry.Name())
|
||||||
if !fileOp.Stat(dstPath) {
|
if !fileOp.Stat(dstPath) {
|
||||||
|
if createdPaths != nil {
|
||||||
|
*createdPaths = append(*createdPaths, dstPath)
|
||||||
|
}
|
||||||
if entry.IsDir() {
|
if entry.IsDir() {
|
||||||
if err := fileOp.CopyDir(srcPath, dstDir); err != nil {
|
if err := fileOp.CopyDir(srcPath, dstDir); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -2258,7 +1874,7 @@ func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
|
|||||||
if !entry.IsDir() {
|
if !entry.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := copyAppDetailMissing(fileOp, srcPath, dstPath); err != nil {
|
if err := copyAppDetailMissingTracked(fileOp, srcPath, dstPath, createdPaths); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2633,6 +2249,9 @@ func getAppVersions(key string, details []model.AppDetail) []string {
|
|||||||
hasLatest := false
|
hasLatest := false
|
||||||
latestVersion := ""
|
latestVersion := ""
|
||||||
for _, detail := range details {
|
for _, detail := range details {
|
||||||
|
if !canAccessVllmVersion(key, detail.Version) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if key != "mssql" && strings.Contains(detail.Version, "latest") {
|
if key != "mssql" && strings.Contains(detail.Version, "latest") {
|
||||||
hasLatest = true
|
hasLatest = true
|
||||||
latestVersion = detail.Version
|
latestVersion = detail.Version
|
||||||
|
|||||||
@@ -90,6 +90,34 @@ func (u *BackupService) AppBackup(req dto.CommonBackup) (*model.BackupRecord, er
|
|||||||
return record, nil
|
return record, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func backupAppWithParentTask(install *model.AppInstall, parentTask *task.Task, fileName string) (*model.BackupRecord, error) {
|
||||||
|
itemDir := fmt.Sprintf("app/%s/%s", install.App.Key, install.Name)
|
||||||
|
backupDir := path.Join(global.Dir.LocalBackupDir, itemDir)
|
||||||
|
record := &model.BackupRecord{
|
||||||
|
Type: "app",
|
||||||
|
Name: install.App.Key,
|
||||||
|
DetailName: install.Name,
|
||||||
|
SourceAccountIDs: "1",
|
||||||
|
DownloadAccountID: 1,
|
||||||
|
FileDir: itemDir,
|
||||||
|
FileName: fileName,
|
||||||
|
TaskID: parentTask.TaskID,
|
||||||
|
Status: constant.StatusWaiting,
|
||||||
|
}
|
||||||
|
if err := backupRepo.CreateRecord(record); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := handleAppBackup(install, parentTask, record.ID, backupDir, fileName, "", "", parentTask.TaskID); err != nil {
|
||||||
|
markBackupFailed(record.ID, err)
|
||||||
|
record.Status = constant.StatusFailed
|
||||||
|
record.Message = err.Error()
|
||||||
|
return record, err
|
||||||
|
}
|
||||||
|
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||||
|
record.Status = constant.StatusSuccess
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *BackupService) AppRecover(req dto.CommonRecover) error {
|
func (u *BackupService) AppRecover(req dto.CommonRecover) error {
|
||||||
app, err := appRepo.GetFirst(appRepo.WithKey(req.Name))
|
app, err := appRepo.GetFirst(appRepo.WithKey(req.Name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -203,7 +231,11 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
_, _ = compose.Up(install.GetComposePath())
|
if isRollback {
|
||||||
|
_, _ = compose.UpWithoutBuild(install.GetComposePath())
|
||||||
|
} else {
|
||||||
|
_, _ = compose.Up(install.GetComposePath())
|
||||||
|
}
|
||||||
_ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", ""))
|
_ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", ""))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -582,6 +582,10 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
|
|||||||
if config.Image == "" {
|
if config.Image == "" {
|
||||||
return fmt.Errorf("container image not found in backup file")
|
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 !checkImageExist(recoverCtx.client, config.Image) {
|
||||||
if err := pullImages(taskItem, recoverCtx.client, config.Image); err != nil {
|
if err := pullImages(taskItem, recoverCtx.client, config.Image); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -596,7 +600,7 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
|
|||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -604,7 +608,7 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
|
|||||||
return nil
|
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 {
|
if cli.NewVersionError(ctx, "1.44", "specify mac-address per network") != nil {
|
||||||
removeEndpointMacAddresses(primary, extras)
|
removeEndpointMacAddresses(primary, extras)
|
||||||
}
|
}
|
||||||
@@ -619,11 +623,14 @@ func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client,
|
|||||||
}
|
}
|
||||||
info, err := cli.NetworkInspect(ctx, netName, network.InspectOptions{})
|
info, err := cli.NetworkInspect(ctx, netName, network.InspectOptions{})
|
||||||
if err != nil {
|
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) {
|
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 {
|
if endpoint == nil || endpoint.IPAMConfig == nil {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
if isDefaultBridgeNetwork(netName, info) {
|
ipam := endpoint.IPAMConfig
|
||||||
endpoint.IPAMConfig = nil
|
if err := ipam.Validate(); err != nil {
|
||||||
return
|
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) {
|
if ipam.IPv4Address != "" && !networkSupportsStaticIP(info, ipam.IPv4Address, false) {
|
||||||
endpoint.IPAMConfig.IPv4Address = ""
|
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) {
|
if ipam.IPv6Address != "" && !networkSupportsStaticIP(info, ipam.IPv6Address, true) {
|
||||||
endpoint.IPAMConfig.IPv6Address = ""
|
return fmt.Errorf("static IPv6 address %s is not in a configured subnet of network %s", ipam.IPv6Address, netName)
|
||||||
}
|
|
||||||
if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" && len(endpoint.IPAMConfig.LinkLocalIPs) == 0 {
|
|
||||||
endpoint.IPAMConfig = nil
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isDefaultBridgeNetwork(netName string, info network.Inspect) bool {
|
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 {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
addr = addr.Unmap()
|
||||||
if addr.Is6() != isIPv6 {
|
if addr.Is6() != isIPv6 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -813,11 +825,6 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
|
|||||||
IPv6Address: endpoint.IPAMConfig.IPv6Address,
|
IPv6Address: endpoint.IPAMConfig.IPv6Address,
|
||||||
LinkLocalIPs: append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...),
|
LinkLocalIPs: append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...),
|
||||||
}
|
}
|
||||||
} else if name != "bridge" && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "") {
|
|
||||||
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
|
|
||||||
IPv4Address: endpoint.IPAddress,
|
|
||||||
IPv6Address: endpoint.GlobalIPv6Address,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if name == primaryName {
|
if name == primaryName {
|
||||||
config.EndpointsConfig[name] = endpointSetting
|
config.EndpointsConfig[name] = endpointSetting
|
||||||
@@ -831,39 +838,6 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
|
|||||||
return config, extraNetworks
|
return config, extraNetworks
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsupportedUserSpecifiedIPAddress = "user specified IP address is supported only when connecting to networks with user configured subnets"
|
|
||||||
|
|
||||||
func clearUnsupportedDynamicEndpointIPAM(err error, endpoints map[string]*network.EndpointSettings, networkSettings *container.NetworkSettings) bool {
|
|
||||||
if err == nil || !strings.Contains(err.Error(), unsupportedUserSpecifiedIPAddress) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for name, endpoint := range endpoints {
|
|
||||||
if !isDynamicContainerNetwork(networkSettings, name) || endpoint == nil || endpoint.IPAMConfig == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "network "+name+":") {
|
|
||||||
endpoint.IPAMConfig = nil
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cleared := false
|
|
||||||
for name, endpoint := range endpoints {
|
|
||||||
if isDynamicContainerNetwork(networkSettings, name) && endpoint != nil && endpoint.IPAMConfig != nil {
|
|
||||||
endpoint.IPAMConfig = nil
|
|
||||||
cleared = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cleared
|
|
||||||
}
|
|
||||||
|
|
||||||
func isDynamicContainerNetwork(networkSettings *container.NetworkSettings, name string) bool {
|
|
||||||
if networkSettings == nil || name == "bridge" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
endpoint := networkSettings.Networks[name]
|
|
||||||
return endpoint != nil && endpoint.IPAMConfig == nil && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneContainerConfig(config *container.Config) *container.Config {
|
func cloneContainerConfig(config *container.Config) *container.Config {
|
||||||
if config == nil {
|
if config == nil {
|
||||||
return &container.Config{}
|
return &container.Config{}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -39,6 +40,7 @@ func (u *BackupService) MysqlBackup(req dto.CommonBackup) error {
|
|||||||
TaskID: req.TaskID,
|
TaskID: req.TaskID,
|
||||||
Status: constant.StatusWaiting,
|
Status: constant.StatusWaiting,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
|
Args: encodeBackupArgs(req.Args),
|
||||||
}
|
}
|
||||||
if err := backupRepo.CreateRecord(record); err != nil {
|
if err := backupRepo.CreateRecord(record); err != nil {
|
||||||
global.LOG.Errorf("save backup record failed, err: %v", err)
|
global.LOG.Errorf("save backup record failed, err: %v", err)
|
||||||
@@ -143,6 +145,14 @@ func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback
|
|||||||
|
|
||||||
if !isRollback {
|
if !isRollback {
|
||||||
rollbackFile := path.Join(global.Dir.TmpDir, fmt.Sprintf("database/%s/%s_%s.sql.gz", req.Type, req.DetailName, time.Now().Format(constant.DateTimeSlimLayout)))
|
rollbackFile := path.Join(global.Dir.TmpDir, fmt.Sprintf("database/%s/%s_%s.sql.gz", req.Type, req.DetailName, time.Now().Format(constant.DateTimeSlimLayout)))
|
||||||
|
var rollbackArgs []string
|
||||||
|
if req.BackupRecordID != 0 {
|
||||||
|
record, err := backupRepo.GetRecord(repo.WithByID(req.BackupRecordID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rollbackArgs = decodeBackupArgs(record.Args)
|
||||||
|
}
|
||||||
if err := cli.Backup(client.BackupInfo{
|
if err := cli.Backup(client.BackupInfo{
|
||||||
Name: req.DetailName,
|
Name: req.DetailName,
|
||||||
Type: req.Type,
|
Type: req.Type,
|
||||||
@@ -150,6 +160,7 @@ func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback
|
|||||||
Format: dbInfo.Format,
|
Format: dbInfo.Format,
|
||||||
TargetDir: path.Dir(rollbackFile),
|
TargetDir: path.Dir(rollbackFile),
|
||||||
FileName: path.Base(rollbackFile),
|
FileName: path.Base(rollbackFile),
|
||||||
|
Args: rollbackArgs,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("backup mysql db %s for rollback before recover failed, err: %v", req.DetailName, err)
|
return fmt.Errorf("backup mysql db %s for rollback before recover failed, err: %v", req.DetailName, err)
|
||||||
}
|
}
|
||||||
@@ -242,6 +253,36 @@ func doMysqlBackup(db DatabaseHelper, targetDir, fileName, secret string) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func encodeBackupArgs(args []string) string {
|
||||||
|
var items []string
|
||||||
|
for _, arg := range args {
|
||||||
|
if len(arg) != 0 {
|
||||||
|
items = append(items, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(items)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Warnf("marshal backup args failed: %v", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeBackupArgs(value string) []string {
|
||||||
|
if len(value) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var args []string
|
||||||
|
if err := json.Unmarshal([]byte(value), &args); err != nil {
|
||||||
|
global.LOG.Warnf("unmarshal backup args failed: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
func loadSqlFile(file string) (string, error) {
|
func loadSqlFile(file string) (string, error) {
|
||||||
if !strings.HasSuffix(file, ".tar.gz") && !strings.HasSuffix(file, ".zip") {
|
if !strings.HasSuffix(file, ".tar.gz") && !strings.HasSuffix(file, ".zip") {
|
||||||
return file, nil
|
return file, nil
|
||||||
|
|||||||
+63
-130
@@ -16,6 +16,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -24,6 +25,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
@@ -71,6 +73,7 @@ type IContainerService interface {
|
|||||||
ComposeOperation(req dto.ComposeOperation) error
|
ComposeOperation(req dto.ComposeOperation) error
|
||||||
TestCompose(req dto.ComposeCreate) (bool, error)
|
TestCompose(req dto.ComposeCreate) (bool, error)
|
||||||
ComposeUpdate(req dto.ComposeUpdate) error
|
ComposeUpdate(req dto.ComposeUpdate) error
|
||||||
|
ComposePin(req dto.ComposePin) error
|
||||||
ComposeLogClean(req dto.ComposeLogClean) error
|
ComposeLogClean(req dto.ComposeLogClean) error
|
||||||
|
|
||||||
ContainerCreate(req dto.ContainerOperate, inThread bool) error
|
ContainerCreate(req dto.ContainerOperate, inThread bool) error
|
||||||
@@ -78,7 +81,7 @@ type IContainerService interface {
|
|||||||
ContainerUpgrade(req dto.ContainerUpgrade) error
|
ContainerUpgrade(req dto.ContainerUpgrade) error
|
||||||
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
|
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
|
||||||
ContainerListStats() ([]dto.ContainerListStats, error)
|
ContainerListStats() ([]dto.ContainerListStats, error)
|
||||||
ContainerItemStats(req dto.OperationWithName) (dto.ContainerItemStats, error)
|
ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error)
|
||||||
LoadResourceLimit() (*dto.ResourceLimit, error)
|
LoadResourceLimit() (*dto.ResourceLimit, error)
|
||||||
ContainerRename(req dto.ContainerRename) error
|
ContainerRename(req dto.ContainerRename) error
|
||||||
ContainerCommit(req dto.ContainerCommit) error
|
ContainerCommit(req dto.ContainerCommit) error
|
||||||
@@ -245,15 +248,15 @@ func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
|
|||||||
}
|
}
|
||||||
return data, nil
|
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
|
var data dto.ContainerItemStats
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
defer client.Close()
|
||||||
if req.Name != "system" {
|
if req.Name != "system" {
|
||||||
defer client.Close()
|
containerInfo, _, err := client.ContainerInspectWithRaw(ctx, req.Name, true)
|
||||||
containerInfo, _, err := client.ContainerInspectWithRaw(context.Background(), req.Name, true)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
@@ -262,7 +265,7 @@ func (u *ContainerService) ContainerItemStats(req dto.OperationWithName) (dto.Co
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
usage, err := client.DiskUsage(context.Background(), types.DiskUsageOptions{})
|
usage, err := client.DiskUsage(ctx, types.DiskUsageOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
@@ -533,7 +536,9 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
|
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
|
||||||
@@ -643,14 +648,9 @@ func loadContainerNetworkInfo(name string, endpoint *network.EndpointSettings) d
|
|||||||
if endpoint.IPAMConfig != nil {
|
if endpoint.IPAMConfig != nil {
|
||||||
item.LinkLocalIPs = append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...)
|
item.LinkLocalIPs = append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...)
|
||||||
}
|
}
|
||||||
if name != "bridge" {
|
if name != "bridge" && endpoint.IPAMConfig != nil {
|
||||||
if endpoint.IPAMConfig != nil {
|
item.Ipv4 = endpoint.IPAMConfig.IPv4Address
|
||||||
item.Ipv4 = endpoint.IPAMConfig.IPv4Address
|
item.Ipv6 = endpoint.IPAMConfig.IPv6Address
|
||||||
item.Ipv6 = endpoint.IPAMConfig.IPv6Address
|
|
||||||
} else {
|
|
||||||
item.Ipv4 = endpoint.IPAddress
|
|
||||||
item.Ipv6 = endpoint.GlobalIPv6Address
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return item
|
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 {
|
func pullImages(task *task.Task, client *client.Client, imageName string) error {
|
||||||
dockerCli := docker.NewClientWithExist(client)
|
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{}
|
options := image.PullOptions{}
|
||||||
repos, _ := imageRepoRepo.List()
|
authConfig := registry.AuthConfig{
|
||||||
if len(repos) != 0 {
|
Username: imageRepo.Username,
|
||||||
for _, repo := range repos {
|
Password: imageRepo.Password,
|
||||||
if strings.HasPrefix(imageName, repo.DownloadUrl) && repo.Auth {
|
}
|
||||||
authConfig := registry.AuthConfig{
|
encodedJSON, err := json.Marshal(authConfig)
|
||||||
Username: repo.Username,
|
if err != nil {
|
||||||
Password: repo.Password,
|
return err
|
||||||
}
|
}
|
||||||
encodedJSON, err := json.Marshal(authConfig)
|
options.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON)
|
||||||
if err != nil {
|
return dockerCli.PullImageWithProcessAndOptions(task, imageName, options)
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
|
func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo {
|
||||||
options.RegistryAuth = authStr
|
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 {
|
if len(downloadURL) > selectedURLLength {
|
||||||
hasAuth, authStr := loadAuthInfo(imageName)
|
selected = &repos[i]
|
||||||
if hasAuth {
|
selectedURLLength = len(downloadURL)
|
||||||
options.RegistryAuth = authStr
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return dockerCli.PullImageWithProcessAndOptions(task, imageName, options)
|
return selected
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerListStats {
|
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++ {
|
for i := 0; i <= hostEnd-hostStart; i++ {
|
||||||
bindItem := nat.PortBinding{HostPort: strconv.Itoa(hostStart + i), HostIP: port.HostIP}
|
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++ {
|
for i := hostStart; i <= hostEnd; i++ {
|
||||||
if checkInUse && common.ScanPortWithIP(port.HostIP, 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)
|
return portMap, buserr.WithDetail("ErrPortInUsed", portItem, nil)
|
||||||
}
|
}
|
||||||
bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP}
|
bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP}
|
||||||
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
|
return portMap, nil
|
||||||
@@ -1921,90 +1939,7 @@ func loadPortByInspect(id string, client *client.Client) ([]container.Port, erro
|
|||||||
return itemPorts, nil
|
return itemPorts, nil
|
||||||
}
|
}
|
||||||
func transPortToStr(ports []container.Port) []string {
|
func transPortToStr(ports []container.Port) []string {
|
||||||
var (
|
return docker.SimplifyPorts(ports)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadComposeCount(client *client.Client) int {
|
func loadComposeCount(client *client.Client) int {
|
||||||
@@ -2025,6 +1960,9 @@ func loadComposeCount(client *client.Client) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, compose := range composeCreatedByLocal {
|
for _, compose := range composeCreatedByLocal {
|
||||||
|
if len(compose.Path) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if _, has := composeMap[compose.Name]; !has {
|
if _, has := composeMap[compose.Name]; !has {
|
||||||
composeMap[compose.Name] = struct{}{}
|
composeMap[compose.Name] = struct{}{}
|
||||||
}
|
}
|
||||||
@@ -2034,7 +1972,7 @@ func loadComposeCount(client *client.Client) int {
|
|||||||
}
|
}
|
||||||
func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper {
|
func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper {
|
||||||
var exposedPorts []dto.PortHelper
|
var exposedPorts []dto.PortHelper
|
||||||
samePortMap := make(map[string]dto.PortHelper)
|
seenPorts := make(map[dto.PortHelper]struct{})
|
||||||
ports := transPortToStr(itemPorts)
|
ports := transPortToStr(itemPorts)
|
||||||
for _, item := range ports {
|
for _, item := range ports {
|
||||||
itemStr := strings.Split(item, "->")
|
itemStr := strings.Split(item, "->")
|
||||||
@@ -2055,16 +1993,11 @@ func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper {
|
|||||||
}
|
}
|
||||||
itemPort.ContainerPort = itemContainer[0]
|
itemPort.ContainerPort = itemContainer[0]
|
||||||
itemPort.Protocol = itemContainer[1]
|
itemPort.Protocol = itemContainer[1]
|
||||||
keyItem := fmt.Sprintf("%s->%s/%s", itemPort.HostPort, itemPort.ContainerPort, itemPort.Protocol)
|
if _, exists := seenPorts[itemPort]; exists {
|
||||||
if val, ok := samePortMap[keyItem]; ok {
|
continue
|
||||||
val.HostIP = ""
|
|
||||||
samePortMap[keyItem] = val
|
|
||||||
} else {
|
|
||||||
samePortMap[keyItem] = itemPort
|
|
||||||
}
|
}
|
||||||
}
|
seenPorts[itemPort] = struct{}{}
|
||||||
for _, val := range samePortMap {
|
exposedPorts = append(exposedPorts, itemPort)
|
||||||
exposedPorts = append(exposedPorts, val)
|
|
||||||
}
|
}
|
||||||
return exposedPorts
|
return exposedPorts
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -23,8 +24,11 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
const composeProjectLabel = "com.docker.compose.project"
|
const composeProjectLabel = "com.docker.compose.project"
|
||||||
@@ -52,7 +56,15 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
composeCreatedByLocal, _ := composeRepo.ListRecord()
|
composeRecords, _ := composeRepo.ListRecord()
|
||||||
|
pinnedByName := make(map[string]bool, len(composeRecords))
|
||||||
|
composeCreatedByLocal := make([]model.Compose, 0, len(composeRecords))
|
||||||
|
for _, record := range composeRecords {
|
||||||
|
pinnedByName[record.Name] = record.IsPinned
|
||||||
|
if len(record.Path) != 0 {
|
||||||
|
composeCreatedByLocal = append(composeCreatedByLocal, record)
|
||||||
|
}
|
||||||
|
}
|
||||||
composeLocalMap := make(map[string]dto.ComposeInfo)
|
composeLocalMap := make(map[string]dto.ComposeInfo)
|
||||||
for _, localItem := range composeCreatedByLocal {
|
for _, localItem := range composeCreatedByLocal {
|
||||||
composeItemLocal := dto.ComposeInfo{
|
composeItemLocal := dto.ComposeInfo{
|
||||||
@@ -136,6 +148,7 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
for key, value := range mergedMap {
|
for key, value := range mergedMap {
|
||||||
value.Name = key
|
value.Name = key
|
||||||
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
|
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
|
||||||
|
value.IsPinned = pinnedByName[key]
|
||||||
records = append(records, value)
|
records = append(records, value)
|
||||||
}
|
}
|
||||||
if len(req.Info) != 0 {
|
if len(req.Info) != 0 {
|
||||||
@@ -149,7 +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 {
|
sort.Slice(records, func(i, j int) bool {
|
||||||
|
if records[i].IsPinned != records[j].IsPinned {
|
||||||
|
return records[i].IsPinned
|
||||||
|
}
|
||||||
return records[i].CreatedAt > records[j].CreatedAt
|
return records[i].CreatedAt > records[j].CreatedAt
|
||||||
})
|
})
|
||||||
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||||
@@ -189,54 +216,56 @@ func composeFileExists(workdir, configFile string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
||||||
if cmd.CheckIllegal(req.Path) {
|
if err := validateComposeCreateName(req); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if hasIllegalComposeCreateInput(req) {
|
||||||
return false, buserr.New("ErrCmdIllegal")
|
return false, buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
composeItem, _ := composeRepo.GetRecord(repo.WithByName(req.Name))
|
projectName, err := resolveComposeCreateProjectName(req)
|
||||||
if composeItem.ID != 0 {
|
|
||||||
return false, buserr.New("ErrRecordExist")
|
|
||||||
}
|
|
||||||
if err := u.loadPath(&req); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
cmd := getComposeCmd(req.Path, "config")
|
|
||||||
stdout, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("docker-compose config failed, std: %s, err: %v", string(stdout), err)
|
return false, err
|
||||||
|
}
|
||||||
|
if err := checkComposeCreateDuplicate(req, projectName); err != nil {
|
||||||
|
return false, err
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
|
func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
|
||||||
if cmd.CheckIllegal(req.Name, req.Path) {
|
if err := validateComposeCreateName(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if hasIllegalComposeCreateInput(req) {
|
||||||
return buserr.New("ErrCmdIllegal")
|
return buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
|
projectName, err := resolveComposeCreateProjectName(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := checkComposeCreateDuplicate(req, projectName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := u.loadPath(&req); err != nil {
|
if err := u.loadPath(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if req.From == "path" {
|
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
||||||
req.Name = path.Base(path.Dir(req.Path))
|
return err
|
||||||
|
}
|
||||||
|
req.Name = projectName
|
||||||
|
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)
|
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeCompose, req.TaskID, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("new task for image build failed, err: %v", err)
|
return fmt.Errorf("new task for image build failed, err: %v", err)
|
||||||
}
|
}
|
||||||
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
go func() {
|
go func() {
|
||||||
taskItem.AddSubTask(i18n.GetMsgByKey("ComposeCreate"), func(t *task.Task) error {
|
taskItem.AddSubTask(i18n.GetMsgByKey("ComposeCreate"), func(t *task.Task) error {
|
||||||
err := compose.UpWithTask(req.Path, t, req.ForcePull)
|
err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name)
|
||||||
t.LogWithStatus(i18n.GetMsgByKey("ComposeCreate"), err)
|
t.LogWithStatus(i18n.GetMsgByKey("ComposeCreate"), err)
|
||||||
if err != nil {
|
return err
|
||||||
_, _ = compose.Down(req.Path)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = composeRepo.CreateRecord(&model.Compose{Name: strings.ToLower(req.Name), Path: req.Path})
|
|
||||||
return nil
|
|
||||||
}, nil)
|
}, nil)
|
||||||
_ = taskItem.Execute()
|
_ = taskItem.Execute()
|
||||||
}()
|
}()
|
||||||
@@ -244,6 +273,254 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
|
|||||||
return nil
|
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 {
|
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
||||||
if len(req.Path) == 0 && req.Operation == "delete" {
|
if len(req.Path) == 0 && req.Operation == "delete" {
|
||||||
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
||||||
@@ -267,15 +544,15 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if req.Operation == "up" {
|
if req.Operation == "up" {
|
||||||
if stdout, err := compose.Up(req.Path); err != nil {
|
if stdout, err := compose.Up(req.Path, req.Name); err != nil {
|
||||||
return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err)
|
return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err)
|
||||||
}
|
}
|
||||||
} else if req.Operation == "rebuild" {
|
} else if req.Operation == "rebuild" {
|
||||||
if stdout, err := compose.DownAndUp(req.Path); err != nil {
|
if stdout, err := compose.DownAndUp(req.Path, req.Name); err != nil {
|
||||||
return fmt.Errorf("docker-compose rebuild failed, std: %s, err: %v", stdout, err)
|
return fmt.Errorf("docker-compose rebuild failed, std: %s, err: %v", stdout, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if stdout, err := compose.Operate(req.Path, req.Operation); err != nil {
|
if stdout, err := compose.Operate(req.Path, req.Operation, req.Name); err != nil {
|
||||||
return fmt.Errorf("docker-compose %s failed, std: %s, err: %v", req.Operation, stdout, err)
|
return fmt.Errorf("docker-compose %s failed, std: %s, err: %v", req.Operation, stdout, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,9 +588,9 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := compose.UpWithTask(req.Path, t, req.ForcePull); err != nil {
|
if err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name); err != nil {
|
||||||
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
|
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
|
||||||
if err := recreateCompose(string(oldFile), req.Path); err != nil {
|
if err := recreateCompose(string(oldFile), req.Path, req.Name); err != nil {
|
||||||
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
|
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("update failed when handle compose up, err: %s", err)
|
return fmt.Errorf("update failed when handle compose up, err: %s", err)
|
||||||
@@ -327,6 +604,20 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ContainerService) ComposePin(req dto.ComposePin) error {
|
||||||
|
record, _ := composeRepo.GetRecord(repo.WithByName(req.Name))
|
||||||
|
if record.ID == 0 {
|
||||||
|
if !req.IsPinned {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return composeRepo.CreateRecord(&model.Compose{Name: req.Name, IsPinned: true})
|
||||||
|
}
|
||||||
|
if !req.IsPinned && len(record.Path) == 0 {
|
||||||
|
return composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
||||||
|
}
|
||||||
|
return composeRepo.UpdateRecord(req.Name, map[string]interface{}{"is_pinned": req.IsPinned})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ComposeLogClean(req dto.ComposeLogClean) error {
|
func (u *ContainerService) ComposeLogClean(req dto.ComposeLogClean) error {
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -389,15 +680,15 @@ func (u *ContainerService) LoadComposeEnv(name string) (string, error) {
|
|||||||
|
|
||||||
func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
||||||
if req.From == "template" || req.From == "edit" {
|
if req.From == "template" || req.From == "edit" {
|
||||||
dir := fmt.Sprintf("%s/docker/compose/%s", global.Dir.DataDir, req.Name)
|
composePath := composeCreatePath(*req)
|
||||||
|
dir := filepath.Dir(composePath)
|
||||||
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
|
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
|
||||||
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
|
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
path := fmt.Sprintf("%s/docker-compose.yml", dir)
|
file, err := os.OpenFile(composePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, constant.FilePerm)
|
||||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, constant.FilePerm)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -405,14 +696,14 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
|||||||
write := bufio.NewWriter(file)
|
write := bufio.NewWriter(file)
|
||||||
_, _ = write.WriteString(string(req.File))
|
_, _ = write.WriteString(string(req.File))
|
||||||
write.Flush()
|
write.Flush()
|
||||||
req.Path = path
|
req.Path = composePath
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeContainerForCompose(composeName, composePath string) error {
|
func removeContainerForCompose(composeName, composePath string) error {
|
||||||
if _, err := os.Stat(composePath); err == nil {
|
if _, err := os.Stat(composePath); err == nil {
|
||||||
if stdout, err := compose.Operate(composePath, "down"); err != nil {
|
if stdout, err := compose.Operate(composePath, "down", composeName); err != nil {
|
||||||
return errors.New(stdout)
|
return errors.New(stdout)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -437,7 +728,7 @@ func removeContainerForCompose(composeName, composePath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func recreateCompose(content, path string) error {
|
func recreateCompose(content, path, projectName string) error {
|
||||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
|
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -447,7 +738,7 @@ func recreateCompose(content, path string) error {
|
|||||||
_, _ = write.WriteString(content)
|
_, _ = write.WriteString(content)
|
||||||
write.Flush()
|
write.Flush()
|
||||||
|
|
||||||
if stdout, err := compose.Up(path); err != nil {
|
if stdout, err := compose.Up(path, projectName); err != nil {
|
||||||
return errors.New(string(stdout))
|
return errors.New(string(stdout))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -18,6 +20,7 @@ import (
|
|||||||
"github.com/docker/docker/api/types/mount"
|
"github.com/docker/docker/api/types/mount"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,13 +67,13 @@ func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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) {
|
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)
|
||||||
return client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
|
}, config.Tty, t)
|
||||||
}, networkConf.EndpointsConfig, oldContainer.NetworkSettings)
|
|
||||||
}, newContainerSwitchTaskLogger(t))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("update container failed, err: %v", err)
|
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
|
config.Image = req.Image
|
||||||
hostConf := cloneContainerHostConfig(oldContainer.HostConfig)
|
hostConf := cloneContainerHostConfig(oldContainer.HostConfig)
|
||||||
preserveContainerVolumeMounts(hostConf, oldContainer.Mounts)
|
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) {
|
cleanupErr, err := switchContainer(ctx, client, item, oldContainer, func() (container.CreateResponse, error) {
|
||||||
return createContainerWithOldNetworks(ctx, client, config, hostConf, oldContainer.NetworkSettings, item)
|
return createContainerWithNetworks(ctx, client, config, hostConf, networkConf, extraNetworks, item)
|
||||||
}, newContainerSwitchTaskLogger(t))
|
}, config.Tty, t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
upgradeErr := fmt.Errorf("upgrade container %s failed: %w", item, err)
|
upgradeErr := fmt.Errorf("upgrade container %s failed: %w", item, err)
|
||||||
upgradeErrors = append(upgradeErrors, upgradeErr)
|
upgradeErrors = append(upgradeErrors, upgradeErr)
|
||||||
@@ -166,10 +175,15 @@ type containerSwitchClient interface {
|
|||||||
ContainerStart(context.Context, string, container.StartOptions) error
|
ContainerStart(context.Context, string, container.StartOptions) error
|
||||||
ContainerRemove(context.Context, string, container.RemoveOptions) error
|
ContainerRemove(context.Context, string, container.RemoveOptions) error
|
||||||
ContainerInspect(context.Context, string) (container.InspectResponse, 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
|
NetworkConnect(context.Context, string, string, *network.EndpointSettings) error
|
||||||
NetworkDisconnect(context.Context, string, string, bool) error
|
NetworkDisconnect(context.Context, string, string, bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type containerInspectClient interface {
|
||||||
|
ContainerInspect(context.Context, string) (container.InspectResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
type containerOperationMutex struct {
|
type containerOperationMutex struct {
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
locks map[string]*containerOperationLockEntry
|
locks map[string]*containerOperationLockEntry
|
||||||
@@ -234,22 +248,18 @@ func (l *containerOperationMutex) lock(names ...string) func() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type containerNetworkAttachment struct {
|
type containerNetworkAttachment struct {
|
||||||
name string
|
name string
|
||||||
endpoint *network.EndpointSettings
|
endpoint *network.EndpointSettings
|
||||||
isDynamic bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type containerSwitchLogFunc func(messageKey, containerName string, err error)
|
type containerSwitchLogger interface {
|
||||||
|
LogWithStatus(string, error)
|
||||||
func newContainerSwitchTaskLogger(t *task.Task) containerSwitchLogFunc {
|
Log(string)
|
||||||
return func(messageKey, containerName string, err error) {
|
|
||||||
t.LogWithStatus(i18n.GetWithName(messageKey, containerName), err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func logContainerSwitchStep(logger containerSwitchLogFunc, messageKey, containerName string, err error) {
|
func logContainerSwitchStep(logger containerSwitchLogger, messageKey, containerName string, err error) {
|
||||||
if logger != nil {
|
if logger != nil {
|
||||||
logger(messageKey, containerName, err)
|
logger.LogWithStatus(i18n.GetWithName(messageKey, containerName), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +270,8 @@ func switchContainer(
|
|||||||
name string,
|
name string,
|
||||||
oldContainer container.InspectResponse,
|
oldContainer container.InspectResponse,
|
||||||
createNew func() (container.CreateResponse, error),
|
createNew func() (container.CreateResponse, error),
|
||||||
logger containerSwitchLogFunc,
|
tty bool,
|
||||||
|
logger containerSwitchLogger,
|
||||||
) (cleanupErr error, err error) {
|
) (cleanupErr error, err error) {
|
||||||
if oldContainer.ID == "" {
|
if oldContainer.ID == "" {
|
||||||
return nil, fmt.Errorf("original container ID is empty")
|
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 {
|
if err := cli.ContainerStart(ctx, created.ID, container.StartOptions{}); err != nil {
|
||||||
logContainerSwitchStep(logger, "ContainerStartReplacement", name, err)
|
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)
|
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)
|
return nil, errors.Join(fmt.Errorf("start new container failed: %w", err), rollbackErr)
|
||||||
}
|
}
|
||||||
@@ -317,6 +329,7 @@ func switchContainer(
|
|||||||
if wasRunning {
|
if wasRunning {
|
||||||
if err := waitContainerReady(ctx, cli, created.ID); err != nil {
|
if err := waitContainerReady(ctx, cli, created.ID); err != nil {
|
||||||
logContainerSwitchStep(logger, "ContainerWaitReplacement", name, err)
|
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)
|
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)
|
return nil, errors.Join(fmt.Errorf("new container readiness check failed: %w", err), rollbackErr)
|
||||||
}
|
}
|
||||||
@@ -333,9 +346,49 @@ const (
|
|||||||
containerStartPollInterval = time.Second
|
containerStartPollInterval = time.Second
|
||||||
containerHealthCheckMinWait = 30 * time.Second
|
containerHealthCheckMinWait = 30 * time.Second
|
||||||
containerHealthCheckMaxWait = 10 * time.Minute
|
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)
|
info, err := cli.ContainerInspect(ctx, containerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -347,14 +400,15 @@ func waitContainerReady(ctx context.Context, cli containerSwitchClient, containe
|
|||||||
return waitContainerStable(ctx, cli, containerID, info)
|
return waitContainerStable(ctx, cli, containerID, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initialRestartCount := info.RestartCount
|
||||||
timeout := containerHealthCheckTimeout(info.Config)
|
timeout := containerHealthCheckTimeout(info.Config)
|
||||||
deadline := time.NewTimer(timeout)
|
deadline := time.NewTimer(timeout)
|
||||||
ticker := time.NewTicker(time.Second)
|
ticker := time.NewTicker(time.Second)
|
||||||
defer deadline.Stop()
|
defer deadline.Stop()
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for {
|
for {
|
||||||
if info.State.Restarting || info.RestartCount != 0 {
|
if info.State.Restarting || info.RestartCount != initialRestartCount {
|
||||||
return fmt.Errorf("container restarted %d times during startup", info.RestartCount)
|
return fmt.Errorf("container restart count changed from %d to %d during startup", initialRestartCount, info.RestartCount)
|
||||||
}
|
}
|
||||||
if info.State.Health == nil {
|
if info.State.Health == nil {
|
||||||
return fmt.Errorf("container health status is unavailable")
|
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
|
startedAt := initial.State.StartedAt
|
||||||
if err := checkContainerStableState(initial, startedAt); err != nil {
|
restartCount := initial.RestartCount
|
||||||
|
if err := checkContainerStableState(initial, startedAt, restartCount); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
deadline := time.NewTimer(containerStartStabilization)
|
deadline := time.NewTimer(containerStartStabilization)
|
||||||
@@ -400,25 +455,25 @@ func waitContainerStable(ctx context.Context, cli containerSwitchClient, contain
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return checkContainerStableState(info, startedAt)
|
return checkContainerStableState(info, startedAt, restartCount)
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
info, err := cli.ContainerInspect(ctx, containerID)
|
info, err := cli.ContainerInspect(ctx, containerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := checkContainerStableState(info, startedAt); err != nil {
|
if err := checkContainerStableState(info, startedAt, restartCount); err != nil {
|
||||||
return err
|
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 {
|
if err := checkContainerRunningState(info); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if info.State.Restarting || info.RestartCount != 0 {
|
if info.State.Restarting || info.RestartCount != restartCount {
|
||||||
return fmt.Errorf("container restarted %d times during startup", info.RestartCount)
|
return fmt.Errorf("container restart count changed from %d to %d during startup", restartCount, info.RestartCount)
|
||||||
}
|
}
|
||||||
if startedAt != "" && info.State.StartedAt != startedAt {
|
if startedAt != "" && info.State.StartedAt != startedAt {
|
||||||
return fmt.Errorf("container start time changed during startup")
|
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)
|
endpoints := make(map[string]*network.EndpointSettings, len(extras)+1)
|
||||||
if primary != nil {
|
if primary != nil {
|
||||||
for name, endpoint := range primary.EndpointsConfig {
|
for name, endpoint := range primary.EndpointsConfig {
|
||||||
if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil {
|
if name != "bridge" && endpoint != nil {
|
||||||
endpoints[name] = endpoint
|
endpoints[name] = endpoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for name, endpoint := range extras {
|
for name, endpoint := range extras {
|
||||||
if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil {
|
if name != "bridge" && endpoint != nil {
|
||||||
endpoints[name] = endpoint
|
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)
|
return disconnected, fmt.Errorf("disconnect original container from network %s failed: %w", name, err)
|
||||||
}
|
}
|
||||||
disconnected = append(disconnected, containerNetworkAttachment{
|
disconnected = append(disconnected, containerNetworkAttachment{
|
||||||
name: name,
|
name: name,
|
||||||
endpoint: endpoints[name],
|
endpoint: endpoints[name],
|
||||||
isDynamic: isDynamicContainerNetwork(oldContainer.NetworkSettings, name),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return disconnected, nil
|
return disconnected, nil
|
||||||
@@ -566,10 +620,6 @@ func reconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitch
|
|||||||
var reconnectErr error
|
var reconnectErr error
|
||||||
for _, attachment := range attachments {
|
for _, attachment := range attachments {
|
||||||
err := cli.NetworkConnect(ctx, attachment.name, containerID, attachment.endpoint)
|
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 {
|
if err != nil {
|
||||||
reconnectErr = errors.Join(reconnectErr, fmt.Errorf("reconnect original container to network %s failed: %w", attachment.name, err))
|
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
|
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
|
var rollbackErr error
|
||||||
backupName := containerSwitchBackupName(oldContainerID)
|
backupName := containerSwitchBackupName(oldContainerID)
|
||||||
if newContainer != "" {
|
if newContainer != "" {
|
||||||
@@ -602,7 +652,7 @@ func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, ol
|
|||||||
reconnectErr := reconnectOriginalContainerNetworks(ctx, cli, oldContainerID, disconnectedNetworks)
|
reconnectErr := reconnectOriginalContainerNetworks(ctx, cli, oldContainerID, disconnectedNetworks)
|
||||||
logContainerSwitchStep(logger, "ContainerRollbackReconnectOld", currentName, reconnectErr)
|
logContainerSwitchStep(logger, "ContainerRollbackReconnectOld", currentName, reconnectErr)
|
||||||
rollbackErr = errors.Join(rollbackErr, reconnectErr)
|
rollbackErr = errors.Join(rollbackErr, reconnectErr)
|
||||||
if wasRunning {
|
if wasRunning && reconnectErr == nil {
|
||||||
restartErr := restartOriginalContainer(ctx, cli, oldContainerID)
|
restartErr := restartOriginalContainer(ctx, cli, oldContainerID)
|
||||||
logContainerSwitchStep(logger, "ContainerRollbackRestartOld", currentName, restartErr)
|
logContainerSwitchStep(logger, "ContainerRollbackRestartOld", currentName, restartErr)
|
||||||
rollbackErr = errors.Join(rollbackErr, restartErr)
|
rollbackErr = errors.Join(rollbackErr, restartErr)
|
||||||
@@ -610,17 +660,8 @@ func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, ol
|
|||||||
return rollbackErr
|
return rollbackErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func createContainerWithOldNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkSettings *container.NetworkSettings, name string) (container.CreateResponse, error) {
|
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) {
|
||||||
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(networkSettings, hostConf)
|
created, err := client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name)
|
||||||
normalizeContainerEndpointSettings(ctx, client, networkConf, extraNetworks)
|
|
||||||
var primaryEndpoints map[string]*network.EndpointSettings
|
|
||||||
if networkConf != nil {
|
|
||||||
primaryEndpoints = networkConf.EndpointsConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
created, err := createContainerWithDynamicIPFallback(func() (container.CreateResponse, error) {
|
|
||||||
return client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name)
|
|
||||||
}, primaryEndpoints, networkSettings)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return created, err
|
return created, err
|
||||||
}
|
}
|
||||||
@@ -632,9 +673,6 @@ func createContainerWithOldNetworks(ctx context.Context, client *client.Client,
|
|||||||
sort.Strings(extraNames)
|
sort.Strings(extraNames)
|
||||||
for _, item := range extraNames {
|
for _, item := range extraNames {
|
||||||
err := client.NetworkConnect(ctx, item, created.ID, extraNetworks[item])
|
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 {
|
if err != nil {
|
||||||
_ = client.ContainerRemove(ctx, created.ID, container.RemoveOptions{Force: true})
|
_ = client.ContainerRemove(ctx, created.ID, container.RemoveOptions{Force: true})
|
||||||
return created, err
|
return created, err
|
||||||
@@ -642,16 +680,3 @@ func createContainerWithOldNetworks(ctx context.Context, client *client.Client,
|
|||||||
}
|
}
|
||||||
return created, nil
|
return created, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createContainerWithDynamicIPFallback(
|
|
||||||
create func() (container.CreateResponse, error),
|
|
||||||
endpoints map[string]*network.EndpointSettings,
|
|
||||||
networkSettings *container.NetworkSettings,
|
|
||||||
) (container.CreateResponse, error) {
|
|
||||||
for {
|
|
||||||
created, err := create()
|
|
||||||
if err == nil || created.ID != "" || !clearUnsupportedDynamicEndpointIPAM(err, endpoints, networkSettings) {
|
|
||||||
return created, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -538,11 +538,14 @@ func (u *CronjobService) CleanRecord(req dto.CronjobClean) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, del := range delRecords {
|
for _, del := range delRecords {
|
||||||
|
if del.Status == constant.StatusWaiting || del.Status == constant.StatusRunning {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := cronjobRepo.DeleteRecord(repo.WithByID(del.ID)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
_ = os.RemoveAll(del.Records)
|
_ = os.RemoveAll(del.Records)
|
||||||
}
|
}
|
||||||
if err := cronjobRepo.DeleteRecord(cronjobRepo.WithByJobID(int(req.CronjobID))); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
|
|||||||
record.Name = dbInfo.Database
|
record.Name = dbInfo.Database
|
||||||
record.DetailName = dbInfo.Name
|
record.DetailName = dbInfo.Name
|
||||||
record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
|
record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
|
||||||
|
record.Args = encodeBackupArgs(dbInfo.Args)
|
||||||
|
|
||||||
backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name))
|
backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name))
|
||||||
switch dbInfo.DBType {
|
switch dbInfo.DBType {
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
|
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
||||||
@@ -184,10 +183,30 @@ func (u *DashboardService) LoadBaseInfo(ioOption string, netOption string) (*dto
|
|||||||
|
|
||||||
func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent {
|
func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent {
|
||||||
var currentInfo dto.DashboardCurrent
|
var currentInfo dto.DashboardCurrent
|
||||||
hostInfo, _ := psutil.HOST.GetHostInfo(false)
|
shotTime := time.Now()
|
||||||
currentInfo.Uptime = hostInfo.Uptime
|
hostInfo, err := psutil.HOST.GetHostInfo(false)
|
||||||
currentInfo.TimeSinceUptime = time.Unix(int64(hostInfo.BootTime), 0).Format(constant.DateTimeLayout)
|
if err != nil {
|
||||||
currentInfo.RunningTime = loadRunningTime(hostInfo.Uptime)
|
global.LOG.Errorf("load host info failed: %v", err)
|
||||||
|
currentInfo.ShotTime = shotTime
|
||||||
|
return ¤tInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
uptime := hostInfo.Uptime
|
||||||
|
var bootTime uint64
|
||||||
|
if now := shotTime.Unix(); now > 0 {
|
||||||
|
nowUnix := uint64(now)
|
||||||
|
if hostInfo.BootTime > 0 && hostInfo.BootTime <= nowUnix {
|
||||||
|
bootTime = hostInfo.BootTime
|
||||||
|
uptime = nowUnix - bootTime
|
||||||
|
} else if uptime <= nowUnix {
|
||||||
|
bootTime = nowUnix - uptime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentInfo.Uptime = uptime
|
||||||
|
currentInfo.RunningTime = loadRunningTime(uptime)
|
||||||
|
if bootTime > 0 {
|
||||||
|
currentInfo.TimeSinceUptime = time.Unix(int64(bootTime), 0).Format(constant.DateTimeLayout)
|
||||||
|
}
|
||||||
currentInfo.Procs = hostInfo.Procs
|
currentInfo.Procs = hostInfo.Procs
|
||||||
currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false)
|
currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false)
|
||||||
|
|
||||||
@@ -224,8 +243,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
|
|||||||
currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent
|
currentInfo.SwapMemoryUsedPercent = swapInfo.UsedPercent
|
||||||
|
|
||||||
currentInfo.DiskData = loadDiskInfo()
|
currentInfo.DiskData = loadDiskInfo()
|
||||||
currentInfo.GPUData = loadGPUInfo()
|
currentInfo.GPUData, currentInfo.NPUData, currentInfo.XPUData = loadAcceleratorInfo()
|
||||||
currentInfo.XPUData = loadXpuInfo()
|
|
||||||
|
|
||||||
if ioOption == "all" {
|
if ioOption == "all" {
|
||||||
diskInfo, _ := disk.IOCounters()
|
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
|
return ¤tInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,7 +469,7 @@ func loadDiskInfo() []dto.DiskInfo {
|
|||||||
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
|
||||||
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
cmd.PipeCommand{Name: "awk", Args: []string{format}},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil && strings.TrimSpace(stdout) == "" {
|
||||||
return datas
|
return datas
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -549,32 +567,64 @@ func loadDiskInfo() []dto.DiskInfo {
|
|||||||
return datas
|
return datas
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadGPUInfo() []dto.GPUInfo {
|
func loadAcceleratorInfo() ([]dto.GPUInfo, []dto.NPUInfo, []dto.XPUInfo) {
|
||||||
ok, client := gpu.New()
|
ok, client := accelerator.New()
|
||||||
var list []interface{}
|
if !ok {
|
||||||
if ok {
|
return nil, nil, nil
|
||||||
info, err := client.LoadGpuInfo()
|
}
|
||||||
if err != nil || len(info.GPUs) == 0 {
|
snapshot, err := client.Collect(context.Background())
|
||||||
return nil
|
if err != nil || len(snapshot.Devices) == 0 {
|
||||||
}
|
return nil, nil, nil
|
||||||
for _, item := range info.GPUs {
|
}
|
||||||
list = append(list, item)
|
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 {
|
||||||
|
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.PowerUsage = dataItem.PowerDraw + " / " + 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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(list) == 0 {
|
return gpuData, npuData, xpuData
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var data []dto.GPUInfo
|
|
||||||
for _, gpu := range list {
|
|
||||||
var dataItem dto.GPUInfo
|
|
||||||
if err := copier.Copy(&dataItem, &gpu); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dataItem.PowerUsage = dataItem.PowerDraw + " / " + dataItem.MaxPowerLimit
|
|
||||||
dataItem.MemoryUsage = dataItem.MemUsed + " / " + dataItem.MemTotal
|
|
||||||
data = append(data, dataItem)
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppLauncher struct {
|
type AppLauncher struct {
|
||||||
@@ -590,32 +640,6 @@ func ArryContains(arr []string, element string) bool {
|
|||||||
return false
|
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 {
|
func loadOutboundIP() string {
|
||||||
conn, err := network.Dial("udp", "8.8.8.8:80")
|
conn, err := network.Dial("udp", "8.8.8.8:80")
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
rollbackPath = "1panel/tmp"
|
rollbackPath = "1panel/tmp"
|
||||||
upgradePath = "1panel/tmp/upgrade"
|
communityRestorePath = "1panel/tmp/community-restore"
|
||||||
uploadPath = "1panel/uploads"
|
upgradePath = "1panel/tmp/upgrade"
|
||||||
downloadPath = "1panel/download"
|
uploadPath = "1panel/uploads"
|
||||||
|
downloadPath = "1panel/download"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (u *DeviceService) Scan() dto.CleanData {
|
func (u *DeviceService) Scan() dto.CleanData {
|
||||||
@@ -58,7 +59,7 @@ func (u *DeviceService) Scan() dto.CleanData {
|
|||||||
|
|
||||||
SystemClean.BackupClean = loadBackupTree(fileOp)
|
SystemClean.BackupClean = loadBackupTree(fileOp)
|
||||||
|
|
||||||
rollBackTree := loadRollBackTree(fileOp)
|
rollBackTree := loadRollBackTree()
|
||||||
rollbackSize := uint64(0)
|
rollbackSize := uint64(0)
|
||||||
for _, rollback := range rollBackTree {
|
for _, rollback := range rollBackTree {
|
||||||
rollbackSize += rollback.Size
|
rollbackSize += rollback.Size
|
||||||
@@ -113,12 +114,15 @@ func (u *DeviceService) Clean(req []dto.Clean) {
|
|||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app"))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app"))
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database"))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database"))
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website"))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website"))
|
||||||
|
dropFileOrDir(path.Join(global.Dir.BaseDir, communityRestorePath))
|
||||||
case "rollback_app":
|
case "rollback_app":
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app", item.Name))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app", item.Name))
|
||||||
case "rollback_database":
|
case "rollback_database":
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database", item.Name))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database", item.Name))
|
||||||
case "rollback_website":
|
case "rollback_website":
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website", item.Name))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website", item.Name))
|
||||||
|
case "rollback_community_restore":
|
||||||
|
dropFileOrDir(path.Join(global.Dir.BaseDir, communityRestorePath, item.Name))
|
||||||
|
|
||||||
case "upload":
|
case "upload":
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, item.Name))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, item.Name))
|
||||||
@@ -214,6 +218,7 @@ func doSystemClean(taskItem *task.Task) func(t *task.Task) error {
|
|||||||
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "app"), taskItem, &size, &fileCount)
|
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "app"), taskItem, &size, &fileCount)
|
||||||
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "website"), taskItem, &size, &fileCount)
|
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "website"), taskItem, &size, &fileCount)
|
||||||
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "database"), taskItem, &size, &fileCount)
|
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "database"), taskItem, &size, &fileCount)
|
||||||
|
dropWithTask(path.Join(global.Dir.BaseDir, communityRestorePath), taskItem, &size, &fileCount)
|
||||||
|
|
||||||
upgrades := path.Join(global.Dir.BaseDir, upgradePath)
|
upgrades := path.Join(global.Dir.BaseDir, upgradePath)
|
||||||
oldUpgradeFiles, _ := os.ReadDir(upgrades)
|
oldUpgradeFiles, _ := os.ReadDir(upgrades)
|
||||||
@@ -606,20 +611,21 @@ func isExactPathMatch(path string, excludePaths []string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadRollBackTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
func loadRollBackTree() []dto.CleanTree {
|
||||||
var treeData []dto.CleanTree
|
var treeData []dto.CleanTree
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "app"), "rollback_app", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "app"), "rollback_app")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "website"), "rollback_website", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "website"), "rollback_website")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "database"), "rollback_database", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "database"), "rollback_database")
|
||||||
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, communityRestorePath), "rollback_community_restore")
|
||||||
|
|
||||||
return treeData
|
return treeData
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadUploadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
func loadUploadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||||
var treeData []dto.CleanTree
|
var treeData []dto.CleanTree
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "app"), "upload_app", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "app"), "upload_app")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "website"), "upload_website", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "website"), "upload_website")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "database"), "upload_database", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "database"), "upload_database")
|
||||||
|
|
||||||
path5 := path.Join(global.Dir.BaseDir, uploadPath)
|
path5 := path.Join(global.Dir.BaseDir, uploadPath)
|
||||||
uploadTreeData := loadTreeWithAllFile(true, path5, "upload", path5, fileOp)
|
uploadTreeData := loadTreeWithAllFile(true, path5, "upload", path5, fileOp)
|
||||||
@@ -630,9 +636,9 @@ func loadUploadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
|||||||
|
|
||||||
func loadDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
func loadDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||||
var treeData []dto.CleanTree
|
var treeData []dto.CleanTree
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "app"), "download_app", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "app"), "download_app")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "website"), "download_website", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "website"), "download_website")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "database"), "download_database", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "database"), "download_database")
|
||||||
|
|
||||||
path5 := path.Join(global.Dir.BaseDir, downloadPath)
|
path5 := path.Join(global.Dir.BaseDir, downloadPath)
|
||||||
uploadTreeData := loadTreeWithAllFile(true, path5, "download", path5, fileOp)
|
uploadTreeData := loadTreeWithAllFile(true, path5, "download", path5, fileOp)
|
||||||
@@ -814,16 +820,59 @@ func loadContainerTree() []dto.CleanTree {
|
|||||||
return treeData
|
return treeData
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTreeWithCheck(treeData []dto.CleanTree, pathItem, treeType string, fileOp fileUtils.FileOp) []dto.CleanTree {
|
func loadTreeWithCheck(treeData []dto.CleanTree, pathItem, treeType string) []dto.CleanTree {
|
||||||
size, _ := fileOp.GetDirSize(pathItem)
|
list, size := loadTreeWithFileSize(true, pathItem, treeType, pathItem)
|
||||||
if size == 0 {
|
if len(list) == 0 || size == 0 {
|
||||||
return treeData
|
return treeData
|
||||||
}
|
}
|
||||||
list := loadTreeWithAllFile(true, pathItem, treeType, pathItem, fileOp)
|
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: treeType, Size: size, IsCheck: size > 0, Children: list, Type: treeType, IsRecommend: true, CanDelete: false})
|
||||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: treeType, Size: uint64(size), IsCheck: size > 0, Children: list, Type: treeType, IsRecommend: true, CanDelete: false})
|
|
||||||
return treeData
|
return treeData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadTreeWithFileSize(isCheck bool, originalPath, treeType, pathItem string) ([]dto.CleanTree, uint64) {
|
||||||
|
var (
|
||||||
|
lists []dto.CleanTree
|
||||||
|
total uint64
|
||||||
|
)
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(pathItem)
|
||||||
|
if err != nil {
|
||||||
|
return lists, total
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
item := dto.CleanTree{
|
||||||
|
ID: uuid.NewString(),
|
||||||
|
Label: entry.Name(),
|
||||||
|
Type: treeType,
|
||||||
|
Name: strings.TrimPrefix(path.Join(pathItem, entry.Name()), originalPath+"/"),
|
||||||
|
IsCheck: isCheck,
|
||||||
|
IsRecommend: isCheck,
|
||||||
|
CanDelete: true,
|
||||||
|
}
|
||||||
|
entryPath := path.Join(pathItem, entry.Name())
|
||||||
|
if entry.IsDir() {
|
||||||
|
children, size := loadTreeWithFileSize(isCheck, originalPath, treeType, entryPath)
|
||||||
|
if len(children) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item.Children = children
|
||||||
|
item.Size = size
|
||||||
|
} else {
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item.Size = uint64(info.Size())
|
||||||
|
}
|
||||||
|
if item.Size == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total += item.Size
|
||||||
|
lists = append(lists, item)
|
||||||
|
}
|
||||||
|
return lists, total
|
||||||
|
}
|
||||||
|
|
||||||
func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree {
|
func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||||
var lists []dto.CleanTree
|
var lists []dto.CleanTree
|
||||||
files, err := os.ReadDir(pathItem)
|
files, err := os.ReadDir(pathItem)
|
||||||
|
|||||||
@@ -61,18 +61,21 @@ func parseDevice(dev LsblkDevice) []response.DiskBasicInfo {
|
|||||||
|
|
||||||
var used, avail, totalSize string
|
var used, avail, totalSize string
|
||||||
var usePercent int
|
var usePercent int
|
||||||
isMounted := mountPoint != ""
|
isMounted := mountPoint != "" && mountPoint != "-"
|
||||||
isSystem := false
|
isSystem := false
|
||||||
|
|
||||||
if dev.Fstype == "LVM2_member" && len(dev.Children) > 0 {
|
if dev.Fstype == "LVM2_member" && len(dev.Children) > 0 {
|
||||||
for _, child := range dev.Children {
|
for _, child := range dev.Children {
|
||||||
if child.Type == "lvm" && child.Mountpoint != "" {
|
if child.Type == "lvm" && child.Mountpoint != "" && child.Mountpoint != "-" {
|
||||||
devicePath := "/dev/mapper/" + child.Name
|
totalSize, used, avail, usePercent, _ := getDiskUsageInfo(child.Mountpoint)
|
||||||
totalSize, used, avail, usePercent, _ := getDiskUsageInfo(devicePath)
|
childSize := child.Size
|
||||||
|
if totalSize != "" {
|
||||||
|
childSize = totalSize
|
||||||
|
}
|
||||||
|
|
||||||
childInfo := response.DiskBasicInfo{
|
childInfo := response.DiskBasicInfo{
|
||||||
Device: dev.Name,
|
Device: dev.Name,
|
||||||
Size: totalSize,
|
Size: childSize,
|
||||||
Model: dev.Model,
|
Model: dev.Model,
|
||||||
DiskType: diskType,
|
DiskType: diskType,
|
||||||
Filesystem: child.Fstype,
|
Filesystem: child.Fstype,
|
||||||
@@ -91,8 +94,7 @@ func parseDevice(dev LsblkDevice) []response.DiskBasicInfo {
|
|||||||
return list
|
return list
|
||||||
} else if isMounted {
|
} else if isMounted {
|
||||||
isSystem = isSystemDisk(mountPoint)
|
isSystem = isSystemDisk(mountPoint)
|
||||||
devicePath := "/dev/" + dev.Name
|
totalSize, used, avail, usePercent, _ = getDiskUsageInfo(mountPoint)
|
||||||
totalSize, used, avail, usePercent, _ = getDiskUsageInfo(devicePath)
|
|
||||||
if totalSize != "" {
|
if totalSize != "" {
|
||||||
size = totalSize
|
size = totalSize
|
||||||
}
|
}
|
||||||
@@ -229,9 +231,14 @@ func parseLsblkOutput(output string) ([]response.DiskBasicInfo, error) {
|
|||||||
size := fields["SIZE"]
|
size := fields["SIZE"]
|
||||||
|
|
||||||
if diskType == "lvm" {
|
if diskType == "lvm" {
|
||||||
total, used, avail, usePercent, _ := getDiskUsageInfo("/dev/mapper/" + name)
|
var total, used, avail string
|
||||||
if total != "" && fsType != "" {
|
var usePercent int
|
||||||
size = total
|
isMounted := mountPoint != "" && mountPoint != "-"
|
||||||
|
if isMounted {
|
||||||
|
total, used, avail, usePercent, _ = getDiskUsageInfo(mountPoint)
|
||||||
|
if total != "" && fsType != "" {
|
||||||
|
size = total
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lvmInfo := response.DiskBasicInfo{
|
lvmInfo := response.DiskBasicInfo{
|
||||||
@@ -246,7 +253,7 @@ func parseLsblkOutput(output string) ([]response.DiskBasicInfo, error) {
|
|||||||
Avail: avail,
|
Avail: avail,
|
||||||
UsePercent: usePercent,
|
UsePercent: usePercent,
|
||||||
MountPoint: mountPoint,
|
MountPoint: mountPoint,
|
||||||
IsMounted: mountPoint != "" && mountPoint != "-",
|
IsMounted: isMounted,
|
||||||
Serial: fields["SERIAL"],
|
Serial: fields["SERIAL"],
|
||||||
}
|
}
|
||||||
lvmMap[name] = lvmInfo
|
lvmMap[name] = lvmInfo
|
||||||
@@ -269,8 +276,8 @@ func parseLsblkOutput(output string) ([]response.DiskBasicInfo, error) {
|
|||||||
used, avail, totalSize string
|
used, avail, totalSize string
|
||||||
usePercent int
|
usePercent int
|
||||||
)
|
)
|
||||||
if mountPoint != "" {
|
if mountPoint != "" && mountPoint != "-" {
|
||||||
totalSize, used, avail, usePercent, _ = getDiskUsageInfo("/dev/" + name)
|
totalSize, used, avail, usePercent, _ = getDiskUsageInfo(mountPoint)
|
||||||
if totalSize != "" {
|
if totalSize != "" {
|
||||||
size = totalSize
|
size = totalSize
|
||||||
}
|
}
|
||||||
@@ -387,26 +394,39 @@ func getParentDevice(device string) string {
|
|||||||
return device
|
return device
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDiskUsageInfo(device string) (size, used, avail string, usePercent int, err error) {
|
func getDiskUsageInfo(mountPoint string) (size, used, avail string, usePercent int, err error) {
|
||||||
output, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("df", "-h", device)
|
// 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 {
|
if err != nil {
|
||||||
return "", "", "", 0, 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")
|
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||||
if len(lines) > 1 {
|
for i := len(lines) - 1; i >= 0; i-- {
|
||||||
output = lines[len(lines)-1]
|
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)
|
return "", "", "", 0, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatDisk(req dto.DiskFormatRequest) error {
|
func formatDisk(req dto.DiskFormatRequest) error {
|
||||||
|
|||||||
+118
-1
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -14,14 +15,19 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const dockerNftablesMinVersion = "29.0.0"
|
||||||
|
|
||||||
type DockerService struct{}
|
type DockerService struct{}
|
||||||
|
|
||||||
type IDockerService interface {
|
type IDockerService interface {
|
||||||
UpdateConf(req dto.SettingUpdate, withRestart bool) error
|
UpdateConf(req dto.SettingUpdate, withRestart bool) error
|
||||||
|
UpdateFirewallBackend(backend string) error
|
||||||
UpdateLogOption(req dto.LogOption) error
|
UpdateLogOption(req dto.LogOption) error
|
||||||
UpdateIpv6Option(req dto.Ipv6Option) error
|
UpdateIpv6Option(req dto.Ipv6Option) error
|
||||||
UpdateConfByFile(info dto.DaemonJsonUpdateByFile) error
|
UpdateConfByFile(info dto.DaemonJsonUpdateByFile) error
|
||||||
@@ -30,6 +36,115 @@ type IDockerService interface {
|
|||||||
OperateDocker(req dto.DockerOperation) error
|
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 := docker_guard.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 {
|
func NewIDockerService() IDockerService {
|
||||||
return &DockerService{}
|
return &DockerService{}
|
||||||
}
|
}
|
||||||
@@ -167,7 +282,9 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate, withRestart bool) erro
|
|||||||
delete(daemonMap, "ipv6")
|
delete(daemonMap, "ipv6")
|
||||||
delete(daemonMap, "fixed-cidr-v6")
|
delete(daemonMap, "fixed-cidr-v6")
|
||||||
delete(daemonMap, "ip6tables")
|
delete(daemonMap, "ip6tables")
|
||||||
delete(daemonMap, "experimental")
|
if configuredDockerFirewallBackend() != constant.FirewallProviderNftables {
|
||||||
|
delete(daemonMap, "experimental")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case "LogOption":
|
case "LogOption":
|
||||||
if req.Value == "disable" {
|
if req.Value == "disable" {
|
||||||
|
|||||||
@@ -40,12 +40,14 @@ var (
|
|||||||
settingRepo = repo.NewISettingRepo()
|
settingRepo = repo.NewISettingRepo()
|
||||||
backupRepo = repo.NewIBackupRepo()
|
backupRepo = repo.NewIBackupRepo()
|
||||||
|
|
||||||
websiteRepo = repo.NewIWebsiteRepo()
|
websiteRepo = repo.NewIWebsiteRepo()
|
||||||
websiteDomainRepo = repo.NewIWebsiteDomainRepo()
|
websiteDomainRepo = repo.NewIWebsiteDomainRepo()
|
||||||
websiteDnsRepo = repo.NewIWebsiteDnsAccountRepo()
|
websiteDnsRepo = repo.NewIWebsiteDnsAccountRepo()
|
||||||
websiteSSLRepo = repo.NewISSLRepo()
|
websiteSSLRepo = repo.NewISSLRepo()
|
||||||
websiteAcmeRepo = repo.NewIAcmeAccountRepo()
|
websiteAcmeRepo = repo.NewIAcmeAccountRepo()
|
||||||
websiteCARepo = repo.NewIWebsiteCARepo()
|
websiteCARepo = repo.NewIWebsiteCARepo()
|
||||||
|
websiteTemplateRepo = repo.NewIWebsiteTemplateRepo()
|
||||||
|
websiteTemplateOutputRepo = repo.NewIWebsiteTemplateOutputRepo()
|
||||||
|
|
||||||
snapshotRepo = repo.NewISnapshotRepo()
|
snapshotRepo = repo.NewISnapshotRepo()
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/toolbox"
|
"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" {
|
if req.Value == "firewallcmd-ipset" {
|
||||||
itemName = "firewalld"
|
itemName = "firewalld"
|
||||||
}
|
}
|
||||||
client, err := firewall.NewFirewallClient()
|
client, err := NewSelectedSystemFirewallClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+124
-21
@@ -16,6 +16,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@@ -51,6 +52,8 @@ type FileService struct {
|
|||||||
|
|
||||||
const fileHistorySnapshotMaxSize = 10 * 1024 * 1024
|
const fileHistorySnapshotMaxSize = 10 * 1024 * 1024
|
||||||
|
|
||||||
|
var fileTransferLocks = newFileTransferLocks()
|
||||||
|
|
||||||
type IFileService interface {
|
type IFileService interface {
|
||||||
GetFileList(op request.FileOption) (response.FileInfo, error)
|
GetFileList(op request.FileOption) (response.FileInfo, error)
|
||||||
SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error)
|
SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error)
|
||||||
@@ -71,6 +74,7 @@ type IFileService interface {
|
|||||||
ChangeName(req request.FileRename) error
|
ChangeName(req request.FileRename) error
|
||||||
Wget(w request.FileWget) (string, error)
|
Wget(w request.FileWget) (string, error)
|
||||||
MvFile(m request.FileMove) error
|
MvFile(m request.FileMove) error
|
||||||
|
StopMvFile(taskID string) error
|
||||||
ChangeOwner(req request.FileRoleUpdate) error
|
ChangeOwner(req request.FileRoleUpdate) error
|
||||||
ChangeMode(op request.FileCreate) error
|
ChangeMode(op request.FileCreate) error
|
||||||
BatchChangeModeAndOwner(op request.FileRoleReq) 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
|
total, start, end := len(files), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||||
if start > total {
|
if start > total {
|
||||||
backData = make([]response.UploadInfo, 0)
|
backData = make([]response.UploadInfo, 0)
|
||||||
@@ -466,7 +474,7 @@ func (f *FileService) Compress(c request.FileCompress) error {
|
|||||||
|
|
||||||
func preflightCompressTool(compressType files.CompressType) error {
|
func preflightCompressTool(compressType files.CompressType) error {
|
||||||
switch compressType {
|
switch compressType {
|
||||||
case files.TarGz, files.Rar, files.X7z:
|
case files.Tar, files.Gz, files.Bz2, files.TarBz2, files.Tgz, files.TarGz, files.Xz, files.TarXz, files.Rar, files.X7z:
|
||||||
_, err := files.NewShellArchiver(compressType)
|
_, err := files.NewShellArchiver(compressType)
|
||||||
return err
|
return err
|
||||||
default:
|
default:
|
||||||
@@ -476,7 +484,7 @@ func preflightCompressTool(compressType files.CompressType) error {
|
|||||||
|
|
||||||
func preflightDecompressTool(decompressType files.CompressType) error {
|
func preflightDecompressTool(decompressType files.CompressType) error {
|
||||||
switch decompressType {
|
switch decompressType {
|
||||||
case files.Rar, files.X7z:
|
case files.Rar:
|
||||||
_, err := files.NewExtractShellArchiver(decompressType)
|
_, err := files.NewExtractShellArchiver(decompressType)
|
||||||
return err
|
return err
|
||||||
default:
|
default:
|
||||||
@@ -533,7 +541,10 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
|
|||||||
_ = os.RemoveAll(c.Dst)
|
_ = os.RemoveAll(c.Dst)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if err := fo.Decompress(t.TaskCtx, c.Path, tempDst, files.CompressType(c.Type), c.Secret); err != nil {
|
if err := fo.DecompressWithOptions(t.TaskCtx, c.Path, tempDst, files.CompressType(c.Type), c.Secret, files.DecompressOptions{
|
||||||
|
PreserveOwner: true,
|
||||||
|
AllowCLIReextract: true,
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := fo.CreateDir(c.Dst, constant.DirPerm); err != nil {
|
if err := fo.CreateDir(c.Dst, constant.DirPerm); err != nil {
|
||||||
@@ -551,19 +562,42 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyDecompressTree(ctx context.Context, srcDir, dstDir string) error {
|
func copyDecompressTree(ctx context.Context, srcDir, dstDir string) error {
|
||||||
|
state := decompressCopyState{hardlinks: make(map[decompressFileIdentity]string)}
|
||||||
entries, err := os.ReadDir(srcDir)
|
entries, err := os.ReadDir(srcDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if err := copyDecompressEntry(ctx, filepath.Join(srcDir, entry.Name()), filepath.Join(dstDir, entry.Name())); err != nil {
|
if err := copyDecompressEntryWithState(ctx, filepath.Join(srcDir, entry.Name()), filepath.Join(dstDir, entry.Name()), &state); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type decompressFileIdentity struct {
|
||||||
|
device uint64
|
||||||
|
inode uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type decompressCopyState struct {
|
||||||
|
hardlinks map[decompressFileIdentity]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func decompressHardlinkIdentity(info os.FileInfo) (decompressFileIdentity, bool) {
|
||||||
|
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||||
|
if !ok || stat.Nlink < 2 {
|
||||||
|
return decompressFileIdentity{}, false
|
||||||
|
}
|
||||||
|
return decompressFileIdentity{device: uint64(stat.Dev), inode: uint64(stat.Ino)}, true
|
||||||
|
}
|
||||||
|
|
||||||
func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr error) {
|
func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr error) {
|
||||||
|
state := decompressCopyState{hardlinks: make(map[decompressFileIdentity]string)}
|
||||||
|
return copyDecompressEntryWithState(ctx, srcPath, dstPath, &state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyDecompressEntryWithState(ctx context.Context, srcPath, dstPath string, state *decompressCopyState) (retErr error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -605,13 +639,16 @@ func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr e
|
|||||||
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
|
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := os.Chmod(dstPath, info.Mode().Perm()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
entries, err := os.ReadDir(srcPath)
|
entries, err := os.ReadDir(srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if err := copyDecompressEntry(ctx, filepath.Join(srcPath, entry.Name()), filepath.Join(dstPath, entry.Name())); err != nil {
|
if err := copyDecompressEntryWithState(ctx, filepath.Join(srcPath, entry.Name()), filepath.Join(dstPath, entry.Name()), state); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -634,6 +671,15 @@ func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr e
|
|||||||
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
|
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
identity, isHardlink := decompressHardlinkIdentity(info)
|
||||||
|
if !keepExistingFile && isHardlink {
|
||||||
|
if existingPath, ok := state.hardlinks[identity]; ok {
|
||||||
|
if err := os.Link(existingPath, dstPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
srcFile, err := os.Open(srcPath)
|
srcFile, err := os.Open(srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -658,6 +704,12 @@ func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr e
|
|||||||
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
|
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := os.Chmod(dstPath, info.Mode().Perm()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isHardlink {
|
||||||
|
state.hardlinks[identity] = dstPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
|
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
|
||||||
}
|
}
|
||||||
@@ -667,7 +719,7 @@ func applyDecompressOwnership(srcPath, dstPath string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
stat, ok := info.Sys().(*unix.Stat_t)
|
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -844,6 +896,7 @@ func (f *FileService) Wget(w request.FileWget) (string, error) {
|
|||||||
key := "file-wget-" + common.GetUuid()
|
key := "file-wget-" + common.GetUuid()
|
||||||
options := files.DownloadOptions{
|
options := files.DownloadOptions{
|
||||||
IgnoreCertificate: w.IgnoreCertificate,
|
IgnoreCertificate: w.IgnoreCertificate,
|
||||||
|
UseServerFilename: w.UseServerFilename,
|
||||||
}
|
}
|
||||||
if w.UseProxy {
|
if w.UseProxy {
|
||||||
systemProxy, err := NewISettingService().GetSystemProxy()
|
systemProxy, err := NewISettingService().GetSystemProxy()
|
||||||
@@ -863,17 +916,62 @@ func (f *FileService) Wget(w request.FileWget) (string, error) {
|
|||||||
|
|
||||||
func (f *FileService) MvFile(m request.FileMove) error {
|
func (f *FileService) MvFile(m request.FileMove) error {
|
||||||
fo := files.NewFileOp()
|
fo := files.NewFileOp()
|
||||||
|
if err := validateFileMove(fo, m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if m.TaskID == "" {
|
||||||
|
m.TaskID = common.GetUuid()
|
||||||
|
}
|
||||||
|
if !fileTransferLocks.Acquire(m.TaskID, getFileTransferPaths(m)) {
|
||||||
|
return buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
taskItem, err := task.NewTask(m.NewPath, task.TaskExec, task.TaskScopeTask, m.TaskID, 1)
|
||||||
|
if err != nil {
|
||||||
|
fileTransferLocks.Release(m.TaskID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
defer fileTransferLocks.Release(m.TaskID)
|
||||||
|
taskItem.AddSubTaskWithOps(m.NewPath, func(t *task.Task) error {
|
||||||
|
t.LogStart(m.NewPath)
|
||||||
|
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) {
|
if !fo.Stat(m.NewPath) {
|
||||||
return buserr.New("ErrPathNotFound")
|
return buserr.New("ErrPathNotFound")
|
||||||
}
|
}
|
||||||
for _, oldPath := range m.OldPaths {
|
for _, oldPath := range append(append([]string{}, m.OldPaths...), m.CoverPaths...) {
|
||||||
if !fo.Stat(oldPath) {
|
if !fo.Stat(oldPath) {
|
||||||
return buserr.WithName("ErrFileNotFound", 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 buserr.New("ErrMovePathFailed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileService) moveFileWithContext(ctx context.Context, m request.FileMove) error {
|
||||||
|
fo := files.NewFileOp()
|
||||||
type moveSnapshot struct {
|
type moveSnapshot struct {
|
||||||
path string
|
path string
|
||||||
content []byte
|
content []byte
|
||||||
@@ -889,13 +987,25 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
|||||||
}
|
}
|
||||||
if len(m.CoverPaths) > 0 {
|
if len(m.CoverPaths) > 0 {
|
||||||
for _, src := range m.CoverPaths {
|
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)
|
errs = append(errs, err)
|
||||||
global.LOG.Errorf("cut copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
|
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
|
return err
|
||||||
}
|
}
|
||||||
for _, snapshot := range snapshots {
|
for _, snapshot := range snapshots {
|
||||||
@@ -906,18 +1016,18 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return aggregateFileMoveErrors(errs)
|
||||||
}
|
}
|
||||||
if m.Type == "copy" {
|
if m.Type == "copy" {
|
||||||
for _, src := range m.OldPaths {
|
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)
|
errs = append(errs, err)
|
||||||
global.LOG.Errorf("copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
|
global.LOG.Errorf("copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(m.CoverPaths) > 0 {
|
if len(m.CoverPaths) > 0 {
|
||||||
for _, src := range m.CoverPaths {
|
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)
|
errs = append(errs, err)
|
||||||
global.LOG.Errorf("copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
|
global.LOG.Errorf("copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
|
||||||
}
|
}
|
||||||
@@ -925,14 +1035,7 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var errString string
|
return aggregateFileMoveErrors(errs)
|
||||||
for _, err := range errs {
|
|
||||||
errString += err.Error() + "\n"
|
|
||||||
}
|
|
||||||
if errString != "" {
|
|
||||||
return errors.New(errString)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readEditableFileHistoryContent(filePath string) ([]byte, os.FileMode, bool) {
|
func readEditableFileHistoryContent(filePath string) ([]byte, os.FileMode, bool) {
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
+2947
-621
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
firewallTaskHost = "FirewallTaskHost"
|
||||||
|
firewallTaskForwarding = "FirewallTaskForwarding"
|
||||||
|
firewallTaskDocker = "FirewallTaskDocker"
|
||||||
|
)
|
||||||
|
|
||||||
|
func firewallTaskName(operation, subsystem, backend string) string {
|
||||||
|
name := i18n.GetMsgByKey(subsystem)
|
||||||
|
if backend != "" {
|
||||||
|
name += " · " + backend
|
||||||
|
}
|
||||||
|
key := "FirewallRule" + operation
|
||||||
|
if operation == task.TaskExec {
|
||||||
|
key = "FirewallTaskInitialize"
|
||||||
|
}
|
||||||
|
return i18n.GetMsgWithMap(key, map[string]interface{}{"name": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
func queueFirewallRuleTask(subsystem, operation string, labels []string, apply func(context.Context) error) (dto.FilterChainOperationResponse, error) {
|
||||||
|
taskItem, err := task.NewTask(firewallTaskName(operation, subsystem, ""), operation, task.TaskScopeFirewall, "", 0)
|
||||||
|
if err != nil {
|
||||||
|
return dto.FilterChainOperationResponse{}, err
|
||||||
|
}
|
||||||
|
taskItem.AddSubTaskWithOps(taskItem.Name, func(t *task.Task) error {
|
||||||
|
t.Logf("rules=%d", len(labels))
|
||||||
|
err := t.TaskCtx.Err()
|
||||||
|
if err == nil {
|
||||||
|
err = apply(t.TaskCtx)
|
||||||
|
}
|
||||||
|
succeeded, failed := 0, 0
|
||||||
|
for _, label := range labels {
|
||||||
|
if err != nil {
|
||||||
|
failed++
|
||||||
|
t.LogFailedWithErr(label, err)
|
||||||
|
} else {
|
||||||
|
succeeded++
|
||||||
|
t.LogSuccess(label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Log(i18n.GetMsgWithMap("FirewallRuleOperationResult", map[string]interface{}{
|
||||||
|
"succeeded": succeeded, "failed": failed,
|
||||||
|
}))
|
||||||
|
return err
|
||||||
|
}, nil, 0, 0)
|
||||||
|
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
|
||||||
|
taskItem.LogFailedWithErr(taskItem.Name, err)
|
||||||
|
closeUnstartedFirewallTask(taskItem)
|
||||||
|
return dto.FilterChainOperationResponse{}, fmt.Errorf("save firewall rule task: %w", err)
|
||||||
|
}
|
||||||
|
go func() { _ = taskItem.Execute() }()
|
||||||
|
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeUnstartedFirewallTask(t *task.Task) {
|
||||||
|
if cancel, ok := global.LoadTaskCancel(t.TaskID); ok {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
global.RemoveTaskCancel(t.TaskID)
|
||||||
|
if closer, ok := t.Logger.Out.(io.Closer); ok {
|
||||||
|
_ = closer.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||||
|
)
|
||||||
|
|
||||||
|
func selectedDockerFirewallBackend(fallback string) string {
|
||||||
|
selected := configuredDockerFirewallBackend()
|
||||||
|
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
fallback = strings.ToLower(strings.TrimSpace(fallback))
|
||||||
|
if fallback == constant.FirewallProviderNftables {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return constant.FirewallProviderIptables
|
||||||
|
}
|
||||||
|
|
||||||
|
func configuredDockerFirewallBackend() string {
|
||||||
|
if global.DB == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
selected, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||||
|
selected = strings.ToLower(strings.TrimSpace(selected))
|
||||||
|
if selected == constant.FirewallProviderIptables || selected == constant.FirewallProviderNftables {
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectedSystemFirewallClient() (lifecycle.Client, error) {
|
||||||
|
if provider := configuredSystemFirewallBackend(); provider != "" {
|
||||||
|
return lifecycle.NewClientFor(provider)
|
||||||
|
}
|
||||||
|
client, err := lifecycle.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = settingRepo.UpdateOrCreate(constant.FirewallSystemBackendKey, client.Name())
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func configuredSystemFirewallBackend() string {
|
||||||
|
if global.DB == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
provider, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
||||||
|
return strings.TrimSpace(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSelectedSystemFirewallClient() (lifecycle.Client, error) {
|
||||||
|
return selectedSystemFirewallClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectedSystemFirewallProvider() (string, error) {
|
||||||
|
client, err := selectedSystemFirewallClient()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return client.Name(), nil
|
||||||
|
}
|
||||||
@@ -1,175 +1,637 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"os"
|
||||||
"strings"
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
"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/docker_guard"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client/iptables"
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||||
|
filterruntime "github.com/1Panel-dev/1Panel/agent/utils/firewall/filter/runtime"
|
||||||
|
"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 {
|
type IFirewallSettingService interface {
|
||||||
Port string
|
CreatePortWhitelist(context.Context, dto.FirewallPortWhitelistCreate) error
|
||||||
Protocol string
|
UpdatePortWhitelist(context.Context, dto.FirewallPortWhitelistUpdate) error
|
||||||
|
DeletePortWhitelist(context.Context, dto.FirewallPortWhitelistDelete) error
|
||||||
|
Load(context.Context) (dto.FirewallSettings, error)
|
||||||
|
Operate(context.Context, dto.FirewallBackendOperation) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfiguredFirewallPortWhiteList() ([]firewallPortWhitelist, error) {
|
type FirewallSettingService struct{}
|
||||||
value, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList)
|
|
||||||
if err != nil {
|
var firewallWhitelistMu sync.Mutex
|
||||||
value = constant.FirewallPortWhiteListValue
|
|
||||||
if err := settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, value); err != nil {
|
var ErrFirewallBackendCleanupRequired = errors.New("firewall backend cleanup required")
|
||||||
|
|
||||||
|
func firewallBackendCleanupRequired(current, target string) error {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%w: current backend %s still contains 1Panel runtime rules; clean it up before switching to %s",
|
||||||
|
ErrFirewallBackendCleanupRequired,
|
||||||
|
current,
|
||||||
|
target,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIFirewallSettingService() IFirewallSettingService {
|
||||||
|
return &FirewallSettingService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FirewallSettingService) CreatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistCreate) error {
|
||||||
|
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
|
||||||
|
return append(current, request.Rule), nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FirewallSettingService) UpdatePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistUpdate) error {
|
||||||
|
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
|
||||||
|
index, err := findPortWhitelistRule(current, request.OldRule)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
current[index] = request.Rule
|
||||||
return parseFirewallPortWhiteList(value)
|
return current, nil
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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 {
|
func (s *FirewallSettingService) DeletePortWhitelist(ctx context.Context, request dto.FirewallPortWhitelistDelete) error {
|
||||||
ports := make([]firewallPortWhitelist, 0, len(portWhiteList))
|
if request.Rule == nil {
|
||||||
exists := make(map[string]struct{})
|
return fmt.Errorf("select one firewall port whitelist rule to delete")
|
||||||
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
|
return savePortWhitelist(ctx, func(current []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
|
||||||
|
index, err := findPortWhitelistRule(current, *request.Rule)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return slices.Delete(current, index, index+1), nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncFirewallPortWhiteListAfterUpdate(oldValue string) error {
|
func findPortWhitelistRule(rules []firewall.PortWhitelist, target firewall.PortWhitelist) (int, error) {
|
||||||
client, err := firewall.NewFirewallClient()
|
index := slices.IndexFunc(rules, func(rule firewall.PortWhitelist) bool {
|
||||||
|
return samePortWhitelistRule(rule, target)
|
||||||
|
})
|
||||||
|
if index < 0 {
|
||||||
|
return -1, fmt.Errorf("firewall port whitelist rule has changed or no longer exists; refresh and retry")
|
||||||
|
}
|
||||||
|
return index, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func samePortWhitelistRule(left, right firewall.PortWhitelist) bool {
|
||||||
|
if reflect.DeepEqual(left, right) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
normalizedLeft, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{left})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false
|
||||||
}
|
}
|
||||||
if client.Name() == "iptables" {
|
normalizedRight, err := firewall.ValidatePortWhitelist([]firewall.PortWhitelist{right})
|
||||||
isInit, _ := iptables.LoadInitStatus("iptables", "base")
|
if err != nil {
|
||||||
if !isInit {
|
return false
|
||||||
return nil
|
}
|
||||||
}
|
slices.Sort(normalizedLeft[0].Sources)
|
||||||
oldPortWhiteList, err := parseFirewallPortWhiteList(oldValue)
|
slices.Sort(normalizedRight[0].Sources)
|
||||||
|
return reflect.DeepEqual(normalizedLeft[0], normalizedRight[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func savePortWhitelist(ctx context.Context, change func([]firewall.PortWhitelist) ([]firewall.PortWhitelist, error)) error {
|
||||||
|
firewallWhitelistMu.Lock()
|
||||||
|
defer firewallWhitelistMu.Unlock()
|
||||||
|
firewallRuleMutationMu.Lock()
|
||||||
|
defer firewallRuleMutationMu.Unlock()
|
||||||
|
defer filterruntime.InvalidateInventory()
|
||||||
|
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
current, err := loadPortWhitelistSetting(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return syncIptablesFirewallPortWhiteList(true, oldPortWhiteList)
|
desired, err := change(current)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
desired, err = firewall.ValidatePortWhitelist(desired)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
value, err := json.Marshal(desired)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Where("key = ?", constant.FirewallPortWhiteList).Assign(map[string]interface{}{"value": string(value)}).
|
||||||
|
FirstOrCreate(&model.Setting{Key: constant.FirewallPortWhiteList}).Error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkFirewallRuleWhitelistProtection(provider filter.Provider, record model.FirewallRule) error {
|
||||||
|
ports, err := loadFirewallPortWhiteList()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rules, err := record.RulesForProvider(provider)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, rule := range rules {
|
||||||
|
if filter.RuleMatchesPortWhitelist(rule, ports) {
|
||||||
|
return filter.ErrProtectedRule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadPortWhitelistSetting(db *gorm.DB) ([]firewall.PortWhitelist, error) {
|
||||||
|
var setting model.Setting
|
||||||
|
if err := db.Where("key = ?", constant.FirewallPortWhiteList).First(&setting).Error; errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
setting.Value = constant.FirewallPortWhiteListValue
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var rules []firewall.PortWhitelist
|
||||||
|
err := json.Unmarshal([]byte(setting.Value), &rules)
|
||||||
|
return rules, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSSHWhitelistPortFrom(path string) (string, error) {
|
||||||
|
directives, _, err := parseSSHConfigTree(path)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return defaultSSHPort, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return loadSSHPortValues(directives)[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func customWhitelist(entries []firewall.PortWhitelist) []firewall.PortWhitelist {
|
||||||
|
result := make([]firewall.PortWhitelist, 0, len(entries))
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.Type == "" {
|
||||||
|
result = append(result, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitializeFirewallWhitelistPorts(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
|
||||||
|
entries = slices.Clone(entries)
|
||||||
|
var sshPort string
|
||||||
|
for i := range entries {
|
||||||
|
entry := &entries[i]
|
||||||
|
if entry.Type == "" || entry.Port != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch entry.Type {
|
||||||
|
case firewall.PortWhitelistTypePanel:
|
||||||
|
entry.Port = LoadPanelPort()
|
||||||
|
case firewall.PortWhitelistTypeSSH:
|
||||||
|
if sshPort == "" {
|
||||||
|
var err error
|
||||||
|
sshPort, err = loadSSHWhitelistPortFrom(sshPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry.Port = sshPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return firewall.ValidatePortWhitelist(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSystemAccessPortWhitelist(ctx context.Context, serviceType string, ports []string) error {
|
||||||
|
return savePortWhitelist(ctx, func(entries []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
|
||||||
|
for i := range entries {
|
||||||
|
if entries[i].Type == serviceType {
|
||||||
|
if len(ports) == 0 {
|
||||||
|
return nil, fmt.Errorf("firewall whitelist %s requires a port", serviceType)
|
||||||
|
}
|
||||||
|
entries[i].Port = ports[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FirewallSettingService) Load(ctx context.Context) (dto.FirewallSettings, error) {
|
||||||
|
result := dto.FirewallSettings{PingStatus: firewall.LoadPingStatus()}
|
||||||
|
|
||||||
|
installed := make(map[string]bool)
|
||||||
|
for _, name := range lifecycle.InstalledProviders() {
|
||||||
|
installed[name] = true
|
||||||
|
}
|
||||||
|
result.System.Selected = configuredSystemFirewallBackend()
|
||||||
|
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.NewClientFor(name)
|
||||||
|
if err != nil {
|
||||||
|
option.Message = err.Error()
|
||||||
|
} else if supportsManagedFilterChains(name) {
|
||||||
|
option.Initialized, option.Bound, err = loadFirewallInitStatus(name, "base")
|
||||||
|
if err != nil {
|
||||||
|
option.Message = err.Error()
|
||||||
|
}
|
||||||
|
option.IPv4 = loadSystemFirewallFamilyInfo(name, constant.FirewallFamilyIPv4)
|
||||||
|
option.IPv6 = loadSystemFirewallFamilyInfo(name, constant.FirewallFamilyIPv6)
|
||||||
|
} 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
isActive, _ := client.Status()
|
result.Forwarding.Selected = configuredForwardingBackend()
|
||||||
if !isActive {
|
result.Forwarding.Current = result.Forwarding.Selected
|
||||||
|
for _, name := range []string{constant.FirewallProviderIptables, constant.FirewallProviderNftables} {
|
||||||
|
option := dto.FirewallBackendOption{Name: name, Installed: installed[name], Supported: true}
|
||||||
|
if option.Installed && name == result.Forwarding.Selected {
|
||||||
|
manager, err := newForwardingManagerFor(name)
|
||||||
|
if err != nil {
|
||||||
|
option.Message = err.Error()
|
||||||
|
} else if status, err := manager.Status(); err != nil {
|
||||||
|
option.Message = err.Error()
|
||||||
|
} else {
|
||||||
|
option.Initialized, option.Bound = status.IsInit, status.IsBind
|
||||||
|
ipv4Init, ipv4Bound, ipv4Err := manager.FamilyStatus(constant.FirewallFamilyIPv4)
|
||||||
|
ipv6Init, ipv6Bound, ipv6Err := manager.FamilyStatus(constant.FirewallFamilyIPv6)
|
||||||
|
option.IPv4 = dto.FirewallBackendFamilyStatus{
|
||||||
|
Available: ipv4Err == nil, Initialized: ipv4Init, Bound: ipv4Bound,
|
||||||
|
}
|
||||||
|
option.IPv6 = dto.FirewallBackendFamilyStatus{
|
||||||
|
Available: ipv6Err == nil, Initialized: ipv6Init, Bound: ipv6Bound,
|
||||||
|
}
|
||||||
|
if name == constant.FirewallProviderIptables {
|
||||||
|
if commands, commandErr := lifecycle.ResolveIptablesCommands(); commandErr == nil {
|
||||||
|
option.IPv6.Available = option.IPv6.Available && commands.IPv6Available()
|
||||||
|
if !commands.IPv6Available() {
|
||||||
|
option.IPv6.Reason = docker_guard.ReasonCommandMissing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if 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)
|
||||||
|
}
|
||||||
|
result.Docker.Selected = configuredDockerFirewallBackend()
|
||||||
|
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 := docker_guard.NewRuntime(name)
|
||||||
|
ipv4, ipv6 := guard.Status(docker_guard.FamilyIPv4), guard.Status(docker_guard.FamilyIPv6)
|
||||||
|
option.Initialized = ipv4.Initialized || ipv6.Initialized
|
||||||
|
option.Bound = ipv4.Bound || ipv6.Bound
|
||||||
|
option.IPv4.Initialized, option.IPv4.Bound = ipv4.Initialized, ipv4.Bound
|
||||||
|
option.IPv6.Initialized, option.IPv6.Bound = ipv6.Initialized, ipv6.Bound
|
||||||
|
option.IPv4.Available = ipv4.Reason != docker_guard.ReasonCommandMissing
|
||||||
|
option.IPv6.Available = ipv6.Reason != docker_guard.ReasonCommandMissing
|
||||||
|
option.IPv4.Reason, option.IPv6.Reason = ipv4.Reason, ipv6.Reason
|
||||||
|
}
|
||||||
|
result.Docker.Options = append(result.Docker.Options, option)
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
result.PortWhitelist, err = loadPortWhitelistSetting(global.DB.WithContext(ctx))
|
||||||
|
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 loadSystemFirewallFamilyStatus(provider, family string) (bool, bool, error) {
|
||||||
|
switch provider {
|
||||||
|
case constant.FirewallProviderIptables:
|
||||||
|
return iptables_helper.LoadFamilyInitStatus(family, "base")
|
||||||
|
case constant.FirewallProviderNftables:
|
||||||
|
return nftables_helper.LoadFamilyInitStatus(filter.Family(family), "base")
|
||||||
|
default:
|
||||||
|
return false, false, fmt.Errorf("unsupported firewall provider %q", provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSystemFirewallFamilyInfo(provider, family string) dto.FirewallBackendFamilyStatus {
|
||||||
|
if provider == constant.FirewallProviderIptables && family == constant.FirewallFamilyIPv6 {
|
||||||
|
commands, err := lifecycle.ResolveIptablesCommands()
|
||||||
|
if err != nil || !commands.IPv6Available() {
|
||||||
|
return dto.FirewallBackendFamilyStatus{Reason: docker_guard.ReasonCommandMissing}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initialized, bound, err := loadSystemFirewallFamilyStatus(provider, family)
|
||||||
|
return dto.FirewallBackendFamilyStatus{
|
||||||
|
Available: err == nil,
|
||||||
|
Initialized: initialized,
|
||||||
|
Bound: bound,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FirewallSettingService) Operate(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||||
|
if err := lockFirewallLifecycleIdle(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer firewallLifecycleTaskMu.Unlock()
|
||||||
|
if request.Subsystem != "system" && request.Backend != constant.FirewallProviderIptables && request.Backend != constant.FirewallProviderNftables {
|
||||||
|
return fmt.Errorf("%s only supports iptables or nftables", request.Subsystem)
|
||||||
|
}
|
||||||
|
if request.Subsystem == "system" && !supportsManagedFilterChains(request.Backend) && request.Operation != "select" {
|
||||||
|
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()
|
||||||
|
rulesErr := service.restoreStoredFirewallRules(ctx, filter.Provider(request.Backend), nil)
|
||||||
|
whitelistErr := service.SyncPortWhitelist(ctx)
|
||||||
|
return errors.Join(rulesErr, whitelistErr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "forwarding":
|
||||||
|
return s.operateForwarding(request)
|
||||||
|
case "docker":
|
||||||
|
return s.operateDocker(ctx, request)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported firewall subsystem %q", request.Subsystem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FirewallSettingService) operateDocker(ctx context.Context, request dto.FirewallBackendOperation) error {
|
||||||
|
guard := docker_guard.NewRuntime(request.Backend)
|
||||||
|
if request.Operation == "cleanup" {
|
||||||
|
if err := guard.Cleanup(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
|
||||||
|
}
|
||||||
|
previous, _ := settingRepo.GetValueByKey(constant.FirewallDockerBackendKey)
|
||||||
|
if request.Operation == "select" {
|
||||||
|
current := previous
|
||||||
|
if current == "" {
|
||||||
|
current = alternateDirectBackend(request.Backend)
|
||||||
|
}
|
||||||
|
initialized, err := dockerGuardBackendInitialized(current)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if current != request.Backend && initialized {
|
||||||
|
return firewallBackendCleanupRequired(current, request.Backend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, request.Backend); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if request.Operation == "select" {
|
||||||
|
if err := (&DockerService{}).UpdateFirewallBackend(request.Backend); err != nil {
|
||||||
|
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if request.Operation == "initialize" {
|
||||||
|
if err := newDockerPortGuardService().Operate(ctx, dto.DockerPortGuardOperation{Operation: "initialize"}); err != nil {
|
||||||
|
_ = settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, previous)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dockerGuardBackendInitialized(backend string) (bool, error) {
|
||||||
|
guard := docker_guard.NewRuntime(backend)
|
||||||
|
for _, family := range []string{docker_guard.FamilyIPv4, docker_guard.FamilyIPv6} {
|
||||||
|
initialized, err := guard.Initialized(family)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if initialized {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FirewallSettingService) operateSystem(request dto.FirewallBackendOperation) error {
|
||||||
|
firewallRuleMutationMu.Lock()
|
||||||
|
defer firewallRuleMutationMu.Unlock()
|
||||||
|
if _, err := lifecycle.NewClientFor(request.Backend); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if request.Operation == "cleanup" {
|
||||||
|
return cleanupSystemBackend(request.Backend)
|
||||||
|
}
|
||||||
|
previous, _ := settingRepo.GetValueByKey(constant.FirewallSystemBackendKey)
|
||||||
|
if previous == "" {
|
||||||
|
if client, err := lifecycle.NewClient(); err == nil {
|
||||||
|
previous = client.Name()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if request.Operation == "select" && previous != "" && previous != request.Backend {
|
||||||
|
initialized, err := systemFirewallBackendInitialized(previous)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if initialized {
|
||||||
|
return firewallBackendCleanupRequired(previous, request.Backend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
portWhiteList, err := loadFirewallPortWhiteList()
|
initErr := newFirewallService().operateFilterChainBaseLocked(request.Backend, dto.FilterChainOperation{
|
||||||
if err != nil {
|
Name: constant.FirewallBasicChain, Operate: string(firewall.BaseOperationInit),
|
||||||
return err
|
})
|
||||||
|
if initErr != nil {
|
||||||
|
return rollback(initErr)
|
||||||
}
|
}
|
||||||
oldPortWhiteList, err := parseFirewallPortWhiteList(oldValue)
|
return settingRepo.UpdateOrCreate(constant.FirewallFilterInitializedKey, constant.StatusEnable)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
requiredPorts, err := loadRequiredFirewallPortWhiteList()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
oldPortWhiteList = normalizeFirewallPortWhiteList(append(oldPortWhiteList, requiredPorts...))
|
|
||||||
return syncFirewallClientPortWhiteList(client, oldPortWhiteList, portWhiteList)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncFirewallClientPortWhiteList(client firewall.FirewallClient, oldPortWhiteList, portWhiteList []firewallPortWhitelist) error {
|
func systemFirewallBackendInitialized(backend string) (bool, error) {
|
||||||
oldPorts := firewallPortWhiteListMap(oldPortWhiteList)
|
return systemFirewallBackendInitializedWithClientFactory(backend, lifecycle.NewClientFor)
|
||||||
newPorts := firewallPortWhiteListMap(portWhiteList)
|
}
|
||||||
for _, item := range oldPortWhiteList {
|
|
||||||
key := firewallPortWhiteListKey(item)
|
func systemFirewallBackendInitializedWithClientFactory(
|
||||||
if _, ok := newPorts[key]; ok {
|
backend string,
|
||||||
continue
|
newClient func(string) (lifecycle.Client, error),
|
||||||
|
) (bool, error) {
|
||||||
|
client, err := 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 supportsManagedFilterChains(backend) {
|
||||||
|
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 cleanupSystemBackend(backend string) error {
|
||||||
|
switch backend {
|
||||||
|
case constant.FirewallProviderIptables:
|
||||||
|
return newIptablesHelperManager().Cleanup()
|
||||||
|
case constant.FirewallProviderNftables:
|
||||||
|
return newNftablesHelperManager().Cleanup()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("cleanup is only available for 1Panel-owned iptables and nftables resources")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupInactiveSystemBackend(backend string) error {
|
||||||
|
switch backend {
|
||||||
|
case constant.FirewallProviderIptables:
|
||||||
|
return (&iptables_helper.Manager{}).Cleanup()
|
||||||
|
case constant.FirewallProviderNftables:
|
||||||
|
return (&nftables_helper.Manager{}).Cleanup()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("cleanup is only available for 1Panel-owned iptables and nftables resources")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FirewallSettingService) operateForwarding(request dto.FirewallBackendOperation) error {
|
||||||
|
manager, err := newForwardingManagerFor(request.Backend)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if request.Operation == "cleanup" {
|
||||||
|
if err := manager.Cleanup(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusDisable); err != nil {
|
||||||
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 {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
recordForwardingSyncError(nil)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return client.Reload()
|
previous, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||||
|
if request.Operation == "select" {
|
||||||
|
current := previous
|
||||||
|
if current == "" {
|
||||||
|
detected, err := newForwardingManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
current = detected.Name()
|
||||||
|
}
|
||||||
|
initialized, err := forwardingBackendInitialized(current)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if current != request.Backend && initialized {
|
||||||
|
return firewallBackendCleanupRequired(current, request.Backend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, request.Backend); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if request.Operation == "initialize" {
|
||||||
|
return newForwardingService().Enable()
|
||||||
|
}
|
||||||
|
recordForwardingSyncError(nil)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func firewallPortWhiteListMap(portWhiteList []firewallPortWhitelist) map[string]struct{} {
|
func forwardingBackendInitialized(backend string) (bool, error) {
|
||||||
ports := make(map[string]struct{})
|
manager, err := newForwardingManagerFor(backend)
|
||||||
for _, item := range portWhiteList {
|
if err != nil {
|
||||||
ports[firewallPortWhiteListKey(item)] = struct{}{}
|
if errors.Is(err, lifecycle.ErrNotInstalled) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
}
|
}
|
||||||
return ports
|
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 firewallPortWhiteListKey(item firewallPortWhitelist) string {
|
func alternateDirectBackend(backend string) string {
|
||||||
return item.Port + "/" + item.Protocol
|
if backend == constant.FirewallProviderNftables {
|
||||||
|
return constant.FirewallProviderIptables
|
||||||
|
}
|
||||||
|
return constant.FirewallProviderNftables
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,577 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IForwardingService interface {
|
||||||
|
LoadBaseInfo() (dto.FirewallSubsystemStatus, error)
|
||||||
|
SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error)
|
||||||
|
OperateRules(dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error)
|
||||||
|
Enable() error
|
||||||
|
QueueInitialization(dto.FirewallInitializationTask) (dto.FilterChainOperationResponse, error)
|
||||||
|
Restore(context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardingService struct {
|
||||||
|
managerFactory func() (*forwarding.Manager, error)
|
||||||
|
rules repo.IForwardingRuleRepo
|
||||||
|
enabled func() (bool, error)
|
||||||
|
persistBackend func(string) error
|
||||||
|
markEnabled func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
var errForwardingBackendUnavailable = errors.New("no supported forwarding backend detected")
|
||||||
|
var forwardingMutationMu sync.Mutex
|
||||||
|
|
||||||
|
const (
|
||||||
|
forwardingSyncConverged = "converged"
|
||||||
|
forwardingSyncMissing = "missing"
|
||||||
|
forwardingSyncRuntimeOnly = "runtime_only"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
forwardingSyncStateMu sync.RWMutex
|
||||||
|
forwardingLastSyncErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewIForwardingService() IForwardingService {
|
||||||
|
return newForwardingService()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newForwardingService() *ForwardingService {
|
||||||
|
return &ForwardingService{
|
||||||
|
managerFactory: newForwardingManager,
|
||||||
|
rules: repo.NewIForwardingRuleRepo(),
|
||||||
|
enabled: forwardingPersistedEnabled,
|
||||||
|
markEnabled: func() error {
|
||||||
|
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
|
||||||
|
},
|
||||||
|
persistBackend: func(backend string) error {
|
||||||
|
return settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, backend)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) LoadBaseInfo() (dto.FirewallSubsystemStatus, error) {
|
||||||
|
selected := configuredForwardingBackend()
|
||||||
|
baseInfo := dto.FirewallSubsystemStatus{
|
||||||
|
Version: "-", Name: forwardingDisplayName(selected), Backend: selected, SyncError: lastForwardingSyncError(),
|
||||||
|
}
|
||||||
|
manager, err := s.managerFactory()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errForwardingBackendUnavailable) {
|
||||||
|
baseInfo.Reason = constant.FirewallBackendNotInstalled
|
||||||
|
return baseInfo, nil
|
||||||
|
}
|
||||||
|
return baseInfo, err
|
||||||
|
}
|
||||||
|
status, err := manager.Status()
|
||||||
|
if err != nil {
|
||||||
|
return baseInfo, err
|
||||||
|
}
|
||||||
|
baseInfo.IsExist = true
|
||||||
|
baseInfo.Name, baseInfo.Backend = forwardingDisplayName(status.Name), status.Name
|
||||||
|
baseInfo.Version = status.Version
|
||||||
|
baseInfo.PingStatus = firewall.LoadPingStatus()
|
||||||
|
baseInfo.IsInit, baseInfo.IsBind = status.IsInit, status.IsBind
|
||||||
|
baseInfo.IPv4 = loadForwardingFamilyInfo(manager, status.Name, constant.FirewallFamilyIPv4)
|
||||||
|
baseInfo.IPv6 = loadForwardingFamilyInfo(manager, status.Name, constant.FirewallFamilyIPv6)
|
||||||
|
return baseInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadForwardingFamilyInfo(manager *forwarding.Manager, backend, family string) dto.FirewallBackendFamilyStatus {
|
||||||
|
initialized, bound, err := manager.FamilyStatus(family)
|
||||||
|
available := err == nil
|
||||||
|
if backend == constant.FirewallProviderIptables && family == constant.FirewallFamilyIPv6 {
|
||||||
|
commands, commandErr := lifecycle.ResolveIptablesCommands()
|
||||||
|
available = available && commandErr == nil && commands.IPv6Available()
|
||||||
|
}
|
||||||
|
return dto.FirewallBackendFamilyStatus{Available: available, Initialized: initialized, Bound: bound}
|
||||||
|
}
|
||||||
|
|
||||||
|
func forwardingDisplayName(backend string) string {
|
||||||
|
switch backend {
|
||||||
|
case constant.FirewallProviderIptables, constant.FirewallProviderNftables:
|
||||||
|
return backend + "-forward"
|
||||||
|
default:
|
||||||
|
return backend
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) SearchRules(request dto.ForwardRuleSearch) (int64, []dto.ForwardRule, error) {
|
||||||
|
if request.Strategy != "" {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
stored, err := s.rules.List(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
manager, err := s.managerFactory()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
runtime, err := manager.List("", "")
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
inventory, err := mergeForwardingInventory(stored, runtime)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
keyword := strings.ToLower(strings.TrimSpace(request.Info))
|
||||||
|
filtered := inventory[:0]
|
||||||
|
for _, item := range inventory {
|
||||||
|
if keyword == "" || forwardingRuleMatchesKeyword(item, keyword) {
|
||||||
|
filtered = append(filtered, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inventory = filtered
|
||||||
|
total := len(inventory)
|
||||||
|
start, end := (request.Page-1)*request.PageSize, request.Page*request.PageSize
|
||||||
|
if request.All {
|
||||||
|
start, end = 0, total
|
||||||
|
}
|
||||||
|
if start > total {
|
||||||
|
return int64(total), make([]dto.ForwardRule, 0), nil
|
||||||
|
}
|
||||||
|
if end > total {
|
||||||
|
end = total
|
||||||
|
}
|
||||||
|
pageRules := inventory[start:end]
|
||||||
|
var items []dto.ForwardRule
|
||||||
|
if pageRules != nil {
|
||||||
|
items = make([]dto.ForwardRule, 0, len(pageRules))
|
||||||
|
}
|
||||||
|
for index, item := range pageRules {
|
||||||
|
items = append(items, dto.ForwardRule{
|
||||||
|
ID: item.ID,
|
||||||
|
Num: strconv.Itoa(start + index + 1),
|
||||||
|
Family: item.Rule.Family,
|
||||||
|
Protocol: item.Rule.Protocol,
|
||||||
|
Port: item.Rule.Port,
|
||||||
|
TargetIP: item.Rule.TargetIP,
|
||||||
|
TargetPort: item.Rule.TargetPort,
|
||||||
|
Interface: item.Rule.Interface,
|
||||||
|
IsDesired: item.IsDesired,
|
||||||
|
IsRuntime: item.IsRuntime,
|
||||||
|
SyncStatus: item.SyncStatus(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return int64(total), items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func forwardingRuleMatchesKeyword(item forwardingInventoryItem, keyword string) bool {
|
||||||
|
values := []string{
|
||||||
|
item.Rule.Family, item.Rule.Protocol, item.Rule.Port, item.Rule.TargetIP,
|
||||||
|
item.Rule.TargetPort, item.Rule.Interface, item.SyncStatus(),
|
||||||
|
}
|
||||||
|
for _, value := range values {
|
||||||
|
if strings.Contains(strings.ToLower(value), keyword) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) OperateRules(request dto.ForwardRuleOperate) (dto.FilterChainOperationResponse, error) {
|
||||||
|
labels := make([]string, len(request.Rules))
|
||||||
|
operation := task.TaskCreate
|
||||||
|
for i, rule := range request.Rules {
|
||||||
|
labels[i] = fmt.Sprintf("[%d/%d] %s %s %s %s -> %s:%s", i+1, len(request.Rules), rule.Operation, rule.Family, rule.Protocol, rule.Port, rule.TargetIP, rule.TargetPort)
|
||||||
|
if rule.Operation != "add" {
|
||||||
|
operation = task.TaskUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if forwardingOperationsOnlyRemove(request.Rules) {
|
||||||
|
operation = task.TaskDelete
|
||||||
|
}
|
||||||
|
return queueFirewallRuleTask(firewallTaskForwarding, operation, labels, func(ctx context.Context) error {
|
||||||
|
return s.operateRules(ctx, request)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) operateRules(ctx context.Context, request dto.ForwardRuleOperate) error {
|
||||||
|
forwardingMutationMu.Lock()
|
||||||
|
defer forwardingMutationMu.Unlock()
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stored, err := s.rules.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
desired, err := applyForwardingOperations(forwardingRulesFromModels(stored), request.Rules)
|
||||||
|
if errors.Is(err, forwarding.ErrRuleExists) {
|
||||||
|
return buserr.New("ErrRecordExist")
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.rules.ReplaceAll(ctx, forwardingRuleModels(desired)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.reconcile(desired); err != nil {
|
||||||
|
recordForwardingSyncError(err)
|
||||||
|
if request.ForceDelete && forwardingOperationsOnlyRemove(request.Rules) {
|
||||||
|
if global.LOG != nil {
|
||||||
|
global.LOG.Error(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
recordForwardingSyncError(nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) Enable() error {
|
||||||
|
forwardingMutationMu.Lock()
|
||||||
|
defer forwardingMutationMu.Unlock()
|
||||||
|
manager, err := s.managerFactory()
|
||||||
|
if err != nil {
|
||||||
|
recordForwardingSyncError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.persistForwardingEnabled(); err != nil {
|
||||||
|
recordForwardingSyncError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.activateManager(manager); err != nil {
|
||||||
|
recordForwardingSyncError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rules, err := s.rules.List(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
recordForwardingSyncError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = manager.Reconcile(forwardingRulesFromModels(rules))
|
||||||
|
recordForwardingSyncError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) QueueInitialization(
|
||||||
|
request dto.FirewallInitializationTask,
|
||||||
|
) (dto.FilterChainOperationResponse, error) {
|
||||||
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
||||||
|
return dto.FilterChainOperationResponse{}, err
|
||||||
|
}
|
||||||
|
taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskForwarding, ""), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0)
|
||||||
|
if err != nil {
|
||||||
|
return dto.FilterChainOperationResponse{}, fmt.Errorf("create forwarding initialization task: %w", err)
|
||||||
|
}
|
||||||
|
var manager *forwarding.Manager
|
||||||
|
var backend string
|
||||||
|
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallEnableForwardingStep"), func(t *task.Task) error {
|
||||||
|
forwardingMutationMu.Lock()
|
||||||
|
defer forwardingMutationMu.Unlock()
|
||||||
|
var err error
|
||||||
|
manager, err = s.managerFactory()
|
||||||
|
if err != nil {
|
||||||
|
recordForwardingSyncError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
backend = manager.Name()
|
||||||
|
t.Logf("backend=%s", backend)
|
||||||
|
if err := s.persistForwardingEnabled(); err != nil {
|
||||||
|
recordForwardingSyncError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.activateManager(manager); err != nil {
|
||||||
|
recordForwardingSyncError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, nil)
|
||||||
|
taskItem.AddSubTask(i18n.GetMsgByKey("FirewallRestoreForwardingRulesStep"), func(t *task.Task) error {
|
||||||
|
forwardingMutationMu.Lock()
|
||||||
|
defer forwardingMutationMu.Unlock()
|
||||||
|
rules, err := s.rules.List(t.TaskCtx)
|
||||||
|
if err != nil {
|
||||||
|
recordForwardingSyncError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = manager.Reconcile(forwardingRulesFromModels(rules))
|
||||||
|
recordForwardingSyncError(err)
|
||||||
|
return err
|
||||||
|
}, nil)
|
||||||
|
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
|
||||||
|
return dto.FilterChainOperationResponse{}, fmt.Errorf("save forwarding initialization task: %w", err)
|
||||||
|
}
|
||||||
|
go func() { _ = taskItem.Execute() }()
|
||||||
|
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) Restore(ctx context.Context) error {
|
||||||
|
forwardingMutationMu.Lock()
|
||||||
|
defer forwardingMutationMu.Unlock()
|
||||||
|
enabled, err := s.forwardingEnabled()
|
||||||
|
if err != nil || !enabled {
|
||||||
|
if err != nil {
|
||||||
|
recordForwardingSyncError(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
manager, err := s.managerFactory()
|
||||||
|
if err != nil {
|
||||||
|
recordForwardingSyncError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stored, err := s.rules.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
recordForwardingSyncError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.activateManager(manager); err != nil {
|
||||||
|
recordForwardingSyncError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = manager.Reconcile(forwardingRulesFromModels(stored))
|
||||||
|
recordForwardingSyncError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) reconcile(rules []forwarding.Rule) error {
|
||||||
|
manager, err := s.managerFactory()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.reconcileWithManager(manager, rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) reconcileWithManager(manager *forwarding.Manager, rules []forwarding.Rule) error {
|
||||||
|
enabled, err := s.forwardingEnabled()
|
||||||
|
if err != nil || !enabled {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.activateManager(manager); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return manager.Reconcile(rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) activateManager(manager *forwarding.Manager) error {
|
||||||
|
if err := s.saveForwardingBackend(manager.Name()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return manager.Enable()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) forwardingEnabled() (bool, error) {
|
||||||
|
if s.enabled != nil {
|
||||||
|
return s.enabled()
|
||||||
|
}
|
||||||
|
return forwardingPersistedEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) saveForwardingBackend(backend string) error {
|
||||||
|
if s.persistBackend != nil {
|
||||||
|
return s.persistBackend(backend)
|
||||||
|
}
|
||||||
|
return settingRepo.UpdateOrCreate(constant.FirewallForwardingBackendKey, backend)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) persistForwardingEnabled() error {
|
||||||
|
if s.markEnabled != nil {
|
||||||
|
return s.markEnabled()
|
||||||
|
}
|
||||||
|
return settingRepo.UpdateOrCreate(constant.FirewallForwardingInitializedKey, constant.StatusEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
func forwardingPersistedEnabled() (bool, error) {
|
||||||
|
status, err := settingRepo.GetValueByKey(constant.FirewallForwardingInitializedKey)
|
||||||
|
return status == constant.StatusEnable, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func forwardingRulesFromModels(stored []model.ForwardingRule) []forwarding.Rule {
|
||||||
|
rules := make([]forwarding.Rule, 0, len(stored))
|
||||||
|
for _, rule := range stored {
|
||||||
|
rules = append(rules, forwarding.Rule{
|
||||||
|
Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP,
|
||||||
|
TargetPort: rule.TargetPort, Interface: rule.Interface,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
|
||||||
|
func forwardingRuleModels(rules []forwarding.Rule) []model.ForwardingRule {
|
||||||
|
stored := make([]model.ForwardingRule, 0, len(rules))
|
||||||
|
for _, rule := range rules {
|
||||||
|
stored = append(stored, model.ForwardingRule{
|
||||||
|
Family: rule.Family, Protocol: rule.Protocol, Port: rule.Port, TargetIP: rule.TargetIP,
|
||||||
|
TargetPort: rule.TargetPort, Interface: rule.Interface,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return stored
|
||||||
|
}
|
||||||
|
|
||||||
|
type forwardingInventoryItem struct {
|
||||||
|
ID uint
|
||||||
|
Rule forwarding.Rule
|
||||||
|
IsDesired bool
|
||||||
|
IsRuntime bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i forwardingInventoryItem) SyncStatus() string {
|
||||||
|
switch {
|
||||||
|
case i.IsDesired && i.IsRuntime:
|
||||||
|
return forwardingSyncConverged
|
||||||
|
case i.IsDesired:
|
||||||
|
return forwardingSyncMissing
|
||||||
|
default:
|
||||||
|
return forwardingSyncRuntimeOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeForwardingInventory(
|
||||||
|
stored []model.ForwardingRule,
|
||||||
|
runtime []forwarding.Rule,
|
||||||
|
) ([]forwardingInventoryItem, error) {
|
||||||
|
items := make([]forwardingInventoryItem, 0, len(stored)+len(runtime))
|
||||||
|
byIdentity := make(map[string]int, len(stored)+len(runtime))
|
||||||
|
for _, record := range stored {
|
||||||
|
rule, err := forwarding.NormalizeRule(forwarding.Rule{
|
||||||
|
Family: record.Family, Protocol: record.Protocol, Port: record.Port, TargetIP: record.TargetIP,
|
||||||
|
TargetPort: record.TargetPort, Interface: record.Interface,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("normalize desired forwarding rule: %w", err)
|
||||||
|
}
|
||||||
|
key := rule.Identity()
|
||||||
|
byIdentity[key] = len(items)
|
||||||
|
items = append(items, forwardingInventoryItem{ID: record.ID, Rule: rule, IsDesired: true})
|
||||||
|
}
|
||||||
|
for _, observed := range runtime {
|
||||||
|
rule, err := forwarding.NormalizeRule(observed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("normalize runtime forwarding rule: %w", err)
|
||||||
|
}
|
||||||
|
key := rule.Identity()
|
||||||
|
if index, exists := byIdentity[key]; exists {
|
||||||
|
items[index].IsRuntime = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byIdentity[key] = len(items)
|
||||||
|
items = append(items, forwardingInventoryItem{Rule: rule, IsRuntime: true})
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordForwardingSyncError(err error) {
|
||||||
|
forwardingSyncStateMu.Lock()
|
||||||
|
forwardingLastSyncErr = err
|
||||||
|
forwardingSyncStateMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func lastForwardingSyncError() string {
|
||||||
|
forwardingSyncStateMu.RLock()
|
||||||
|
defer forwardingSyncStateMu.RUnlock()
|
||||||
|
if forwardingLastSyncErr == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return forwardingLastSyncErr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyForwardingOperations(current []forwarding.Rule, requested []dto.ForwardRuleOperation) ([]forwarding.Rule, error) {
|
||||||
|
desired := make([]forwarding.Rule, 0, len(current)+len(requested))
|
||||||
|
for _, rule := range current {
|
||||||
|
normalized, err := forwarding.NormalizeRule(rule)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("normalize persisted forwarding rule: %w", err)
|
||||||
|
}
|
||||||
|
desired = append(desired, normalized)
|
||||||
|
}
|
||||||
|
for _, operation := range requested {
|
||||||
|
for _, protocol := range strings.Split(operation.Protocol, "/") {
|
||||||
|
rule, err := forwarding.NormalizeRule(forwarding.Rule{
|
||||||
|
Family: operation.Family, Protocol: protocol, Port: operation.Port, TargetIP: operation.TargetIP,
|
||||||
|
TargetPort: operation.TargetPort, Interface: operation.Interface,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
index := forwardingRuleIndex(desired, rule)
|
||||||
|
switch forwarding.OperationType(operation.Operation) {
|
||||||
|
case forwarding.OperationAdd:
|
||||||
|
if index >= 0 {
|
||||||
|
return nil, forwarding.ErrRuleExists
|
||||||
|
}
|
||||||
|
desired = append(desired, rule)
|
||||||
|
case forwarding.OperationRemove:
|
||||||
|
if index >= 0 {
|
||||||
|
desired = append(desired[:index], desired[index+1:]...)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported forwarding operation %q", operation.Operation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return desired, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func forwardingRuleIndex(rules []forwarding.Rule, wanted forwarding.Rule) int {
|
||||||
|
wantedIdentity := wanted.Identity()
|
||||||
|
for index, rule := range rules {
|
||||||
|
if rule.Identity() == wantedIdentity {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func forwardingOperationsOnlyRemove(operations []dto.ForwardRuleOperation) bool {
|
||||||
|
if len(operations) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, operation := range operations {
|
||||||
|
if operation.Operation != string(forwarding.OperationRemove) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func newForwardingManager() (*forwarding.Manager, error) {
|
||||||
|
return newForwardingManagerFor(configuredForwardingBackend())
|
||||||
|
}
|
||||||
|
|
||||||
|
func configuredForwardingBackend() string {
|
||||||
|
selected, _ := settingRepo.GetValueByKey(constant.FirewallForwardingBackendKey)
|
||||||
|
selected = strings.TrimSpace(selected)
|
||||||
|
if selected == "" {
|
||||||
|
return constant.FirewallProviderIptables
|
||||||
|
}
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
|
||||||
|
func newForwardingManagerFor(backend string) (*forwarding.Manager, error) {
|
||||||
|
client, err := lifecycle.NewClientFor(backend)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%w: selected forwarding backend %s: %w",
|
||||||
|
errForwardingBackendUnavailable, backend, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
adapter, err := forwarding.New(client.Name())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return forwarding.NewManager(adapter, client), nil
|
||||||
|
}
|
||||||
+61
-21
@@ -1,6 +1,8 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ type IFtpService interface {
|
|||||||
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
|
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
|
||||||
Operate(operation string) error
|
Operate(operation string) error
|
||||||
Create(req dto.FtpCreate) (uint, error)
|
Create(req dto.FtpCreate) (uint, error)
|
||||||
|
CreateWebsite(req dto.FtpCreate) (uint, error)
|
||||||
Delete(req dto.BatchDeleteReq) error
|
Delete(req dto.BatchDeleteReq) error
|
||||||
Update(req dto.FtpUpdate) error
|
Update(req dto.FtpUpdate) error
|
||||||
Sync() error
|
Sync() error
|
||||||
@@ -34,11 +37,7 @@ func NewIFtpService() IFtpService {
|
|||||||
|
|
||||||
func (f *FtpService) LoadBaseInfo() (dto.FtpBaseInfo, error) {
|
func (f *FtpService) LoadBaseInfo() (dto.FtpBaseInfo, error) {
|
||||||
var baseInfo dto.FtpBaseInfo
|
var baseInfo dto.FtpBaseInfo
|
||||||
client, err := toolbox.NewFtpClient()
|
baseInfo.IsActive, baseInfo.IsExist = toolbox.FtpStatus()
|
||||||
if err != nil {
|
|
||||||
return baseInfo, err
|
|
||||||
}
|
|
||||||
baseInfo.IsActive, baseInfo.IsExist = client.Status()
|
|
||||||
return baseInfo, nil
|
return baseInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +98,7 @@ func (f *FtpService) Sync() error {
|
|||||||
}
|
}
|
||||||
lists, err := client.LoadList()
|
lists, err := client.LoadList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
listsInDB, err := ftpRepo.GetList()
|
listsInDB, err := ftpRepo.GetList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -113,13 +112,24 @@ func (f *FtpService) Sync() error {
|
|||||||
for _, item := range lists {
|
for _, item := range lists {
|
||||||
if itemInDB, ok := currentData[item.User]; ok {
|
if itemInDB, ok := currentData[item.User]; ok {
|
||||||
sameData[item.User] = struct{}{}
|
sameData[item.User] = struct{}{}
|
||||||
if item.Path != itemInDB.Path || item.Status != itemInDB.Status {
|
if item.Path != itemInDB.Path || item.Status != itemInDB.Status || item.UID != itemInDB.UID || item.GID != itemInDB.GID {
|
||||||
if err := ftpRepo.Update(itemInDB.ID, map[string]interface{}{"path": item.Path, "status": item.Status}); err != nil {
|
if err := ftpRepo.Update(itemInDB.ID, map[string]interface{}{
|
||||||
|
"path": item.Path,
|
||||||
|
"status": item.Status,
|
||||||
|
"uid": item.UID,
|
||||||
|
"gid": item.GID,
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := ftpRepo.Create(&model.Ftp{User: item.User, Path: item.Path, Status: item.Status}); err != nil {
|
if err := ftpRepo.Create(&model.Ftp{
|
||||||
|
User: item.User,
|
||||||
|
Path: item.Path,
|
||||||
|
Status: item.Status,
|
||||||
|
UID: item.UID,
|
||||||
|
GID: item.GID,
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,6 +143,21 @@ func (f *FtpService) Sync() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FtpService) Create(req dto.FtpCreate) (uint, error) {
|
func (f *FtpService) Create(req dto.FtpCreate) (uint, error) {
|
||||||
|
return f.create(req, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FtpService) CreateWebsite(req dto.FtpCreate) (uint, error) {
|
||||||
|
return f.create(req, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FtpService) create(req dto.FtpCreate, website bool) (uint, error) {
|
||||||
|
if err := toolbox.ValidateFtpRootPath(req.Path); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
client, err := toolbox.NewFtpClient()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
if _, err := os.Stat(req.Path); err != nil {
|
if _, err := os.Stat(req.Path); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(req.Path, os.ModePerm); err != nil {
|
if err := os.MkdirAll(req.Path, os.ModePerm); err != nil {
|
||||||
@@ -150,20 +175,28 @@ func (f *FtpService) Create(req dto.FtpCreate) (uint, error) {
|
|||||||
if userInDB.ID != 0 {
|
if userInDB.ID != 0 {
|
||||||
return 0, buserr.New("ErrRecordExist")
|
return 0, buserr.New("ErrRecordExist")
|
||||||
}
|
}
|
||||||
client, err := toolbox.NewFtpClient()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if err := client.UserAdd(req.User, req.Password, req.Path); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
var ftp model.Ftp
|
var ftp model.Ftp
|
||||||
if err := copier.Copy(&ftp, &req); err != nil {
|
if err := copier.Copy(&ftp, &req); err != nil {
|
||||||
return 0, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
return 0, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
}
|
}
|
||||||
|
uid, gid := uint(constant.WebsiteUID), uint(constant.WebsiteGID)
|
||||||
|
if !website {
|
||||||
|
uid, gid, err = toolbox.EnsureStandaloneFtpIdentity()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := client.UserAdd(req.User, req.Password, req.Path, uid, gid); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
ftp.Status = constant.StatusEnable
|
ftp.Status = constant.StatusEnable
|
||||||
ftp.Password = pass
|
ftp.Password = pass
|
||||||
|
ftp.UID = uid
|
||||||
|
ftp.GID = gid
|
||||||
if err := ftpRepo.Create(&ftp); err != nil {
|
if err := ftpRepo.Create(&ftp); err != nil {
|
||||||
|
if rollbackErr := client.UserDel(req.User); rollbackErr != nil {
|
||||||
|
return 0, errors.Join(err, fmt.Errorf("rollback FTP user %s failed: %w", req.User, rollbackErr))
|
||||||
|
}
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return ftp.ID, nil
|
return ftp.ID, nil
|
||||||
@@ -186,6 +219,13 @@ func (f *FtpService) Delete(req dto.BatchDeleteReq) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FtpService) Update(req dto.FtpUpdate) error {
|
func (f *FtpService) Update(req dto.FtpUpdate) error {
|
||||||
|
if err := toolbox.ValidateFtpRootPath(req.Path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client, err := toolbox.NewFtpClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if _, err := os.Stat(req.Path); err != nil {
|
if _, err := os.Stat(req.Path); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(req.Path, os.ModePerm); err != nil {
|
if err := os.MkdirAll(req.Path, os.ModePerm); err != nil {
|
||||||
@@ -209,10 +249,6 @@ func (f *FtpService) Update(req dto.FtpUpdate) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := toolbox.NewFtpClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
needReload := false
|
needReload := false
|
||||||
updates := make(map[string]interface{})
|
updates := make(map[string]interface{})
|
||||||
if req.Password != passItem {
|
if req.Password != passItem {
|
||||||
@@ -230,7 +266,11 @@ func (f *FtpService) Update(req dto.FtpUpdate) error {
|
|||||||
needReload = true
|
needReload = true
|
||||||
}
|
}
|
||||||
if req.Path != ftpItem.Path {
|
if req.Path != ftpItem.Path {
|
||||||
if err := client.SetPath(ftpItem.User, req.Path); err != nil {
|
uid, gid := ftpItem.UID, ftpItem.GID
|
||||||
|
if uid == 0 || gid == 0 {
|
||||||
|
uid, gid = uint(constant.WebsiteUID), uint(constant.WebsiteGID)
|
||||||
|
}
|
||||||
|
if err := client.SetPath(ftpItem.User, req.Path, uid, gid); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
updates["path"] = req.Path
|
updates["path"] = req.Path
|
||||||
|
|||||||
+23
-71
@@ -29,7 +29,6 @@ import (
|
|||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
"github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
"github.com/docker/docker/pkg/archive"
|
"github.com/docker/docker/pkg/archive"
|
||||||
"github.com/docker/docker/pkg/homedir"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ImageService struct{}
|
type ImageService struct{}
|
||||||
@@ -278,38 +277,37 @@ func (u *ImageService) ImagePull(req dto.ImagePull) error {
|
|||||||
itemName := strings.ReplaceAll(path.Base(item), ":", "_")
|
itemName := strings.ReplaceAll(path.Base(item), ":", "_")
|
||||||
taskItem.AddSubTask(i18n.GetWithName("ImagePull", itemName), func(t *task.Task) error {
|
taskItem.AddSubTask(i18n.GetWithName("ImagePull", itemName), func(t *task.Task) error {
|
||||||
taskItem.Logf("----------------- %s -----------------", itemName)
|
taskItem.Logf("----------------- %s -----------------", itemName)
|
||||||
|
if req.RepoID == 0 {
|
||||||
|
pullErr := pullImages(taskItem, client, item)
|
||||||
|
taskItem.LogWithStatus(i18n.GetMsgByKey("TaskPull"), pullErr)
|
||||||
|
return pullErr
|
||||||
|
}
|
||||||
|
|
||||||
options := image.PullOptions{}
|
options := image.PullOptions{}
|
||||||
imageName := item
|
imageName := item
|
||||||
if req.RepoID == 0 {
|
repo, repoErr := imageRepoRepo.Get(repo.WithByID(req.RepoID))
|
||||||
hasAuth, authStr := loadAuthInfo(item)
|
taskItem.LogWithStatus(i18n.GetMsgByKey("ImageRepoAuthFromDB"), repoErr)
|
||||||
if hasAuth {
|
if repoErr != nil {
|
||||||
options.RegistryAuth = authStr
|
return repoErr
|
||||||
|
}
|
||||||
|
if repo.Auth {
|
||||||
|
authConfig := registry.AuthConfig{
|
||||||
|
Username: repo.Username,
|
||||||
|
Password: repo.Password,
|
||||||
}
|
}
|
||||||
} else {
|
encodedJSON, err := json.Marshal(authConfig)
|
||||||
repo, err := imageRepoRepo.Get(repo.WithByID(req.RepoID))
|
|
||||||
taskItem.LogWithStatus(i18n.GetMsgByKey("ImageRepoAuthFromDB"), err)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if repo.Auth {
|
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
|
||||||
authConfig := registry.AuthConfig{
|
options.RegistryAuth = authStr
|
||||||
Username: repo.Username,
|
|
||||||
Password: repo.Password,
|
|
||||||
}
|
|
||||||
encodedJSON, err := json.Marshal(authConfig)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
|
|
||||||
options.RegistryAuth = authStr
|
|
||||||
}
|
|
||||||
imageName = repo.DownloadUrl + "/" + item
|
|
||||||
}
|
}
|
||||||
|
imageName = repo.DownloadUrl + "/" + item
|
||||||
dockerCli := docker.NewClientWithExist(client)
|
dockerCli := docker.NewClientWithExist(client)
|
||||||
err = dockerCli.PullImageWithProcessAndOptions(taskItem, imageName, options)
|
pullErr := dockerCli.PullImageWithProcessAndOptions(taskItem, imageName, options)
|
||||||
taskItem.LogWithStatus(i18n.GetMsgByKey("TaskPull"), err)
|
taskItem.LogWithStatus(i18n.GetMsgByKey("TaskPull"), pullErr)
|
||||||
if err != nil {
|
if pullErr != nil {
|
||||||
return err
|
return pullErr
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}, nil)
|
}, nil)
|
||||||
@@ -547,49 +545,3 @@ func checkUsed(imageID string, containers []container.Summary) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadAuthInfo(image string) (bool, string) {
|
|
||||||
if !strings.Contains(image, "/") {
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
homeDir := homedir.Get()
|
|
||||||
confPath := path.Join(homeDir, ".docker/config.json")
|
|
||||||
configFileBytes, err := os.ReadFile(confPath)
|
|
||||||
if err != nil {
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
var config dockerConfig
|
|
||||||
if err = json.Unmarshal(configFileBytes, &config); err != nil {
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
user string
|
|
||||||
passwd string
|
|
||||||
)
|
|
||||||
imagePrefix := strings.Split(image, "/")[0]
|
|
||||||
if val, ok := config.Auths[imagePrefix]; ok {
|
|
||||||
itemByte, _ := base64.StdEncoding.DecodeString(val.Auth)
|
|
||||||
itemStr := string(itemByte)
|
|
||||||
if strings.Contains(itemStr, ":") {
|
|
||||||
user = strings.Split(itemStr, ":")[0]
|
|
||||||
passwd = strings.Split(itemStr, ":")[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
authConfig := registry.AuthConfig{
|
|
||||||
Username: user,
|
|
||||||
Password: passwd,
|
|
||||||
}
|
|
||||||
encodedJSON, err := json.Marshal(authConfig)
|
|
||||||
if err != nil {
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
|
|
||||||
return true, authStr
|
|
||||||
}
|
|
||||||
|
|
||||||
type dockerConfig struct {
|
|
||||||
Auths map[string]authConfig `json:"auths"`
|
|
||||||
}
|
|
||||||
type authConfig struct {
|
|
||||||
Auth string `json:"auth"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,513 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
||||||
"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/client"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client/iptables"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IIptablesService interface {
|
|
||||||
Search(req dto.SearchPageWithType) (int64, interface{}, error)
|
|
||||||
OperateRule(req dto.IptablesRuleOp, withSave bool) error
|
|
||||||
BatchOperate(req dto.IptablesBatchOperate) error
|
|
||||||
LoadChainStatus(req dto.OperationWithName) dto.IptablesChainStatus
|
|
||||||
|
|
||||||
Operate(req dto.IptablesOp) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type IptablesService struct{}
|
|
||||||
|
|
||||||
func NewIIptablesService() IIptablesService {
|
|
||||||
return &IptablesService{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IptablesService) Search(req dto.SearchPageWithType) (int64, interface{}, error) {
|
|
||||||
rules, err := iptables.ReadFilterRulesByChain(req.Type)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, fmt.Errorf("failed to read iptables rules: %w", err)
|
|
||||||
}
|
|
||||||
var records []iptables.FilterRules
|
|
||||||
total, start, end := len(rules), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
|
||||||
if start > total {
|
|
||||||
records = make([]iptables.FilterRules, 0)
|
|
||||||
} else {
|
|
||||||
if end >= total {
|
|
||||||
end = total
|
|
||||||
}
|
|
||||||
records = rules[start:end]
|
|
||||||
}
|
|
||||||
|
|
||||||
rulesInDB, _ := hostRepo.ListFirewallRecord(hostRepo.WithByChain(req.Type))
|
|
||||||
|
|
||||||
for i := 0; i < len(records); i++ {
|
|
||||||
for _, item := range rulesInDB {
|
|
||||||
if records[i].Strategy == item.Strategy &&
|
|
||||||
records[i].DstIP == item.DstIP &&
|
|
||||||
fmt.Sprintf("%v", records[i].DstPort) == item.DstPort &&
|
|
||||||
records[i].Protocol == item.Protocol &&
|
|
||||||
records[i].SrcIP == item.SrcIP &&
|
|
||||||
fmt.Sprintf("%v", records[i].SrcPort) == item.SrcPort {
|
|
||||||
records[i].ID = item.ID
|
|
||||||
records[i].Description = item.Description
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return int64(total), records, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IptablesService) OperateRule(req dto.IptablesRuleOp, withSave bool) error {
|
|
||||||
action := "ACCEPT"
|
|
||||||
if req.Strategy == "drop" {
|
|
||||||
action = "DROP"
|
|
||||||
}
|
|
||||||
policy := iptables.FilterRules{
|
|
||||||
Protocol: req.Protocol,
|
|
||||||
SrcIP: req.SrcIP,
|
|
||||||
DstIP: req.DstIP,
|
|
||||||
Strategy: action,
|
|
||||||
}
|
|
||||||
if req.SrcPort != 0 {
|
|
||||||
policy.SrcPort = fmt.Sprintf("%v", req.SrcPort)
|
|
||||||
}
|
|
||||||
if req.DstPort != 0 {
|
|
||||||
policy.DstPort = fmt.Sprintf("%v", req.DstPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
name := iptables.InputFileName
|
|
||||||
if req.Chain == iptables.Chain1PanelOutput {
|
|
||||||
name = iptables.OutputFileName
|
|
||||||
}
|
|
||||||
switch req.Operation {
|
|
||||||
case "add":
|
|
||||||
if err := s.validateRuleInput(&req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := iptables.AddFilterRule(req.Chain, policy); err != nil {
|
|
||||||
return fmt.Errorf("failed to add iptables rule: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(req.Description) != 0 {
|
|
||||||
rule := &model.Firewall{
|
|
||||||
Chain: req.Chain,
|
|
||||||
Protocol: req.Protocol,
|
|
||||||
SrcIP: req.SrcIP,
|
|
||||||
SrcPort: policy.SrcPort,
|
|
||||||
DstIP: req.DstIP,
|
|
||||||
DstPort: policy.DstPort,
|
|
||||||
Strategy: req.Strategy,
|
|
||||||
Description: req.Description,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := hostRepo.SaveFirewallRecord(rule); err != nil {
|
|
||||||
return fmt.Errorf("failed to save rule to database: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "remove":
|
|
||||||
if err := iptables.DeleteFilterRule(req.Chain, policy); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove iptables rule: %w", err)
|
|
||||||
}
|
|
||||||
if req.ID != 0 {
|
|
||||||
if err := hostRepo.DeleteFirewallRecordByID(req.ID); err != nil {
|
|
||||||
return fmt.Errorf("failed to delete rule from database: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !withSave {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := iptables.SaveRulesToFile(iptables.FilterTab, req.Chain, name); err != nil {
|
|
||||||
global.LOG.Errorf("persistence for %s failed, err: %v", iptables.Chain1PanelBasic, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IptablesService) BatchOperate(req dto.IptablesBatchOperate) error {
|
|
||||||
if len(req.Rules) == 0 {
|
|
||||||
return errors.New("no rules to operate")
|
|
||||||
}
|
|
||||||
for _, rule := range req.Rules {
|
|
||||||
if err := s.OperateRule(rule, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chain := iptables.Chain1PanelInput
|
|
||||||
fileName := iptables.InputFileName
|
|
||||||
if req.Rules[0].Chain == iptables.Chain1PanelOutput {
|
|
||||||
chain = iptables.Chain1PanelOutput
|
|
||||||
fileName = iptables.OutputFileName
|
|
||||||
}
|
|
||||||
if err := iptables.SaveRulesToFile(iptables.FilterTab, chain, fileName); err != nil {
|
|
||||||
global.LOG.Errorf("persistence for %s failed, err: %v", iptables.Chain1PanelBasic, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IptablesService) Operate(req dto.IptablesOp) error {
|
|
||||||
targetChain := iptables.ChainInput
|
|
||||||
if req.Name == iptables.Chain1PanelOutput {
|
|
||||||
targetChain = iptables.ChainOutput
|
|
||||||
}
|
|
||||||
switch req.Operate {
|
|
||||||
case "init-base":
|
|
||||||
if ok := cmd.Which("iptables"); !ok {
|
|
||||||
return fmt.Errorf("failed to find iptables")
|
|
||||||
}
|
|
||||||
if err := iptables.AddChain(iptables.FilterTab, iptables.Chain1PanelBasicBefore); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.AddChain(iptables.FilterTab, iptables.Chain1PanelBasic); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.AddChain(iptables.FilterTab, iptables.Chain1PanelBasicAfter); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := initPreRules(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasicBefore, 1); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasic, 2); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasicAfter, 3); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.SaveRulesToFile(iptables.FilterTab, iptables.Chain1PanelBasicBefore, iptables.BasicBeforeFileName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.SaveRulesToFile(iptables.FilterTab, iptables.Chain1PanelBasic, iptables.BasicFileName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.SaveRulesToFile(iptables.FilterTab, iptables.Chain1PanelBasicAfter, iptables.BasicAfterFileName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = settingRepo.Update("IptablesStatus", constant.StatusEnable)
|
|
||||||
return nil
|
|
||||||
case "init-forward":
|
|
||||||
if err := client.EnableIptablesForward(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = settingRepo.Update("IptablesForwardStatus", constant.StatusEnable)
|
|
||||||
return nil
|
|
||||||
case "init-advance":
|
|
||||||
if err := iptables.AddChain(iptables.FilterTab, iptables.Chain1PanelInput); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.AddChain(iptables.FilterTab, iptables.Chain1PanelOutput); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainOutput, iptables.Chain1PanelOutput, 1); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
number := loadBindNumber(iptables.Chain1PanelInput)
|
|
||||||
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelInput, number); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = settingRepo.Update("IptablesInputStatus", constant.StatusEnable)
|
|
||||||
_ = settingRepo.Update("IptablesOutputStatus", constant.StatusEnable)
|
|
||||||
return nil
|
|
||||||
case "bind-base":
|
|
||||||
if err := initPreRules(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasicBefore, 1); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasic, 2); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasicAfter, 3); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.SaveRulesToFile(iptables.FilterTab, iptables.Chain1PanelBasicBefore, iptables.BasicBeforeFileName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.SaveRulesToFile(iptables.FilterTab, iptables.Chain1PanelBasic, iptables.BasicFileName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.SaveRulesToFile(iptables.FilterTab, iptables.Chain1PanelBasicAfter, iptables.BasicAfterFileName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = settingRepo.Update("IptablesStatus", constant.StatusEnable)
|
|
||||||
return nil
|
|
||||||
case "bind-base-without-init":
|
|
||||||
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasicBefore, 1); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasic, 2); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.BindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasicAfter, 3); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = settingRepo.Update("IptablesStatus", constant.StatusEnable)
|
|
||||||
return nil
|
|
||||||
case "unbind-base":
|
|
||||||
if err := iptables.UnbindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasicAfter); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.UnbindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasicBefore); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.UnbindChain(iptables.FilterTab, iptables.ChainInput, iptables.Chain1PanelBasic); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = settingRepo.Update("IptablesStatus", constant.StatusDisable)
|
|
||||||
return nil
|
|
||||||
case "bind":
|
|
||||||
if err := iptables.BindChain(iptables.FilterTab, targetChain, req.Name, loadBindNumber(req.Name)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if req.Name == iptables.Chain1PanelInput {
|
|
||||||
_ = settingRepo.Update("IptablesInputStatus", constant.StatusEnable)
|
|
||||||
}
|
|
||||||
if req.Name == iptables.Chain1PanelOutput {
|
|
||||||
_ = settingRepo.Update("IptablesOutputStatus", constant.StatusEnable)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case "unbind":
|
|
||||||
if err := iptables.UnbindChain(iptables.FilterTab, targetChain, req.Name); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if req.Name == iptables.Chain1PanelInput {
|
|
||||||
_ = settingRepo.Update("IptablesInputStatus", constant.StatusDisable)
|
|
||||||
}
|
|
||||||
if req.Name == iptables.Chain1PanelOutput {
|
|
||||||
_ = settingRepo.Update("IptablesOutputStatus", constant.StatusDisable)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IptablesService) LoadChainStatus(req dto.OperationWithName) dto.IptablesChainStatus {
|
|
||||||
var data dto.IptablesChainStatus
|
|
||||||
var err error
|
|
||||||
data.DefaultStrategy, err = iptables.LoadDefaultStrategy(req.Name)
|
|
||||||
if err != nil {
|
|
||||||
global.LOG.Error(err)
|
|
||||||
}
|
|
||||||
switch req.Name {
|
|
||||||
case iptables.Chain1PanelBasic:
|
|
||||||
data.IsBind, _ = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainInput, req.Name)
|
|
||||||
case iptables.Chain1PanelInput:
|
|
||||||
data.IsBind, _ = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainInput, req.Name)
|
|
||||||
case iptables.Chain1PanelOutput:
|
|
||||||
data.IsBind, _ = iptables.CheckChainBind(iptables.FilterTab, iptables.ChainOutput, req.Name)
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IptablesService) validateRuleInput(req *dto.IptablesRuleOp) error {
|
|
||||||
if req.Protocol != "" {
|
|
||||||
validProtocols := map[string]bool{"tcp": true, "udp": true, "icmp": true, "all": true}
|
|
||||||
if !validProtocols[strings.ToLower(req.Protocol)] {
|
|
||||||
return fmt.Errorf("invalid protocol: %s, must be tcp, udp, icmp or all", req.Protocol)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if req.SrcIP != "" {
|
|
||||||
if err := s.validateIPOrCIDR(req.SrcIP); err != nil {
|
|
||||||
return fmt.Errorf("invalid source IP: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if req.DstIP != "" {
|
|
||||||
if err := s.validateIPOrCIDR(req.DstIP); err != nil {
|
|
||||||
return fmt.Errorf("invalid destination IP: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if req.SrcPort > 65535 {
|
|
||||||
return fmt.Errorf("invalid source port: %d, must be between 1 and 65535", req.SrcPort)
|
|
||||||
}
|
|
||||||
if req.DstPort > 65535 {
|
|
||||||
return fmt.Errorf("invalid destination port: %d, must be between 1 and 65535", req.DstPort)
|
|
||||||
}
|
|
||||||
if (req.SrcPort > 0 || req.DstPort > 0) && req.Protocol == "" {
|
|
||||||
return fmt.Errorf("port specification requires protocol (tcp/udp)")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IptablesService) validateIPOrCIDR(ipStr string) error {
|
|
||||||
if strings.Contains(ipStr, "/") {
|
|
||||||
_, _, err := net.ParseCIDR(ipStr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid CIDR format: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ip := net.ParseIP(ipStr)
|
|
||||||
if ip == nil {
|
|
||||||
return fmt.Errorf("invalid IP address format")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadBindNumber(chain string) int {
|
|
||||||
if chain == iptables.Chain1PanelOutput {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
number := 1
|
|
||||||
if exist, _ := iptables.CheckChainExist(iptables.FilterTab, iptables.Chain1PanelBasicBefore); exist {
|
|
||||||
number++
|
|
||||||
}
|
|
||||||
if exist, _ := iptables.CheckChainExist(iptables.FilterTab, iptables.Chain1PanelBasic); exist {
|
|
||||||
number++
|
|
||||||
}
|
|
||||||
return number
|
|
||||||
}
|
|
||||||
|
|
||||||
func initPreRules() error {
|
|
||||||
if err := iptables.AddRule(iptables.FilterTab, iptables.Chain1PanelBasicBefore, "-i", "lo", "-j", "ACCEPT", "-m", "comment", "--comment", "Loopback Whitelist"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.AddRule(iptables.FilterTab, iptables.Chain1PanelBasicBefore, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT", "-m", "comment", "--comment", "ESTABLISHED Whitelist"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := syncIptablesFirewallPortWhiteList(false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.AddRule(iptables.FilterTab, iptables.Chain1PanelBasicAfter, "-p", "tcp", "-j", "DROP"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := iptables.AddRule(iptables.FilterTab, iptables.Chain1PanelBasicAfter, "-p", "udp", "-j", "DROP"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncIptablesFirewallPortWhiteList(withSave bool, oldConfiguredPortWhiteList ...[]firewallPortWhitelist) error {
|
|
||||||
requiredPorts, err := loadRequiredFirewallPortWhiteList()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := applyRequiredFirewallPortWhiteListRules(requiredPorts, withSave); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
portWhiteList, err := loadConfiguredFirewallPortWhiteList()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return applyFirewallPortWhiteListRules(portWhiteList, withSave, oldConfiguredPortWhiteList...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyRequiredFirewallPortWhiteListRules(portWhiteList []firewallPortWhitelist, withSave bool) error {
|
|
||||||
if err := syncRequiredFirewallPortWhiteListRules(portWhiteList); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, item := range portWhiteList {
|
|
||||||
if err := iptables.AddRule(iptables.FilterTab, iptables.Chain1PanelBasicBefore, "-p", item.Protocol, "-m", item.Protocol, "--dport", item.Port, "-j", "ACCEPT"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !withSave {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := iptables.SaveRulesToFile(iptables.FilterTab, iptables.Chain1PanelBasicBefore, iptables.BasicBeforeFileName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return iptables.SaveRulesToFile(iptables.FilterTab, iptables.Chain1PanelBasicAfter, iptables.BasicAfterFileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyFirewallPortWhiteListRules(portWhiteList []firewallPortWhitelist, withSave bool, oldConfiguredPortWhiteList ...[]firewallPortWhitelist) error {
|
|
||||||
if err := syncFirewallPortWhiteListRules(portWhiteList, oldConfiguredPortWhiteList...); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, item := range portWhiteList {
|
|
||||||
if err := iptables.AddRule(iptables.FilterTab, iptables.Chain1PanelBasic, "-p", item.Protocol, "-m", item.Protocol, "--dport", item.Port, "-j", "ACCEPT"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !withSave {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return iptables.SaveRulesToFile(iptables.FilterTab, iptables.Chain1PanelBasic, iptables.BasicFileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncRequiredFirewallPortWhiteListRules(portWhiteList []firewallPortWhitelist) error {
|
|
||||||
tcpWhitelist := make(map[string]struct{})
|
|
||||||
udpWhitelist := make(map[string]struct{})
|
|
||||||
for _, item := range portWhiteList {
|
|
||||||
if item.Protocol == "udp" {
|
|
||||||
udpWhitelist[item.Port] = struct{}{}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tcpWhitelist[item.Port] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cleanExtraFirewallPortRules(iptables.Chain1PanelBasicBefore, "tcp", tcpWhitelist); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := cleanExtraFirewallPortRules(iptables.Chain1PanelBasicBefore, "udp", udpWhitelist); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return cleanExtraFirewallPortRules(iptables.Chain1PanelBasicAfter, "udp", map[string]struct{}{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncFirewallPortWhiteListRules(portWhiteList []firewallPortWhitelist, oldConfiguredPortWhiteList ...[]firewallPortWhitelist) error {
|
|
||||||
portWhitelist := firewallPortWhiteListMap(portWhiteList)
|
|
||||||
if len(oldConfiguredPortWhiteList) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, item := range oldConfiguredPortWhiteList[0] {
|
|
||||||
if _, ok := portWhitelist[firewallPortWhiteListKey(item)]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !iptables.CheckRuleExist(iptables.FilterTab, iptables.Chain1PanelBasic, "-p", item.Protocol, "--dport", item.Port, "-j", "ACCEPT") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := iptables.DeleteRule(iptables.FilterTab, iptables.Chain1PanelBasic, "-p", item.Protocol, "--dport", item.Port, "-j", "ACCEPT"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanExtraFirewallPortRules(chain, protocol string, whitelist map[string]struct{}) error {
|
|
||||||
rules, err := iptables.ReadFilterRulesByChain(chain)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
kept := make(map[string]struct{})
|
|
||||||
for _, rule := range rules {
|
|
||||||
if rule.Strategy != "accept" || rule.Protocol != protocol || rule.DstPort == "" || rule.SrcIP != "" || rule.DstIP != "" || rule.SrcPort != "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := whitelist[rule.DstPort]; ok {
|
|
||||||
if _, seen := kept[rule.DstPort]; !seen {
|
|
||||||
kept[rule.DstPort] = struct{}{}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := iptables.DeleteRule(iptables.FilterTab, chain, "-p", protocol, "-m", protocol, "--dport", rule.DstPort, "-j", "ACCEPT"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadPanelPort() string {
|
|
||||||
if !global.IsMaster {
|
|
||||||
return global.CONF.Base.Port
|
|
||||||
} else {
|
|
||||||
var portSetting model.Setting
|
|
||||||
_ = global.CoreDB.Where("key = ?", "ServerPort").First(&portSetting).Error
|
|
||||||
if len(portSetting.Value) != 0 {
|
|
||||||
return portSetting.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
+59
-18
@@ -7,13 +7,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||||
@@ -73,20 +73,22 @@ func (u *LogService) ReadSystemLog(req dto.SystemLogReq) (dto.SystemLogRes, erro
|
|||||||
return u.readFileSystemLog(req, startTime, endTime, pageSize, cursor)
|
return u.readFileSystemLog(req, startTime, endTime, pageSize, cursor)
|
||||||
}
|
}
|
||||||
queryArgs := buildJournalQueryArgs(req, startTime, endTime, pageSize, cursor)
|
queryArgs := buildJournalQueryArgs(req, startTime, endTime, pageSize, cursor)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
output, err := executeJournalQuery(journalctl, queryArgs)
|
||||||
output, err := exec.CommandContext(ctx, journalctl, queryArgs...).CombinedOutput()
|
|
||||||
cancel()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if journalctlGrepUnsupported(req.Keyword, string(output)) {
|
return handleJournalQueryError(req, output, err, func() ([]byte, error) {
|
||||||
return dto.SystemLogRes{}, buserr.New("ErrSystemLogKeywordFilterUnsupported")
|
probeReq := req
|
||||||
}
|
probeReq.Keyword = ""
|
||||||
return dto.SystemLogRes{}, fmt.Errorf("read host system logs failed: %s", strings.TrimSpace(string(output)))
|
probeReq.Priority = ""
|
||||||
|
probeReq.Service = ""
|
||||||
|
probeArgs := buildJournalQueryArgs(probeReq, startTime, endTime, 1, cursor)
|
||||||
|
return executeJournalQuery(journalctl, probeArgs)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
content := strings.TrimSpace(string(output))
|
content := strings.TrimSpace(string(output))
|
||||||
if content == "" || strings.HasPrefix(content, "-- No entries --") {
|
if content == "" || strings.HasPrefix(content, "-- No entries --") {
|
||||||
return dto.SystemLogRes{Source: "journalctl", Items: []dto.SystemLogItem{}}, nil
|
return dto.SystemLogRes{Source: "journalctl", Items: []dto.SystemLogItem{}}, nil
|
||||||
}
|
}
|
||||||
items := parseJournalLogItems(content)
|
items := trimJournalLogItemsToStartTime(parseJournalLogItems(content), startTime)
|
||||||
if cursor != nil && cursor.JournalCursor == "" {
|
if cursor != nil && cursor.JournalCursor == "" {
|
||||||
items, err = skipSystemLogCursorItems(items, *cursor)
|
items, err = skipSystemLogCursorItems(items, *cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -96,6 +98,12 @@ func (u *LogService) ReadSystemLog(req dto.SystemLogReq) (dto.SystemLogRes, erro
|
|||||||
return buildSystemLogResponse("journalctl", items, pageSize, cursor)
|
return buildSystemLogResponse("journalctl", items, pageSize, cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func executeJournalQuery(journalctl string, queryArgs []string) ([]byte, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
return exec.CommandContext(ctx, journalctl, queryArgs...).CombinedOutput()
|
||||||
|
}
|
||||||
|
|
||||||
func (u *LogService) GetSystemLogStatus() (dto.SystemLogStatus, error) {
|
func (u *LogService) GetSystemLogStatus() (dto.SystemLogStatus, error) {
|
||||||
journalctl, err := exec.LookPath("journalctl")
|
journalctl, err := exec.LookPath("journalctl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -134,13 +142,44 @@ func journalctlHelpSupportsGrep(help string) bool {
|
|||||||
return strings.Contains(help, "--grep=")
|
return strings.Contains(help, "--grep=")
|
||||||
}
|
}
|
||||||
|
|
||||||
func journalctlGrepUnsupported(keyword, output string) bool {
|
func handleJournalQueryError(
|
||||||
if strings.TrimSpace(keyword) == "" {
|
req dto.SystemLogReq,
|
||||||
return false
|
output []byte,
|
||||||
|
queryErr error,
|
||||||
|
probe func() ([]byte, error),
|
||||||
|
) (dto.SystemLogRes, error) {
|
||||||
|
if hasSystemLogFilter(req) && probe != nil {
|
||||||
|
probeOutput, probeErr := probe()
|
||||||
|
if probeErr == nil {
|
||||||
|
return dto.SystemLogRes{Source: "journalctl", Items: []dto.SystemLogItem{}}, nil
|
||||||
|
}
|
||||||
|
return dto.SystemLogRes{}, newJournalQueryError(probeOutput, probeErr)
|
||||||
}
|
}
|
||||||
output = strings.ToLower(output)
|
return dto.SystemLogRes{}, newJournalQueryError(output, queryErr)
|
||||||
return strings.Contains(output, "grep") &&
|
}
|
||||||
(strings.Contains(output, "unrecognized option") || strings.Contains(output, "unknown option"))
|
|
||||||
|
func newJournalQueryError(output []byte, queryErr error) error {
|
||||||
|
message := strings.TrimSpace(string(output))
|
||||||
|
if message == "" && queryErr != nil {
|
||||||
|
message = queryErr.Error()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("read host system logs failed: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimJournalLogItemsToStartTime(items []dto.SystemLogItem, startTime time.Time) []dto.SystemLogItem {
|
||||||
|
startTimestamp := startTime.UnixMicro()
|
||||||
|
for i, item := range items {
|
||||||
|
if item.Timestamp < startTimestamp {
|
||||||
|
return items[:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasSystemLogFilter(req dto.SystemLogReq) bool {
|
||||||
|
return strings.TrimSpace(req.Keyword) != "" ||
|
||||||
|
strings.TrimSpace(req.Priority) != "" ||
|
||||||
|
strings.TrimSpace(req.Service) != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func firstOutputLine(output string) string {
|
func firstOutputLine(output string) string {
|
||||||
@@ -155,12 +194,14 @@ func firstOutputLine(output string) string {
|
|||||||
func buildJournalQueryArgs(req dto.SystemLogReq, startTime, endTime time.Time, pageSize int, cursor *systemLogCursor) []string {
|
func buildJournalQueryArgs(req dto.SystemLogReq, startTime, endTime time.Time, pageSize int, cursor *systemLogCursor) []string {
|
||||||
args := []string{
|
args := []string{
|
||||||
"--no-pager", "--reverse", "--output=json",
|
"--no-pager", "--reverse", "--output=json",
|
||||||
"--since", formatJournalQueryTime(startTime),
|
|
||||||
}
|
}
|
||||||
if cursor != nil && cursor.JournalCursor != "" {
|
if cursor != nil && cursor.JournalCursor != "" {
|
||||||
args = append(args, "--after-cursor="+cursor.JournalCursor)
|
args = append(args, "--after-cursor="+cursor.JournalCursor)
|
||||||
} else {
|
} else {
|
||||||
args = append(args, "--until", formatJournalQueryTime(endTime))
|
args = append(args,
|
||||||
|
"--since", formatJournalQueryTime(startTime),
|
||||||
|
"--until", formatJournalQueryTime(endTime),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if service := strings.TrimSpace(req.Service); service != "" {
|
if service := strings.TrimSpace(req.Service); service != "" {
|
||||||
args = append(args, "-u", service)
|
args = append(args, "-u", service)
|
||||||
@@ -169,7 +210,7 @@ func buildJournalQueryArgs(req dto.SystemLogReq, startTime, endTime time.Time, p
|
|||||||
args = append(args, "--priority", priority+".."+priority)
|
args = append(args, "--priority", priority+".."+priority)
|
||||||
}
|
}
|
||||||
if keyword := strings.TrimSpace(req.Keyword); keyword != "" {
|
if keyword := strings.TrimSpace(req.Keyword); keyword != "" {
|
||||||
args = append(args, "--grep", keyword)
|
args = append(args, "--grep", regexp.QuoteMeta(keyword), "--case-sensitive=no")
|
||||||
}
|
}
|
||||||
queryLines := pageSize + 1
|
queryLines := pageSize + 1
|
||||||
if cursor != nil && cursor.JournalCursor == "" {
|
if cursor != nil && cursor.JournalCursor == "" {
|
||||||
|
|||||||
+103
-130
@@ -8,7 +8,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -19,8 +18,7 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"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/accelerator"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/psutil"
|
"github.com/1Panel-dev/1Panel/agent/utils/psutil"
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
@@ -130,63 +128,75 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
|
|||||||
|
|
||||||
func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions {
|
func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions {
|
||||||
var data dto.MonitorGPUOptions
|
var data dto.MonitorGPUOptions
|
||||||
gpuExist, gpuClient := gpu.New()
|
exist, client := accelerator.New()
|
||||||
xpuExist, xpuClient := xpu.New()
|
if !exist {
|
||||||
if !gpuExist && !xpuExist {
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
if gpuExist {
|
snapshot, err := client.Collect(context.Background())
|
||||||
data.GPUType = "gpu"
|
if err != nil {
|
||||||
gpuInfo, err := gpuClient.LoadGpuInfo()
|
global.LOG.Errorf("Load accelerator info failed, err: %v", err)
|
||||||
if err != nil || len(gpuInfo.GPUs) == 0 {
|
|
||||||
global.LOG.Error("Load GPU info failed or no GPU found, err: ", err)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
sort.Slice(gpuInfo.GPUs, func(i, j int) bool {
|
|
||||||
return gpuInfo.GPUs[i].Index < gpuInfo.GPUs[j].Index
|
|
||||||
})
|
|
||||||
for _, item := range gpuInfo.GPUs {
|
|
||||||
var chartHide dto.GPUChartHide
|
|
||||||
chartHide.ProductName = fmt.Sprintf("%d - %s", item.Index, item.ProductName)
|
|
||||||
chartHide.GPU = item.GPUUtil == "" || item.GPUUtil == "N/A"
|
|
||||||
if (item.MemTotal == "" || item.MemTotal == "N/A") && (item.MemUsed == "" || item.MemUsed == "N/A") {
|
|
||||||
chartHide.Memory = true
|
|
||||||
}
|
|
||||||
if (item.MaxPowerLimit == "" || item.MaxPowerLimit == "N/A") && (item.PowerDraw == "" || item.PowerDraw == "N/A") {
|
|
||||||
chartHide.Power = true
|
|
||||||
}
|
|
||||||
chartHide.Temperature = item.Temperature == "" || item.Temperature == "N/A"
|
|
||||||
chartHide.Speed = item.FanSpeed == "" || item.FanSpeed == "N/A"
|
|
||||||
data.ChartHide = append(data.ChartHide, chartHide)
|
|
||||||
data.Options = append(data.Options, fmt.Sprintf("%d - %s", item.Index, item.ProductName))
|
|
||||||
}
|
|
||||||
return data
|
return data
|
||||||
} else {
|
}
|
||||||
|
if warning := snapshot.Warning(); warning != nil {
|
||||||
|
global.LOG.Warnf("Load accelerator info partially failed, err: %v", warning)
|
||||||
|
}
|
||||||
|
return loadGPUOptions(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadGPUOptions(snapshot *accelerator.Snapshot) dto.MonitorGPUOptions {
|
||||||
|
var data dto.MonitorGPUOptions
|
||||||
|
hasGPUOrNPU := false
|
||||||
|
hasXPU := false
|
||||||
|
for _, item := range snapshot.Devices {
|
||||||
|
if item.Kind == accelerator.KindXPU {
|
||||||
|
hasXPU = true
|
||||||
|
} else {
|
||||||
|
hasGPUOrNPU = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case hasGPUOrNPU && hasXPU:
|
||||||
|
data.GPUType = "mixed"
|
||||||
|
case hasXPU:
|
||||||
data.GPUType = "xpu"
|
data.GPUType = "xpu"
|
||||||
xpu, err := xpuClient.LoadGpuInfo()
|
case hasGPUOrNPU:
|
||||||
if err != nil || len(xpu.Xpu) == 0 {
|
data.GPUType = "gpu"
|
||||||
global.LOG.Error("Load XPU info failed or no XPU found, err: ", err)
|
|
||||||
}
|
|
||||||
sort.Slice(xpu.Xpu, func(i, j int) bool {
|
|
||||||
return xpu.Xpu[i].Basic.DeviceID < xpu.Xpu[j].Basic.DeviceID
|
|
||||||
})
|
|
||||||
for _, item := range xpu.Xpu {
|
|
||||||
var chartHide dto.GPUChartHide
|
|
||||||
chartHide.GPU = true
|
|
||||||
chartHide.Speed = true
|
|
||||||
chartHide.ProductName = fmt.Sprintf("%d - %s", item.Basic.DeviceID, item.Basic.DeviceName)
|
|
||||||
if (item.Stats.MemoryUsed == "" || item.Stats.MemoryUsed == "N/A") && (item.Basic.Memory == "" || item.Basic.FreeMemory == "N/A") {
|
|
||||||
chartHide.Memory = true
|
|
||||||
}
|
|
||||||
if item.Stats.Power == "" || item.Stats.Power == "N/A" {
|
|
||||||
chartHide.Power = true
|
|
||||||
}
|
|
||||||
chartHide.Temperature = item.Stats.Temperature == "" || item.Stats.Temperature == "N/A"
|
|
||||||
data.ChartHide = append(data.ChartHide, chartHide)
|
|
||||||
data.Options = append(data.Options, fmt.Sprintf("%d - %s", item.Basic.DeviceID, item.Basic.DeviceName))
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sort.Slice(snapshot.Devices, func(i, j int) bool {
|
||||||
|
if snapshot.Devices[i].Kind != snapshot.Devices[j].Kind {
|
||||||
|
return snapshot.Devices[i].Kind < snapshot.Devices[j].Kind
|
||||||
|
}
|
||||||
|
if snapshot.Devices[i].Vendor != snapshot.Devices[j].Vendor {
|
||||||
|
return snapshot.Devices[i].Vendor < snapshot.Devices[j].Vendor
|
||||||
|
}
|
||||||
|
if snapshot.Devices[i].NPUIndex != snapshot.Devices[j].NPUIndex {
|
||||||
|
return snapshot.Devices[i].NPUIndex < snapshot.Devices[j].NPUIndex
|
||||||
|
}
|
||||||
|
if snapshot.Devices[i].ChipIndex != snapshot.Devices[j].ChipIndex {
|
||||||
|
return snapshot.Devices[i].ChipIndex < snapshot.Devices[j].ChipIndex
|
||||||
|
}
|
||||||
|
return snapshot.Devices[i].Index < snapshot.Devices[j].Index
|
||||||
|
})
|
||||||
|
for _, item := range snapshot.Devices {
|
||||||
|
optionType := "gpu"
|
||||||
|
if item.Kind == accelerator.KindXPU {
|
||||||
|
optionType = "xpu"
|
||||||
|
}
|
||||||
|
chartHide := dto.GPUChartHide{
|
||||||
|
ProductName: item.Label,
|
||||||
|
Type: optionType,
|
||||||
|
GPU: !item.Capabilities.Utilization,
|
||||||
|
Memory: !item.Capabilities.Memory,
|
||||||
|
Power: !item.Capabilities.Power,
|
||||||
|
PowerLimit: !item.Capabilities.PowerLimit,
|
||||||
|
Temperature: !item.Capabilities.Temperature,
|
||||||
|
Speed: !item.Capabilities.FanSpeed,
|
||||||
|
}
|
||||||
|
data.ChartHide = append(data.ChartHide, chartHide)
|
||||||
|
data.Options = append(data.Options, chartHide.ProductName)
|
||||||
|
}
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MonitorService) LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error) {
|
func (m *MonitorService) LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error) {
|
||||||
@@ -297,8 +307,7 @@ func (m *MonitorService) CleanData() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *MonitorService) Run() {
|
func (m *MonitorService) Run() {
|
||||||
saveGPUDataToDB()
|
saveAcceleratorDataToDB()
|
||||||
saveXPUDataToDB()
|
|
||||||
var itemModel model.MonitorBase
|
var itemModel model.MonitorBase
|
||||||
totalPercent, _ := cpu.Percent(3*time.Second, false)
|
totalPercent, _ := cpu.Percent(3*time.Second, false)
|
||||||
if len(totalPercent) == 1 {
|
if len(totalPercent) == 1 {
|
||||||
@@ -344,6 +353,7 @@ func (m *MonitorService) Run() {
|
|||||||
_ = monitorRepo.DelMonitorBase(timeForDelete)
|
_ = monitorRepo.DelMonitorBase(timeForDelete)
|
||||||
_ = monitorRepo.DelMonitorIO(timeForDelete)
|
_ = monitorRepo.DelMonitorIO(timeForDelete)
|
||||||
_ = monitorRepo.DelMonitorNet(timeForDelete)
|
_ = monitorRepo.DelMonitorNet(timeForDelete)
|
||||||
|
_ = monitorRepo.DelMonitorGPU(timeForDelete)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MonitorService) loadDiskIO() {
|
func (m *MonitorService) loadDiskIO() {
|
||||||
@@ -592,93 +602,56 @@ func StartMonitor(removeBefore bool, interval string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveGPUDataToDB() {
|
func saveAcceleratorDataToDB() {
|
||||||
exist, client := gpu.New()
|
exist, client := accelerator.New()
|
||||||
if !exist {
|
if !exist {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gpuInfo, err := client.LoadGpuInfo()
|
snapshot, err := client.Collect(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
global.LOG.Errorf("load accelerator monitor data failed, err: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var list []model.MonitorGPU
|
if warning := snapshot.Warning(); warning != nil {
|
||||||
for _, gpuItem := range gpuInfo.GPUs {
|
global.LOG.Warnf("load accelerator monitor data partially failed, err: %v", warning)
|
||||||
item := model.MonitorGPU{
|
}
|
||||||
ProductName: fmt.Sprintf("%d - %s", gpuItem.Index, gpuItem.ProductName),
|
list := make([]model.MonitorGPU, 0, len(snapshot.Devices))
|
||||||
GPUUtil: loadGPUInfoFloat(gpuItem.GPUUtil),
|
for _, device := range snapshot.Devices {
|
||||||
Temperature: loadGPUInfoFloat(gpuItem.Temperature),
|
list = append(list, newMonitorGPU(device))
|
||||||
PowerDraw: loadGPUInfoFloat(gpuItem.PowerDraw),
|
|
||||||
MaxPowerLimit: loadGPUInfoFloat(gpuItem.MaxPowerLimit),
|
|
||||||
MemUsed: loadGPUInfoFloat(gpuItem.MemUsed),
|
|
||||||
MemTotal: loadGPUInfoFloat(gpuItem.MemTotal),
|
|
||||||
FanSpeed: loadGPUInfoInt(gpuItem.FanSpeed),
|
|
||||||
}
|
|
||||||
process, _ := json.Marshal(gpuItem.Processes)
|
|
||||||
if len(process) != 0 {
|
|
||||||
item.Processes = string(process)
|
|
||||||
}
|
|
||||||
list = append(list, item)
|
|
||||||
}
|
}
|
||||||
if err := repo.NewIMonitorRepo().BatchCreateMonitorGPU(list); err != nil {
|
if err := repo.NewIMonitorRepo().BatchCreateMonitorGPU(list); err != nil {
|
||||||
global.LOG.Errorf("batch create gpu monitor data failed, err: %v", err)
|
global.LOG.Errorf("batch create accelerator monitor data failed, err: %v", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func saveXPUDataToDB() {
|
|
||||||
exist, client := xpu.New()
|
func newMonitorGPU(device accelerator.Device) model.MonitorGPU {
|
||||||
if !exist {
|
item := model.MonitorGPU{
|
||||||
return
|
ProductName: device.Label,
|
||||||
|
GPUUtil: device.Metrics.Utilization.ValueOrZero(),
|
||||||
|
Temperature: device.Metrics.Temperature.ValueOrZero(),
|
||||||
|
PowerDraw: device.Metrics.Power.ValueOrZero(),
|
||||||
|
MaxPowerLimit: device.Metrics.PowerLimit.ValueOrZero(),
|
||||||
|
MemUsed: device.Metrics.MemoryUsed.ValueOrZero(),
|
||||||
|
MemTotal: device.Metrics.MemoryTotal.ValueOrZero(),
|
||||||
|
FanSpeed: int(device.Metrics.FanSpeed.ValueOrZero()),
|
||||||
}
|
}
|
||||||
xpuInfo, err := client.LoadGpuInfo()
|
if len(device.Processes) == 0 {
|
||||||
if err != nil {
|
return item
|
||||||
return
|
|
||||||
}
|
}
|
||||||
var list []model.MonitorGPU
|
processes := make([]dto.GPUProcess, 0, len(device.Processes))
|
||||||
for _, xpuItem := range xpuInfo.Xpu {
|
for _, process := range device.Processes {
|
||||||
item := model.MonitorGPU{
|
processes = append(processes, dto.GPUProcess{
|
||||||
ProductName: fmt.Sprintf("%d - %s", xpuItem.Basic.DeviceID, xpuItem.Basic.DeviceName),
|
Pid: process.PID,
|
||||||
Temperature: loadGPUInfoFloat(xpuItem.Stats.Temperature),
|
Type: process.Type,
|
||||||
PowerDraw: loadGPUInfoFloat(xpuItem.Stats.Power),
|
ProcessName: process.Name,
|
||||||
MemUsed: loadGPUInfoFloat(xpuItem.Stats.MemoryUsed),
|
UsedMemory: process.Memory,
|
||||||
MemTotal: loadGPUInfoFloat(xpuItem.Basic.Memory),
|
})
|
||||||
}
|
|
||||||
if len(xpuItem.Processes) != 0 {
|
|
||||||
var processItem []dto.GPUProcess
|
|
||||||
for _, ps := range xpuItem.Processes {
|
|
||||||
processItem = append(processItem, dto.GPUProcess{
|
|
||||||
Pid: fmt.Sprintf("%v", ps.PID),
|
|
||||||
Type: ps.SHR,
|
|
||||||
ProcessName: ps.Command,
|
|
||||||
UsedMemory: ps.Memory,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
process, _ := json.Marshal(processItem)
|
|
||||||
if len(process) != 0 {
|
|
||||||
item.Processes = string(process)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
list = append(list, item)
|
|
||||||
}
|
}
|
||||||
if err := repo.NewIMonitorRepo().BatchCreateMonitorGPU(list); err != nil {
|
processData, err := json.Marshal(processes)
|
||||||
global.LOG.Errorf("batch create gpu monitor data failed, err: %v", err)
|
if err == nil {
|
||||||
return
|
item.Processes = string(processData)
|
||||||
}
|
}
|
||||||
}
|
return item
|
||||||
func loadGPUInfoInt(val string) int {
|
|
||||||
val = strings.TrimSuffix(val, "%")
|
|
||||||
val = strings.TrimSpace(val)
|
|
||||||
data, _ := strconv.Atoi(val)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
func loadGPUInfoFloat(val string) float64 {
|
|
||||||
val = strings.TrimSpace(val)
|
|
||||||
suffixes := []string{"W", "MB", "MiB", "°C", "C", "%"}
|
|
||||||
for _, suffix := range suffixes {
|
|
||||||
val = strings.TrimSuffix(val, suffix)
|
|
||||||
}
|
|
||||||
val = strings.TrimSpace(val)
|
|
||||||
data, _ := strconv.ParseFloat(val, 64)
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func sumDiskIOCounters(ioStats map[string]disk.IOCountersStat) disk.IOCountersStat {
|
func sumDiskIOCounters(ioStats map[string]disk.IOCountersStat) disk.IOCountersStat {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ type NginxService struct {
|
|||||||
|
|
||||||
type INginxService interface {
|
type INginxService interface {
|
||||||
GetNginxConfig() (*response.NginxFile, error)
|
GetNginxConfig() (*response.NginxFile, error)
|
||||||
GetConfigByScope(req request.NginxScopeReq) ([]response.NginxParam, error)
|
GetConfigByScope(req request.NginxScopeReq) (interface{}, error)
|
||||||
UpdateConfigByScope(req request.NginxConfigUpdate) error
|
UpdateConfigByScope(req request.NginxConfigUpdate) error
|
||||||
GetStatus() (response.NginxStatus, error)
|
GetStatus() (response.NginxStatus, error)
|
||||||
UpdateConfigFile(req request.NginxConfigFileUpdate) error
|
UpdateConfigFile(req request.NginxConfigFileUpdate) error
|
||||||
@@ -62,7 +62,10 @@ func (n NginxService) GetNginxConfig() (*response.NginxFile, error) {
|
|||||||
return &response.NginxFile{Content: string(byteContent)}, nil
|
return &response.NginxFile{Content: string(byteContent)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n NginxService) GetConfigByScope(req request.NginxScopeReq) ([]response.NginxParam, error) {
|
func (n NginxService) GetConfigByScope(req request.NginxScopeReq) (interface{}, error) {
|
||||||
|
if req.Scope == dto.Brotli {
|
||||||
|
return getNginxBrotliParams()
|
||||||
|
}
|
||||||
keys, ok := dto.ScopeKeyMap[req.Scope]
|
keys, ok := dto.ScopeKeyMap[req.Scope]
|
||||||
if !ok || len(keys) == 0 {
|
if !ok || len(keys) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -71,6 +74,9 @@ func (n NginxService) GetConfigByScope(req request.NginxScopeReq) ([]response.Ng
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n NginxService) UpdateConfigByScope(req request.NginxConfigUpdate) error {
|
func (n NginxService) UpdateConfigByScope(req request.NginxConfigUpdate) error {
|
||||||
|
if req.Scope == dto.Brotli {
|
||||||
|
return updateNginxBrotliParams(getNginxParams(req.Params, dto.BrotliKeys))
|
||||||
|
}
|
||||||
keys, ok := dto.ScopeKeyMap[req.Scope]
|
keys, ok := dto.ScopeKeyMap[req.Scope]
|
||||||
if !ok || len(keys) == 0 {
|
if !ok || len(keys) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
)
|
||||||
|
|
||||||
|
// stockNginxGzipDirectives is the gzip block shipped by the OpenResty app
|
||||||
|
// since 1.21.4.3. The upgrade only rewrites values when the installed
|
||||||
|
// nginx.conf still carries exactly these directives and values, which proves
|
||||||
|
// the user never tuned compression. Any deviation aborts the rewrite.
|
||||||
|
var stockNginxGzipDirectives = map[string]string{
|
||||||
|
"gzip": "on",
|
||||||
|
"gzip_min_length": "1k",
|
||||||
|
"gzip_buffers": "4 16k",
|
||||||
|
"gzip_http_version": "1.1",
|
||||||
|
"gzip_comp_level": "2",
|
||||||
|
"gzip_types": "text/plain application/javascript application/x-javascript text/javascript text/css application/xml",
|
||||||
|
"gzip_vary": "on",
|
||||||
|
"gzip_proxied": "expired no-cache no-store private auth",
|
||||||
|
"gzip_disable": `"MSIE [1-6]\."`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// correctedNginxGzipDirectives replaces the stock values in place. gzip lives
|
||||||
|
// in the http block of nginx.conf and must stay there: repeating it from an
|
||||||
|
// included file would make nginx reject the configuration with a duplicate
|
||||||
|
// directive error, and the compression settings page reads and writes these
|
||||||
|
// same keys in nginx.conf.
|
||||||
|
var correctedNginxGzipDirectives = map[string]string{
|
||||||
|
"gzip_comp_level": "5",
|
||||||
|
"gzip_types": strings.Join(nginxCompressibleTypes, " "),
|
||||||
|
"gzip_proxied": "any",
|
||||||
|
}
|
||||||
|
|
||||||
|
// obsoleteNginxGzipDirectives are dropped outright.
|
||||||
|
var obsoleteNginxGzipDirectives = map[string]struct{}{
|
||||||
|
// A per-request User-Agent regex for browsers with no measurable share.
|
||||||
|
"gzip_disable": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
var nginxGzipDirectiveRe = regexp.MustCompile(`(?m)^[ \t]*(gzip[a-z_]*)[ \t]+([^;\n]*);[ \t]*$`)
|
||||||
|
|
||||||
|
func nginxMainConfigPath(install model.AppInstall) string {
|
||||||
|
return path.Join(install.GetPath(), nginxModuleConfDir, "nginx.conf")
|
||||||
|
}
|
||||||
|
|
||||||
|
// upgradeStockNginxGzipConfig rewrites the factory gzip defaults in place.
|
||||||
|
//
|
||||||
|
// Upgrades deliberately preserve the user's nginx.conf, so corrected defaults
|
||||||
|
// shipped with a new OpenResty version would otherwise never reach existing
|
||||||
|
// installations.
|
||||||
|
//
|
||||||
|
// The config parser is not used: its dumper regenerates the whole file, drops
|
||||||
|
// standalone comments and reorders proxy includes, which would be destructive
|
||||||
|
// on a user's main config. Lines are edited individually so everything outside
|
||||||
|
// the gzip block stays byte-identical.
|
||||||
|
func upgradeStockNginxGzipConfig(install model.AppInstall) error {
|
||||||
|
configPath := nginxMainConfigPath(install)
|
||||||
|
content, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !isStockNginxGzipConfig(string(content)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
updated := rewriteNginxGzipDirectives(string(content))
|
||||||
|
if updated == string(content) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = nginxCheckAndReload(string(content), configPath, install.ContainerName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
global.LOG.Info("updated the stock OpenResty gzip configuration to the current defaults")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isStockNginxGzipConfig reports whether every gzip directive in the config
|
||||||
|
// matches the factory defaults exactly, with none missing and none extra.
|
||||||
|
func isStockNginxGzipConfig(content string) bool {
|
||||||
|
found := make(map[string]string)
|
||||||
|
for _, match := range nginxGzipDirectiveRe.FindAllStringSubmatch(content, -1) {
|
||||||
|
name := match[1]
|
||||||
|
value := strings.Join(strings.Fields(match[2]), " ")
|
||||||
|
if _, ok := found[name]; ok {
|
||||||
|
// A directive repeated in the http block means the config was
|
||||||
|
// edited by hand; leave it alone.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
found[name] = value
|
||||||
|
}
|
||||||
|
if len(found) != len(stockNginxGzipDirectives) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for name, expected := range stockNginxGzipDirectives {
|
||||||
|
if found[name] != expected {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// rewriteNginxGzipDirectives updates known values in place, drops obsolete
|
||||||
|
// directives and appends directives that are missing, preserving the original
|
||||||
|
// indentation and leaving every other line untouched.
|
||||||
|
func rewriteNginxGzipDirectives(content string) string {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
result := make([]string, 0, len(lines))
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
lastGzipIndex := -1
|
||||||
|
lastGzipIndent := " "
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
match := nginxGzipDirectiveRe.FindStringSubmatch(line)
|
||||||
|
if match == nil {
|
||||||
|
result = append(result, line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := match[1]
|
||||||
|
// The indentation belongs to the line itself; a top-level directive
|
||||||
|
// must not inherit the indent a previous, nested directive used.
|
||||||
|
lineIndent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||||
|
if lineIndent == "" {
|
||||||
|
lineIndent = " "
|
||||||
|
}
|
||||||
|
lastGzipIndent = lineIndent
|
||||||
|
if _, obsolete := obsoleteNginxGzipDirectives[name]; obsolete {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[name] = struct{}{}
|
||||||
|
if replacement, ok := correctedNginxGzipDirectives[name]; ok {
|
||||||
|
result = append(result, lineIndent+name+" "+replacement+";")
|
||||||
|
} else {
|
||||||
|
result = append(result, line)
|
||||||
|
}
|
||||||
|
lastGzipIndex = len(result) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directives introduced by a newer default set are appended right after
|
||||||
|
// the existing block so they stay visually grouped.
|
||||||
|
var missing []string
|
||||||
|
for name := range correctedNginxGzipDirectives {
|
||||||
|
if _, ok := seen[name]; !ok {
|
||||||
|
missing = append(missing, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(missing) == 0 || lastGzipIndex < 0 {
|
||||||
|
return strings.Join(result, "\n")
|
||||||
|
}
|
||||||
|
sort.Strings(missing)
|
||||||
|
added := make([]string, 0, len(missing))
|
||||||
|
for _, name := range missing {
|
||||||
|
added = append(added, lastGzipIndent+name+" "+correctedNginxGzipDirectives[name]+";")
|
||||||
|
}
|
||||||
|
tail := append(added, result[lastGzipIndex+1:]...)
|
||||||
|
return strings.Join(append(result[:lastGzipIndex+1], tail...), "\n")
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
|
||||||
|
)
|
||||||
|
|
||||||
|
const stockNginxConf = `user root;
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;
|
||||||
|
|
||||||
|
events {
|
||||||
|
use epoll;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
server_names_hash_bucket_size 512;
|
||||||
|
keepalive_requests 5000;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1k;
|
||||||
|
gzip_buffers 4 16k;
|
||||||
|
gzip_http_version 1.1;
|
||||||
|
gzip_comp_level 2;
|
||||||
|
gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_disable "MSIE [1-6]\.";
|
||||||
|
|
||||||
|
limit_conn_zone $binary_remote_addr zone=perip:10m;
|
||||||
|
|
||||||
|
include /usr/local/openresty/nginx/conf/http.d/*.conf;
|
||||||
|
include /usr/local/openresty/nginx/conf/conf.d/*.conf;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestIsStockNginxGzipConfig(t *testing.T) {
|
||||||
|
if !isStockNginxGzipConfig(stockNginxConf) {
|
||||||
|
t.Fatal("factory configuration should be detected as stock")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsStockNginxGzipConfigRejectsTunedValues(t *testing.T) {
|
||||||
|
cases := map[string]string{
|
||||||
|
"comp level changed": strings.Replace(stockNginxConf, "gzip_comp_level 2;", "gzip_comp_level 6;", 1),
|
||||||
|
"gzip disabled": strings.Replace(stockNginxConf, "gzip on;", "gzip off;", 1),
|
||||||
|
"types extended": strings.Replace(stockNginxConf,
|
||||||
|
"application/xml;", "application/xml application/json;", 1),
|
||||||
|
"directive removed": strings.Replace(stockNginxConf, " gzip_vary on;\n", "", 1),
|
||||||
|
"directive added": strings.Replace(stockNginxConf, " gzip_vary on;\n",
|
||||||
|
" gzip_vary on;\n gzip_static on;\n", 1),
|
||||||
|
}
|
||||||
|
for name, content := range cases {
|
||||||
|
if isStockNginxGzipConfig(content) {
|
||||||
|
t.Errorf("%s: tuned configuration must not be rewritten", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsStockNginxGzipConfigRejectsDuplicateDirective(t *testing.T) {
|
||||||
|
content := strings.Replace(stockNginxConf, " gzip on;\n", " gzip on;\n gzip on;\n", 1)
|
||||||
|
if isStockNginxGzipConfig(content) {
|
||||||
|
t.Fatal("a duplicated directive indicates a hand-edited config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRewriteNginxGzipDirectives(t *testing.T) {
|
||||||
|
result := rewriteNginxGzipDirectives(stockNginxConf)
|
||||||
|
|
||||||
|
for _, expected := range []string{
|
||||||
|
" gzip_comp_level 5;",
|
||||||
|
" gzip_proxied any;",
|
||||||
|
" gzip on;",
|
||||||
|
" gzip_vary on;",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(result, expected) {
|
||||||
|
t.Errorf("expected directive missing: %s\n%s", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(result, "application/json") {
|
||||||
|
t.Error("gzip_types should now cover application/json")
|
||||||
|
}
|
||||||
|
if strings.Contains(result, "gzip_disable") {
|
||||||
|
t.Error("obsolete gzip_disable should have been dropped")
|
||||||
|
}
|
||||||
|
if strings.Contains(result, "gzip_comp_level 2;") {
|
||||||
|
t.Error("stale comp level should have been replaced")
|
||||||
|
}
|
||||||
|
// Everything outside the gzip block must survive untouched.
|
||||||
|
for _, keep := range []string{
|
||||||
|
"server_names_hash_bucket_size 512;",
|
||||||
|
"keepalive_requests 5000;",
|
||||||
|
"limit_conn_zone $binary_remote_addr zone=perip:10m;",
|
||||||
|
"include /usr/local/openresty/nginx/conf/http.d/*.conf;",
|
||||||
|
"include /usr/local/openresty/nginx/conf/conf.d/*.conf;",
|
||||||
|
"include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;",
|
||||||
|
"user root;",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(result, keep) {
|
||||||
|
t.Errorf("unrelated line was altered or dropped: %s", keep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(result, "}\n") {
|
||||||
|
t.Error("trailing newline was not preserved")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRewriteNginxGzipDirectivesIsIdempotent(t *testing.T) {
|
||||||
|
once := rewriteNginxGzipDirectives(stockNginxConf)
|
||||||
|
twice := rewriteNginxGzipDirectives(once)
|
||||||
|
if once != twice {
|
||||||
|
t.Errorf("rewrite is not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", once, twice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRewriteNginxGzipDirectivesAppendsMissing(t *testing.T) {
|
||||||
|
// gzip_proxied absent from the source must be appended, not silently lost.
|
||||||
|
content := strings.Replace(stockNginxConf,
|
||||||
|
" gzip_proxied expired no-cache no-store private auth;\n", "", 1)
|
||||||
|
result := rewriteNginxGzipDirectives(content)
|
||||||
|
if !strings.Contains(result, "gzip_proxied any;") {
|
||||||
|
t.Errorf("missing directive was not appended:\n%s", result)
|
||||||
|
}
|
||||||
|
if !strings.Contains(result, "limit_conn_zone $binary_remote_addr zone=perip:10m;") {
|
||||||
|
t.Error("appending must not clobber following lines")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRewriteNginxGzipDirectivesKeepsGzipLikeNames(t *testing.T) {
|
||||||
|
// gunzip and proxy_set_header must survive: only directives whose name
|
||||||
|
// starts with "gzip" are managed here.
|
||||||
|
content := "http {\n gunzip on;\n gzip on;\n proxy_set_header Accept-Encoding gzip;\n}\n"
|
||||||
|
result := rewriteNginxGzipDirectives(content)
|
||||||
|
if !strings.Contains(result, "gunzip on;") {
|
||||||
|
t.Error("gunzip directive must be preserved")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result, "proxy_set_header Accept-Encoding gzip;") {
|
||||||
|
t.Error("proxy_set_header must be preserved")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result, " gzip on;") {
|
||||||
|
t.Error("gzip directive should be kept in place")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The gzip.conf template, the upgrade maps and the appstore defaults are three
|
||||||
|
// copies of one intent. Pin the first two so they cannot drift apart silently.
|
||||||
|
func TestGzipTemplateMatchesCorrectedDefaults(t *testing.T) {
|
||||||
|
template := nginx_conf.GetWebsiteFile("gzip.conf")
|
||||||
|
if len(template) == 0 {
|
||||||
|
t.Fatal("gzip.conf template is missing from the embedded files")
|
||||||
|
}
|
||||||
|
expected := make(map[string]string, len(stockNginxGzipDirectives))
|
||||||
|
for name, value := range stockNginxGzipDirectives {
|
||||||
|
expected[name] = value
|
||||||
|
}
|
||||||
|
for name := range obsoleteNginxGzipDirectives {
|
||||||
|
delete(expected, name)
|
||||||
|
}
|
||||||
|
for name, value := range correctedNginxGzipDirectives {
|
||||||
|
expected[name] = value
|
||||||
|
}
|
||||||
|
found := make(map[string]string)
|
||||||
|
for _, match := range nginxGzipDirectiveRe.FindAllStringSubmatch(string(template), -1) {
|
||||||
|
found[match[1]] = strings.Join(strings.Fields(match[2]), " ")
|
||||||
|
}
|
||||||
|
if len(found) != len(expected) {
|
||||||
|
t.Fatalf("template has %d directives, corrected defaults have %d", len(found), len(expected))
|
||||||
|
}
|
||||||
|
for name, want := range expected {
|
||||||
|
if got, ok := found[name]; !ok {
|
||||||
|
t.Errorf("template is missing %s", name)
|
||||||
|
} else if got != want {
|
||||||
|
t.Errorf("%s: template has %q, corrected defaults have %q", name, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// nginxHTTPConfDir holds http-context directives generated by 1Panel.
|
||||||
|
// load_module is a main-context directive and therefore lives in
|
||||||
|
// modules-enabled, which cannot host http-context directives such as
|
||||||
|
// "brotli on". The directory is included by nginx.conf before conf.d so
|
||||||
|
// that per-site configuration keeps overriding these defaults.
|
||||||
|
nginxHTTPConfDir = "http.d"
|
||||||
|
|
||||||
|
nginxHTTPConfigPrefix = "1panel-http-"
|
||||||
|
nginxHTTPConfigHeader = "# Managed by 1Panel. Manual changes will be overwritten.\n"
|
||||||
|
|
||||||
|
// nginxHTTPIncludeDirective is the include line that loads the managed
|
||||||
|
// directory. Fresh installs carry it in the shipped nginx.conf; existing
|
||||||
|
// ones get it inserted by the panel the first time a module needs
|
||||||
|
// http-context configuration.
|
||||||
|
nginxHTTPIncludeDirective = "include /usr/local/openresty/nginx/conf/http.d/*.conf;"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// nginxHTTPIncludeRe matches the include line wherever it appears. The
|
||||||
|
// absolute path prefix, quoting and whitespace are all optional in the
|
||||||
|
// match so a variant written by an older installer or by hand still
|
||||||
|
// counts; a commented-out copy does not.
|
||||||
|
nginxHTTPIncludeRe = regexp.MustCompile(`(?m)^[ \t]*include\s+"?(/usr/local/openresty/nginx/conf/)?http\.d/\*\.conf"?\s*;[ \t]*\r?$`)
|
||||||
|
|
||||||
|
// nginxConfDIncludeRe locates the site-config include, the preferred
|
||||||
|
// insertion point, and captures its indentation.
|
||||||
|
nginxConfDIncludeRe = regexp.MustCompile(`(?m)^([ \t]*)include\s+"?(/usr/local/openresty/nginx/conf/)?conf\.d/\*\.conf"?\s*;[ \t]*\r?$`)
|
||||||
|
|
||||||
|
// nginxHTTPBlockStartRe locates the http block opening, the fallback
|
||||||
|
// insertion point, and captures its indentation.
|
||||||
|
nginxHTTPBlockStartRe = regexp.MustCompile(`(?m)^([ \t]*)http[ \t]*\{[ \t]*\r?$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// nginxHTTPIncludePresent reports whether nginx.conf already loads http.d.
|
||||||
|
func nginxHTTPIncludePresent(install model.AppInstall) bool {
|
||||||
|
content, err := os.ReadFile(nginxMainConfigPath(install))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return nginxHTTPIncludeRe.MatchString(string(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeNginxFileAtomic writes through a temp file plus rename so a crash or a
|
||||||
|
// concurrent reader never observes a half-written config.
|
||||||
|
func writeNginxFileAtomic(filePath string, content []byte) error {
|
||||||
|
tmpPath := filePath + ".tmp"
|
||||||
|
if err := os.WriteFile(tmpPath, content, constant.FilePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(tmpPath, filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nginxFileLineEnding picks the file's own style so an inserted or rewritten
|
||||||
|
// line does not mix LF into a CRLF file.
|
||||||
|
func nginxFileLineEnding(content string) string {
|
||||||
|
if strings.Contains(content, "\r\n") {
|
||||||
|
return "\r\n"
|
||||||
|
}
|
||||||
|
return "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertNginxHTTPInclude returns the config with the http.d include added.
|
||||||
|
//
|
||||||
|
// The include goes right before the conf.d include so panel-managed defaults
|
||||||
|
// are evaluated before per-site configuration; without one, it goes at the
|
||||||
|
// top of the http block. The inserted line follows the file's own line-ending
|
||||||
|
// style, and everything else stays byte-identical. A config without a
|
||||||
|
// locatable http block is rejected, and callers degrade instead of failing
|
||||||
|
// their operation over it.
|
||||||
|
func insertNginxHTTPInclude(content string) (string, error) {
|
||||||
|
if nginxHTTPIncludeRe.MatchString(content) {
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
eol := nginxFileLineEnding(content)
|
||||||
|
if m := nginxConfDIncludeRe.FindStringSubmatchIndex(content); m != nil {
|
||||||
|
indent := content[m[2]:m[3]]
|
||||||
|
return content[:m[0]] + indent + nginxHTTPIncludeDirective + eol + content[m[0]:], nil
|
||||||
|
}
|
||||||
|
if m := nginxHTTPBlockStartRe.FindStringSubmatchIndex(content); m != nil {
|
||||||
|
indent := content[m[2]:m[3]] + " "
|
||||||
|
return content[:m[1]] + eol + indent + nginxHTTPIncludeDirective + content[m[1]:], nil
|
||||||
|
}
|
||||||
|
return "", errors.New("no insertion point for the http.d include in nginx.conf")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureNginxHTTPIncludeActive makes nginx.conf load http.d, inserting the
|
||||||
|
// include when missing. It returns whether the directory is loaded after the
|
||||||
|
// call, plus the original config content so the caller can roll back the edit
|
||||||
|
// together with the rest of its changes.
|
||||||
|
func ensureNginxHTTPIncludeActive(install model.AppInstall) (active bool, snapshot []byte, err error) {
|
||||||
|
configPath := nginxMainConfigPath(install)
|
||||||
|
content, readErr := os.ReadFile(configPath)
|
||||||
|
if readErr != nil {
|
||||||
|
return false, nil, readErr
|
||||||
|
}
|
||||||
|
if nginxHTTPIncludeRe.MatchString(string(content)) {
|
||||||
|
if err = os.MkdirAll(nginxHTTPConfigDir(install), constant.DirPerm); err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
return true, nil, nil
|
||||||
|
}
|
||||||
|
updated, insErr := insertNginxHTTPInclude(string(content))
|
||||||
|
if insErr != nil {
|
||||||
|
return false, nil, insErr
|
||||||
|
}
|
||||||
|
if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
if err = os.MkdirAll(nginxHTTPConfigDir(install), constant.DirPerm); err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
return true, content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// nginxHTTPDirective is a single http-context directive rendered into a
|
||||||
|
// managed file.
|
||||||
|
type nginxHTTPDirective struct {
|
||||||
|
Name string
|
||||||
|
Params []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d nginxHTTPDirective) render() string {
|
||||||
|
if len(d.Params) == 0 {
|
||||||
|
return d.Name + ";"
|
||||||
|
}
|
||||||
|
return d.Name + " " + strings.Join(d.Params, " ") + ";"
|
||||||
|
}
|
||||||
|
|
||||||
|
func nginxHTTPConfigDir(install model.AppInstall) string {
|
||||||
|
return path.Join(install.GetPath(), nginxModuleConfDir, nginxHTTPConfDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nginxHTTPConfigFileName(order int, name string) string {
|
||||||
|
return fmt.Sprintf("%s%04d-%s.conf", nginxHTTPConfigPrefix, order, nginxModulePathName(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderNginxHTTPConfig builds the content of a managed http.d file.
|
||||||
|
func renderNginxHTTPConfig(directives []nginxHTTPDirective) []byte {
|
||||||
|
var content strings.Builder
|
||||||
|
content.WriteString(nginxHTTPConfigHeader)
|
||||||
|
for _, directive := range directives {
|
||||||
|
content.WriteString(directive.render())
|
||||||
|
content.WriteString("\n")
|
||||||
|
}
|
||||||
|
return []byte(content.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var nginxHTTPDirectiveRe = regexp.MustCompile(`^[ \t]*([a-z_][a-z0-9_]*)[ \t]+([^;]*);[ \t]*$`)
|
||||||
|
|
||||||
|
// readNginxHTTPDirectives parses a managed file back into directive values.
|
||||||
|
// A missing or unreadable file yields no directives, which makes callers fall
|
||||||
|
// back to their defaults.
|
||||||
|
func readNginxHTTPDirectives(filePath string) map[string][]string {
|
||||||
|
content, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
directives := make(map[string][]string)
|
||||||
|
for _, line := range strings.Split(string(content), "\n") {
|
||||||
|
match := nginxHTTPDirectiveRe.FindStringSubmatch(line)
|
||||||
|
if match == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
directives[match[1]] = strings.Fields(match[2])
|
||||||
|
}
|
||||||
|
return directives
|
||||||
|
}
|
||||||
|
|
||||||
|
// snapshotManagedNginxHTTPConfigs captures every managed file so a failed
|
||||||
|
// nginx -t can be rolled back.
|
||||||
|
func snapshotManagedNginxHTTPConfigs(configDir string) (nginxModuleConfigSnapshot, error) {
|
||||||
|
snapshot := make(nginxModuleConfigSnapshot)
|
||||||
|
entries, err := os.ReadDir(configDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return snapshot, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() || !strings.HasPrefix(entry.Name(), nginxHTTPConfigPrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
content, readErr := os.ReadFile(path.Join(configDir, entry.Name()))
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, readErr
|
||||||
|
}
|
||||||
|
snapshot[entry.Name()] = content
|
||||||
|
}
|
||||||
|
return snapshot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyManagedNginxHTTPConfigs writes the desired managed files and removes
|
||||||
|
// managed files that are no longer wanted. Files not carrying the managed
|
||||||
|
// prefix are never touched.
|
||||||
|
func applyManagedNginxHTTPConfigs(configDir string, desired map[string][]byte) error {
|
||||||
|
if err := os.MkdirAll(configDir, constant.DirPerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
entries, err := os.ReadDir(configDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
names := make([]string, 0, len(desired))
|
||||||
|
for fileName := range desired {
|
||||||
|
names = append(names, fileName)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
for _, fileName := range names {
|
||||||
|
tmpPath := path.Join(configDir, "."+fileName+".tmp")
|
||||||
|
if err = os.WriteFile(tmpPath, desired[fileName], constant.FilePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = os.Rename(tmpPath, path.Join(configDir, fileName)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() || !strings.HasPrefix(entry.Name(), nginxHTTPConfigPrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := desired[entry.Name()]; !ok {
|
||||||
|
if err = os.Remove(path.Join(configDir, entry.Name())); err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const plainNginxConf = `user root;
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;
|
||||||
|
|
||||||
|
events {
|
||||||
|
use epoll;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_comp_level 5;
|
||||||
|
|
||||||
|
limit_conn_zone $binary_remote_addr zone=perip:10m;
|
||||||
|
|
||||||
|
include /usr/local/openresty/nginx/conf/conf.d/*.conf;
|
||||||
|
include /usr/local/openresty/nginx/conf/default/*.conf;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestInsertNginxHTTPIncludeBeforeConfD(t *testing.T) {
|
||||||
|
got, err := insertNginxHTTPInclude(plainNginxConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, " "+nginxHTTPIncludeDirective) {
|
||||||
|
t.Fatalf("include not inserted with matching indent:\n%s", got)
|
||||||
|
}
|
||||||
|
// Ordering is the point of the insertion site: panel defaults must be
|
||||||
|
// evaluated before per-site configuration.
|
||||||
|
httpIdx := strings.Index(got, "conf/http.d/*.conf")
|
||||||
|
confDIdx := strings.Index(got, "conf/conf.d/*.conf")
|
||||||
|
if httpIdx < 0 || confDIdx < 0 || httpIdx > confDIdx {
|
||||||
|
t.Fatalf("http.d must be included before conf.d (http.d=%d conf.d=%d)", httpIdx, confDIdx)
|
||||||
|
}
|
||||||
|
// The rest of the file must be untouched.
|
||||||
|
stripped := strings.Replace(got, " "+nginxHTTPIncludeDirective+"\n", "", 1)
|
||||||
|
if stripped != plainNginxConf {
|
||||||
|
t.Fatal("insertion altered content outside the inserted line")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInsertNginxHTTPIncludeIsIdempotent(t *testing.T) {
|
||||||
|
once, err := insertNginxHTTPInclude(plainNginxConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
twice, err := insertNginxHTTPInclude(once)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if twice != once {
|
||||||
|
t.Fatal("a second insertion must be a no-op")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInsertNginxHTTPIncludeFallsBackToHTTPBlock(t *testing.T) {
|
||||||
|
content := strings.Replace(plainNginxConf,
|
||||||
|
" include /usr/local/openresty/nginx/conf/conf.d/*.conf;\n", "", 1)
|
||||||
|
got, err := insertNginxHTTPInclude(content)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
httpIdx := strings.Index(got, "http {")
|
||||||
|
incIdx := strings.Index(got, nginxHTTPIncludeDirective)
|
||||||
|
if incIdx < 0 || incIdx < httpIdx {
|
||||||
|
t.Fatalf("include should land inside the http block:\n%s", got)
|
||||||
|
}
|
||||||
|
// Indented one level deeper than the http keyword.
|
||||||
|
if !strings.Contains(got, " "+nginxHTTPIncludeDirective) {
|
||||||
|
t.Errorf("fallback indentation is wrong:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInsertNginxHTTPIncludeRejectsConfigWithoutHTTPBlock(t *testing.T) {
|
||||||
|
if _, err := insertNginxHTTPInclude("events {}\n"); err == nil {
|
||||||
|
t.Fatal("a config without an http block must be rejected so callers can degrade")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInsertNginxHTTPIncludeIgnoresCommentedIncludes(t *testing.T) {
|
||||||
|
commented := strings.Replace(plainNginxConf,
|
||||||
|
" include /usr/local/openresty/nginx/conf/conf.d/*.conf;",
|
||||||
|
" # include /usr/local/openresty/nginx/conf/conf.d/*.conf;", 1)
|
||||||
|
got, err := insertNginxHTTPInclude(commented)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// The commented conf.d line is not a valid anchor; the fallback must win.
|
||||||
|
if strings.Index(got, nginxHTTPIncludeDirective) < strings.Index(got, "http {") {
|
||||||
|
t.Fatal("a commented include must not be used as the anchor")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInsertNginxHTTPIncludeHandlesCRLF(t *testing.T) {
|
||||||
|
crlf := strings.ReplaceAll(plainNginxConf, "\n", "\r\n")
|
||||||
|
got, err := insertNginxHTTPInclude(crlf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, nginxHTTPIncludeDirective) {
|
||||||
|
t.Fatal("include missing on a CRLF file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNginxHTTPIncludeRe(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
content string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"present", plainNginxConf + " " + nginxHTTPIncludeDirective + "\n", true},
|
||||||
|
{"absent", plainNginxConf, false},
|
||||||
|
{"commented out", "# " + nginxHTTPIncludeDirective, false},
|
||||||
|
// nginx accepts quoted paths, and a hand-written or legacy installer
|
||||||
|
// may use them; a quoted include must count as present.
|
||||||
|
{"quoted absolute path", `include "/usr/local/openresty/nginx/conf/http.d/*.conf";`, true},
|
||||||
|
{"quoted with extra whitespace", ` include "/usr/local/openresty/nginx/conf/http.d/*.conf" ;`, true},
|
||||||
|
{"relative path form", ` include http.d/*.conf;`, true},
|
||||||
|
{"a different directory does not count", ` include /usr/local/openresty/nginx/conf/conf.d/*.conf;`, false},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := nginxHTTPIncludeRe.MatchString(tc.content); got != tc.want {
|
||||||
|
t.Fatalf("expected %v, got %v", tc.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The anchor for insertion must tolerate the same variants, or a config with
|
||||||
|
// a quoted conf.d include would take the http-block fallback for no reason.
|
||||||
|
func TestInsertNginxHTTPIncludeWithQuotedConfDAnchor(t *testing.T) {
|
||||||
|
quoted := strings.Replace(plainNginxConf,
|
||||||
|
" include /usr/local/openresty/nginx/conf/conf.d/*.conf;",
|
||||||
|
` include "/usr/local/openresty/nginx/conf/conf.d/*.conf";`, 1)
|
||||||
|
got, err := insertNginxHTTPInclude(quoted)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
httpIdx := strings.Index(got, "conf/http.d/*.conf")
|
||||||
|
confDIdx := strings.Index(got, "conf/conf.d/*.conf")
|
||||||
|
if httpIdx < 0 || confDIdx < 0 || httpIdx > confDIdx {
|
||||||
|
t.Fatalf("quoted anchor not used; include misplaced:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A CRLF file must keep its own line endings after insertion.
|
||||||
|
func TestInsertNginxHTTPIncludeKeepsCRLFStyle(t *testing.T) {
|
||||||
|
crlf := strings.ReplaceAll(plainNginxConf, "\n", "\r\n")
|
||||||
|
got, err := insertNginxHTTPInclude(crlf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
idx := strings.Index(got, nginxHTTPIncludeDirective)
|
||||||
|
if idx < 0 {
|
||||||
|
t.Fatal("include missing")
|
||||||
|
}
|
||||||
|
if got[idx-1] == '\n' || (idx >= 2 && got[idx-2:idx] != "\r\n" && got[idx-1] != ' ') {
|
||||||
|
// The inserted line must end with \r\n like the rest of the file.
|
||||||
|
}
|
||||||
|
end := idx + len(nginxHTTPIncludeDirective)
|
||||||
|
if end+2 > len(got) || got[end:end+2] != "\r\n" {
|
||||||
|
t.Fatalf("inserted line does not end with CRLF: %q", got[end:end+4])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
@@ -164,6 +165,51 @@ func nginxModuleDynamicSupported(install model.AppInstall) bool {
|
|||||||
fileOp.Stat(path.Join(buildPath, nginxModuleCatalogFile))
|
fileOp.Stat(path.Join(buildPath, nginxModuleCatalogFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nginxModuleStaticSupported reports whether the install can recompile its own
|
||||||
|
// OpenResty image, which is what a static module build needs. Versions before
|
||||||
|
// dynamic modules existed ship a compose file with a build section and the
|
||||||
|
// sources under build/; the oldest ones only reference a prebuilt image and
|
||||||
|
// cannot compile anything.
|
||||||
|
func nginxModuleStaticSupported(install model.AppInstall) bool {
|
||||||
|
if !files.NewFileOp().Stat(path.Join(install.GetPath(), nginxModuleBuildDir, "Dockerfile")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
envStr, err := coverEnvJsonToStr(install.Env)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
project, err := dockerUtils.GetComposeProject(install.Name, install.GetPath(),
|
||||||
|
[]byte(install.DockerCompose), []byte(envStr), true)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, service := range project.AllServices() {
|
||||||
|
if service.Build != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultNginxModuleBuildMode picks the mode an install can actually perform.
|
||||||
|
//
|
||||||
|
// Module state written before build modes existed carries no buildMode at all.
|
||||||
|
// Rejecting it would fail loadNginxModules, and with it every module operation
|
||||||
|
// and the upgrade itself, so the value is inferred from what the install can
|
||||||
|
// do rather than assumed.
|
||||||
|
func defaultNginxModuleBuildMode(install model.AppInstall) string {
|
||||||
|
if nginxModuleDynamicSupported(install) {
|
||||||
|
return nginxModuleBuildDynamic
|
||||||
|
}
|
||||||
|
if nginxModuleStaticSupported(install) {
|
||||||
|
return nginxModuleBuildStatic
|
||||||
|
}
|
||||||
|
// Neither builder is available. Dynamic keeps the module inert instead of
|
||||||
|
// triggering an image rebuild that cannot succeed; the build itself still
|
||||||
|
// reports the missing capability.
|
||||||
|
return nginxModuleBuildDynamic
|
||||||
|
}
|
||||||
|
|
||||||
func syncNginxModuleBuilder(detailBuildDir, installBuildDir string) error {
|
func syncNginxModuleBuilder(detailBuildDir, installBuildDir string) error {
|
||||||
sourcePath := path.Join(detailBuildDir, nginxModuleBuilderFile)
|
sourcePath := path.Join(detailBuildDir, nginxModuleBuilderFile)
|
||||||
targetPath := path.Join(installBuildDir, nginxModuleBuilderFile)
|
targetPath := path.Join(installBuildDir, nginxModuleBuilderFile)
|
||||||
@@ -515,6 +561,7 @@ type openrestyUpgradeSnapshot struct {
|
|||||||
|
|
||||||
var openrestyUpgradeSnapshotPaths = []string{
|
var openrestyUpgradeSnapshotPaths = []string{
|
||||||
nginxModuleBuildDir,
|
nginxModuleBuildDir,
|
||||||
|
nginxModuleModulesDir,
|
||||||
"scripts",
|
"scripts",
|
||||||
path.Join(nginxModuleConfDir, nginxModuleEnabledConfDir),
|
path.Join(nginxModuleConfDir, nginxModuleEnabledConfDir),
|
||||||
path.Join(nginxModuleConfDir, "nginx.conf"),
|
path.Join(nginxModuleConfDir, "nginx.conf"),
|
||||||
@@ -653,10 +700,56 @@ func reconcileDynamicNginxModuleConfig(install model.AppInstall, modules []dto.N
|
|||||||
return fmt.Errorf("validate combined dynamic module configuration: %w", err)
|
return fmt.Errorf("validate combined dynamic module configuration: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = applyManagedNginxModuleConfigs(configDir, desired); err != nil {
|
|
||||||
|
// Runtime directives live in http.d because load_module is main-context
|
||||||
|
// while directives such as "brotli on" are http-context. Both sets are
|
||||||
|
// written before nginx -t runs, so nginx only ever observes the final,
|
||||||
|
// consistent state; on failure both are rolled back together.
|
||||||
|
//
|
||||||
|
// The include that loads http.d is inserted on demand: only when a module
|
||||||
|
// actually needs runtime configuration. An install whose nginx.conf cannot
|
||||||
|
// be edited safely keeps the previous behaviour — the module loads but the
|
||||||
|
// runtime directives are skipped — rather than failing the operation.
|
||||||
|
httpConfigDir := nginxHTTPConfigDir(install)
|
||||||
|
desiredHTTP := desiredNginxModuleRuntimeConfigs(install, modules, target)
|
||||||
|
httpActive := nginxHTTPIncludePresent(install)
|
||||||
|
var nginxConfSnapshot []byte
|
||||||
|
if len(desiredHTTP) > 0 && !httpActive {
|
||||||
|
active, confSnapshot, includeErr := ensureNginxHTTPIncludeActive(install)
|
||||||
|
if includeErr != nil {
|
||||||
|
global.LOG.Warnf("cannot insert the http.d include into nginx.conf, skipping runtime directives: %v", includeErr)
|
||||||
|
desiredHTTP = nil
|
||||||
|
} else {
|
||||||
|
httpActive = active
|
||||||
|
nginxConfSnapshot = confSnapshot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var httpSnapshot nginxModuleConfigSnapshot
|
||||||
|
if httpActive {
|
||||||
|
if httpSnapshot, err = snapshotManagedNginxHTTPConfigs(httpConfigDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
restore := func() {
|
||||||
_ = applyManagedNginxModuleConfigs(configDir, snapshot)
|
_ = applyManagedNginxModuleConfigs(configDir, snapshot)
|
||||||
|
if httpActive {
|
||||||
|
_ = applyManagedNginxHTTPConfigs(httpConfigDir, httpSnapshot)
|
||||||
|
}
|
||||||
|
if nginxConfSnapshot != nil {
|
||||||
|
_ = os.WriteFile(nginxMainConfigPath(install), nginxConfSnapshot, constant.FilePerm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = applyManagedNginxModuleConfigs(configDir, desired); err != nil {
|
||||||
|
restore()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if httpActive {
|
||||||
|
if err = applyManagedNginxHTTPConfigs(httpConfigDir, desiredHTTP); err != nil {
|
||||||
|
restore()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if !reload {
|
if !reload {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -665,11 +758,11 @@ func reconcileDynamicNginxModuleConfig(install model.AppInstall, modules []dto.N
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil {
|
if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil {
|
||||||
_ = applyManagedNginxModuleConfigs(configDir, snapshot)
|
restore()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = opNginx(install.ContainerName, constant.NginxReload); err != nil {
|
if err = opNginx(install.ContainerName, constant.NginxReload); err != nil {
|
||||||
_ = applyManagedNginxModuleConfigs(configDir, snapshot)
|
restore()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -721,6 +814,14 @@ func applyManagedNginxModuleConfigs(configDir string, desired map[string][]byte)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hasEnabledStaticNginxModules reports whether a full image rebuild is needed.
|
||||||
|
//
|
||||||
|
// Module state is the only input on purpose. RESTY_CONFIG_OPTIONS_MORE in .env
|
||||||
|
// is derived state: configureStaticNginxModules rewrites it from the modules
|
||||||
|
// below, and every build path calls that function before building. Treating a
|
||||||
|
// leftover value as a reason to rebuild would start a full recompile that
|
||||||
|
// configureStaticNginxModules has already reduced to an empty option list, so
|
||||||
|
// the rebuild could only reproduce the image it started from.
|
||||||
func hasEnabledStaticNginxModules(modules []dto.NginxModule) bool {
|
func hasEnabledStaticNginxModules(modules []dto.NginxModule) bool {
|
||||||
for _, module := range modules {
|
for _, module := range modules {
|
||||||
normalizeNginxModule(&module)
|
normalizeNginxModule(&module)
|
||||||
@@ -731,17 +832,6 @@ func hasEnabledStaticNginxModules(modules []dto.NginxModule) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func staticNginxBuildRequired(install model.AppInstall, modules []dto.NginxModule) bool {
|
|
||||||
if hasEnabledStaticNginxModules(modules) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
envs, err := gotenv.Read(install.GetEnvPath())
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(envs["RESTY_CONFIG_OPTIONS_MORE"]) != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func configureStaticNginxModules(install model.AppInstall, modules []dto.NginxModule, mirror string) error {
|
func configureStaticNginxModules(install model.AppInstall, modules []dto.NginxModule, mirror string) error {
|
||||||
buildPath := path.Join(install.GetPath(), nginxModuleBuildDir)
|
buildPath := path.Join(install.GetPath(), nginxModuleBuildDir)
|
||||||
var params, packages []string
|
var params, packages []string
|
||||||
@@ -806,11 +896,17 @@ func executeNginxModuleBuild(install model.AppInstall, reqModules []string, forc
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
staticBuild := staticNginxBuildRequired(install, modules)
|
// Only the module list decides this. A leftover RESTY_CONFIG_OPTIONS_MORE
|
||||||
if !staticBuild && hasDynamicNginxModuleBuildTask(modules, reqModules) {
|
// used to force the static path here, which meant a full image rebuild for
|
||||||
if !nginxModuleDynamicSupported(install) {
|
// an install that has no static module left to compile.
|
||||||
return errors.New("the installed OpenResty version does not support dynamic module builds")
|
staticBuild := hasEnabledStaticNginxModules(modules)
|
||||||
}
|
if !staticBuild && hasDynamicNginxModuleBuildTask(modules, reqModules) && !nginxModuleDynamicSupported(install) {
|
||||||
|
// The catalog and the builder have always shipped together, and an
|
||||||
|
// install missing the catalog fails to load its module state before
|
||||||
|
// this point, so this branch is a guard rather than a real path. Keep
|
||||||
|
// the error actionable instead of faking a build the state machine
|
||||||
|
// cannot record.
|
||||||
|
return buserr.New("ErrModuleBuildUnsupported")
|
||||||
}
|
}
|
||||||
if staticBuild {
|
if staticBuild {
|
||||||
return executeStaticNginxModuleBuild(install, modules, mirror, force, parentTask)
|
return executeStaticNginxModuleBuild(install, modules, mirror, force, parentTask)
|
||||||
@@ -885,7 +981,17 @@ func loadNginxModulesWithCatalog(install model.AppInstall, catalogPath string) (
|
|||||||
Builds: state.Builds, LastError: state.LastError,
|
Builds: state.Builds, LastError: state.LastError,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// Catalog entries always declare a mode; state written before build modes
|
||||||
|
// existed does not. Fill the gap from the install's capabilities so an
|
||||||
|
// upgrade from such a version can still read its own module state.
|
||||||
|
fallbackMode := ""
|
||||||
for i := range modules {
|
for i := range modules {
|
||||||
|
if modules[i].BuildMode == "" {
|
||||||
|
if fallbackMode == "" {
|
||||||
|
fallbackMode = defaultNginxModuleBuildMode(install)
|
||||||
|
}
|
||||||
|
modules[i].BuildMode = fallbackMode
|
||||||
|
}
|
||||||
if err = validateNginxModuleBuildMode(modules[i]); err != nil {
|
if err = validateNginxModuleBuildMode(modules[i]); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,483 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nginxCompressibleTypes is shared by gzip_types and brotli_types so both
|
||||||
|
// encoders cover the same content. Already compressed formats (images other
|
||||||
|
// than SVG, woff/woff2, archives, media) are deliberately excluded:
|
||||||
|
// recompressing them costs CPU and usually grows the payload.
|
||||||
|
var nginxCompressibleTypes = []string{
|
||||||
|
"text/plain",
|
||||||
|
"text/css",
|
||||||
|
"text/xml",
|
||||||
|
"text/javascript",
|
||||||
|
"application/json",
|
||||||
|
"application/ld+json",
|
||||||
|
"application/javascript",
|
||||||
|
"application/x-javascript",
|
||||||
|
"application/xml",
|
||||||
|
"application/xhtml+xml",
|
||||||
|
"application/rss+xml",
|
||||||
|
"application/atom+xml",
|
||||||
|
"application/wasm",
|
||||||
|
"image/svg+xml",
|
||||||
|
"font/ttf",
|
||||||
|
"font/otf",
|
||||||
|
}
|
||||||
|
|
||||||
|
// nginxModuleRuntimeDefaults maps a module to the http-context directives that
|
||||||
|
// make it actually do something once loaded. Without these, enabling a module
|
||||||
|
// only emits load_module, leaving it loaded but inert.
|
||||||
|
//
|
||||||
|
// brotli_static is intentionally omitted: nginx does not verify that a .br
|
||||||
|
// file is newer than its source, so a stale artifact would be served
|
||||||
|
// indefinitely with no error.
|
||||||
|
var nginxModuleRuntimeDefaults = map[string][]nginxHTTPDirective{
|
||||||
|
"ngx_brotli": {
|
||||||
|
{Name: "brotli", Params: []string{"on"}},
|
||||||
|
// Brotli level 5 reaches roughly gzip level 9 ratio at a fraction of
|
||||||
|
// the cost. The nginx default of 6 is tuned for static assets and is
|
||||||
|
// too expensive for dynamic responses.
|
||||||
|
{Name: "brotli_comp_level", Params: []string{"5"}},
|
||||||
|
{Name: "brotli_min_length", Params: []string{"1k"}},
|
||||||
|
{Name: "brotli_types", Params: nginxCompressibleTypes},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// nginxModuleRuntimeLoadOrder keeps managed file names stable and ordered
|
||||||
|
// independently of the module load order used for load_module.
|
||||||
|
var nginxModuleRuntimeLoadOrder = map[string]int{
|
||||||
|
"ngx_brotli": 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
func nginxModuleRuntimeOrder(name string) int {
|
||||||
|
if order, ok := nginxModuleRuntimeLoadOrder[name]; ok {
|
||||||
|
return order
|
||||||
|
}
|
||||||
|
return 900
|
||||||
|
}
|
||||||
|
|
||||||
|
// desiredNginxModuleRuntimeConfigs renders the managed http.d files for every
|
||||||
|
// enabled module that has a ready build and known runtime defaults.
|
||||||
|
//
|
||||||
|
// Values the user changed through the compression settings page are read back
|
||||||
|
// from the current managed file, so reconciling after an unrelated module
|
||||||
|
// change does not silently reset them to the defaults.
|
||||||
|
//
|
||||||
|
// A module the user already configured by hand in nginx.conf is skipped
|
||||||
|
// entirely. Emitting the same directive from an included file would make nginx
|
||||||
|
// reject the configuration as a duplicate, so their setup is left as the only
|
||||||
|
// definition.
|
||||||
|
func desiredNginxModuleRuntimeConfigs(install model.AppInstall, modules []dto.NginxModule, target dto.NginxModuleTarget) map[string][]byte {
|
||||||
|
desired := make(map[string][]byte)
|
||||||
|
for _, module := range modules {
|
||||||
|
normalizeNginxModule(&module)
|
||||||
|
// A custom module that happens to share a built-in name must not pick
|
||||||
|
// up the built-in's runtime defaults; the table is for catalog modules.
|
||||||
|
if module.Custom {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
directives, ok := nginxModuleRuntimeDefaults[module.Name]
|
||||||
|
if !ok || !module.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !nginxModuleRuntimeReady(module, target) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if nginxModuleConfiguredByUser(install, module.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(module.Name), module.Name)
|
||||||
|
current := readNginxHTTPDirectives(path.Join(nginxHTTPConfigDir(install), fileName))
|
||||||
|
desired[fileName] = renderNginxHTTPConfig(mergeNginxRuntimeDirectives(directives, current))
|
||||||
|
}
|
||||||
|
return desired
|
||||||
|
}
|
||||||
|
|
||||||
|
// nginxModuleConfiguredByUser reports whether the user already manages any of
|
||||||
|
// the module's directives by hand.
|
||||||
|
//
|
||||||
|
// Users who enabled brotli before the panel managed it did so by editing
|
||||||
|
// nginx.conf or a file it includes. That definition has to keep winning: it is
|
||||||
|
// the one nginx has been running with, and adding a second one from http.d
|
||||||
|
// would break the configuration outright.
|
||||||
|
//
|
||||||
|
// Any brotli* directive counts, not just the primary one. A user who only
|
||||||
|
// tuned brotli_comp_level has still taken ownership of the block, and nginx
|
||||||
|
// allows the same directive at http and server scope, so a site-scoped value
|
||||||
|
// must suppress the managed one too.
|
||||||
|
func nginxModuleConfiguredByUser(install model.AppInstall, moduleName string) bool {
|
||||||
|
if _, ok := nginxModuleRuntimeDefaults[moduleName]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, filePath := range nginxModuleUserConfigPaths(install) {
|
||||||
|
content, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if nginxModuleUserDirectiveRe.MatchString(string(content)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// nginxModuleUserDirectiveRe matches any active (non-commented) brotli*
|
||||||
|
// directive at the start of a line, wherever it was written.
|
||||||
|
var nginxModuleUserDirectiveRe = regexp.MustCompile(`(?m)^[ \t]*brotli[a-z_]*[ \t]+[^;\n]*;`)
|
||||||
|
|
||||||
|
// nginxModuleUserConfigPaths lists the files that may carry a user's brotli
|
||||||
|
// configuration: the main config and the http-scope files it includes. The
|
||||||
|
// stream include is skipped on purpose — brotli is an http module and has no
|
||||||
|
// business there.
|
||||||
|
func nginxModuleUserConfigPaths(install model.AppInstall) []string {
|
||||||
|
return nginxModuleUserConfigPathsWithSiteDir(install, GetWebSiteRootDir())
|
||||||
|
}
|
||||||
|
|
||||||
|
// nginxModuleUserConfigPathsWithSiteDir is the testable core: the site conf
|
||||||
|
// directory is injected so unit tests do not need the settings database.
|
||||||
|
func nginxModuleUserConfigPathsWithSiteDir(install model.AppInstall, siteDir string) []string {
|
||||||
|
paths := []string{nginxMainConfigPath(install)}
|
||||||
|
paths = append(paths, globConfFiles(path.Join(siteDir, "conf.d"))...)
|
||||||
|
paths = append(paths, globConfFiles(path.Join(install.GetPath(), nginxModuleConfDir, "default"))...)
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
func globConfFiles(dir string) []string {
|
||||||
|
matches, err := filepath.Glob(path.Join(dir, "*.conf"))
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
// nginxUserDirectivePattern matches a directive the user wrote in nginx.conf,
|
||||||
|
// capturing its indentation so a rewrite can keep the line's shape. Leading
|
||||||
|
// whitespace only, so a commented-out line never matches.
|
||||||
|
func nginxUserDirectivePattern(name string) *regexp.Regexp {
|
||||||
|
return regexp.MustCompile(`(?m)^([ \t]*)` + regexp.QuoteMeta(name) + `[ \t]+[^;\n]*;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nginxConfigDefinesDirective reports whether a directive is set anywhere in
|
||||||
|
// the file, ignoring commented-out lines.
|
||||||
|
func nginxConfigDefinesDirective(content, name string) bool {
|
||||||
|
return nginxUserDirectivePattern(name).MatchString(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeNginxRuntimeDirectives keeps the declared directive set and ordering
|
||||||
|
// while preferring values already present in the managed file.
|
||||||
|
func mergeNginxRuntimeDirectives(defaults []nginxHTTPDirective, current map[string][]string) []nginxHTTPDirective {
|
||||||
|
if len(current) == 0 {
|
||||||
|
return defaults
|
||||||
|
}
|
||||||
|
merged := make([]nginxHTTPDirective, 0, len(defaults))
|
||||||
|
for _, directive := range defaults {
|
||||||
|
if params, ok := current[directive.Name]; ok && len(params) > 0 {
|
||||||
|
directive.Params = params
|
||||||
|
}
|
||||||
|
merged = append(merged, directive)
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
// nginxBrotliModuleName is the catalog name of the brotli module.
|
||||||
|
const nginxBrotliModuleName = "ngx_brotli"
|
||||||
|
|
||||||
|
// getNginxBrotliParams reports the brotli settings currently in effect, and
|
||||||
|
// where they come from.
|
||||||
|
//
|
||||||
|
// Brotli is normally served from the managed http.d file instead of
|
||||||
|
// nginx.conf, so the directives can be removed together with the module. When
|
||||||
|
// the module is disabled the declared defaults are returned, which lets the
|
||||||
|
// settings page show what would be applied once it is enabled.
|
||||||
|
//
|
||||||
|
// If the user configured brotli anywhere nginx loads it from, those values
|
||||||
|
// are reported instead and ManagedExternally is set. Showing the managed
|
||||||
|
// defaults there would misrepresent what the server is actually running, and
|
||||||
|
// the panel must not write a second copy.
|
||||||
|
func getNginxBrotliParams() (*response.NginxBrotliRes, error) {
|
||||||
|
install, err := getAppInstallByKey(constant.AppOpenresty)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
managedExternally := nginxModuleConfiguredByUser(install, nginxBrotliModuleName)
|
||||||
|
var current map[string][]string
|
||||||
|
if managedExternally {
|
||||||
|
current = readNginxUserBrotliDirectives(install)
|
||||||
|
} else {
|
||||||
|
fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
|
||||||
|
current = readNginxHTTPDirectives(path.Join(nginxHTTPConfigDir(install), fileName))
|
||||||
|
}
|
||||||
|
res := &response.NginxBrotliRes{
|
||||||
|
ManagedExternally: managedExternally,
|
||||||
|
// Without the include, values the panel would write would never reach
|
||||||
|
// nginx, so they are reported as unavailable rather than shown as if
|
||||||
|
// they were in effect.
|
||||||
|
ManagedUnavailable: !managedExternally && !nginxHTTPIncludePresent(install),
|
||||||
|
}
|
||||||
|
for _, directive := range mergeNginxRuntimeDirectives(nginxModuleRuntimeDefaults[nginxBrotliModuleName], current) {
|
||||||
|
res.Params = append(res.Params, response.NginxParam{Name: directive.Name, Params: directive.Params})
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readNginxUserBrotliDirectives collects the brotli directives the user wrote
|
||||||
|
// in any of the files nginx loads them from.
|
||||||
|
func readNginxUserBrotliDirectives(install model.AppInstall) map[string][]string {
|
||||||
|
directives := make(map[string][]string)
|
||||||
|
for _, filePath := range nginxModuleUserConfigPaths(install) {
|
||||||
|
content, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, name := range dto.BrotliKeys {
|
||||||
|
pattern := regexp.MustCompile(`(?m)^[ \t]*` + regexp.QuoteMeta(name) + `[ \t]+([^;\n]*);`)
|
||||||
|
if match := pattern.FindStringSubmatch(string(content)); match != nil {
|
||||||
|
if _, exists := directives[name]; !exists {
|
||||||
|
directives[name] = strings.Fields(strings.TrimSpace(match[1]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return directives
|
||||||
|
}
|
||||||
|
|
||||||
|
// nginxBrotliValueRe whitelists what a brotli value may contain. The values
|
||||||
|
// are written into nginx.conf and the managed files verbatim; rejecting
|
||||||
|
// anything outside this set blocks both directive injection (`;`, newline,
|
||||||
|
// braces, quotes) and the `$` group-reference expansion of
|
||||||
|
// regexp.ReplaceAllString, which the in-place rewrite uses.
|
||||||
|
var nginxBrotliValueRe = regexp.MustCompile(`^[a-zA-Z0-9._+\-/:* ]+$`)
|
||||||
|
|
||||||
|
// validateNginxBrotliValues rejects any value outside the whitelist. The UI
|
||||||
|
// only sends on/off, numbers and sizes, but the endpoint is reachable
|
||||||
|
// directly.
|
||||||
|
func validateNginxBrotliValues(values map[string][]string) error {
|
||||||
|
for name, params := range values {
|
||||||
|
for _, param := range params {
|
||||||
|
if !nginxBrotliValueRe.MatchString(param) {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", fmt.Sprintf("invalid value for %s", name), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateNginxBrotliParams persists brotli settings to the managed http.d file.
|
||||||
|
//
|
||||||
|
// Writing is refused unless the module is enabled and built: the directives
|
||||||
|
// would reference a module that is not loaded and nginx would fail to start.
|
||||||
|
func updateNginxBrotliParams(params []dto.NginxParam) error {
|
||||||
|
install, err := getAppInstallByKey(constant.AppOpenresty)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
modules, err := loadNginxModules(install)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
values := make(map[string][]string, len(params))
|
||||||
|
for _, param := range params {
|
||||||
|
values[param.Name] = param.Params
|
||||||
|
}
|
||||||
|
if err = validateNginxBrotliValues(values); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range modules {
|
||||||
|
if modules[i].Name != nginxBrotliModuleName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !modules[i].Enable {
|
||||||
|
return buserr.New("ErrBrotliDisabled")
|
||||||
|
}
|
||||||
|
// The user configured brotli in nginx.conf before the panel managed
|
||||||
|
// it. Update those lines in place: writing a managed file as well
|
||||||
|
// would define every directive twice and nginx would refuse to start.
|
||||||
|
if nginxModuleConfiguredByUser(install, nginxBrotliModuleName) {
|
||||||
|
return updateUserNginxBrotliParams(install, values)
|
||||||
|
}
|
||||||
|
// A managed write needs the include. Installations missing it are
|
||||||
|
// upgraded in place here; when nginx.conf cannot be edited safely the
|
||||||
|
// write is refused with an actionable error instead of writing values
|
||||||
|
// nginx would never load.
|
||||||
|
if !nginxHTTPIncludePresent(install) {
|
||||||
|
configPath := nginxMainConfigPath(install)
|
||||||
|
content, readErr := os.ReadFile(configPath)
|
||||||
|
if readErr != nil {
|
||||||
|
return readErr
|
||||||
|
}
|
||||||
|
updated, insErr := insertNginxHTTPInclude(string(content))
|
||||||
|
if insErr != nil {
|
||||||
|
return buserr.New("ErrBrotliUnsupported")
|
||||||
|
}
|
||||||
|
if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = os.MkdirAll(nginxHTTPConfigDir(install), constant.DirPerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = nginxCheckAndReload(string(content), configPath, install.ContainerName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
|
||||||
|
configDir := nginxHTTPConfigDir(install)
|
||||||
|
snapshot, snapErr := snapshotManagedNginxHTTPConfigs(configDir)
|
||||||
|
if snapErr != nil {
|
||||||
|
return snapErr
|
||||||
|
}
|
||||||
|
merged := mergeNginxRuntimeDirectives(nginxModuleRuntimeDefaults[nginxBrotliModuleName], values)
|
||||||
|
desired := map[string][]byte{fileName: renderNginxHTTPConfig(merged)}
|
||||||
|
for name, content := range snapshot {
|
||||||
|
if name != fileName {
|
||||||
|
desired[name] = content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = applyManagedNginxHTTPConfigs(configDir, desired); err != nil {
|
||||||
|
_ = applyManagedNginxHTTPConfigs(configDir, snapshot)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil {
|
||||||
|
_ = applyManagedNginxHTTPConfigs(configDir, snapshot)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = opNginx(install.ContainerName, constant.NginxReload); err != nil {
|
||||||
|
_ = applyManagedNginxHTTPConfigs(configDir, snapshot)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// The directory is bind-mounted read-only and the include is a glob: a
|
||||||
|
// missing mount or an unrecognised include lets nginx -t pass while
|
||||||
|
// loading nothing. Read the effective configuration back instead of
|
||||||
|
// trusting the files we wrote.
|
||||||
|
if err = assertNginxBrotliActive(install.ContainerName); err != nil {
|
||||||
|
_ = applyManagedNginxHTTPConfigs(configDir, snapshot)
|
||||||
|
return buserr.New("ErrBrotliUnsupported")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return buserr.New("ErrBrotliDisabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// assertNginxBrotliActive confirms the managed brotli directives are in the
|
||||||
|
// running server's effective configuration. It is the only check that catches
|
||||||
|
// a bind mount that never reached the container or an include variant the
|
||||||
|
// detection missed — both pass nginx -t and reload silently.
|
||||||
|
func assertNginxBrotliActive(containerName string) error {
|
||||||
|
out, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout(
|
||||||
|
"docker", "exec", "-i", containerName, "nginx", "-T")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !nginxModuleUserDirectiveRe.MatchString(out) {
|
||||||
|
return errors.New("brotli directives are not in the effective nginx configuration")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateUserNginxBrotliParams rewrites the brotli directives the user wrote
|
||||||
|
// into nginx.conf, in place.
|
||||||
|
//
|
||||||
|
// Only the values change: each directive keeps its original line and
|
||||||
|
// indentation, and every other line is untouched, so a hand-maintained config
|
||||||
|
// survives an edit from the settings page. Directives the user did not write
|
||||||
|
// are not introduced, since the panel cannot know where they intended them.
|
||||||
|
//
|
||||||
|
// A managed file can still be on disk when the panel managed brotli before
|
||||||
|
// the user wrote their own directives. Leaving it behind would make every
|
||||||
|
// directive duplicate once the user's config is touched, so it is removed
|
||||||
|
// first and rolled back together with the config on a failed nginx -t.
|
||||||
|
func updateUserNginxBrotliParams(install model.AppInstall, values map[string][]string) error {
|
||||||
|
configPath := nginxMainConfigPath(install)
|
||||||
|
content, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configDir := nginxHTTPConfigDir(install)
|
||||||
|
httpSnapshot, snapErr := snapshotManagedNginxHTTPConfigs(configDir)
|
||||||
|
if snapErr != nil {
|
||||||
|
return snapErr
|
||||||
|
}
|
||||||
|
managedFile := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
|
||||||
|
if _, stale := httpSnapshot[managedFile]; stale {
|
||||||
|
remaining := make(map[string][]byte, len(httpSnapshot))
|
||||||
|
for name, fileContent := range httpSnapshot {
|
||||||
|
if name != managedFile {
|
||||||
|
remaining[name] = fileContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = applyManagedNginxHTTPConfigs(configDir, remaining); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
restore := func() {
|
||||||
|
_ = writeNginxFileAtomic(configPath, content)
|
||||||
|
_ = applyManagedNginxHTTPConfigs(configDir, httpSnapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := string(content)
|
||||||
|
for _, name := range dto.BrotliKeys {
|
||||||
|
params, ok := values[name]
|
||||||
|
if !ok || len(params) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pattern := nginxUserDirectivePattern(name)
|
||||||
|
if !pattern.MatchString(updated) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
replacement := "${1}" + name + " " + strings.Join(params, " ") + ";"
|
||||||
|
updated = pattern.ReplaceAllString(updated, replacement)
|
||||||
|
}
|
||||||
|
if updated == string(content) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err = writeNginxFileAtomic(configPath, []byte(updated)); err != nil {
|
||||||
|
restore()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil {
|
||||||
|
restore()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = opNginx(install.ContainerName, constant.NginxReload); err != nil {
|
||||||
|
restore()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// nginxModuleRuntimeReady reports whether the module is actually usable.
|
||||||
|
//
|
||||||
|
// Dynamic modules need a ready build for the current target, otherwise the
|
||||||
|
// .so is missing and nginx would reject the directives. Static modules are
|
||||||
|
// compiled into the binary and carry no artifacts, so an enabled static
|
||||||
|
// module is considered ready. This rests on a data premise: the catalog only
|
||||||
|
// declares a module static when the image ships it. Checking for a build
|
||||||
|
// record instead would be wrong here — reconcile runs inside the static build
|
||||||
|
// flow, before the record for the build in progress exists, and would drop
|
||||||
|
// the runtime configuration of the module that was just compiled in.
|
||||||
|
func nginxModuleRuntimeReady(module dto.NginxModule, target dto.NginxModuleTarget) bool {
|
||||||
|
if module.BuildMode == nginxModuleBuildStatic {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
build := findCurrentNginxModuleBuild(module, target)
|
||||||
|
if build == nil || build.Status != nginxModuleStatusReady {
|
||||||
|
build = findLatestNginxModuleBuild(module, target)
|
||||||
|
}
|
||||||
|
return build != nil && build.Status == nginxModuleStatusReady
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user