Compare commits

...
Author SHA1 Message Date
ssongliu c04848a92a feat: sync script library on startup (#13376) 2026-07-27 15:10:09 +08:00
ssongliu 748f5f070a fix: show placeholder for empty host log service (#13375) 2026-07-27 13:56:24 +08:00
蘭 2dcb367622 fix: Fix quick data selection errors with Shift (#13374)
* fix: Fix quick data selection errors with Shift

* fix: Fix quick data selection errors with Shift
2026-07-27 13:48:32 +08:00
ssongliu 26bd2d08be fix: improve host system log filtering compatibility (#13373) 2026-07-27 11:25:26 +08:00
ssongliu 739686ff89 fix: avoid decimal time unit conversions (#13372) 2026-07-27 10:50:42 +08:00
ssongliu 68a871a5d7 fix: restore fail2ban after firewall start (#13364) 2026-07-27 10:16:04 +08:00
ssongliu b47bba4827 feat: add VM health check translations (#13361) 2026-07-24 16:00:29 +08:00
CityFun 3b91859a49 fix: Fix an issue where an application could still be installed after its version had been deleted remotely. (#13360) 2026-07-24 16:00:02 +08:00
CityFun c417bc9c2b chore: change lego sdk (#13357) 2026-07-23 18:37:36 +08:00
CityFun 7c95becfeb feat: Optimize the OpenResty module compilation logic. (#13355)
* refactor: separate openresty module catalog and state

* feat: Optimize the OpenResty module compilation logic.
2026-07-23 16:53:56 +08:00
ssongliu 6b1480c5fa feat: move container batch operations to table footer (#13348) 2026-07-23 15:45:53 +08:00
ssongliu 7f450544e1 feat: improve host log management (#13345) 2026-07-23 14:53:26 +08:00
ssongliu 0c046edfe5 fix(database): improve MySQL user display (#13344) 2026-07-23 14:44:07 +08:00
Snrat 2a2e6607b8 feat: 重构OpenResty模块部分以支持动态编译 (#13291)
* feat: support dynamic module build for OpenResty

* feat: add dynamic module build page for OpenResty

* refactor: drop auto fallback, gate dynamic build by version support

- remove auto-to-static fallback; dynamic build failure now reports the
  error and hints switching to static build manually
- gate dynamic builds on module support files (Dockerfile.modules +
  module.catalog.json) instead of version numbers, expose
  dynamicSupported in the modules API
- collect repeated path/status/operate strings into constants
- move nginx module regex patterns into utils/re with semantic helpers
- reorganize nginx_module.go around the main build flows and inline
  single-use thin helpers

* feat: limit nginx module build mode options by version support

- build mode radio offers only dynamic and static (auto maps to dynamic
  for legacy data)
- disable the dynamic option with a hint when the installed OpenResty
  version lacks dynamic build support

* feat: complete i18n for nginx module pages

Fill in the new nginx module keys for all eleven language files
(translations other than zh/en are draft machine translations).

* feat: probe dynamic module support on load and drop the auto build mode

- probe each non-static module's configure params when loading the
  module list and report dynamicSupport=supported/unsupported up front
- normalize the legacy auto build mode to dynamic

* feat: clarify module build modes in the UI

- build drawer lists dynamic modules (tagged, hot-reload) and static
  modules (tagged, full rebuild + container restart) separately
- disable the dynamic option per module when its params do not support
  dynamic build, distinct from the version gate hint
- drop the auto build mode wording everywhere and sync all eleven
  language files

* feat: clarify purpose of the nginx module build drawer

- add a purpose hint explaining dynamic (hot reload) vs static (full
  rebuild + container restart)
- drop the per-module mode tags now that section headers carry the
  semantics
- allow submitting with zero dynamic modules selected when static
  modules are present, so static-only users can trigger a build

* feat: pass apt mirror through to dynamic module builds

The mirror selected in the build dialog (or CONTAINER_PACKAGE_URL in the
app env as fallback) is now forwarded as a build arg so the module
builder uses the same apt source as the static build path. test-builder
gains a --mirror option.

* feat: add Lao translations for nginx module pages
2026-07-23 13:55:42 +08:00
ssongliu f6ed11ac55 fix(database): support multiple MySQL user hosts (#13342) 2026-07-23 10:01:39 +08:00
ssongliu d330f60b2d fix: avoid duplicate disk IO aggregation (#13333) 2026-07-22 22:23:39 +08:00
CityFun 10c8cafcee chore: update dependencies (#13341) 2026-07-22 18:41:21 +08:00
ssongliu d1534c88aa fix: align dashboard column heights (#13332) 2026-07-22 14:20:58 +08:00
CityFun eb6b4c7cb7 chore: update dependencies (#13327) 2026-07-21 19:08:20 +08:00
ssongliu c77ddcfebe feat(database): improve MySQL user authorization (#13326) 2026-07-21 19:08:09 +08:00
ssongliu af12526a6c fix(container): make updates rollback-safe (#13315) 2026-07-21 15:40:07 +08:00
CityFun b347abe100 feat: Cache List Page Filter Conditions (#13314) 2026-07-21 15:39:46 +08:00
蘭 092b57413b feat(i18n): improve Lao (lo) language support (#13313) 2026-07-20 18:38:42 +08:00
ssongliu e8f35e4e06 fix: optimize terminal output performance (#13276) (#13312) 2026-07-20 18:26:00 +08:00
BoneNI b019e7526d feat: Add official Lao (lo_LA) language support (#13154)
* Add Lao language localization for lo.ts

* Add support for Lao language module

* Add Lao language translations to fu.ts

* Add Lao language support in i18n.go

* Add Lao language localization for lo.yaml
2026-07-20 17:23:23 +08:00
CityFun 1b06467521 feat: change website ssl apply logic (#13296) 2026-07-20 14:06:59 +08:00
ssongliu 7f3fd0cb1d feat: add runtime diagnostics (#13295) 2026-07-20 13:55:55 +08:00
CityFun 3e9848554b feat: update some translate (#13288) 2026-07-20 10:27:40 +08:00
ssongliu b02cdbc5f9 feat: add host system logs (#13285) 2026-07-17 17:54:36 +08:00
ssongliu d7723fd42a refactor(agent): replace onedrive sdk with graph api (#13284) 2026-07-17 13:48:04 +08:00
CityFun 8b265bb81e feat: Support fetching model lists for model accounts (#13281) 2026-07-17 13:47:49 +08:00
ssongliu 89b6c94e42 refactor(frontend): consolidate table components (#13280) 2026-07-17 10:02:40 +08:00
ssongliu 219ac160b6 feat: support mysql database users (#13278) 2026-07-16 14:24:38 +08:00
ssongliu 51ca1eab18 fix: refine virtual machine network hints (#13277) 2026-07-16 11:21:52 +08:00
ssongliu 1ab3da1fab fix: apply timeout to snapshot uploads (#13275)
* fix: apply timeout to snapshot uploads

* fix: honor snapshot upload timeouts for sftp and upyun
2026-07-16 11:21:26 +08:00
ssongliu 1d1b12c2fa fix: clean failed backup artifacts (#13274) 2026-07-15 18:06:19 +08:00
ssongliu 07a658582d fix: restore fail2ban bans after firewall restart (#13272) 2026-07-15 16:31:09 +08:00
ssongliu 61da74ae50 feat: hint file creation in protected directories (#13270) 2026-07-15 16:30:52 +08:00
王贺 90334f3cb0 fix: load SSL certificates when binding MCP website (#13271) 2026-07-15 16:08:55 +08:00
ssongliu fb9278bfc6 fix: prevent empty image import (#13269)
Refs #13228
2026-07-15 15:55:21 +08:00
ssongliu 7750936af6 feat: add node health check settings (#13268) 2026-07-15 15:55:02 +08:00
蘭 7b695af996 fix: Fix the issue of file permissions changing after decompressing files (#13267) 2026-07-15 15:54:05 +08:00
CityFun 2874849557 feat: Add Custom CLI Arguments Field for MCP Server Configuration (#13265) 2026-07-15 15:53:44 +08:00
王贺 9fd1f3e661 ci: remove disabled workflows (#13261) 2026-07-14 10:02:17 +08:00
Kobi Hikri 85182d9164 ci: pin third-party actions to full commit SHAs (#13259)
Pin the third-party actions referenced by mutable @master/@main tags to their
current commit SHA (tag kept in a trailing comment). Several run in jobs holding
secrets:

- SonarSource/sonarcloud-github-action@master (sonarcloud-scan.yml) — SONAR_TOKEN
- Yikun/hub-mirror-action@master (sync2gitee.yml) — GITEE_PRIVATE_KEY, GITEE_TOKEN
- fit2cloud/LLM-CodeReview-Action@main (llm-code-review.yml) — tokens + LLM API key
- crate-ci/typos@master (tyops-check.yml)

A moved tag would run unreviewed code with those secrets. Behaviour unchanged;
per GitHub's guidance to pin actions to a full-length commit SHA.

Signed-off-by: Kobi Hikri <kobi.hikri@gmail.com>
2026-07-14 09:58:52 +08:00
王贺 d9b2ea051f fix: resolve locale build errors (#13258) 2026-07-13 22:43:02 +08:00
ssongliu 097fe6dcfb fix: align remote download translations (#13255) 2026-07-13 18:16:58 +08:00
CityFun d5beca6d96 feat: change some translate (#13253) 2026-07-13 11:29:33 +08:00
ssongliu e3a356f893 feat: add VM orphan cleanup UI translations (#13252) 2026-07-13 10:48:51 +08:00
ssongliu 8be2a9ab02 fix: initialize enterprise database before hooks (#13247) 2026-07-10 18:31:04 +08:00
ssongliu 3d7f0f5143 fix: support cancelling chunk uploads (#13246) 2026-07-10 18:00:45 +08:00
ssongliu 1fd62fbaf7 fix: support cancelling chunk uploads (#13233)
* fix: support cancelling chunk uploads

* fix: validate chunk upload filenames
2026-07-10 15:46:59 +08:00
CityFun f1372dda85 core: Upgrade dependencies (#13231) 2026-07-09 21:38:14 +08:00
ssongliu 64c354adb2 feat: add VM overview status (#13232) 2026-07-09 21:36:48 +08:00
蘭 a379e1f840 ref: add some translate (#13225) 2026-07-09 18:32:16 +08:00
蘭 270ba799f7 feat: implement directory size caching and sorting functionality (#13220) 2026-07-09 11:11:37 +08:00
ssongliu 60dcfacb88 feat: add vm overview translations (#13224) 2026-07-09 11:11:15 +08:00
CityFun b474d720e9 style: change website page style (#13219) 2026-07-08 18:37:37 +08:00
ssongliu b2192b7252 fix: trim mariadb remote backup image tag (#13218) 2026-07-08 17:04:18 +08:00
ssongliu 6a988ad96a feat: update vm resource messages (#13216) 2026-07-08 15:01:49 +08:00
CityFun 029ac5efee feat: add some translate (#13215) 2026-07-07 18:42:55 +08:00
ssongliu 582cf1f533 feat: update vm and file resources (#13210) 2026-07-07 14:33:34 +08:00
ssongliu b8a4e8e9e0 chore: update xpack menu migration (#13207) 2026-07-06 11:14:29 +08:00
ssongliu 4a462aecc7 fix: reset firewall import dialogs on open (#13198) 2026-07-03 16:51:08 +08:00
ssongliu 74f221b895 fix: use svg renderer for container monitor charts (#13197) 2026-07-03 16:50:47 +08:00
ssongliu cbcb628bff fix: normalize firewall import addresses (#13196) 2026-07-03 16:37:03 +08:00
蘭 f602645e31 feat: Implement code editor layout with splitter for improved UI (#13195) 2026-07-03 16:01:44 +08:00
CityFun ec9e609f10 feat: add some translate (#13194) 2026-07-03 15:59:35 +08:00
ssongliu 1c1f3c7761 feat: support importing all firewall rules (#13192)
* feat: support importing all firewall rules

* fix: preserve selected firewall import action
2026-07-03 15:59:22 +08:00
ssongliu fae49284b7 fix: support mariadb manual backup args (#13193) 2026-07-03 15:58:15 +08:00
蘭 072a608a63 feat: File editor supports right-click operation (#13191) 2026-07-03 15:04:04 +08:00
蘭 1e6fd9c3a0 ref: Optimize menu settings (#13190) 2026-07-03 12:10:47 +08:00
ssongliu 23200cbe85 fix: support mariadb cronjob backup args (#13188) 2026-07-03 11:41:52 +08:00
ssongliu 08604e8c02 fix: fix dashboard running time calculation (#13187) 2026-07-03 11:41:37 +08:00
蘭 62a472d2d1 ref: refactor code editor theme management (#13186) 2026-07-03 11:41:22 +08:00
ssongliu 2c84665e8b chore: organize i18n language files (#13183) 2026-07-02 21:13:22 +08:00
蘭 616bd6830c feat: add menu accordion setting and update related components (#13182) 2026-07-02 21:13:07 +08:00
ssongliu e55ea96f00 feat: support virtual machine (#13171)
* feat: support virtual machine

* feat: add vm operation logs
2026-07-02 15:07:48 +08:00
蘭 aa676e139c feat: implement code editor theme management and synchronization (#13169) 2026-07-02 14:49:52 +08:00
ssongliu 7708fea58d feat: update operation log content (#13166) 2026-07-01 18:14:33 +08:00
ssongliu 34bdff01ad fix: allow ssl push on selected node (#13165) 2026-07-01 17:04:00 +08:00
CityFun 5a49579ddf feat: Optimize error messages for HTTP certificate requests (#13164) 2026-07-01 16:19:57 +08:00
CityFun b98a2b06c9 feat: Add multiple dimensions to website search (#13163) 2026-07-01 16:19:43 +08:00
ssongliu 9073b16baa feat: support compose rebuild operation (#13162) 2026-07-01 16:19:25 +08:00
ssongliu 2ec4db8d7e feat: adjust batch application installation API (#13161) 2026-07-01 16:19:12 +08:00
CityFun fe9628f91c feat: Add TaskLog for MCP Servers (#13160) 2026-07-01 16:18:59 +08:00
蘭 18a469e4be fix: Enhance Python language support in Monaco editor (#13158) 2026-07-01 16:18:42 +08:00
蘭 8c691cc074 fix: Resolve the issue of inability to access mobile web login (#13157) 2026-07-01 16:18:26 +08:00
ssongliu 2cd31a47f8 fix: complete operation logs (#13155) 2026-06-30 22:22:20 +08:00
CityFun d97aa9c2a0 feat: Optimize SSH session performance on Ubuntu 25. (#13152)
* feat: Optimize SSH session performance on Ubuntu 25.

* feat: Optimize SSH session performance on Ubuntu 25.

* feat: Optimize SSH session performance on Ubuntu 25.

* feat: Optimize SSH session performance on Ubuntu 25.
2026-06-30 18:36:28 +08:00
ssongliu f31f0a89ff feat: support certificate synchronization (#13153) 2026-06-30 18:31:45 +08:00
CityFun 0a1621166e feat: Unify the default website and default site under the same button. (#13149) 2026-06-30 15:56:25 +08:00
蘭 8623aa5318 feat: Enhance API authentication with HMAC-SHA256 support and user role management (#13148) 2026-06-30 14:16:36 +08:00
CityFun ec50599526 feat: Node runtime environment supports configuring whether to install node_modules. (#13147) 2026-06-30 14:16:22 +08:00
CityFun 458a30c520 feat: Add a description for syncing WAF IP groups. (#13143) 2026-06-29 18:42:02 +08:00
Andrea Leone 9a660db4c3 feat: add Ionos DNS Provider (#13024)
* feat: add Ionos DNS Provider

* Fix typo string formatting for APIKey in Ionos config
2026-06-29 16:11:33 +08:00
Reza Alipour Kondori f4fc71d32b add some translate and update Persian (#13132)
* Add Persian (fa) translations

* Update index.ts

Add fa to LOCALE_LOADERS

* Update fu.ts

Add fa item

* Update index.vue

enable Persian (fa) in language selector

* Update login-form.vue

enable Persian (fa) in language selector

* Create fa.yaml

add fa backend translations

* Update i18n.go

Add fa to langFiles variable

* Add Persian (fa) Translation to agent

* Add Persian README file

Add Persian (fa) README.fa.md to project

* fix(i18n): add Persian (fa) to backend validation and login dropdown

* Update README.fa.md

* Upade link in readme
2026-06-29 15:25:56 +08:00
王贺 e45a8185e3 fix container upgrade network IP handling (#13126) 2026-06-26 18:38:11 +08:00
CityFun 7be9189b3c fix: Fixed issue with reload ssl failed (#13129) 2026-06-26 18:08:38 +08:00
蘭 bdf13c01f6 ref: Improve support for Persian (fa) language (#13128) 2026-06-26 18:08:22 +08:00
王贺 902bc90aef fix: Fix updating the application host binding IP (#13127) 2026-06-26 18:08:08 +08:00
Octopusandocto-patch 27ba50d8be feat: add MiniMax M3 model to provider catalog (#13119)
Add the latest MiniMax-M3 flagship model to the MiniMax provider model
list and set it as the default. M3 offers a 1M context window, 128K max
output, reasoning, and image input. Drop the older M2.5 entries while
keeping M2.7 as an alternative, and update the account verification probe
to use M3.

Co-authored-by: octo-patch <266937838+octo-patch@users.noreply.github.com>
2026-06-26 17:55:04 +08:00
Reza Alipour Kondori fb483ecb90 Add Persian (fa) translations (#13100)
* Add Persian (fa) translations

* Update index.ts

Add fa to LOCALE_LOADERS

* Update fu.ts

Add fa item

* Update index.vue

enable Persian (fa) in language selector

* Update login-form.vue

enable Persian (fa) in language selector

* Create fa.yaml

add fa backend translations

* Update i18n.go

Add fa to langFiles variable

* Add Persian (fa) Translation to agent
2026-06-26 17:17:32 +08:00
蘭 2de7079dd2 feat: add new localization strings for skill management features (#13124) 2026-06-26 16:17:57 +08:00
CityFun 2e4ac604a8 fix: Fix incomplete log download issue (#13123) 2026-06-26 16:17:40 +08:00
CityFun 46af80142e feat: add username/password support for hermes-agent (#13121)
* feat: add username/password support for hermes-agent

* feat: add username/password support for hermes-agent

* feat: add username/password support for hermes-agent
2026-06-26 16:17:23 +08:00
CityFun 677b47dcdb fix: Fixed issue with openclaw config telegram failed (#13114) 2026-06-25 18:40:34 +08:00
王贺 56cccdf7c0 refactor: remove skills hub server leftovers (#13115) 2026-06-25 15:09:21 +08:00
蘭 3e0d0bb809 feat: add quick toggle comment functionality in the editor (#13107) 2026-06-24 18:23:51 +08:00
ssongliu 86aa6c09c6 fix: resolve snapshot extraction directory missing issue (#13104) 2026-06-24 18:23:37 +08:00
蘭 f67e1d897c ref: add some translate (#13102) 2026-06-24 18:23:23 +08:00
蘭 9e518daa62 ref: add some translate (#13093) 2026-06-23 14:05:48 +08:00
CityFun 2e5455e655 feat: Add “Remove old images during app upgrade” configuration to the App Store. (#13092) 2026-06-23 14:05:33 +08:00
CityFun 868f1db4c5 feat: Create and delete runtime environments, and add task logs. (#13081) 2026-06-18 18:46:32 +08:00
王贺 efd84b1e1b feat: support skills hub server frontend (#13074) 2026-06-18 16:57:53 +08:00
CityFun 5591fe50f0 chore: optimize runtime environment container configuration (#13071)
* chore: optimize runtime environment container configuration

* chore: optimize runtime environment container configuration
2026-06-17 19:09:31 +08:00
王贺 5e0a598799 feat: update waf prompt translations (#13070) 2026-06-17 17:00:28 +08:00
CityFun 45d7b30abc feat: Add support for syncing self-signed certificates and manually u… (#13068)
* feat: Add support for syncing self-signed certificates and manually uploaded certificates to other nodes.

* feat: Add support for syncing self-signed certificates and manually
2026-06-17 17:00:15 +08:00
ssongliu 46af88e1d0 feat: support app and image synchronization (#13067) 2026-06-17 15:41:09 +08:00
CityFun 7f331f5503 fix: 解决证书推送到其他节点报错的问题 (#13065) 2026-06-17 15:31:51 +08:00
igophper 7b74e09d30 fix: manage expanded node states in the tree view component (#13064) 2026-06-17 12:36:23 +08:00
蘭 01e4455110 refactor: Remove error logging for missing alert config in alert handling (#13060) 2026-06-17 11:08:45 +08:00
CityFun 922a2d21c8 feat: add protocolVersion param for MCP Server (#13057)
* feat: add protocolVersion param for MCP Server

* feat: add protocolVersion param for MCP Server
2026-06-16 16:47:27 +08:00
CityFun 439c4794b7 feat: Optimize the vLLM display (#13056) 2026-06-15 16:42:00 +08:00
ssongliu 5ac7f16b67 feat: optimize image deletion logs (#13053) 2026-06-15 15:41:46 +08:00
蘭 bfb79066b4 fix: Adjust the default values of alert attributes (#13052) 2026-06-15 15:41:33 +08:00
蘭 19602b42fb feat: add QR code verification endpoint to password expiration checks (#13049) 2026-06-15 15:15:09 +08:00
ssongliu f987af264f feat: disable script library auto-sync on startup (#13047) 2026-06-15 13:46:24 +08:00
ssongliu a6c76dffc3 feat: add snapshot rollback prompt (#13045) 2026-06-15 13:46:13 +08:00
CityFun a2eed66bc4 fix: fixed issue with refresh agent page failed (#13035) 2026-06-12 18:14:35 +08:00
CityFun a49b9b132d feat: update app upgrade logic (#13032) 2026-06-12 17:44:34 +08:00
ssongliu e8505a249a fix: resolve password expiration issue in multi-user mode (#13030) 2026-06-12 14:16:16 +08:00
421 changed files with 48880 additions and 67315 deletions
-25
View File
@@ -1,25 +0,0 @@
name: LLM Code Review
permissions:
contents: read
pull-requests: write
on:
pull_request:
types: [opened, reopened, synchronize]
jobs:
llm-code-review:
runs-on: ubuntu-latest
steps:
- uses: fit2cloud/LLM-CodeReview-Action@main
env:
GITHUB_TOKEN: ${{ secrets.FIT2CLOUDRD_LLM_CODE_REVIEW_TOKEN }}
OPENAI_API_KEY: ${{ secrets.ALIYUN_LLM_API_KEY }}
LANGUAGE: English
OPENAI_API_ENDPOINT: https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL: qwen2.5-coder-3b-instruct
PROMPT: "Please check the following code differences for any irregularities, potential issues, or optimization suggestions, and provide your answers in English."
top_p: 1
temperature: 1
# max_tokens: 10000
MAX_PATCH_LENGTH: 10000
IGNORE_PATTERNS: "/node_modules,*.md,/dist,/.github"
FILE_PATTERNS: "*.java,*.go,*.py,*.vue,*.ts,*.js,*.css,*.scss,*.html"
-21
View File
@@ -1,21 +0,0 @@
name: SonarCloud Scan
on:
push:
branches:
- dev
pull_request:
types: [opened, synchronize, reopened]
jobs:
sonarcloud:
name: SonarCloud
if: github.repository == '1Panel-dev/1Panel'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUBTOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Mirror the Github organization repos to Gitee. - name: Mirror the Github organization repos to Gitee.
uses: Yikun/hub-mirror-action@master uses: Yikun/hub-mirror-action@ba51c01b28a6c9f95a25d4f1bcf6af2a147c0e18 # master
with: with:
src: 'github/1Panel-dev' src: 'github/1Panel-dev'
dst: 'gitee/fit2cloud-feizhiyun' dst: 'gitee/fit2cloud-feizhiyun'
-11
View File
@@ -1,11 +0,0 @@
name: Typos Check
on: pull_request
jobs:
run:
name: Spell Check with Typos
runs-on: ubuntu-latest
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v2
- name: Check spelling
uses: crate-ci/typos@master
+3 -1
View File
@@ -25,7 +25,7 @@
<a href="/docs/README.pt-br.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-d9d9d9"></a> <a href="/docs/README.pt-br.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-d9d9d9"></a>
<a href="/docs/README.ar.md"><img alt="العربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a> <a href="/docs/README.ar.md"><img alt="العربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
<a href="/docs/README.de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-d9d9d9"></a> <a href="/docs/README.de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-d9d9d9"></a>
<a href="/docs/README.es.md"><img alt="Español" src="https://img.shields.io/badge/Español-d9d9d9"></a> <a href="/docs/README.es-es.md"><img alt="Español" src="https://img.shields.io/badge/Español-d9d9d9"></a>
<a href="/docs/README.fr.md"><img alt="français" src="https://img.shields.io/badge/français-d9d9d9"></a> <a href="/docs/README.fr.md"><img alt="français" src="https://img.shields.io/badge/français-d9d9d9"></a>
<a href="/docs/README.ko.md"><img alt="한국어" src="https://img.shields.io/badge/한국어-d9d9d9"></a> <a href="/docs/README.ko.md"><img alt="한국어" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
<a href="/docs/README.id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-d9d9d9"></a> <a href="/docs/README.id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-d9d9d9"></a>
@@ -33,6 +33,8 @@
<a href="/docs/README.tr.md"><img alt="Türkçe" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a> <a href="/docs/README.tr.md"><img alt="Türkçe" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
<a href="/docs/README.ru.md"><img alt="Русский" src="https://img.shields.io/badge/Русский-d9d9d9"></a> <a href="/docs/README.ru.md"><img alt="Русский" src="https://img.shields.io/badge/Русский-d9d9d9"></a>
<a href="/docs/README.ms.md"><img alt="Bahasa Melayu" src="https://img.shields.io/badge/Bahasa Melayu-d9d9d9"></a> <a href="/docs/README.ms.md"><img alt="Bahasa Melayu" src="https://img.shields.io/badge/Bahasa Melayu-d9d9d9"></a>
<a href="/docs/README.fa.md"><img alt="Persian" src="https://img.shields.io/badge/%D9%81%D8%A7%D8%B1%D8%B3%DB%8C-d9d9d9"></a>
<a href="/docs/README.lo.md"><img alt="ພາສາລາວ" src="https://img.shields.io/badge/%E0%BA%9E%E0%BA%B2%E0%BA%AA%E0%BA%B2%E0%BA%A5%E0%BA%B2%E0%BA%A7-d9d9d9"></a>
</p> </p>
--- ---
+21
View File
@@ -500,6 +500,27 @@ func (b *BaseApi) GetAgentAccountModels(c *gin.Context) {
helper.SuccessWithData(c, list) helper.SuccessWithData(c, list)
} }
// @Tags AI
// @Summary Discover custom provider models
// @Accept json
// @Param request body dto.AgentAccountModelDiscoverReq true "request"
// @Success 200 {array} dto.AgentAccountModel
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/accounts/models/discover [post]
func (b *BaseApi) DiscoverAgentAccountModels(c *gin.Context) {
var req dto.AgentAccountModelDiscoverReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
list, err := agentService.DiscoverAccountModels(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, list)
}
// @Tags AI // @Tags AI
// @Summary Create model account model // @Summary Create model account model
// @Accept json // @Accept json
+6 -1
View File
@@ -413,10 +413,15 @@ func (b *BaseApi) Backup(c *gin.Context) {
switch req.Type { switch req.Type {
case "app": case "app":
if _, err := backupService.AppBackup(req); err != nil { record, err := backupService.AppBackup(req)
if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
if req.IsImmediate {
helper.SuccessWithData(c, record)
return
}
case "mysql", "mariadb", constant.AppMysqlCluster: case "mysql", "mariadb", constant.AppMysqlCluster:
if err := backupService.MysqlBackup(req); err != nil { if err := backupService.MysqlBackup(req); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
+215 -12
View File
@@ -41,20 +41,40 @@ func (b *BaseApi) CreateMysql(c *gin.Context) {
} }
// @Tags Database Mysql // @Tags Database Mysql
// @Summary Bind user of mysql database // @Summary List mysql users
// @Accept json // @Accept json
// @Param request body dto.BindUser true "request" // @Param request body dto.MysqlUserSearch true "request"
// @Success 200 // @Success 200 {array} dto.MysqlUser
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /databases/bind [post] // @Router /databases/users/search [post]
// @x-panel-log {"bodyKeys":["database", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"绑定 mysql 数据库名 [database] [username]","formatEN":"bind mysql database [database] [username]"} func (b *BaseApi) ListMysqlUsers(c *gin.Context) {
func (b *BaseApi) BindUser(c *gin.Context) { var req dto.MysqlUserSearch
var req dto.BindUser
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
data, err := mysqlService.ListUsers(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Database Mysql
// @Summary Create mysql user
// @Accept json
// @Param request body dto.MysqlUserCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users [post]
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 mysql 数据库 [database] 用户 [username]@[host]","formatEN":"create mysql database [database] user [username]@[host]"}
func (b *BaseApi) CreateMysqlUser(c *gin.Context) {
var req dto.MysqlUserCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if len(req.Password) != 0 { if len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password) password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil { if err != nil {
@@ -63,8 +83,191 @@ func (b *BaseApi) BindUser(c *gin.Context) {
} }
req.Password = string(password) req.Password = string(password)
} }
if err := mysqlService.CreateUser(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
if err := mysqlService.BindUser(req); err != nil { // @Tags Database Mysql
// @Summary Delete mysql user
// @Accept json
// @Param request body dto.MysqlUserDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/del [post]
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 mysql 数据库 [database] 用户 [username]@[host]","formatEN":"delete mysql database [database] user [username]@[host]"}
func (b *BaseApi) DeleteMysqlUser(c *gin.Context) {
var req dto.MysqlUserDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := mysqlService.DeleteUser(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Update mysql user
// @Accept json
// @Param request body dto.MysqlUserUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/update [post]
// @x-panel-log {"bodyKeys":["database","username","host","newHost","description"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]","formatEN":"update mysql database [database] user [username] access [host] => [newHost] description [description]"}
func (b *BaseApi) UpdateMysqlUser(c *gin.Context) {
var req dto.MysqlUserUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := mysqlService.UpdateUser(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Change mysql user password
// @Accept json
// @Param request body dto.MysqlUserPassword true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/password [post]
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mysql 数据库 [database] 用户 [username]@[host] 密码","formatEN":"update mysql database [database] user [username]@[host] password"}
func (b *BaseApi) ChangeMysqlUserPassword(c *gin.Context) {
var req dto.MysqlUserPassword
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
helper.BadRequest(c, err)
return
}
req.Password = string(password)
}
if err := mysqlService.ChangeUserPassword(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Save mysql user password locally
// @Accept json
// @Param request body dto.MysqlUserPassword true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/password/save [post]
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"补充 mysql 数据库 [database] 用户 [username]@[host] 密码","formatEN":"save mysql database [database] user [username]@[host] password locally"}
func (b *BaseApi) SaveMysqlUserPassword(c *gin.Context) {
var req dto.MysqlUserPassword
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
helper.BadRequest(c, err)
return
}
req.Password = string(password)
}
if err := mysqlService.SaveUserPassword(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary List mysql grants
// @Accept json
// @Param request body dto.MysqlUserSearch true "request"
// @Success 200 {array} dto.MysqlGrant
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/grants/search [post]
func (b *BaseApi) ListMysqlGrants(c *gin.Context) {
var req dto.MysqlUserSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := mysqlService.ListGrants(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Database Mysql
// @Summary List mysql grant summary
// @Accept json
// @Param request body dto.MysqlGrantSummarySearch true "request"
// @Success 200 {object} map[string][]dto.MysqlUser
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/grants/summary [post]
func (b *BaseApi) ListMysqlGrantSummary(c *gin.Context) {
var req dto.MysqlGrantSummarySearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := mysqlService.ListGrantSummary(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Database Mysql
// @Summary Grant mysql user
// @Accept json
// @Param request body dto.MysqlGrantCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/grants [post]
// @x-panel-log {"bodyKeys":["database","db","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"授权 mysql 数据库 [database] 用户 [username]@[host] 访问 [db]","formatEN":"grant mysql database [database] user [username]@[host] access to [db]"}
func (b *BaseApi) GrantMysqlUser(c *gin.Context) {
var req dto.MysqlGrantCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := mysqlService.GrantUser(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Revoke mysql grant
// @Accept json
// @Param request body dto.MysqlGrantDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/grants/del [post]
// @x-panel-log {"bodyKeys":["database","db","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"取消 mysql 数据库 [database] 用户 [username]@[host] 对 [db] 的授权","formatEN":"revoke mysql database [database] user [username]@[host] access to [db]"}
func (b *BaseApi) RevokeMysqlGrant(c *gin.Context) {
var req dto.MysqlGrantDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := mysqlService.RevokeGrant(req); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
@@ -94,14 +297,14 @@ func (b *BaseApi) UpdateMysqlDescription(c *gin.Context) {
} }
// @Tags Database Mysql // @Tags Database Mysql
// @Summary Change mysql password // @Summary Change mysql root password
// @Accept json // @Accept json
// @Param request body dto.ChangeDBInfo true "request" // @Param request body dto.ChangeDBInfo true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /databases/change/password [post] // @Router /databases/change/password [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"更新数据库 [name] 密码","formatEN":"Update database [name] password"} // @x-panel-log {"bodyKeys":["database"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新数据库 [database] root 密码","formatEN":"Update database [database] root password"}
func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) { func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) {
var req dto.ChangeDBInfo var req dto.ChangeDBInfo
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -125,14 +328,14 @@ func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) {
} }
// @Tags Database Mysql // @Tags Database Mysql
// @Summary Change mysql access // @Summary Change mysql root access
// @Accept json // @Accept json
// @Param request body dto.ChangeDBInfo true "request" // @Param request body dto.ChangeDBInfo true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /databases/change/access [post] // @Router /databases/change/access [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"更新数据库 [name] 访问权限","formatEN":"Update database [name] access"} // @x-panel-log {"bodyKeys":["database"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新数据库 [database] root 访问权限","formatEN":"Update database [database] root access"}
func (b *BaseApi) ChangeMysqlAccess(c *gin.Context) { func (b *BaseApi) ChangeMysqlAccess(c *gin.Context) {
var req dto.ChangeDBInfo var req dto.ChangeDBInfo
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
+9 -8
View File
@@ -37,14 +37,15 @@ var (
cronjobService = service.NewICronjobService() cronjobService = service.NewICronjobService()
fileService = service.NewIFileService() fileService = service.NewIFileService()
fileHistoryService = service.NewIFileHistoryService() fileHistoryService = service.NewIFileHistoryService()
fileShareService = service.NewIFileShareService() fileShareService = service.NewIFileShareService()
sshService = service.NewISSHService() sshService = service.NewISSHService()
firewallService = service.NewIFirewallService() firewallService = service.NewIFirewallService()
iptablesService = service.NewIIptablesService() iptablesService = service.NewIIptablesService()
monitorService = service.NewIMonitorService() monitorService = service.NewIMonitorService()
systemService = service.NewISystemService() systemService = service.NewISystemService()
runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService()
deviceService = service.NewIDeviceService() deviceService = service.NewIDeviceService()
fail2banService = service.NewIFail2BanService() fail2banService = service.NewIFail2BanService()
+69 -2
View File
@@ -12,7 +12,9 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync"
"syscall" "syscall"
"time"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -28,6 +30,11 @@ import (
qrcode "github.com/skip2/go-qrcode" qrcode "github.com/skip2/go-qrcode"
) )
var cancelledChunkUploads = struct {
sync.RWMutex
ids map[string]struct{}
}{ids: make(map[string]struct{})}
// @Tags File // @Tags File
// @Summary List files // @Summary List files
// @Accept json // @Accept json
@@ -873,7 +880,24 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
} }
} }
filename := c.PostForm("filename") filename := c.PostForm("filename")
fileDir := filepath.Join(tmpDir, filename) if filename == "" || filepath.Base(filename) != filename || strings.ContainsAny(filename, `/\\`) {
helper.BadRequest(c, errors.New("invalid filename"))
return
}
uploadID := strings.TrimSpace(c.PostForm("uploadID"))
cancellable := uploadID != ""
if cancellable && (filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`)) {
helper.BadRequest(c, errors.New("invalid upload ID"))
return
}
if !cancellable {
uploadID = filename
}
fileDir := filepath.Join(tmpDir, uploadID)
if cancellable && chunkUploadCancelled(uploadID) {
helper.BadRequest(c, errors.New("upload cancelled"))
return
}
if chunkIndex == 0 { if chunkIndex == 0 {
if fileOp.Stat(fileDir) { if fileOp.Stat(fileDir) {
_ = fileOp.DeleteDir(fileDir) _ = fileOp.DeleteDir(fileDir)
@@ -884,7 +908,7 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
defer func() { defer func() {
if err != nil { if err != nil {
_ = os.Remove(fileDir) _ = os.RemoveAll(fileDir)
} }
}() }()
var ( var (
@@ -904,6 +928,11 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err)) helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
return return
} }
if cancellable && chunkUploadCancelled(uploadID) {
err = errors.New("upload cancelled")
helper.BadRequest(c, err)
return
}
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex)) chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
err = os.WriteFile(chunkPath, chunkData, constant.DirPerm) err = os.WriteFile(chunkPath, chunkData, constant.DirPerm)
@@ -922,12 +951,50 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err)) helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
return return
} }
if cancellable {
cancelledChunkUploads.Lock()
delete(cancelledChunkUploads.ids, uploadID)
cancelledChunkUploads.Unlock()
}
helper.SuccessWithData(c, true) helper.SuccessWithData(c, true)
} else { } else {
return return
} }
} }
// StopChunkUpload removes temporary chunks left by a cancelled upload.
func (b *BaseApi) StopChunkUpload(c *gin.Context) {
var req request.FileProcessReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
uploadID := strings.TrimSpace(req.Key)
if uploadID == "" || filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`) {
helper.BadRequest(c, errors.New("invalid upload ID"))
return
}
cancelledChunkUploads.Lock()
cancelledChunkUploads.ids[uploadID] = struct{}{}
cancelledChunkUploads.Unlock()
time.AfterFunc(10*time.Minute, func() {
cancelledChunkUploads.Lock()
delete(cancelledChunkUploads.ids, uploadID)
cancelledChunkUploads.Unlock()
})
if err := os.RemoveAll(filepath.Join(global.Dir.TmpDir, "upload", uploadID)); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
func chunkUploadCancelled(uploadID string) bool {
cancelledChunkUploads.RLock()
_, ok := cancelledChunkUploads.ids[uploadID]
cancelledChunkUploads.RUnlock()
return ok
}
var wsUpgrade = websocket.Upgrader{ var wsUpgrade = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { CheckOrigin: func(r *http.Request) bool {
return true return true
+55
View File
@@ -2,6 +2,7 @@ package v2
import ( import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -20,3 +21,57 @@ func (b *BaseApi) GetSystemFiles(c *gin.Context) {
helper.SuccessWithData(c, data) helper.SuccessWithData(c, data)
} }
// @Tags Logs
// @Summary Get host system log status
// @Produce json
// @Success 200 {object} dto.SystemLogStatus
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /logs/system/status [get]
func (b *BaseApi) GetSystemLogStatus(c *gin.Context) {
data, err := logService.GetSystemLogStatus()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Logs
// @Summary Read host logs
// @Accept json
// @Param request body dto.SystemLogReq true "request"
// @Produce json
// @Success 200 {object} dto.SystemLogRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /logs/system/read [post]
func (b *BaseApi) ReadSystemLog(c *gin.Context) {
var req dto.SystemLogReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := logService.ReadSystemLog(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Logs
// @Summary List running host services
// @Produce json
// @Success 200 {array} string
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /logs/system/services [get]
func (b *BaseApi) ListRunningServices(c *gin.Context) {
data, err := logService.ListRunningServices()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
+63
View File
@@ -23,6 +23,27 @@ func (b *BaseApi) PageMcpServers(c *gin.Context) {
helper.SuccessWithData(c, list) helper.SuccessWithData(c, list)
} }
// @Tags McpServer
// @Summary Load mcp server detail
// @Accept json
// @Param request body request.McpServerDetail true "request"
// @Success 200 {object} response.McpServerDTO
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/mcp/server/detail [post]
func (b *BaseApi) LoadMcpServerDetail(c *gin.Context) {
var req request.McpServerDetail
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := mcpServerService.Detail(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags McpServer // @Tags McpServer
// @Summary Create mcp server // @Summary Create mcp server
// @Accept json // @Accept json
@@ -107,6 +128,48 @@ func (b *BaseApi) OperateMcpServer(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
// @Tags McpServer
// @Summary Sync mcp server status
// @Accept json
// @Param request body request.McpServerStatusSync true "request"
// @Success 200 {array} response.McpServerStatusDTO
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/mcp/server/status/sync [post]
func (b *BaseApi) SyncMcpServerStatus(c *gin.Context) {
var req request.McpServerStatusSync
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := mcpServerService.SyncStatus(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags McpServer
// @Summary Test mcp server connection
// @Accept json
// @Param request body request.McpServerConnectionTest true "request"
// @Success 200 {object} response.McpServerConnectionTestRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/mcp/server/connection/test [post]
func (b *BaseApi) TestMcpServerConnection(c *gin.Context) {
var req request.McpServerConnectionTest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := mcpServerService.TestConnection(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags McpServer // @Tags McpServer
// @Summary Bind Domain for mcp server // @Summary Bind Domain for mcp server
// @Accept json // @Accept json
+63
View File
@@ -0,0 +1,63 @@
package v2
import (
"os"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)
// @Tags RuntimeDiagnostics
// @Summary Load runtime diagnostics summary
// @Success 200 {object} dto.RuntimeDiagnosticsSummary
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/diagnostics/summary [get]
func (b *BaseApi) LoadRuntimeDiagnosticsSummary(c *gin.Context) {
data, err := runtimeDiagnosticsService.Summary()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags RuntimeDiagnostics
// @Summary Load grouped goroutine snapshot
// @Success 200 {object} dto.RuntimeGoroutineSnapshot
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/diagnostics/goroutines [get]
func (b *BaseApi) LoadRuntimeGoroutines(c *gin.Context) {
data, err := runtimeDiagnosticsService.Goroutines()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithDataGzipped(c, data)
}
// @Tags RuntimeDiagnostics
// @Summary Capture runtime profile
// @Param request body dto.RuntimeProfileCreate true "request"
// @Success 200 {file} file
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/diagnostics/profiles [post]
func (b *BaseApi) CreateRuntimeProfile(c *gin.Context) {
var req dto.RuntimeProfileCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
profile, err := runtimeDiagnosticsService.CreateProfile(req)
if err != nil {
helper.BadRequest(c, err)
return
}
defer os.Remove(profile.Path)
c.Header("Content-Disposition", `attachment; filename="`+profile.Name+`"`)
c.Header("Content-Type", "application/octet-stream")
c.File(profile.Path)
c.Abort()
}
+15
View File
@@ -224,6 +224,21 @@ func (b *BaseApi) ExportSSHLogs(c *gin.Context) {
helper.SuccessWithData(c, tmpFile) helper.SuccessWithData(c, tmpFile)
} }
// @Tags SSH
// @Summary Clean host SSH logs
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/ssh/log/clean [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 SSH 登录日志","formatEN":"clean SSH login logs"}
func (b *BaseApi) CleanSSHLogs(c *gin.Context) {
if err := sshService.CleanLog(); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags SSH // @Tags SSH
// @Summary Load host SSH conf // @Summary Load host SSH conf
// @Accept json // @Accept json
+25
View File
@@ -210,6 +210,27 @@ func (b *BaseApi) UpdateWebsiteSSL(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
// @Tags Website SSL
// @Summary Push ssl to nodes
// @Accept json
// @Param request body request.WebsiteSSLPush true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/ssl/push [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"推送证书到节点 [domain]","formatEN":"Push ssl to nodes [domain]"}
func (b *BaseApi) PushWebsiteSSLToNode(c *gin.Context) {
var req request.WebsiteSSLPush
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteSSLService.PushToNode(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website SSL // @Tags Website SSL
// @Summary Upload ssl // @Summary Upload ssl
// @Accept json // @Accept json
@@ -248,6 +269,8 @@ func (b *BaseApi) UploadSSLFile(c *gin.Context) {
var req request.WebsiteSSLFileUpload var req request.WebsiteSSLFileUpload
req.Description = c.PostForm("description") req.Description = c.PostForm("description")
req.Nodes = c.PostForm("nodes")
req.PushNode, _ = strconv.ParseBool(c.PostForm("pushNode"))
sslID := c.PostForm("sslID") sslID := c.PostForm("sslID")
if sslID != "" { if sslID != "" {
req.SSLID, _ = strconv.ParseUint(sslID, 10, 64) req.SSLID, _ = strconv.ParseUint(sslID, 10, 64)
@@ -283,6 +306,8 @@ func (b *BaseApi) UploadSSLFile(c *gin.Context) {
Certificate: string(certificateContent), Certificate: string(certificateContent),
Description: req.Description, Description: req.Description,
SSLID: uint(req.SSLID), SSLID: uint(req.SSLID),
PushNode: req.PushNode,
Nodes: req.Nodes,
} }
if err := websiteSSLService.Upload(uploadReq); err != nil { if err := websiteSSLService.Upload(uploadReq); err != nil {
+75 -54
View File
@@ -3,28 +3,30 @@ package dto
import "time" import "time"
type AgentCreateReq struct { type AgentCreateReq struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Remark string `json:"remark"` Remark string `json:"remark"`
AppVersion string `json:"appVersion" validate:"required"` AppVersion string `json:"appVersion" validate:"required"`
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"` WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
BridgePort int `json:"bridgePort"` BridgePort int `json:"bridgePort"`
AllowedOrigins []string `json:"allowedOrigins"` AllowedOrigins []string `json:"allowedOrigins"`
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"` AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
Model string `json:"model"` Model string `json:"model"`
AccountID uint `json:"accountId"` AccountID uint `json:"accountId"`
Token string `json:"token"` Token string `json:"token"`
TaskID string `json:"taskID"` DashboardUsername string `json:"dashboardUsername"`
Advanced bool `json:"advanced"` DashboardPassword string `json:"dashboardPassword"`
ContainerName string `json:"containerName"` TaskID string `json:"taskID"`
AllowPort bool `json:"allowPort"` Advanced bool `json:"advanced"`
SpecifyIP string `json:"specifyIP"` ContainerName string `json:"containerName"`
RestartPolicy string `json:"restartPolicy"` AllowPort bool `json:"allowPort"`
CpuQuota float64 `json:"cpuQuota"` SpecifyIP string `json:"specifyIP"`
MemoryLimit float64 `json:"memoryLimit"` RestartPolicy string `json:"restartPolicy"`
MemoryUnit string `json:"memoryUnit"` CpuQuota float64 `json:"cpuQuota"`
PullImage bool `json:"pullImage"` MemoryLimit float64 `json:"memoryLimit"`
EditCompose bool `json:"editCompose"` MemoryUnit string `json:"memoryUnit"`
DockerCompose string `json:"dockerCompose"` PullImage bool `json:"pullImage"`
EditCompose bool `json:"editCompose"`
DockerCompose string `json:"dockerCompose"`
} }
type AgentBatchInstallReq struct { type AgentBatchInstallReq struct {
@@ -38,6 +40,8 @@ type AgentBatchInstallReq struct {
Model string `json:"model"` Model string `json:"model"`
AccountID uint `json:"accountId"` AccountID uint `json:"accountId"`
Token string `json:"token"` Token string `json:"token"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
Advanced bool `json:"advanced"` Advanced bool `json:"advanced"`
ContainerName string `json:"containerName"` ContainerName string `json:"containerName"`
@@ -114,11 +118,11 @@ type AgentItem struct {
ProviderName string `json:"providerName"` ProviderName string `json:"providerName"`
Model string `json:"model"` Model string `json:"model"`
APIType string `json:"apiType"` APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
APIKey string `json:"apiKey"` APIKey string `json:"apiKey"`
Token string `json:"token"` Token string `json:"token"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
Status string `json:"status"` Status string `json:"status"`
Message string `json:"message"` Message string `json:"message"`
AppInstallID uint `json:"appInstallId"` AppInstallID uint `json:"appInstallId"`
@@ -286,19 +290,22 @@ type AgentOverviewSnapshot struct {
} }
type AgentAccountModel struct { type AgentAccountModel struct {
RecordID uint `json:"recordId"` RecordID uint `json:"recordId"`
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"`
Input []string `json:"input"`
} }
type AgentAccountModelReq struct { type AgentAccountModelReq struct {
AccountID uint `json:"accountId" validate:"required"` AccountID uint `json:"accountId" validate:"required"`
} }
type AgentAccountModelDiscoverReq struct {
Provider string `json:"provider" validate:"required"`
BaseURL string `json:"baseURL" validate:"required"`
APIKey string `json:"apiKey" validate:"required"`
APIType string `json:"apiType" validate:"required"`
}
type AgentAccountModelCreateReq struct { type AgentAccountModelCreateReq struct {
AccountID uint `json:"accountId" validate:"required"` AccountID uint `json:"accountId" validate:"required"`
Model AgentAccountModel `json:"model" validate:"required"` Model AgentAccountModel `json:"model" validate:"required"`
@@ -322,6 +329,8 @@ type AgentAccountCreateReq struct {
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"`
VerifyModel string `json:"verifyModel"`
Remark string `json:"remark"` Remark string `json:"remark"`
} }
@@ -332,6 +341,8 @@ type AgentAccountUpdateReq struct {
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"`
VerifyModel string `json:"verifyModel"`
Remark string `json:"remark"` Remark string `json:"remark"`
SyncAgents bool `json:"syncAgents"` SyncAgents bool `json:"syncAgents"`
} }
@@ -340,6 +351,9 @@ type AgentAccountVerifyReq struct {
Provider string `json:"provider" validate:"required"` Provider string `json:"provider" validate:"required"`
APIKey string `json:"apiKey" validate:"required"` APIKey string `json:"apiKey" validate:"required"`
BaseURL string `json:"baseURL"` BaseURL string `json:"baseURL"`
APIType string `json:"apiType" validate:"required"`
AuthMode string `json:"authMode"`
Model string `json:"model"`
} }
type AgentAccountDeleteReq struct { type AgentAccountDeleteReq struct {
@@ -367,26 +381,34 @@ type AgentAccountInfo struct {
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
Models []AgentAccountModel `json:"models"` Models []AgentAccountModel `json:"models"`
APIType string `json:"apiType"` APIType string `json:"apiType"`
AuthMode string `json:"authMode"`
VerifyModel string `json:"verifyModel"`
Verified bool `json:"verified"` Verified bool `json:"verified"`
Remark string `json:"remark"` Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
} }
type ProviderModelInfo struct { type ProviderModelInfo struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
ContextWindow int `json:"contextWindow"` }
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"` type ProviderAPIInfo struct {
Input []string `json:"input"` APIType string `json:"apiType"`
BaseURL string `json:"baseUrl"`
EditableBaseURL bool `json:"editableBaseUrl"`
DefaultAuthMode string `json:"defaultAuthMode"`
AuthModes []string `json:"authModes"`
} }
type ProviderInfo struct { type ProviderInfo struct {
Sort uint `json:"-"` Sort uint `json:"-"`
Provider string `json:"provider"` Provider string `json:"provider"`
DisplayName string `json:"displayName"` DisplayName string `json:"displayName"`
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
Models []ProviderModelInfo `json:"models"` DefaultAPIType string `json:"defaultApiType"`
APITypes []ProviderAPIInfo `json:"apiTypes"`
Models []ProviderModelInfo `json:"models"`
} }
type AgentFeishuConfigReq struct { type AgentFeishuConfigReq struct {
@@ -407,11 +429,6 @@ type AgentFeishuConfigUpdateReq struct {
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"` Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"`
} }
type AgentFeishuPairingApproveReq struct {
AgentID uint `json:"agentId" validate:"required"`
PairingCode string `json:"pairingCode" validate:"required"`
}
type AgentFeishuConfig struct { type AgentFeishuConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
ThreadSession bool `json:"threadSession"` ThreadSession bool `json:"threadSession"`
@@ -660,16 +677,20 @@ 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" validate:"required"`
BrowserEnabled bool `json:"browserEnabled"` BrowserEnabled bool `json:"browserEnabled"`
NPMRegistry string `json:"npmRegistry" validate:"required"` NPMRegistry string `json:"npmRegistry" validate:"required"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
} }
type AgentOtherConfig struct { type AgentOtherConfig struct {
UserTimezone string `json:"userTimezone"` UserTimezone string `json:"userTimezone"`
BrowserEnabled bool `json:"browserEnabled"` BrowserEnabled bool `json:"browserEnabled"`
NPMRegistry string `json:"npmRegistry"` NPMRegistry string `json:"npmRegistry"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
} }
type AgentConfigFileReq struct { type AgentConfigFileReq struct {
+3
View File
@@ -2,6 +2,7 @@ package dto
import ( import (
"context" "context"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/app/task"
) )
@@ -132,6 +133,8 @@ type Locale struct {
Ko string `json:"ko"` Ko string `json:"ko"`
Tr string `json:"tr"` Tr string `json:"tr"`
Es string `json:"es-es" yaml:"es-es"` Es string `json:"es-es" yaml:"es-es"`
Fa string `json:"fa"`
Lo string `json:"lo"`
} }
type AppForm struct { type AppForm struct {
+9 -8
View File
@@ -65,14 +65,15 @@ type UploadForRecover struct {
} }
type CommonBackup struct { type CommonBackup struct {
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"` Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"`
Name string `json:"name"` Name string `json:"name"`
DetailName string `json:"detailName"` DetailName string `json:"detailName"`
Secret string `json:"secret"` Secret string `json:"secret"`
StopBefore bool `json:"stopBefore"` IsImmediate bool `json:"isImmediate"`
TaskID string `json:"taskID"` StopBefore bool `json:"stopBefore"`
FileName string `json:"fileName"` TaskID string `json:"taskID"`
Args []string `json:"args"` FileName string `json:"fileName"`
Args []string `json:"args"`
Description string `json:"description"` Description string `json:"description"`
} }
+10 -5
View File
@@ -134,10 +134,15 @@ type ExtraHost struct {
IP string `json:"ip"` IP string `json:"ip"`
} }
type ContainerNetwork struct { type ContainerNetwork struct {
Network string `json:"network"` Network string `json:"network"`
Ipv4 string `json:"ipv4"` Ipv4 string `json:"ipv4"`
Ipv6 string `json:"ipv6"` Ipv6 string `json:"ipv6"`
MacAddr string `json:"macAddr"` MacAddr string `json:"macAddr"`
Links []string `json:"links"`
Aliases []string `json:"aliases"`
DriverOpts map[string]string `json:"driverOpts"`
GwPriority int `json:"gwPriority"`
LinkLocalIPs []string `json:"linkLocalIPs"`
} }
type ContainerCreateByCommand struct { type ContainerCreateByCommand struct {
@@ -319,7 +324,7 @@ type ComposeCreate struct {
type ComposeOperation struct { type ComposeOperation struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Path string `json:"path"` Path string `json:"path"`
Operation string `json:"operation" validate:"required,oneof=up start restart stop down delete"` Operation string `json:"operation" validate:"required,oneof=up start restart stop down delete rebuild"`
WithFile bool `json:"withFile"` WithFile bool `json:"withFile"`
Force bool `json:"force"` Force bool `json:"force"`
} }
+10 -2
View File
@@ -79,8 +79,9 @@ type NodeCurrent struct {
} }
type DashboardCurrent struct { type DashboardCurrent struct {
Uptime uint64 `json:"uptime"` Uptime uint64 `json:"uptime"`
TimeSinceUptime string `json:"timeSinceUptime"` TimeSinceUptime string `json:"timeSinceUptime"`
RunningTime RunningTime `json:"runningTime"`
Procs uint64 `json:"procs"` Procs uint64 `json:"procs"`
@@ -128,6 +129,13 @@ type DashboardCurrent struct {
ShotTime time.Time `json:"shotTime"` ShotTime time.Time `json:"shotTime"`
} }
type RunningTime struct {
Days uint64 `json:"days"`
Hours uint64 `json:"hours"`
Minutes uint64 `json:"minutes"`
Seconds uint64 `json:"seconds"`
}
type AppLauncherSync struct { type AppLauncherSync struct {
Keys []string `json:"keys"` Keys []string `json:"keys"`
} }
+69 -10
View File
@@ -60,25 +60,84 @@ type MysqlDBCreate struct {
Database string `json:"database" validate:"required"` Database string `json:"database" validate:"required"`
Format string `json:"format" validate:"required"` Format string `json:"format" validate:"required"`
Collation string `json:"collation"` Collation string `json:"collation"`
Username string `json:"username" validate:"required"` Username string `json:"username"`
Password string `json:"password" validate:"required"` Password string `json:"password"`
Permission string `json:"permission" validate:"required"` Permission string `json:"permission" validate:"required"`
Description string `json:"description"` Description string `json:"description"`
} }
type MysqlUser struct {
Username string `json:"username"`
Host string `json:"host"`
Password string `json:"password"`
Description string `json:"description"`
IsDelete bool `json:"isDelete"`
}
type MysqlGrant struct {
Database string `json:"database"`
Username string `json:"username"`
Host string `json:"host"`
}
type MysqlGrantSummarySearch struct {
Database string `json:"database" validate:"required"`
DBs []string `json:"dbs" validate:"required"`
}
type MysqlUserSearch struct {
Database string `json:"database" validate:"required"`
}
type MysqlUserCreate struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Host string `json:"host" validate:"required"`
Description string `json:"description"`
DBs []string `json:"dbs"`
}
type MysqlUserDelete struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
}
type MysqlUserUpdate struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
NewHost string `json:"newHost" validate:"required"`
Description string `json:"description"`
}
type MysqlUserPassword struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
Password string `json:"password" validate:"required"`
}
type MysqlGrantCreate struct {
Database string `json:"database" validate:"required"`
DB string `json:"db" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
}
type MysqlGrantDelete struct {
Database string `json:"database" validate:"required"`
DB string `json:"db" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
}
type MysqlFormatCollationOption struct { type MysqlFormatCollationOption struct {
Format string `json:"format"` Format string `json:"format"`
Collations []string `json:"collations"` Collations []string `json:"collations"`
} }
type BindUser struct {
Database string `json:"database" validate:"required"`
DB string `json:"db" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Permission string `json:"permission" validate:"required"`
}
type MysqlLoadDB struct { type MysqlLoadDB struct {
From string `json:"from" validate:"required,oneof=local remote"` From string `json:"from" validate:"required,oneof=local remote"`
Type string `json:"type" validate:"required,oneof=mysql mariadb mysql-cluster"` Type string `json:"type" validate:"required,oneof=mysql mariadb mysql-cluster"`
+36
View File
@@ -1,6 +1,8 @@
package dto package dto
import ( import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
) )
@@ -14,3 +16,37 @@ type SearchTaskLogReq struct {
type TaskDTO struct { type TaskDTO struct {
model.Task model.Task
} }
type SystemLogReq struct {
PageSize int `json:"pageSize" validate:"omitempty,min=1,max=500"`
Cursor string `json:"cursor"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
Keyword string `json:"keyword"`
Priority string `json:"priority"`
Service string `json:"service"`
}
type SystemLogRes struct {
Source string `json:"source"`
Items []SystemLogItem `json:"items"`
HasMore bool `json:"hasMore"`
NextCursor string `json:"nextCursor"`
}
type SystemLogStatus struct {
Source string `json:"source"`
Version string `json:"version"`
KeywordFilterSupported bool `json:"keywordFilterSupported"`
Message string `json:"message"`
}
type SystemLogItem struct {
Timestamp int64 `json:"-"`
Cursor string `json:"-"`
Time string `json:"time"`
Priority string `json:"priority"`
Service string `json:"service"`
Message string `json:"message"`
Raw string `json:"raw"`
}
+39 -5
View File
@@ -1,6 +1,8 @@
package dto package dto
import ( import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components" "github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
) )
@@ -87,9 +89,41 @@ var LBAlgorithms = map[string]struct{}{"ip_hash": {}, "least_conn": {}}
var RealIPKeys = map[string]struct{}{"X-Forwarded-For": {}, "X-Real-IP": {}, "CF-Connecting-IP": {}} var RealIPKeys = map[string]struct{}{"X-Forwarded-For": {}, "X-Real-IP": {}, "CF-Connecting-IP": {}}
type NginxModule struct { type NginxModule struct {
Name string `json:"name"` Name string `json:"name"`
Script string `json:"script"` Custom bool `json:"custom,omitempty"`
Packages []string `json:"packages"` Script string `json:"script"`
Params string `json:"params"` Packages []string `json:"packages"`
Enable bool `json:"enable"` Params string `json:"params"`
Enable bool `json:"enable"`
BuildMode string `json:"buildMode,omitempty"`
Provider string `json:"provider,omitempty"`
LoadOrder int `json:"loadOrder,omitempty"`
Builds []NginxModuleBuild `json:"builds,omitempty"`
LastError string `json:"lastError,omitempty"`
}
type NginxModuleBuild struct {
Provider string `json:"provider"`
BuildMode string `json:"buildMode"`
Status string `json:"status"`
Hash string `json:"hash"`
Target NginxModuleTarget `json:"target"`
Artifacts []NginxModuleArtifact `json:"artifacts,omitempty"`
Error string `json:"error,omitempty"`
BuiltAt time.Time `json:"builtAt,omitempty"`
}
type NginxModuleTarget struct {
Key string `json:"key"`
OpenRestyVersion string `json:"openrestyVersion"`
Architecture string `json:"architecture"`
Image string `json:"image,omitempty"`
ImageDigest string `json:"imageDigest,omitempty"`
BuilderDigest string `json:"builderDigest,omitempty"`
}
type NginxModuleArtifact struct {
Name string `json:"name"`
Path string `json:"path"`
Checksum string `json:"checksum"`
} }
+1
View File
@@ -99,6 +99,7 @@ type AppInstallUpgrade struct {
DetailID uint `json:"detailId"` DetailID uint `json:"detailId"`
Backup bool `json:"backup"` Backup bool `json:"backup"`
PullImage bool `json:"pullImage"` PullImage bool `json:"pullImage"`
DeleteImage bool `json:"deleteImage"`
DockerCompose string `json:"dockerCompose"` DockerCompose string `json:"dockerCompose"`
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
} }
+16
View File
@@ -21,6 +21,10 @@ type McpServerCreate struct {
StreamableHttpPath string `json:"streamableHttpPath"` StreamableHttpPath string `json:"streamableHttpPath"`
OutputTransport string `json:"outputTransport" validate:"required"` OutputTransport string `json:"outputTransport" validate:"required"`
Type string `json:"type" validate:"required"` Type string `json:"type" validate:"required"`
GatewayImage string `json:"gatewayImage"`
ProtocolVersion string `json:"protocolVersion"`
GatewayArgs string `json:"gatewayArgs" validate:"max=4096"`
TaskID string `json:"taskID"`
} }
type McpServerUpdate struct { type McpServerUpdate struct {
@@ -32,11 +36,23 @@ type McpServerDelete struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
} }
type McpServerDetail struct {
ID uint `json:"id" validate:"required"`
}
type McpServerStatusSync struct {
IDs []uint `json:"ids"`
}
type McpServerOperate struct { type McpServerOperate struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"` Operate string `json:"operate" validate:"required"`
} }
type McpServerConnectionTest struct {
ID uint `json:"id" validate:"required"`
}
type McpBindDomain struct { type McpBindDomain struct {
Domain string `json:"domain" validate:"required"` Domain string `json:"domain" validate:"required"`
SSLID uint `json:"sslID"` SSLID uint `json:"sslID"`
+13 -8
View File
@@ -114,17 +114,22 @@ type NginxRedirectUpdate struct {
} }
type NginxBuildReq struct { type NginxBuildReq struct {
TaskID string `json:"taskID" validate:"required"` TaskID string `json:"taskID" validate:"required"`
Mirror string `json:"mirror" validate:"required"` Mirror string `json:"mirror" validate:"required"`
Modules []string `json:"modules"`
Force bool `json:"force"`
} }
type NginxModuleUpdate struct { type NginxModuleUpdate struct {
Operate string `json:"operate" validate:"required,oneof=create delete update"` Operate string `json:"operate" validate:"required,oneof=create delete update"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Script string `json:"script"` Script string `json:"script"`
Packages string `json:"packages"` Packages string `json:"packages"`
Enable bool `json:"enable"` Enable bool `json:"enable"`
Params string `json:"params"` Params string `json:"params"`
BuildMode string `json:"buildMode" validate:"omitempty,oneof=dynamic static"`
Provider string `json:"provider" validate:"omitempty,oneof=local prebuilt"`
LoadOrder int `json:"loadOrder" validate:"omitempty,min=0,max=9999"`
} }
type NginxOperateReq struct { type NginxOperateReq struct {
+8 -3
View File
@@ -21,13 +21,14 @@ type RuntimeCreate struct {
Source string `json:"source"` Source string `json:"source"`
CodeDir string `json:"codeDir"` CodeDir string `json:"codeDir"`
Remark string `json:"remark"` Remark string `json:"remark"`
TaskID string `json:"taskID"`
Params map[string]interface{} `json:"params"` Params map[string]interface{} `json:"params"`
NodeConfig NodeConfig
} }
type NodeConfig struct { type NodeConfig struct {
Install bool `json:"install"` Install *bool `json:"install"`
Clean bool `json:"clean"` Clean bool `json:"clean"`
ExposedPorts []ExposedPort `json:"exposedPorts"` ExposedPorts []ExposedPort `json:"exposedPorts"`
Environments []Environment `json:"environments"` Environments []Environment `json:"environments"`
@@ -42,12 +43,14 @@ type Environment struct {
type Volume struct { type Volume struct {
Source string `json:"source"` Source string `json:"source"`
Target string `json:"target"` Target string `json:"target"`
Mode string `json:"mode"`
} }
type ExposedPort struct { type ExposedPort struct {
HostPort int `json:"hostPort"` HostPort int `json:"hostPort"`
ContainerPort int `json:"containerPort"` ContainerPort int `json:"containerPort"`
HostIP string `json:"hostIP"` HostIP string `json:"hostIP"`
Protocol string `json:"protocol"`
} }
type ExtraHost struct { type ExtraHost struct {
@@ -56,8 +59,10 @@ type ExtraHost struct {
} }
type RuntimeDelete struct { type RuntimeDelete struct {
ID uint `json:"id"` ID uint `json:"id"`
ForceDelete bool `json:"forceDelete"` ForceDelete bool `json:"forceDelete"`
DeleteImage bool `json:"deleteImage"`
TaskID string `json:"taskID"`
} }
type RuntimeUpdate struct { type RuntimeUpdate struct {
+14
View File
@@ -126,6 +126,16 @@ type WebsiteSSLUpload struct {
Type string `json:"type" validate:"required,oneof=paste local"` Type string `json:"type" validate:"required,oneof=paste local"`
SSLID uint `json:"sslID"` SSLID uint `json:"sslID"`
Description string `json:"description"` Description string `json:"description"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
}
type WebsiteSSLPush struct {
ID uint `json:"id" validate:"required"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
TaskID string `json:"taskID" validate:"required"`
Sync bool `json:"sync"`
} }
type WebsiteCASearch struct { type WebsiteCASearch struct {
@@ -157,6 +167,8 @@ type WebsiteCAObtain struct {
Description string `json:"description"` Description string `json:"description"`
ExecShell bool `json:"execShell"` ExecShell bool `json:"execShell"`
Shell string `json:"shell"` Shell string `json:"shell"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
} }
type WebsiteCARenew struct { type WebsiteCARenew struct {
@@ -167,4 +179,6 @@ type WebsiteSSLFileUpload struct {
Type string `json:"type"` Type string `json:"type"`
Description string `json:"description"` Description string `json:"description"`
SSLID uint64 `json:"sslID"` SSLID uint64 `json:"sslID"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
} }
+30 -28
View File
@@ -104,34 +104,36 @@ type AppDetail struct {
} }
type AppInstallDTO struct { type AppInstallDTO struct {
ID uint `json:"id"` ID uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
AppID uint `json:"appID"` AppID uint `json:"appID"`
AppDetailID uint `json:"appDetailID"` AppDetailID uint `json:"appDetailID"`
Version string `json:"version"` Version string `json:"version"`
Status string `json:"status"` Status string `json:"status"`
Message string `json:"message"` Message string `json:"message"`
HttpPort int `json:"httpPort"` HttpPort int `json:"httpPort"`
HttpsPort int `json:"httpsPort"` HttpsPort int `json:"httpsPort"`
Path string `json:"path"` Path string `json:"path"`
CanUpdate bool `json:"canUpdate"` CanUpdate bool `json:"canUpdate"`
Icon string `json:"icon"` Icon string `json:"icon"`
AppName string `json:"appName"` AppName string `json:"appName"`
Ready int `json:"ready"` Ready int `json:"ready"`
Total int `json:"total"` Total int `json:"total"`
AppKey string `json:"appKey"` AppKey string `json:"appKey"`
AppType string `json:"appType"` AppType string `json:"appType"`
AppStatus string `json:"appStatus"` AppStatus string `json:"appStatus"`
DockerCompose string `json:"dockerCompose"` DockerCompose string `json:"dockerCompose"`
WebUI string `json:"webUI"` WebUI string `json:"webUI"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
Favorite bool `json:"favorite"` Favorite bool `json:"favorite"`
SortOrder int `json:"sortOrder"` SortOrder int `json:"sortOrder"`
App AppDetail `json:"app"` App AppDetail `json:"app"`
Container string `json:"container"` Container string `json:"container"`
IsEdit bool `json:"isEdit"` IsEdit bool `json:"isEdit"`
LinkDB bool `json:"linkDB"` LinkDB bool `json:"linkDB"`
ServiceName string `json:"serviceName"` ResourceKeys []string `json:"resourceKeys"`
ServiceName string `json:"serviceName"`
Env map[string]interface{} `json:"env"`
} }
type AppInstallInfo struct { type AppInstallInfo struct {
+14
View File
@@ -16,6 +16,12 @@ type McpServerDTO struct {
Volumes []request.Volume `json:"volumes"` Volumes []request.Volume `json:"volumes"`
} }
type McpServerStatusDTO struct {
ID uint `json:"id"`
Status string `json:"status"`
Message string `json:"message"`
}
type McpBindDomainRes struct { type McpBindDomainRes struct {
Domain string `json:"domain"` Domain string `json:"domain"`
SSLID uint `json:"sslID"` SSLID uint `json:"sslID"`
@@ -24,3 +30,11 @@ type McpBindDomainRes struct {
WebsiteID uint `json:"websiteID"` WebsiteID uint `json:"websiteID"`
ConnUrl string `json:"connUrl"` ConnUrl string `json:"connUrl"`
} }
type McpServerConnectionTestRes struct {
Success bool `json:"success"`
Endpoint string `json:"endpoint"`
OutputTransport string `json:"outputTransport"`
ProtocolVersion string `json:"protocolVersion,omitempty"`
Message string `json:"message"`
}
+16 -7
View File
@@ -69,16 +69,25 @@ type NginxProxyCache struct {
} }
type NginxModule struct { type NginxModule struct {
Name string `json:"name"` Name string `json:"name"`
Script string `json:"script"` Custom bool `json:"custom"`
Packages string `json:"packages"` Script string `json:"script"`
Params string `json:"params"` Packages string `json:"packages"`
Enable bool `json:"enable"` Params string `json:"params"`
Enable bool `json:"enable"`
BuildMode string `json:"buildMode"`
Provider string `json:"provider"`
LoadOrder int `json:"loadOrder"`
BuildStatus string `json:"buildStatus"`
LoadStatus string `json:"loadStatus"`
Artifacts []dto.NginxModuleArtifact `json:"artifacts"`
LastError string `json:"lastError"`
} }
type NginxBuildConfig struct { type NginxBuildConfig struct {
Mirror string `json:"mirror"` Mirror string `json:"mirror"`
Modules []NginxModule `json:"modules"` DynamicSupported bool `json:"dynamicSupported"`
Modules []NginxModule `json:"modules"`
} }
type NginxConfigRes struct { type NginxConfigRes struct {
+30
View File
@@ -0,0 +1,30 @@
package dto
import "time"
type RuntimeDiagnosticsSummary struct {
RSS uint64 `json:"rss"`
HeapAlloc uint64 `json:"heapAlloc"`
HeapObjects uint64 `json:"heapObjects"`
Goroutines int `json:"goroutines"`
}
type RuntimeGoroutineGroup struct {
State string `json:"state"`
Top string `json:"top"`
Count int `json:"count"`
Stack []string `json:"stack"`
}
type RuntimeGoroutineSnapshot struct {
Total int `json:"total"`
GroupCount int `json:"groupCount"`
Truncated bool `json:"truncated"`
CapturedAt time.Time `json:"capturedAt"`
Goroutines []RuntimeGoroutineGroup `json:"goroutines"`
}
type RuntimeProfileCreate struct {
Type string `json:"type" validate:"required,oneof=cpu heap goroutine mutex block"`
Duration int `json:"duration" validate:"omitempty,min=5,max=30"`
}
+2
View File
@@ -7,6 +7,8 @@ type AgentAccount struct {
APIKey string `json:"apiKey"` APIKey string `json:"apiKey"`
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
APIType string `json:"apiType"` APIType string `json:"apiType"`
AuthMode string `json:"authMode"`
VerifyModel string `json:"verifyModel"`
RememberAPIKey bool `json:"rememberApiKey"` RememberAPIKey bool `json:"rememberApiKey"`
Verified bool `json:"verified"` Verified bool `json:"verified"`
Remark string `json:"remark"` Remark string `json:"remark"`
+4 -8
View File
@@ -2,14 +2,10 @@ package model
type AgentAccountModel struct { type AgentAccountModel struct {
BaseModel BaseModel
AccountID uint `json:"accountId" gorm:"index"` AccountID uint `json:"accountId" gorm:"index"`
Model string `json:"model" gorm:"index"` Model string `json:"model" gorm:"index"`
Name string `json:"name"` Name string `json:"name"`
ContextWindow int `json:"contextWindow"` SortOrder int `json:"sortOrder" gorm:"index"`
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"`
Input string `json:"input" gorm:"type:text"`
SortOrder int `json:"sortOrder" gorm:"index"`
} }
func (AgentAccountModel) TableName() string { func (AgentAccountModel) TableName() string {
+21
View File
@@ -0,0 +1,21 @@
package model
type DatabaseUser struct {
BaseModel
Type string `json:"type" gorm:"not null;uniqueIndex:idx_database_user"`
Database string `json:"database" gorm:"not null;uniqueIndex:idx_database_user"`
Username string `json:"username" gorm:"not null;uniqueIndex:idx_database_user"`
Host string `json:"host" gorm:"uniqueIndex:idx_database_user"`
Password string `json:"password"`
Description string `json:"description"`
IsDelete bool `json:"isDelete"`
}
type DatabaseUserGrant struct {
BaseModel
Type string `json:"type" gorm:"not null;uniqueIndex:idx_database_user_grant"`
Database string `json:"database" gorm:"not null;uniqueIndex:idx_database_user_grant"`
DBName string `json:"dbName" gorm:"not null;uniqueIndex:idx_database_user_grant"`
Username string `json:"username" gorm:"not null;uniqueIndex:idx_database_user_grant"`
Host string `json:"host" gorm:"not null;uniqueIndex:idx_database_user_grant"`
}
+3
View File
@@ -18,4 +18,7 @@ type McpServer struct {
StreamableHttpPath string `json:"streamableHttpPath"` StreamableHttpPath string `json:"streamableHttpPath"`
OutputTransport string `json:"outputTransport"` OutputTransport string `json:"outputTransport"`
Type string `json:"type"` Type string `json:"type"`
GatewayImage string `json:"gatewayImage"`
ProtocolVersion string `json:"protocolVersion"`
GatewayArgs string `json:"gatewayArgs"`
} }
+242 -330
View File
@@ -1,321 +1,181 @@
package provider package provider
import ( import (
"fmt"
"net/url"
"strings" "strings"
) )
type Model struct { type APIConfig struct {
ID string APIType string
Name string BaseURL string
ContextWindow int EditableBaseURL bool
MaxTokens int DefaultAuthMode string
Reasoning bool AuthModes []string
Input []string
} }
type RuntimeDefault struct { const (
APIType string AuthModeBearer = "bearer"
ContextWindow int AuthModeXAPIKey = "x-api-key"
MaxTokens int )
Input []string
type Model struct {
ID string
Name string
} }
type Meta struct { type Meta struct {
Key string Key string
DisplayName string DisplayName string
Sort uint Sort uint
DefaultBaseURL string DefaultAPIType string
APIConfigs []APIConfig
EnvKey string EnvKey string
Default RuntimeDefault
Models []Model Models []Model
} }
var catalog = map[string]Meta{ var catalog = map[string]Meta{
"custom": { "custom": {
Key: "custom", Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY",
DisplayName: "Custom", APIConfigs: editableAPIConfigs("openai-completions", "openai-responses", "anthropic-messages"),
Sort: 10,
DefaultBaseURL: "",
EnvKey: "CUSTOM_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{},
}, },
"ollama": { "ollama": {
Key: "ollama", Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses",
DisplayName: "Ollama", APIConfigs: editableAPIConfigs("openai-responses", "openai-completions"),
Sort: 15,
Default: RuntimeDefault{
APIType: "openai-responses",
ContextWindow: 160000,
MaxTokens: 8192,
Input: []string{"text"},
},
}, },
"vllm": { "vllm": {
Key: "vllm", Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY",
DisplayName: "vLLM", APIConfigs: editableAPIConfigs("openai-completions", "openai-responses", "anthropic-messages"),
Sort: 20,
DefaultBaseURL: "",
EnvKey: "VLLM_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{},
}, },
"deepseek": { "deepseek": {
Key: "deepseek", Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY",
DisplayName: "DeepSeek", APIConfigs: []APIConfig{
Sort: 25, {APIType: "openai-completions", BaseURL: "https://api.deepseek.com/v1"},
DefaultBaseURL: "https://api.deepseek.com/v1", anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
EnvKey: "DEEPSEEK_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 131072,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "deepseek/deepseek-v4-flash", Name: "deepseek-v4-flash"},
{ID: "deepseek/deepseek-v4-pro", Name: "deepseek-v4-pro"},
}, },
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
}, },
"bailian-coding-plan": { "bailian-coding-plan": {
Key: "bailian-coding-plan", Key: "bailian-coding-plan", DisplayName: "阿里云百炼 Coding Plan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
DisplayName: "阿里云百炼 Coding Plan", APIConfigs: []APIConfig{
Sort: 30, {APIType: "openai-completions", BaseURL: "https://coding.dashscope.aliyuncs.com/v1"},
DefaultBaseURL: "https://coding.dashscope.aliyuncs.com/v1", anthropicAPIConfig("https://coding.dashscope.aliyuncs.com/apps/anthropic", AuthModeBearer),
EnvKey: "QWEN_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text"},
}, },
Models: []Model{ Models: []Model{
{ID: "bailian-coding-plan/qwen3.5-plus", Name: "Qwen3.5-Plus", Reasoning: true}, {ID: "qwen3-coder-plus", Name: "Qwen3-Coder-Plus"},
{ID: "bailian-coding-plan/qwen3-max", Name: "Qwen3-Max", Reasoning: true}, {ID: "qwen3-max-2026-01-23", Name: "Qwen3-Max-2026-01-23"},
{ID: "bailian-coding-plan/qwen3-coder-next", Name: "Qwen3-Coder-Next", Reasoning: true}, {ID: "qwen3-coder-next", Name: "Qwen3-Coder-Next"},
{ID: "bailian-coding-plan/qwen3-coder-plus", Name: "Qwen3-Coder-Plus", Reasoning: true}, {ID: "glm-4.7", Name: "GLM-4.7"},
{ID: "bailian-coding-plan/minimax-m2.5", Name: "MiniMax M2.5", Reasoning: true}, {ID: "kimi-k2.5", Name: "Kimi K2.5"},
{ID: "bailian-coding-plan/glm-5", Name: "GLM-5", Reasoning: true}, {ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"},
{ID: "bailian-coding-plan/kimi-k2.5", Name: "Kimi-k2.5", Reasoning: true}, {ID: "glm-5", Name: "GLM-5"},
{ID: "bailian-coding-plan/glm-4.7", Name: "GLM-4.7", Reasoning: true}, {ID: "MiniMax-M2.5", Name: "MiniMax M2.5"},
{ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"},
{ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"},
}, },
}, },
"ark-coding-plan": { "ark-coding-plan": {
Key: "ark-coding-plan", Key: "ark-coding-plan", DisplayName: "方舟 Coding Plan", Sort: 35, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY",
DisplayName: "方舟 Coding Plan", APIConfigs: []APIConfig{
Sort: 35, {APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3"},
DefaultBaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3", anthropicAPIConfig("https://ark.cn-beijing.volces.com/api/coding", AuthModeBearer),
EnvKey: "ARK_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 4096,
Input: []string{"text"},
}, },
Models: []Model{ Models: []Model{
{ID: "ark-coding-plan/ark-code-latest", Name: "Ark Coding Plan", ContextWindow: 256000, MaxTokens: 4096}, {ID: "ark-code-latest", Name: "Ark Coding Plan"}, {ID: "doubao-seed-code", Name: "Doubao Seed Code"},
{ID: "ark-coding-plan/doubao-seed-code", Name: "Doubao Seed Code", ContextWindow: 256000, MaxTokens: 4096}, {ID: "glm-4.7", Name: "GLM 4.7 Coding"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"},
{ID: "ark-coding-plan/glm-4.7", Name: "GLM 4.7 Coding", ContextWindow: 200000, MaxTokens: 4096}, {ID: "kimi-k2.5", Name: "Kimi K2.5 Coding"}, {ID: "doubao-seed-code-preview-251028", Name: "Doubao Seed Code Preview"},
{ID: "ark-coding-plan/kimi-k2-thinking", Name: "Kimi K2 Thinking", ContextWindow: 256000, MaxTokens: 4096},
{ID: "ark-coding-plan/kimi-k2.5", Name: "Kimi K2.5 Coding", ContextWindow: 256000, MaxTokens: 4096},
{ID: "ark-coding-plan/doubao-seed-code-preview-251028", Name: "Doubao Seed Code Preview", ContextWindow: 256000, MaxTokens: 4096},
}, },
}, },
"zai": { "zai": {
Key: "zai", Key: "zai", DisplayName: "Z.ai", Sort: 40, DefaultAPIType: "openai-completions", EnvKey: "ZAI_API_KEY",
DisplayName: "Z.ai", APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true}},
Sort: 40, 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"}},
DefaultBaseURL: "https://open.bigmodel.cn/api/paas/v4",
EnvKey: "ZAI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 204800,
MaxTokens: 131072,
Input: []string{"text"},
},
Models: []Model{
{ID: "zai/glm-5", Name: "GLM-5", Reasoning: true},
{ID: "zai/glm-4.7", Name: "GLM-4.7", Reasoning: true},
{ID: "zai/glm-4.7-flash", Name: "GLM-4.7-Flash", Reasoning: true},
{ID: "zai/glm-4.7-flashx", Name: "GLM-4.7-FlashX", Reasoning: true},
},
}, },
"minimax": { "minimax": {
Key: "minimax", Key: "minimax", DisplayName: "MiniMax (CN)", Sort: 45, DefaultAPIType: "anthropic-messages", EnvKey: "MINIMAX_API_KEY",
DisplayName: "MiniMax (CN)", APIConfigs: []APIConfig{
Sort: 45, anthropicAPIConfig("https://api.minimaxi.com/anthropic", AuthModeXAPIKey, AuthModeBearer),
DefaultBaseURL: "https://api.minimaxi.com/anthropic", {APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
EnvKey: "MINIMAX_API_KEY",
Default: RuntimeDefault{
APIType: "anthropic-messages",
ContextWindow: 200000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "minimax/MiniMax-M2.7", Name: "MiniMax M2.7"},
{ID: "minimax/MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"},
{ID: "minimax/MiniMax-M2.5", Name: "MiniMax M2.5", Reasoning: true},
{ID: "minimax/MiniMax-M2.5-highspeed", Name: "MiniMax M2.5 highspeed"},
}, },
Models: []Model{{ID: "MiniMax-M3", Name: "MiniMax M3"}, {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}},
}, },
"xiaomi": { "xiaomi": {
Key: "xiaomi", Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
DisplayName: "Xiaomi", APIConfigs: []APIConfig{
Sort: 46, {APIType: "openai-completions", BaseURL: "https://api.xiaomimimo.com/v1"},
DefaultBaseURL: "https://api.xiaomimimo.com/v1", anthropicAPIConfig("https://api.xiaomimimo.com/anthropic", AuthModeBearer),
EnvKey: "XIAOMI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 262144,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "xiaomi/mimo-v2-flash", Name: "Xiaomi MiMo V2 Flash", ContextWindow: 262144, MaxTokens: 8192, Input: []string{"text"}},
{ID: "xiaomi/mimo-v2-pro", Name: "Xiaomi MiMo V2 Pro", ContextWindow: 1048576, MaxTokens: 32000, Reasoning: true, Input: []string{"text"}},
{ID: "xiaomi/mimo-v2-omni", Name: "Xiaomi MiMo V2 Omni", ContextWindow: 262144, MaxTokens: 32000, Reasoning: true, Input: []string{"text", "image"}},
}, },
Models: []Model{{ID: "mimo-v2-flash", Name: "Xiaomi MiMo V2 Flash"}, {ID: "mimo-v2-pro", Name: "Xiaomi MiMo V2 Pro"}, {ID: "mimo-v2-omni", Name: "Xiaomi MiMo V2 Omni"}},
}, },
"kimi": { "kimi": {
Key: "kimi", Key: "kimi", DisplayName: "Kimi (CN)", Sort: 50, DefaultAPIType: "openai-completions", EnvKey: "KIMI_API_KEY",
DisplayName: "Kimi (CN)", APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.cn/v1"}},
Sort: 50, Models: []Model{{ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}},
DefaultBaseURL: "https://api.moonshot.cn/v1",
EnvKey: "KIMI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "kimi/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true},
{ID: "kimi/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
{ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true},
},
}, },
"kimi-coding": { "kimi-coding": {
Key: "kimi-coding", Key: "kimi-coding", DisplayName: "Kimi Coding", Sort: 51, DefaultAPIType: "anthropic-messages", EnvKey: "KIMI_API_KEY",
DisplayName: "Kimi Coding", APIConfigs: []APIConfig{anthropicAPIConfig("https://api.kimi.com/coding/", AuthModeXAPIKey)},
Sort: 51, Models: []Model{{ID: "kimi-code", Name: "Kimi Code"}, {ID: "k2p5", Name: "Kimi K2.5"}},
DefaultBaseURL: "https://api.kimi.com/coding/",
EnvKey: "KIMI_API_KEY",
Default: RuntimeDefault{
APIType: "anthropic-messages",
ContextWindow: 262144,
MaxTokens: 32768,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "kimi-coding/kimi-code", Name: "Kimi Code", ContextWindow: 262144, MaxTokens: 32768, Reasoning: true, Input: []string{"text", "image"}},
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5", ContextWindow: 262144, MaxTokens: 32768, Reasoning: true, Input: []string{"text", "image"}},
},
}, },
"openai": { "openai": {
Key: "openai", Key: "openai", DisplayName: "OpenAI", Sort: 55, DefaultAPIType: "openai-responses", EnvKey: "OPENAI_API_KEY",
DisplayName: "OpenAI", APIConfigs: []APIConfig{
Sort: 55, {APIType: "openai-responses", BaseURL: "https://api.openai.com/v1"},
DefaultBaseURL: "https://api.openai.com/v1", {APIType: "openai-completions", BaseURL: "https://api.openai.com/v1"},
EnvKey: "OPENAI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-responses",
ContextWindow: 272000,
MaxTokens: 128000,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "openai/gpt-5.4", Name: "gpt-5.4", ContextWindow: 272000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
{ID: "openai/gpt-5.4-pro", Name: "gpt-5.4-pro", ContextWindow: 1050000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
{ID: "openai/gpt-5.4-mini", Name: "gpt-5.4-mini", ContextWindow: 400000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
{ID: "openai/gpt-5.4-nano", Name: "gpt-5.4-nano", ContextWindow: 400000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
}, },
Models: []Model{{ID: "gpt-5.4", Name: "gpt-5.4"}, {ID: "gpt-5.4-pro", Name: "gpt-5.4-pro"}, {ID: "gpt-5.4-mini", Name: "gpt-5.4-mini"}, {ID: "gpt-5.4-nano", Name: "gpt-5.4-nano"}},
}, },
"openrouter": { "openrouter": {
Key: "openrouter", Key: "openrouter", DisplayName: "OpenRouter", Sort: 56, DefaultAPIType: "openai-completions", EnvKey: "OPENROUTER_API_KEY",
DisplayName: "OpenRouter", APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://openrouter.ai/api/v1"}},
Sort: 56, Models: []Model{{ID: "openrouter/free", Name: "openrouter/free"}, {ID: "openrouter/auto", Name: "openrouter/auto"}},
DefaultBaseURL: "https://openrouter.ai/api/v1",
EnvKey: "OPENROUTER_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "openrouter/free", Name: "openrouter/free"},
{ID: "openrouter/auto", Name: "openrouter/auto"},
},
}, },
"anthropic": { "anthropic": {
Key: "anthropic", Key: "anthropic", DisplayName: "Anthropic", Sort: 60, DefaultAPIType: "anthropic-messages", EnvKey: "ANTHROPIC_API_KEY",
DisplayName: "Anthropic", APIConfigs: []APIConfig{anthropicAPIConfig("https://api.anthropic.com", AuthModeXAPIKey)},
Sort: 60, Models: []Model{{ID: "claude-sonnet-4-6", Name: "Claude Sonnet 4.6"}, {ID: "claude-opus-4-6", Name: "Claude Opus 4.6"}, {ID: "claude-opus-4-5", Name: "Claude Opus 4.5"}, {ID: "claude-sonnet-4-5", Name: "Claude Sonnet 4.5"}, {ID: "claude-haiku-4-5", Name: "Claude Haiku 4.5"}},
DefaultBaseURL: "https://api.anthropic.com",
EnvKey: "ANTHROPIC_API_KEY",
Default: RuntimeDefault{
APIType: "anthropic-messages",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "anthropic/claude-sonnet-4-6", Name: "Claude Sonnet 4.6", Reasoning: true},
{ID: "anthropic/claude-opus-4-6", Name: "Claude Opus 4.6", Reasoning: true},
{ID: "anthropic/claude-opus-4-5", Name: "Claude Opus 4.5"},
{ID: "anthropic/claude-sonnet-4-5", Name: "Claude Sonnet 4.5"},
{ID: "anthropic/claude-haiku-4-5", Name: "Claude Haiku 4.5"},
},
}, },
"gemini": { "gemini": {
Key: "gemini", Key: "gemini", DisplayName: "Gemini", Sort: 65, DefaultAPIType: "gemini-generate-content", EnvKey: "GEMINI_API_KEY",
DisplayName: "Gemini", APIConfigs: []APIConfig{{APIType: "gemini-generate-content", BaseURL: "https://generativelanguage.googleapis.com"}},
Sort: 65, Models: []Model{{ID: "gemini-3-flash-preview", Name: "Gemini 3 Flash Preview"}, {ID: "gemini-flash-latest", Name: "Gemini Flash Latest"}, {ID: "gemini-3-pro-preview", Name: "Gemini 3 Pro Preview"}},
DefaultBaseURL: "https://generativelanguage.googleapis.com",
EnvKey: "GEMINI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview", Reasoning: true},
{ID: "google/gemini-flash-latest", Name: "Gemini Flash Latest"},
{ID: "google/gemini-3-pro-preview", Name: "Gemini 3 Pro Preview", Reasoning: true},
},
}, },
"moonshot": { "moonshot": {
Key: "moonshot", Key: "moonshot", DisplayName: "Moonshot (Global)", Sort: 70, DefaultAPIType: "openai-completions", EnvKey: "MOONSHOT_API_KEY",
DisplayName: "Moonshot (Global)", APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.ai/v1"}},
Sort: 70, Models: []Model{{ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}},
DefaultBaseURL: "https://api.moonshot.ai/v1",
EnvKey: "MOONSHOT_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true},
{ID: "moonshot/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
{ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true},
},
}, },
} }
func editableAPIConfigs(apiTypes ...string) []APIConfig {
configs := make([]APIConfig, 0, len(apiTypes))
for _, apiType := range apiTypes {
if apiType == "anthropic-messages" {
config := anthropicAPIConfig("", AuthModeXAPIKey, AuthModeBearer)
config.EditableBaseURL = true
configs = append(configs, config)
continue
}
configs = append(configs, APIConfig{APIType: apiType, EditableBaseURL: true})
}
return configs
}
func anthropicAPIConfig(baseURL, defaultAuthMode string, additionalAuthModes ...string) APIConfig {
authModes := []string{defaultAuthMode}
for _, authMode := range additionalAuthModes {
if authMode != defaultAuthMode {
authModes = append(authModes, authMode)
}
}
return APIConfig{
APIType: "anthropic-messages",
BaseURL: baseURL,
DefaultAuthMode: defaultAuthMode,
AuthModes: authModes,
}
}
func Get(key string) (Meta, bool) { func Get(key string) (Meta, bool) {
meta, ok := catalog[key] meta, ok := catalog[key]
if !ok { if !ok {
@@ -332,12 +192,101 @@ func All() map[string]Meta {
return result return result
} }
func DefaultBaseURL(key string) (string, bool) { func FindAPIConfig(key, apiType string) (APIConfig, bool) {
meta, ok := catalog[key] meta, ok := catalog[key]
if !ok || strings.TrimSpace(meta.DefaultBaseURL) == "" { if !ok {
return APIConfig{}, false
}
target := strings.TrimSpace(apiType)
if target == "" {
target = meta.DefaultAPIType
}
for _, config := range meta.APIConfigs {
if config.APIType == target {
return config, true
}
}
return APIConfig{}, false
}
func ResolveAuthMode(provider, apiType, requested string) (string, error) {
config, ok := FindAPIConfig(provider, apiType)
if !ok {
return "", fmt.Errorf("provider %s does not support api type %s", provider, apiType)
}
if config.APIType != "anthropic-messages" {
return "", nil
}
authMode := strings.TrimSpace(requested)
if authMode == "" {
authMode = config.DefaultAuthMode
}
for _, allowed := range config.AuthModes {
if authMode == allowed {
return authMode, nil
}
}
return "", fmt.Errorf("provider %s does not support auth mode %s", provider, authMode)
}
func DefaultBaseURL(key string) (string, bool) {
config, ok := FindAPIConfig(key, "")
if !ok || strings.TrimSpace(config.BaseURL) == "" {
return "", false return "", false
} }
return meta.DefaultBaseURL, true return config.BaseURL, true
}
func DefaultAPIType(key string) string {
meta, ok := catalog[key]
if !ok || strings.TrimSpace(meta.DefaultAPIType) == "" {
return "openai-completions"
}
return meta.DefaultAPIType
}
func ResolveBaseURL(key, apiType, requested string) (string, error) {
config, ok := FindAPIConfig(key, apiType)
if !ok {
return "", fmt.Errorf("provider %s does not support api type %s", key, apiType)
}
if !config.EditableBaseURL {
return strings.TrimRight(config.BaseURL, "/"), nil
}
baseURL := strings.TrimSpace(requested)
if baseURL == "" {
baseURL = config.BaseURL
}
if baseURL == "" {
return "", fmt.Errorf("base url is required")
}
parsed, err := url.Parse(baseURL)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return "", fmt.Errorf("invalid base url")
}
parsed.Path = normalizeEndpointPath(config.APIType, parsed.Path)
parsed.RawQuery = ""
parsed.Fragment = ""
return strings.TrimRight(parsed.String(), "/"), nil
}
func normalizeEndpointPath(apiType, value string) string {
path := strings.TrimRight(value, "/")
suffixes := []string{}
switch apiType {
case "openai-completions":
suffixes = []string{"/chat/completions"}
case "openai-responses":
suffixes = []string{"/responses"}
case "anthropic-messages":
suffixes = []string{"/v1/messages", "/messages"}
}
for _, suffix := range suffixes {
if strings.HasSuffix(strings.ToLower(path), suffix) {
return path[:len(path)-len(suffix)]
}
}
return path
} }
func EnvKey(key string) string { func EnvKey(key string) string {
@@ -350,89 +299,52 @@ func EnvKey(key string) string {
func DisplayName(key string) string { func DisplayName(key string) string {
meta, ok := catalog[key] meta, ok := catalog[key]
if !ok { if !ok || strings.TrimSpace(meta.DisplayName) == "" {
return key
}
if strings.TrimSpace(meta.DisplayName) == "" {
return key return key
} }
return meta.DisplayName return meta.DisplayName
} }
func FindModel(key, modelID string) (Model, bool) { func NormalizeModelID(provider, modelID string) string {
meta, ok := Get(key) target := strings.TrimLeft(strings.TrimSpace(modelID), "/")
if !ok { for _, prefix := range legacyModelPrefixes[provider] {
return Model{}, false legacyPrefix := prefix + "/"
} if !strings.HasPrefix(target, legacyPrefix) {
for _, item := range meta.Models { continue
if item.ID == modelID { }
return item, true candidate := strings.TrimSpace(strings.TrimPrefix(target, legacyPrefix))
if candidate != "" {
return candidate
} }
} }
return Model{}, false return target
}
var legacyModelPrefixes = map[string][]string{
"custom": {"custom"},
"vllm": {"custom"},
"ollama": {"ollama"},
"deepseek": {"deepseek"},
"bailian-coding-plan": {"bailian-coding-plan"},
"ark-coding-plan": {"ark-coding-plan"},
"zai": {"zai"},
"minimax": {"minimax"},
"xiaomi": {"xiaomi"},
"kimi": {"kimi", "moonshot"},
"kimi-coding": {"kimi-coding"},
"openai": {"openai"},
"anthropic": {"anthropic"},
"gemini": {"google", "gemini"},
"moonshot": {"moonshot"},
} }
func cloneMeta(meta Meta) Meta { func cloneMeta(meta Meta) Meta {
clone := meta clone := meta
if len(meta.Default.Input) > 0 { clone.APIConfigs = make([]APIConfig, len(meta.APIConfigs))
clone.Default.Input = make([]string, len(meta.Default.Input)) for index, config := range meta.APIConfigs {
copy(clone.Default.Input, meta.Default.Input) clone.APIConfigs[index] = config
} clone.APIConfigs[index].AuthModes = append([]string(nil), config.AuthModes...)
if len(meta.Models) > 0 {
clone.Models = make([]Model, len(meta.Models))
for i, item := range meta.Models {
clone.Models[i] = normalizeModel(meta, item)
}
} }
clone.Models = append([]Model(nil), meta.Models...)
return clone return clone
} }
func normalizeModel(meta Meta, model Model) Model {
clone := model
clone.ID = strings.TrimSpace(clone.ID)
clone.Name = strings.TrimSpace(clone.Name)
if clone.Name == "" {
clone.Name = clone.ID
}
if clone.MaxTokens <= 0 {
clone.MaxTokens = meta.Default.MaxTokens
}
if clone.ContextWindow <= 0 {
clone.ContextWindow = meta.Default.ContextWindow
}
if len(clone.Input) == 0 && len(meta.Default.Input) > 0 {
clone.Input = make([]string, len(meta.Default.Input))
copy(clone.Input, meta.Default.Input)
}
return clone
}
func ResolveRuntimeParams(provider, apiType string, maxTokens, contextWindow int) (string, int, int) {
defaultAPIType := "openai-completions"
defaultMaxTokens := 8192
defaultContextWindow := 256000
if meta, ok := Get(provider); ok {
if meta.Default.APIType != "" {
defaultAPIType = meta.Default.APIType
}
if meta.Default.MaxTokens > 0 {
defaultMaxTokens = meta.Default.MaxTokens
}
if meta.Default.ContextWindow > 0 {
defaultContextWindow = meta.Default.ContextWindow
}
}
resolvedAPI := apiType
if strings.TrimSpace(apiType) == "" {
resolvedAPI = defaultAPIType
}
resolvedMaxTokens := defaultMaxTokens
resolvedContextWindow := defaultContextWindow
if maxTokens > 0 {
resolvedMaxTokens = maxTokens
}
if contextWindow > 0 {
resolvedContextWindow = contextWindow
}
return resolvedAPI, resolvedMaxTokens, resolvedContextWindow
}
+64
View File
@@ -0,0 +1,64 @@
package provider
import (
"encoding/json"
"fmt"
"net/http"
"strings"
)
func DiscoverModels(baseURL, apiKey string) ([]string, error) {
req, err := http.NewRequest(http.MethodGet, buildModelDiscoveryURL(baseURL), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := (&http.Client{Timeout: defaultVerifyTimeout}).Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
return nil, fmt.Errorf("request failed: %s", resp.Status)
}
var payload struct {
Data []struct {
ID string `json:"id"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, err
}
models := make([]string, 0, len(payload.Data))
seen := make(map[string]struct{}, len(payload.Data))
for _, item := range payload.Data {
id := strings.TrimSpace(item.ID)
if id == "" {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
models = append(models, id)
}
if len(models) == 0 {
return nil, fmt.Errorf("no models found")
}
return models, nil
}
func buildModelDiscoveryURL(baseURL string) string {
base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
for _, apiType := range []string{"openai-completions", "openai-responses", "anthropic-messages"} {
base = normalizeEndpointPath(apiType, base)
}
switch {
case strings.HasSuffix(base, "/v1/models"):
return base
case strings.HasSuffix(base, "/v1"):
return base + "/models"
default:
return base + "/v1/models"
}
}
+31 -54
View File
@@ -15,72 +15,49 @@ type OpenClawProviderPatch struct {
AuthHeader bool AuthHeader bool
} }
func BuildOpenClawProviderPatch(provider, modelName, apiType, baseURL, apiKey string) (*OpenClawProviderPatch, error) { func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL, apiKey string) (*OpenClawProviderPatch, error) {
if modelName == "" { if modelName == "" {
return nil, fmt.Errorf("model is required") return nil, fmt.Errorf("model is required")
} }
resolvedAPIType, _, _ := ResolveRuntimeParams(provider, apiType, 0, 0) resolvedAPIType := apiType
modelID := resolveOpenClawModelID(provider, modelName) if _, ok := FindAPIConfig(provider, resolvedAPIType); !ok {
switch provider { resolvedAPIType = DefaultAPIType(provider)
case "deepseek":
return newOpenClawProviderPatch(modelName, "deepseek", modelID, apiKey, baseURL, "openai-completions", false), nil
case "gemini":
return newOpenClawProviderPatch("google/"+modelID, "google", modelID, apiKey, baseURL, resolvedAPIType, false), nil
case "moonshot", "kimi":
return buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey), nil
case "bailian-coding-plan":
return newOpenClawProviderPatch("bailian-coding-plan/"+modelID, "bailian-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil
case "ark-coding-plan":
return newOpenClawProviderPatch("ark-coding-plan/"+modelID, "ark-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil
case "minimax":
return newOpenClawProviderPatch("minimax/"+modelID, "minimax", modelID, apiKey, baseURL, "anthropic-messages", true), nil
case "xiaomi":
return newOpenClawProviderPatch("xiaomi/"+modelID, "xiaomi", modelID, apiKey, baseURL, "openai-completions", false), nil
case "custom", "vllm":
return newOpenClawProviderPatch(provider+"/"+modelID, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil
case "ollama":
return newOpenClawProviderPatch(modelName, "ollama", modelID, "ollama", baseURL, resolvedAPIType, false), nil
case "kimi-coding":
return newOpenClawProviderPatch(modelName, "kimi-coding", modelID, apiKey, baseURL, "anthropic-messages", false), nil
case "zai":
return newOpenClawProviderPatch("zai/"+modelID, "zai", modelID, apiKey, baseURL, "openai-completions", false), nil
default:
return newOpenClawProviderPatch(modelName, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil
} }
} resolvedAuthMode, err := ResolveAuthMode(provider, resolvedAPIType, authMode)
if err != nil {
func buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey string) *OpenClawProviderPatch { return nil, err
}
usesBearer := resolvedAuthMode == AuthModeBearer
modelID := NormalizeModelID(provider, modelName)
providerKey := provider providerKey := provider
primaryModel := modelName preserveQualifiedModel := false
if provider == "kimi" { switch provider {
case "gemini":
providerKey = "google"
resolvedAPIType = "google-generative-ai"
usesBearer = false
case "moonshot", "kimi":
providerKey = "moonshot" providerKey = "moonshot"
primaryModel = "moonshot/" + modelID resolvedAPIType = "openai-completions"
usesBearer = false
case "ollama":
apiKey = "ollama"
usesBearer = false
case "openai", "openrouter", "anthropic":
preserveQualifiedModel = strings.Contains(modelName, "/")
} }
return newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, "openai-completions", false)
}
func newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, apiType string, authHeader bool) *OpenClawProviderPatch { primaryModel := providerKey + "/" + modelID
if preserveQualifiedModel {
primaryModel = modelName
}
return &OpenClawProviderPatch{ return &OpenClawProviderPatch{
PrimaryModel: primaryModel, PrimaryModel: primaryModel,
ProviderKey: providerKey, ProviderKey: providerKey,
ModelID: modelID, ModelID: modelID,
APIKey: apiKey, APIKey: apiKey,
BaseURL: baseURL, BaseURL: baseURL,
APIType: apiType, APIType: resolvedAPIType,
AuthHeader: authHeader, AuthHeader: usesBearer,
} }, nil
}
func resolveOpenClawModelID(provider, modelName string) string {
if provider == "custom" || provider == "vllm" {
target := strings.TrimLeft(modelName, "/")
if parts := strings.SplitN(target, "/", 2); len(parts) == 2 && parts[0] == "custom" {
return strings.TrimLeft(parts[1], "/")
}
return target
}
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
return parts[1]
}
return modelName
} }
+69 -119
View File
@@ -3,7 +3,9 @@ package provider
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -18,160 +20,108 @@ type VerifyRequest struct {
Body []byte Body []byte
} }
const ( type verifyErrorResponse struct {
defaultVerifyTimeout = 30 * time.Second Error struct {
) Message string `json:"message"`
} `json:"error"`
Message string `json:"message"`
}
func SkipVerification(key string) bool { const defaultVerifyTimeout = 30 * time.Second
switch key {
case "custom", "vllm", "ollama", "kimi-coding": func SkipVerification(provider string) bool {
switch provider {
case "vllm", "ollama", "kimi-coding":
return true return true
default: default:
return false return false
} }
} }
func VerifyAccount(provider, baseURL, apiKey string) error { func VerifyAccount(provider, apiType, authMode, baseURL, apiKey, model string) error {
req := BuildVerifyRequest(provider, baseURL, apiKey) req := BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model)
var body *bytes.Buffer httpReq, err := http.NewRequest(req.Method, req.URL, bytes.NewReader(req.Body))
if len(req.Body) > 0 {
body = bytes.NewBuffer(req.Body)
} else {
body = bytes.NewBuffer(nil)
}
httpReq, err := http.NewRequest(req.Method, req.URL, body)
if err != nil { if err != nil {
return err return err
} }
for key, value := range req.Headers { for key, value := range req.Headers {
httpReq.Header.Set(key, value) httpReq.Header.Set(key, value)
} }
resp, err := (&http.Client{Timeout: verifyTimeout()}).Do(httpReq) httpReq.Header.Set("Accept", "application/json")
resp, err := (&http.Client{Timeout: defaultVerifyTimeout}).Do(httpReq)
if err != nil { if err != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", err) return buserr.WithErr("ErrAgentAccountUnavailable", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode >= 400 { if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return buserr.WithErr("ErrAgentAccountUnavailable", fmt.Errorf("verify failed: %s", resp.Status)) body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
if readErr != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", readErr)
}
return buserr.WithErr("ErrAgentAccountUnavailable", errors.New(verifyHTTPError(resp.StatusCode, body)))
} }
return nil return nil
} }
func verifyTimeout() time.Duration { func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model string) VerifyRequest {
return defaultVerifyTimeout baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
} headers := map[string]string{"Content-Type": "application/json"}
request := VerifyRequest{Method: http.MethodPost, Headers: headers}
func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest { if provider == "gemini" {
base := strings.TrimRight(strings.TrimSpace(baseURL), "/") request.URL = baseURL + "/v1beta/models/" + strings.TrimSpace(model) + ":generateContent"
headers := map[string]string{}
request := VerifyRequest{Method: http.MethodGet, Headers: headers}
switch provider {
case "anthropic", "kimi-coding":
headers["x-api-key"] = apiKey
headers["anthropic-version"] = "2023-06-01"
if strings.Contains(base, "/v1") {
request.URL = base + "/models"
} else {
request.URL = base + "/v1/models"
}
case "gemini":
request.Method = http.MethodPost
if strings.Contains(base, "/v1beta") {
request.URL = base + "/models/gemini-3-flash-preview:generateContent"
} else {
request.URL = base + "/v1beta/models/gemini-3-flash-preview:generateContent"
}
headers["x-goog-api-key"] = apiKey headers["x-goog-api-key"] = apiKey
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{ request.Body = mustJSON(map[string]interface{}{
"contents": []map[string]interface{}{{ "contents": []map[string]interface{}{{"parts": []map[string]string{{"text": "test"}}}},
"parts": []map[string]string{{
"text": "Explain how AI works in a few words",
}},
}},
}) })
case "zai": return request
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey) }
request.URL = base + "/models"
case "bailian-coding-plan": switch apiType {
request.Method = http.MethodPost case "anthropic-messages":
if !strings.Contains(base, "/v1") { request.URL = baseURL + "/v1/messages"
base = base + "/v1" if authMode == AuthModeBearer {
headers["Authorization"] = "Bearer " + apiKey
} else {
headers["x-api-key"] = apiKey
} }
request.URL = base + "/chat/completions"
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{
"model": "qwen3.5-plus",
"messages": []map[string]string{{"role": "user", "content": "test"}},
"max_tokens": 1,
})
case "ark-coding-plan":
request.Method = http.MethodPost
if !strings.Contains(base, "/api/coding/v3") {
base = "https://ark.cn-beijing.volces.com/api/coding/v3"
}
request.URL = base + "/chat/completions"
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{
"model": "ark-code-latest",
"messages": []map[string]string{{"role": "user", "content": "test"}},
"max_tokens": 1,
})
case "minimax":
request.Method = http.MethodPost
headers["x-api-key"] = apiKey
headers["anthropic-version"] = "2023-06-01" headers["anthropic-version"] = "2023-06-01"
headers["Content-Type"] = "application/json"
if strings.Contains(base, "/v1") {
request.URL = base + "/messages"
} else {
request.URL = base + "/v1/messages"
}
request.Body = mustJSON(map[string]interface{}{ request.Body = mustJSON(map[string]interface{}{
"model": "MiniMax-M2.5", "model": model, "max_tokens": 1, "stream": false,
"max_tokens": 1, "messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
"messages": []map[string]interface{}{{
"role": "user",
"content": []map[string]string{{
"type": "text",
"text": "test",
}},
}},
}) })
case "xiaomi": case "openai-responses":
request.Method = http.MethodPost request.URL = baseURL + "/responses"
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey) headers["Authorization"] = "Bearer " + apiKey
headers["Content-Type"] = "application/json" request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": 1, "stream": false})
if !strings.Contains(base, "/v1") {
base = base + "/v1"
}
request.URL = base + "/chat/completions"
request.Body = mustJSON(map[string]interface{}{
"model": "mimo-v2-flash",
"max_tokens": 1,
"messages": []map[string]string{{"role": "user", "content": "test"}},
})
case "openrouter":
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
if strings.Contains(base, "/v1") {
request.URL = base + "/key"
} else {
request.URL = base + "/v1/key"
}
default: default:
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey) request.URL = baseURL + "/chat/completions"
if strings.Contains(base, "/v1") { if provider != "ollama" || strings.TrimSpace(apiKey) != "" {
request.URL = base + "/models" headers["Authorization"] = "Bearer " + apiKey
} else {
request.URL = base + "/v1/models"
} }
request.Body = mustJSON(map[string]interface{}{
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": 1, "stream": false,
})
} }
return request return request
} }
func verifyHTTPError(statusCode int, body []byte) string {
message := strings.TrimSpace(string(body))
var payload verifyErrorResponse
if err := json.Unmarshal(body, &payload); err == nil {
if value := strings.TrimSpace(payload.Error.Message); value != "" {
message = value
} else if value := strings.TrimSpace(payload.Message); value != "" {
message = value
}
}
if message == "" {
return fmt.Sprintf("validation request returned status %d", statusCode)
}
return message
}
func mustJSON(value interface{}) []byte { func mustJSON(value interface{}) []byte {
payload, err := json.Marshal(value) payload, err := json.Marshal(value)
if err != nil { if err != nil {
+7
View File
@@ -15,6 +15,7 @@ type IAppInstallResourceRpo interface {
WithAppInstallId(appInstallId uint) DBOption WithAppInstallId(appInstallId uint) DBOption
WithLinkId(linkId uint) DBOption WithLinkId(linkId uint) DBOption
WithResourceId(resourceId uint) DBOption WithResourceId(resourceId uint) DBOption
WithResourceIds(resourceIds []uint) DBOption
GetBy(opts ...DBOption) ([]model.AppInstallResource, error) GetBy(opts ...DBOption) ([]model.AppInstallResource, error)
GetFirst(opts ...DBOption) (model.AppInstallResource, error) GetFirst(opts ...DBOption) (model.AppInstallResource, error)
Create(ctx context.Context, resource *model.AppInstallResource) error Create(ctx context.Context, resource *model.AppInstallResource) error
@@ -44,6 +45,12 @@ func (a AppInstallResourceRpo) WithResourceId(resourceId uint) DBOption {
} }
} }
func (a AppInstallResourceRpo) WithResourceIds(resourceIds []uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("resource_id IN ?", resourceIds)
}
}
func (a AppInstallResourceRpo) GetBy(opts ...DBOption) ([]model.AppInstallResource, error) { func (a AppInstallResourceRpo) GetBy(opts ...DBOption) ([]model.AppInstallResource, error) {
db := global.DB.Model(&model.AppInstallResource{}) db := global.DB.Model(&model.AppInstallResource{})
var resources []model.AppInstallResource var resources []model.AppInstallResource
+121
View File
@@ -0,0 +1,121 @@
package repo
import (
"context"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"gorm.io/gorm"
)
type DatabaseUserRepo struct{}
type IDatabaseUserRepo interface {
Get(opts ...DBOption) (model.DatabaseUser, error)
List(opts ...DBOption) ([]model.DatabaseUser, error)
Save(user *model.DatabaseUser) error
Delete(opts ...DBOption) error
DeleteBy(ctx context.Context, opts ...DBOption) error
Update(vars map[string]interface{}, opts ...DBOption) error
WithByDatabase(database string) DBOption
WithByUser(username, host string) DBOption
WithByUserList(users [][2]string) DBOption
}
func NewIDatabaseUserRepo() IDatabaseUserRepo {
return &DatabaseUserRepo{}
}
func (u *DatabaseUserRepo) Get(opts ...DBOption) (model.DatabaseUser, error) {
var user model.DatabaseUser
db := global.DB.Model(&model.DatabaseUser{})
for _, opt := range opts {
db = opt(db)
}
if err := db.First(&user).Error; err != nil {
return user, err
}
password, err := encrypt.StringDecrypt(user.Password)
if err != nil {
global.LOG.Errorf("decrypt database user %s password failed, err: %v", user.Username, err)
}
user.Password = password
return user, nil
}
func (u *DatabaseUserRepo) List(opts ...DBOption) ([]model.DatabaseUser, error) {
var users []model.DatabaseUser
db := global.DB.Model(&model.DatabaseUser{})
for _, opt := range opts {
db = opt(db)
}
if err := db.Find(&users).Error; err != nil {
return users, err
}
for i := 0; i < len(users); i++ {
password, err := encrypt.StringDecrypt(users[i].Password)
if err != nil {
global.LOG.Errorf("decrypt database user %s password failed, err: %v", users[i].Username, err)
}
users[i].Password = password
}
return users, nil
}
func (u *DatabaseUserRepo) Save(user *model.DatabaseUser) error {
if len(user.Password) != 0 {
password, err := encrypt.StringEncrypt(user.Password)
if err != nil {
return fmt.Errorf("encrypt database user %s password failed, err: %v", user.Username, err)
}
user.Password = password
}
return global.DB.Save(user).Error
}
func (u *DatabaseUserRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.DatabaseUser{}).Error
}
func (u *DatabaseUserRepo) DeleteBy(ctx context.Context, opts ...DBOption) error {
return getTx(ctx, opts...).Delete(&model.DatabaseUser{}).Error
}
func (u *DatabaseUserRepo) Update(vars map[string]interface{}, opts ...DBOption) error {
db := global.DB.Model(&model.DatabaseUser{})
for _, opt := range opts {
db = opt(db)
}
return db.Updates(vars).Error
}
func (u *DatabaseUserRepo) WithByDatabase(database string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("database = ?", database)
}
}
func (u *DatabaseUserRepo) WithByUser(username, host string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("username = ? AND host = ?", username, host)
}
}
func (u *DatabaseUserRepo) WithByUserList(users [][2]string) DBOption {
return func(g *gorm.DB) *gorm.DB {
if len(users) == 0 {
return g.Where("1 = 0")
}
values := make([][]interface{}, 0, len(users))
for _, user := range users {
values = append(values, []interface{}{user[0], user[1]})
}
return g.Where("(username, host) IN ?", values)
}
}
+109
View File
@@ -0,0 +1,109 @@
package repo
import (
"context"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
type DatabaseUserGrantRepo struct{}
type IDatabaseUserGrantRepo interface {
Get(opts ...DBOption) (model.DatabaseUserGrant, error)
List(opts ...DBOption) ([]model.DatabaseUserGrant, error)
Save(grant *model.DatabaseUserGrant) error
Replace(dbType, database string, grants []model.DatabaseUserGrant) error
Delete(opts ...DBOption) error
DeleteBy(ctx context.Context, opts ...DBOption) error
Update(vars map[string]interface{}, opts ...DBOption) error
WithByDatabase(database string) DBOption
WithByDBName(dbName string) DBOption
WithByDBNames(dbNames []string) DBOption
WithByUser(username, host string) DBOption
}
func (u *DatabaseUserGrantRepo) Get(opts ...DBOption) (model.DatabaseUserGrant, error) {
var grant model.DatabaseUserGrant
db := global.DB.Model(&model.DatabaseUserGrant{})
for _, opt := range opts {
db = opt(db)
}
err := db.First(&grant).Error
return grant, err
}
func NewIDatabaseUserGrantRepo() IDatabaseUserGrantRepo {
return &DatabaseUserGrantRepo{}
}
func (u *DatabaseUserGrantRepo) List(opts ...DBOption) ([]model.DatabaseUserGrant, error) {
var grants []model.DatabaseUserGrant
db := global.DB.Model(&model.DatabaseUserGrant{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&grants).Error
return grants, err
}
func (u *DatabaseUserGrantRepo) Save(grant *model.DatabaseUserGrant) error {
return global.DB.Save(grant).Error
}
func (u *DatabaseUserGrantRepo) Replace(dbType, database string, grants []model.DatabaseUserGrant) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("`type` = ? AND database = ?", dbType, database).Delete(&model.DatabaseUserGrant{}).Error; err != nil {
return err
}
if len(grants) != 0 {
return tx.Create(&grants).Error
}
return nil
})
}
func (u *DatabaseUserGrantRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.DatabaseUserGrant{}).Error
}
func (u *DatabaseUserGrantRepo) DeleteBy(ctx context.Context, opts ...DBOption) error {
return getTx(ctx, opts...).Delete(&model.DatabaseUserGrant{}).Error
}
func (u *DatabaseUserGrantRepo) Update(vars map[string]interface{}, opts ...DBOption) error {
db := global.DB.Model(&model.DatabaseUserGrant{})
for _, opt := range opts {
db = opt(db)
}
return db.Updates(vars).Error
}
func (u *DatabaseUserGrantRepo) WithByDatabase(database string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("database = ?", database)
}
}
func (u *DatabaseUserGrantRepo) WithByDBName(dbName string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("db_name = ?", dbName)
}
}
func (u *DatabaseUserGrantRepo) WithByDBNames(dbNames []string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("db_name IN ?", dbNames)
}
}
func (u *DatabaseUserGrantRepo) WithByUser(username, host string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("username = ? AND host = ?", username, host)
}
}
+21
View File
@@ -17,10 +17,12 @@ type IWebsiteRepo interface {
WithGroupID(groupId uint) DBOption WithGroupID(groupId uint) DBOption
WithDefaultServer() DBOption WithDefaultServer() DBOption
WithDomainLike(domain string) DBOption WithDomainLike(domain string) DBOption
WithSearchKeyword(keyword string, ids []uint) DBOption
WithRuntimeID(runtimeID uint) DBOption WithRuntimeID(runtimeID uint) DBOption
WithParentID(websiteID uint) DBOption WithParentID(websiteID uint) DBOption
WithType(websiteType string) DBOption WithType(websiteType string) DBOption
WithDBType(dbType string) DBOption WithDBType(dbType string) DBOption
WithDBTypes(dbTypes []string) DBOption
WithDBID(dbID uint) DBOption WithDBID(dbID uint) DBOption
Page(page, size int, opts ...DBOption) (int64, []model.Website, error) Page(page, size int, opts ...DBOption) (int64, []model.Website, error)
@@ -76,6 +78,19 @@ func (w *WebsiteRepo) WithDomainLike(domain string) DBOption {
} }
} }
func (w *WebsiteRepo) WithSearchKeyword(keyword string, ids []uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
if keyword == "" {
return db
}
keyword = "%" + keyword + "%"
if len(ids) == 0 {
return db.Where("(primary_domain like ? OR alias like ?)", keyword, keyword)
}
return db.Where("(primary_domain like ? OR alias like ? OR id in (?))", keyword, keyword, ids)
}
}
func (w *WebsiteRepo) WithAlias(alias string) DBOption { func (w *WebsiteRepo) WithAlias(alias string) DBOption {
return func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB {
return db.Where("alias = ?", alias) return db.Where("alias = ?", alias)
@@ -118,6 +133,12 @@ func (w *WebsiteRepo) WithDBType(dbType string) DBOption {
} }
} }
func (w *WebsiteRepo) WithDBTypes(dbTypes []string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("db_type IN ?", dbTypes)
}
}
func (w *WebsiteRepo) WithDBID(dbID uint) DBOption { func (w *WebsiteRepo) WithDBID(dbID uint) DBOption {
return func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB {
return db.Where("db_id = ?", dbID) return db.Where("db_id = ?", dbID)
+230 -137
View File
@@ -8,8 +8,8 @@ import (
"os" "os"
"path" "path"
"sort" "sort"
"strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -30,6 +30,8 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
var agentAccountMutationMu sync.Mutex
type IAgentService interface { type IAgentService interface {
Create(req dto.AgentCreateReq) (*dto.AgentItem, error) Create(req dto.AgentCreateReq) (*dto.AgentItem, error)
BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error) BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error)
@@ -77,6 +79,7 @@ type IAgentService interface {
PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error)
CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error)
GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error)
CreateAccountModel(req dto.AgentAccountModelCreateReq) error CreateAccountModel(req dto.AgentAccountModelCreateReq) error
UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error
DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error
@@ -144,7 +147,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 { if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 {
return nil, buserr.New("ErrNameIsExist") return nil, buserr.New("ErrNameIsExist")
} }
if !xpack.MultiNodeProvider.IsXpack() { if !global.CONF.Base.IsEnterprise && !xpack.MultiNodeProvider.IsXpack() {
count, _, err := agentRepo.Page(1, 1) count, _, err := agentRepo.Page(1, 1)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -165,8 +168,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
provider := "" provider := ""
baseURL := "" baseURL := ""
apiType := "" apiType := ""
maxTokens := 0
contextWindow := 0
apiKey := "" apiKey := ""
runtimeModel := "" runtimeModel := ""
accountID := uint(0) accountID := uint(0)
@@ -176,6 +177,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
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent { if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
if req.AccountID == 0 { if req.AccountID == 0 {
@@ -196,8 +198,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
} }
storedModel = resolvedRuntime.StoredModel storedModel = resolvedRuntime.StoredModel
apiType = resolvedRuntime.APIType apiType = resolvedRuntime.APIType
maxTokens = resolvedRuntime.MaxTokens
contextWindow = resolvedRuntime.ContextWindow
runtimeModel = resolvedRuntime.PrimaryModel runtimeModel = resolvedRuntime.PrimaryModel
apiKey = account.APIKey apiKey = account.APIKey
accountID = account.ID accountID = account.ID
@@ -222,9 +222,13 @@ 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)
installHooks = &appInstallHooks{ installHooks = &appInstallHooks{
AfterCopyData: func(appInstall *model.AppInstall) error { AfterCopyData: func(appInstall *model.AppInstall) error {
return prepareHermesInstallFiles(appInstall, account, storedModel) if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil {
return err
}
return writeHermesDashboardAuthEnv(path.Join(appInstall.GetPath(), ".env"), hermesAuth, false)
}, },
} }
} }
@@ -242,12 +246,14 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
params["PROVIDER"] = provider params["PROVIDER"] = provider
params["MODEL"] = runtimeModel params["MODEL"] = runtimeModel
params["API_TYPE"] = apiType params["API_TYPE"] = apiType
params["MAX_TOKENS"] = maxTokens
params["CONTEXT_WINDOW"] = contextWindow
params["BASE_URL"] = baseURL params["BASE_URL"] = baseURL
params["API_KEY"] = apiKey params["API_KEY"] = apiKey
params["OPENCLAW_GATEWAY_TOKEN"] = token params["OPENCLAW_GATEWAY_TOKEN"] = token
} }
if agentType == constant.AppHermesAgent {
params[hermesDashboardUsernameEnvKey] = hermesAuth.Username
params[hermesDashboardPasswordEnvKey] = hermesAuth.Password
}
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" { if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
return nil, buserr.New("ErrAgentComposeRequired") return nil, buserr.New("ErrAgentComposeRequired")
@@ -282,22 +288,20 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
configPath = path.Join(appInstall.GetPath(), "data", "config.yaml") configPath = path.Join(appInstall.GetPath(), "data", "config.yaml")
} }
agent := &model.Agent{ agent := &model.Agent{
Name: req.Name, Name: req.Name,
Remark: req.Remark, Remark: req.Remark,
AgentType: agentType, AgentType: agentType,
Provider: provider, Provider: provider,
Model: storedModel, Model: storedModel,
APIType: apiType, APIType: apiType,
MaxTokens: maxTokens, BaseURL: baseURL,
ContextWindow: contextWindow, APIKey: apiKey,
BaseURL: baseURL, Token: token,
APIKey: apiKey, Status: appInstall.Status,
Token: token, Message: appInstall.Message,
Status: appInstall.Status, AppInstallID: appInstall.ID,
Message: appInstall.Message, AccountID: accountID,
AppInstallID: appInstall.ID, ConfigPath: configPath,
AccountID: accountID,
ConfigPath: configPath,
} }
if err := agentRepo.Create(agent); err != nil { if err := agentRepo.Create(agent); err != nil {
return nil, err return nil, err
@@ -395,7 +399,7 @@ func (a AgentService) BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dt
results = append(results, result) results = append(results, result)
continue continue
} }
installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchSkillInstallTaskID(req.TaskID, agent.ID), agent.ID) installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchTaskID(req.TaskID, "batch-skill-install", agent.ID), agent.ID)
if err != nil { if err != nil {
result.Message = err.Error() result.Message = err.Error()
results = append(results, result) results = append(results, result)
@@ -437,7 +441,7 @@ func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBat
if operate == constant.Delete { if operate == constant.Delete {
if err := a.Delete(dto.AgentDeleteReq{ if err := a.Delete(dto.AgentDeleteReq{
ID: agent.ID, ID: agent.ID,
TaskID: buildBatchOperateTaskID(req.TaskID, agent.ID), TaskID: buildBatchTaskID(req.TaskID, "batch-operate", agent.ID),
ForceDelete: req.ForceDelete, ForceDelete: req.ForceDelete,
}); err != nil { }); err != nil {
result.Message = err.Error() result.Message = err.Error()
@@ -465,7 +469,7 @@ func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBat
if err := NewIAppInstalledService().Operate(request.AppInstalledOperate{ if err := NewIAppInstalledService().Operate(request.AppInstalledOperate{
InstallId: install.ID, InstallId: install.ID,
Operate: operate, Operate: operate,
TaskID: buildBatchOperateTaskID(req.TaskID, agent.ID), TaskID: buildBatchTaskID(req.TaskID, "batch-operate", agent.ID),
}); err != nil { }); err != nil {
result.Message = err.Error() result.Message = err.Error()
} else { } else {
@@ -549,35 +553,19 @@ func buildBatchUpgradePlans(req dto.AgentBatchUpgradeReq) ([]batchUpgradePlan, [
DetailID: detail.ID, DetailID: detail.ID,
Backup: req.Backup, Backup: req.Backup,
PullImage: req.PullImage, PullImage: req.PullImage,
TaskID: buildBatchUpgradeTaskID(req.TaskID, install.ID), TaskID: buildBatchTaskID(req.TaskID, "batch-upgrade", install.ID),
}, },
}) })
} }
return plans, results, nil return plans, results, nil
} }
func buildBatchUpgradeTaskID(taskID string, appInstallID uint) string { func buildBatchTaskID(taskID, prefix string, id uint) string {
taskID = strings.TrimSpace(taskID) taskID = strings.TrimSpace(taskID)
if taskID == "" { if taskID == "" {
taskID = fmt.Sprintf("batch-upgrade-%d-%d", appInstallID, time.Now().UnixNano()) taskID = fmt.Sprintf("%s-%d-%d", prefix, id, time.Now().UnixNano())
} }
return fmt.Sprintf("%s-%d", taskID, appInstallID) return fmt.Sprintf("%s-%d", taskID, id)
}
func buildBatchSkillInstallTaskID(taskID string, agentID uint) string {
taskID = strings.TrimSpace(taskID)
if taskID == "" {
taskID = fmt.Sprintf("batch-skill-install-%d-%d", agentID, time.Now().UnixNano())
}
return fmt.Sprintf("%s-%d", taskID, agentID)
}
func buildBatchOperateTaskID(taskID string, agentID uint) string {
taskID = strings.TrimSpace(taskID)
if taskID == "" {
taskID = fmt.Sprintf("batch-operate-%d-%d", agentID, time.Now().UnixNano())
}
return fmt.Sprintf("%s-%d", taskID, agentID)
} }
func batchOperateSkipMessage(operate constant.AppOperate, status string) string { func batchOperateSkipMessage(operate constant.AppOperate, status string) string {
@@ -604,28 +592,30 @@ func batchOperateSkipMessage(operate constant.AppOperate, status string) string
func buildCreateReqFromBatchInstallReq(req dto.AgentBatchInstallReq) dto.AgentCreateReq { func buildCreateReqFromBatchInstallReq(req dto.AgentBatchInstallReq) dto.AgentCreateReq {
return dto.AgentCreateReq{ return dto.AgentCreateReq{
Name: req.Name, Name: req.Name,
Remark: req.Remark, Remark: req.Remark,
AppVersion: req.AppVersion, AppVersion: req.AppVersion,
WebUIPort: req.WebUIPort, WebUIPort: req.WebUIPort,
BridgePort: req.BridgePort, BridgePort: req.BridgePort,
AllowedOrigins: req.AllowedOrigins, AllowedOrigins: req.AllowedOrigins,
AgentType: req.AgentType, AgentType: req.AgentType,
Model: req.Model, Model: req.Model,
AccountID: req.AccountID, AccountID: req.AccountID,
Token: req.Token, Token: req.Token,
TaskID: req.TaskID, DashboardUsername: req.DashboardUsername,
Advanced: req.Advanced, DashboardPassword: req.DashboardPassword,
ContainerName: req.ContainerName, TaskID: req.TaskID,
AllowPort: req.AllowPort, Advanced: req.Advanced,
SpecifyIP: req.SpecifyIP, ContainerName: req.ContainerName,
RestartPolicy: req.RestartPolicy, AllowPort: req.AllowPort,
CpuQuota: req.CpuQuota, SpecifyIP: req.SpecifyIP,
MemoryLimit: req.MemoryLimit, RestartPolicy: req.RestartPolicy,
MemoryUnit: req.MemoryUnit, CpuQuota: req.CpuQuota,
PullImage: req.PullImage, MemoryLimit: req.MemoryLimit,
EditCompose: req.EditCompose, MemoryUnit: req.MemoryUnit,
DockerCompose: req.DockerCompose, PullImage: req.PullImage,
EditCompose: req.EditCompose,
DockerCompose: req.DockerCompose,
} }
} }
@@ -704,6 +694,7 @@ func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (u
account.RememberAPIKey = snapshot.RememberAPIKey account.RememberAPIKey = snapshot.RememberAPIKey
account.BaseURL = snapshot.BaseURL account.BaseURL = snapshot.BaseURL
account.APIType = snapshot.APIType account.APIType = snapshot.APIType
account.AuthMode = snapshot.AuthMode
account.Remark = snapshot.Remark account.Remark = snapshot.Remark
account.Verified = true account.Verified = true
@@ -711,6 +702,11 @@ func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (u
if err != nil { if err != nil {
return 0, err return 0, err
} }
verifyModel, err := resolveAgentAccountVerifyModel(account.Provider, snapshot.VerifyModel, initialModels)
if err != nil {
return 0, err
}
account.VerifyModel = verifyModel
if err := global.DB.Transaction(func(tx *gorm.DB) error { if err := global.DB.Transaction(func(tx *gorm.DB) error {
if account.ID == 0 { if account.ID == 0 {
if err := tx.Create(account).Error; err != nil { if err := tx.Create(account).Error; err != nil {
@@ -948,7 +944,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
return err return err
} }
modelName := resolvedRuntime.StoredModel modelName := resolvedRuntime.StoredModel
apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow apiType := resolvedRuntime.APIType
confDir := path.Dir(agent.ConfigPath) confDir := path.Dir(agent.ConfigPath)
if agent.AgentType == constant.AppHermesAgent { if agent.AgentType == constant.AppHermesAgent {
cfg, err := readHermesConfig(agent.ConfigPath) cfg, err := readHermesConfig(agent.ConfigPath)
@@ -969,8 +965,6 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
agent.Provider = account.Provider agent.Provider = account.Provider
agent.Model = modelName agent.Model = modelName
agent.APIType = apiType agent.APIType = apiType
agent.MaxTokens = maxTokens
agent.ContextWindow = contextWindow
agent.BaseURL = account.BaseURL agent.BaseURL = account.BaseURL
agent.APIKey = account.APIKey agent.APIKey = account.APIKey
agent.AccountID = account.ID agent.AccountID = account.ID
@@ -984,20 +978,29 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
models := make([]dto.ProviderModelInfo, 0, len(def.Models)) models := make([]dto.ProviderModelInfo, 0, len(def.Models))
for _, item := range def.Models { for _, item := range def.Models {
models = append(models, dto.ProviderModelInfo{ models = append(models, dto.ProviderModelInfo{
ID: item.ID, ID: item.ID,
Name: item.Name, Name: item.Name,
ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: append([]string(nil), item.Input...),
}) })
} }
apiTypes := make([]dto.ProviderAPIInfo, 0, len(def.APIConfigs))
for _, item := range def.APIConfigs {
apiTypes = append(apiTypes, dto.ProviderAPIInfo{
APIType: item.APIType,
BaseURL: item.BaseURL,
EditableBaseURL: item.EditableBaseURL,
DefaultAuthMode: item.DefaultAuthMode,
AuthModes: item.AuthModes,
})
}
baseURL, _ := providercatalog.DefaultBaseURL(key)
providers = append(providers, dto.ProviderInfo{ providers = append(providers, dto.ProviderInfo{
Sort: def.Sort, Sort: def.Sort,
Provider: key, Provider: key,
DisplayName: def.DisplayName, DisplayName: def.DisplayName,
BaseURL: def.DefaultBaseURL, BaseURL: baseURL,
Models: models, DefaultAPIType: def.DefaultAPIType,
APITypes: apiTypes,
Models: models,
}) })
} }
sort.Slice(providers, func(i, j int) bool { sort.Slice(providers, func(i, j int) bool {
@@ -1007,11 +1010,21 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
} }
func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error { func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
agentAccountMutationMu.Lock()
defer agentAccountMutationMu.Unlock()
provider := req.Provider provider := req.Provider
if exist, _ := agentAccountRepo.GetFirst(repo.WithByProvider(provider), repo.WithByName(req.Name)); exist != nil && exist.ID > 0 { if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil {
return buserr.New("ErrRecordExist") return err
} }
resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL) initialModels, err := buildInitialAgentAccountModels(&model.AgentAccount{Provider: provider}, req.Models)
if err != nil {
return err
}
verifyModel, err := resolveAgentAccountVerifyModel(provider, req.VerifyModel, initialModels)
if err != nil {
return err
}
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel)
if err != nil { if err != nil {
return err return err
} }
@@ -1021,14 +1034,12 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
APIKey: resolvedInput.APIKey, APIKey: resolvedInput.APIKey,
RememberAPIKey: req.RememberAPIKey, RememberAPIKey: req.RememberAPIKey,
BaseURL: resolvedInput.BaseURL, BaseURL: resolvedInput.BaseURL,
APIType: req.APIType, APIType: resolvedInput.APIType,
AuthMode: resolvedInput.AuthMode,
VerifyModel: verifyModel,
Verified: true, Verified: true,
Remark: req.Remark, Remark: req.Remark,
} }
initialModels, err := buildInitialAgentAccountModels(account, req.Models)
if err != nil {
return err
}
if err := global.DB.Transaction(func(tx *gorm.DB) error { if err := global.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(account).Error; err != nil { if err := tx.Create(account).Error; err != nil {
return err return err
@@ -1045,12 +1056,29 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
} }
func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error { func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
agentAccountMutationMu.Lock()
defer agentAccountMutationMu.Unlock()
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID)) account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
if err != nil { if err != nil {
return err return err
} }
provider := account.Provider provider := account.Provider
resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL) if err := ensureAgentAccountNameAvailable(provider, req.Name, account.ID); err != nil {
return err
}
models, err := loadAgentAccountModels(account)
if err != nil {
return err
}
requestedVerifyModel := req.VerifyModel
if strings.TrimSpace(requestedVerifyModel) == "" {
requestedVerifyModel = account.VerifyModel
}
verifyModel, err := resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
if err != nil {
return err
}
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel)
if err != nil { if err != nil {
return err return err
} }
@@ -1058,7 +1086,9 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
account.APIKey = resolvedInput.APIKey account.APIKey = resolvedInput.APIKey
account.RememberAPIKey = req.RememberAPIKey account.RememberAPIKey = req.RememberAPIKey
account.BaseURL = resolvedInput.BaseURL account.BaseURL = resolvedInput.BaseURL
account.APIType = req.APIType account.APIType = resolvedInput.APIType
account.AuthMode = resolvedInput.AuthMode
account.VerifyModel = verifyModel
account.Remark = req.Remark account.Remark = req.Remark
account.Verified = true account.Verified = true
@@ -1104,17 +1134,33 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
BaseURL: item.BaseURL, BaseURL: item.BaseURL,
Models: nil, Models: nil,
APIType: item.APIType, APIType: item.APIType,
AuthMode: item.AuthMode,
VerifyModel: item.VerifyModel,
Verified: item.Verified, Verified: item.Verified,
Remark: item.Remark, Remark: item.Remark,
CreatedAt: item.CreatedAt, CreatedAt: item.CreatedAt,
}) })
} }
for i := range items { if len(list) > 0 {
models, err := loadAgentAccountModels(&list[i]) accountIDs := make([]uint, 0, len(list))
if err != nil { for _, account := range list {
accountIDs = append(accountIDs, account.ID)
}
var rows []model.AgentAccountModel
if err := global.DB.Where("account_id IN ?", accountIDs).Order("account_id ASC, sort_order ASC, id ASC").Find(&rows).Error; err != nil {
return 0, nil, err return 0, nil, err
} }
items[i].Models = models modelsByAccount := make(map[uint][]dto.AgentAccountModel, len(list))
for _, row := range rows {
modelsByAccount[row.AccountID] = append(modelsByAccount[row.AccountID], dto.AgentAccountModel{
RecordID: row.ID,
ID: strings.TrimSpace(row.Model),
Name: strings.TrimSpace(row.Name),
})
}
for index, account := range list {
items[index].Models = modelsByAccount[account.ID]
}
} }
return count, items, nil return count, items, nil
} }
@@ -1131,6 +1177,21 @@ func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.Agen
return loadAgentAccountModels(account) return loadAgentAccountModels(account)
} }
func (a AgentService) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) {
if req.APIType != "openai-completions" && req.APIType != "openai-responses" {
return nil, buserr.New("ErrAgentAccountModelsRequired")
}
baseURL, err := providercatalog.ResolveBaseURL(req.Provider, req.APIType, req.BaseURL)
if err != nil {
return nil, buserr.WithErr("ErrAgentAccountUnavailable", err)
}
models, err := providercatalog.DiscoverModels(baseURL, req.APIKey)
if err != nil {
return nil, buserr.WithErr("ErrAgentAccountUnavailable", err)
}
return buildDiscoveredAgentAccountModels(models), nil
}
func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) error { func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) error {
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
if err != nil { if err != nil {
@@ -1140,24 +1201,19 @@ func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) err
if err != nil { if err != nil {
return err return err
} }
nextModel := cloneAgentAccountModel(req.Model) nextModel, err := normalizeAgentAccountModel(account, req.Model)
if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok {
return buserr.New("ErrRecordExist")
}
inputPayload, err := json.Marshal(nextModel.Input)
if err != nil { if err != nil {
return err return err
} }
if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok {
return buserr.New("ErrRecordExist")
}
sortOrder := len(models) + 1 sortOrder := len(models) + 1
record := &model.AgentAccountModel{ record := &model.AgentAccountModel{
AccountID: account.ID, AccountID: account.ID,
Model: nextModel.ID, Model: nextModel.ID,
Name: nextModel.Name, Name: nextModel.Name,
ContextWindow: nextModel.ContextWindow, SortOrder: sortOrder,
MaxTokens: nextModel.MaxTokens,
Reasoning: nextModel.Reasoning,
Input: string(inputPayload),
SortOrder: sortOrder,
} }
if err := agentAccountModelRepo.Create(record); err != nil { if err := agentAccountModelRepo.Create(record); err != nil {
return err return err
@@ -1178,7 +1234,11 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
if err != nil { if err != nil {
return err return err
} }
nextModel := cloneAgentAccountModel(req.Model) nextModel, err := normalizeAgentAccountModel(account, req.Model)
if err != nil {
return err
}
nextModel.RecordID = req.Model.RecordID
for _, item := range models { for _, item := range models {
if item.RecordID == req.Model.RecordID { if item.RecordID == req.Model.RecordID {
continue continue
@@ -1198,19 +1258,18 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
if err := ensureAccountModelsNotBound(account, nextModels); err != nil { if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
return err return err
} }
inputPayload, err := json.Marshal(nextModel.Input) previousModelID := record.Model
if err != nil {
return err
}
record.Model = nextModel.ID record.Model = nextModel.ID
record.Name = nextModel.Name record.Name = nextModel.Name
record.ContextWindow = nextModel.ContextWindow
record.MaxTokens = nextModel.MaxTokens
record.Reasoning = nextModel.Reasoning
record.Input = string(inputPayload)
if err := agentAccountModelRepo.Save(record); err != nil { if err := agentAccountModelRepo.Save(record); err != nil {
return err return err
} }
if sameProviderModelID(account.Provider, account.VerifyModel, previousModelID) {
account.VerifyModel = nextModel.ID
if err := agentAccountRepo.Save(account); err != nil {
return err
}
}
terminalai.InvalidateTerminalRuntimeCache() terminalai.InvalidateTerminalRuntimeCache()
terminalai.InvalidateFileAIRuntimeCache() terminalai.InvalidateFileAIRuntimeCache()
return a.syncAgentsByAccount(account) return a.syncAgentsByAccount(account)
@@ -1221,9 +1280,13 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err
if err != nil { if err != nil {
return err return err
} }
if _, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID)); err != nil { record, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID))
if err != nil {
return err return err
} }
if sameProviderModelID(account.Provider, account.VerifyModel, record.Model) {
return buserr.New("ErrAgentVerifyModelInUse")
}
models, err := loadAgentAccountModels(account) models, err := loadAgentAccountModels(account)
if err != nil { if err != nil {
return err return err
@@ -1257,25 +1320,45 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error {
} }
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error { func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
_, err := resolveAgentAccountInput(req.Provider, req.APIKey, req.BaseURL) _, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model)
return err return err
} }
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error { func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
if exists, _ := agentRepo.GetFirst(repo.WithByAccountID(req.ID)); exists != nil && exists.ID > 0 { agentAccountMutationMu.Lock()
return buserr.New("ErrAgentAccountBound") defer agentAccountMutationMu.Unlock()
} if err := global.DB.Transaction(func(tx *gorm.DB) error {
if aiStatus, _ := settingRepo.GetValueByKey("AIStatus"); strings.EqualFold(strings.TrimSpace(aiStatus), constant.StatusEnable) { var agentCount int64
if aiAccountID, _ := settingRepo.GetValueByKey("AIAccountID"); strings.TrimSpace(aiAccountID) == strconv.FormatUint(uint64(req.ID), 10) { if err := tx.Model(&model.Agent{}).Where("account_id = ?", req.ID).Count(&agentCount).Error; err != nil {
return err
}
if agentCount > 0 {
return buserr.New("ErrAgentAccountBound")
}
used, err := agentAccountUsedBySetting(tx, req.ID, "AIStatus", "AIAccountID")
if err != nil {
return err
}
if used {
return buserr.New("ErrTerminalAIAccountInUse") return buserr.New("ErrTerminalAIAccountInUse")
} }
} used, err = agentAccountUsedBySetting(tx, req.ID, "FileAIStatus", "FileAIAccountID")
if err := agentAccountModelRepo.Delete(repo.WithByAccountID(req.ID)); err != nil { if err != nil {
return err
}
if used {
return buserr.New("ErrFileAIAccountInUse")
}
if err := tx.Where("account_id = ?", req.ID).Delete(&model.AgentAccountModel{}).Error; err != nil {
return err
}
return tx.Delete(&model.AgentAccount{}, req.ID).Error
}); err != nil {
return err return err
} }
terminalai.InvalidateTerminalRuntimeCache() terminalai.InvalidateTerminalRuntimeCache()
terminalai.InvalidateFileAIRuntimeCache() terminalai.InvalidateFileAIRuntimeCache()
return agentAccountRepo.DeleteByID(req.ID) return nil
} }
func (a AgentService) GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error) { func (a AgentService) GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error) {
@@ -1327,10 +1410,13 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
if err != nil { if err != nil {
return nil, err return nil, err
} }
auth := readHermesDashboardAuthFromInstall(install)
return &dto.AgentOtherConfig{ return &dto.AgentOtherConfig{
UserTimezone: cfg.Timezone, UserTimezone: cfg.Timezone,
BrowserEnabled: true, BrowserEnabled: true,
NPMRegistry: "https://registry.npmjs.org/", NPMRegistry: "https://registry.npmjs.org/",
DashboardUsername: auth.Username,
DashboardPassword: auth.Password,
}, nil }, nil
} }
conf, err := readOpenclawConfig(agent.ConfigPath) conf, err := readOpenclawConfig(agent.ConfigPath)
@@ -1355,12 +1441,21 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
if err != nil { if err != nil {
return err return err
} }
previousAuth := readHermesDashboardAuthFromInstall(install)
nextAuth := normalizeHermesDashboardAuth(req.DashboardUsername, req.DashboardPassword)
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil { 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 {
return err
}
operate := constant.Restart
if previousAuth.Username != nextAuth.Username || previousAuth.Password != nextAuth.Password {
operate = constant.Rebuild
}
return NewIAppInstalledService().Operate(request.AppInstalledOperate{ return NewIAppInstalledService().Operate(request.AppInstalledOperate{
InstallId: install.ID, InstallId: install.ID,
Operate: constant.Restart, Operate: operate,
}) })
} }
if err := ensureContainerRunning(install.ContainerName); err != nil { if err := ensureContainerRunning(install.ContainerName); err != nil {
@@ -1534,7 +1629,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
return err return err
} }
modelName := resolvedRuntime.StoredModel modelName := resolvedRuntime.StoredModel
apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow apiType := resolvedRuntime.APIType
confDir := path.Dir(agent.ConfigPath) confDir := path.Dir(agent.ConfigPath)
switch agent.AgentType { switch agent.AgentType {
case constant.AppOpenclaw: case constant.AppOpenclaw:
@@ -1562,8 +1657,6 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
agent.Provider = account.Provider agent.Provider = account.Provider
agent.Model = modelName agent.Model = modelName
agent.APIType = apiType agent.APIType = apiType
agent.MaxTokens = maxTokens
agent.ContextWindow = contextWindow
_ = agentRepo.Save(&agent) _ = agentRepo.Save(&agent)
} }
return nil return nil
+27 -6
View File
@@ -811,9 +811,7 @@ func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig
result.RequireMention = result.GroupPolicy == "allowlist" result.RequireMention = result.GroupPolicy == "allowlist"
result.GroupAllowFrom = extractStringList(telegram["groupAllowFrom"]) result.GroupAllowFrom = extractStringList(telegram["groupAllowFrom"])
result.Proxy = extractStringValue(telegram["proxy"]) result.Proxy = extractStringValue(telegram["proxy"])
if streaming := extractStringValue(telegram["streaming"]); streaming != "" { result.Streaming = normalizeTelegramStreamingMode(telegram["streaming"], result.Streaming)
result.Streaming = streaming
}
accounts := childMap(telegram, "accounts") accounts := childMap(telegram, "accounts")
if len(accounts) == 0 { if len(accounts) == 0 {
botToken := extractStringValue(telegram["botToken"]) botToken := extractStringValue(telegram["botToken"])
@@ -839,7 +837,7 @@ func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig
BotToken: extractStringValue(account["botToken"]), BotToken: extractStringValue(account["botToken"]),
DmPolicy: extractStringValue(account["dmPolicy"]), DmPolicy: extractStringValue(account["dmPolicy"]),
GroupPolicy: extractStringValue(account["groupPolicy"]), GroupPolicy: extractStringValue(account["groupPolicy"]),
Streaming: extractStringValue(account["streaming"]), Streaming: normalizeTelegramStreamingMode(account["streaming"], result.Streaming),
}) })
} }
result.DefaultAccount = normalizeDefaultAccount(extractStringValue(telegram["defaultAccount"]), getTelegramBotAccountIDs(bots)) result.DefaultAccount = normalizeDefaultAccount(extractStringValue(telegram["defaultAccount"]), getTelegramBotAccountIDs(bots))
@@ -879,7 +877,7 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
} else { } else {
delete(telegram, "proxy") delete(telegram, "proxy")
} }
telegram["streaming"] = config.Streaming telegram["streaming"] = buildTelegramStreamingConfig(config.Streaming)
accounts := make(map[string]interface{}, len(config.Bots)) accounts := make(map[string]interface{}, len(config.Bots))
for _, bot := range config.Bots { for _, bot := range config.Bots {
account := map[string]interface{}{ account := map[string]interface{}{
@@ -888,7 +886,7 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
"botToken": bot.BotToken, "botToken": bot.BotToken,
"dmPolicy": bot.DmPolicy, "dmPolicy": bot.DmPolicy,
"groupPolicy": bot.GroupPolicy, "groupPolicy": bot.GroupPolicy,
"streaming": bot.Streaming, "streaming": buildTelegramStreamingConfig(bot.Streaming),
} }
if bot.DmPolicy == "open" { if bot.DmPolicy == "open" {
account["allowFrom"] = []string{"*"} account["allowFrom"] = []string{"*"}
@@ -899,6 +897,29 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
delete(telegram, "botToken") delete(telegram, "botToken")
} }
func normalizeTelegramStreamingMode(value interface{}, defaultMode string) string {
mode := defaultMode
switch typed := value.(type) {
case string:
mode = typed
case map[string]interface{}:
mode = extractStringValue(typed["mode"])
}
mode = strings.ToLower(strings.TrimSpace(mode))
switch mode {
case "off", "partial", "block", "progress":
return mode
default:
return "partial"
}
}
func buildTelegramStreamingConfig(mode string) map[string]interface{} {
return map[string]interface{}{
"mode": normalizeTelegramStreamingMode(mode, "partial"),
}
}
func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig { func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig {
result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", AllowFrom: []string{}, RequireMention: false, GroupPolicy: "open"} result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", AllowFrom: []string{}, RequireMention: false, GroupPolicy: "open"}
discord := getChannelConfig(conf, "discord") discord := getChannelConfig(conf, "discord")
+84 -68
View File
@@ -13,14 +13,19 @@ 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/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/common"
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
"github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/joho/godotenv"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
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"`
@@ -71,7 +76,7 @@ func writeHermesConfig(confDir string, account *model.AgentAccount, modelName st
} }
} }
provider := resolveHermesProvider(account.Provider) provider := resolveHermesProvider(account.Provider, account.APIType)
configPath := path.Join(confDir, "config.yaml") configPath := path.Join(confDir, "config.yaml")
cfg, err := readHermesConfigMap(configPath) cfg, err := readHermesConfigMap(configPath)
if err != nil { if err != nil {
@@ -112,6 +117,52 @@ 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 {
@@ -152,7 +203,7 @@ func writeHermesConfigMap(configPath string, cfg map[string]interface{}) error {
} }
func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig, error) { func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env")) envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -209,7 +260,7 @@ func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig,
func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramConfig) error { func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readHermesEnvMap(envPath) envMap, err := readAgentEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -229,7 +280,7 @@ func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramCo
envMap["TELEGRAM_ALLOWED_USERS"] = allow envMap["TELEGRAM_ALLOWED_USERS"] = allow
} }
} }
if err := writeHermesEnvMap(envPath, envMap, []string{ if err := writeAgentEnvMap(envPath, envMap, []string{
"TELEGRAM_BOT_TOKEN", "TELEGRAM_BOT_TOKEN",
"TELEGRAM_ALLOWED_USERS", "TELEGRAM_ALLOWED_USERS",
"TELEGRAM_ALLOW_ALL_USERS", "TELEGRAM_ALLOW_ALL_USERS",
@@ -248,7 +299,7 @@ func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramCo
} }
func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, error) { func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env")) envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -300,7 +351,7 @@ func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, er
func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConfig) error { func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readHermesEnvMap(envPath) envMap, err := readAgentEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -320,7 +371,7 @@ func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConf
envMap["DISCORD_ALLOWED_USERS"] = allow envMap["DISCORD_ALLOWED_USERS"] = allow
} }
} }
if err := writeHermesEnvMap(envPath, envMap, []string{ if err := writeAgentEnvMap(envPath, envMap, []string{
"DISCORD_BOT_TOKEN", "DISCORD_BOT_TOKEN",
"DISCORD_ALLOWED_USERS", "DISCORD_ALLOWED_USERS",
"DISCORD_ALLOW_ALL_USERS", "DISCORD_ALLOW_ALL_USERS",
@@ -340,14 +391,14 @@ func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConf
func deleteHermesEnvKeys(confDir string, keys ...string) error { func deleteHermesEnvKeys(confDir string, keys ...string) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readHermesEnvMap(envPath) envMap, err := readAgentEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
for _, key := range keys { for _, key := range keys {
delete(envMap, key) delete(envMap, key)
} }
return writeHermesEnvMap(envPath, envMap, keys) return writeAgentEnvMap(envPath, envMap, keys)
} }
func deleteHermesConfigSections(confDir string, topLevelKeys []string, platformKeys []string) error { func deleteHermesConfigSections(confDir string, topLevelKeys []string, platformKeys []string) error {
@@ -407,11 +458,18 @@ func normalizeHermesTimezone(timezone string) string {
return timezone return timezone
} }
func resolveHermesProvider(provider string) string { func resolveHermesProvider(provider, apiType string) string {
if apiType == "anthropic-messages" && (provider == "deepseek" || provider == "bailian-coding-plan" || provider == "ark-coding-plan" || provider == "xiaomi") {
return "anthropic"
}
switch provider { switch provider {
case "": case "":
return "custom" return "custom"
case "openrouter", "anthropic", "gemini", "zai", "kimi-coding", "xiaomi": case "moonshot":
return "kimi-coding"
case "kimi":
return "kimi-coding-cn"
case "openrouter", "anthropic", "deepseek", "gemini", "zai", "kimi-coding", "xiaomi":
return provider return provider
case "minimax": case "minimax":
return "minimax-cn" return "minimax-cn"
@@ -428,17 +486,7 @@ func resolveHermesModel(sourceProvider, targetProvider, modelName string) string
if targetProvider != "custom" { if targetProvider != "custom" {
return target return target
} }
if sourceProvider == "custom" || sourceProvider == "vllm" { return providercatalog.NormalizeModelID(sourceProvider, target)
return normalizeCustomModel(target)
}
if strings.Contains(target, "/") {
parts := strings.SplitN(target, "/", 2)
model := strings.TrimSpace(parts[1])
if model != "" {
return model
}
}
return target
} }
func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) (string, error) { func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) (string, error) {
@@ -449,7 +497,7 @@ func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels [
if configuredModel == "" { if configuredModel == "" {
return "", buserr.New("ErrAgentModelNotInAccount") return "", buserr.New("ErrAgentModelNotInAccount")
} }
provider := resolveHermesProvider(account.Provider) provider := resolveHermesProvider(account.Provider, account.APIType)
for _, item := range accountModels { for _, item := range accountModels {
if resolveHermesModel(account.Provider, provider, item.ID) == configuredModel { if resolveHermesModel(account.Provider, provider, item.ID) == configuredModel {
return item.ID, nil return item.ID, nil
@@ -462,7 +510,8 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
if account == nil { if account == nil {
return nil return nil
} }
if resolveHermesProvider(account.Provider) == "custom" { resolvedProvider := resolveHermesProvider(account.Provider, account.APIType)
if resolvedProvider == "custom" {
if account.APIKey == "" { if account.APIKey == "" {
return nil return nil
} }
@@ -480,6 +529,11 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
} }
entries = append(entries, hermesEnvEntry{Key: key, Value: value}) entries = append(entries, hermesEnvEntry{Key: key, Value: value})
} }
if resolvedProvider == "anthropic" {
appendEntry("ANTHROPIC_API_KEY", apiKey)
appendEntry("ANTHROPIC_BASE_URL", baseURL)
return entries
}
switch account.Provider { switch account.Provider {
case "openrouter": case "openrouter":
@@ -487,6 +541,7 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
appendEntry("OPENROUTER_BASE_URL", baseURL) appendEntry("OPENROUTER_BASE_URL", baseURL)
case "anthropic": case "anthropic":
appendEntry("ANTHROPIC_API_KEY", apiKey) appendEntry("ANTHROPIC_API_KEY", apiKey)
appendEntry("ANTHROPIC_BASE_URL", baseURL)
case "gemini": case "gemini":
appendEntry("GOOGLE_API_KEY", apiKey) appendEntry("GOOGLE_API_KEY", apiKey)
appendEntry("GEMINI_API_KEY", apiKey) appendEntry("GEMINI_API_KEY", apiKey)
@@ -532,7 +587,7 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
} }
func writeHermesModelEnv(envPath string, account *model.AgentAccount) error { func writeHermesModelEnv(envPath string, account *model.AgentAccount) error {
envMap, err := readHermesEnvMap(envPath) envMap, err := readAgentEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -548,7 +603,7 @@ func writeHermesModelEnv(envPath string, account *model.AgentAccount) error {
envMap[entry.Key] = entry.Value envMap[entry.Key] = entry.Value
order = append(order, entry.Key) order = append(order, entry.Key)
} }
return writeHermesEnvMap(envPath, envMap, order) return writeAgentEnvMap(envPath, envMap, order)
} }
func hermesManagedModelEnvKeys() []string { func hermesManagedModelEnvKeys() []string {
@@ -556,6 +611,7 @@ func hermesManagedModelEnvKeys() []string {
"OPENROUTER_API_KEY", "OPENROUTER_API_KEY",
"OPENROUTER_BASE_URL", "OPENROUTER_BASE_URL",
"ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY",
"ANTHROPIC_BASE_URL",
"GOOGLE_API_KEY", "GOOGLE_API_KEY",
"GEMINI_API_KEY", "GEMINI_API_KEY",
"GEMINI_BASE_URL", "GEMINI_BASE_URL",
@@ -598,46 +654,6 @@ func hermesManagedModelEnvKeys() []string {
return result return result
} }
func writeHermesEnv(envPath string, entries []hermesEnvEntry) error {
envMap, err := readHermesEnvMap(envPath)
if err != nil {
return err
}
for _, entry := range entries {
if entry.Key == "" || entry.Value == "" {
continue
}
envMap[entry.Key] = entry.Value
}
order := make([]string, 0, len(entries))
for _, entry := range entries {
if entry.Key == "" {
continue
}
order = append(order, entry.Key)
}
return writeHermesEnvMap(envPath, envMap, order)
}
func readHermesEnvMap(envPath string) (map[string]string, error) {
fileOp := files.NewFileOp()
if !fileOp.Stat(envPath) {
return map[string]string{}, nil
}
envMap, err := godotenv.Read(envPath)
if err != nil {
return nil, err
}
return envMap, nil
}
func writeHermesEnvMap(envPath string, envMap map[string]string, order []string) error {
if len(envMap) == 0 {
return files.NewFileOp().SaveFile(envPath, "", 0600)
}
return agentenv.WriteWithOrder(envMap, envPath, order)
}
func splitHermesEnvList(value string) []string { func splitHermesEnvList(value string) []string {
if value == "" { if value == "" {
return []string{} return []string{}
+13 -13
View File
@@ -34,7 +34,7 @@ print('Restart the Hermes-Agent container to apply the new Weixin settings.')
` `
func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error) { func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env")) envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -78,7 +78,7 @@ func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error)
func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig) error { func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readHermesEnvMap(envPath) envMap, err := readAgentEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -102,7 +102,7 @@ func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig)
envMap["QQ_GROUP_ALLOWED_USERS"] = allow envMap["QQ_GROUP_ALLOWED_USERS"] = allow
} }
} }
if err := writeHermesEnvMap(envPath, envMap, []string{ if err := writeAgentEnvMap(envPath, envMap, []string{
"QQ_APP_ID", "QQ_APP_ID",
"QQ_CLIENT_SECRET", "QQ_CLIENT_SECRET",
"QQ_ALLOW_ALL_USERS", "QQ_ALLOW_ALL_USERS",
@@ -163,7 +163,7 @@ func deleteHermesQQBotChannelConfig(confDir string) error {
} }
func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error) { func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env")) envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -194,7 +194,7 @@ func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error)
func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig) error { func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readHermesEnvMap(envPath) envMap, err := readAgentEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -229,7 +229,7 @@ func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig)
envMap["WECOM_GROUP_ALLOWED_USERS"] = allow envMap["WECOM_GROUP_ALLOWED_USERS"] = allow
} }
} }
if err := writeHermesEnvMap(envPath, envMap, []string{ if err := writeAgentEnvMap(envPath, envMap, []string{
"WECOM_BOT_ID", "WECOM_BOT_ID",
"WECOM_SECRET", "WECOM_SECRET",
"WECOM_DM_POLICY", "WECOM_DM_POLICY",
@@ -282,7 +282,7 @@ func deleteHermesWecomChannelConfig(confDir string) error {
} }
func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig, error) { func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env")) envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -332,7 +332,7 @@ func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig,
func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkConfig) error { func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readHermesEnvMap(envPath) envMap, err := readAgentEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -356,7 +356,7 @@ func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkCo
} else if config.DmPolicy == "open" { } else if config.DmPolicy == "open" {
envMap["DINGTALK_ALLOW_ALL_USERS"] = "true" envMap["DINGTALK_ALLOW_ALL_USERS"] = "true"
} }
if err := writeHermesEnvMap(envPath, envMap, []string{ if err := writeAgentEnvMap(envPath, envMap, []string{
"DINGTALK_CLIENT_ID", "DINGTALK_CLIENT_ID",
"DINGTALK_CLIENT_SECRET", "DINGTALK_CLIENT_SECRET",
"DINGTALK_ALLOW_ALL_USERS", "DINGTALK_ALLOW_ALL_USERS",
@@ -398,7 +398,7 @@ func deleteHermesDingTalkChannelConfig(confDir string) error {
} }
func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, error) { func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env")) envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -452,7 +452,7 @@ func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, erro
func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig) error { func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readHermesEnvMap(envPath) envMap, err := readAgentEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -488,7 +488,7 @@ func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig
envMap["FEISHU_ALLOWED_USERS"] = allow envMap["FEISHU_ALLOWED_USERS"] = allow
} }
} }
if err := writeHermesEnvMap(envPath, envMap, []string{ if err := writeAgentEnvMap(envPath, envMap, []string{
"FEISHU_APP_ID", "FEISHU_APP_ID",
"FEISHU_APP_SECRET", "FEISHU_APP_SECRET",
"FEISHU_DOMAIN", "FEISHU_DOMAIN",
@@ -529,7 +529,7 @@ func deleteHermesFeishuChannelConfig(confDir string) error {
} }
func readHermesWeixinChannelConfig(confDir string) (*dto.AgentWeixinConfig, error) { func readHermesWeixinChannelConfig(confDir string) (*dto.AgentWeixinConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env")) envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
+180 -268
View File
@@ -4,10 +4,12 @@ import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -20,8 +22,10 @@ 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/common" "github.com/1Panel-dev/1Panel/agent/utils/common"
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
"github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/req_helper" "github.com/1Panel-dev/1Panel/agent/utils/req_helper"
"github.com/joho/godotenv"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -35,6 +39,39 @@ type resolvedAgentAccountInput struct {
Provider string Provider string
APIKey string APIKey string
BaseURL string BaseURL string
APIType string
AuthMode string
}
func ensureAgentAccountNameAvailable(provider, name string, excludeID uint) error {
opts := []repo.DBOption{repo.WithByProvider(provider), repo.WithByName(name)}
if excludeID > 0 {
opts = append(opts, repo.WithByNOTID(excludeID))
}
account, err := agentAccountRepo.GetFirst(opts...)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
if account != nil && account.ID > 0 {
return buserr.New("ErrRecordExist")
}
return nil
}
func agentAccountUsedBySetting(tx *gorm.DB, accountID uint, statusKey, accountIDKey string) (bool, error) {
var settings []model.Setting
if err := tx.Where("key IN ?", []string{statusKey, accountIDKey}).Find(&settings).Error; err != nil {
return false, err
}
values := make(map[string]string, len(settings))
for _, setting := range settings {
values[setting.Key] = setting.Value
}
return strings.EqualFold(strings.TrimSpace(values[statusKey]), constant.StatusEnable) &&
strings.TrimSpace(values[accountIDKey]) == strconv.FormatUint(uint64(accountID), 10), nil
} }
func loadOpenclawAgentByID(agentID uint) (*model.Agent, error) { func loadOpenclawAgentByID(agentID uint) (*model.Agent, error) {
@@ -59,19 +96,26 @@ func ensureContainerRunning(containerName string) error {
return nil return nil
} }
func resolveAgentAccountInput(provider, apiKey, baseURL string) (resolvedAgentAccountInput, error) { func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string) (resolvedAgentAccountInput, error) {
resolvedAPIKey := strings.TrimSpace(apiKey) resolvedAPIKey := strings.TrimSpace(apiKey)
resolvedBaseURL := strings.TrimSpace(baseURL) resolvedAPIType := strings.TrimSpace(apiType)
if resolvedBaseURL == "" { resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
if requiresInitialAgentAccountModels(provider) { if err != nil {
return resolvedAgentAccountInput{}, err
}
resolvedBaseURL, err := providercatalog.ResolveBaseURL(provider, resolvedAPIType, baseURL)
if err != nil {
if strings.Contains(err.Error(), "base url is required") {
return resolvedAgentAccountInput{}, buserr.New("ErrAgentBaseURLRequired") return resolvedAgentAccountInput{}, buserr.New("ErrAgentBaseURLRequired")
} }
if defaultURL, ok := providercatalog.DefaultBaseURL(provider); ok { return resolvedAgentAccountInput{}, err
resolvedBaseURL = defaultURL }
} modelID = strings.TrimSpace(modelID)
if modelID == "" {
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
} }
if !providercatalog.SkipVerification(provider) { if !providercatalog.SkipVerification(provider) {
if err := providercatalog.VerifyAccount(provider, resolvedBaseURL, resolvedAPIKey); err != nil { if err := providercatalog.VerifyAccount(provider, resolvedAPIType, resolvedAuthMode, resolvedBaseURL, resolvedAPIKey, modelID); err != nil {
return resolvedAgentAccountInput{}, err return resolvedAgentAccountInput{}, err
} }
} }
@@ -79,6 +123,8 @@ func resolveAgentAccountInput(provider, apiKey, baseURL string) (resolvedAgentAc
Provider: provider, Provider: provider,
APIKey: resolvedAPIKey, APIKey: resolvedAPIKey,
BaseURL: resolvedBaseURL, BaseURL: resolvedBaseURL,
APIType: resolvedAPIType,
AuthMode: resolvedAuthMode,
}, nil }, nil
} }
@@ -335,26 +381,24 @@ func setOtherConfig(conf map[string]interface{}, config dto.AgentOtherConfig) {
func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem { func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem {
agentType := agent.AgentType agentType := agent.AgentType
item := dto.AgentItem{ item := dto.AgentItem{
ID: agent.ID, ID: agent.ID,
Name: agent.Name, Name: agent.Name,
Remark: agent.Remark, Remark: agent.Remark,
AgentType: agentType, AgentType: agentType,
Provider: agent.Provider, Provider: agent.Provider,
ProviderName: providercatalog.DisplayName(agent.Provider), ProviderName: providercatalog.DisplayName(agent.Provider),
Model: agent.Model, Model: agent.Model,
APIType: agent.APIType, APIType: agent.APIType,
MaxTokens: agent.MaxTokens, BaseURL: agent.BaseURL,
ContextWindow: agent.ContextWindow, APIKey: maskKey(agent.APIKey),
BaseURL: agent.BaseURL, Token: agent.Token,
APIKey: maskKey(agent.APIKey), Status: agent.Status,
Token: agent.Token, Message: agent.Message,
Status: agent.Status, AppInstallID: agent.AppInstallID,
Message: agent.Message, WebsiteID: agent.WebsiteID,
AppInstallID: agent.AppInstallID, AccountID: agent.AccountID,
WebsiteID: agent.WebsiteID, ConfigPath: agent.ConfigPath,
AccountID: agent.AccountID, CreatedAt: agent.CreatedAt,
ConfigPath: agent.ConfigPath,
CreatedAt: agent.CreatedAt,
} }
if appInstall != nil && appInstall.ID > 0 { if appInstall != nil && appInstall.ID > 0 {
item.Container = appInstall.ContainerName item.Container = appInstall.ContainerName
@@ -376,6 +420,11 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
item.BridgePort = toInt(bridge) item.BridgePort = toInt(bridge)
} }
} }
if agentType == constant.AppHermesAgent {
auth := readHermesDashboardAuthFromInstall(appInstall)
item.DashboardUsername = auth.Username
item.DashboardPassword = auth.Password
}
} }
return item return item
} }
@@ -677,13 +726,9 @@ type modelProvider struct {
} }
type modelEntry struct { type modelEntry struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Reasoning bool `json:"reasoning"` Input []string `json:"input,omitempty"`
Input []string `json:"input"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Cost modelCost `json:"cost"`
} }
func requiresOpenclawProviderModels(provider string) bool { func requiresOpenclawProviderModels(provider string) bool {
@@ -703,13 +748,6 @@ func applyOpenclawModelsConfig(conf map[string]interface{}, models *modelsConfig
return nil return nil
} }
type modelCost struct {
Input float64 `json:"input"`
Output float64 `json:"output"`
CacheRead float64 `json:"cacheRead"`
CacheWrite float64 `json:"cacheWrite"`
}
type browserConfig struct { type browserConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
ExecutablePath string `json:"executablePath"` ExecutablePath string `json:"executablePath"`
@@ -860,13 +898,15 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
if err := writeOpenclawConfigRaw(configPath, conf); err != nil { if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
return err return err
} }
envPath := path.Join(confDir, ".env") envMap := map[string]string{
lines := []string{fmt.Sprintf("OPENCLAW_GATEWAY_TOKEN=%s", token)} "OPENCLAW_GATEWAY_TOKEN": token,
if envKey := providercatalog.EnvKey(account.Provider); envKey != "" && account.APIKey != "" {
lines = append(lines, fmt.Sprintf("%s=%s", envKey, account.APIKey))
} }
content := strings.Join(lines, "\n") + "\n" order := []string{"OPENCLAW_GATEWAY_TOKEN"}
return fileOp.SaveFile(envPath, content, 0600) if envKey := providercatalog.EnvKey(account.Provider); envKey != "" && account.APIKey != "" {
envMap[envKey] = account.APIKey
order = append(order, envKey)
}
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order)
} }
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) { func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
@@ -1053,7 +1093,7 @@ func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel s
} }
func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) { func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) {
providerPatch, err := providercatalog.BuildOpenClawProviderPatch(account.Provider, model.ID, account.APIType, account.BaseURL, account.APIKey) providerPatch, err := providercatalog.BuildOpenClawProviderPatch(account.Provider, model.ID, account.APIType, account.AuthMode, account.BaseURL, account.APIKey)
if err != nil { if err != nil {
return "", modelEntry{}, "", modelProvider{}, err return "", modelEntry{}, "", modelProvider{}, err
} }
@@ -1065,28 +1105,24 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen
}, nil }, nil
} }
var openclawVisionModelPattern = regexp.MustCompile(`(?i)(\b(gpt-4o|gpt-4\.1|gpt-[5-9]|o[134])\b|\bclaude-(3|4|sonnet|opus|haiku)\b|\bgemini\b|\b(qwen[\w.-]*-?vl|qwen-vl|qwen3\.[5-9]-plus)\b|\b(kimi-k2\.(5|6)|kimi-k2\.7-code|minimax-m3)\b|\b(vision|llava|pixtral|internvl|mllama|minicpm-v|glm-4v|omni)\b|(^|[-_/])vl([-_/]|$))`)
func buildOpenclawModelEntry(modelID string, model dto.AgentAccountModel) modelEntry { func buildOpenclawModelEntry(modelID string, model dto.AgentAccountModel) modelEntry {
name := strings.TrimSpace(model.Name) name := strings.TrimSpace(model.Name)
if name == "" { if name == "" {
name = strings.TrimSpace(modelID) name = strings.TrimSpace(modelID)
} }
return modelEntry{ entry := modelEntry{ID: strings.TrimSpace(modelID), Name: name}
ID: strings.TrimSpace(modelID), if openclawVisionModelPattern.MatchString(modelID) {
Name: name, entry.Input = []string{"text", "image"}
Reasoning: model.Reasoning,
Input: sanitizeAgentAccountModelInputs(model.Input),
ContextWindow: model.ContextWindow,
MaxTokens: model.MaxTokens,
Cost: modelCost{},
} }
return entry
} }
type openclawAccountModelRuntime struct { type openclawAccountModelRuntime struct {
StoredModel string StoredModel string
PrimaryModel string PrimaryModel string
APIType string APIType string
MaxTokens int
ContextWindow int
} }
func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.AgentAccountModel) (openclawAccountModelRuntime, error) { func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.AgentAccountModel) (openclawAccountModelRuntime, error) {
@@ -1095,11 +1131,9 @@ func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.Age
return openclawAccountModelRuntime{}, err return openclawAccountModelRuntime{}, err
} }
return openclawAccountModelRuntime{ return openclawAccountModelRuntime{
StoredModel: model.ID, StoredModel: model.ID,
PrimaryModel: primaryModel, PrimaryModel: primaryModel,
APIType: account.APIType, APIType: account.APIType,
MaxTokens: model.MaxTokens,
ContextWindow: model.ContextWindow,
}, nil }, nil
} }
@@ -1119,15 +1153,11 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto
if account == nil { if account == nil {
return nil, fmt.Errorf("account is required") return nil, fmt.Errorf("account is required")
} }
if requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 { if account.Provider != "custom" && requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 {
return nil, buserr.New("ErrAgentAccountSingleInitialModel") return nil, buserr.New("ErrAgentAccountSingleInitialModel")
} }
if len(requested) > 0 { if len(requested) > 0 {
models := make([]dto.AgentAccountModel, 0, len(requested)) return normalizeAgentAccountModels(account, requested)
for _, item := range requested {
models = append(models, cloneAgentAccountModel(item))
}
return models, nil
} }
meta, ok := providercatalog.Get(account.Provider) meta, ok := providercatalog.Get(account.Provider)
if !ok || len(meta.Models) == 0 { if !ok || len(meta.Models) == 0 {
@@ -1139,15 +1169,22 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto
requested = make([]dto.AgentAccountModel, 0, len(meta.Models)) requested = make([]dto.AgentAccountModel, 0, len(meta.Models))
for _, item := range meta.Models { for _, item := range meta.Models {
requested = append(requested, dto.AgentAccountModel{ requested = append(requested, dto.AgentAccountModel{
ID: item.ID, ID: item.ID,
Name: item.Name, Name: item.Name,
ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: append([]string(nil), item.Input...),
}) })
} }
return requested, nil return normalizeAgentAccountModels(account, requested)
}
func buildDiscoveredAgentAccountModels(modelIDs []string) []dto.AgentAccountModel {
models := make([]dto.AgentAccountModel, 0, len(modelIDs))
for _, modelID := range modelIDs {
models = append(models, dto.AgentAccountModel{
ID: modelID,
Name: modelID,
})
}
return models
} }
func compactPersistedAgentAccountModelSortOrder(accountID uint) error { func compactPersistedAgentAccountModelSortOrder(accountID uint) error {
@@ -1175,18 +1212,6 @@ func loadAgentAccountModels(account *model.AgentAccount) ([]dto.AgentAccountMode
return listPersistedAgentAccountModels(account.ID) return listPersistedAgentAccountModels(account.ID)
} }
func cloneAgentAccountModel(model dto.AgentAccountModel) dto.AgentAccountModel {
return dto.AgentAccountModel{
RecordID: model.RecordID,
ID: model.ID,
Name: model.Name,
ContextWindow: model.ContextWindow,
MaxTokens: model.MaxTokens,
Reasoning: model.Reasoning,
Input: append([]string(nil), model.Input...),
}
}
func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, existing []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) { func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, existing []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) {
if account == nil { if account == nil {
return nil, fmt.Errorf("account is required") return nil, fmt.Errorf("account is required")
@@ -1210,12 +1235,8 @@ func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, exi
continue continue
} }
requested = append(requested, dto.AgentAccountModel{ requested = append(requested, dto.AgentAccountModel{
ID: item.ID, ID: item.ID,
Name: item.Name, Name: item.Name,
ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: append([]string(nil), item.Input...),
}) })
} }
if len(requested) == len(existing) { if len(requested) == len(existing) {
@@ -1234,18 +1255,10 @@ func listPersistedAgentAccountModels(accountID uint) ([]dto.AgentAccountModel, e
} }
result := make([]dto.AgentAccountModel, 0, len(rows)) result := make([]dto.AgentAccountModel, 0, len(rows))
for _, row := range rows { for _, row := range rows {
inputs := []string{}
if strings.TrimSpace(row.Input) != "" {
_ = json.Unmarshal([]byte(row.Input), &inputs)
}
result = append(result, dto.AgentAccountModel{ result = append(result, dto.AgentAccountModel{
RecordID: row.ID, RecordID: row.ID,
ID: strings.TrimSpace(row.Model), ID: strings.TrimSpace(row.Model),
Name: strings.TrimSpace(row.Name), Name: strings.TrimSpace(row.Name),
ContextWindow: row.ContextWindow,
MaxTokens: row.MaxTokens,
Reasoning: row.Reasoning,
Input: sanitizeAgentAccountModelInputs(inputs),
}) })
} }
return result, nil return result, nil
@@ -1256,19 +1269,11 @@ func replacePersistedAgentAccountModelsWithTx(tx *gorm.DB, accountID uint, model
return err return err
} }
for index, item := range models { for index, item := range models {
inputPayload, err := json.Marshal(sanitizeAgentAccountModelInputs(item.Input))
if err != nil {
return err
}
record := &model.AgentAccountModel{ record := &model.AgentAccountModel{
AccountID: accountID, AccountID: accountID,
Model: strings.TrimSpace(item.ID), Model: strings.TrimSpace(item.ID),
Name: strings.TrimSpace(item.Name), Name: strings.TrimSpace(item.Name),
ContextWindow: item.ContextWindow, SortOrder: index + 1,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: string(inputPayload),
SortOrder: index + 1,
} }
if err := tx.Create(record).Error; err != nil { if err := tx.Create(record).Error; err != nil {
return err return err
@@ -1309,86 +1314,17 @@ func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAcco
if modelID == "" { if modelID == "" {
return dto.AgentAccountModel{}, fmt.Errorf("model is required") return dto.AgentAccountModel{}, fmt.Errorf("model is required")
} }
modelID = providercatalog.NormalizeModelID(account.Provider, modelID)
name := strings.TrimSpace(model.Name) name := strings.TrimSpace(model.Name)
if name == "" { if name == "" {
name = modelID name = modelID
} }
inputs := sanitizeAgentAccountModelInputs(model.Input)
return dto.AgentAccountModel{ return dto.AgentAccountModel{
ID: normalizeAgentAccountModelID(account.Provider, modelID), ID: modelID,
Name: name, Name: name,
ContextWindow: model.ContextWindow,
MaxTokens: model.MaxTokens,
Reasoning: model.Reasoning,
Input: inputs,
}, nil }, nil
} }
func normalizeAgentAccountModelID(provider, requestedID string) string {
switch provider {
case "custom", "vllm":
return normalizeCustomModel(requestedID)
case "ollama":
target := strings.TrimSpace(requestedID)
if strings.HasPrefix(target, "ollama/") {
return target
}
target = strings.TrimLeft(strings.TrimSpace(target), "/")
if target == "" {
return ""
}
return "ollama/" + target
default:
target := strings.TrimSpace(requestedID)
if target == "" {
return ""
}
prefix := poolModelPrefix(provider)
if strings.Contains(target, "/") {
parts := strings.SplitN(target, "/", 2)
targetPrefix := parts[0]
targetModel := strings.TrimSpace(parts[1])
if targetModel == "" {
return strings.TrimSpace(target)
}
for _, item := range supportedProviderModelPrefixes(provider) {
if item == targetPrefix {
if prefix != "" {
return prefix + "/" + targetModel
}
return strings.TrimSpace(target)
}
}
return strings.TrimSpace(target)
}
target = strings.TrimLeft(strings.TrimSpace(target), "/")
if prefix == "" {
return target
}
return prefix + "/" + target
}
}
func sanitizeAgentAccountModelInputs(values []string) []string {
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
normalized := value
if normalized != "text" && normalized != "image" {
continue
}
if _, ok := seen[normalized]; ok {
continue
}
seen[normalized] = struct{}{}
result = append(result, normalized)
}
if len(result) == 0 {
return []string{"text"}
}
return result
}
func requiresInitialAgentAccountModels(provider string) bool { func requiresInitialAgentAccountModels(provider string) bool {
switch provider { switch provider {
case "custom", "vllm", "ollama": case "custom", "vllm", "ollama":
@@ -1398,36 +1334,14 @@ func requiresInitialAgentAccountModels(provider string) bool {
} }
} }
func normalizeComparableProviderModelID(provider, modelID string) string {
target := strings.TrimSpace(modelID)
if target == "" {
return ""
}
if !strings.Contains(target, "/") {
return target
}
parts := strings.SplitN(target, "/", 2)
prefix := parts[0]
model := strings.TrimSpace(parts[1])
if model == "" {
return target
}
for _, item := range supportedProviderModelPrefixes(provider) {
if item == prefix {
return model
}
}
return target
}
func sameProviderModelID(provider, left, right string) bool { func sameProviderModelID(provider, left, right string) bool {
leftTrimmed := strings.TrimSpace(left) leftTrimmed := strings.TrimSpace(left)
rightTrimmed := strings.TrimSpace(right) rightTrimmed := strings.TrimSpace(right)
if leftTrimmed == rightTrimmed { if leftTrimmed == rightTrimmed {
return true return true
} }
leftComparable := normalizeComparableProviderModelID(provider, leftTrimmed) leftComparable := providercatalog.NormalizeModelID(provider, leftTrimmed)
rightComparable := normalizeComparableProviderModelID(provider, rightTrimmed) rightComparable := providercatalog.NormalizeModelID(provider, rightTrimmed)
return leftComparable != "" && leftComparable == rightComparable return leftComparable != "" && leftComparable == rightComparable
} }
@@ -1448,6 +1362,20 @@ func requireAgentAccountModelForProvider(provider string, models []dto.AgentAcco
return selectedAccountModel, nil return selectedAccountModel, nil
} }
func resolveAgentAccountVerifyModel(provider, requested string, models []dto.AgentAccountModel) (string, error) {
if len(models) == 0 {
return "", buserr.New("ErrAgentAccountModelsRequired")
}
if strings.TrimSpace(requested) == "" {
return models[0].ID, nil
}
selected, ok := findAgentAccountModelForProvider(provider, models, requested)
if !ok {
return "", buserr.New("ErrAgentModelNotInAccount")
}
return selected.ID, nil
}
func ensureAccountModelsNotBound(account *model.AgentAccount, models []dto.AgentAccountModel) error { func ensureAccountModelsNotBound(account *model.AgentAccount, models []dto.AgentAccountModel) error {
if account == nil || account.ID == 0 { if account == nil || account.ID == 0 {
return nil return nil
@@ -1524,6 +1452,41 @@ func readInstallEnv(envStr string) map[string]interface{} {
return data return data
} }
func readAgentEnvMap(envPath string) (map[string]string, error) {
fileOp := files.NewFileOp()
if !fileOp.Stat(envPath) {
return map[string]string{}, nil
}
envMap, err := godotenv.Read(envPath)
if err != nil {
return nil, err
}
return envMap, nil
}
func writeAgentEnvMap(envPath string, envMap map[string]string, order []string) error {
if len(envMap) == 0 {
return files.NewFileOp().SaveFile(envPath, "", 0600)
}
return agentenv.WriteWithOrder(envMap, envPath, order)
}
func upsertAgentEnv(envPath string, values map[string]string, order []string, overwrite bool) error {
envMap, err := readAgentEnvMap(envPath)
if err != nil {
return err
}
for key, value := range values {
if key == "" {
continue
}
if overwrite || strings.TrimSpace(envMap[key]) == "" {
envMap[key] = value
}
}
return writeAgentEnvMap(envPath, envMap, order)
}
func maskKey(value string) string { func maskKey(value string) string {
trim := strings.TrimSpace(value) trim := strings.TrimSpace(value)
if len(trim) <= 6 { if len(trim) <= 6 {
@@ -1551,57 +1514,6 @@ func toInt(value interface{}) int {
} }
} }
func normalizeCustomModel(modelName string) string {
trim := strings.TrimSpace(modelName)
trim = strings.TrimLeft(trim, "/")
if parts := strings.SplitN(trim, "/", 2); len(parts) == 2 {
if strings.EqualFold(parts[0], "custom") {
return strings.TrimLeft(strings.TrimSpace(parts[1]), "/")
}
}
return trim
}
func runtimeProviderModelPrefix(provider string) string {
switch provider {
case "gemini":
return "google"
case "kimi":
return "moonshot"
default:
return provider
}
}
func poolModelPrefix(provider string) string {
meta, ok := providercatalog.Get(provider)
if ok && len(meta.Models) > 0 {
parts := strings.SplitN(strings.TrimSpace(meta.Models[0].ID), "/", 2)
if len(parts) == 2 && strings.TrimSpace(parts[0]) != "" {
return parts[0]
}
}
return provider
}
func supportedProviderModelPrefixes(provider string) []string {
values := []string{poolModelPrefix(provider), runtimeProviderModelPrefix(provider)}
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
target := value
if target == "" {
continue
}
if _, ok := seen[target]; ok {
continue
}
seen[target] = struct{}{}
result = append(result, target)
}
return result
}
func generateToken() string { func generateToken() string {
bytes := make([]byte, 24) bytes := make([]byte, 24)
if _, err := rand.Read(bytes); err != nil { if _, err := rand.Read(bytes); err != nil {
+1 -5
View File
@@ -162,7 +162,7 @@ func fillAgentWebsiteItems(items []dto.AgentItem, explicitWebsiteMap map[uint]mo
} }
} }
func uniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]model.Website { func UniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]model.Website {
websiteMap := make(map[uint]model.Website) websiteMap := make(map[uint]model.Website)
duplicateAppInstallIDs := make(map[uint]struct{}) duplicateAppInstallIDs := make(map[uint]struct{})
for _, website := range websites { for _, website := range websites {
@@ -182,10 +182,6 @@ func uniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]m
return websiteMap return websiteMap
} }
func UniqueDeploymentWebsiteMapForMigration(websites []model.Website) map[uint]model.Website {
return uniqueDeploymentWebsiteMapByAppInstall(websites)
}
func uniqueUintList(items []uint) []uint { func uniqueUintList(items []uint) []uint {
itemMap := make(map[uint]struct{}, len(items)) itemMap := make(map[uint]struct{}, len(items))
uniq := make([]uint, 0, len(items)) uniq := make([]uint, 0, len(items))
+139
View File
@@ -0,0 +1,139 @@
package service
import (
"encoding/json"
"strings"
"github.com/1Panel-dev/1Panel/agent/utils/common"
)
const (
vllmAppKeyForUpgrade = "vllm"
vllmImageEnvKey = "IMAGE"
vllmImageTypeNvidia = "nvidia"
vllmImageTypeIntel = "intel"
vllmImageTypeAscend = "ascend"
)
func resolveVllmVersionFamily(version, image string) string {
normalizedVersion := strings.ToLower(strings.TrimSpace(version))
if strings.HasPrefix(normalizedVersion, vllmImageTypeIntel+"-") {
return vllmImageTypeIntel
}
if strings.HasPrefix(normalizedVersion, vllmImageTypeAscend+"-") {
return vllmImageTypeAscend
}
if strings.HasPrefix(normalizedVersion, vllmImageTypeNvidia+"-") {
return vllmImageTypeNvidia
}
normalizedImage := strings.ToLower(strings.TrimSpace(image))
if strings.Contains(normalizedImage, "intel/") || strings.Contains(normalizedImage, "llm-scaler-vllm") {
return vllmImageTypeIntel
}
if strings.Contains(normalizedImage, "ascend/") || strings.Contains(normalizedImage, "vllm-ascend") {
return vllmImageTypeAscend
}
return vllmImageTypeNvidia
}
func trimVllmVersionFamily(version string) string {
trimmed := strings.TrimSpace(version)
normalized := strings.ToLower(trimmed)
for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel, vllmImageTypeAscend} {
prefix := family + "-"
if strings.HasPrefix(normalized, prefix) {
return strings.TrimSpace(trimmed[len(prefix):])
}
}
return trimmed
}
func buildDefaultVllmImageByVersion(version string) string {
tag := trimVllmVersionFamily(version)
family := resolveVllmVersionFamily(version, "")
if family == vllmImageTypeIntel {
return "intel/llm-scaler-vllm:" + tag
}
if tag != "" && !strings.HasPrefix(strings.ToLower(tag), "v") {
tag = "v" + tag
}
if family == vllmImageTypeAscend {
return "quay.io/ascend/vllm-ascend:" + tag
}
return "vllm/vllm-openai:" + tag
}
func isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage string) bool {
currentFamily := resolveVllmVersionFamily(currentVersion, currentImage)
targetFamily := resolveVllmVersionFamily(targetVersion, "")
return currentFamily == targetFamily
}
func hasVllmVersionFamilyPrefix(version string) bool {
normalized := strings.ToLower(strings.TrimSpace(version))
return strings.HasPrefix(normalized, vllmImageTypeNvidia+"-") ||
strings.HasPrefix(normalized, vllmImageTypeIntel+"-") ||
strings.HasPrefix(normalized, vllmImageTypeAscend+"-")
}
func isVllmUpgradeCandidate(currentVersion, targetVersion, currentImage string) bool {
if strings.TrimSpace(currentVersion) == strings.TrimSpace(targetVersion) {
return false
}
if !isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage) {
return false
}
if common.CompareVersion(targetVersion, currentVersion) {
return true
}
return !hasVllmVersionFamilyPrefix(currentVersion) &&
resolveVllmVersionFamily(targetVersion, "") == vllmImageTypeNvidia &&
trimVllmVersionFamily(currentVersion) == trimVllmVersionFamily(targetVersion)
}
func buildVllmUpgradeImage(currentImage, currentVersion, targetVersion string) string {
trimmedImage := strings.TrimSpace(currentImage)
if trimmedImage == "" || trimmedImage == buildDefaultVllmImageByVersion(currentVersion) {
return buildDefaultVllmImageByVersion(targetVersion)
}
return trimmedImage
}
func loadVllmImageFromEnv(raw string) string {
envs := make(map[string]interface{})
if strings.TrimSpace(raw) == "" {
return ""
}
if err := json.Unmarshal([]byte(raw), &envs); err != nil {
return ""
}
if image, ok := envs[vllmImageEnvKey].(string); ok {
return strings.TrimSpace(image)
}
return ""
}
func setVllmImageInEnvContent(content []byte, image string) []byte {
normalizedImage := strings.TrimSpace(image)
if normalizedImage == "" {
return content
}
lines := strings.Split(string(content), "\n")
replaced := false
for index, line := range lines {
if strings.HasPrefix(line, vllmImageEnvKey+"=") {
lines[index] = vllmImageEnvKey + "=" + normalizedImage
replaced = true
break
}
}
if !replaced {
if len(lines) > 0 && lines[len(lines)-1] == "" {
lines[len(lines)-1] = vllmImageEnvKey + "=" + normalizedImage
lines = append(lines, "")
} else {
lines = append(lines, vllmImageEnvKey+"="+normalizedImage)
}
}
return []byte(strings.Join(lines, "\n"))
}
-1
View File
@@ -705,7 +705,6 @@ func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType st
} }
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType)) config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
if err != nil { if err != nil {
global.LOG.Errorf("alert config not found for type %s: %v", configType, err)
return return
} }
doSendAlert(alert, alertType, quota, quotaType, params, config) doSendAlert(alert, alertType, quota, quotaType, params, config)
-1
View File
@@ -93,7 +93,6 @@ func (s *AlertSender) sendByLegacyMethod(method string, quota string, params []d
} }
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType)) config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
if err != nil { if err != nil {
global.LOG.Errorf("alert config not found for type %s: %v", configType, err)
return return
} }
if !alertUtil.IsAlertConfigEnabled(config) { if !alertUtil.IsAlertConfigEnabled(config) {
+9 -1
View File
@@ -303,6 +303,9 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
filename := filepath.Base(appDetailDTO.DownloadUrl) filename := filepath.Base(appDetailDTO.DownloadUrl)
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml") dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml")
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s) statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound {
return appDetailDTO, buserr.New("ErrAppVersionUnavailable")
}
if err != nil { if err != nil {
return appDetailDTO, buserr.WithDetail("ErrGetCompose", err.Error(), err) return appDetailDTO, buserr.WithDetail("ErrGetCompose", err.Error(), err)
} }
@@ -426,7 +429,12 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
} else { } else {
if appDetail.DockerCompose == "" { if appDetail.DockerCompose == "" {
dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version) dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version)
_, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s) var statusCode int
statusCode, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound {
err = buserr.New("ErrAppVersionUnavailable")
return
}
if err != nil { if err != nil {
return return
} }
+4
View File
@@ -294,6 +294,7 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
DetailID: req.DetailId, DetailID: req.DetailId,
Backup: req.Backup, Backup: req.Backup,
PullImage: req.PullImage, PullImage: req.PullImage,
DeleteImage: req.DeleteImage,
DockerCompose: req.DockerCompose, DockerCompose: req.DockerCompose,
TaskID: req.TaskID, TaskID: req.TaskID,
} }
@@ -600,6 +601,9 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
filename := filepath.Base(detail.DownloadUrl) filename := filepath.Base(detail.DownloadUrl)
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml") dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml")
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s) statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound {
return versions, buserr.New("ErrAppVersionUnavailable")
}
if err != nil { if err != nil {
return versions, err return versions, err
} }
+425 -144
View File
@@ -1,7 +1,6 @@
package service package service
import ( import (
"bufio"
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
@@ -89,7 +88,15 @@ func checkPort(key string, params map[string]interface{}) (int, error) {
return 0, nil return 0, nil
} }
func isPortInUse(port int, protocol string) bool {
return common.ScanPortWithProto(port, normalizeComposeProtocol(protocol))
}
func checkPortExist(port int) error { func checkPortExist(port int) error {
return checkPortExistWithProtocol(port, "")
}
func checkPortExistWithProtocol(port int, protocol string) error {
errMap := make(map[string]interface{}) errMap := make(map[string]interface{})
errMap["port"] = port errMap["port"] = port
appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port)) appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port))
@@ -110,7 +117,7 @@ func checkPortExist(port int) error {
errMap["name"] = domain.Domain errMap["name"] = domain.Domain
return buserr.WithMap("ErrPortExist", errMap, nil) return buserr.WithMap("ErrPortExist", errMap, nil)
} }
if common.ScanPort(port) { if isPortInUse(port, protocol) {
return buserr.WithDetail("ErrPortInUsed", port, nil) return buserr.WithDetail("ErrPortInUsed", port, nil)
} }
return nil return nil
@@ -291,8 +298,8 @@ func createLink(ctx context.Context, installTask *task.Task, app model.App, appI
} }
resourceId = oldMysqlDb.ID resourceId = oldMysqlDb.ID
if oldMysqlDb.ID > 0 { if oldMysqlDb.ID > 0 {
if oldMysqlDb.Username != dbConfig.DbUser || oldMysqlDb.Password != dbConfig.Password { if err := ensureAppMysqlDBUser(database, dbConfig); err != nil {
return buserr.New("ErrDbUserNotValid") return err
} }
} else { } else {
var createMysql dto.MysqlDBCreate var createMysql dto.MysqlDBCreate
@@ -357,33 +364,13 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
return err return err
} }
if deleteReq.DeleteImage { if deleteReq.DeleteImage {
delImageStr := i18n.GetMsgByKey("TaskDelete") + i18n.GetMsgByKey("Image")
content, err := op.GetContent(install.GetEnvPath()) content, err := op.GetContent(install.GetEnvPath())
if err != nil { if err != nil {
return err return err
} }
images, err := docker.GetImagesFromDockerCompose(content, []byte(install.DockerCompose)) if err = deleteAppImagesByCompose(t, content, []byte(install.DockerCompose), nil); err != nil {
if err != nil {
return err return err
} }
client, err := docker.NewClient()
if err != nil {
return err
}
defer client.Close()
for _, image := range images {
imageID, err := client.GetImageIDByName(image)
if err == nil {
imgStr := delImageStr + image
t.Log(imgStr)
if err = client.DeleteImage(imageID); err != nil {
t.LogFailedWithErr(imgStr, err)
continue
}
t.LogSuccess(delImageStr + image)
}
}
} }
} }
tx, ctx := helper.GetTxAndContext() tx, ctx := helper.GetTxAndContext()
@@ -424,6 +411,20 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
switch install.App.Key { switch install.App.Key {
case constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster: case constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster:
if err = databaseUserGrantRepo.DeleteBy(
ctx,
repo.WithByType(install.App.Key),
databaseUserGrantRepo.WithByDatabase(install.Name),
); err != nil {
return err
}
if err = databaseUserRepo.DeleteBy(
ctx,
repo.WithByType(install.App.Key),
databaseUserRepo.WithByDatabase(install.Name),
); err != nil {
return err
}
_ = mysqlRepo.Delete(ctx, mysqlRepo.WithByMysqlName(install.Name)) _ = mysqlRepo.Delete(ctx, mysqlRepo.WithByMysqlName(install.Name))
case constant.AppMongodb: case constant.AppMongodb:
_ = mongodbRepo.Delete(ctx, mongodbRepo.WithByMongodbName(install.Name)) _ = mongodbRepo.Delete(ctx, mongodbRepo.WithByMongodbName(install.Name))
@@ -468,6 +469,107 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
return nil return nil
} }
type appImageID struct {
name string
id string
}
func getAppImageIDsByCompose(client docker.Client, envContent, composeContent []byte) ([]appImageID, error) {
images, err := docker.GetImagesFromDockerCompose(envContent, composeContent)
if err != nil {
return nil, err
}
imageIDs := make([]appImageID, 0, len(images))
for _, image := range images {
imageID, err := client.GetImageIDByName(image)
if err == nil && imageID != "" {
imageIDs = append(imageIDs, appImageID{name: image, id: imageID})
}
}
return imageIDs, nil
}
func deleteAppImagesByCompose(t *task.Task, envContent, composeContent []byte, excludeImages []string) error {
client, err := docker.NewClient()
if err != nil {
return err
}
defer client.Close()
imageIDs, err := getAppImageIDsByCompose(client, envContent, composeContent)
if err != nil {
return err
}
return deleteAppImagesByIDs(t, client, imageIDs, excludeImages)
}
func deleteAppImagesByIDs(t *task.Task, client docker.Client, imageIDs []appImageID, excludeImages []string) error {
delImageStr := i18n.GetMsgByKey("TaskDelete") + i18n.GetMsgByKey("Image")
excludeImageIDs := make(map[string]struct{}, len(excludeImages))
for _, image := range excludeImages {
imageID, err := client.GetImageIDByName(image)
if err == nil && imageID != "" {
excludeImageIDs[imageID] = struct{}{}
}
}
deletedImageIDs := make(map[string]struct{}, len(imageIDs))
for _, image := range imageIDs {
if _, ok := excludeImageIDs[image.id]; ok {
continue
}
if _, ok := deletedImageIDs[image.id]; ok {
continue
}
deletedImageIDs[image.id] = struct{}{}
imgStr := delImageStr + image.name
t.Log(imgStr)
if err := client.DeleteImage(image.id); err != nil {
t.LogFailedWithErr(imgStr, err)
continue
}
t.LogSuccess(imgStr)
}
return nil
}
func ensureAppMysqlDBUser(database model.Database, dbConfig dto.AppDatabase) error {
const host = "%"
mysqlService := NewIMysqlService()
users, err := mysqlService.ListUsers(dto.MysqlUserSearch{Database: database.Name})
if err != nil {
return err
}
userExists := false
passwordValid := false
for _, user := range users {
if user.Username != dbConfig.DbUser || user.Host != host || user.IsDelete {
continue
}
userExists = true
passwordValid = user.Password == dbConfig.Password
break
}
if !userExists || !passwordValid {
return buserr.New("ErrDbUserNotValid")
}
grants, err := mysqlService.ListGrants(dto.MysqlUserSearch{Database: database.Name})
if err != nil {
return err
}
for _, grant := range grants {
if grant.Database == dbConfig.DbName && grant.Username == dbConfig.DbUser && grant.Host == host {
return nil
}
}
return mysqlService.GrantUser(dto.MysqlGrantCreate{
Database: database.Name,
DB: dbConfig.DbName,
Username: dbConfig.DbUser,
Host: host,
})
}
func deleteLink(del dto.DelAppLink) error { func deleteLink(del dto.DelAppLink) error {
install := del.Install install := del.Install
resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID)) resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID))
@@ -477,19 +579,20 @@ func deleteLink(del dto.DelAppLink) error {
for _, re := range resources { for _, re := range resources {
switch re.Key { switch re.Key {
case constant.AppMysql, constant.AppMariaDB: case constant.AppMysql, constant.AppMariaDB:
mysqlService := NewIMysqlService()
database, _ := mysqlRepo.Get(repo.WithByID(re.ResourceId)) database, _ := mysqlRepo.Get(repo.WithByID(re.ResourceId))
if reflect.DeepEqual(database, model.DatabaseMysql{}) { if reflect.DeepEqual(database, model.DatabaseMysql{}) {
continue continue
} }
if err := mysqlService.Delete(del.Ctx, dto.MysqlDBDelete{ if err := deleteMysqlDatabaseForResourceOwner(del.Ctx, dto.MysqlDBDelete{
ID: database.ID, ID: database.ID,
ForceDelete: del.ForceDelete, ForceDelete: del.ForceDelete,
DeleteBackup: true, DeleteBackup: true,
Type: re.Key, Type: re.Key,
Database: database.MysqlName, Database: database.MysqlName,
}); err != nil && !del.ForceDelete { }, dto.DBResource{Type: constant.TypeApp, Name: install.Name}); err != nil {
return err if isMysqlDatabaseResourceInUseError(err) || !del.ForceDelete {
return err
}
} }
case constant.AppPostgresql: case constant.AppPostgresql:
pgsqlService := NewIPostgresqlService() pgsqlService := NewIPostgresqlService()
@@ -564,11 +667,58 @@ func handleUpgradeCompose(install model.AppInstall, detail model.AppDetail) (map
if oldServiceValue["restart"] != nil { if oldServiceValue["restart"] != nil {
serviceValue["restart"] = oldServiceValue["restart"] serviceValue["restart"] = oldServiceValue["restart"]
} }
if install.App.Key == constant.AppOpenresty {
mergeOpenrestyModuleVolumes(serviceValue, oldServiceValue)
}
servicesMap[install.ServiceName] = serviceValue servicesMap[install.ServiceName] = serviceValue
composeMap["services"] = servicesMap composeMap["services"] = servicesMap
return composeMap, nil return composeMap, nil
} }
// mergeOpenrestyModuleVolumes carries the dynamic module mounts of the old
// compose over to the upgraded one when it does not declare them, so built
// module artifacts and their load configuration stay mounted across upgrades.
func mergeOpenrestyModuleVolumes(serviceValue, oldServiceValue map[string]interface{}) {
oldVolumes, ok := oldServiceValue["volumes"].([]interface{})
if !ok {
return
}
newVolumes, _ := serviceValue["volumes"].([]interface{})
existing := make(map[string]struct{}, len(newVolumes))
for _, volume := range newVolumes {
if containerPath, ok := composeVolumeContainerPath(volume); ok {
existing[containerPath] = struct{}{}
}
}
for _, volume := range oldVolumes {
containerPath, ok := composeVolumeContainerPath(volume)
if !ok {
continue
}
if !strings.Contains(containerPath, nginxModuleEnabledConfDir) && !strings.Contains(containerPath, "nginx/modules/1panel") {
continue
}
if _, ok = existing[containerPath]; ok {
continue
}
newVolumes = append(newVolumes, volume)
existing[containerPath] = struct{}{}
}
serviceValue["volumes"] = newVolumes
}
func composeVolumeContainerPath(volume interface{}) (string, bool) {
volumeStr, ok := volume.(string)
if !ok {
return "", false
}
parts := strings.Split(volumeStr, ":")
if len(parts) < 2 {
return "", false
}
return parts[1], true
}
func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) { func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) {
if detail.DockerCompose == "" { if detail.DockerCompose == "" {
return "", nil return "", nil
@@ -602,74 +752,35 @@ func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string
return string(composeByte), nil return string(composeByte), nil
} }
func buildNginx(parentTask *task.Task) error { func buildNginx(parentTask *task.Task, nginxInstall model.AppInstall, catalogPath string) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
buildPath := path.Join(nginxInstall.GetPath(), "build") buildPath := path.Join(nginxInstall.GetPath(), nginxModuleBuildDir)
if !fileOp.Stat(buildPath) { if !fileOp.Stat(buildPath) {
return buserr.New("ErrBuildDirNotFound") return buserr.New("ErrBuildDirNotFound")
} }
moduleConfigPath := path.Join(buildPath, "module.json") modules, err := loadNginxModulesWithCatalog(nginxInstall, catalogPath)
moduleContent, err := fileOp.GetContent(moduleConfigPath)
if err != nil { if err != nil {
return err return err
} }
var ( previousModules := cloneNginxModules(modules)
modules []dto.NginxModule staticBuild := hasEnabledStaticNginxModules(modules)
addModuleParams []string if err = configureStaticNginxModules(nginxInstall, modules, ""); err != nil {
addPackages []string return err
)
if len(moduleContent) > 0 {
_ = json.Unmarshal(moduleContent, &modules)
bashFile, err := os.OpenFile(path.Join(buildPath, "tmp", "pre.sh"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, constant.DirPerm)
if err != nil {
return err
}
defer bashFile.Close()
bashFileWriter := bufio.NewWriter(bashFile)
for _, module := range modules {
if !module.Enable {
continue
}
_, err = bashFileWriter.WriteString(module.Script + "\n")
if err != nil {
return err
}
addModuleParams = append(addModuleParams, module.Params)
addPackages = append(addPackages, module.Packages...)
}
err = bashFileWriter.Flush()
if err != nil {
return err
}
} }
envs, err := gotenv.Read(nginxInstall.GetEnvPath()) if staticBuild {
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image"))
parentTask.LogStart(logStr)
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*parentTask), cmd.WithTimeout(120*time.Minute))
if err = cmdMgr.Run("docker", "compose", "-f", nginxInstall.GetComposePath(), "build"); err != nil {
return err
}
parentTask.LogSuccess(logStr)
}
modules, err = buildDynamicNginxModules(nginxInstall, modules, nil, false, "", catalogPath, parentTask)
if err != nil { if err != nil {
return err return err
} }
envs["RESTY_CONFIG_OPTIONS_MORE"] = "" return commitNginxModuleBuilds(nginxInstall, previousModules, modules, false, catalogPath)
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = ""
if len(addModuleParams) > 0 {
envs["RESTY_CONFIG_OPTIONS_MORE"] = strings.Join(addModuleParams, " ")
}
if len(addPackages) > 0 {
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = strings.Join(addPackages, " ")
}
_ = gotenv.Write(envs, nginxInstall.GetEnvPath())
if len(addModuleParams) == 0 && len(addPackages) == 0 {
return nil
}
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image"))
parentTask.LogStart(logStr)
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*parentTask), cmd.WithTimeout(60*time.Minute))
if err = cmdMgr.Run("docker", "compose", "-f", nginxInstall.GetComposePath(), "build"); err != nil {
return err
}
parentTask.LogSuccess(logStr)
return nil
} }
func upgradeInstall(req request.AppInstallUpgrade) error { func upgradeInstall(req request.AppInstallUpgrade) error {
@@ -677,6 +788,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil { if err != nil {
return err return err
} }
originalInstall := install
oldVersion := install.Version oldVersion := install.Version
detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID)) detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
if err != nil { if err != nil {
@@ -696,8 +808,9 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
install.Status = constant.StatusUpgrading install.Status = constant.StatusUpgrading
var ( var (
upErr error upErr error
backupFile string backupFile string
nginxUpgradeSnapshot *openrestyUpgradeSnapshot
) )
backUpApp := func(t *task.Task) error { backUpApp := func(t *task.Task) error {
backupService := NewIBackupService() backupService := NewIBackupService()
@@ -751,6 +864,15 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil { if err != nil {
return err 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 { if install.App.Key == vllmAppKeyForUpgrade {
envs := make(map[string]interface{}) envs := make(map[string]interface{})
if err = json.Unmarshal([]byte(install.Env), &envs); err != nil { if err = json.Unmarshal([]byte(install.Env), &envs); err != nil {
@@ -765,51 +887,35 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
install.Env = string(paramByte) install.Env = string(paramByte)
content = setVllmImageInEnvContent(content, image) content = setVllmImageInEnvContent(content, image)
} }
if req.PullImage {
composeContent := []byte(detail.DockerCompose)
if install.App.Key == vllmAppKeyForUpgrade {
composeContent = []byte(install.DockerCompose)
}
if req.DockerCompose != "" {
composeContent = []byte(req.DockerCompose)
}
images, err := docker.GetImagesFromDockerCompose(content, composeContent)
if err != nil {
return err
}
dockerCLi, err := docker.NewClient()
if err != nil {
return err
}
defer dockerCLi.Close()
for _, image := range images {
t.Log(i18n.GetWithName("PullImageStart", image))
if err = dockerCLi.PullImageWithProcess(t, image); err != nil {
err = buserr.WithNameAndErr("ErrDockerPullImage", "", err)
return err
}
t.LogSuccess(i18n.GetMsgByKey("PullImage"))
}
}
_ = copyAppDetailMissing(fileOp, detailDir, install.GetPath()) _ = copyAppDetailMissing(fileOp, detailDir, install.GetPath())
if install.App.Key == constant.AppOpenresty { if install.App.Key == constant.AppOpenresty {
installBuildDir := path.Join(install.GetPath(), "build") installBuildDir := path.Join(install.GetPath(), nginxModuleBuildDir)
detailBuildDir := path.Join(detailDir, "build") detailBuildDir := path.Join(detailDir, nginxModuleBuildDir)
if !fileOp.Stat(installBuildDir) { if !fileOp.Stat(installBuildDir) {
if err := fileOp.CreateDir(installBuildDir, constant.DirPerm); err != nil { if err := fileOp.CreateDir(installBuildDir, constant.DirPerm); err != nil {
return err return err
} }
} }
if err := fileOp.DeleteDir(path.Join(installBuildDir, "tmp")); err != nil { if err := fileOp.DeleteDir(path.Join(installBuildDir, nginxModuleTmpDir)); err != nil {
return err return err
} }
if err := fileOp.CopyDir(path.Join(detailBuildDir, "tmp"), installBuildDir); err != nil { if err := fileOp.CopyDir(path.Join(detailBuildDir, nginxModuleTmpDir), installBuildDir); err != nil {
return err return err
} }
if err := fileOp.CopyFile(path.Join(detailBuildDir, "Dockerfile"), installBuildDir); err != nil { if err := fileOp.CopyFile(path.Join(detailBuildDir, "Dockerfile"), installBuildDir); err != nil {
return err 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 { if err := fileOp.CopyFile(path.Join(detailBuildDir, "nginx.conf"), installBuildDir); err != nil {
return err return err
} }
@@ -847,6 +953,66 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
install.Version = detail.Version install.Version = detail.Version
install.AppDetailId = req.DetailID 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, err := compose.Down(install.GetComposePath()); err != nil {
if out != "" { if out != "" {
upErr = errors.New(out) upErr = errors.New(out)
@@ -881,7 +1047,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
} }
if install.App.Key == constant.AppOpenresty { if install.App.Key == constant.AppOpenresty {
if err = buildNginx(t); err != nil { if err = buildNginx(t, install, targetNginxCatalogPath); err != nil {
t.Log(err.Error()) t.Log(err.Error())
return err return err
} }
@@ -897,36 +1063,134 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
} }
t.LogSuccess(logStr) t.LogSuccess(logStr)
install.Status = constant.StatusRunning install.Status = constant.StatusRunning
return appInstallRepo.Save(context.Background(), &install) 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) { rollBackApp := func(t *task.Task) {
if req.Backup { if req.Backup {
t.Log(i18n.GetWithName("AppRecover", install.Name)) t.Log(i18n.GetWithName("AppRecover", install.Name))
if err := NewIBackupService().AppRecover(dto.CommonRecover{Name: install.App.Key, DetailName: install.Name, Type: "app", DownloadAccountID: 1, File: backupFile}); err != nil { recoverErr := NewIBackupService().AppRecover(dto.CommonRecover{
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), err) 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 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)) t.LogSuccess(i18n.GetWithName("AppRecover", install.Name))
return 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))
}
} }
upgradeTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp), upgradeApp, rollBackApp, 0, 1*time.Hour) 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() { go func() {
err = upgradeTask.Execute() if taskErr := upgradeTask.Execute(); taskErr != nil {
if err != nil {
existInstall, _ := appInstallRepo.GetFirst(repo.WithByID(req.InstallID)) existInstall, _ := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
if existInstall.ID > 0 && existInstall.Status != constant.StatusRunning { if existInstall.ID > 0 && existInstall.Status != constant.StatusRunning {
existInstall.Status = constant.StatusUpgradeErr existInstall.Status = constant.StatusUpgradeErr
existInstall.Message = err.Error() existInstall.Message = taskErr.Error()
_ = appInstallRepo.Save(context.Background(), &existInstall) _ = appInstallRepo.Save(context.Background(), &existInstall)
} }
} }
}() }()
return appInstallRepo.Save(context.Background(), &install) return nil
} }
func skipCheckStatus(service types.ServiceConfig) bool { func skipCheckStatus(service types.ServiceConfig) bool {
@@ -1624,6 +1888,9 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
synAppInstall(containersMap, &installed, false) synAppInstall(containersMap, &installed, false)
} }
resourceKeys := getAppInstallResourceKeys(installed.ID)
envMap := make(map[string]interface{})
_ = json.Unmarshal([]byte(installed.Env), &envMap)
installDTO := response.AppInstallDTO{ installDTO := response.AppInstallDTO{
ID: installed.ID, ID: installed.ID,
Name: installed.Name, Name: installed.Name,
@@ -1645,14 +1912,16 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
Website: installed.App.Website, Website: installed.App.Website,
Document: installed.App.Document, Document: installed.App.Document,
}, },
Favorite: installed.Favorite, Favorite: installed.Favorite,
SortOrder: installed.SortOrder, SortOrder: installed.SortOrder,
Container: installed.ContainerName, Container: installed.ContainerName,
ServiceName: strings.ToLower(installed.ServiceName), ServiceName: strings.ToLower(installed.ServiceName),
ResourceKeys: resourceKeys,
Env: envMap,
} }
if !updated && !checkUpdate { if !updated && !checkUpdate {
installDTO.LinkDB = hasLinkDB(installed.ID) installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys)
res = append(res, installDTO) res = append(res, installDTO)
continue continue
} }
@@ -1660,7 +1929,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
if installed.Version == "latest" { if installed.Version == "latest" {
if checkUpdate { if checkUpdate {
installDTO.CanUpdate = false installDTO.CanUpdate = false
installDTO.LinkDB = hasLinkDB(installed.ID) installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys)
res = append(res, installDTO) res = append(res, installDTO)
} }
continue continue
@@ -1689,7 +1958,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
if len(versions) == 0 { if len(versions) == 0 {
if checkUpdate { if checkUpdate {
installDTO.CanUpdate = false installDTO.CanUpdate = false
installDTO.LinkDB = hasLinkDB(installed.ID) installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys)
res = append(res, installDTO) res = append(res, installDTO)
} }
continue continue
@@ -1721,7 +1990,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
res = append(res, installDTO) res = append(res, installDTO)
} }
} else if checkUpdate { } else if checkUpdate {
installDTO.LinkDB = hasLinkDB(installed.ID) installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys)
res = append(res, installDTO) res = append(res, installDTO)
} }
} }
@@ -2152,7 +2421,7 @@ func handleOpenrestyFile(appInstall *model.AppInstall) error {
func handleDefaultServer(appInstall *model.AppInstall) error { func handleDefaultServer(appInstall *model.AppInstall) error {
installDir := appInstall.GetPath() installDir := appInstall.GetPath()
defaultConfigPath := path.Join(installDir, "conf", "default", "00.default.conf") defaultConfigPath := path.Join(installDir, nginxModuleConfDir, "default", "00.default.conf")
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
content, err := fileOp.GetContent(defaultConfigPath) content, err := fileOp.GetContent(defaultConfigPath)
if err != nil { if err != nil {
@@ -2166,7 +2435,7 @@ func handleDefaultServer(appInstall *model.AppInstall) error {
} }
func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRejectHandshake bool) error { func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRejectHandshake bool) error {
sslDir := path.Join(appInstall.GetPath(), "conf", "ssl") sslDir := path.Join(appInstall.GetPath(), nginxModuleConfDir, "ssl")
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
if !fileOp.Stat(sslDir) { if !fileOp.Stat(sslDir) {
return errors.New("ssl dir not found") return errors.New("ssl dir not found")
@@ -2196,7 +2465,7 @@ func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRe
_ = NewIWebsiteSSLService().Delete([]uint{websiteSSL.ID}) _ = NewIWebsiteSSLService().Delete([]uint{websiteSSL.ID})
}() }()
} }
defaultConfigPath := path.Join(appInstall.GetPath(), "conf", "default", "00.default.conf") defaultConfigPath := path.Join(appInstall.GetPath(), nginxModuleConfDir, "default", "00.default.conf")
content, err := os.ReadFile(defaultConfigPath) content, err := os.ReadFile(defaultConfigPath)
if err != nil { if err != nil {
return err return err
@@ -2288,13 +2557,25 @@ func needsUpdate(localTag *model.Tag, remoteTag dto.Tag, translations string) bo
} }
func hasLinkDB(installID uint) bool { func hasLinkDB(installID uint) bool {
return hasLinkDBFromKeys(getAppInstallResourceKeys(installID))
}
func getAppInstallResourceKeys(installID uint) []string {
resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(installID)) resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(installID))
keys := make([]string, 0, len(resources))
for _, resource := range resources {
keys = append(keys, resource.Key)
}
return keys
}
func hasLinkDBFromKeys(resourceKeys []string) bool {
hasDB := false hasDB := false
if len(resources) > 0 { if len(resourceKeys) > 0 {
for _, resource := range resources { for _, resourceKey := range resourceKeys {
if resource.Key == constant.AppPostgres || resource.Key == constant.AppMysql || if resourceKey == constant.AppPostgres || resourceKey == constant.AppMysql ||
resource.Key == constant.AppMariaDB || resource.Key == constant.AppMysqlCluster || resourceKey == constant.AppMariaDB || resourceKey == constant.AppMysqlCluster ||
resource.Key == constant.AppPostgresql || resource.Key == constant.AppPostgresqlCluster { resourceKey == constant.AppPostgresql || resourceKey == constant.AppPostgresqlCluster {
hasDB = true hasDB = true
break break
} }
+78 -6
View File
@@ -2,6 +2,7 @@ package service
import ( import (
"bufio" "bufio"
"context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -372,7 +373,7 @@ func (u *BackupService) checkBackupConn(backup *model.BackupAccount) (bool, erro
targetPath = strings.TrimPrefix(targetPath, "/") targetPath = strings.TrimPrefix(targetPath, "/")
} }
if _, err := client.Upload(fileItem, targetPath); err != nil { if _, err := client.Upload(context.Background(), fileItem, targetPath); err != nil {
return false, err return false, err
} }
_, _ = client.Delete(path.Join(backup.BackupPath, "test/1panel")) _, _ = client.Delete(path.Join(backup.BackupPath, "test/1panel"))
@@ -437,6 +438,10 @@ type backupClientHelper struct {
} }
func NewBackupClientMap(ids []string) map[string]backupClientHelper { func NewBackupClientMap(ids []string) map[string]backupClientHelper {
return NewBackupClientMapWithContext(context.Background(), ids)
}
func NewBackupClientMapWithContext(ctx context.Context, ids []string) map[string]backupClientHelper {
var accounts []model.BackupAccount var accounts []model.BackupAccount
var idItems []uint var idItems []uint
for i := 0; i < len(ids); i++ { for i := 0; i < len(ids); i++ {
@@ -446,7 +451,7 @@ func NewBackupClientMap(ids []string) map[string]backupClientHelper {
accounts, _ = backupRepo.List(repo.WithByIDs(idItems)) accounts, _ = backupRepo.List(repo.WithByIDs(idItems))
clientMap := make(map[string]backupClientHelper) clientMap := make(map[string]backupClientHelper)
for _, item := range accounts { for _, item := range accounts {
backClient, err := newClient(&item, true) backClient, err := newClientWithContext(ctx, &item, true)
itemHelper := backupClientHelper{ itemHelper := backupClientHelper{
client: backClient, client: backClient,
name: item.Name, name: item.Name,
@@ -463,7 +468,11 @@ func NewBackupClientMap(ids []string) map[string]backupClientHelper {
return clientMap return clientMap
} }
func uploadWithMap(taskItem task.Task, accountMap map[string]backupClientHelper, src, dst, accountIDs string, downloadAccountID, retry uint) error { func uploadWithMap(taskItem task.Task, accountMap map[string]backupClientHelper, src, dst, accountIDs string, downloadAccountID, retry uint, cleanOnFailure bool) error {
return uploadWithMapWithContext(context.Background(), taskItem, accountMap, src, dst, accountIDs, downloadAccountID, retry, cleanOnFailure, true)
}
func uploadWithMapWithContext(ctx context.Context, taskItem task.Task, accountMap map[string]backupClientHelper, src, dst, accountIDs string, downloadAccountID, retry uint, cleanOnFailure, removeSrc bool) error {
accounts := strings.Split(accountIDs, ",") accounts := strings.Split(accountIDs, ",")
for _, account := range accounts { for _, account := range accounts {
if len(account) == 0 { if len(account) == 0 {
@@ -489,10 +498,13 @@ func uploadWithMap(taskItem task.Task, accountMap map[string]backupClientHelper,
"backup": name, "backup": name,
})) }))
for i := 0; i < int(retry)+1; i++ { for i := 0; i < int(retry)+1; i++ {
_, err := itemBackup.client.Upload(src, path.Join(itemBackup.backupPath, dst)) _, err := itemBackup.client.Upload(ctx, src, path.Join(itemBackup.backupPath, dst))
taskItem.LogWithStatus(i18n.GetMsgByKey("Upload"), err) taskItem.LogWithStatus(i18n.GetMsgByKey("Upload"), err)
if err != nil { if err != nil {
if account == fmt.Sprintf("%d", downloadAccountID) { if account == fmt.Sprintf("%d", downloadAccountID) {
if cleanOnFailure {
cleanupCronjobBackupArtifacts(accountMap, src, dst)
}
return err return err
} }
} else { } else {
@@ -502,11 +514,71 @@ func uploadWithMap(taskItem task.Task, accountMap map[string]backupClientHelper,
itemBackup.hasBackup = true itemBackup.hasBackup = true
accountMap[account] = itemBackup accountMap[account] = itemBackup
} }
os.RemoveAll(src) if removeSrc {
os.RemoveAll(src)
}
return nil return nil
} }
func cleanupCronjobBackupArtifacts(accountMap map[string]backupClientHelper, src, dst string) {
if err := os.RemoveAll(src); err != nil {
global.LOG.Errorf("remove failed local cronjob backup file %s failed, err: %v", src, err)
}
for _, account := range accountMap {
if !account.isOk {
continue
}
if _, err := account.client.Delete(path.Join(account.backupPath, dst)); err != nil {
global.LOG.Errorf("remove failed cronjob backup file %s failed, err: %v", dst, err)
}
}
}
func markBackupFailed(recordID uint, backupErr error) {
_ = backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": backupErr.Error()})
record, err := backupRepo.GetRecord(repo.WithByID(recordID))
if err != nil || record.ID == 0 {
global.LOG.Errorf("load failed backup record %d for cleanup failed, err: %v", recordID, err)
return
}
filePath := path.Join(record.FileDir, record.FileName)
if err := os.Remove(path.Join(global.Dir.LocalBackupDir, filePath)); err != nil && !os.IsNotExist(err) {
global.LOG.Errorf("remove failed local backup file %s failed, err: %v", filePath, err)
}
cleaned := make(map[string]struct{})
for _, accountID := range strings.Split(record.SourceAccountIDs, ",") {
if accountID == "" {
continue
}
if _, ok := cleaned[accountID]; ok {
continue
}
cleaned[accountID] = struct{}{}
id, err := strconv.Atoi(accountID)
if err != nil {
global.LOG.Errorf("parse backup account %s for failed backup cleanup failed, err: %v", accountID, err)
continue
}
account, storageClient, err := NewBackupClientWithID(uint(id))
if err != nil {
global.LOG.Errorf("new backup client for failed backup cleanup failed, err: %v", err)
continue
}
if _, err := storageClient.Delete(path.Join(account.BackupPath, filePath)); err != nil {
global.LOG.Errorf("remove failed backup file %s failed, err: %v", filePath, err)
}
}
}
func newClient(account *model.BackupAccount, isEncrypt bool) (cloud_storage.CloudStorageClient, error) { func newClient(account *model.BackupAccount, isEncrypt bool) (cloud_storage.CloudStorageClient, error) {
return newClientWithContext(context.Background(), account, isEncrypt)
}
func newClientWithContext(ctx context.Context, account *model.BackupAccount, isEncrypt bool) (cloud_storage.CloudStorageClient, error) {
varMap := make(map[string]interface{}) varMap := make(map[string]interface{})
if len(account.Vars) != 0 { if len(account.Vars) != 0 {
if err := json.Unmarshal([]byte(account.Vars), &varMap); err != nil { if err := json.Unmarshal([]byte(account.Vars), &varMap); err != nil {
@@ -531,7 +603,7 @@ func newClient(account *model.BackupAccount, isEncrypt bool) (cloud_storage.Clou
varMap["password"] = account.Credential varMap["password"] = account.Credential
} }
client, err := cloud_storage.NewCloudStorageClient(account.Type, varMap) client, err := cloud_storage.NewCloudStorageClientWithContext(ctx, account.Type, varMap)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+74 -9
View File
@@ -63,11 +63,30 @@ func (u *BackupService) AppBackup(req dto.CommonBackup) (*model.BackupRecord, er
return nil, err return nil, err
} }
if err = handleAppBackup(&install, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil { if !req.IsImmediate {
global.LOG.Errorf("backup app %s failed, err: %v", req.DetailName, err) if err = handleAppBackup(&install, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil {
return nil, err markBackupFailed(record.ID, err)
global.LOG.Errorf("backup app %s failed, err: %v", req.DetailName, err)
return nil, err
}
return record, nil
} }
backupTask, err := task.NewTaskWithOps(install.Name, task.TaskBackup, task.TaskScopeBackup, req.TaskID, install.ID)
if err != nil {
markBackupFailed(record.ID, err)
record.Status = constant.StatusFailed
record.Message = err.Error()
return nil, err
}
if err = doAppBackup(&install, backupTask, backupDir, fileName, "", req.Secret); err != nil {
markBackupFailed(record.ID, err)
record.Status = constant.StatusFailed
record.Message = err.Error()
return nil, err
}
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusSuccess})
record.Status = constant.StatusSuccess
return record, nil return record, nil
} }
@@ -143,7 +162,7 @@ func handleAppBackup(install *model.AppInstall, parentTask *task.Task, recordID
backupTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 3, time.Hour) backupTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 3, time.Hour)
go func() { go func() {
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) markBackupFailed(recordID, err)
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
@@ -261,7 +280,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
if err != nil { if err != nil {
return err return err
} }
newDB, err := reCreateDB(db.ID, database, backupEnvMap) newDB, err := reCreateDB(db.ID, database, backupEnvMap, install.Name)
if err != nil { if err != nil {
return err return err
} }
@@ -389,10 +408,16 @@ func doAppBackup(install *model.AppInstall, parentTask *task.Task, backupDir, fi
return nil return nil
} }
func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{}) (*model.DatabaseMysql, error) { func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{}, appInstallName string) (*model.DatabaseMysql, error) {
mysqlService := NewIMysqlService() mysqlService := NewIMysqlService()
ctx := context.Background() ctx := context.Background()
_ = mysqlService.Delete(ctx, dto.MysqlDBDelete{ID: dbID, Database: database.Name, Type: database.Type, DeleteBackup: false, ForceDelete: true}) if err := deleteMysqlDatabaseForResourceOwner(
ctx,
dto.MysqlDBDelete{ID: dbID, Database: database.Name, Type: database.Type, DeleteBackup: false, ForceDelete: true},
dto.DBResource{Type: constant.TypeApp, Name: appInstallName},
); err != nil {
return nil, err
}
dbInfo := getDBCreateInfoFromEnv(envMap, "utf8mb4") dbInfo := getDBCreateInfoFromEnv(envMap, "utf8mb4")
createDB, err := mysqlService.Create(context.Background(), dto.MysqlDBCreate{ createDB, err := mysqlService.Create(context.Background(), dto.MysqlDBCreate{
@@ -400,17 +425,57 @@ func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{
From: database.From, From: database.From,
Database: database.Name, Database: database.Name,
Format: dbInfo.Format, Format: dbInfo.Format,
Username: dbInfo.User,
Password: dbInfo.Password,
Permission: "%", Permission: "%",
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(dbInfo.User) != 0 {
if err := ensureMysqlDBUser(mysqlService, database, dbInfo); err != nil {
return nil, err
}
}
updateCronjobsDBRef(dbID, createDB.ID) updateCronjobsDBRef(dbID, createDB.ID)
return createDB, nil return createDB, nil
} }
func ensureMysqlDBUser(mysqlService IMysqlService, database model.Database, dbInfo dbRecreateInfo) error {
const host = "%"
users, err := mysqlService.ListUsers(dto.MysqlUserSearch{Database: database.Name})
if err != nil {
return err
}
var oldUser dto.MysqlUser
exists := false
for _, user := range users {
if user.Username == dbInfo.User && user.Host == host && !user.IsDelete {
oldUser = user
exists = true
break
}
}
if exists {
if len(oldUser.Password) != 0 && oldUser.Password != dbInfo.Password {
return buserr.New("ErrDbUserNotValid")
}
} else {
if err := mysqlService.CreateUser(dto.MysqlUserCreate{
Database: database.Name,
Username: dbInfo.User,
Host: host,
Password: dbInfo.Password,
}); err != nil {
return err
}
}
return mysqlService.GrantUser(dto.MysqlGrantCreate{
Database: database.Name,
DB: dbInfo.Name,
Username: dbInfo.User,
Host: host,
})
}
func reCreatePostgresqlDB(dbID uint, database model.Database, envMap map[string]interface{}) (*model.DatabasePostgresql, error) { func reCreatePostgresqlDB(dbID uint, database model.Database, envMap map[string]interface{}) (*model.DatabasePostgresql, error) {
postgresqlService := NewIPostgresqlService() postgresqlService := NewIPostgresqlService()
_ = postgresqlService.Delete(context.Background(), dto.PostgresqlDBDelete{ _ = postgresqlService.Delete(context.Background(), dto.PostgresqlDBDelete{
+2 -2
View File
@@ -98,7 +98,7 @@ func (u *BackupService) ComposeBackup(req dto.CommonBackup) error {
return err return err
} }
if err := handleComposeBackup(req, nil, record.ID, backupDir, fileName); err != nil { if err := handleComposeBackup(req, nil, record.ID, backupDir, fileName); err != nil {
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) markBackupFailed(record.ID, err)
return err return err
} }
return nil return nil
@@ -163,7 +163,7 @@ func handleComposeBackup(req dto.CommonBackup, parentTask *task.Task, recordID u
go func() { go func() {
defer composeCtx.close() defer composeCtx.close()
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) markBackupFailed(recordID, err)
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+82 -18
View File
@@ -106,7 +106,7 @@ func (u *BackupService) ContainerBackup(req dto.CommonBackup) error {
return err return err
} }
if err := handleContainerBackup(req.Name, nil, record.ID, backupDir, fileName, req.TaskID, req.Secret, req.StopBefore); err != nil { if err := handleContainerBackup(req.Name, nil, record.ID, backupDir, fileName, req.TaskID, req.Secret, req.StopBefore); err != nil {
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) markBackupFailed(record.ID, err)
return err return err
} }
return nil return nil
@@ -158,7 +158,7 @@ func handleContainerBackup(containerName string, parentTask *task.Task, recordID
go func() { go func() {
defer backupCtx.close() defer backupCtx.close()
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) markBackupFailed(recordID, err)
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
@@ -604,23 +604,40 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
return nil return nil
} }
func removeUnsupportedEndpointStaticIPAM(cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) { func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
if cli.NewVersionError(ctx, "1.44", "specify mac-address per network") != nil {
removeEndpointMacAddresses(primary, extras)
}
endpointGroups := []map[string]*network.EndpointSettings{extras}
if primary != nil { if primary != nil {
removeUnsupportedEndpointStaticIPAMFromEndpoints(cli, primary.EndpointsConfig) endpointGroups = append(endpointGroups, primary.EndpointsConfig)
}
for _, endpoints := range endpointGroups {
for netName, endpoint := range endpoints {
if endpoint == nil || endpoint.IPAMConfig == nil {
continue
}
info, err := cli.NetworkInspect(ctx, netName, network.InspectOptions{})
if err != nil {
continue
}
removeUnsupportedEndpointStaticIP(netName, info, endpoint)
}
} }
removeUnsupportedEndpointStaticIPAMFromEndpoints(cli, extras)
} }
func removeUnsupportedEndpointStaticIPAMFromEndpoints(cli *client.Client, endpoints map[string]*network.EndpointSettings) { func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
for netName, endpoint := range endpoints { if primary != nil {
if endpoint == nil || endpoint.IPAMConfig == nil { for _, endpoint := range primary.EndpointsConfig {
continue if endpoint != nil {
endpoint.MacAddress = ""
}
} }
info, err := cli.NetworkInspect(context.Background(), netName, network.InspectOptions{}) }
if err != nil { for _, endpoint := range extras {
continue if endpoint != nil {
endpoint.MacAddress = ""
} }
removeUnsupportedEndpointStaticIP(netName, info, endpoint)
} }
} }
@@ -639,7 +656,7 @@ func removeUnsupportedEndpointStaticIP(netName string, info network.Inspect, end
if endpoint.IPAMConfig.IPv6Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv6Address, true) { if endpoint.IPAMConfig.IPv6Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv6Address, true) {
endpoint.IPAMConfig.IPv6Address = "" endpoint.IPAMConfig.IPv6Address = ""
} }
if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" { if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" && len(endpoint.IPAMConfig.LinkLocalIPs) == 0 {
endpoint.IPAMConfig = nil endpoint.IPAMConfig = nil
} }
} }
@@ -776,13 +793,27 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
if name == "host" || name == "none" { if name == "host" || name == "none" {
continue continue
} }
endpointSetting := &network.EndpointSettings{Aliases: append([]string(nil), endpoint.Aliases...), MacAddress: endpoint.MacAddress} if endpoint == nil {
if name == primaryName {
config.EndpointsConfig[name] = &network.EndpointSettings{}
} else {
extraNetworks[name] = &network.EndpointSettings{}
}
continue
}
endpointSetting := &network.EndpointSettings{
Links: append([]string(nil), endpoint.Links...),
Aliases: append([]string(nil), endpoint.Aliases...),
DriverOpts: cloneStringMap(endpoint.DriverOpts),
GwPriority: endpoint.GwPriority,
}
if endpoint.IPAMConfig != nil { if endpoint.IPAMConfig != nil {
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{ endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: endpoint.IPAMConfig.IPv4Address, IPv4Address: endpoint.IPAMConfig.IPv4Address,
IPv6Address: endpoint.IPAMConfig.IPv6Address, IPv6Address: endpoint.IPAMConfig.IPv6Address,
LinkLocalIPs: append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...),
} }
} else if endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "" { } else if name != "bridge" && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "") {
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{ endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: endpoint.IPAddress, IPv4Address: endpoint.IPAddress,
IPv6Address: endpoint.GlobalIPv6Address, IPv6Address: endpoint.GlobalIPv6Address,
@@ -800,6 +831,39 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
return config, extraNetworks return config, extraNetworks
} }
const unsupportedUserSpecifiedIPAddress = "user specified IP address is supported only when connecting to networks with user configured subnets"
func clearUnsupportedDynamicEndpointIPAM(err error, endpoints map[string]*network.EndpointSettings, networkSettings *container.NetworkSettings) bool {
if err == nil || !strings.Contains(err.Error(), unsupportedUserSpecifiedIPAddress) {
return false
}
for name, endpoint := range endpoints {
if !isDynamicContainerNetwork(networkSettings, name) || endpoint == nil || endpoint.IPAMConfig == nil {
continue
}
if strings.Contains(err.Error(), "network "+name+":") {
endpoint.IPAMConfig = nil
return true
}
}
cleared := false
for name, endpoint := range endpoints {
if isDynamicContainerNetwork(networkSettings, name) && endpoint != nil && endpoint.IPAMConfig != nil {
endpoint.IPAMConfig = nil
cleared = true
}
}
return cleared
}
func isDynamicContainerNetwork(networkSettings *container.NetworkSettings, name string) bool {
if networkSettings == nil || name == "bridge" {
return false
}
endpoint := networkSettings.Networks[name]
return endpoint != nil && endpoint.IPAMConfig == nil && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "")
}
func cloneContainerConfig(config *container.Config) *container.Config { func cloneContainerConfig(config *container.Config) *container.Config {
if config == nil { if config == nil {
return &container.Config{} return &container.Config{}
+2 -2
View File
@@ -54,7 +54,7 @@ func (u *BackupService) MongodbBackup(req dto.CommonBackup) error {
} }
if err := handleMongodbBackup(req, nil, record.ID, targetDir, fileName); err != nil { if err := handleMongodbBackup(req, nil, record.ID, targetDir, fileName); err != nil {
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) markBackupFailed(record.ID, err)
return err return err
} }
return nil return nil
@@ -98,7 +98,7 @@ func handleMongodbBackup(req dto.CommonBackup, parentTask *task.Task, recordID u
) )
go func() { go func() {
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) markBackupFailed(recordID, err)
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+2 -2
View File
@@ -47,7 +47,7 @@ func (u *BackupService) MysqlBackup(req dto.CommonBackup) error {
databaseHelper := DatabaseHelper{Database: req.Name, DBType: req.Type, Name: req.DetailName, Args: req.Args} databaseHelper := DatabaseHelper{Database: req.Name, DBType: req.Type, Name: req.DetailName, Args: req.Args}
if err := handleMysqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID, req.Secret); err != nil { if err := handleMysqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID, req.Secret); err != nil {
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) markBackupFailed(record.ID, err)
return err return err
} }
return nil return nil
@@ -100,7 +100,7 @@ func handleMysqlBackup(db DatabaseHelper, parentTask *task.Task, recordID uint,
backupTask.AddSubTaskWithOps(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 0, 3*time.Hour) backupTask.AddSubTaskWithOps(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 0, 3*time.Hour)
go func() { go func() {
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) markBackupFailed(recordID, err)
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+3 -1
View File
@@ -39,10 +39,12 @@ func (u *BackupService) PostgresqlBackup(req dto.CommonBackup) error {
} }
if err := backupRepo.CreateRecord(record); err != nil { if err := backupRepo.CreateRecord(record); err != nil {
global.LOG.Errorf("save backup record failed, err: %v", err) global.LOG.Errorf("save backup record failed, err: %v", err)
return err
} }
databaseHelper := DatabaseHelper{Database: req.Name, DBType: req.Type, Name: req.DetailName} databaseHelper := DatabaseHelper{Database: req.Name, DBType: req.Type, Name: req.DetailName}
if err := handlePostgresqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID, req.Secret); err != nil { if err := handlePostgresqlBackup(databaseHelper, nil, record.ID, targetDir, fileName, req.TaskID, req.Secret); err != nil {
markBackupFailed(record.ID, err)
return err return err
} }
return nil return nil
@@ -88,7 +90,7 @@ func handlePostgresqlBackup(db DatabaseHelper, parentTask *task.Task, recordID u
backupTask.AddSubTaskWithOps(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 0, 3*time.Hour) backupTask.AddSubTaskWithOps(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 0, 3*time.Hour)
go func() { go func() {
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) markBackupFailed(recordID, err)
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+2 -1
View File
@@ -59,6 +59,7 @@ func (u *BackupService) RedisBackup(req dto.CommonBackup) error {
} }
if err := handleRedisBackup(redisInfo, nil, record.ID, backupDir, fileName, req.Secret, req.TaskID); err != nil { if err := handleRedisBackup(redisInfo, nil, record.ID, backupDir, fileName, req.Secret, req.TaskID); err != nil {
markBackupFailed(record.ID, err)
return err return err
} }
return nil return nil
@@ -129,7 +130,7 @@ func handleRedisBackup(redisInfo *repo.RootInfo, parentTask *task.Task, recordID
itemTask.AddSubTaskWithOps(i18n.GetMsgByKey("TaskBackup"), backupDatabase, nil, 3, time.Hour) itemTask.AddSubTaskWithOps(i18n.GetMsgByKey("TaskBackup"), backupDatabase, nil, 3, time.Hour)
go func() { go func() {
if err := itemTask.Execute(); err != nil { if err := itemTask.Execute(); err != nil {
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) markBackupFailed(recordID, err)
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+2 -1
View File
@@ -54,6 +54,7 @@ func (u *BackupService) WebsiteBackup(req dto.CommonBackup) error {
return err return err
} }
if err = handleWebsiteBackup(&website, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil { if err = handleWebsiteBackup(&website, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil {
markBackupFailed(record.ID, err)
global.LOG.Errorf("backup website %s failed, err: %v", website.Alias, err) global.LOG.Errorf("backup website %s failed, err: %v", website.Alias, err)
return err return err
} }
@@ -235,7 +236,7 @@ func handleWebsiteBackup(website *model.Website, parentTask *task.Task, recordID
backupTask.AddSubTaskWithOps(task.GetTaskName(website.Alias, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 3, time.Hour) backupTask.AddSubTaskWithOps(task.GetTaskName(website.Alias, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 3, time.Hour)
go func() { go func() {
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()}) markBackupFailed(recordID, err)
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+137 -238
View File
@@ -15,6 +15,7 @@ import (
"os/exec" "os/exec"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -32,7 +33,6 @@ import (
"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/docker" "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/build" "github.com/docker/docker/api/types/build"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
@@ -53,6 +53,8 @@ import (
type ContainerService struct{} type ContainerService struct{}
var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>")
type IContainerService interface { type IContainerService interface {
Page(req dto.PageContainer) (int64, interface{}, error) Page(req dto.PageContainer) (int64, interface{}, error)
List() []dto.ContainerOptions List() []dto.ContainerOptions
@@ -483,15 +485,19 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
if err != nil { if err != nil {
return err return err
} }
defer client.Close() unlock := containerOperationLock.lock(req.Name)
ctx := context.Background() ctx := context.Background()
newContainer, _ := client.ContainerInspect(ctx, req.Name) newContainer, _ := client.ContainerInspect(ctx, req.Name)
if newContainer.ContainerJSONBase != nil { if newContainer.ContainerJSONBase != nil {
unlock()
_ = client.Close()
return buserr.New("ErrContainerName") return buserr.New("ErrContainerName")
} }
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeContainer, req.TaskID, 1) taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeContainer, req.TaskID, 1)
if err != nil { if err != nil {
unlock()
_ = client.Close()
global.LOG.Errorf("new task for create container failed, err: %v", err) global.LOG.Errorf("new task for create container failed, err: %v", err)
return err return err
} }
@@ -527,18 +533,20 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
if err != nil { if err != nil {
return err return err
} }
removeUnsupportedEndpointStaticIPAM(client, networkConf, nil) normalizeContainerEndpointSettings(ctx, client, networkConf, nil)
con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name) con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
if err != nil { if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed")) taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
_ = client.ContainerRemove(ctx, req.Name, container.RemoveOptions{RemoveVolumes: true, Force: true}) if con.ID != "" {
_ = client.ContainerRemove(ctx, con.ID, container.RemoveOptions{RemoveVolumes: true, Force: true})
}
return err return err
} }
err = client.ContainerStart(ctx, con.ID, container.StartOptions{}) err = client.ContainerStart(ctx, con.ID, container.StartOptions{})
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err) taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err)
if err != nil { if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed")) taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
_ = client.ContainerRemove(ctx, req.Name, container.RemoveOptions{RemoveVolumes: true, Force: true}) _ = client.ContainerRemove(ctx, con.ID, container.RemoveOptions{RemoveVolumes: true, Force: true})
return fmt.Errorf("create successful but start failed, err: %v", err) return fmt.Errorf("create successful but start failed, err: %v", err)
} }
return nil return nil
@@ -546,12 +554,16 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
if inThread { if inThread {
go func() { go func() {
defer unlock()
defer client.Close()
if err := taskItem.Execute(); err != nil { if err := taskItem.Execute(); err != nil {
global.LOG.Error(err.Error()) global.LOG.Error(err.Error())
} }
}() }()
return nil return nil
} }
defer unlock()
defer client.Close()
return taskItem.Execute() return taskItem.Execute()
} }
@@ -572,21 +584,7 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
data.Image = oldContainer.Config.Image data.Image = oldContainer.Config.Image
if oldContainer.NetworkSettings != nil { if oldContainer.NetworkSettings != nil {
for net, val := range oldContainer.NetworkSettings.Networks { for net, val := range oldContainer.NetworkSettings.Networks {
netItem := dto.ContainerNetwork{ data.Networks = append(data.Networks, loadContainerNetworkInfo(net, val))
Network: net,
MacAddr: val.MacAddress,
}
if val.IPAMConfig != nil {
if netItem.Network != "bridge" {
netItem.Ipv4 = val.IPAMConfig.IPv4Address
netItem.Ipv6 = val.IPAMConfig.IPv6Address
}
} else {
if netItem.Network != "bridge" {
netItem.Ipv4 = val.IPAddress
}
}
data.Networks = append(data.Networks, netItem)
} }
} }
@@ -632,141 +630,40 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
return &data, nil return &data, nil
} }
func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error { func loadContainerNetworkInfo(name string, endpoint *network.EndpointSettings) dto.ContainerNetwork {
client, err := docker.NewDockerClient() item := dto.ContainerNetwork{Network: name}
if err != nil { if endpoint == nil {
return err return item
} }
defer client.Close() item.MacAddr = endpoint.MacAddress
ctx := context.Background() item.Links = append([]string(nil), endpoint.Links...)
oldContainer, err := client.ContainerInspect(ctx, req.Name) item.Aliases = append([]string(nil), endpoint.Aliases...)
if err != nil { item.DriverOpts = cloneStringMap(endpoint.DriverOpts)
return err item.GwPriority = endpoint.GwPriority
if endpoint.IPAMConfig != nil {
item.LinkLocalIPs = append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...)
} }
if name != "bridge" {
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskUpdate, task.TaskScopeContainer, req.TaskID, 1) if endpoint.IPAMConfig != nil {
if err != nil { item.Ipv4 = endpoint.IPAMConfig.IPv4Address
global.LOG.Errorf("new task for create container failed, err: %v", err) item.Ipv6 = endpoint.IPAMConfig.IPv6Address
return err } else {
} item.Ipv4 = endpoint.IPAddress
go func() { item.Ipv6 = endpoint.GlobalIPv6Address
taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error {
if !checkImageExist(client, req.Image) || req.ForcePull {
if err := pullImages(taskItem, client, req.Image); err != nil {
if !req.ForcePull {
return err
}
return fmt.Errorf("pull image %s failed, err: %v", req.Image, err)
}
}
return nil
}, nil)
taskItem.AddSubTask(i18n.GetWithName("ContainerCreate", req.Name), func(t *task.Task) error {
err := client.ContainerRemove(ctx, req.Name, container.RemoveOptions{Force: true})
taskItem.LogWithStatus(i18n.GetWithName("ContainerRemoveOld", req.Name), err)
if err != nil {
return err
}
config, hostConf, networkConf, err := loadConfigInfo(false, req, &oldContainer)
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerLoadInfo"), err)
if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerRecreate"))
reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings)
return err
}
removeUnsupportedEndpointStaticIPAM(client, networkConf, nil)
con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerRecreate"))
reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings)
return fmt.Errorf("update container failed, err: %v", err)
}
err = client.ContainerStart(ctx, con.ID, container.StartOptions{})
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err)
if err != nil {
return fmt.Errorf("update successful but start failed, err: %v", err)
}
return nil
}, nil)
if err := taskItem.Execute(); err != nil {
global.LOG.Error(err.Error())
} }
}() }
return item
return nil
} }
func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error { func cloneStringMap(source map[string]string) map[string]string {
client, err := docker.NewDockerClient() if len(source) == 0 {
if err != nil { return nil
return err
} }
defer client.Close() result := make(map[string]string, len(source))
ctx := context.Background() for key, value := range source {
taskItem, err := task.NewTaskWithOps(req.Image, task.TaskUpgrade, task.TaskScopeImage, req.TaskID, 1) result[key] = value
if err != nil {
global.LOG.Errorf("new task for create container failed, err: %v", err)
return err
} }
go func() { return result
taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error {
taskItem.LogStart(i18n.GetWithName("ContainerImagePull", req.Image))
if !checkImageExist(client, req.Image) || req.ForcePull {
if err := pullImages(taskItem, client, req.Image); err != nil {
if !req.ForcePull {
return err
}
return fmt.Errorf("pull image %s failed, err: %v", req.Image, err)
}
}
return nil
}, nil)
for _, item := range req.Names {
var oldContainer container.InspectResponse
taskItem.AddSubTask(i18n.GetWithName("ContainerLoadInfo", item), func(t *task.Task) error {
taskItem.Logf("----------------- %s -----------------", item)
oldContainer, err = client.ContainerInspect(ctx, item)
if err != nil {
return err
}
return nil
}, nil)
taskItem.AddSubTask(i18n.GetWithName("ContainerCreate", item), func(t *task.Task) error {
config := oldContainer.Config
config.Image = req.Image
hostConf := oldContainer.HostConfig
err := client.ContainerRemove(ctx, item, container.RemoveOptions{Force: true})
taskItem.LogWithStatus(i18n.GetWithName("ContainerRemoveOld", item), err)
if err != nil {
return err
}
con, err := createContainerWithOldNetworks(ctx, client, config, hostConf, oldContainer.NetworkSettings, item)
if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerRecreate"))
reCreateAfterUpdate(item, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings)
return fmt.Errorf("upgrade container failed, err: %v", err)
}
err = client.ContainerStart(ctx, con.ID, container.StartOptions{})
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err)
if err != nil {
return fmt.Errorf("upgrade successful but start failed, err: %v", err)
}
return nil
}, nil)
}
if err := taskItem.Execute(); err != nil {
global.LOG.Error(err.Error())
}
}()
return nil
} }
func (u *ContainerService) ContainerRename(req dto.ContainerRename) error { func (u *ContainerService) ContainerRename(req dto.ContainerRename) error {
@@ -776,6 +673,8 @@ func (u *ContainerService) ContainerRename(req dto.ContainerRename) error {
return err return err
} }
defer client.Close() defer client.Close()
unlock := containerOperationLock.lock(req.Name, req.NewName)
defer unlock()
newContainer, _ := client.ContainerInspect(ctx, req.NewName) newContainer, _ := client.ContainerInspect(ctx, req.NewName)
if newContainer.ContainerJSONBase != nil { if newContainer.ContainerJSONBase != nil {
@@ -820,44 +719,48 @@ func (u *ContainerService) ContainerCommit(req dto.ContainerCommit) error {
} }
func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error { func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error {
var err error
ctx := context.Background() ctx := context.Background()
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
return err return err
} }
defer client.Close()
taskItem, err := task.NewTaskWithOps(strings.Join(req.Names, " "), req.Operation, task.TaskScopeContainer, req.TaskID, 1) taskItem, err := task.NewTaskWithOps(strings.Join(req.Names, " "), req.Operation, task.TaskScopeContainer, req.TaskID, 1)
if err != nil { if err != nil {
_ = client.Close()
return fmt.Errorf("new task for container commit failed, err: %v", err) return fmt.Errorf("new task for container commit failed, err: %v", err)
} }
for _, item := range req.Names { for _, item := range req.Names {
item := item
taskItem.AddSubTask(item, func(t *task.Task) error { taskItem.AddSubTask(item, func(t *task.Task) error {
unlock := containerOperationLock.lock(item)
defer unlock()
var operationErr error
switch req.Operation { switch req.Operation {
case constant.ContainerOpStart: case constant.ContainerOpStart:
err = client.ContainerStart(ctx, item, container.StartOptions{}) operationErr = client.ContainerStart(ctx, item, container.StartOptions{})
case constant.ContainerOpStop: case constant.ContainerOpStop:
err = client.ContainerStop(ctx, item, container.StopOptions{}) operationErr = client.ContainerStop(ctx, item, container.StopOptions{})
case constant.ContainerOpRestart: case constant.ContainerOpRestart:
err = client.ContainerRestart(ctx, item, container.StopOptions{}) operationErr = client.ContainerRestart(ctx, item, container.StopOptions{})
case constant.ContainerOpKill: case constant.ContainerOpKill:
err = client.ContainerKill(ctx, item, "SIGKILL") operationErr = client.ContainerKill(ctx, item, "SIGKILL")
case constant.ContainerOpPause: case constant.ContainerOpPause:
err = client.ContainerPause(ctx, item) operationErr = client.ContainerPause(ctx, item)
case constant.ContainerOpUnpause: case constant.ContainerOpUnpause:
err = client.ContainerUnpause(ctx, item) operationErr = client.ContainerUnpause(ctx, item)
case constant.ContainerOpRemove: case constant.ContainerOpRemove:
err = client.ContainerRemove(ctx, item, container.RemoveOptions{RemoveVolumes: true, Force: true}) operationErr = client.ContainerRemove(ctx, item, container.RemoveOptions{RemoveVolumes: true, Force: true})
} }
return err return operationErr
}, nil) }, nil)
} }
go func() { go func() {
defer client.Close()
_ = taskItem.Execute() _ = taskItem.Execute()
}() }()
return err return nil
} }
func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error { func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
@@ -866,6 +769,8 @@ func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error {
return err return err
} }
defer client.Close() defer client.Close()
unlock := containerOperationLock.lock(req.Name)
defer unlock()
ctx := context.Background() ctx := context.Background()
containerItem, err := client.ContainerInspect(ctx, req.Name) containerItem, err := client.ContainerInspect(ctx, req.Name)
if err != nil { if err != nil {
@@ -1036,6 +941,7 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
if cmd.CheckIllegal(container, since, tail) { if cmd.CheckIllegal(container, since, tail) {
return buserr.New("ErrCmdIllegal") return buserr.New("ErrCmdIllegal")
} }
ctx := c.Request.Context()
commandArg := []string{"logs", container} commandArg := []string{"logs", container}
dockerCommand := global.CONF.DockerConfig.Command dockerCommand := global.CONF.DockerConfig.Command
if containerType == "compose" { if containerType == "compose" {
@@ -1065,10 +971,11 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
} }
var dockerCmd *exec.Cmd var dockerCmd *exec.Cmd
if containerType == "compose" && dockerCommand == "docker-compose" { if containerType == "compose" && dockerCommand == "docker-compose" {
dockerCmd = exec.Command("docker-compose", commandArg...) dockerCmd = exec.CommandContext(ctx, "docker-compose", commandArg...)
} else { } else {
dockerCmd = exec.Command("docker", commandArg...) dockerCmd = exec.CommandContext(ctx, "docker", commandArg...)
} }
dockerCmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
stdout, err := dockerCmd.StdoutPipe() stdout, err := dockerCmd.StdoutPipe()
if err != nil { if err != nil {
return err return err
@@ -1077,15 +984,20 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
if err := dockerCmd.Start(); err != nil { if err := dockerCmd.Start(); err != nil {
return err return err
} }
defer func() { done := make(chan struct{})
if dockerCmd.Process != nil { go func() {
_ = dockerCmd.Process.Kill() select {
_ = dockerCmd.Wait() case <-ctx.Done():
killContainerLogProcess(dockerCmd)
case <-done:
} }
}() }()
defer close(done)
tempFile, err := os.CreateTemp("", "cmd_output_*.txt") tempFile, err := os.CreateTemp("", "cmd_output_*.txt")
if err != nil { if err != nil {
killContainerLogProcess(dockerCmd)
_ = dockerCmd.Wait()
return err return err
} }
defer tempFile.Close() defer tempFile.Close()
@@ -1094,31 +1006,19 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
global.LOG.Errorf("os.Remove() failed: %v", err) global.LOG.Errorf("os.Remove() failed: %v", err)
} }
}() }()
errCh := make(chan error, 1) copyErr := copyContainerLogOutput(tempFile, stdout)
go func() { waitErr := dockerCmd.Wait()
scanner := bufio.NewScanner(stdout) if copyErr != nil {
var ansiRegex = re.GetRegex(re.AnsiEscapePattern) return copyErr
for scanner.Scan() { }
line := scanner.Text() if waitErr != nil {
cleanLine := ansiRegex.ReplaceAllString(line, "") if ctx.Err() != nil {
if _, err := tempFile.WriteString(cleanLine + "\n"); err != nil { return ctx.Err()
errCh <- err
return
}
} }
if err := scanner.Err(); err != nil { return waitErr
errCh <- err }
return if _, err := tempFile.Seek(0, io.SeekStart); err != nil {
} return err
errCh <- nil
}()
select {
case err := <-errCh:
if err != nil {
global.LOG.Errorf("Error: %v", err)
}
case <-time.After(40 * time.Second):
global.LOG.Errorf("Download container logs timeout reached")
} }
info, _ := tempFile.Stat() info, _ := tempFile.Stat()
@@ -1128,6 +1028,36 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
return nil return nil
} }
func copyContainerLogOutput(dst io.Writer, src io.Reader) error {
reader := bufio.NewReader(src)
for {
line, err := reader.ReadString('\n')
if len(line) > 0 {
cleanLine := containerLogAnsiRegex.ReplaceAllString(line, "")
if _, writeErr := io.WriteString(dst, cleanLine); writeErr != nil {
return writeErr
}
}
if err != nil {
if err == io.EOF {
return nil
}
return err
}
}
}
func killContainerLogProcess(command *exec.Cmd) {
if command == nil || command.Process == nil {
return
}
if pgid, err := syscall.Getpgid(command.Process.Pid); err == nil {
_ = syscall.Kill(-pgid, syscall.SIGKILL)
return
}
_ = command.Process.Kill()
}
func (u *ContainerService) ContainerStats(id string) (*dto.ContainerStats, error) { func (u *ContainerService) ContainerStats(id string) (*dto.ContainerStats, error) {
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
@@ -1805,7 +1735,7 @@ func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerLis
return data return data
} }
func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) { func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error) {
portMap := make(nat.PortMap) portMap := make(nat.PortMap)
if len(ports) == 0 { if len(ports) == 0 {
return portMap, nil return portMap, nil
@@ -1830,7 +1760,7 @@ func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) {
portMap[nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))] = []nat.PortBinding{bindItem} portMap[nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))] = []nat.PortBinding{bindItem}
} }
for i := hostStart; i <= hostEnd; i++ { for i := hostStart; i <= hostEnd; i++ {
if common.ScanPortWithIP(port.HostIP, i) { if checkInUse && common.ScanPortWithIP(port.HostIP, i) {
return portMap, buserr.WithDetail("ErrPortInUsed", i, nil) return portMap, buserr.WithDetail("ErrPortInUsed", i, nil)
} }
} }
@@ -1841,7 +1771,7 @@ func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) {
} else { } else {
portItem, _ = strconv.Atoi(port.HostPort) portItem, _ = strconv.Atoi(port.HostPort)
} }
if common.ScanPortWithIP(port.HostIP, portItem) { if checkInUse && common.ScanPortWithIP(port.HostIP, portItem) {
return portMap, buserr.WithDetail("ErrPortInUsed", portItem, nil) return portMap, buserr.WithDetail("ErrPortInUsed", portItem, nil)
} }
bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP} bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP}
@@ -1860,7 +1790,7 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta
} }
var networkConf network.NetworkingConfig var networkConf network.NetworkingConfig
portMap, err := checkPortStats(req.ExposedPorts) portMap, err := checkPortStats(req.ExposedPorts, isCreate)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
@@ -1888,15 +1818,21 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta
case "host", "none", "bridge": case "host", "none", "bridge":
hostConf.NetworkMode = container.NetworkMode(item.Network) hostConf.NetworkMode = container.NetworkMode(item.Network)
} }
if item.Ipv4 != "" || item.Ipv6 != "" { endpoint := &network.EndpointSettings{
networkConf.EndpointsConfig[item.Network] = &network.EndpointSettings{ Links: append([]string(nil), item.Links...),
IPAMConfig: &network.EndpointIPAMConfig{ Aliases: append([]string(nil), item.Aliases...),
IPv4Address: item.Ipv4, DriverOpts: cloneStringMap(item.DriverOpts),
IPv6Address: item.Ipv6, GwPriority: item.GwPriority,
}, MacAddress: item.MacAddr} MacAddress: item.MacAddr,
} else {
networkConf.EndpointsConfig[item.Network] = &network.EndpointSettings{}
} }
if item.Ipv4 != "" || item.Ipv6 != "" || len(item.LinkLocalIPs) != 0 {
endpoint.IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: item.Ipv4,
IPv6Address: item.Ipv6,
LinkLocalIPs: append([]string(nil), item.LinkLocalIPs...),
}
}
networkConf.EndpointsConfig[item.Network] = endpoint
} }
} else { } else {
return nil, nil, nil, fmt.Errorf("please set up the network") return nil, nil, nil, fmt.Errorf("please set up the network")
@@ -1943,43 +1879,6 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta
return &config, &hostConf, &networkConf, nil return &config, &hostConf, &networkConf, nil
} }
func reCreateAfterUpdate(name string, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkConf *container.NetworkSettings) {
ctx := context.Background()
oldContainer, err := createContainerWithOldNetworks(ctx, client, config, hostConf, networkConf, name)
if err != nil {
global.LOG.Errorf("recreate after container update failed, err: %v", err)
return
}
if err := client.ContainerStart(ctx, oldContainer.ID, container.StartOptions{}); err != nil {
global.LOG.Errorf("restart after container update failed, err: %v", err)
}
global.LOG.Info("recreate after container update successful")
}
func createContainerWithOldNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkSettings *container.NetworkSettings, name string) (container.CreateResponse, error) {
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(networkSettings, hostConf)
removeUnsupportedEndpointStaticIPAM(client, networkConf, extraNetworks)
created, err := client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name)
if err != nil {
return created, err
}
extraNames := make([]string, 0, len(extraNetworks))
for item := range extraNetworks {
extraNames = append(extraNames, item)
}
sort.Strings(extraNames)
for _, item := range extraNames {
if err := client.NetworkConnect(ctx, item, created.ID, extraNetworks[item]); err != nil {
_ = client.ContainerRemove(ctx, created.ID, container.RemoveOptions{Force: true})
return created, err
}
}
return created, nil
}
func loadVolumeBinds(binds []container.MountPoint) []dto.VolumeHelper { func loadVolumeBinds(binds []container.MountPoint) []dto.VolumeHelper {
var datas []dto.VolumeHelper var datas []dto.VolumeHelper
for _, bind := range binds { for _, bind := range binds {
+4
View File
@@ -270,6 +270,10 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
if stdout, err := compose.Up(req.Path); err != nil { if stdout, err := compose.Up(req.Path); 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" {
if stdout, err := compose.DownAndUp(req.Path); err != nil {
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); 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)
+657
View File
@@ -0,0 +1,657 @@
package service
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
ctx := context.Background()
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskUpdate, task.TaskScopeContainer, req.TaskID, 1)
if err != nil {
_ = client.Close()
global.LOG.Errorf("new task for create container failed, err: %v", err)
return err
}
go func() {
defer client.Close()
taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error {
if !checkImageExist(client, req.Image) || req.ForcePull {
if err := pullImages(taskItem, client, req.Image); err != nil {
if !req.ForcePull {
return err
}
return fmt.Errorf("pull image %s failed, err: %v", req.Image, err)
}
}
return nil
}, nil)
taskItem.AddSubTaskWithOps(task.GetTaskName(req.Name, task.TaskUpdate, task.TaskScopeContainer), func(t *task.Task) error {
t.LogStart(i18n.GetWithName("ContainerAcquireLock", req.Name))
unlock := containerOperationLock.lock(req.Name)
defer unlock()
t.LogWithStatus(i18n.GetWithName("ContainerAcquireLock", req.Name), nil)
oldContainer, err := client.ContainerInspect(ctx, req.Name)
if err != nil {
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerLoadInfo"), err)
return err
}
config, hostConf, networkConf, err := loadConfigInfo(false, req, &oldContainer)
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerLoadInfo"), err)
if err != nil {
return err
}
normalizeContainerEndpointSettings(ctx, client, networkConf, nil)
cleanupErr, err := switchContainer(ctx, client, req.Name, oldContainer, func() (container.CreateResponse, error) {
return createContainerWithDynamicIPFallback(func() (container.CreateResponse, error) {
return client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
}, networkConf.EndpointsConfig, oldContainer.NetworkSettings)
}, newContainerSwitchTaskLogger(t))
if err != nil {
return fmt.Errorf("update container failed, err: %v", err)
}
if cleanupErr != nil {
taskItem.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr))
}
return nil
}, nil, 0, 0)
if err := taskItem.Execute(); err != nil {
global.LOG.Error(err.Error())
}
}()
return nil
}
func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
ctx := context.Background()
taskItem, err := task.NewTaskWithOps(req.Image, task.TaskUpgrade, task.TaskScopeImage, req.TaskID, 1)
if err != nil {
_ = client.Close()
global.LOG.Errorf("new task for create container failed, err: %v", err)
return err
}
go func() {
defer client.Close()
taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error {
taskItem.LogStart(i18n.GetWithName("ContainerImagePull", req.Image))
if !checkImageExist(client, req.Image) || req.ForcePull {
if err := pullImages(taskItem, client, req.Image); err != nil {
if !req.ForcePull {
return err
}
return fmt.Errorf("pull image %s failed, err: %v", req.Image, err)
}
}
return nil
}, nil)
var upgradeErrors []error
for _, item := range req.Names {
item := item
taskItem.AddSubTaskWithIgnoreErr(i18n.GetWithName("ContainerUpgradeItem", item), func(t *task.Task) error {
t.Logf("----------------- %s -----------------", item)
t.LogStart(i18n.GetWithName("ContainerAcquireLock", item))
unlock := containerOperationLock.lock(item)
defer unlock()
t.LogWithStatus(i18n.GetWithName("ContainerAcquireLock", item), nil)
oldContainer, inspectErr := client.ContainerInspect(ctx, item)
t.LogWithStatus(i18n.GetWithName("ContainerLoadInfo", item), inspectErr)
if inspectErr != nil {
err := fmt.Errorf("reload container %s failed: %w", item, inspectErr)
upgradeErrors = append(upgradeErrors, err)
return err
}
config := cloneContainerConfig(oldContainer.Config)
config.Image = req.Image
hostConf := cloneContainerHostConfig(oldContainer.HostConfig)
preserveContainerVolumeMounts(hostConf, oldContainer.Mounts)
cleanupErr, err := switchContainer(ctx, client, item, oldContainer, func() (container.CreateResponse, error) {
return createContainerWithOldNetworks(ctx, client, config, hostConf, oldContainer.NetworkSettings, item)
}, newContainerSwitchTaskLogger(t))
if err != nil {
upgradeErr := fmt.Errorf("upgrade container %s failed: %w", item, err)
upgradeErrors = append(upgradeErrors, upgradeErr)
return upgradeErr
}
if cleanupErr != nil {
t.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr))
}
return nil
})
}
taskItem.AddSubTask(i18n.GetMsgByKey("ContainerUpgradeSummary"), func(t *task.Task) error {
return errors.Join(upgradeErrors...)
}, nil)
if err := taskItem.Execute(); err != nil {
global.LOG.Error(err.Error())
}
}()
return nil
}
type containerSwitchClient interface {
ContainerStop(context.Context, string, container.StopOptions) error
ContainerRename(context.Context, string, string) error
ContainerStart(context.Context, string, container.StartOptions) error
ContainerRemove(context.Context, string, container.RemoveOptions) error
ContainerInspect(context.Context, string) (container.InspectResponse, error)
NetworkConnect(context.Context, string, string, *network.EndpointSettings) error
NetworkDisconnect(context.Context, string, string, bool) error
}
type containerOperationMutex struct {
mutex sync.Mutex
locks map[string]*containerOperationLockEntry
}
var containerOperationLock containerOperationMutex
type containerOperationLockEntry struct {
mutex sync.Mutex
references int
}
func (l *containerOperationMutex) lock(names ...string) func() {
nameSet := make(map[string]struct{}, len(names))
for _, name := range names {
if name != "" {
nameSet[name] = struct{}{}
}
}
orderedNames := make([]string, 0, len(nameSet))
for name := range nameSet {
orderedNames = append(orderedNames, name)
}
sort.Strings(orderedNames)
if len(orderedNames) == 0 {
return func() {}
}
l.mutex.Lock()
if l.locks == nil {
l.locks = make(map[string]*containerOperationLockEntry)
}
entries := make([]*containerOperationLockEntry, 0, len(orderedNames))
for _, name := range orderedNames {
entry := l.locks[name]
if entry == nil {
entry = &containerOperationLockEntry{}
l.locks[name] = entry
}
entry.references++
entries = append(entries, entry)
}
l.mutex.Unlock()
for _, entry := range entries {
entry.mutex.Lock()
}
return func() {
for index := len(entries) - 1; index >= 0; index-- {
entries[index].mutex.Unlock()
}
l.mutex.Lock()
defer l.mutex.Unlock()
for index, name := range orderedNames {
entry := entries[index]
entry.references--
if entry.references == 0 {
delete(l.locks, name)
}
}
}
}
type containerNetworkAttachment struct {
name string
endpoint *network.EndpointSettings
isDynamic bool
}
type containerSwitchLogFunc func(messageKey, containerName string, err error)
func newContainerSwitchTaskLogger(t *task.Task) containerSwitchLogFunc {
return func(messageKey, containerName string, err error) {
t.LogWithStatus(i18n.GetWithName(messageKey, containerName), err)
}
}
func logContainerSwitchStep(logger containerSwitchLogFunc, messageKey, containerName string, err error) {
if logger != nil {
logger(messageKey, containerName, err)
}
}
// switchContainer keeps the stopped original container as the rollback target until the replacement starts.
func switchContainer(
ctx context.Context,
cli containerSwitchClient,
name string,
oldContainer container.InspectResponse,
createNew func() (container.CreateResponse, error),
logger containerSwitchLogFunc,
) (cleanupErr error, err error) {
if oldContainer.ID == "" {
return nil, fmt.Errorf("original container ID is empty")
}
wasRunning := oldContainer.State != nil && oldContainer.State.Running
if wasRunning && oldContainer.HostConfig != nil && oldContainer.HostConfig.AutoRemove {
return nil, fmt.Errorf("cannot safely replace container %s with auto-remove enabled", name)
}
if wasRunning {
if err := cli.ContainerStop(ctx, oldContainer.ID, container.StopOptions{}); err != nil {
logContainerSwitchStep(logger, "ContainerStopOld", name, err)
current, inspectErr := cli.ContainerInspect(ctx, oldContainer.ID)
if inspectErr == nil && current.State != nil && !current.State.Running {
restartErr := restartOriginalContainer(ctx, cli, oldContainer.ID)
logContainerSwitchStep(logger, "ContainerRollbackRestartOld", name, restartErr)
return nil, errors.Join(fmt.Errorf("stop original container failed: %w", err), restartErr)
}
return nil, fmt.Errorf("stop original container failed: %w", err)
}
logContainerSwitchStep(logger, "ContainerStopOld", name, nil)
}
backupName := containerSwitchBackupName(oldContainer.ID)
if err := cli.ContainerRename(ctx, oldContainer.ID, backupName); err != nil {
logContainerSwitchStep(logger, "ContainerRenameOld", name, err)
if wasRunning {
restartErr := restartOriginalContainer(ctx, cli, oldContainer.ID)
logContainerSwitchStep(logger, "ContainerRollbackRestartOld", name, restartErr)
return nil, errors.Join(fmt.Errorf("rename original container failed: %w", err), restartErr)
}
return nil, fmt.Errorf("rename original container failed: %w", err)
}
logContainerSwitchStep(logger, "ContainerRenameOld", name, nil)
disconnectedNetworks, disconnectErr := disconnectOriginalContainerNetworks(ctx, cli, oldContainer)
logContainerSwitchStep(logger, "ContainerDisconnectOld", backupName, disconnectErr)
if disconnectErr != nil {
rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, "", disconnectedNetworks, logger)
return nil, errors.Join(disconnectErr, rollbackErr)
}
created, createErr := createNew()
logContainerSwitchStep(logger, "ContainerCreateReplacement", name, createErr)
if createErr != nil {
rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger)
return nil, errors.Join(createErr, rollbackErr)
}
if err := cli.ContainerStart(ctx, created.ID, container.StartOptions{}); err != nil {
logContainerSwitchStep(logger, "ContainerStartReplacement", name, err)
rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger)
return nil, errors.Join(fmt.Errorf("start new container failed: %w", err), rollbackErr)
}
logContainerSwitchStep(logger, "ContainerStartReplacement", name, nil)
if wasRunning {
if err := waitContainerReady(ctx, cli, created.ID); err != nil {
logContainerSwitchStep(logger, "ContainerWaitReplacement", name, err)
rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger)
return nil, errors.Join(fmt.Errorf("new container readiness check failed: %w", err), rollbackErr)
}
logContainerSwitchStep(logger, "ContainerWaitReplacement", name, nil)
}
cleanupErr = cli.ContainerRemove(ctx, oldContainer.ID, container.RemoveOptions{Force: true, RemoveVolumes: false})
logContainerSwitchStep(logger, "ContainerRemoveOld", backupName, cleanupErr)
return cleanupErr, nil
}
const (
containerStartStabilization = 10 * time.Second
containerStartPollInterval = time.Second
containerHealthCheckMinWait = 30 * time.Second
containerHealthCheckMaxWait = 10 * time.Minute
)
func waitContainerReady(ctx context.Context, cli containerSwitchClient, containerID string) error {
info, err := cli.ContainerInspect(ctx, containerID)
if err != nil {
return err
}
if err := checkContainerRunningState(info); err != nil {
return err
}
if info.State.Health == nil {
return waitContainerStable(ctx, cli, containerID, info)
}
timeout := containerHealthCheckTimeout(info.Config)
deadline := time.NewTimer(timeout)
ticker := time.NewTicker(time.Second)
defer deadline.Stop()
defer ticker.Stop()
for {
if info.State.Restarting || info.RestartCount != 0 {
return fmt.Errorf("container restarted %d times during startup", info.RestartCount)
}
if info.State.Health == nil {
return fmt.Errorf("container health status is unavailable")
}
switch info.State.Health.Status {
case container.Healthy:
return nil
case container.Unhealthy:
return fmt.Errorf("container health status is unhealthy")
}
select {
case <-ctx.Done():
return ctx.Err()
case <-deadline.C:
return fmt.Errorf("container health check timed out after %s", timeout)
case <-ticker.C:
info, err = cli.ContainerInspect(ctx, containerID)
if err != nil {
return err
}
if err := checkContainerRunningState(info); err != nil {
return err
}
}
}
}
func waitContainerStable(ctx context.Context, cli containerSwitchClient, containerID string, initial container.InspectResponse) error {
startedAt := initial.State.StartedAt
if err := checkContainerStableState(initial, startedAt); err != nil {
return err
}
deadline := time.NewTimer(containerStartStabilization)
ticker := time.NewTicker(containerStartPollInterval)
defer deadline.Stop()
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-deadline.C:
info, err := cli.ContainerInspect(ctx, containerID)
if err != nil {
return err
}
return checkContainerStableState(info, startedAt)
case <-ticker.C:
info, err := cli.ContainerInspect(ctx, containerID)
if err != nil {
return err
}
if err := checkContainerStableState(info, startedAt); err != nil {
return err
}
}
}
}
func checkContainerStableState(info container.InspectResponse, startedAt string) error {
if err := checkContainerRunningState(info); err != nil {
return err
}
if info.State.Restarting || info.RestartCount != 0 {
return fmt.Errorf("container restarted %d times during startup", info.RestartCount)
}
if startedAt != "" && info.State.StartedAt != startedAt {
return fmt.Errorf("container start time changed during startup")
}
return nil
}
func checkContainerRunningState(info container.InspectResponse) error {
if info.State == nil {
return fmt.Errorf("container state is unavailable")
}
if !info.State.Running {
return fmt.Errorf("container exited with code %d: %s", info.State.ExitCode, info.State.Error)
}
return nil
}
func containerHealthCheckTimeout(config *container.Config) time.Duration {
if config == nil || config.Healthcheck == nil {
return containerHealthCheckMinWait
}
health := config.Healthcheck
interval := health.Interval
if interval <= 0 {
interval = 30 * time.Second
}
checkTimeout := health.Timeout
if checkTimeout <= 0 {
checkTimeout = 30 * time.Second
}
retries := health.Retries
if retries <= 0 {
retries = 3
}
timeout := health.StartPeriod + time.Duration(retries)*(interval+checkTimeout)
if timeout < containerHealthCheckMinWait {
return containerHealthCheckMinWait
}
if timeout > containerHealthCheckMaxWait {
return containerHealthCheckMaxWait
}
return timeout
}
func preserveContainerVolumeMounts(hostConfig *container.HostConfig, oldMounts []container.MountPoint) {
if hostConfig == nil {
return
}
for _, oldMount := range oldMounts {
if oldMount.Type != mount.TypeVolume || oldMount.Name == "" || oldMount.Destination == "" {
continue
}
preserved := false
for index := range hostConfig.Mounts {
if hostConfig.Mounts[index].Target == oldMount.Destination {
hostConfig.Mounts[index].Type = mount.TypeVolume
hostConfig.Mounts[index].Source = oldMount.Name
preserved = true
}
}
for index, raw := range hostConfig.Binds {
destination, mode := containerBindDestinationAndMode(raw)
if destination != oldMount.Destination {
continue
}
hostConfig.Binds[index] = oldMount.Name + ":" + oldMount.Destination
if mode != "" {
hostConfig.Binds[index] += ":" + mode
}
preserved = true
}
if !preserved {
hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{
Type: mount.TypeVolume,
Source: oldMount.Name,
Target: oldMount.Destination,
ReadOnly: !oldMount.RW,
})
}
}
}
func containerBindDestinationAndMode(raw string) (string, string) {
parts := strings.SplitN(raw, ":", 3)
switch len(parts) {
case 1:
return parts[0], ""
case 2:
return parts[1], ""
default:
return parts[1], parts[2]
}
}
func containerSwitchBackupName(containerID string) string {
if len(containerID) > 12 {
containerID = containerID[:12]
}
return "1panel-backup-" + containerID
}
func restartOriginalContainer(ctx context.Context, cli containerSwitchClient, containerID string) error {
if err := cli.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
return fmt.Errorf("restart original container failed: %w", err)
}
return nil
}
func disconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitchClient, oldContainer container.InspectResponse) ([]containerNetworkAttachment, error) {
primary, extras := buildContainerRecoverNetworkConfig(oldContainer.NetworkSettings, oldContainer.HostConfig)
endpoints := make(map[string]*network.EndpointSettings, len(extras)+1)
if primary != nil {
for name, endpoint := range primary.EndpointsConfig {
if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil {
endpoints[name] = endpoint
}
}
}
for name, endpoint := range extras {
if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil {
endpoints[name] = endpoint
}
}
names := make([]string, 0, len(endpoints))
for name := range endpoints {
names = append(names, name)
}
sort.Strings(names)
disconnected := make([]containerNetworkAttachment, 0, len(names))
for _, name := range names {
if err := cli.NetworkDisconnect(ctx, name, oldContainer.ID, true); err != nil {
return disconnected, fmt.Errorf("disconnect original container from network %s failed: %w", name, err)
}
disconnected = append(disconnected, containerNetworkAttachment{
name: name,
endpoint: endpoints[name],
isDynamic: isDynamicContainerNetwork(oldContainer.NetworkSettings, name),
})
}
return disconnected, nil
}
func reconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitchClient, containerID string, attachments []containerNetworkAttachment) error {
var reconnectErr error
for _, attachment := range attachments {
err := cli.NetworkConnect(ctx, attachment.name, containerID, attachment.endpoint)
if err != nil && attachment.isDynamic && strings.Contains(err.Error(), unsupportedUserSpecifiedIPAddress) {
attachment.endpoint.IPAMConfig = nil
err = cli.NetworkConnect(ctx, attachment.name, containerID, attachment.endpoint)
}
if err != nil {
reconnectErr = errors.Join(reconnectErr, fmt.Errorf("reconnect original container to network %s failed: %w", attachment.name, err))
}
}
return reconnectErr
}
func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, oldContainerID, originalName string, wasRunning bool, newContainer string, disconnectedNetworks []containerNetworkAttachment, logger containerSwitchLogFunc) error {
var rollbackErr error
backupName := containerSwitchBackupName(oldContainerID)
if newContainer != "" {
removeErr := cli.ContainerRemove(ctx, newContainer, container.RemoveOptions{Force: true, RemoveVolumes: true})
if client.IsErrNotFound(removeErr) {
removeErr = nil
}
logContainerSwitchStep(logger, "ContainerRollbackRemoveReplacement", originalName, removeErr)
if removeErr != nil {
rollbackErr = errors.Join(rollbackErr, fmt.Errorf("remove failed replacement container failed: %w", removeErr))
}
}
renameErr := cli.ContainerRename(ctx, oldContainerID, originalName)
logContainerSwitchStep(logger, "ContainerRollbackRenameOld", backupName, renameErr)
if renameErr != nil {
rollbackErr = errors.Join(rollbackErr, fmt.Errorf("restore original container name failed: %w", renameErr))
}
currentName := originalName
if renameErr != nil {
currentName = backupName
}
reconnectErr := reconnectOriginalContainerNetworks(ctx, cli, oldContainerID, disconnectedNetworks)
logContainerSwitchStep(logger, "ContainerRollbackReconnectOld", currentName, reconnectErr)
rollbackErr = errors.Join(rollbackErr, reconnectErr)
if wasRunning {
restartErr := restartOriginalContainer(ctx, cli, oldContainerID)
logContainerSwitchStep(logger, "ContainerRollbackRestartOld", currentName, restartErr)
rollbackErr = errors.Join(rollbackErr, restartErr)
}
return rollbackErr
}
func createContainerWithOldNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkSettings *container.NetworkSettings, name string) (container.CreateResponse, error) {
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(networkSettings, hostConf)
normalizeContainerEndpointSettings(ctx, client, networkConf, extraNetworks)
var primaryEndpoints map[string]*network.EndpointSettings
if networkConf != nil {
primaryEndpoints = networkConf.EndpointsConfig
}
created, err := createContainerWithDynamicIPFallback(func() (container.CreateResponse, error) {
return client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name)
}, primaryEndpoints, networkSettings)
if err != nil {
return created, err
}
extraNames := make([]string, 0, len(extraNetworks))
for item := range extraNetworks {
extraNames = append(extraNames, item)
}
sort.Strings(extraNames)
for _, item := range extraNames {
err := client.NetworkConnect(ctx, item, created.ID, extraNetworks[item])
if clearUnsupportedDynamicEndpointIPAM(err, map[string]*network.EndpointSettings{item: extraNetworks[item]}, networkSettings) {
err = client.NetworkConnect(ctx, item, created.ID, extraNetworks[item])
}
if err != nil {
_ = client.ContainerRemove(ctx, created.ID, container.RemoveOptions{Force: true})
return created, err
}
}
return created, nil
}
func createContainerWithDynamicIPFallback(
create func() (container.CreateResponse, error),
endpoints map[string]*network.EndpointSettings,
networkSettings *container.NetworkSettings,
) (container.CreateResponse, error) {
for {
created, err := create()
if err == nil || created.ID != "" || !clearUnsupportedDynamicEndpointIPAM(err, endpoints, networkSettings) {
return created, err
}
}
}
+1 -1
View File
@@ -644,7 +644,7 @@ func (u *CronjobService) HandleStop(id uint) error {
if len(record.TaskID) == 0 { if len(record.TaskID) == 0 {
return nil return nil
} }
if cancel, ok := global.TaskCtxMap[record.TaskID]; ok { if cancel, ok := global.LoadTaskCancel(record.TaskID); ok {
cancel() cancel()
} }
return nil return nil
+5 -5
View File
@@ -60,7 +60,7 @@ func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time, t
src := path.Join(backupDir, record.FileName) src := path.Join(backupDir, record.FileName)
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/") dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil { if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil {
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr { if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++ retry++
return err return err
@@ -119,7 +119,7 @@ func (u *CronjobService) handleWebsite(cronjob model.Cronjob, startTime time.Tim
src := path.Join(backupDir, record.FileName) src := path.Join(backupDir, record.FileName)
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/") dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil { if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil {
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr { if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++ retry++
return err return err
@@ -216,7 +216,7 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
src := path.Join(backupDir, record.FileName) src := path.Join(backupDir, record.FileName)
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/") dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil { if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil {
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr { if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++ retry++
return err return err
@@ -274,7 +274,7 @@ func (u *CronjobService) handleDirectory(cronjob model.Cronjob, startTime time.T
src := path.Join(backupDir, fileName) src := path.Join(backupDir, fileName)
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/") dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil { if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil {
return err return err
} }
record.FileDir = path.Dir(dst) record.FileDir = path.Dir(dst)
@@ -310,7 +310,7 @@ func (u *CronjobService) handleSystemLog(cronjob model.Cronjob, startTime time.T
src := path.Join(path.Dir(backupDir), fileName) src := path.Join(path.Dir(backupDir), fileName)
dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/") dst := strings.TrimPrefix(src, global.Dir.LocalBackupDir+"/tmp/")
if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes); err != nil { if err := uploadWithMap(*task, accountMap, src, dst, cronjob.SourceAccountIDs, cronjob.DownloadAccountID, cronjob.RetryTimes, true); err != nil {
return err return err
} }
record.FileDir = path.Dir(dst) record.FileDir = path.Dir(dst)
+16 -7
View File
@@ -187,6 +187,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
hostInfo, _ := psutil.HOST.GetHostInfo(false) hostInfo, _ := psutil.HOST.GetHostInfo(false)
currentInfo.Uptime = hostInfo.Uptime currentInfo.Uptime = hostInfo.Uptime
currentInfo.TimeSinceUptime = time.Unix(int64(hostInfo.BootTime), 0).Format(constant.DateTimeLayout) currentInfo.TimeSinceUptime = time.Unix(int64(hostInfo.BootTime), 0).Format(constant.DateTimeLayout)
currentInfo.RunningTime = loadRunningTime(hostInfo.Uptime)
currentInfo.Procs = hostInfo.Procs currentInfo.Procs = hostInfo.Procs
currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false) currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false)
@@ -228,13 +229,12 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
if ioOption == "all" { if ioOption == "all" {
diskInfo, _ := disk.IOCounters() diskInfo, _ := disk.IOCounters()
for _, state := range diskInfo { state := sumDiskIOCounters(diskInfo)
currentInfo.IOReadBytes += state.ReadBytes currentInfo.IOReadBytes = state.ReadBytes
currentInfo.IOWriteBytes += state.WriteBytes currentInfo.IOWriteBytes = state.WriteBytes
currentInfo.IOCount += (state.ReadCount + state.WriteCount) currentInfo.IOCount = state.ReadCount + state.WriteCount
currentInfo.IOReadTime += state.ReadTime currentInfo.IOReadTime = state.ReadTime
currentInfo.IOWriteTime += state.WriteTime currentInfo.IOWriteTime = state.WriteTime
}
} else { } else {
diskInfo, _ := disk.IOCounters(ioOption) diskInfo, _ := disk.IOCounters(ioOption)
for _, state := range diskInfo { for _, state := range diskInfo {
@@ -267,6 +267,15 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
return &currentInfo return &currentInfo
} }
func loadRunningTime(uptime uint64) dto.RunningTime {
return dto.RunningTime{
Days: uptime / 86400,
Hours: (uptime % 86400) / 3600,
Minutes: (uptime % 3600) / 60,
Seconds: uptime % 60,
}
}
func (u *DashboardService) LoadTopCPU() []dto.Process { func (u *DashboardService) LoadTopCPU() []dto.Process {
return loadTopCPU() return loadTopCPU()
} }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,84 @@
package service
import (
"fmt"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
)
func composePortEnvKeys(index int) (containerPort, hostPort, hostIP, protocol string) {
return fmt.Sprintf("CONTAINER_PORT_%d", index),
fmt.Sprintf("HOST_PORT_%d", index),
fmt.Sprintf("HOST_IP_%d", index),
fmt.Sprintf("PORT_PROTOCOL_%d", index)
}
func isComposePortEnvKey(key string) bool {
return strings.HasPrefix(key, "CONTAINER_PORT_") ||
strings.HasPrefix(key, "HOST_PORT_") ||
strings.HasPrefix(key, "HOST_IP_") ||
strings.HasPrefix(key, "PORT_PROTOCOL_")
}
func formatComposePortMapping(hostIP, hostPort, containerPort, protocol string) string {
return fmt.Sprintf("${%s}:${%s}:${%s}/%s", hostIP, hostPort, containerPort, normalizeComposeProtocol(protocol))
}
func normalizeComposeProtocol(protocol string) string {
switch strings.ToLower(strings.TrimSpace(protocol)) {
case "udp":
return "udp"
default:
return "tcp"
}
}
func formatComposeVolume(source, target, mode string) string {
return fmt.Sprintf("%s:%s:%s", source, target, normalizeComposeVolumeMode(mode))
}
func normalizeComposeVolumeMode(mode string) string {
switch strings.ToLower(strings.TrimSpace(mode)) {
case "ro":
return "ro"
default:
return "rw"
}
}
func loadComposeExposedPortsFromEnv(envs map[string]string, defaultHostIP string, strict bool) ([]request.ExposedPort, error) {
var ports []request.ExposedPort
for key, value := range envs {
if !strings.HasPrefix(key, "CONTAINER_PORT_") {
continue
}
index := strings.TrimPrefix(key, "CONTAINER_PORT_")
containerPort, err := strconv.Atoi(value)
if err != nil {
if strict {
return nil, err
}
continue
}
hostPort, err := strconv.Atoi(envs["HOST_PORT_"+index])
if err != nil {
if strict {
return nil, err
}
continue
}
hostIP := envs["HOST_IP_"+index]
if hostIP == "" {
hostIP = defaultHostIP
}
ports = append(ports, request.ExposedPort{
ContainerPort: containerPort,
HostPort: hostPort,
HostIP: hostIP,
Protocol: normalizeComposeProtocol(envs["PORT_PROTOCOL_"+index]),
})
}
return ports, nil
}
+6 -4
View File
@@ -19,10 +19,12 @@ var (
agentAccountRepo = repo.NewIAgentAccountRepo() agentAccountRepo = repo.NewIAgentAccountRepo()
agentAccountModelRepo = repo.NewIAgentAccountModelRepo() agentAccountModelRepo = repo.NewIAgentAccountModelRepo()
mysqlRepo = repo.NewIMysqlRepo() mysqlRepo = repo.NewIMysqlRepo()
postgresqlRepo = repo.NewIPostgresqlRepo() postgresqlRepo = repo.NewIPostgresqlRepo()
mongodbRepo = repo.NewIMongodbRepo() mongodbRepo = repo.NewIMongodbRepo()
databaseRepo = repo.NewIDatabaseRepo() databaseRepo = repo.NewIDatabaseRepo()
databaseUserRepo = repo.NewIDatabaseUserRepo()
databaseUserGrantRepo = repo.NewIDatabaseUserGrantRepo()
imageRepoRepo = repo.NewIImageRepoRepo() imageRepoRepo = repo.NewIImageRepoRepo()
composeRepo = repo.NewIComposeTemplateRepo() composeRepo = repo.NewIComposeTemplateRepo()
+15 -6
View File
@@ -485,7 +485,7 @@ func preflightDecompressTool(decompressType files.CompressType) error {
} }
func (f *FileService) StopCompress(taskID string) error { func (f *FileService) StopCompress(taskID string) error {
if cancel, ok := global.TaskCtxMap[taskID]; ok { if cancel, ok := global.LoadTaskCancel(taskID); ok {
cancel() cancel()
return nil return nil
} }
@@ -493,7 +493,7 @@ func (f *FileService) StopCompress(taskID string) error {
} }
func (f *FileService) StopDeCompress(taskID string) error { func (f *FileService) StopDeCompress(taskID string) error {
if cancel, ok := global.TaskCtxMap[taskID]; ok { if cancel, ok := global.LoadTaskCancel(taskID); ok {
cancel() cancel()
return nil return nil
} }
@@ -621,9 +621,16 @@ func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr e
return os.Chtimes(dstPath, info.ModTime(), info.ModTime()) return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
} }
if err := os.RemoveAll(dstPath); err != nil { dstInfo, err := os.Lstat(dstPath)
keepExistingFile := err == nil && dstInfo.Mode().IsRegular()
if err != nil && !os.IsNotExist(err) {
return err return err
} }
if !keepExistingFile {
if err := os.RemoveAll(dstPath); err != nil {
return err
}
}
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil { if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
return err return err
} }
@@ -647,8 +654,10 @@ func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr e
if _, err := io.Copy(dstFile, srcFile); err != nil { if _, err := io.Copy(dstFile, srcFile); err != nil {
return err return err
} }
if err := applyDecompressOwnership(srcPath, dstPath); err != nil { if !keepExistingFile {
return err if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
return err
}
} }
return os.Chtimes(dstPath, info.ModTime(), info.ModTime()) return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
} }
@@ -1116,7 +1125,7 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
} }
logFilePath = path.Join(global.Dir.LogDir, "ai", safeName) logFilePath = path.Join(global.Dir.LogDir, "ai", safeName)
default: default:
return nil, buserr.New("ErrNotSupportType") return nil, buserr.WithName("ErrNotSupportType", req.Type)
} }
file, err := os.Open(logFilePath) file, err := os.Open(logFilePath)
+33 -1
View File
@@ -166,6 +166,12 @@ func (u *FirewallService) OperateFirewall(req dto.FirewallOperation) error {
if err != nil { if err != nil {
return err return err
} }
fail2BanState := newFirewallFail2BanState()
if client.Name() == "firewalld" && req.Operation == "stop" {
if err := fail2BanState.rememberBeforeFirewallStop(); err != nil {
return err
}
}
needRestartDocker := false needRestartDocker := false
switch req.Operation { switch req.Operation {
case "start": case "start":
@@ -210,6 +216,11 @@ func (u *FirewallService) OperateFirewall(req dto.FirewallOperation) error {
return fmt.Errorf("failed to restart Docker: %v", err) return fmt.Errorf("failed to restart Docker: %v", err)
} }
} }
if client.Name() == "firewalld" && req.Operation == "start" {
if err := fail2BanState.restoreAfterFirewallStart(); err != nil {
return err
}
}
return nil return nil
} }
@@ -222,7 +233,7 @@ func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool)
req.Chain = iptables.Chain1PanelBasic req.Chain = iptables.Chain1PanelBasic
} }
protos := strings.Split(req.Protocol, "/") protos := strings.Split(req.Protocol, "/")
itemAddress := strings.Split(strings.TrimSuffix(req.Address, ","), ",") itemAddress := splitFirewallRuleAddresses(req.Address)
if client.Name() == "ufw" { if client.Name() == "ufw" {
if strings.Contains(req.Port, ",") || strings.Contains(req.Port, "-") { if strings.Contains(req.Port, ",") || strings.Contains(req.Port, "-") {
@@ -515,6 +526,7 @@ func (u *FirewallService) operatePort(client firewall.FirewallClient, req dto.Po
if err := copier.Copy(&fireInfo, &req); err != nil { if err := copier.Copy(&fireInfo, &req); err != nil {
return err return err
} }
fireInfo.Address = normalizeFirewallRuleAddress(fireInfo.Address)
if client.Name() == "ufw" { if client.Name() == "ufw" {
if len(fireInfo.Address) != 0 && !strings.EqualFold(fireInfo.Address, "Anywhere") { if len(fireInfo.Address) != 0 && !strings.EqualFold(fireInfo.Address, "Anywhere") {
@@ -529,6 +541,26 @@ func (u *FirewallService) operatePort(client firewall.FirewallClient, req dto.Po
return client.Port(fireInfo, req.Operation) return client.Port(fireInfo, req.Operation)
} }
func splitFirewallRuleAddresses(address string) []string {
parts := strings.Split(strings.TrimSuffix(address, ","), ",")
addresses := make([]string, 0, len(parts))
for _, part := range parts {
addresses = append(addresses, normalizeFirewallRuleAddress(part))
}
if len(addresses) == 0 {
return []string{""}
}
return addresses
}
func normalizeFirewallRuleAddress(address string) string {
address = strings.TrimSpace(address)
if strings.EqualFold(address, "Anywhere") {
return ""
}
return address
}
type portOfApp struct { type portOfApp struct {
AppName string AppName string
HttpPort string HttpPort string
@@ -0,0 +1,78 @@
package service
import (
"fmt"
"os"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/controller"
)
const fail2BanRestoreWithFirewallMarker = "/run/1panel_fail2ban_restore_with_firewall"
type firewallFail2BanState struct {
markerPath string
isExist func(string) bool
isActive func(string) bool
start func(string) error
}
func newFirewallFail2BanState() *firewallFail2BanState {
return &firewallFail2BanState{
markerPath: fail2BanRestoreWithFirewallMarker,
isExist: func(serviceName string) bool {
exists, err := controller.CheckExist(serviceName)
if err != nil {
global.LOG.Warnf("check %s installation before stopping the firewall failed: %v", serviceName, err)
}
return exists
},
isActive: func(serviceName string) bool {
active, err := controller.CheckActive(serviceName)
if err != nil {
global.LOG.Warnf("check %s status before stopping the firewall failed: %v", serviceName, err)
}
return active
},
start: controller.HandleStart,
}
}
func (s *firewallFail2BanState) rememberBeforeFirewallStop() error {
if !s.isExist("fail2ban.service") {
return nil
}
if !s.isActive("fail2ban.service") {
return nil
}
return s.markForRestore()
}
func (s *firewallFail2BanState) markForRestore() error {
if err := os.WriteFile(s.markerPath, nil, 0600); err != nil {
return fmt.Errorf("mark Fail2Ban for restoration with the firewall: %w", err)
}
return nil
}
func (s *firewallFail2BanState) restoreAfterFirewallStart() error {
_, err := os.Stat(s.markerPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("load Fail2Ban restore marker after starting the firewall: %w", err)
}
if err := s.start("fail2ban.service"); err != nil {
return fmt.Errorf("restore Fail2Ban after starting the firewall: %w", err)
}
return s.clearRestoreMarker()
}
func (s *firewallFail2BanState) clearRestoreMarker() error {
if err := os.Remove(s.markerPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("clear Fail2Ban firewall restore status: %w", err)
}
return nil
}
+1 -1
View File
@@ -502,7 +502,7 @@ func (u *ImageService) ImageRemove(req dto.BatchDelete) error {
return err return err
} }
defer client.Close() defer client.Close()
taskItem, err := task.NewTaskWithOps(task.TaskScopeImage, task.TaskDelete, task.TaskScopeContainer, req.TaskID, 1) taskItem, err := task.NewTaskWithOps(strings.Join(req.Names, ","), task.TaskDelete, task.TaskScopeImage, req.TaskID, 1)
if err != nil { if err != nil {
global.LOG.Errorf("new task for create container failed, err: %v", err) global.LOG.Errorf("new task for create container failed, err: %v", err)
return err return err
+465
View File
@@ -1,18 +1,483 @@
package service package service
import ( import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os" "os"
"os/exec"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/re"
) )
type LogService struct{} type LogService struct{}
const maxSystemLogCursorSkip = 100
const maxFileSystemLogLines = 1000
var (
syslogPriorityKeywords = []struct {
keyword string
priority string
}{
{"emerg", "0"}, {"panic", "0"}, {"alert", "1"}, {"crit", "2"}, {"fatal", "2"},
{"error", "3"}, {"err", "3"}, {"warn", "4"}, {"notice", "5"}, {"debug", "7"}, {"info", "6"},
}
)
type systemLogCursor struct {
EndTime int64 `json:"endTime"`
Skipped int `json:"skipped"`
JournalCursor string `json:"journalCursor,omitempty"`
}
type ILogService interface { type ILogService interface {
ListSystemLogFile() ([]string, error) ListSystemLogFile() ([]string, error)
GetSystemLogStatus() (dto.SystemLogStatus, error)
ReadSystemLog(req dto.SystemLogReq) (dto.SystemLogRes, error)
ListRunningServices() ([]string, error)
}
func (u *LogService) ReadSystemLog(req dto.SystemLogReq) (dto.SystemLogRes, error) {
pageSize := req.PageSize
if pageSize == 0 {
pageSize = 100
}
now := time.Now()
startTime := req.StartTime
if startTime.IsZero() {
startTime = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
}
endTime := req.EndTime
if endTime.IsZero() {
endTime = now
}
cursor, err := decodeSystemLogCursor(req.Cursor)
if err != nil {
return dto.SystemLogRes{}, err
}
if cursor != nil && cursor.JournalCursor == "" {
endTime = time.Unix(0, cursor.EndTime*int64(time.Microsecond))
}
journalctl, err := exec.LookPath("journalctl")
if err != nil {
return u.readFileSystemLog(req, startTime, endTime, pageSize, cursor)
}
queryArgs := buildJournalQueryArgs(req, startTime, endTime, pageSize, cursor)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
output, err := exec.CommandContext(ctx, journalctl, queryArgs...).CombinedOutput()
cancel()
if err != nil {
if journalctlGrepUnsupported(req.Keyword, string(output)) {
return dto.SystemLogRes{}, buserr.New("ErrSystemLogKeywordFilterUnsupported")
}
return dto.SystemLogRes{}, fmt.Errorf("read host system logs failed: %s", strings.TrimSpace(string(output)))
}
content := strings.TrimSpace(string(output))
if content == "" || strings.HasPrefix(content, "-- No entries --") {
return dto.SystemLogRes{Source: "journalctl", Items: []dto.SystemLogItem{}}, nil
}
items := parseJournalLogItems(content)
if cursor != nil && cursor.JournalCursor == "" {
items, err = skipSystemLogCursorItems(items, *cursor)
if err != nil {
return dto.SystemLogRes{}, err
}
}
return buildSystemLogResponse("journalctl", items, pageSize, cursor)
}
func (u *LogService) GetSystemLogStatus() (dto.SystemLogStatus, error) {
journalctl, err := exec.LookPath("journalctl")
if err != nil {
return dto.SystemLogStatus{
Source: "file",
KeywordFilterSupported: true,
}, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
versionOutput, err := exec.CommandContext(ctx, journalctl, "--version").CombinedOutput()
cancel()
if err != nil {
return dto.SystemLogStatus{}, fmt.Errorf("read journalctl version failed: %s", strings.TrimSpace(string(versionOutput)))
}
ctx, cancel = context.WithTimeout(context.Background(), 3*time.Second)
helpOutput, err := exec.CommandContext(ctx, journalctl, "--help").CombinedOutput()
cancel()
if err != nil {
return dto.SystemLogStatus{}, fmt.Errorf("read journalctl capabilities failed: %s", strings.TrimSpace(string(helpOutput)))
}
supported := journalctlHelpSupportsGrep(string(helpOutput))
status := dto.SystemLogStatus{
Source: "journalctl",
Version: firstOutputLine(string(versionOutput)),
KeywordFilterSupported: supported,
}
if !supported {
status.Message = i18n.Get("ErrSystemLogKeywordFilterUnsupported")
}
return status, nil
}
func journalctlHelpSupportsGrep(help string) bool {
return strings.Contains(help, "--grep=")
}
func journalctlGrepUnsupported(keyword, output string) bool {
if strings.TrimSpace(keyword) == "" {
return false
}
output = strings.ToLower(output)
return strings.Contains(output, "grep") &&
(strings.Contains(output, "unrecognized option") || strings.Contains(output, "unknown option"))
}
func firstOutputLine(output string) string {
for _, line := range strings.Split(output, "\n") {
if line = strings.TrimSpace(line); line != "" {
return line
}
}
return ""
}
func buildJournalQueryArgs(req dto.SystemLogReq, startTime, endTime time.Time, pageSize int, cursor *systemLogCursor) []string {
args := []string{
"--no-pager", "--reverse", "--output=json",
"--since", formatJournalQueryTime(startTime),
}
if cursor != nil && cursor.JournalCursor != "" {
args = append(args, "--after-cursor="+cursor.JournalCursor)
} else {
args = append(args, "--until", formatJournalQueryTime(endTime))
}
if service := strings.TrimSpace(req.Service); service != "" {
args = append(args, "-u", service)
}
if priority := strings.TrimSpace(req.Priority); priority != "" {
args = append(args, "--priority", priority+".."+priority)
}
if keyword := strings.TrimSpace(req.Keyword); keyword != "" {
args = append(args, "--grep", keyword)
}
queryLines := pageSize + 1
if cursor != nil && cursor.JournalCursor == "" {
queryLines += cursor.Skipped
}
return append(args, "--lines="+strconv.Itoa(queryLines))
}
func (u *LogService) readFileSystemLog(req dto.SystemLogReq, startTime, endTime time.Time, pageSize int, cursor *systemLogCursor) (dto.SystemLogRes, error) {
items := make([]dto.SystemLogItem, 0)
for _, logFile := range []string{"/var/log/syslog", "/var/log/messages", "/var/log/system.log"} {
if _, err := os.Stat(logFile); err != nil {
continue
}
content, err := readLastSystemLogLines(logFile, maxFileSystemLogLines)
if err != nil {
return dto.SystemLogRes{}, err
}
for _, line := range strings.Split(content, "\n") {
item, ok := parseFileSystemLogItem(line)
if !ok || !matchSystemLogItem(item, req, startTime, endTime) {
continue
}
items = append(items, item)
}
}
sort.SliceStable(items, func(i, j int) bool { return items[i].Timestamp > items[j].Timestamp })
if cursor != nil {
var err error
items, err = skipSystemLogCursorItems(items, *cursor)
if err != nil {
return dto.SystemLogRes{}, err
}
}
return buildSystemLogResponse("file", items, pageSize, cursor)
}
func readLastSystemLogLines(logFile string, lines int) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
output, err := exec.CommandContext(ctx, "tail", "-n", strconv.Itoa(lines), logFile).Output()
if err != nil {
return "", err
}
return string(output), nil
}
func buildSystemLogResponse(source string, items []dto.SystemLogItem, pageSize int, cursor *systemLogCursor) (dto.SystemLogRes, error) {
hasMore := len(items) > pageSize
if hasMore {
items = items[:pageSize]
}
res := dto.SystemLogRes{Source: source, Items: items, HasMore: hasMore}
if !hasMore || len(items) == 0 {
return res, nil
}
lastItem := items[len(items)-1]
next := systemLogCursor{EndTime: lastItem.Timestamp}
if lastItem.Cursor != "" {
next.JournalCursor = lastItem.Cursor
} else {
next.Skipped = countSystemLogTimestamp(items, lastItem.Timestamp)
if cursor != nil && cursor.EndTime == lastItem.Timestamp {
next.Skipped += cursor.Skipped
}
}
nextCursor, err := encodeSystemLogCursor(next)
if err != nil {
return dto.SystemLogRes{}, err
}
res.NextCursor = nextCursor
return res, nil
}
func decodeSystemLogCursor(value string) (*systemLogCursor, error) {
value = strings.TrimSpace(value)
if value == "" {
return nil, nil
}
decoded, err := base64.RawURLEncoding.DecodeString(value)
if err != nil {
return nil, fmt.Errorf("invalid system log cursor")
}
var cursor systemLogCursor
if err := json.Unmarshal(decoded, &cursor); err != nil || cursor.EndTime <= 0 || cursor.Skipped < 0 || cursor.Skipped > maxSystemLogCursorSkip {
return nil, fmt.Errorf("invalid system log cursor")
}
return &cursor, nil
}
func encodeSystemLogCursor(cursor systemLogCursor) (string, error) {
value, err := json.Marshal(cursor)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(value), nil
}
func skipSystemLogCursorItems(items []dto.SystemLogItem, cursor systemLogCursor) ([]dto.SystemLogItem, error) {
skipped := 0
for len(items) > 0 && skipped < cursor.Skipped && items[0].Timestamp == cursor.EndTime {
items = items[1:]
skipped++
}
if skipped != cursor.Skipped {
return nil, fmt.Errorf("system log cursor has expired")
}
return items, nil
}
func countSystemLogTimestamp(items []dto.SystemLogItem, timestamp int64) int {
count := 0
for _, item := range items {
if item.Timestamp == timestamp {
count++
}
}
return count
}
func formatJournalQueryTime(value time.Time) string {
// Fractional seconds are rejected by some journalctl versions.
return value.In(time.Local).Format("2006-01-02 15:04:05")
}
func parseFileSystemLogItem(line string) (dto.SystemLogItem, bool) {
raw := strings.TrimSpace(line)
if raw == "" {
return dto.SystemLogItem{}, false
}
priority := "6"
if matches := re.GetRegex(re.SyslogPRIPattern).FindStringSubmatch(raw); len(matches) == 2 {
value, _ := strconv.Atoi(matches[1])
priority = strconv.Itoa(value % 8)
raw = strings.TrimSpace(strings.TrimPrefix(raw, matches[0]))
}
logTime, rest, ok := parseFileSystemLogTime(raw)
if !ok {
return dto.SystemLogItem{}, false
}
service, message := parseFileSystemLogService(rest)
if priority == "6" {
priority = detectFileSystemLogPriority(message)
}
return dto.SystemLogItem{
Timestamp: logTime.UnixMicro(),
Time: logTime.Format("2006-01-02 15:04:05"),
Priority: priority,
Service: service,
Message: message,
Raw: line,
}, true
}
func parseFileSystemLogTime(value string) (time.Time, string, bool) {
if matches := re.GetRegex(re.SyslogRFC3339Pattern).FindStringSubmatch(value); len(matches) == 3 {
for _, layout := range []string{time.RFC3339Nano, "2006-01-02 15:04:05.999999999Z07:00"} {
if parsed, err := time.Parse(layout, matches[1]); err == nil {
return parsed.Local(), matches[2], true
}
}
for _, layout := range []string{"2006-01-02T15:04:05.999999999", "2006-01-02 15:04:05.999999999"} {
if parsed, err := time.ParseInLocation(layout, matches[1], time.Local); err == nil {
return parsed, matches[2], true
}
}
}
if matches := re.GetRegex(re.SyslogRFC3164Pattern).FindStringSubmatch(value); len(matches) == 3 {
parsed, err := time.ParseInLocation("Jan _2 15:04:05", matches[1], time.Local)
if err != nil {
return time.Time{}, "", false
}
now := time.Now()
parsed = time.Date(now.Year(), parsed.Month(), parsed.Day(), parsed.Hour(), parsed.Minute(), parsed.Second(), 0, time.Local)
if parsed.After(now.Add(24 * time.Hour)) {
parsed = parsed.AddDate(-1, 0, 0)
}
return parsed, matches[2], true
}
return time.Time{}, "", false
}
func parseFileSystemLogService(value string) (string, string) {
if matches := re.GetRegex(re.SyslogServicePattern).FindStringSubmatch(value); len(matches) == 3 {
return matches[1], matches[2]
}
// RFC5424 records have the form HOST APP-NAME PROCID MSGID STRUCTURED-DATA MSG.
fields := strings.Fields(value)
if len(fields) >= 6 {
return fields[1], strings.Join(fields[5:], " ")
}
return "", value
}
func detectFileSystemLogPriority(message string) string {
lowerMessage := strings.ToLower(message)
for _, item := range syslogPriorityKeywords {
if strings.Contains(lowerMessage, item.keyword) {
return item.priority
}
}
return "6"
}
func matchSystemLogItem(item dto.SystemLogItem, req dto.SystemLogReq, startTime, endTime time.Time) bool {
itemTime := time.UnixMicro(item.Timestamp)
if itemTime.Before(startTime) || itemTime.After(endTime) {
return false
}
if priority := strings.TrimSpace(req.Priority); priority != "" && item.Priority != priority {
return false
}
if service := strings.TrimSpace(req.Service); service != "" && !matchFileSystemLogService(item.Service, service) {
return false
}
if keyword := strings.TrimSpace(req.Keyword); keyword != "" && !strings.Contains(strings.ToLower(item.Raw), strings.ToLower(keyword)) {
return false
}
return true
}
func matchFileSystemLogService(itemService, requestedService string) bool {
itemService = strings.TrimSpace(itemService)
requestedService = strings.TrimSpace(requestedService)
if itemService == "" || requestedService == "" {
return itemService == requestedService
}
return strings.EqualFold(strings.TrimSuffix(itemService, ".service"), strings.TrimSuffix(requestedService, ".service"))
}
func (u *LogService) ListRunningServices() ([]string, error) {
systemctl, err := exec.LookPath("systemctl")
if err != nil {
return []string{}, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
args := []string{"list-units", "--type=service", "--state=running", "--no-legend", "--no-pager", "--plain"}
output, err := exec.CommandContext(ctx, systemctl, args...).Output()
if err != nil {
return []string{}, nil
}
services := make([]string, 0)
for _, line := range strings.Split(string(output), "\n") {
fields := strings.Fields(line)
if len(fields) == 0 || !strings.HasSuffix(fields[0], ".service") {
continue
}
services = append(services, fields[0])
}
sort.Strings(services)
return services, nil
}
func parseJournalLogItems(content string) []dto.SystemLogItem {
entries := strings.Split(content, "\n")
items := make([]dto.SystemLogItem, 0, len(entries))
for _, entry := range entries {
var fields map[string]interface{}
if err := json.Unmarshal([]byte(entry), &fields); err != nil {
continue
}
items = append(items, dto.SystemLogItem{
Timestamp: journalFieldMicroseconds(fields),
Cursor: journalFieldString(fields, "__CURSOR"),
Time: formatJournalTimestamp(journalFieldString(fields, "__REALTIME_TIMESTAMP")),
Priority: journalFieldString(fields, "PRIORITY"),
Service: journalFieldString(fields, "_SYSTEMD_UNIT"),
Message: journalFieldString(fields, "MESSAGE"),
Raw: entry,
})
}
return items
}
func journalFieldMicroseconds(fields map[string]interface{}) int64 {
value, _ := strconv.ParseInt(journalFieldString(fields, "__REALTIME_TIMESTAMP"), 10, 64)
return value
}
func journalFieldString(fields map[string]interface{}, key string) string {
value, ok := fields[key]
if !ok || value == nil {
return ""
}
switch item := value.(type) {
case string:
return item
case float64:
return strconv.FormatInt(int64(item), 10)
case []interface{}:
values := make([]string, 0, len(item))
for _, value := range item {
values = append(values, fmt.Sprint(value))
}
return strings.Join(values, " ")
default:
return fmt.Sprint(item)
}
}
func formatJournalTimestamp(value string) string {
microseconds, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return value
}
return time.Unix(0, microseconds*int64(time.Microsecond)).Local().Format("2006-01-02 15:04:05")
} }
func NewILogService() ILogService { func NewILogService() ILogService {
+427 -66
View File
@@ -1,18 +1,24 @@
package service package service
import ( import (
"bytes"
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http"
"path" "path"
"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/cmd/server/ai" "github.com/1Panel-dev/1Panel/agent/cmd/server/ai"
"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf" "github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
@@ -25,18 +31,31 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/nginx" "github.com/1Panel-dev/1Panel/agent/utils/nginx"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components" "github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser" "github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
"github.com/docker/docker/api/types/container"
"github.com/mattn/go-shellwords"
"github.com/subosito/gotenv" "github.com/subosito/gotenv"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
type McpServerService struct{} type McpServerService struct{}
const (
defaultMcpGatewayImageNpx = "supercorp/supergateway:3.4.3"
defaultMcpGatewayImageUvx = "supercorp/supergateway:uvx"
defaultMcpProtocolVersion = "2025-06-18"
mcpOutputTransportSSE = "sse"
mcpOutputTransportStreamableHTTP = "streamableHttp"
)
type IMcpServerService interface { type IMcpServerService interface {
Page(req request.McpServerSearch) response.McpServersRes Page(req request.McpServerSearch) response.McpServersRes
Detail(req request.McpServerDetail) (response.McpServerDTO, error)
Create(create request.McpServerCreate) error Create(create request.McpServerCreate) error
Update(req request.McpServerUpdate) error Update(req request.McpServerUpdate) error
Delete(id uint) error Delete(id uint) error
Operate(req request.McpServerOperate) error Operate(req request.McpServerOperate) error
SyncStatus(req request.McpServerStatusSync) ([]response.McpServerStatusDTO, error)
TestConnection(req request.McpServerConnectionTest) (response.McpServerConnectionTestRes, error)
GetBindDomain() (response.McpBindDomainRes, error) GetBindDomain() (response.McpBindDomainRes, error)
BindDomain(req request.McpBindDomain) error BindDomain(req request.McpBindDomain) error
UpdateBindDomain(req request.McpBindDomainUpdate) error UpdateBindDomain(req request.McpBindDomainUpdate) error
@@ -54,44 +73,36 @@ func (m McpServerService) Page(req request.McpServerSearch) response.McpServersR
total, data, _ := mcpServerRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize) total, data, _ := mcpServerRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize)
for _, item := range data { for _, item := range data {
_ = syncMcpServerContainerStatus(&item) normalizeMcpServerGateway(&item)
serverDTO := response.McpServerDTO{ items = append(items, response.McpServerDTO{
McpServer: item, McpServer: item,
Environments: make([]request.Environment, 0), Environments: make([]request.Environment, 0),
Volumes: make([]request.Volume, 0), Volumes: make([]request.Volume, 0),
} })
project, err := docker.GetComposeProject(item.Name, path.Join(global.Dir.McpDir, item.Name), []byte(item.DockerCompose), []byte(item.Env), true)
if err != nil {
global.LOG.Errorf("get mcp compose project error: %s", err.Error())
continue
}
for _, service := range project.Services {
if service.Environment != nil {
for key, value := range service.Environment {
serverDTO.Environments = append(serverDTO.Environments, request.Environment{
Key: key,
Value: *value,
})
}
}
if service.Volumes != nil {
for _, volume := range service.Volumes {
serverDTO.Volumes = append(serverDTO.Volumes, request.Volume{
Source: volume.Source,
Target: volume.Target,
})
}
}
}
items = append(items, serverDTO)
} }
res.Total = total res.Total = total
res.Items = items res.Items = items
return res return res
} }
func (m McpServerService) Detail(req request.McpServerDetail) (response.McpServerDTO, error) {
mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return response.McpServerDTO{}, err
}
normalizeMcpServerGateway(mcpServer)
serverDTO := response.McpServerDTO{
McpServer: *mcpServer,
Environments: make([]request.Environment, 0),
Volumes: make([]request.Volume, 0),
}
if err := loadMcpServerComposeConfig(&serverDTO); err != nil {
return response.McpServerDTO{}, err
}
return serverDTO, nil
}
func (m McpServerService) Update(req request.McpServerUpdate) error { func (m McpServerService) Update(req request.McpServerUpdate) error {
go pullImage(req.Type)
mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID)) mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
if err != nil { if err != nil {
return err return err
@@ -115,7 +126,11 @@ func (m McpServerService) Update(req request.McpServerUpdate) error {
mcpServer.HostIP = req.HostIP mcpServer.HostIP = req.HostIP
mcpServer.OutputTransport = req.OutputTransport mcpServer.OutputTransport = req.OutputTransport
mcpServer.Type = req.Type mcpServer.Type = req.Type
if req.OutputTransport == "sse" { mcpServer.GatewayImage = req.GatewayImage
mcpServer.ProtocolVersion = req.ProtocolVersion
mcpServer.GatewayArgs = req.GatewayArgs
normalizeMcpServerGateway(mcpServer)
if req.OutputTransport == mcpOutputTransportSSE {
mcpServer.SsePath = req.SsePath mcpServer.SsePath = req.SsePath
} else { } else {
mcpServer.StreamableHttpPath = req.StreamableHttpPath mcpServer.StreamableHttpPath = req.StreamableHttpPath
@@ -137,12 +152,11 @@ func (m McpServerService) Update(req request.McpServerUpdate) error {
if err := mcpServerRepo.Save(mcpServer); err != nil { if err := mcpServerRepo.Save(mcpServer); err != nil {
return err return err
} }
go startMcp(mcpServer) go runMcpServerTask(mcpServer, req.TaskID, task.TaskUpdate)
return nil return nil
} }
func (m McpServerService) Create(create request.McpServerCreate) error { func (m McpServerService) Create(create request.McpServerCreate) error {
go pullImage(create.Type)
servers, _ := mcpServerRepo.List() servers, _ := mcpServerRepo.List()
for _, server := range servers { for _, server := range servers {
if server.Port == create.Port { if server.Port == create.Port {
@@ -177,8 +191,12 @@ func (m McpServerService) Create(create request.McpServerCreate) error {
HostIP: create.HostIP, HostIP: create.HostIP,
OutputTransport: create.OutputTransport, OutputTransport: create.OutputTransport,
Type: create.Type, Type: create.Type,
GatewayImage: create.GatewayImage,
ProtocolVersion: create.ProtocolVersion,
GatewayArgs: create.GatewayArgs,
} }
if create.OutputTransport == "sse" { normalizeMcpServerGateway(mcpServer)
if create.OutputTransport == mcpOutputTransportSSE {
mcpServer.SsePath = create.SsePath mcpServer.SsePath = create.SsePath
} else { } else {
mcpServer.StreamableHttpPath = create.StreamableHttpPath mcpServer.StreamableHttpPath = create.StreamableHttpPath
@@ -205,7 +223,7 @@ func (m McpServerService) Create(create request.McpServerCreate) error {
return err return err
} }
addProxy(mcpServer) addProxy(mcpServer)
go startMcp(mcpServer) go runMcpServerTask(mcpServer, create.TaskID, task.TaskCreate)
return nil return nil
} }
@@ -235,6 +253,9 @@ func (m McpServerService) Operate(req request.McpServerOperate) error {
if err != nil { if err != nil {
return err return err
} }
if err := refreshMcpServerFiles(mcpServer); err != nil {
return err
}
composePath := path.Join(mcpServer.Dir, "docker-compose.yml") composePath := path.Join(mcpServer.Dir, "docker-compose.yml")
var out string var out string
switch req.Operate { switch req.Operate {
@@ -255,6 +276,148 @@ func (m McpServerService) Operate(req request.McpServerOperate) error {
return mcpServerRepo.Save(mcpServer) return mcpServerRepo.Save(mcpServer)
} }
func (m McpServerService) SyncStatus(req request.McpServerStatusSync) ([]response.McpServerStatusDTO, error) {
var (
servers []model.McpServer
err error
)
if len(req.IDs) > 0 {
servers, err = mcpServerRepo.List(repo.WithByIDs(req.IDs))
} else {
servers, err = mcpServerRepo.List()
}
if err != nil {
return nil, err
}
if len(servers) == 0 {
return []response.McpServerStatusDTO{}, nil
}
containerNames := make([]string, 0, len(servers))
for _, server := range servers {
if server.ContainerName != "" {
containerNames = append(containerNames, server.ContainerName)
}
}
containerMap := make(map[string]*container.Summary)
if len(containerNames) > 0 {
cli, err := docker.NewClient()
if err != nil {
return nil, err
}
defer cli.Close()
containers, err := cli.ListContainersByName(containerNames)
if err != nil {
return nil, err
}
for i := range containers {
for _, name := range containers[i].Names {
containerMap[strings.TrimPrefix(name, "/")] = &containers[i]
}
}
}
res := make([]response.McpServerStatusDTO, 0, len(servers))
for i := range servers {
oldStatus := servers[i].Status
applyMcpContainerStatus(&servers[i], containerMap[servers[i].ContainerName])
if servers[i].Status != oldStatus {
if err := mcpServerRepo.Save(&servers[i]); err != nil {
return nil, err
}
}
res = append(res, response.McpServerStatusDTO{
ID: servers[i].ID,
Status: servers[i].Status,
Message: servers[i].Message,
})
}
return res, nil
}
func (m McpServerService) TestConnection(req request.McpServerConnectionTest) (response.McpServerConnectionTestRes, error) {
mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return response.McpServerConnectionTestRes{}, err
}
normalizeMcpServerGateway(mcpServer)
endpoint := buildMcpEndpoint(mcpServer)
res := response.McpServerConnectionTestRes{
Endpoint: endpoint,
OutputTransport: mcpServer.OutputTransport,
ProtocolVersion: mcpServer.ProtocolVersion,
}
if endpoint == "" {
res.Message = "empty endpoint"
return res, nil
}
client := &http.Client{Timeout: 10 * time.Second}
if mcpServer.OutputTransport == mcpOutputTransportSSE {
httpReq, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
res.Message = err.Error()
return res, nil
}
httpRes, err := client.Do(httpReq)
if err != nil {
res.Message = err.Error()
return res, nil
}
defer httpRes.Body.Close()
contentType := httpRes.Header.Get("Content-Type")
if httpRes.StatusCode >= 200 && httpRes.StatusCode < 300 && strings.Contains(contentType, "text/event-stream") {
res.Success = true
res.Message = "ok"
return res, nil
}
res.Message = fmt.Sprintf("unexpected response: status=%d content-type=%s", httpRes.StatusCode, contentType)
return res, nil
}
payload := map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": map[string]interface{}{
"protocolVersion": mcpServer.ProtocolVersion,
"capabilities": map[string]interface{}{},
"clientInfo": map[string]string{
"name": "1Panel",
"version": "1.0.0",
},
},
}
body, _ := json.Marshal(payload)
httpReq, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
res.Message = err.Error()
return res, nil
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json, text/event-stream")
httpRes, err := client.Do(httpReq)
if err != nil {
res.Message = err.Error()
return res, nil
}
defer httpRes.Body.Close()
res.Success, res.Message = handleMcpConnectionHTTPResponse(httpRes)
return res, nil
}
func handleMcpConnectionHTTPResponse(httpRes *http.Response) (bool, string) {
if httpRes.StatusCode >= 200 && httpRes.StatusCode < 300 {
return true, "ok"
}
respBody, _ := io.ReadAll(httpRes.Body)
message := strings.TrimSpace(string(respBody))
if message == "" {
message = httpRes.Status
}
return false, message
}
func (m McpServerService) GetBindDomain() (response.McpBindDomainRes, error) { func (m McpServerService) GetBindDomain() (response.McpBindDomainRes, error) {
var res response.McpBindDomainRes var res response.McpBindDomainRes
websiteID := GetWebsiteID() websiteID := GetWebsiteID()
@@ -534,11 +697,9 @@ func addMCPProxy(websiteID uint) error {
} }
func updateMcpServer(mcpServer *model.McpServer) error { func updateMcpServer(mcpServer *model.McpServer) error {
env := handleEnv(mcpServer) if err := refreshMcpServerFiles(mcpServer); err != nil {
if err := gotenv.Write(env, path.Join(mcpServer.Dir, ".env")); err != nil {
return err return err
} }
_ = mcpServerRepo.Save(mcpServer)
composePath := path.Join(global.Dir.McpDir, mcpServer.Name, "docker-compose.yml") composePath := path.Join(global.Dir.McpDir, mcpServer.Name, "docker-compose.yml")
_, _ = compose.Down(composePath) _, _ = compose.Down(composePath)
if _, err := compose.Up(composePath); err != nil { if _, err := compose.Up(composePath); err != nil {
@@ -548,6 +709,159 @@ func updateMcpServer(mcpServer *model.McpServer) error {
return mcpServerRepo.Save(mcpServer) return mcpServerRepo.Save(mcpServer)
} }
func refreshMcpServerFiles(mcpServer *model.McpServer) error {
normalizeMcpServerGateway(mcpServer)
if err := handleCreateParams(mcpServer, nil, nil); err != nil {
return err
}
env := handleEnv(mcpServer)
if err := gotenv.Write(env, path.Join(mcpServer.Dir, ".env")); err != nil {
return err
}
composePath := path.Join(mcpServer.Dir, "docker-compose.yml")
if err := files.NewFileOp().SaveFile(composePath, mcpServer.DockerCompose, 0644); err != nil {
return err
}
return mcpServerRepo.Save(mcpServer)
}
func normalizeMcpServerGateway(mcpServer *model.McpServer) {
if mcpServer.Type == "" {
mcpServer.Type = "npx"
}
if strings.TrimSpace(mcpServer.GatewayImage) == "" {
if mcpServer.Type == "uvx" {
mcpServer.GatewayImage = defaultMcpGatewayImageUvx
} else {
mcpServer.GatewayImage = defaultMcpGatewayImageNpx
}
}
if strings.TrimSpace(mcpServer.ProtocolVersion) == "" {
mcpServer.ProtocolVersion = defaultMcpProtocolVersion
}
}
var managedMcpGatewayArgs = map[string]struct{}{
"--": {},
"stdio": {},
"sse": {},
"streamablehttp": {},
"outputtransport": {},
"port": {},
"baseurl": {},
"ssepath": {},
"messagepath": {},
"streamablehttppath": {},
"protocolversion": {},
}
func normalizeMcpGatewayArgName(name string) string {
if name == "--" || !strings.HasPrefix(name, "--") {
return name
}
name = strings.TrimPrefix(name, "--")
name = strings.TrimPrefix(name, "no-")
name = strings.SplitN(name, ".", 2)[0]
return strings.ToLower(strings.ReplaceAll(name, "-", ""))
}
func parseMcpGatewayArgs(raw string) ([]string, error) {
if strings.TrimSpace(raw) == "" {
return nil, nil
}
parser := shellwords.NewParser()
args, err := parser.Parse(raw)
if err != nil {
return nil, buserr.New("ErrMcpGatewayArgsParse")
}
if parser.Position >= 0 {
return nil, buserr.New("ErrMcpGatewayArgsParse")
}
for _, arg := range args {
name := strings.SplitN(arg, "=", 2)[0]
if _, ok := managedMcpGatewayArgs[normalizeMcpGatewayArgName(name)]; ok {
return nil, buserr.WithName("ErrMcpGatewayArgsConflict", name)
}
}
return args, nil
}
func buildSupergatewayCommand(mcpServer *model.McpServer) ([]string, error) {
normalizeMcpServerGateway(mcpServer)
command := []string{
"--stdio", mcpServer.Command,
"--outputTransport", mcpServer.OutputTransport,
"--port", strconv.Itoa(mcpServer.Port),
}
if mcpServer.OutputTransport == mcpOutputTransportSSE {
command = append(command,
"--baseUrl", mcpServer.BaseURL,
"--ssePath", mcpServer.SsePath,
"--messagePath", fmt.Sprintf("%s/messages", mcpServer.SsePath),
)
} else {
command = append(command,
"--streamableHttpPath", mcpServer.StreamableHttpPath,
"--protocolVersion", mcpServer.ProtocolVersion,
)
}
gatewayArgs, err := parseMcpGatewayArgs(mcpServer.GatewayArgs)
if err != nil {
return nil, err
}
return append(command, gatewayArgs...), nil
}
func buildMcpEndpoint(mcpServer *model.McpServer) string {
if mcpServer.BaseURL == "" {
return ""
}
if mcpServer.OutputTransport == mcpOutputTransportSSE {
return strings.TrimRight(mcpServer.BaseURL, "/") + "/" + strings.TrimLeft(mcpServer.SsePath, "/")
}
return strings.TrimRight(mcpServer.BaseURL, "/") + "/" + strings.TrimLeft(mcpServer.StreamableHttpPath, "/")
}
func loadMcpServerComposeConfig(serverDTO *response.McpServerDTO) error {
serverDTO.Environments = make([]request.Environment, 0)
serverDTO.Volumes = make([]request.Volume, 0)
dir := serverDTO.Dir
if dir == "" {
dir = path.Join(global.Dir.McpDir, serverDTO.Name)
}
project, err := docker.GetComposeProject(serverDTO.Name, dir, []byte(serverDTO.DockerCompose), []byte(serverDTO.Env), true)
if err != nil {
return err
}
for _, service := range project.Services {
if service.Environment != nil {
for key, value := range service.Environment {
if value == nil {
continue
}
serverDTO.Environments = append(serverDTO.Environments, request.Environment{
Key: key,
Value: *value,
})
}
}
if service.Volumes != nil {
for _, volume := range service.Volumes {
mode := "rw"
if volume.ReadOnly {
mode = "ro"
}
serverDTO.Volumes = append(serverDTO.Volumes, request.Volume{
Source: volume.Source,
Target: volume.Target,
Mode: mode,
})
}
}
}
return nil
}
func handleEnv(mcpServer *model.McpServer) gotenv.Env { func handleEnv(mcpServer *model.McpServer) gotenv.Env {
env := make(gotenv.Env) env := make(gotenv.Env)
env["CONTAINER_NAME"] = mcpServer.ContainerName env["CONTAINER_NAME"] = mcpServer.ContainerName
@@ -558,6 +872,7 @@ func handleEnv(mcpServer *model.McpServer) gotenv.Env {
env["HOST_IP"] = mcpServer.HostIP env["HOST_IP"] = mcpServer.HostIP
env["STREAMABLE_HTTP_PATH"] = mcpServer.StreamableHttpPath env["STREAMABLE_HTTP_PATH"] = mcpServer.StreamableHttpPath
env["OUTPUT_TRANSPORT"] = mcpServer.OutputTransport env["OUTPUT_TRANSPORT"] = mcpServer.OutputTransport
env["GATEWAY_IMAGE"] = mcpServer.GatewayImage
envStr, _ := gotenv.Marshal(env) envStr, _ := gotenv.Marshal(env)
mcpServer.Env = envStr mcpServer.Env = envStr
return env return env
@@ -592,26 +907,43 @@ func handleCreateParams(mcpServer *model.McpServer, environments []request.Envir
} }
delete(services, serviceName) delete(services, serviceName)
} }
normalizeMcpServerGateway(mcpServer)
command, err := buildSupergatewayCommand(mcpServer)
if err != nil {
return err
}
serviceValue["command"] = command
serviceValue["image"] = mcpServer.GatewayImage
serviceValue["ports"] = []string{
formatComposePortMapping("HOST_IP", "PANEL_APP_PORT_HTTP", "PANEL_APP_PORT_HTTP", ""),
}
oldEnv, hasOldEnv := serviceValue["environment"]
delete(serviceValue, "environment") delete(serviceValue, "environment")
if len(environments) > 0 { if environments != nil && len(environments) > 0 {
envMap := make(map[string]string) envMap := make(map[string]string)
for _, env := range environments { for _, env := range environments {
envMap[env.Key] = env.Value envMap[env.Key] = env.Value
} }
serviceValue["environment"] = envMap serviceValue["environment"] = envMap
} else if environments == nil {
// Preserve existing custom environment when regenerating compose during start/restart.
if mcpServer.ID > 0 && hasOldEnv {
serviceValue["environment"] = oldEnv
}
} }
oldVolumes, hasOldVolumes := serviceValue["volumes"]
delete(serviceValue, "volumes") delete(serviceValue, "volumes")
if len(volumes) > 0 { if volumes != nil && len(volumes) > 0 {
volumeList := make([]string, 0) volumeList := make([]string, 0)
for _, volume := range volumes { for _, volume := range volumes {
volumeList = append(volumeList, fmt.Sprintf("%s:%s", volume.Source, volume.Target)) volumeList = append(volumeList, formatComposeVolume(volume.Source, volume.Target, volume.Mode))
} }
serviceValue["volumes"] = volumeList serviceValue["volumes"] = volumeList
} } else if volumes == nil {
if mcpServer.Type == "npx" { // Preserve existing custom volumes when regenerating compose during start/restart.
serviceValue["image"] = "supercorp/supergateway:latest" if mcpServer.ID > 0 && hasOldVolumes {
} else { serviceValue["volumes"] = oldVolumes
serviceValue["image"] = "supercorp/supergateway:uvx" }
} }
services[mcpServer.Name] = serviceValue services[mcpServer.Name] = serviceValue
@@ -623,19 +955,56 @@ func handleCreateParams(mcpServer *model.McpServer, environments []request.Envir
return nil return nil
} }
func runMcpServerTask(mcpServer *model.McpServer, taskID, operate string) {
mcpTask, err := task.NewTaskWithOps(mcpServer.Name, operate, task.TaskScopeAI, taskID, mcpServer.ID)
if err != nil {
mcpServer.Status = constant.StatusError
mcpServer.Message = err.Error()
_ = mcpServerRepo.Save(mcpServer)
return
}
mcpTask.AddSubTask(task.GetTaskName(mcpServer.Name, operate, task.TaskScopeAI), func(t *task.Task) error {
return startMcpWithTask(mcpServer, t)
}, nil)
_ = mcpTask.Execute()
}
func startMcp(mcpServer *model.McpServer) { func startMcp(mcpServer *model.McpServer) {
_ = startMcpWithTask(mcpServer, nil)
}
func startMcpWithTask(mcpServer *model.McpServer, taskItem *task.Task) error {
if err := refreshMcpServerFiles(mcpServer); err != nil {
mcpServer.Status = constant.StatusError
mcpServer.Message = err.Error()
_ = mcpServerRepo.Save(mcpServer)
return err
}
composePath := path.Join(global.Dir.McpDir, mcpServer.Name, "docker-compose.yml") composePath := path.Join(global.Dir.McpDir, mcpServer.Name, "docker-compose.yml")
if mcpServer.Status != constant.StatusNormal { if mcpServer.Status != constant.StatusNormal {
_, _ = compose.Down(composePath) _, _ = compose.Down(composePath)
} }
if out, err := compose.Up(composePath); err != nil { var err error
var out string
if taskItem != nil {
err = compose.UpWithTask(composePath, taskItem, false)
} else {
out, err = compose.Up(composePath)
}
if err != nil {
mcpServer.Status = constant.StatusError mcpServer.Status = constant.StatusError
mcpServer.Message = out if out != "" {
mcpServer.Message = out
} else {
mcpServer.Message = err.Error()
}
_ = mcpServerRepo.Save(mcpServer)
return err
} else { } else {
mcpServer.Status = constant.StatusRunning mcpServer.Status = constant.StatusRunning
mcpServer.Message = "" mcpServer.Message = ""
} }
_ = syncMcpServerContainerStatus(mcpServer) return syncMcpServerContainerStatus(mcpServer)
} }
func syncMcpServerContainerStatus(mcpServer *model.McpServer) error { func syncMcpServerContainerStatus(mcpServer *model.McpServer) error {
@@ -650,11 +1019,19 @@ func syncMcpServerContainerStatus(mcpServer *model.McpServer) error {
return err return err
} }
if len(containers) == 0 { if len(containers) == 0 {
mcpServer.Status = constant.StatusStopped applyMcpContainerStatus(mcpServer, nil)
return mcpServerRepo.Save(mcpServer) return mcpServerRepo.Save(mcpServer)
} }
container := containers[0] applyMcpContainerStatus(mcpServer, &containers[0])
switch container.State { return mcpServerRepo.Save(mcpServer)
}
func applyMcpContainerStatus(mcpServer *model.McpServer, containerSummary *container.Summary) {
if containerSummary == nil {
mcpServer.Status = constant.StatusStopped
return
}
switch containerSummary.State {
case "exited": case "exited":
mcpServer.Status = constant.StatusError mcpServer.Status = constant.StatusError
case "running": case "running":
@@ -668,7 +1045,6 @@ func syncMcpServerContainerStatus(mcpServer *model.McpServer) error {
mcpServer.Status = constant.StatusStopped mcpServer.Status = constant.StatusStopped
} }
} }
return mcpServerRepo.Save(mcpServer)
} }
func GetWebsiteID() uint { func GetWebsiteID() uint {
@@ -679,18 +1055,3 @@ func GetWebsiteID() uint {
websiteIDUint, _ := strconv.ParseUint(websiteID.Value, 10, 64) websiteIDUint, _ := strconv.ParseUint(websiteID.Value, 10, 64)
return uint(websiteIDUint) return uint(websiteIDUint)
} }
func pullImage(imageType string) {
if global.CONF.Base.IsOffline {
return
}
if imageType == "npx" {
if err := docker.PullImage("supercorp/supergateway:latest"); err != nil {
global.LOG.Errorf("docker pull mcp image error: %s", err.Error())
}
} else {
if err := docker.PullImage("supercorp/supergateway:uvx"); err != nil {
global.LOG.Errorf("docker pull mcp image error: %s", err.Error())
}
}
}
+52 -10
View File
@@ -4,9 +4,12 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"path/filepath"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/repo"
@@ -34,7 +37,12 @@ type MonitorService struct {
NetIO chan ([]net.IOCountersStat) NetIO chan ([]net.IOCountersStat)
} }
var monitorCancel context.CancelFunc var (
monitorCancel context.CancelFunc
hostSysPath = loadHostSysPath()
blockDevicePartitionCache sync.Map
)
type IMonitorService interface { type IMonitorService interface {
Run() Run()
@@ -341,18 +349,10 @@ func (m *MonitorService) Run() {
func (m *MonitorService) loadDiskIO() { func (m *MonitorService) loadDiskIO() {
ioStat, _ := disk.IOCounters() ioStat, _ := disk.IOCounters()
var diskIOList []disk.IOCountersStat var diskIOList []disk.IOCountersStat
var ioStatAll disk.IOCountersStat
for _, io := range ioStat { for _, io := range ioStat {
ioStatAll.Name = "all"
ioStatAll.ReadBytes += io.ReadBytes
ioStatAll.WriteBytes += io.WriteBytes
ioStatAll.ReadTime += io.ReadTime
ioStatAll.WriteTime += io.WriteTime
ioStatAll.WriteCount += io.WriteCount
ioStatAll.ReadCount += io.ReadCount
diskIOList = append(diskIOList, io) diskIOList = append(diskIOList, io)
} }
diskIOList = append(diskIOList, ioStatAll) diskIOList = append(diskIOList, sumDiskIOCounters(ioStat))
m.DiskIO <- diskIOList m.DiskIO <- diskIOList
} }
@@ -680,3 +680,45 @@ func loadGPUInfoFloat(val string) float64 {
data, _ := strconv.ParseFloat(val, 64) data, _ := strconv.ParseFloat(val, 64)
return data return data
} }
func sumDiskIOCounters(ioStats map[string]disk.IOCountersStat) disk.IOCountersStat {
total := disk.IOCountersStat{Name: "all"}
for name, stat := range ioStats {
if isBlockDevicePartition(name) {
continue
}
total.ReadCount += stat.ReadCount
total.MergedReadCount += stat.MergedReadCount
total.WriteCount += stat.WriteCount
total.MergedWriteCount += stat.MergedWriteCount
total.ReadBytes += stat.ReadBytes
total.WriteBytes += stat.WriteBytes
total.ReadTime += stat.ReadTime
total.WriteTime += stat.WriteTime
total.IopsInProgress += stat.IopsInProgress
total.IoTime += stat.IoTime
total.WeightedIO += stat.WeightedIO
}
return total
}
func isBlockDevicePartition(name string) bool {
deviceName := filepath.Base(name)
if cached, ok := blockDevicePartitionCache.Load(deviceName); ok {
return cached.(bool)
}
_, err := os.Stat(filepath.Join(hostSysPath, "class", "block", deviceName, "partition"))
isPartition := err == nil
actual, _ := blockDevicePartitionCache.LoadOrStore(deviceName, isPartition)
return actual.(bool)
}
func loadHostSysPath() string {
hostSys := os.Getenv("HOST_SYS")
if hostSys == "" {
return "/sys"
}
return hostSys
}
+151 -126
View File
@@ -1,8 +1,6 @@
package service package service
import ( import (
"bufio"
"encoding/json"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/agent/utils/nginx" "github.com/1Panel-dev/1Panel/agent/utils/nginx"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser" "github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
@@ -17,7 +15,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/subosito/gotenv" "github.com/subosito/gotenv"
"github.com/1Panel-dev/1Panel/agent/utils/compose" "github.com/1Panel-dev/1Panel/agent/utils/compose"
@@ -57,7 +54,7 @@ func (n NginxService) GetNginxConfig() (*response.NginxFile, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
configPath := path.Join(global.Dir.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "nginx.conf") configPath := path.Join(global.Dir.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, nginxModuleConfDir, "nginx.conf")
byteContent, err := files.NewFileOp().GetContent(configPath) byteContent, err := files.NewFileOp().GetContent(configPath)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -138,7 +135,7 @@ func (n NginxService) UpdateConfigFile(req request.NginxConfigFileUpdate) error
if err != nil { if err != nil {
return err return err
} }
filePath := path.Join(global.Dir.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "nginx.conf") filePath := path.Join(global.Dir.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, nginxModuleConfDir, "nginx.conf")
if req.Backup { if req.Backup {
backupPath := path.Join(path.Dir(filePath), "bak") backupPath := path.Join(path.Dir(filePath), "bak")
if !fileOp.Stat(backupPath) { if !fileOp.Stat(backupPath) {
@@ -181,71 +178,20 @@ func (n NginxService) Build(req request.NginxBuildReq) error {
if err = task.CheckTaskIsExecuting(taskName); err != nil { if err = task.CheckTaskIsExecuting(taskName); err != nil {
return err return err
} }
fileOp := files.NewFileOp() if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, nginxInstall.ID); err != nil {
buildPath := path.Join(nginxInstall.GetPath(), "build") return err
if !fileOp.Stat(buildPath) { }
buildPath := path.Join(nginxInstall.GetPath(), nginxModuleBuildDir)
if !files.NewFileOp().Stat(buildPath) {
return buserr.New("ErrBuildDirNotFound") return buserr.New("ErrBuildDirNotFound")
} }
moduleConfigPath := path.Join(buildPath, "module.json")
moduleContent, err := fileOp.GetContent(moduleConfigPath)
if err != nil {
return err
}
var (
modules []dto.NginxModule
addModuleParams []string
addPackages []string
)
if len(moduleContent) > 0 {
_ = json.Unmarshal(moduleContent, &modules)
bashFile, err := os.OpenFile(path.Join(buildPath, "tmp", "pre.sh"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, constant.DirPerm)
if err != nil {
return err
}
defer bashFile.Close()
bashFileWriter := bufio.NewWriter(bashFile)
for _, module := range modules {
if !module.Enable {
continue
}
_, err = bashFileWriter.WriteString(module.Script + "\n")
if err != nil {
return err
}
addModuleParams = append(addModuleParams, module.Params)
addPackages = append(addPackages, module.Packages...)
}
err = bashFileWriter.Flush()
if err != nil {
return err
}
}
envs, err := gotenv.Read(nginxInstall.GetEnvPath())
if err != nil {
return err
}
envs["CONTAINER_PACKAGE_URL"] = req.Mirror
envs["RESTY_CONFIG_OPTIONS_MORE"] = ""
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = ""
if len(addModuleParams) > 0 {
envs["RESTY_CONFIG_OPTIONS_MORE"] = strings.Join(addModuleParams, " ")
}
if len(addPackages) > 0 {
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = strings.Join(addPackages, " ")
}
_ = gotenv.Write(envs, nginxInstall.GetEnvPath())
buildTask, err := task.NewTaskWithOps(nginxInstall.Name, task.TaskBuild, task.TaskScopeApp, req.TaskID, nginxInstall.ID) buildTask, err := task.NewTaskWithOps(nginxInstall.Name, task.TaskBuild, task.TaskScopeApp, req.TaskID, nginxInstall.ID)
if err != nil { if err != nil {
return err return err
} }
buildTask.AddSubTaskWithOps("", func(t *task.Task) error { buildTask.AddSubTaskWithOps("", func(t *task.Task) error {
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*buildTask), cmd.WithTimeout(120*time.Minute)) return executeNginxModuleBuild(nginxInstall, req.Modules, req.Force, req.Mirror, t, true)
if err = cmdMgr.Run("docker", "compose", "-f", nginxInstall.GetComposePath(), "build"); err != nil {
return err
}
_, err = compose.DownAndUp(nginxInstall.GetComposePath())
return err
}, nil, 0, 120*time.Minute) }, nil, 0, 120*time.Minute)
go func() { go func() {
@@ -259,27 +205,70 @@ func (n NginxService) GetModules() (*response.NginxBuildConfig, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
fileOp := files.NewFileOp() modules, err := loadNginxModules(nginxInstall)
var modules []dto.NginxModule
moduleConfigPath := path.Join(nginxInstall.GetPath(), "build", "module.json")
if !fileOp.Stat(moduleConfigPath) {
return nil, nil
}
moduleContent, err := fileOp.GetContent(moduleConfigPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(moduleContent) > 0 { target, targetWarning, targetErr := resolveNginxModuleTarget(nginxInstall)
_ = json.Unmarshal(moduleContent, &modules) if targetWarning != "" {
global.LOG.Warn(targetWarning)
}
runtimeTarget := target
runtimeTarget.BuilderDigest = ""
setNginxModuleTargetKey(&runtimeTarget)
runtimeTargetErr := targetErr
if runtimeTarget.OpenRestyVersion != "" {
runtimeTargetErr = nil
} }
var resList []response.NginxModule var resList []response.NginxModule
for _, module := range modules { for _, module := range modules {
buildStatus := nginxModuleStatusPending
loadStatus := nginxModuleLoadDisabled
var artifacts []dto.NginxModuleArtifact
if module.BuildMode == nginxModuleBuildStatic && runtimeTargetErr == nil {
if build := findCurrentNginxModuleBuild(module, runtimeTarget); build != nil && build.Status == nginxModuleStatusReady {
buildStatus = nginxModuleStatusReady
if module.Enable {
loadStatus = nginxModuleLoadEnabled
}
} else if findLatestNginxModuleBuild(module, runtimeTarget) != nil {
if module.Enable {
loadStatus = nginxModuleLoadEnabled
}
}
} else if targetErr == nil {
if build := findCurrentNginxModuleBuild(module, target); build != nil {
buildStatus = build.Status
artifacts = build.Artifacts
if build.Status == nginxModuleStatusReady {
if module.Enable {
loadStatus = nginxModuleLoadEnabled
}
}
} else if latestBuild := findLatestNginxModuleBuild(module, target); latestBuild != nil {
artifacts = latestBuild.Artifacts
if module.Enable {
loadStatus = nginxModuleLoadEnabled
}
}
}
if module.BuildMode != nginxModuleBuildStatic && module.LastError != "" {
buildStatus = nginxModuleStatusFailed
}
resList = append(resList, response.NginxModule{ resList = append(resList, response.NginxModule{
Name: module.Name, Name: module.Name,
Script: module.Script, Custom: module.Custom,
Packages: strings.Join(module.Packages, ","), Script: module.Script,
Params: module.Params, Packages: strings.Join(module.Packages, ","),
Enable: module.Enable, Params: module.Params,
Enable: module.Enable,
BuildMode: module.BuildMode,
Provider: module.Provider,
LoadOrder: module.LoadOrder,
BuildStatus: buildStatus,
LoadStatus: loadStatus,
Artifacts: artifacts,
LastError: module.LastError,
}) })
} }
envs, err := gotenv.Read(nginxInstall.GetEnvPath()) envs, err := gotenv.Read(nginxInstall.GetEnvPath())
@@ -288,69 +277,105 @@ func (n NginxService) GetModules() (*response.NginxBuildConfig, error) {
} }
return &response.NginxBuildConfig{ return &response.NginxBuildConfig{
Mirror: envs["CONTAINER_PACKAGE_URL"], Mirror: envs["CONTAINER_PACKAGE_URL"],
Modules: resList, DynamicSupported: nginxModuleDynamicSupported(nginxInstall),
Modules: resList,
}, nil }, nil
} }
func applyNginxModuleUpdate(modules []dto.NginxModule, req request.NginxModuleUpdate) ([]dto.NginxModule, *dto.NginxModule, error) {
switch req.Operate {
case nginxModuleOperateCreate:
if err := validateNginxModuleBuildMode(dto.NginxModule{Name: req.Name, BuildMode: req.BuildMode}); err != nil {
return nil, nil, err
}
for _, module := range modules {
if module.Name == req.Name {
return nil, nil, buserr.New("ErrNameIsExist")
}
}
modules = append(modules, dto.NginxModule{
Name: req.Name,
Custom: true,
Script: req.Script,
Packages: strings.Split(req.Packages, ","),
Params: req.Params,
Enable: req.Enable,
BuildMode: req.BuildMode,
Provider: req.Provider,
LoadOrder: req.LoadOrder,
})
case nginxModuleOperateUpdate:
for i := range modules {
if modules[i].Name != req.Name {
continue
}
if modules[i].Custom {
if err := validateNginxModuleBuildMode(dto.NginxModule{Name: req.Name, BuildMode: req.BuildMode}); err != nil {
return nil, nil, err
}
} else {
modules[i].Enable = req.Enable
return modules, nil, nil
}
modules[i].Enable = req.Enable
modules[i].Script = req.Script
modules[i].Packages = strings.Split(req.Packages, ",")
modules[i].Params = req.Params
modules[i].BuildMode = req.BuildMode
modules[i].Provider = req.Provider
modules[i].LoadOrder = req.LoadOrder
return modules, nil, nil
}
return nil, nil, fmt.Errorf("OpenResty module %s not found", req.Name)
case nginxModuleOperateDelete:
for i := range modules {
if modules[i].Name != req.Name {
continue
}
if !modules[i].Custom {
return nil, nil, fmt.Errorf("built-in OpenResty module %s cannot be deleted", req.Name)
}
deleted := modules[i]
modules = append(modules[:i], modules[i+1:]...)
return modules, &deleted, nil
}
return nil, nil, fmt.Errorf("OpenResty module %s not found", req.Name)
default:
return nil, nil, fmt.Errorf("unsupported OpenResty module operation %q", req.Operate)
}
return modules, nil, nil
}
func (n NginxService) UpdateModule(req request.NginxModuleUpdate) error { func (n NginxService) UpdateModule(req request.NginxModuleUpdate) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil { if err != nil {
return err return err
} }
fileOp := files.NewFileOp() if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, nginxInstall.ID); err != nil {
var ( return err
modules []dto.NginxModule
)
moduleConfigPath := path.Join(nginxInstall.GetPath(), "build", "module.json")
if !fileOp.Stat(moduleConfigPath) {
_ = fileOp.CreateFile(moduleConfigPath)
} }
moduleContent, err := fileOp.GetContent(moduleConfigPath) modules, err := loadNginxModules(nginxInstall)
if err != nil { if err != nil {
return err return err
} }
if len(moduleContent) > 0 { oldModules := cloneNginxModules(modules)
_ = json.Unmarshal(moduleContent, &modules) modules, deletedModule, err := applyNginxModuleUpdate(modules, req)
}
switch req.Operate {
case "create":
for _, module := range modules {
if module.Name == req.Name {
return buserr.New("ErrNameIsExist")
}
}
modules = append(modules, dto.NginxModule{
Name: req.Name,
Script: req.Script,
Packages: strings.Split(req.Packages, ","),
Params: req.Params,
Enable: true,
})
case "update":
for i, module := range modules {
if module.Name == req.Name {
modules[i].Script = req.Script
modules[i].Packages = strings.Split(req.Packages, ",")
modules[i].Params = req.Params
modules[i].Enable = req.Enable
break
}
}
case "delete":
for i, module := range modules {
if module.Name == req.Name {
modules = append(modules[:i], modules[i+1:]...)
break
}
}
}
moduleByte, err := json.Marshal(modules)
if err != nil { if err != nil {
return err return err
} }
return fileOp.SaveFileWithByte(moduleConfigPath, moduleByte, constant.DirPerm) if err = saveNginxModules(nginxInstall, modules); err != nil {
return err
}
if err = reconcileDynamicNginxModuleConfig(nginxInstall, modules, true); err != nil {
_ = saveNginxModules(nginxInstall, oldModules)
_ = reconcileDynamicNginxModuleConfig(nginxInstall, oldModules, false)
return err
}
if deletedModule != nil {
_ = removeNginxModuleArtifacts(nginxInstall, *deletedModule)
}
return nil
} }
func (n NginxService) OperateDefaultHTTPs(req request.NginxDefaultHTTPSUpdate) error { func (n NginxService) OperateDefaultHTTPs(req request.NginxDefaultHTTPSUpdate) error {
@@ -366,7 +391,7 @@ func (n NginxService) OperateDefaultHTTPs(req request.NginxDefaultHTTPSUpdate) e
break break
} }
} }
defaultConfigPath := path.Join(appInstall.GetPath(), "conf", "default", "00.default.conf") defaultConfigPath := path.Join(appInstall.GetPath(), nginxModuleConfDir, "default", "00.default.conf")
content, err := os.ReadFile(defaultConfigPath) content, err := os.ReadFile(defaultConfigPath)
if err != nil { if err != nil {
return err return err
@@ -406,7 +431,7 @@ func (n NginxService) GetDefaultHttpsStatus() (*response.NginxConfigRes, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defaultConfigPath := path.Join(appInstall.GetPath(), "conf", "default", "00.default.conf") defaultConfigPath := path.Join(appInstall.GetPath(), nginxModuleConfDir, "default", "00.default.conf")
content, err := os.ReadFile(defaultConfigPath) content, err := os.ReadFile(defaultConfigPath)
if err != nil { if err != nil {
return nil, err return nil, err
File diff suppressed because it is too large Load Diff
+676
View File
@@ -0,0 +1,676 @@
package service
import (
"encoding/json"
"errors"
"os"
"path"
"strings"
"testing"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
)
func TestNormalizeNginxModuleDoesNotInferBuildMode(t *testing.T) {
module := dto.NginxModule{
Name: "legacy",
Packages: []string{"git", "", "git", " curl "},
}
normalizeNginxModule(&module)
if module.BuildMode != "" {
t.Fatalf("build mode must be explicit, got %s", module.BuildMode)
}
if module.Provider != nginxModuleProviderLocal {
t.Fatalf("expected local provider, got %s", module.Provider)
}
if len(module.Packages) != 2 || module.Packages[0] != "git" || module.Packages[1] != "curl" {
t.Fatalf("unexpected normalized packages: %#v", module.Packages)
}
}
func TestNormalizeDynamicModuleParams(t *testing.T) {
params, err := normalizeDynamicModuleParams("--with-http_dav_module --add-module=/tmp/nginx-dav-ext-module")
if err != nil {
t.Fatal(err)
}
expected := "--with-http_dav_module --add-dynamic-module=/tmp/nginx-dav-ext-module"
if params != expected {
t.Fatalf("expected %q, got %q", expected, params)
}
if _, err = normalizeDynamicModuleParams("--add-module=/tmp/module;touch /tmp/unsafe"); err == nil {
t.Fatal("expected shell metacharacters to be rejected")
}
}
func TestFindCurrentAndLatestNginxModuleBuild(t *testing.T) {
target := dto.NginxModuleTarget{Key: "target"}
module := dto.NginxModule{
Name: "example",
Params: "--add-module=/tmp/example",
BuildMode: nginxModuleBuildDynamic,
Provider: nginxModuleProviderLocal,
}
params, err := normalizeDynamicModuleParams(module.Params)
if err != nil {
t.Fatal(err)
}
currentHash, err := nginxModuleBuildHash(module, target, params)
if err != nil {
t.Fatal(err)
}
oldTime := time.Now().Add(-time.Hour)
newTime := time.Now()
module.Builds = []dto.NginxModuleBuild{
{Hash: "old", Status: nginxModuleStatusReady, Target: target, BuiltAt: oldTime},
{Hash: currentHash, Status: nginxModuleStatusReady, Target: target, BuiltAt: newTime},
}
if build := findCurrentNginxModuleBuild(module, target); build == nil || build.Hash != currentHash {
t.Fatalf("current build was not selected: %#v", build)
}
if build := findLatestNginxModuleBuild(module, target); build == nil || build.Hash != currentHash {
t.Fatalf("latest build was not selected: %#v", build)
}
module.Params = "--add-module=/tmp/example-v2"
if build := findCurrentNginxModuleBuild(module, target); build != nil {
t.Fatalf("changed module input should be stale, got %#v", build)
}
if build := findLatestNginxModuleBuild(module, target); build == nil || build.Hash != currentHash {
t.Fatal("the previous ready build should remain available until replacement")
}
}
func TestNginxModulePathNameAvoidsSanitizedNameCollisions(t *testing.T) {
first := nginxModulePathName("example/module")
second := nginxModulePathName("example-module")
if first == second {
t.Fatalf("module path names collided: %s", first)
}
if len(nginxModulePathName(string(make([]byte, 256)))) > 57 {
t.Fatal("module path name should remain safe for Docker resource names")
}
}
func TestRecordNginxModuleBuildFailureKeepsPreviousReadyBuild(t *testing.T) {
target := dto.NginxModuleTarget{Key: "target"}
ready := dto.NginxModuleBuild{
Hash: "ready", Status: nginxModuleStatusReady, Target: target, BuiltAt: time.Now().Add(-time.Hour),
}
original := []dto.NginxModule{{
Name: "example", BuildMode: nginxModuleBuildDynamic,
Builds: []dto.NginxModuleBuild{ready},
}}
failed := dto.NginxModuleBuild{
Hash: "candidate", Status: nginxModuleStatusFailed, Target: target, Error: "load failed", BuiltAt: time.Now(),
}
result := recordNginxModuleBuildFailure(original, "example", failed, &ready)
if len(result[0].Builds) != 1 || result[0].Builds[0].Hash != "ready" {
t.Fatalf("previous ready build was replaced: %#v", result[0].Builds)
}
if result[0].LastError != failed.Error {
t.Fatalf("failure metadata was not retained: %#v", result[0])
}
result[0].Builds[0].Hash = "mutated"
if original[0].Builds[0].Hash != "ready" {
t.Fatal("module clone shares build state with the original")
}
}
func TestHasDynamicNginxModuleBuildTask(t *testing.T) {
dynamicEnabled := dto.NginxModule{Name: "brotli", Enable: true, BuildMode: nginxModuleBuildDynamic}
staticEnabled := dto.NginxModule{Name: "pagespeed", Enable: true, BuildMode: nginxModuleBuildStatic}
disabledDynamic := dto.NginxModule{Name: "waf", Enable: false, BuildMode: nginxModuleBuildDynamic}
if hasDynamicNginxModuleBuildTask(nil, nil) {
t.Fatal("empty module list should not require a dynamic build")
}
if hasDynamicNginxModuleBuildTask([]dto.NginxModule{staticEnabled}, nil) {
t.Fatal("static-only modules should not require a dynamic build")
}
if hasDynamicNginxModuleBuildTask([]dto.NginxModule{dynamicEnabled}, []string{"other"}) {
t.Fatal("enabled module outside the selection should not require a dynamic build")
}
if !hasDynamicNginxModuleBuildTask([]dto.NginxModule{disabledDynamic}, []string{"waf"}) {
t.Fatal("selected module should require a dynamic build even when disabled")
}
if !hasDynamicNginxModuleBuildTask([]dto.NginxModule{dynamicEnabled, staticEnabled}, nil) {
t.Fatal("enabled dynamic module should require a dynamic build")
}
legacy := dto.NginxModule{Name: "legacy", Enable: true}
if hasDynamicNginxModuleBuildTask([]dto.NginxModule{legacy}, nil) {
t.Fatal("legacy module without a build mode stays static and should not require a dynamic build")
}
if legacy.BuildMode != "" {
t.Fatalf("prescan must normalize a copy, got mutated BuildMode %q", legacy.BuildMode)
}
}
func TestNginxModuleStaticBuildErrorHintOnlyForCustomModules(t *testing.T) {
if hint := nginxModuleStaticBuildErrorHint(dto.NginxModule{Name: "builtin"}); hint != "" {
t.Fatalf("built-in module cannot switch build mode, got hint %q", hint)
}
if hint := nginxModuleStaticBuildErrorHint(dto.NginxModule{Name: "custom", Custom: true}); hint == "" {
t.Fatal("custom module should receive the static-build alternative")
}
}
func TestResolveNginxModuleTargetWithoutBuilder(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.27.1.2"}
install.App.Key = constant.AppOpenresty
_, _, err := resolveNginxModuleTarget(install)
if !errors.Is(err, errNginxModuleBuilderMissing) {
t.Fatalf("expected builder-missing sentinel, got %v", err)
}
if !strings.Contains(err.Error(), "dynamic module builder not found") {
t.Fatalf("unexpected error message: %v", err)
}
}
func TestBuildDynamicNginxModulesFailsWithoutBuilder(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
modules := []dto.NginxModule{{
Name: "rtmp", Enable: true, BuildMode: nginxModuleBuildDynamic,
}}
_, err := buildDynamicNginxModules(install, modules, nil, false, "", "", nil)
if !errors.Is(err, errNginxModuleBuilderMissing) {
t.Fatalf("missing target builder must fail the dynamic build, got %v", err)
}
}
func TestMergeOpenrestyModuleVolumes(t *testing.T) {
newService := map[string]interface{}{}
oldService := map[string]interface{}{
"volumes": []interface{}{
"./conf:/etc/nginx/conf.d:ro",
"./modules:/usr/local/openresty/nginx/modules/1panel:ro",
"./conf/modules-enabled:/usr/local/openresty/nginx/conf/modules-enabled:ro",
12345,
},
}
mergeOpenrestyModuleVolumes(newService, oldService)
merged, ok := newService["volumes"].([]interface{})
if !ok || len(merged) != 2 {
t.Fatalf("expected two module mounts to be merged, got %#v", newService["volumes"])
}
for _, volume := range merged {
if volume == "./conf:/etc/nginx/conf.d:ro" {
t.Fatal("unrelated mount should not be merged")
}
}
}
func TestMergeOpenrestyModuleVolumesKeepsExisting(t *testing.T) {
existing := "./modules:/usr/local/openresty/nginx/modules/1panel:ro"
newService := map[string]interface{}{
"volumes": []interface{}{existing, map[string]interface{}{"type": "bind"}},
}
oldService := map[string]interface{}{
"volumes": []interface{}{
"./modules:/usr/local/openresty/nginx/modules/1panel:ro",
"./conf/modules-enabled:/usr/local/openresty/nginx/conf/modules-enabled:ro",
},
}
mergeOpenrestyModuleVolumes(newService, oldService)
merged, ok := newService["volumes"].([]interface{})
if !ok || len(merged) != 3 {
t.Fatalf("expected only the missing mount to be appended, got %#v", newService["volumes"])
}
if merged[0] != existing {
t.Fatalf("existing mounts must keep their order, got %#v", merged)
}
if merged[2] != "./conf/modules-enabled:/usr/local/openresty/nginx/conf/modules-enabled:ro" {
t.Fatalf("missing module mount was not appended, got %#v", merged)
}
}
func TestResolveNginxModuleBuildMirror(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.27.1.2"}
install.App.Key = constant.AppOpenresty
if err := os.MkdirAll(install.GetPath(), constant.DirPerm); err != nil {
t.Fatal(err)
}
if got := resolveNginxModuleBuildMirror(install, "https://mirror.example.com"); got != "https://mirror.example.com" {
t.Fatalf("request mirror should win, got %q", got)
}
if got := resolveNginxModuleBuildMirror(install, ""); got != "" {
t.Fatalf("missing env file should yield an empty mirror, got %q", got)
}
envContent := "CONTAINER_PACKAGE_URL=https://apt.example.com\nOTHER=value\n"
if err := os.WriteFile(install.GetEnvPath(), []byte(envContent), constant.FilePerm); err != nil {
t.Fatal(err)
}
if got := resolveNginxModuleBuildMirror(install, ""); got != "https://apt.example.com" {
t.Fatalf("env CONTAINER_PACKAGE_URL should be the fallback, got %q", got)
}
if got := resolveNginxModuleBuildMirror(install, "https://mirror.example.com"); got != "https://mirror.example.com" {
t.Fatalf("request mirror should still win over the env value, got %q", got)
}
}
func writeNginxModuleCatalogStateFixture(t *testing.T, install model.AppInstall, catalog []dto.NginxModule, state string) {
t.Helper()
buildDir := path.Join(install.GetPath(), nginxModuleBuildDir)
if err := os.MkdirAll(buildDir, constant.DirPerm); err != nil {
t.Fatal(err)
}
catalogContent, err := json.Marshal(catalog)
if err != nil {
t.Fatal(err)
}
if err = os.WriteFile(path.Join(buildDir, nginxModuleCatalogFile), catalogContent, constant.FilePerm); err != nil {
t.Fatal(err)
}
if state != "" {
if err = os.WriteFile(path.Join(buildDir, nginxModuleStoreFile), []byte(state), constant.FilePerm); err != nil {
t.Fatal(err)
}
}
}
func TestLoadNginxModulesBuiltinStateCannotOverrideCatalogDefinition(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{{
Name: "rtmp", Script: "catalog-script", Packages: []string{"unzip"}, Params: "--add-module=/tmp/rtmp",
BuildMode: nginxModuleBuildDynamic, Provider: nginxModuleProviderLocal, LoadOrder: 20,
}}
state := `[{"name":"rtmp","enable":true,"script":"user-script","params":"--with-user","buildMode":"static","loadOrder":99,"lastError":"failed"}]`
writeNginxModuleCatalogStateFixture(t, install, catalog, state)
modules, err := loadNginxModules(install)
if err != nil {
t.Fatal(err)
}
if len(modules) != 1 {
t.Fatalf("expected one merged module, got %#v", modules)
}
module := modules[0]
if !module.Enable || module.LastError != "failed" {
t.Fatalf("builtin state was not applied: %#v", module)
}
if module.Script != "catalog-script" || module.Params != "--add-module=/tmp/rtmp" ||
module.BuildMode != nginxModuleBuildDynamic || module.LoadOrder != 20 {
t.Fatalf("builtin definition must come from catalog: %#v", module)
}
}
func TestLoadNginxModulesWithoutStateShowsDisabledCatalogModules(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{
{Name: "rtmp", Enable: true, BuildMode: nginxModuleBuildDynamic},
{Name: "geoip2", BuildMode: nginxModuleBuildDynamic},
}
writeNginxModuleCatalogStateFixture(t, install, catalog, "")
modules, err := loadNginxModules(install)
if err != nil {
t.Fatal(err)
}
if len(modules) != len(catalog) {
t.Fatalf("expected all catalog modules, got %#v", modules)
}
for _, module := range modules {
if module.Enable || module.Custom {
t.Fatalf("catalog modules must default to disabled built-ins: %#v", module)
}
}
}
func TestSaveNginxModulesPersistsOnlyBuiltinState(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{{
Name: "rtmp", Script: "catalog-script", Params: "--add-module=/tmp/rtmp",
BuildMode: nginxModuleBuildDynamic, Provider: nginxModuleProviderLocal, LoadOrder: 20,
}}
writeNginxModuleCatalogStateFixture(t, install, catalog, "")
modules, err := loadNginxModules(install)
if err != nil {
t.Fatal(err)
}
modules[0].Enable = true
modules[0].LastError = "failed"
if err = saveNginxModules(install, modules); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleStoreFile))
if err != nil {
t.Fatal(err)
}
var states []map[string]any
if err = json.Unmarshal(content, &states); err != nil {
t.Fatal(err)
}
if len(states) != 1 || states[0]["name"] != "rtmp" || states[0]["enable"] != true || states[0]["lastError"] != "failed" {
t.Fatalf("unexpected builtin state: %s", content)
}
for _, key := range []string{"script", "packages", "params", "buildMode", "provider", "loadOrder"} {
if _, exists := states[0][key]; exists {
t.Fatalf("builtin definition field %q leaked into module state: %s", key, content)
}
}
}
func TestSaveNginxModulesOmitsPristineBuiltinState(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{{Name: "rtmp", BuildMode: nginxModuleBuildDynamic}}
writeNginxModuleCatalogStateFixture(t, install, catalog, "")
modules, err := loadNginxModules(install)
if err != nil {
t.Fatal(err)
}
if err = saveNginxModules(install, modules); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleStoreFile))
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(string(content)) != "[]" {
t.Fatalf("pristine built-in state should not be persisted: %s", content)
}
}
func TestLoadAndSaveNginxCustomModule(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
state := `[{"name":"custom","custom":true,"script":"prepare","packages":["git"],"params":"--add-module=/tmp/custom","enable":true,"buildMode":"static","provider":"local","loadOrder":100}]`
writeNginxModuleCatalogStateFixture(t, install, nil, state)
modules, err := loadNginxModules(install)
if err != nil {
t.Fatal(err)
}
if len(modules) != 1 || !modules[0].Custom {
t.Fatalf("custom module source was not restored: %#v", modules)
}
if modules[0].Script != "prepare" || modules[0].BuildMode != nginxModuleBuildStatic || modules[0].LoadOrder != 100 {
t.Fatalf("custom module definition was not restored: %#v", modules[0])
}
if err = saveNginxModules(install, modules); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleStoreFile))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(content), `"custom": true`) || !strings.Contains(string(content), `"buildMode": "static"`) {
t.Fatalf("custom module definition was not persisted: %s", content)
}
}
func TestActivateNginxModuleCatalogReplacesCatalogAtomically(t *testing.T) {
buildDir := t.TempDir()
activePath := path.Join(buildDir, nginxModuleCatalogFile)
pendingPath := path.Join(buildDir, nginxModuleCatalogPendingFile)
sourcePath := path.Join(buildDir, "target.catalog.json")
if err := os.WriteFile(activePath, []byte(`[{"name":"old"}]`), constant.FilePerm); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(sourcePath, []byte(`[{"name":"new"}]`), constant.FilePerm); err != nil {
t.Fatal(err)
}
if err := stageNginxModuleCatalog(sourcePath, pendingPath); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(activePath)
if err != nil {
t.Fatal(err)
}
if string(content) != `[{"name":"old"}]` {
t.Fatalf("staging target catalog changed the active catalog: %s", content)
}
if err := activateNginxModuleCatalog(pendingPath, activePath); err != nil {
t.Fatal(err)
}
content, err = os.ReadFile(activePath)
if err != nil {
t.Fatal(err)
}
if string(content) != `[{"name":"new"}]` {
t.Fatalf("active catalog was not replaced: %s", content)
}
if _, err = os.Stat(pendingPath); !os.IsNotExist(err) {
t.Fatalf("pending catalog should be consumed, got %v", err)
}
}
func TestActivateNginxModuleCatalogRestoresCatalogWhenCommitFails(t *testing.T) {
buildDir := t.TempDir()
activePath := path.Join(buildDir, nginxModuleCatalogFile)
pendingPath := path.Join(buildDir, nginxModuleCatalogPendingFile)
if err := os.WriteFile(activePath, []byte(`[{"name":"old"}]`), constant.FilePerm); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(pendingPath, []byte(`[{"name":"new"}]`), constant.FilePerm); err != nil {
t.Fatal(err)
}
commitErr := errors.New("save install failed")
err := activateNginxModuleCatalogAndCommit(pendingPath, activePath, func() error {
return commitErr
})
if !errors.Is(err, commitErr) {
t.Fatalf("expected commit error, got %v", err)
}
content, readErr := os.ReadFile(activePath)
if readErr != nil {
t.Fatal(readErr)
}
if string(content) != `[{"name":"old"}]` {
t.Fatalf("failed upgrade must restore the old catalog: %s", content)
}
}
func TestLoadNginxModulesRejectsDuplicateCatalogNames(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{
{Name: "rtmp", Params: "--add-module=/tmp/one", BuildMode: nginxModuleBuildDynamic},
{Name: "rtmp", Params: "--add-module=/tmp/two", BuildMode: nginxModuleBuildDynamic},
}
writeNginxModuleCatalogStateFixture(t, install, catalog, "")
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "duplicate") {
t.Fatalf("expected duplicate catalog name error, got %v", err)
}
}
func TestLoadNginxModulesRejectsDuplicateStateNames(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{{Name: "rtmp", Params: "--add-module=/tmp/rtmp", BuildMode: nginxModuleBuildDynamic}}
writeNginxModuleCatalogStateFixture(t, install, catalog, `[{"name":"rtmp","enable":true},{"name":"rtmp","enable":false}]`)
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "duplicate") {
t.Fatalf("expected duplicate state name error, got %v", err)
}
}
func TestLoadNginxModulesRejectsOrphanBuiltinState(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
writeNginxModuleCatalogStateFixture(t, install, nil, `[{"name":"removed","enable":true}]`)
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "missing from the module catalog") {
t.Fatalf("expected orphan builtin state error, got %v", err)
}
}
func TestLoadNginxModulesRejectsCustomCatalogNameConflict(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{{Name: "rtmp", BuildMode: nginxModuleBuildDynamic}}
writeNginxModuleCatalogStateFixture(t, install, catalog, `[{"name":"rtmp","custom":true,"enable":true}]`)
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "conflicts") {
t.Fatalf("expected custom/catalog conflict error, got %v", err)
}
}
func TestLoadNginxModulesRejectsInvalidBuildModes(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
install := model.AppInstall{Name: "openresty", Version: "1.31.1.1"}
install.App.Key = constant.AppOpenresty
writeNginxModuleCatalogStateFixture(t, install, []dto.NginxModule{{
Name: "rtmp", BuildMode: "auto",
}}, "")
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "invalid build mode") {
t.Fatalf("expected invalid catalog build mode error, got %v", err)
}
writeNginxModuleCatalogStateFixture(t, install, nil, `[{"name":"custom","custom":true,"buildMode":"auto"}]`)
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "invalid build mode") {
t.Fatalf("expected invalid custom build mode error, got %v", err)
}
}
func TestApplyNginxModuleUpdateKeepsBuiltinDefinitionImmutable(t *testing.T) {
modules := []dto.NginxModule{{
Name: "rtmp", Script: "catalog-script", Params: "--add-module=/tmp/rtmp",
BuildMode: nginxModuleBuildDynamic, Provider: nginxModuleProviderLocal, LoadOrder: 20,
}}
req := request.NginxModuleUpdate{
Operate: nginxModuleOperateUpdate, Name: "rtmp", Enable: true, Script: "user-script",
Params: "--with-user", BuildMode: nginxModuleBuildStatic, Provider: "prebuilt", LoadOrder: 99,
}
updated, _, err := applyNginxModuleUpdate(modules, req)
if err != nil {
t.Fatal(err)
}
if !updated[0].Enable {
t.Fatal("builtin enable state was not updated")
}
if updated[0].Script != "catalog-script" || updated[0].Params != "--add-module=/tmp/rtmp" ||
updated[0].BuildMode != nginxModuleBuildDynamic || updated[0].Provider != nginxModuleProviderLocal ||
updated[0].LoadOrder != 20 {
t.Fatalf("builtin definition was modified: %#v", updated[0])
}
}
func TestApplyNginxModuleUpdateRejectsBuiltinDelete(t *testing.T) {
modules := []dto.NginxModule{{Name: "rtmp", BuildMode: nginxModuleBuildDynamic}}
_, _, err := applyNginxModuleUpdate(modules, request.NginxModuleUpdate{
Operate: nginxModuleOperateDelete, Name: "rtmp",
})
if err == nil || !strings.Contains(err.Error(), "cannot be deleted") {
t.Fatalf("expected builtin delete error, got %v", err)
}
}
func TestApplyNginxModuleUpdateCreatesAndDeletesCustomModule(t *testing.T) {
if _, _, err := applyNginxModuleUpdate(nil, request.NginxModuleUpdate{
Operate: nginxModuleOperateCreate, Name: "invalid", BuildMode: "auto",
}); err == nil || !strings.Contains(err.Error(), "invalid build mode") {
t.Fatalf("expected invalid custom build mode error, got %v", err)
}
created, _, err := applyNginxModuleUpdate(nil, request.NginxModuleUpdate{
Operate: nginxModuleOperateCreate, Name: "custom", Script: "prepare", Packages: "git,curl",
Params: "--add-module=/tmp/custom", Enable: true, BuildMode: nginxModuleBuildStatic,
Provider: nginxModuleProviderLocal, LoadOrder: 100,
})
if err != nil {
t.Fatal(err)
}
if len(created) != 1 || !created[0].Custom || created[0].BuildMode != nginxModuleBuildStatic {
t.Fatalf("custom module was not created correctly: %#v", created)
}
updated, _, err := applyNginxModuleUpdate(created, request.NginxModuleUpdate{
Operate: nginxModuleOperateUpdate, Name: "custom", Script: "updated",
Params: "--add-module=/tmp/custom-v2", Enable: false, BuildMode: nginxModuleBuildDynamic,
Provider: nginxModuleProviderLocal, LoadOrder: 75,
})
if err != nil {
t.Fatal(err)
}
if updated[0].Script != "updated" || updated[0].BuildMode != nginxModuleBuildDynamic || updated[0].LoadOrder != 75 {
t.Fatalf("custom module was not updated correctly: %#v", updated[0])
}
remaining, deleted, err := applyNginxModuleUpdate(updated, request.NginxModuleUpdate{
Operate: nginxModuleOperateDelete, Name: "custom",
})
if err != nil {
t.Fatal(err)
}
if len(remaining) != 0 || deleted == nil || deleted.Name != "custom" {
t.Fatalf("custom module was not removed: remaining=%#v deleted=%#v", remaining, deleted)
}
}
+294 -74
View File
@@ -29,6 +29,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/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"
@@ -82,6 +83,47 @@ func NewRuntimeService() IRuntimeService {
return &RuntimeService{} return &RuntimeService{}
} }
var pullRuntimeComposeImages = compose.PullComposeImages
var downRuntimeCompose = compose.Down
type runtimeTaskMeta struct {
resourceName string
operate string
scope string
taskID string
resourceID uint
}
func buildRuntimeCreateTaskMeta(create request.RuntimeCreate, runtimeID uint) runtimeTaskMeta {
return runtimeTaskMeta{
resourceName: create.Name,
operate: task.TaskCreate,
scope: task.TaskScopeRuntime,
taskID: create.TaskID,
resourceID: runtimeID,
}
}
func buildRuntimeDeleteTaskMeta(deleteReq request.RuntimeDelete, runtime *model.Runtime) runtimeTaskMeta {
return runtimeTaskMeta{
resourceName: runtime.Name,
operate: task.TaskDelete,
scope: task.TaskScopeRuntime,
taskID: deleteReq.TaskID,
resourceID: deleteReq.ID,
}
}
func runtimeCreateInitialStatus(create request.RuntimeCreate) string {
if create.Type == constant.RuntimePHP {
if create.Resource == constant.ResourceLocal {
return constant.StatusNormal
}
return constant.StatusBuilding
}
return constant.StatusCreating
}
func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, error) { func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, error) {
var ( var (
opts []repo.DBOption opts []repo.DBOption
@@ -104,20 +146,29 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
return nil, err return nil, err
} }
} }
if create.Type == constant.RuntimePHP && create.Resource == constant.ResourceLocal {
runtime := &model.Runtime{
Name: create.Name,
Resource: create.Resource,
Type: create.Type,
Version: create.Version,
Status: runtimeCreateInitialStatus(create),
Remark: create.Remark,
}
if err := runtimeRepo.Create(context.Background(), runtime); err != nil {
return nil, err
}
if err := startRuntimeCreateTask(create, runtime, model.App{}, model.AppDetail{}, ""); err != nil {
runtime.Status = constant.StatusError
runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime)
return nil, err
}
return runtime, nil
}
var hostPorts []string var hostPorts []string
switch create.Type { switch create.Type {
case constant.RuntimePHP: case constant.RuntimePHP:
if create.Resource == constant.ResourceLocal {
runtime := &model.Runtime{
Name: create.Name,
Resource: create.Resource,
Type: create.Type,
Version: create.Version,
Status: constant.StatusNormal,
Remark: create.Remark,
}
return nil, runtimeRepo.Create(context.Background(), runtime)
}
exist, _ = runtimeRepo.GetFirst(context.Background(), runtimeRepo.WithImage(create.Image)) exist, _ = runtimeRepo.GetFirst(context.Background(), runtimeRepo.WithImage(create.Image))
if exist != nil { if exist != nil {
return nil, buserr.New("ErrImageExist") return nil, buserr.New("ErrImageExist")
@@ -134,10 +185,9 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
if !fileOp.Stat(create.CodeDir) { if !fileOp.Stat(create.CodeDir) {
return nil, buserr.New("ErrPathNotFound") return nil, buserr.New("ErrPathNotFound")
} }
create.Install = true
for _, export := range create.ExposedPorts { for _, export := range create.ExposedPorts {
hostPorts = append(hostPorts, strconv.Itoa(export.HostPort)) hostPorts = append(hostPorts, strconv.Itoa(export.HostPort))
if err := checkPortExist(export.HostPort); err != nil { if err := checkPortExistWithProtocol(export.HostPort, export.Protocol); err != nil {
return nil, err return nil, err
} }
} }
@@ -160,11 +210,6 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
} }
appVersionDir := filepath.Join(app.GetAppResourcePath(), appDetail.Version) appVersionDir := filepath.Join(app.GetAppResourcePath(), appDetail.Version)
if !fileOp.Stat(appVersionDir) {
if err = downloadApp(app, appDetail, nil, nil); err != nil {
return nil, err
}
}
runtime := &model.Runtime{ runtime := &model.Runtime{
Name: create.Name, Name: create.Name,
@@ -176,24 +221,92 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
ContainerName: containerName.(string), ContainerName: containerName.(string),
Port: strings.Join(hostPorts, ","), Port: strings.Join(hostPorts, ","),
Remark: create.Remark, Remark: create.Remark,
} Status: runtimeCreateInitialStatus(create),
switch create.Type {
case constant.RuntimePHP:
if err = handlePHP(create, runtime, fileOp, appVersionDir); err != nil {
return nil, err
}
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDotNet:
if err = handleRuntime(create, runtime, fileOp, appVersionDir); err != nil {
return nil, err
}
} }
if err := runtimeRepo.Create(context.Background(), runtime); err != nil { if err := runtimeRepo.Create(context.Background(), runtime); err != nil {
return nil, err return nil, err
} }
if err := startRuntimeCreateTask(create, runtime, app, appDetail, appVersionDir); err != nil {
runtime.Status = constant.StatusError
runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime)
return nil, err
}
return runtime, nil return runtime, nil
} }
func startRuntimeCreateTask(create request.RuntimeCreate, runtime *model.Runtime, app model.App, appDetail model.AppDetail, appVersionDir string) error {
meta := buildRuntimeCreateTaskMeta(create, runtime.ID)
createTask, err := task.NewTaskWithOps(meta.resourceName, meta.operate, meta.scope, meta.taskID, meta.resourceID)
if err != nil {
return err
}
createTask.AddSubTask(task.GetTaskName(create.Name, task.TaskCreate, task.TaskScopeRuntime), func(t *task.Task) error {
return executeRuntimeCreateTask(create, runtime, app, appDetail, appVersionDir, t)
}, nil)
go func() {
_ = createTask.Execute()
}()
return nil
}
func executeRuntimeCreateTask(create request.RuntimeCreate, runtime *model.Runtime, app model.App, appDetail model.AppDetail, appVersionDir string, taskItem *task.Task) error {
if create.Type == constant.RuntimePHP && create.Resource == constant.ResourceLocal {
runtime.Status = constant.StatusNormal
runtime.Message = ""
return runtimeRepo.Save(runtime)
}
if err := downloadApp(app, appDetail, nil, taskItem.Logger); err != nil {
return markRuntimeCreateFailed(runtime, err)
}
go func() {
RequestDownloadCallBack(appDetail.DownloadCallBackUrl)
}()
fileOp := files.NewFileOp()
var err error
switch create.Type {
case constant.RuntimePHP:
err = handlePHP(create, runtime, fileOp, appVersionDir)
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDotNet:
err = handleRuntime(create, runtime, fileOp, appVersionDir)
}
if err != nil {
return markRuntimeCreateFailed(runtime, err)
}
if err = runtimeRepo.Save(runtime); err != nil {
return err
}
switch create.Type {
case constant.RuntimePHP:
return buildRuntimeWithResult(runtime, "", "", false)
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDotNet:
if err := pullRuntimeImagesBeforeStart(create, runtime, taskItem); err != nil {
return markRuntimeCreateFailed(runtime, err)
}
return startRuntimeWithResult(runtime)
}
return nil
}
func pullRuntimeImagesBeforeStart(create request.RuntimeCreate, runtime *model.Runtime, taskItem *task.Task) error {
switch create.Type {
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDotNet:
return pullRuntimeComposeImages(runtime.GetComposePath(), false, taskItem)
default:
return nil
}
}
func markRuntimeCreateFailed(runtime *model.Runtime, err error) error {
runtime.Status = constant.StatusError
runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime)
return err
}
func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.RuntimeDTO, error) { func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.RuntimeDTO, error) {
var ( var (
opts []repo.DBOption opts []repo.DBOption
@@ -241,30 +354,11 @@ func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.Runt
runtimeDTO.AppID = detail.AppId runtimeDTO.AppID = detail.AppId
} }
for k, v := range envs { for k, v := range envs {
runtimeDTO.Params[k] = v if !isComposePortEnvKey(k) {
if strings.Contains(k, "CONTAINER_PORT") || strings.Contains(k, "HOST_PORT") { runtimeDTO.Params[k] = v
if strings.Contains(k, "CONTAINER_PORT") {
matches := re.GetRegex(re.TrailingDigitsPattern).FindStringSubmatch(k)
if len(matches) < 2 {
continue
}
containerPort, err := strconv.Atoi(v)
if err != nil {
continue
}
hostPort, err := strconv.Atoi(envs[fmt.Sprintf("HOST_PORT_%s", matches[1])])
if err != nil {
continue
}
hostIP := envs[fmt.Sprintf("HOST_IP_%s", matches[1])]
runtimeDTO.ExposedPorts = append(runtimeDTO.ExposedPorts, request.ExposedPort{
ContainerPort: containerPort,
HostPort: hostPort,
HostIP: hostIP,
})
}
} }
} }
runtimeDTO.ExposedPorts, _ = loadComposeExposedPortsFromEnv(envs, "", false)
res = append(res, runtimeDTO) res = append(res, runtimeDTO)
} }
return total, res, nil return total, res, nil
@@ -291,31 +385,37 @@ func (r *RuntimeService) Delete(runtimeDelete request.RuntimeDelete) error {
if website.ID > 0 { if website.ID > 0 {
return buserr.New("ErrDelWithWebsite") return buserr.New("ErrDelWithWebsite")
} }
return startRuntimeDeleteTask(runtimeDelete, runtime)
}
func startRuntimeDeleteTask(runtimeDelete request.RuntimeDelete, runtime *model.Runtime) error {
meta := buildRuntimeDeleteTaskMeta(runtimeDelete, runtime)
deleteTask, err := task.NewTaskWithOps(meta.resourceName, meta.operate, meta.scope, meta.taskID, meta.resourceID)
if err != nil {
return err
}
deleteTask.AddSubTask(task.GetTaskName(runtime.Name, task.TaskDelete, task.TaskScopeRuntime), func(t *task.Task) error {
return executeRuntimeDeleteTask(runtimeDelete, runtime, t)
}, nil)
go func() {
_ = deleteTask.Execute()
}()
return nil
}
func executeRuntimeDeleteTask(runtimeDelete request.RuntimeDelete, runtime *model.Runtime, taskItem *task.Task) error {
if runtime.Resource != constant.ResourceAppstore { if runtime.Resource != constant.ResourceAppstore {
if runtimeDelete.DeleteImage {
deleteRuntimeImages(runtime, taskItem)
}
return runtimeRepo.DeleteBy(repo.WithByID(runtimeDelete.ID)) return runtimeRepo.DeleteBy(repo.WithByID(runtimeDelete.ID))
} }
projectDir := runtime.GetPath() projectDir := runtime.GetPath()
if out, err := compose.Down(runtime.GetComposePath()); err != nil && !runtimeDelete.ForceDelete { if err := stopRuntimeBeforeDelete(runtime, runtimeDelete.ForceDelete, taskItem); err != nil {
if out != "" {
return errors.New(out)
}
return err return err
} }
if runtime.Type == constant.RuntimePHP { if runtimeDelete.DeleteImage {
client, err := docker.NewClient() deleteRuntimeImages(runtime, taskItem)
if err != nil {
return err
}
defer client.Close()
imageID, err := client.GetImageIDByName(runtime.Image)
if err != nil {
return err
}
if imageID != "" {
if err := client.DeleteImage(imageID); err != nil {
global.LOG.Errorf("delete image id [%s] error %v", imageID, err)
}
}
} }
if err := files.NewFileOp().DeleteDir(projectDir); err != nil && !runtimeDelete.ForceDelete { if err := files.NewFileOp().DeleteDir(projectDir); err != nil && !runtimeDelete.ForceDelete {
return err return err
@@ -323,6 +423,121 @@ func (r *RuntimeService) Delete(runtimeDelete request.RuntimeDelete) error {
return runtimeRepo.DeleteBy(repo.WithByID(runtimeDelete.ID)) return runtimeRepo.DeleteBy(repo.WithByID(runtimeDelete.ID))
} }
func getRuntimeStopLog() string {
logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("Runtime")
if strings.TrimSpace(logStr) == "" {
return "StopRuntime"
}
return logStr
}
func stopRuntimeBeforeDelete(runtime *model.Runtime, forceDelete bool, taskItem *task.Task) error {
logStr := getRuntimeStopLog()
if taskItem != nil {
taskItem.Log(logStr)
}
out, err := downRuntimeCompose(runtime.GetComposePath())
if err != nil && !forceDelete {
if out != "" {
err = errors.New(out)
}
if taskItem != nil {
taskItem.LogFailedWithErr(logStr, err)
}
return err
}
if taskItem != nil {
taskItem.LogSuccess(logStr)
}
return nil
}
func getRuntimeDeleteImages(runtime *model.Runtime) ([]string, error) {
var images []string
imageMap := make(map[string]struct{})
appendImage := func(image string) {
image = strings.TrimSpace(image)
if image == "" {
return
}
if _, ok := imageMap[image]; ok {
return
}
imageMap[image] = struct{}{}
images = append(images, image)
}
appendImage(runtime.Image)
if runtime.DockerCompose != "" {
composeImages, err := docker.GetImagesFromDockerCompose([]byte(runtime.Env), []byte(runtime.DockerCompose))
if err != nil {
return nil, err
}
for _, image := range composeImages {
appendImage(image)
}
}
return images, nil
}
func deleteRuntimeImages(runtime *model.Runtime, taskItem *task.Task) {
logPrefix := i18n.GetMsgByKey("TaskDelete") + i18n.GetMsgByKey("Image")
images, err := getRuntimeDeleteImages(runtime)
if err != nil {
global.LOG.Errorf("get runtime [%s] delete images error %v", runtime.Name, err)
if taskItem != nil {
taskItem.LogFailedWithErr(logPrefix, err)
}
return
}
if len(images) == 0 {
if taskItem != nil {
taskItem.LogFailedWithErr(logPrefix, errors.New(i18n.GetWithName("ErrImageNotExist", runtime.Name)))
}
return
}
client, err := docker.NewClient()
if err != nil {
global.LOG.Errorf("delete runtime images [%s] error %v", runtime.Name, err)
if taskItem != nil {
taskItem.LogFailedWithErr(logPrefix, err)
}
return
}
defer client.Close()
for _, imageName := range images {
logStr := logPrefix + imageName
if taskItem != nil {
taskItem.Log(logStr)
}
imageID, err := client.GetImageIDByName(imageName)
if err != nil {
global.LOG.Errorf("get runtime image [%s] error %v", imageName, err)
if taskItem != nil {
taskItem.LogFailedWithErr(logStr, err)
}
continue
}
if imageID == "" {
if taskItem != nil {
taskItem.LogFailedWithErr(logStr, errors.New(i18n.GetWithName("ErrImageNotExist", imageName)))
}
continue
}
if err := client.DeleteImage(imageID); err != nil {
global.LOG.Errorf("delete image id [%s] error %v", imageID, err)
if taskItem != nil {
taskItem.LogFailedWithErr(logStr, err)
}
continue
}
if taskItem != nil {
taskItem.LogSuccess(logStr)
}
}
}
func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) { func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) {
runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(id)) runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(id))
if err != nil { if err != nil {
@@ -435,9 +650,11 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
return buserr.New("ErrImageExist") return buserr.New("ErrImageExist")
} }
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDotNet: case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDotNet:
ownedPortKeys := runtimeOwnedPortKeys(runtime.Env)
for _, export := range req.ExposedPorts { for _, export := range req.ExposedPorts {
hostPorts = append(hostPorts, strconv.Itoa(export.HostPort)) hostPorts = append(hostPorts, strconv.Itoa(export.HostPort))
if err = checkRuntimePortExist(export.HostPort, false, runtime.ID); err != nil { _, owned := ownedPortKeys[composePortCheckKey(export.HostPort, export.Protocol)]
if err = checkRuntimePortExistWithProtocol(export.HostPort, export.Protocol, !owned, runtime.ID); err != nil {
return err return err
} }
} }
@@ -478,7 +695,7 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
Version: req.Version, Version: req.Version,
Remark: req.Remark, Remark: req.Remark,
NodeConfig: request.NodeConfig{ NodeConfig: request.NodeConfig{
Install: true, Install: req.Install,
ExposedPorts: req.ExposedPorts, ExposedPorts: req.ExposedPorts,
Environments: req.Environments, Environments: req.Environments,
Volumes: req.Volumes, Volumes: req.Volumes,
@@ -1058,6 +1275,7 @@ func (r *RuntimeService) UpdatePHPContainer(req request.PHPContainerConfig) erro
var ( var (
composeContent []byte composeContent []byte
) )
ownedPortKeys := runtimeOwnedPortKeys(runtime.Env)
for _, export := range req.ExposedPorts { for _, export := range req.ExposedPorts {
if strconv.Itoa(export.HostPort) == runtime.Port { if strconv.Itoa(export.HostPort) == runtime.Port {
return buserr.WithName("ErrPHPRuntimePortFailed", strconv.Itoa(export.HostPort)) return buserr.WithName("ErrPHPRuntimePortFailed", strconv.Itoa(export.HostPort))
@@ -1065,7 +1283,8 @@ func (r *RuntimeService) UpdatePHPContainer(req request.PHPContainerConfig) erro
if export.ContainerPort == 9000 { if export.ContainerPort == 9000 {
return buserr.New("ErrPHPPortIsDefault") return buserr.New("ErrPHPPortIsDefault")
} }
if err = checkRuntimePortExist(export.HostPort, false, runtime.ID); err != nil { _, owned := ownedPortKeys[composePortCheckKey(export.HostPort, export.Protocol)]
if err = checkRuntimePortExistWithProtocol(export.HostPort, export.Protocol, !owned, runtime.ID); err != nil {
return err return err
} }
} }
@@ -1090,7 +1309,7 @@ func (r *RuntimeService) UpdatePHPContainer(req request.PHPContainerConfig) erro
return err return err
} }
for k := range envs { for k := range envs {
if strings.HasPrefix(k, "CONTAINER_PORT_") || strings.HasPrefix(k, "HOST_PORT_") || strings.HasPrefix(k, "HOST_IP_") || strings.Contains(k, "APP_PORT") { if isComposePortEnvKey(k) || strings.Contains(k, "APP_PORT") {
delete(envs, k) delete(envs, k)
} }
} }
@@ -1102,6 +1321,7 @@ func (r *RuntimeService) UpdatePHPContainer(req request.PHPContainerConfig) erro
ExposedPorts: req.ExposedPorts, ExposedPorts: req.ExposedPorts,
Environments: req.Environments, Environments: req.Environments,
Volumes: req.Volumes, Volumes: req.Volumes,
ExtraHosts: req.ExtraHosts,
}, },
} }
composeContent, err = handleCompose(envs, composeContent, create, projectDir) composeContent, err = handleCompose(envs, composeContent, create, projectDir)
+411
View File
@@ -0,0 +1,411 @@
package service
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"regexp"
"runtime"
stdpprof "runtime/pprof"
"sort"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
profile "github.com/google/pprof/profile"
"github.com/shirou/gopsutil/v4/process"
)
const (
diagnosticsDefaultDuration = 15
diagnosticsMaxProfileSize = 64 * 1024 * 1024
diagnosticsMaxSnapshotSize = 16 * 1024 * 1024
diagnosticsDetailedGoroutines = 10_000
diagnosticsBlockProfileRate = 1_000_000
diagnosticsMaxGroups = 200
)
var (
runtimeDiagnosticsInstance = &RuntimeDiagnosticsService{}
goroutineHeaderPattern = regexp.MustCompile(`^goroutine \d+ \[([^]]+)\]:$`)
goroutineArgPattern = regexp.MustCompile(`0x[0-9a-fA-F]+`)
errProfileSizeLimit = errors.New("runtime profile exceeds the 64 MiB size limit")
)
type cappedWriter struct {
writer io.Writer
remaining int64
exceeded bool
}
func (w *cappedWriter) Write(data []byte) (int, error) {
if int64(len(data)) <= w.remaining {
n, err := w.writer.Write(data)
w.remaining -= int64(n)
return n, err
}
w.exceeded = true
if w.remaining <= 0 {
return 0, errProfileSizeLimit
}
allowed := int(w.remaining)
n, err := w.writer.Write(data[:allowed])
w.remaining -= int64(n)
if err != nil {
return n, err
}
return n, errProfileSizeLimit
}
type IRuntimeDiagnosticsService interface {
Summary() (dto.RuntimeDiagnosticsSummary, error)
Goroutines() (dto.RuntimeGoroutineSnapshot, error)
CreateProfile(req dto.RuntimeProfileCreate) (RuntimeProfileResult, error)
}
type RuntimeProfileResult struct {
Path string
Name string
}
type RuntimeDiagnosticsService struct {
captureMu sync.Mutex
processMu sync.Mutex
process *process.Process
}
func NewIRuntimeDiagnosticsService() IRuntimeDiagnosticsService {
return runtimeDiagnosticsInstance
}
func (s *RuntimeDiagnosticsService) Summary() (dto.RuntimeDiagnosticsSummary, error) {
rss, err := s.processRSS()
if err != nil {
return dto.RuntimeDiagnosticsSummary{}, err
}
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
return dto.RuntimeDiagnosticsSummary{
RSS: rss,
HeapAlloc: stats.HeapAlloc,
HeapObjects: stats.HeapObjects,
Goroutines: runtime.NumGoroutine(),
}, nil
}
func (s *RuntimeDiagnosticsService) Goroutines() (dto.RuntimeGoroutineSnapshot, error) {
total := runtime.NumGoroutine()
if total > diagnosticsDetailedGoroutines {
groups, truncated := compactGoroutineSnapshot(total)
return dto.RuntimeGoroutineSnapshot{
Total: total, GroupCount: len(groups), Truncated: truncated, CapturedAt: time.Now(), Goroutines: groups,
}, nil
}
var data bytes.Buffer
writer := &cappedWriter{writer: &data, remaining: diagnosticsMaxSnapshotSize}
if err := stdpprof.Lookup("goroutine").WriteTo(writer, 2); err != nil {
groups, truncated := compactGoroutineSnapshot(total)
return dto.RuntimeGoroutineSnapshot{
Total: total, GroupCount: len(groups), Truncated: truncated, CapturedAt: time.Now(), Goroutines: groups,
}, nil
}
groups, truncated := parseGoroutineDump(&data, diagnosticsMaxGroups)
result := dto.RuntimeGoroutineSnapshot{
Total: total,
GroupCount: len(groups),
Truncated: truncated,
CapturedAt: time.Now(),
}
result.Goroutines = groups
return result, nil
}
func (s *RuntimeDiagnosticsService) CreateProfile(req dto.RuntimeProfileCreate) (RuntimeProfileResult, error) {
if !s.captureMu.TryLock() {
return RuntimeProfileResult{}, errors.New("another runtime profile is being captured")
}
defer s.captureMu.Unlock()
return captureRuntimeProfile(req)
}
func captureRuntimeProfile(req dto.RuntimeProfileCreate) (result RuntimeProfileResult, err error) {
duration := req.Duration
if duration == 0 {
duration = diagnosticsDefaultDuration
}
if duration < 5 || duration > 30 {
return result, errors.New("profile duration must be between 5 and 30 seconds")
}
if req.Type == "heap" || req.Type == "goroutine" {
duration = 0
}
name := fmt.Sprintf("%s-%s-%ds.pb.gz", req.Type, newRuntimeEventID(), duration)
file, err := os.CreateTemp("", "1panel-runtime-profile-*.tmp")
if err != nil {
return result, err
}
writer := &cappedWriter{writer: file, remaining: diagnosticsMaxProfileSize}
removeOnError := true
defer func() {
_ = file.Close()
if removeOnError {
_ = os.Remove(file.Name())
}
}()
switch req.Type {
case "cpu":
if err = stdpprof.StartCPUProfile(writer); err != nil {
return result, err
}
time.Sleep(time.Duration(duration) * time.Second)
stdpprof.StopCPUProfile()
case "heap":
err = stdpprof.Lookup("heap").WriteTo(writer, 0)
case "goroutine":
err = stdpprof.Lookup("goroutine").WriteTo(writer, 0)
case "mutex":
err = captureWindowedRuntimeProfile("mutex", duration, writer, func() func() {
previous := runtime.SetMutexProfileFraction(5)
return func() { runtime.SetMutexProfileFraction(previous) }
})
case "block":
err = captureWindowedRuntimeProfile("block", duration, writer, func() func() {
runtime.SetBlockProfileRate(diagnosticsBlockProfileRate)
return func() { runtime.SetBlockProfileRate(0) }
})
default:
return result, errors.New("unsupported runtime profile type")
}
if err != nil {
return result, err
}
if writer.exceeded {
return result, errProfileSizeLimit
}
if err = file.Close(); err != nil {
return result, err
}
removeOnError = false
return RuntimeProfileResult{Path: file.Name(), Name: name}, nil
}
func captureWindowedRuntimeProfile(name string, duration int, writer io.Writer, enable func() func()) error {
restore := enable()
sampling := true
defer func() {
if sampling {
restore()
}
}()
before, err := readRuntimeProfile(name)
if err != nil {
return err
}
startedAt := time.Now()
time.Sleep(time.Duration(duration) * time.Second)
restore()
sampling = false
after, err := readRuntimeProfile(name)
if err != nil {
return err
}
delta, err := diffRuntimeProfiles(before, after, startedAt, time.Duration(duration)*time.Second)
if err != nil {
return err
}
return delta.Write(writer)
}
func readRuntimeProfile(name string) (*profile.Profile, error) {
var data bytes.Buffer
writer := &cappedWriter{writer: &data, remaining: diagnosticsMaxProfileSize}
if err := stdpprof.Lookup(name).WriteTo(writer, 0); err != nil {
return nil, err
}
if writer.exceeded {
return nil, errProfileSizeLimit
}
return profile.Parse(&data)
}
func diffRuntimeProfiles(before, after *profile.Profile, startedAt time.Time, duration time.Duration) (*profile.Profile, error) {
baseline := before.Copy()
baseline.Scale(-1)
delta, err := profile.Merge([]*profile.Profile{after, baseline})
if err != nil {
return nil, err
}
delta.TimeNanos = startedAt.UnixNano()
delta.DurationNanos = duration.Nanoseconds()
return delta, nil
}
func (s *RuntimeDiagnosticsService) processRSS() (uint64, error) {
s.processMu.Lock()
defer s.processMu.Unlock()
if s.process == nil {
proc, err := process.NewProcess(int32(os.Getpid()))
if err != nil {
return 0, err
}
s.process = proc
}
memoryInfo, err := s.process.MemoryInfo()
if err != nil {
return 0, err
}
if memoryInfo == nil {
return 0, errors.New("process memory information is unavailable")
}
return memoryInfo.RSS, nil
}
func newRuntimeEventID() string {
return time.Now().Format("20060102-150405.000000000")
}
func parseGoroutineDump(reader io.Reader, maxGroups int) ([]dto.RuntimeGoroutineGroup, bool) {
type groupValue struct {
state string
top string
stack []string
count int
}
groups := make(map[string]*groupValue)
truncated := false
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 64*1024), 4*1024*1024)
var block []string
flush := func() {
if len(block) == 0 {
return
}
matches := goroutineHeaderPattern.FindStringSubmatch(block[0])
state := "unknown"
if len(matches) == 2 {
state = matches[1]
}
stack := append([]string(nil), block[1:]...)
if len(stack) > 40 {
stack = stack[:40]
}
top := "runtime"
functionLines := make([]string, 0, len(stack)/2)
for _, line := range stack {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "created by ") {
continue
}
normalized := goroutineArgPattern.ReplaceAllString(trimmed, "0x…")
functionLines = append(functionLines, normalized)
if top == "runtime" {
top = strings.Split(normalized, "(")[0]
}
}
signature := state + "\n" + strings.Join(functionLines, "\n")
if existing, ok := groups[signature]; ok {
existing.count++
return
}
if len(groups) >= maxGroups {
truncated = true
return
}
groups[signature] = &groupValue{state: state, top: top, stack: stack, count: 1}
}
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "goroutine ") && strings.HasSuffix(line, "]:") {
flush()
block = []string{line}
continue
}
if len(block) > 0 {
block = append(block, line)
}
}
if scanner.Err() != nil {
truncated = true
}
flush()
result := make([]dto.RuntimeGoroutineGroup, 0, len(groups))
for _, group := range groups {
result = append(result, dto.RuntimeGoroutineGroup{State: group.state, Top: group.top, Count: group.count, Stack: group.stack})
}
sort.Slice(result, func(i, j int) bool {
if result[i].Count == result[j].Count {
return result[i].Top < result[j].Top
}
return result[i].Count > result[j].Count
})
return result, truncated
}
func compactGoroutineSnapshot(initialSize int) ([]dto.RuntimeGoroutineGroup, bool) {
records := make([]runtime.StackRecord, initialSize+32)
count, ok := runtime.GoroutineProfile(records)
if !ok {
records = make([]runtime.StackRecord, count+32)
count, ok = runtime.GoroutineProfile(records)
}
truncated := !ok
if count > len(records) {
count = len(records)
truncated = true
}
records = records[:count]
type compactGroup struct {
top string
stack []string
count int
}
groups := make(map[string]*compactGroup)
for _, record := range records {
frames := runtime.CallersFrames(record.Stack())
stack := make([]string, 0, 16)
functions := make([]string, 0, 8)
top := "runtime"
for {
frame, more := frames.Next()
if frame.Function != "" {
if top == "runtime" {
top = frame.Function
}
functions = append(functions, frame.Function)
stack = append(stack, frame.Function, fmt.Sprintf("\t%s:%d", frame.File, frame.Line))
}
if !more || len(functions) >= 20 {
break
}
}
signature := strings.Join(functions, "\n")
if existing, exists := groups[signature]; exists {
existing.count++
continue
}
if len(groups) >= diagnosticsMaxGroups {
truncated = true
continue
}
groups[signature] = &compactGroup{top: top, stack: stack, count: 1}
}
result := make([]dto.RuntimeGoroutineGroup, 0, len(groups))
for _, group := range groups {
result = append(result, dto.RuntimeGoroutineGroup{State: "profiled", Top: group.top, Count: group.count, Stack: group.stack})
}
sort.Slice(result, func(i, j int) bool {
if result[i].Count == result[j].Count {
return result[i].Top < result[j].Top
}
return result[i].Count > result[j].Count
})
return result, truncated
}
+79 -68
View File
@@ -19,7 +19,6 @@ import (
cmd2 "github.com/1Panel-dev/1Panel/agent/utils/cmd" cmd2 "github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/i18n" "github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/app/dto/request"
@@ -33,7 +32,6 @@ import (
"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/files" "github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/subosito/gotenv" "github.com/subosito/gotenv"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@@ -67,20 +65,13 @@ func handleRuntime(create request.RuntimeCreate, runtime *model.Runtime, fileOp
runtime.Env = string(envContent) runtime.Env = string(envContent)
runtime.Status = constant.StatusCreating runtime.Status = constant.StatusCreating
runtime.CodeDir = create.CodeDir runtime.CodeDir = create.CodeDir
nodeDetail, err := appDetailRepo.GetFirst(repo.WithByID(runtime.AppDetailID))
if err != nil {
return err
}
go func() {
RequestDownloadCallBack(nodeDetail.DownloadCallBackUrl)
}()
go startRuntime(runtime)
return return
} }
func runtimeNodeShouldInstallDependencies(create request.RuntimeCreate) bool {
return create.NodeConfig.Install == nil || *create.NodeConfig.Install
}
func isPathInsideOrEqual(baseDir, targetDir string) bool { func isPathInsideOrEqual(baseDir, targetDir string) bool {
baseAbs := resolveRuntimePath(baseDir) baseAbs := resolveRuntimePath(baseDir)
targetAbs := resolveRuntimePath(targetDir) targetAbs := resolveRuntimePath(targetDir)
@@ -131,32 +122,28 @@ func handlePHP(create request.RuntimeCreate, runtime *model.Runtime, fileOp file
runtime.Env = string(envContent) runtime.Env = string(envContent)
runtime.Params = string(forms) runtime.Params = string(forms)
runtime.Status = constant.StatusBuilding runtime.Status = constant.StatusBuilding
go func() {
appDetail, err := appDetailRepo.GetFirst(repo.WithByID(runtime.AppDetailID))
if err == nil {
RequestDownloadCallBack(appDetail.DownloadCallBackUrl)
}
}()
go buildRuntime(runtime, "", "", false)
return return
} }
func startRuntime(runtime *model.Runtime) { func startRuntime(runtime *model.Runtime) {
_ = startRuntimeWithResult(runtime)
}
func startRuntimeWithResult(runtime *model.Runtime) error {
if err := runComposeCmdWithLog("up", runtime.GetComposePath(), runtime.GetLogPath()); err != nil { if err := runComposeCmdWithLog("up", runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
runtime.Status = constant.StatusError runtime.Status = constant.StatusError
runtime.Message = err.Error() runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime) _ = runtimeRepo.Save(runtime)
return return err
} }
if err := SyncRuntimeContainerStatus(runtime); err != nil { if err := SyncRuntimeContainerStatus(runtime); err != nil {
runtime.Status = constant.StatusError runtime.Status = constant.StatusError
runtime.Message = err.Error() runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime) _ = runtimeRepo.Save(runtime)
return return err
} }
return nil
} }
func reCreateRuntime(runtime *model.Runtime) { func reCreateRuntime(runtime *model.Runtime) {
@@ -328,6 +315,10 @@ func deleteImageByID(oldImageID, imageName string, client docker.Client) {
} }
func buildRuntime(runtime *model.Runtime, oldImageID string, oldEnv string, rebuild bool) { func buildRuntime(runtime *model.Runtime, oldImageID string, oldEnv string, rebuild bool) {
_ = buildRuntimeWithResult(runtime, oldImageID, oldEnv, rebuild)
}
func buildRuntimeWithResult(runtime *model.Runtime, oldImageID string, oldEnv string, rebuild bool) error {
runtimePath := runtime.GetPath() runtimePath := runtime.GetPath()
composePath := runtime.GetComposePath() composePath := runtime.GetComposePath()
logPath := path.Join(runtimePath, "build.log") logPath := path.Join(runtimePath, "build.log")
@@ -335,11 +326,17 @@ func buildRuntime(runtime *model.Runtime, oldImageID string, oldEnv string, rebu
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, constant.FilePerm) logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, constant.FilePerm)
if err != nil { if err != nil {
global.LOG.Errorf("failed to open log file: %v", err) global.LOG.Errorf("failed to open log file: %v", err)
return runtime.Status = constant.StatusError
runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime)
return err
} }
defer func() { defer func() {
_ = logFile.Close() _ = logFile.Close()
}() }()
defer func() {
_ = runtimeRepo.Save(runtime)
}()
newPHPVersion := getRuntimeEnv(runtime.Env, "PHP_VERSION") newPHPVersion := getRuntimeEnv(runtime.Env, "PHP_VERSION")
oldPHPVersion := getRuntimeEnv(oldEnv, "PHP_VERSION") oldPHPVersion := getRuntimeEnv(oldEnv, "PHP_VERSION")
@@ -371,16 +368,19 @@ func buildRuntime(runtime *model.Runtime, oldImageID string, oldEnv string, rebu
} else { } else {
runtime.Message = buserr.New("ErrImageBuildErr").Error() + ":" + stderrBuf.String() runtime.Message = buserr.New("ErrImageBuildErr").Error() + ":" + stderrBuf.String()
} }
_ = runtimeRepo.Save(runtime) return err
return
} }
if err = runComposeCmdWithLog(constant.RuntimeDown, runtime.GetComposePath(), runtime.GetLogPath()); err != nil { if err = runComposeCmdWithLog(constant.RuntimeDown, runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
return runtime.Status = constant.StatusError
runtime.Message = err.Error()
return err
} }
client, err := docker.NewClient() client, err := docker.NewClient()
if err != nil { if err != nil {
_, _ = logFile.WriteString(fmt.Sprintf("failed to connect to docker client: %v", err)) _, _ = logFile.WriteString(fmt.Sprintf("failed to connect to docker client: %v", err))
return runtime.Status = constant.StatusError
runtime.Message = err.Error()
return err
} }
runtime.Message = "" runtime.Message = ""
if rebuild && runtime.ID > 0 { if rebuild && runtime.ID > 0 {
@@ -406,14 +406,10 @@ func buildRuntime(runtime *model.Runtime, oldImageID string, oldEnv string, rebu
} }
} }
defer func() {
_ = runtimeRepo.Save(runtime)
}()
if out, err := compose.Up(composePath); err != nil { if out, err := compose.Up(composePath); err != nil {
runtime.Status = constant.StatusStartErr runtime.Status = constant.StatusStartErr
runtime.Message = out runtime.Message = out
return return err
} }
deleteImageID := "" deleteImageID := ""
extensions := getRuntimeEnv(runtime.Env, "PHP_EXTENSIONS") extensions := getRuntimeEnv(runtime.Env, "PHP_EXTENSIONS")
@@ -423,14 +419,14 @@ func buildRuntime(runtime *model.Runtime, oldImageID string, oldEnv string, rebu
if err = cmdMgr.Run("docker", "exec", "-i", runtime.ContainerName, "install-ext", extensions); err != nil { if err = cmdMgr.Run("docker", "exec", "-i", runtime.ContainerName, "install-ext", extensions); err != nil {
runtime.Status = constant.StatusError runtime.Status = constant.StatusError
runtime.Message = buserr.New("ErrImageBuildErr").Error() + ":" + err.Error() runtime.Message = buserr.New("ErrImageBuildErr").Error() + ":" + err.Error()
return return err
} }
commitMgr := cmd2.NewCommandMgr(cmd2.WithTimeout(10*time.Minute), cmd2.WithOutputFile(logPath)) commitMgr := cmd2.NewCommandMgr(cmd2.WithTimeout(10*time.Minute), cmd2.WithOutputFile(logPath))
err = commitMgr.Run("docker", "commit", runtime.ContainerName, runtime.Image) err = commitMgr.Run("docker", "commit", runtime.ContainerName, runtime.Image)
if err != nil { if err != nil {
runtime.Status = constant.StatusError runtime.Status = constant.StatusError
runtime.Message = buserr.New("ErrImageBuildErr").Error() + ":" + err.Error() runtime.Message = buserr.New("ErrImageBuildErr").Error() + ":" + err.Error()
return return err
} }
} }
if oldImageID != "" { if oldImageID != "" {
@@ -443,10 +439,10 @@ func buildRuntime(runtime *model.Runtime, oldImageID string, oldEnv string, rebu
if out, err := compose.DownAndUp(composePath); err != nil { if out, err := compose.DownAndUp(composePath); err != nil {
runtime.Status = constant.StatusStartErr runtime.Status = constant.StatusStartErr
runtime.Message = out runtime.Message = out
return return err
} }
runtime.Status = constant.StatusRunning runtime.Status = constant.StatusRunning
_ = runtimeRepo.Save(runtime) return nil
} }
func handleParams(create request.RuntimeCreate, projectDir string) (composeContent []byte, envContent []byte, forms []byte, err error) { func handleParams(create request.RuntimeCreate, projectDir string) (composeContent []byte, envContent []byte, forms []byte, err error) {
@@ -464,7 +460,7 @@ func handleParams(create request.RuntimeCreate, projectDir string) (composeConte
return return
} }
for k := range env { for k := range env {
if strings.HasPrefix(k, "CONTAINER_PORT_") || strings.HasPrefix(k, "HOST_PORT_") || strings.HasPrefix(k, "HOST_IP_") || strings.Contains(k, "APP_PORT") { if isComposePortEnvKey(k) || strings.Contains(k, "APP_PORT") {
delete(env, k) delete(env, k)
} }
} }
@@ -510,7 +506,7 @@ func handleParams(create request.RuntimeCreate, projectDir string) (composeConte
case constant.RuntimeNode: case constant.RuntimeNode:
create.Params["CODE_DIR"] = create.CodeDir create.Params["CODE_DIR"] = create.CodeDir
create.Params["NODE_VERSION"] = create.Version create.Params["NODE_VERSION"] = create.Version
if create.NodeConfig.Install { if runtimeNodeShouldInstallDependencies(create) {
create.Params["RUN_INSTALL"] = "1" create.Params["RUN_INSTALL"] = "1"
} else { } else {
create.Params["RUN_INSTALL"] = "0" create.Params["RUN_INSTALL"] = "0"
@@ -621,15 +617,16 @@ func handleCompose(env gotenv.Env, composeContent []byte, create request.Runtime
if len(create.ExposedPorts) > 0 { if len(create.ExposedPorts) > 0 {
var ports []interface{} var ports []interface{}
for i, port := range create.ExposedPorts { for i, port := range create.ExposedPorts {
containerPortStr := fmt.Sprintf("CONTAINER_PORT_%d", i) containerPortStr, hostPortStr, hostIPStr, protocolStr := composePortEnvKeys(i)
hostPortStr := fmt.Sprintf("HOST_PORT_%d", i)
existMap[containerPortStr] = struct{}{} existMap[containerPortStr] = struct{}{}
existMap[hostPortStr] = struct{}{} existMap[hostPortStr] = struct{}{}
hostIPStr := fmt.Sprintf("HOST_IP_%d", i) existMap[hostIPStr] = struct{}{}
ports = append(ports, fmt.Sprintf("${%s}:${%s}:${%s}", hostIPStr, hostPortStr, containerPortStr)) existMap[protocolStr] = struct{}{}
ports = append(ports, formatComposePortMapping(hostIPStr, hostPortStr, containerPortStr, port.Protocol))
create.Params[containerPortStr] = port.ContainerPort create.Params[containerPortStr] = port.ContainerPort
create.Params[hostPortStr] = port.HostPort create.Params[hostPortStr] = port.HostPort
create.Params[hostIPStr] = port.HostIP create.Params[hostIPStr] = port.HostIP
create.Params[protocolStr] = normalizeComposeProtocol(port.Protocol)
} }
if create.Type == constant.RuntimePHP { if create.Type == constant.RuntimePHP {
ports = append(ports, "127.0.0.1:${PANEL_APP_PORT_HTTP}:9000") ports = append(ports, "127.0.0.1:${PANEL_APP_PORT_HTTP}:9000")
@@ -662,7 +659,7 @@ func handleCompose(env gotenv.Env, composeContent []byte, create request.Runtime
volumes = append(volumes, fmt.Sprintf("%s:%s", k, v)) volumes = append(volumes, fmt.Sprintf("%s:%s", k, v))
} }
for _, volume := range create.Volumes { for _, volume := range create.Volumes {
volumes = append(volumes, fmt.Sprintf("%s:%s", volume.Source, volume.Target)) volumes = append(volumes, formatComposeVolume(volume.Source, volume.Target, volume.Mode))
} }
var extraHosts []interface{} var extraHosts []interface{}
@@ -678,7 +675,7 @@ func handleCompose(env gotenv.Env, composeContent []byte, create request.Runtime
break break
} }
for k := range env { for k := range env {
if strings.Contains(k, "CONTAINER_PORT_") || strings.Contains(k, "HOST_PORT_") { if isComposePortEnvKey(k) {
if _, ok := existMap[k]; !ok { if _, ok := existMap[k]; !ok {
delete(env, k) delete(env, k)
} }
@@ -872,9 +869,14 @@ func getDockerComposeVolumes(yml []byte) ([]request.Volume, error) {
if len(envArray) > 1 { if len(envArray) > 1 {
target = envArray[1] target = envArray[1]
} }
mode := ""
if len(envArray) > 2 {
mode = envArray[2]
}
res = append(res, request.Volume{ res = append(res, request.Volume{
Source: source, Source: source,
Target: target, Target: target,
Mode: normalizeComposeVolumeMode(mode),
}) })
} }
} }
@@ -908,7 +910,31 @@ func getDockerComposeExtraHosts(yml []byte) ([]request.ExtraHost, error) {
return res, nil return res, nil
} }
func composePortCheckKey(port int, protocol string) string {
return fmt.Sprintf("%d/%s", port, normalizeComposeProtocol(protocol))
}
func runtimeOwnedPortKeys(envStr string) map[string]struct{} {
portKeys := make(map[string]struct{})
envs, err := gotenv.Unmarshal(envStr)
if err != nil {
return portKeys
}
exposedPorts, err := loadComposeExposedPortsFromEnv(envs, "", false)
if err != nil {
return portKeys
}
for _, port := range exposedPorts {
portKeys[composePortCheckKey(port.HostPort, port.Protocol)] = struct{}{}
}
return portKeys
}
func checkRuntimePortExist(port int, scanPort bool, runtimeID uint) error { func checkRuntimePortExist(port int, scanPort bool, runtimeID uint) error {
return checkRuntimePortExistWithProtocol(port, "", scanPort, runtimeID)
}
func checkRuntimePortExistWithProtocol(port int, protocol string, scanPort bool, runtimeID uint) error {
errMap := make(map[string]interface{}) errMap := make(map[string]interface{})
errMap["port"] = port errMap["port"] = port
appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port)) appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port))
@@ -933,7 +959,7 @@ func checkRuntimePortExist(port int, scanPort bool, runtimeID uint) error {
errMap["name"] = domain.Domain errMap["name"] = domain.Domain
return buserr.WithMap("ErrPortExist", errMap, nil) return buserr.WithMap("ErrPortExist", errMap, nil)
} }
if scanPort && common.ScanPort(port) { if scanPort && isPortInUse(port, protocol) {
return buserr.WithDetail("ErrPortInUsed", port, nil) return buserr.WithDetail("ErrPortInUsed", port, nil)
} }
return nil return nil
@@ -1006,31 +1032,16 @@ func handleRuntimeDTO(res *response.RuntimeDTO, runtime model.Runtime) error {
return err return err
} }
for k, v := range envs { for k, v := range envs {
if strings.Contains(k, "CONTAINER_PORT") || strings.Contains(k, "HOST_PORT") { if isComposePortEnvKey(k) {
if strings.Contains(k, "CONTAINER_PORT") { continue
matches := re.GetRegex(re.TrailingDigitsPattern).FindStringSubmatch(k)
if len(matches) < 2 {
return fmt.Errorf("invalid container port key: %s", k)
}
containerPort, err := strconv.Atoi(v)
if err != nil {
return err
}
hostPort, err := strconv.Atoi(envs[fmt.Sprintf("HOST_PORT_%s", matches[1])])
if err != nil {
return err
}
hostIP := envs[fmt.Sprintf("HOST_IP_%s", matches[1])]
res.ExposedPorts = append(res.ExposedPorts, request.ExposedPort{
ContainerPort: containerPort,
HostPort: hostPort,
HostIP: hostIP,
})
}
} else { } else {
res.Params[k] = v res.Params[k] = v
} }
} }
res.ExposedPorts, err = loadComposeExposedPortsFromEnv(envs, "", true)
if err != nil {
return err
}
if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok { if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok {
res.Source = v res.Source = v
} }
+53 -5
View File
@@ -193,16 +193,20 @@ func handleSnapshot(req dto.SnapshotCreate, taskItem *task.Task, jobID, retry, t
req.InterruptStep = "" req.InterruptStep = ""
} }
if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapUpload" { if len(req.InterruptStep) == 0 || req.InterruptStep == "SnapUpload" {
uploadTimeout := time.Duration(timeout) * time.Second
taskItem.AddSubTaskWithAliasAndOps( taskItem.AddSubTaskWithAliasAndOps(
"SnapUpload", "SnapUpload",
func(t *task.Task) error { func(t *task.Task) error {
return snapUpload(itemHelper, req.SourceAccountIDs, req.DownloadAccountID, retry, fmt.Sprintf("%s.tar.gz", rootDir)) return snapUpload(itemHelper, req.SourceAccountIDs, req.DownloadAccountID, retry, fmt.Sprintf("%s.tar.gz", rootDir), uploadTimeout)
}, nil, int(retry), time.Duration(timeout)*time.Second, }, nil, int(retry), uploadTimeout,
) )
req.InterruptStep = "" req.InterruptStep = ""
} }
if err := taskItem.Execute(); err != nil { if err := taskItem.Execute(); err != nil {
_ = snapshotRepo.Update(req.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error(), "interrupt_step": taskItem.Task.CurrentStep}) _ = snapshotRepo.Update(req.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error(), "interrupt_step": taskItem.Task.CurrentStep})
if jobID != 0 {
cleanupFailedCronjobSnapshot(req, rootDir)
}
return err return err
} }
_ = snapshotRepo.Update(req.ID, map[string]interface{}{"status": constant.StatusSuccess, "interrupt_step": ""}) _ = snapshotRepo.Update(req.ID, map[string]interface{}{"status": constant.StatusSuccess, "interrupt_step": ""})
@@ -210,6 +214,29 @@ func handleSnapshot(req dto.SnapshotCreate, taskItem *task.Task, jobID, retry, t
return nil return nil
} }
func cleanupFailedCronjobSnapshot(req dto.SnapshotCreate, rootDir string) {
if err := os.RemoveAll(rootDir); err != nil {
global.LOG.Errorf("remove failed cronjob snapshot directory %s failed, err: %v", rootDir, err)
}
fileName := path.Base(rootDir) + ".tar.gz"
filePath := path.Join("system_snapshot", fileName)
if err := os.Remove(path.Join(global.Dir.LocalBackupDir, "tmp/system", fileName)); err != nil && !os.IsNotExist(err) {
global.LOG.Errorf("remove failed cronjob snapshot file %s failed, err: %v", filePath, err)
}
accounts := NewBackupClientMap(strings.Split(req.SourceAccountIDs, ","))
for _, account := range accounts {
if !account.isOk {
global.LOG.Errorf("remove failed cronjob snapshot file %s from %s failed, err: %s", filePath, account.name, account.message)
continue
}
if _, err := account.client.Delete(path.Join(account.backupPath, filePath)); err != nil {
global.LOG.Errorf("remove failed cronjob snapshot file %s from %s failed, err: %v", filePath, account.name, err)
}
}
}
type snapHelper struct { type snapHelper struct {
SnapID uint SnapID uint
SnapName string SnapName string
@@ -561,15 +588,36 @@ func snapCompress(snap snapHelper, rootDir string, secret string) error {
return nil return nil
} }
func snapUpload(snap snapHelper, accounts string, downloadID, retry uint, file string) error { func snapUpload(snap snapHelper, accounts string, downloadID, retry uint, file string, timeout time.Duration) error {
snap.Task.Log("---------------------- 8 / 8 ----------------------") snap.Task.Log("---------------------- 8 / 8 ----------------------")
snap.Task.LogStart(i18n.GetMsgByKey("SnapUpload")) snap.Task.LogStart(i18n.GetMsgByKey("SnapUpload"))
src := path.Join(global.Dir.LocalBackupDir, "tmp/system", path.Base(file)) src := path.Join(global.Dir.LocalBackupDir, "tmp/system", path.Base(file))
dst := path.Join("system_snapshot", path.Base(file)) dst := path.Join("system_snapshot", path.Base(file))
accountMap := NewBackupClientMap(strings.Split(accounts, ",")) ctx := context.Background()
if timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
downloadAccount := fmt.Sprintf("%d", downloadID)
accountMap := NewBackupClientMapWithContext(ctx, []string{downloadAccount})
if !accountMap[fmt.Sprintf("%d", downloadID)].isOk { if !accountMap[fmt.Sprintf("%d", downloadID)].isOk {
return buserr.New(i18n.GetMsgWithDetail("LoadBackupFailed", accountMap[fmt.Sprintf("%d", downloadID)].message)) return buserr.New(i18n.GetMsgWithDetail("LoadBackupFailed", accountMap[fmt.Sprintf("%d", downloadID)].message))
} }
return uploadWithMap(snap.Task, accountMap, src, dst, accounts, downloadID, retry) remainingAccounts := make([]string, 0)
for _, account := range strings.Split(accounts, ",") {
if account == "" || account == downloadAccount {
continue
}
remainingAccounts = append(remainingAccounts, account)
}
if err := uploadWithMapWithContext(ctx, snap.Task, accountMap, src, dst, downloadAccount, downloadID, retry, false, len(remainingAccounts) == 0); err != nil {
return err
}
if len(remainingAccounts) == 0 {
return nil
}
optionalMap := NewBackupClientMapWithContext(ctx, remainingAccounts)
return uploadWithMapWithContext(ctx, snap.Task, optionalMap, src, dst, strings.Join(remainingAccounts, ","), downloadID, retry, false, true)
} }
+53 -2
View File
@@ -52,6 +52,7 @@ type ISSHService interface {
LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error)
ExportLog(ctx *gin.Context, req dto.SearchSSHLog) (string, error) ExportLog(ctx *gin.Context, req dto.SearchSSHLog) (string, error)
CleanLog() error
SyncRootCert() error SyncRootCert() error
CreateRootCert(req dto.RootCertOperate) error CreateRootCert(req dto.RootCertOperate) error
@@ -658,6 +659,15 @@ type sshFileItem struct {
Year int Year int
} }
func isSSHLogFileName(name string) bool {
for _, baseName := range []string{"auth.log", "secure"} {
if name == baseName || strings.HasPrefix(name, baseName+".") || strings.HasPrefix(name, baseName+"-") {
return true
}
}
return false
}
func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error) { func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error) {
var fileList []sshFileItem var fileList []sshFileItem
var data []dto.SSHHistory var data []dto.SSHHistory
@@ -667,10 +677,16 @@ func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []d
return 0, data, err return 0, data, err
} }
for _, item := range fileItems { for _, item := range fileItems {
if item.IsDir() || (!strings.HasPrefix(item.Name(), "secure") && !strings.HasPrefix(item.Name(), "auth.log")) { if item.IsDir() || !isSSHLogFileName(item.Name()) {
continue
}
info, err := item.Info()
if err != nil {
return 0, data, err
}
if !info.Mode().IsRegular() {
continue continue
} }
info, _ := item.Info()
itemPath := path.Join(baseDir, info.Name()) itemPath := path.Join(baseDir, info.Name())
if strings.HasSuffix(itemPath, ".gz") { if strings.HasSuffix(itemPath, ".gz") {
if _, err := os.Stat(strings.TrimSuffix(itemPath, ".gz")); err == nil { if _, err := os.Stat(strings.TrimSuffix(itemPath, ".gz")); err == nil {
@@ -725,6 +741,41 @@ func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []d
return int64(total), data, nil return int64(total), data, nil
} }
func (u *SSHService) CleanLog() error {
return cleanSSHLogFiles("/var/log")
}
func cleanSSHLogFiles(baseDir string) error {
fileItems, err := os.ReadDir(baseDir)
if err != nil {
return err
}
for _, item := range fileItems {
if item.IsDir() || !isSSHLogFileName(item.Name()) {
continue
}
info, err := item.Info()
if err != nil {
return err
}
if !info.Mode().IsRegular() {
continue
}
itemPath := path.Join(baseDir, item.Name())
if item.Name() == "auth.log" || item.Name() == "secure" {
if err := os.Truncate(itemPath, 0); err != nil {
return err
}
continue
}
if err := os.Remove(itemPath); err != nil {
return err
}
}
return nil
}
func (u *SSHService) ExportLog(ctx *gin.Context, req dto.SearchSSHLog) (string, error) { func (u *SSHService) ExportLog(ctx *gin.Context, req dto.SearchSSHLog) (string, error) {
_, logs, err := u.LoadLog(ctx, req) _, logs, err := u.LoadLog(ctx, req)
if err != nil { if err != nil {
+8 -38
View File
@@ -23,7 +23,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/env" "github.com/1Panel-dev/1Panel/agent/utils/env"
"github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/re"
) )
type TensorRTLLMService struct{} type TensorRTLLMService struct{}
@@ -59,33 +58,7 @@ func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch) response.TensorR
serverDTO.Image = envs["IMAGE"] serverDTO.Image = envs["IMAGE"]
serverDTO.Command = getCommand(item.Env) serverDTO.Command = getCommand(item.Env)
for k, v := range envs { serverDTO.ExposedPorts, _ = loadComposeExposedPortsFromEnv(envs, "0.0.0.0", false)
if strings.Contains(k, "CONTAINER_PORT") || strings.Contains(k, "HOST_PORT") {
if strings.Contains(k, "CONTAINER_PORT") {
matches := re.GetRegex(re.TrailingDigitsPattern).FindStringSubmatch(k)
if len(matches) < 2 {
continue
}
containerPort, err := strconv.Atoi(v)
if err != nil {
continue
}
hostPort, err := strconv.Atoi(envs[fmt.Sprintf("HOST_PORT_%s", matches[1])])
if err != nil {
continue
}
hostIP := envs[fmt.Sprintf("HOST_IP_%s", matches[1])]
if hostIP == "" {
hostIP = "0.0.0.0"
}
serverDTO.ExposedPorts = append(serverDTO.ExposedPorts, request.ExposedPort{
ContainerPort: containerPort,
HostPort: hostPort,
HostIP: hostIP,
})
}
}
}
composeByte, err := files.NewFileOp().GetContent(path.Join(global.Dir.TensorRTLLMDir, item.Name, "docker-compose.yml")) composeByte, err := files.NewFileOp().GetContent(path.Join(global.Dir.TensorRTLLMDir, item.Name, "docker-compose.yml"))
if err == nil { if err == nil {
@@ -149,11 +122,9 @@ func handleLLMParams(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) e
delete(serviceValue, "ports") delete(serviceValue, "ports")
if len(create.ExposedPorts) > 0 { if len(create.ExposedPorts) > 0 {
var ports []interface{} var ports []interface{}
for i := range create.ExposedPorts { for i, port := range create.ExposedPorts {
containerPortStr := fmt.Sprintf("CONTAINER_PORT_%d", i) containerPortStr, hostPortStr, hostIPStr, _ := composePortEnvKeys(i)
hostPortStr := fmt.Sprintf("HOST_PORT_%d", i) ports = append(ports, formatComposePortMapping(hostIPStr, hostPortStr, containerPortStr, port.Protocol))
hostIPStr := fmt.Sprintf("HOST_IP_%d", i)
ports = append(ports, fmt.Sprintf("${%s}:${%s}:${%s}", hostIPStr, hostPortStr, containerPortStr))
} }
serviceValue["ports"] = ports serviceValue["ports"] = ports
} }
@@ -174,7 +145,7 @@ func handleLLMParams(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) e
volumes = append(volumes, fmt.Sprintf("%s:%s", k, v)) volumes = append(volumes, fmt.Sprintf("%s:%s", k, v))
} }
for _, volume := range create.Volumes { for _, volume := range create.Volumes {
volumes = append(volumes, fmt.Sprintf("%s:%s", volume.Source, volume.Target)) volumes = append(volumes, formatComposeVolume(volume.Source, volume.Target, volume.Mode))
} }
serviceValue["volumes"] = volumes serviceValue["volumes"] = volumes
@@ -195,12 +166,11 @@ func handleLLMEnv(llm *model.TensorRTLLM, create request.TensorRTLLMCreate) gote
envMap["IMAGE"] = create.Image envMap["IMAGE"] = create.Image
envMap["COMMAND"] = create.Command envMap["COMMAND"] = create.Command
for i, port := range create.ExposedPorts { for i, port := range create.ExposedPorts {
containerPortStr := fmt.Sprintf("CONTAINER_PORT_%d", i) containerPortStr, hostPortStr, hostIPStr, protocolStr := composePortEnvKeys(i)
hostPortStr := fmt.Sprintf("HOST_PORT_%d", i)
hostIPStr := fmt.Sprintf("HOST_IP_%d", i)
envMap[containerPortStr] = strconv.Itoa(port.ContainerPort) envMap[containerPortStr] = strconv.Itoa(port.ContainerPort)
envMap[hostPortStr] = strconv.Itoa(port.HostPort) envMap[hostPortStr] = strconv.Itoa(port.HostPort)
envMap[hostIPStr] = port.HostIP envMap[hostIPStr] = port.HostIP
envMap[protocolStr] = normalizeComposeProtocol(port.Protocol)
} }
orders := []string{"MODEL_PATH", "COMMAND"} orders := []string{"MODEL_PATH", "COMMAND"}
envStr, _ := env.MarshalWithOrder(envMap, orders) envStr, _ := env.MarshalWithOrder(envMap, orders)
@@ -219,7 +189,7 @@ func (t TensorRTLLMService) Create(create request.TensorRTLLMCreate) error {
} }
} }
for _, export := range create.ExposedPorts { for _, export := range create.ExposedPorts {
if err := checkPortExist(export.HostPort); err != nil { if err := checkPortExistWithProtocol(export.HostPort, export.Protocol); err != nil {
return err return err
} }
} }
+16 -3
View File
@@ -12,6 +12,7 @@ const (
vllmImageEnvKey = "IMAGE" vllmImageEnvKey = "IMAGE"
vllmImageTypeNvidia = "nvidia" vllmImageTypeNvidia = "nvidia"
vllmImageTypeIntel = "intel" vllmImageTypeIntel = "intel"
vllmImageTypeAscend = "ascend"
) )
func resolveVllmVersionFamily(version, image string) string { func resolveVllmVersionFamily(version, image string) string {
@@ -19,6 +20,9 @@ func resolveVllmVersionFamily(version, image string) string {
if strings.HasPrefix(normalizedVersion, vllmImageTypeIntel+"-") { if strings.HasPrefix(normalizedVersion, vllmImageTypeIntel+"-") {
return vllmImageTypeIntel return vllmImageTypeIntel
} }
if strings.HasPrefix(normalizedVersion, vllmImageTypeAscend+"-") {
return vllmImageTypeAscend
}
if strings.HasPrefix(normalizedVersion, vllmImageTypeNvidia+"-") { if strings.HasPrefix(normalizedVersion, vllmImageTypeNvidia+"-") {
return vllmImageTypeNvidia return vllmImageTypeNvidia
} }
@@ -26,13 +30,16 @@ func resolveVllmVersionFamily(version, image string) string {
if strings.Contains(normalizedImage, "intel/") || strings.Contains(normalizedImage, "llm-scaler-vllm") { if strings.Contains(normalizedImage, "intel/") || strings.Contains(normalizedImage, "llm-scaler-vllm") {
return vllmImageTypeIntel return vllmImageTypeIntel
} }
if strings.Contains(normalizedImage, "ascend/") || strings.Contains(normalizedImage, "vllm-ascend") {
return vllmImageTypeAscend
}
return vllmImageTypeNvidia return vllmImageTypeNvidia
} }
func trimVllmVersionFamily(version string) string { func trimVllmVersionFamily(version string) string {
trimmed := strings.TrimSpace(version) trimmed := strings.TrimSpace(version)
normalized := strings.ToLower(trimmed) normalized := strings.ToLower(trimmed)
for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel} { for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel, vllmImageTypeAscend} {
prefix := family + "-" prefix := family + "-"
if strings.HasPrefix(normalized, prefix) { if strings.HasPrefix(normalized, prefix) {
return strings.TrimSpace(trimmed[len(prefix):]) return strings.TrimSpace(trimmed[len(prefix):])
@@ -43,12 +50,16 @@ func trimVllmVersionFamily(version string) string {
func buildDefaultVllmImageByVersion(version string) string { func buildDefaultVllmImageByVersion(version string) string {
tag := trimVllmVersionFamily(version) tag := trimVllmVersionFamily(version)
if resolveVllmVersionFamily(version, "") == vllmImageTypeIntel { family := resolveVllmVersionFamily(version, "")
if family == vllmImageTypeIntel {
return "intel/llm-scaler-vllm:" + tag return "intel/llm-scaler-vllm:" + tag
} }
if tag != "" && !strings.HasPrefix(strings.ToLower(tag), "v") { if tag != "" && !strings.HasPrefix(strings.ToLower(tag), "v") {
tag = "v" + tag tag = "v" + tag
} }
if family == vllmImageTypeAscend {
return "quay.io/ascend/vllm-ascend:" + tag
}
return "vllm/vllm-openai:" + tag return "vllm/vllm-openai:" + tag
} }
@@ -60,7 +71,9 @@ func isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage str
func hasVllmVersionFamilyPrefix(version string) bool { func hasVllmVersionFamilyPrefix(version string) bool {
normalized := strings.ToLower(strings.TrimSpace(version)) normalized := strings.ToLower(strings.TrimSpace(version))
return strings.HasPrefix(normalized, vllmImageTypeNvidia+"-") || strings.HasPrefix(normalized, vllmImageTypeIntel+"-") return strings.HasPrefix(normalized, vllmImageTypeNvidia+"-") ||
strings.HasPrefix(normalized, vllmImageTypeIntel+"-") ||
strings.HasPrefix(normalized, vllmImageTypeAscend+"-")
} }
func isVllmUpgradeCandidate(currentVersion, targetVersion, currentImage string) bool { func isVllmUpgradeCandidate(currentVersion, targetVersion, currentImage string) bool {
+11 -6
View File
@@ -168,15 +168,13 @@ func (w WebsiteService) PageWebsite(req request.WebsiteSearch) (int64, []respons
opts = append(opts, repo.WithOrderRuleBy(req.OrderBy, req.Order), repo.WithOrderRuleBy("updated_at", "descending")) opts = append(opts, repo.WithOrderRuleBy(req.OrderBy, req.Order), repo.WithOrderRuleBy("updated_at", "descending"))
if req.Name != "" { if req.Name != "" {
domains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithDomainLike(req.Name)) domains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithDomainLike(req.Name))
var websiteIds []uint
if len(domains) > 0 { if len(domains) > 0 {
var websiteIds []uint
for _, domain := range domains { for _, domain := range domains {
websiteIds = append(websiteIds, domain.WebsiteID) websiteIds = append(websiteIds, domain.WebsiteID)
} }
opts = append(opts, repo.WithByIDs(websiteIds))
} else {
opts = append(opts, websiteRepo.WithDomainLike(req.Name))
} }
opts = append(opts, websiteRepo.WithSearchKeyword(req.Name, websiteIds))
} }
if req.WebsiteGroupID != 0 { if req.WebsiteGroupID != 0 {
opts = append(opts, websiteRepo.WithGroupID(req.WebsiteGroupID)) opts = append(opts, websiteRepo.WithGroupID(req.WebsiteGroupID))
@@ -717,11 +715,18 @@ func (w WebsiteService) DeleteWebsite(req request.WebsiteDelete) error {
if mysqlDB.ID > 0 { if mysqlDB.ID > 0 {
deleteReq := dto.MysqlDBDelete{ deleteReq := dto.MysqlDBDelete{
ID: mysqlDB.ID, ID: mysqlDB.ID,
Type: website.DbType,
Database: mysqlDB.MysqlName, Database: mysqlDB.MysqlName,
ForceDelete: req.ForceDelete, ForceDelete: req.ForceDelete,
} }
if err = NewIMysqlService().Delete(context.TODO(), deleteReq); err != nil && !req.ForceDelete { if err = deleteMysqlDatabaseForResourceOwner(
return err context.TODO(),
deleteReq,
dto.DBResource{Type: constant.TypeWebsite, Name: website.PrimaryDomain},
); err != nil {
if isMysqlDatabaseResourceInUseError(err) || !req.ForceDelete {
return err
}
} }
} }
case constant.AppPostgresql, constant.AppPostgres: case constant.AppPostgresql, constant.AppPostgres:
+6
View File
@@ -207,6 +207,7 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
Description: req.Description, Description: req.Description,
ExecShell: req.ExecShell, ExecShell: req.ExecShell,
} }
setSSLPushConfig(websiteSSL, req.PushNode, req.Nodes)
if req.ExecShell { if req.ExecShell {
websiteSSL.Shell = req.Shell websiteSSL.Shell = req.Shell
} }
@@ -384,6 +385,11 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
} }
} }
reloadSystemSSL(websiteSSL, logger) reloadSystemSSL(websiteSSL, logger)
if websiteSSL.PushNode {
if err = pushSSLToNode(websiteSSL, logger); err != nil {
return nil, err
}
}
return websiteSSL, nil return websiteSSL, nil
} }
+120 -18
View File
@@ -18,12 +18,12 @@ import (
"github.com/go-acme/lego/v5/certificate" "github.com/go-acme/lego/v5/certificate"
legoLogger "github.com/go-acme/lego/v5/log" legoLogger "github.com/go-acme/lego/v5/log"
"github.com/jinzhu/gorm"
"github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/app/dto/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"
@@ -104,6 +104,7 @@ type IWebsiteSSLService interface {
Delete(ids []uint) error Delete(ids []uint) error
Update(update request.WebsiteSSLUpdate) error Update(update request.WebsiteSSLUpdate) error
Upload(req request.WebsiteSSLUpload) error Upload(req request.WebsiteSSLUpload) error
PushToNode(req request.WebsiteSSLPush) error
ObtainSSL(apply request.WebsiteSSLApply) error ObtainSSL(apply request.WebsiteSSLApply) error
AutoRenewSSL(id uint) error AutoRenewSSL(id uint) error
SyncForRestart() error SyncForRestart() error
@@ -214,10 +215,7 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs
} }
websiteSSL.Dir = create.Dir websiteSSL.Dir = create.Dir
} }
if create.PushNode && global.IsMaster && len(create.Nodes) > 0 { setSSLPushConfig(&websiteSSL, create.PushNode, create.Nodes)
websiteSSL.PushNode = true
websiteSSL.Nodes = create.Nodes
}
var domains []string var domains []string
if create.OtherDomains != "" { if create.OtherDomains != "" {
@@ -282,6 +280,47 @@ func printSSLLog(logger *log.Logger, msgKey string, params map[string]interface{
logger.Println(i18n.GetMsgWithMap(msgKey, params)) logger.Println(i18n.GetMsgWithMap(msgKey, params))
} }
func normalizeSSLPushConfig(pushNode bool, nodes string) (bool, string) {
nodes = strings.TrimSpace(nodes)
if !pushNode || nodes == "" {
return false, ""
}
return true, nodes
}
func setSSLPushConfig(websiteSSL *model.WebsiteSSL, pushNode bool, nodes string) {
pushNode, nodes = normalizeSSLPushConfig(pushNode, nodes)
if !global.IsMaster || !xpack.MultiNodeProvider.IsXpack() {
pushNode = false
nodes = ""
}
websiteSSL.PushNode = pushNode
websiteSSL.Nodes = nodes
}
func pushSSLToNode(websiteSSL *model.WebsiteSSL, logger *log.Logger) error {
printSSLLog(logger, "StartPushSSLToNode", nil)
if err := xpack.MultiNodeProvider.PushSSLToNode(websiteSSL); err != nil {
printSSLLog(logger, "PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()})
return err
}
printSSLLog(logger, "PushSSLToNodeSuccess", nil)
return nil
}
func pushSSLToNodeWithNewLogger(websiteSSL *model.WebsiteSSL) error {
if !websiteSSL.PushNode {
return nil
}
logFile, logger := newWebsiteSSLLogger(websiteSSL, false)
if logFile != nil {
defer func() {
_ = logFile.Close()
}()
}
return pushSSLToNode(websiteSSL, logger)
}
func newWebsiteSSLLogger(websiteSSL *model.WebsiteSSL, autoRenew bool) (*os.File, *log.Logger) { func newWebsiteSSLLogger(websiteSSL *model.WebsiteSSL, autoRenew bool) (*os.File, *log.Logger) {
flags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC flags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC
if autoRenew { if autoRenew {
@@ -422,7 +461,7 @@ func (w WebsiteSSLService) obtainSSL(id uint, autoRenew bool) error {
case constant.Http: case constant.Http:
appInstall, err := getAppInstallByKey(constant.AppOpenresty) appInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil { if err != nil {
if gorm.IsRecordNotFoundError(err) { if errors.Is(err, gormv2.ErrRecordNotFound) {
return buserr.New("ErrOpenrestyNotFound") return buserr.New("ErrOpenrestyNotFound")
} }
return err return err
@@ -567,12 +606,9 @@ func (w WebsiteSSLService) obtainSSL(id uint, autoRenew bool) error {
} }
reloadSystemSSL(websiteSSL, logger) reloadSystemSSL(websiteSSL, logger)
if websiteSSL.PushNode { if websiteSSL.PushNode {
printSSLLog(logger, "StartPushSSLToNode", nil) if err = pushSSLToNode(websiteSSL, logger); err != nil {
if err = xpack.MultiNodeProvider.PushSSLToNode(websiteSSL); err != nil {
printSSLLog(logger, "PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()})
return return
} }
printSSLLog(logger, "PushSSLToNodeSuccess", nil)
} }
}(logFile, logger) }(logFile, logger)
@@ -740,13 +776,13 @@ func (w WebsiteSSLService) Update(update request.WebsiteSSLUpdate) error {
} else { } else {
updateParams["shell"] = "" updateParams["shell"] = ""
} }
if update.PushNode { pushNode, nodes := normalizeSSLPushConfig(update.PushNode, update.Nodes)
updateParams["push_node"] = true if !global.IsMaster || !xpack.MultiNodeProvider.IsXpack() {
updateParams["nodes"] = update.Nodes pushNode = false
} else { nodes = ""
updateParams["push_node"] = false
updateParams["nodes"] = ""
} }
updateParams["push_node"] = pushNode
updateParams["nodes"] = nodes
if websiteSSL.Provider != constant.SelfSigned && websiteSSL.Provider != constant.Manual { if websiteSSL.Provider != constant.SelfSigned && websiteSSL.Provider != constant.Manual {
acmeAccount, err := websiteAcmeRepo.GetFirst(repo.WithByID(update.AcmeAccountID)) acmeAccount, err := websiteAcmeRepo.GetFirst(repo.WithByID(update.AcmeAccountID))
@@ -797,6 +833,7 @@ func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error {
Description: req.Description, Description: req.Description,
Status: constant.SSLReady, Status: constant.SSLReady,
} }
setSSLPushConfig(websiteSSL, req.PushNode, req.Nodes)
var err error var err error
if req.SSLID > 0 { if req.SSLID > 0 {
websiteSSL, err = websiteSSLRepo.GetFirst(repo.WithByID(req.SSLID)) websiteSSL, err = websiteSSLRepo.GetFirst(repo.WithByID(req.SSLID))
@@ -804,6 +841,7 @@ func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error {
return err return err
} }
websiteSSL.Description = req.Description websiteSSL.Description = req.Description
setSSLPushConfig(websiteSSL, req.PushNode, req.Nodes)
} }
if req.Type == "local" { if req.Type == "local" {
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
@@ -891,9 +929,73 @@ func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error {
if err := UpdateSSLConfig(*websiteSSL); err != nil { if err := UpdateSSLConfig(*websiteSSL); err != nil {
return err return err
} }
return websiteSSLRepo.Save(websiteSSL) if err := websiteSSLRepo.Save(websiteSSL); err != nil {
return err
}
return pushSSLToNodeWithNewLogger(websiteSSL)
} }
return websiteSSLRepo.Create(context.Background(), websiteSSL) if err := websiteSSLRepo.Create(context.Background(), websiteSSL); err != nil {
return err
}
return pushSSLToNodeWithNewLogger(websiteSSL)
}
func (w WebsiteSSLService) PushToNode(req request.WebsiteSSLPush) error {
if !global.IsMaster {
return errors.New("only master node can push SSL to nodes")
}
if !xpack.MultiNodeProvider.IsXpack() {
return errors.New("SSL node push is an XPack feature")
}
pushNode, nodes := normalizeSSLPushConfig(req.PushNode, req.Nodes)
if !pushNode {
return errors.New("please select nodes to push SSL")
}
websiteSSL, err := websiteSSLRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return err
}
if websiteSSL.Provider == constant.FromMaster {
return errors.New("SSL imported from master node can not be pushed")
}
if websiteSSL.Status != constant.SSLReady {
return errors.New("only ready SSL can be pushed")
}
if task.CheckResourceTaskIsExecuting(task.TaskPush, task.TaskScopeWebsite, websiteSSL.ID) {
return buserr.New("TaskIsExecuting")
}
if err := websiteSSLRepo.SaveByMap(websiteSSL, map[string]interface{}{
"push_node": pushNode,
"nodes": nodes,
}); err != nil {
return err
}
websiteSSL.PushNode = pushNode
websiteSSL.Nodes = nodes
if req.Sync {
return xpack.MultiNodeProvider.PushSSLToNode(websiteSSL)
}
pushTask, err := task.NewTask(task.GetTaskName(websiteSSL.PrimaryDomain, task.TaskPush, "SSL"), task.TaskPush, task.TaskScopeWebsite, req.TaskID, websiteSSL.ID)
if err != nil {
return err
}
pushTask.AddSubTask(i18n.GetMsgByKey("StartPushSSLToNode"), func(t *task.Task) error {
t.Log(i18n.GetMsgByKey("StartPushSSLToNode"))
if err := xpack.MultiNodeProvider.PushSSLToNode(websiteSSL); err != nil {
t.Log(i18n.GetMsgWithMap("PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()}))
return err
}
t.Log(i18n.GetMsgByKey("PushSSLToNodeSuccess"))
return nil
}, nil)
go func() {
if err := pushTask.Execute(); err != nil {
global.LOG.Errorf("push ssl to node failed, sslID: %d, err: %v", websiteSSL.ID, err)
}
}()
return nil
} }
func (w WebsiteSSLService) DownloadFile(id uint) (*os.File, error) { func (w WebsiteSSLService) DownloadFile(id uint) (*os.File, error) {
+9 -5
View File
@@ -169,8 +169,8 @@ func NewTask(name, operate, taskScope, taskID string, resourceID uint) (*Task, e
} }
taskRepo := repo.NewITaskRepo() taskRepo := repo.NewITaskRepo()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
global.TaskCtxMap[taskID] = cancel global.RegisterTaskCancel(taskID, cancel)
task := &Task{TaskCtx: ctx, Name: name, logFile: logFile, Logger: logger, taskRepo: taskRepo, Task: taskModel} task := &Task{TaskCtx: ctx, TaskID: taskID, Name: name, logFile: logFile, Logger: logger, taskRepo: taskRepo, Task: taskModel}
return task, nil return task, nil
} }
@@ -200,7 +200,7 @@ func ReNewTask(name, operate, taskScope, taskID string, resourceID uint) (*Task,
logger.SetOutput(logFile) logger.SetOutput(logFile)
logger.Print("\n --------------------------------------------------- \n") logger.Print("\n --------------------------------------------------- \n")
taskItem.Status = constant.StatusExecuting taskItem.Status = constant.StatusExecuting
task := &Task{Name: name, logFile: logFile, Logger: logger, taskRepo: taskRepo, Task: &taskItem} task := &Task{TaskID: taskID, Name: name, logFile: logFile, Logger: logger, taskRepo: taskRepo, Task: &taskItem}
task.updateTask(&taskItem) task.updateTask(&taskItem)
return task, nil return task, nil
} }
@@ -231,7 +231,7 @@ func (t *Task) AddSubTaskWithIgnoreErr(name string, action ActionFunc) {
} }
func (s *SubTask) Execute() error { func (s *SubTask) Execute() error {
defer delete(global.TaskCtxMap, s.RootTask.TaskID) defer global.RemoveTaskCancel(s.RootTask.TaskID)
subTaskName := s.Name subTaskName := s.Name
if s.Name == "" { if s.Name == "" {
subTaskName = i18n.GetMsgByKey("SubTask") subTaskName = i18n.GetMsgByKey("SubTask")
@@ -302,7 +302,11 @@ func (t *Task) Execute() error {
continue continue
} }
t.Task.ErrorMsg = err.Error() t.Task.ErrorMsg = err.Error()
t.Task.Status = constant.StatusFailed if errors.Is(err, context.Canceled) {
t.Task.Status = constant.StatusCanceled
} else {
t.Task.Status = constant.StatusFailed
}
for _, rollback := range t.Rollbacks { for _, rollback := range t.Rollbacks {
rollback(t) rollback(t)
} }
+533 -90
View File
@@ -1247,6 +1247,33 @@
"formatZH": "更新 AI 网关分组 [name]", "formatZH": "更新 AI 网关分组 [name]",
"formatEN": "update AI proxy group [name]" "formatEN": "update AI proxy group [name]"
}, },
"/core/enterprise/ai-proxy/model-groups/create": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建 AI 网关模型组 [name]",
"formatEN": "create AI proxy model group [name]"
},
"/core/enterprise/ai-proxy/model-groups/delete": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除 AI 网关模型组 [id]",
"formatEN": "delete AI proxy model group [id]"
},
"/core/enterprise/ai-proxy/model-groups/update": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新 AI 网关模型组 [name]",
"formatEN": "update AI proxy model group [name]"
},
"/core/enterprise/ai-proxy/service/operate": { "/core/enterprise/ai-proxy/service/operate": {
"bodyKeys": [ "bodyKeys": [
"operate" "operate"
@@ -1256,6 +1283,13 @@
"formatZH": "操作 AI 网关服务 [operate]", "formatZH": "操作 AI 网关服务 [operate]",
"formatEN": "operate AI proxy service [operate]" "formatEN": "operate AI proxy service [operate]"
}, },
"/core/enterprise/ai-proxy/settings/logs/clear": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清空 AI 网关日志",
"formatEN": "clear AI proxy logs"
},
"/core/enterprise/ai-proxy/settings/update": { "/core/enterprise/ai-proxy/settings/update": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
@@ -1263,6 +1297,50 @@
"formatZH": "更新 AI 网关设置", "formatZH": "更新 AI 网关设置",
"formatEN": "update AI proxy settings" "formatEN": "update AI proxy settings"
}, },
"/core/enterprise/ai-proxy/smart-route/policy/update": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新 AI 网关智能路由策略",
"formatEN": "update AI proxy smart route policy"
},
"/core/enterprise/ai-proxy/smart-route/samples/create": {
"bodyKeys": [
"type",
"label"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建 AI 网关智能路由样本 [type] [label]",
"formatEN": "create AI proxy smart route sample [type] [label]"
},
"/core/enterprise/ai-proxy/smart-route/samples/delete": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除 AI 网关智能路由样本 [id]",
"formatEN": "delete AI proxy smart route sample [id]"
},
"/core/enterprise/ai-proxy/smart-route/samples/rebuild": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "重建 AI 网关智能路由样本向量",
"formatEN": "rebuild AI proxy smart route sample vectors"
},
"/core/enterprise/ai-proxy/smart-route/samples/update": {
"bodyKeys": [
"id",
"type",
"label"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新 AI 网关智能路由样本 [id] [type] [label]",
"formatEN": "update AI proxy smart route sample [id] [type] [label]"
},
"/core/enterprise/licenses/upload": { "/core/enterprise/licenses/upload": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
@@ -1368,13 +1446,14 @@
"/core/enterprise/skills-hub/import": { "/core/enterprise/skills-hub/import": {
"bodyKeys": [ "bodyKeys": [
"type", "type",
"version",
"repoURL", "repoURL",
"packageURL" "packageURL"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "导入 Skills Hub 技能 [type]", "formatZH": "导入 Skills Hub 技能版本 [version]",
"formatEN": "import Skills Hub skill [type]" "formatEN": "import Skills Hub skill version [version]"
}, },
"/core/enterprise/skills-hub/status": { "/core/enterprise/skills-hub/status": {
"bodyKeys": [ "bodyKeys": [
@@ -1478,11 +1557,13 @@
"formatEN": "update Skills Hub install target [name]" "formatEN": "update Skills Hub install target [name]"
}, },
"/core/enterprise/skills-hub/upload": { "/core/enterprise/skills-hub/upload": {
"bodyKeys": [], "bodyKeys": [
"version"
],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "上传 Skills Hub 技能包", "formatZH": "上传 Skills Hub 技能包版本 [version]",
"formatEN": "upload Skills Hub package" "formatEN": "upload Skills Hub package version [version]"
}, },
"/core/enterprise/users": { "/core/enterprise/users": {
"bodyKeys": [ "bodyKeys": [
@@ -1493,6 +1574,25 @@
"formatZH": "添加用户 [name]", "formatZH": "添加用户 [name]",
"formatEN": "add user [name]" "formatEN": "add user [name]"
}, },
"/core/enterprise/users/api/update": {
"bodyKeys": [
"id",
"ipWhiteList"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "users",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList]",
"formatEN": "update user [name] api config =\u003e IP White List: [ipWhiteList]"
},
"/core/enterprise/users/del": { "/core/enterprise/users/del": {
"bodyKeys": [ "bodyKeys": [
"ids" "ids"
@@ -1529,6 +1629,262 @@
"formatZH": "更新 [name]", "formatZH": "更新 [name]",
"formatEN": "update user [name]" "formatEN": "update user [name]"
}, },
"/core/enterprise/vms": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name]",
"formatEN": "create VM [name]"
},
"/core/enterprise/vms/del": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除虚拟机 [name]",
"formatEN": "delete VM [name]"
},
"/core/enterprise/vms/environment/enable": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "启用虚拟机运行环境",
"formatEN": "enable VM runtime environment"
},
"/core/enterprise/vms/iso": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机镜像 [name] [type]",
"formatEN": "create VM ISO [name] [type]"
},
"/core/enterprise/vms/iso/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_isos",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机镜像 [name]",
"formatEN": "delete VM ISO [name]"
},
"/core/enterprise/vms/iso/update": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机镜像 [name] [type]",
"formatEN": "update VM ISO [name] [type]"
},
"/core/enterprise/vms/networks": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机网络 [name] [type]",
"formatEN": "create VM network [name] [type]"
},
"/core/enterprise/vms/networks/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机网络 [name]",
"formatEN": "delete VM network [name]"
},
"/core/enterprise/vms/networks/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_networks",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机网络 [name]",
"formatEN": "update VM network [name]"
},
"/core/enterprise/vms/operate": {
"bodyKeys": [
"name",
"operate"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "操作虚拟机 [name] [operate]",
"formatEN": "operate VM [name] [operate]"
},
"/core/enterprise/vms/orphans/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清理虚拟机孤立数据",
"formatEN": "clean VM orphaned data"
},
"/core/enterprise/vms/rename": {
"bodyKeys": [
"name",
"newName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "重命名虚拟机 [name] 为 [newName]",
"formatEN": "rename VM [name] to [newName]"
},
"/core/enterprise/vms/snapshots": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 快照 [snapshotName]",
"formatEN": "create VM [name] snapshot [snapshotName]"
},
"/core/enterprise/vms/snapshots/del": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除虚拟机 [name] 快照 [snapshotName]",
"formatEN": "delete VM [name] snapshot [snapshotName]"
},
"/core/enterprise/vms/snapshots/recover": {
"bodyKeys": [
"name",
"snapshotName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "恢复虚拟机 [name] 快照 [snapshotName]",
"formatEN": "recover VM [name] snapshot [snapshotName]"
},
"/core/enterprise/vms/storages": {
"bodyKeys": [
"name",
"type"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机存储 [name] [type]",
"formatEN": "create VM storage [name] [type]"
},
"/core/enterprise/vms/storages/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机存储 [name]",
"formatEN": "delete VM storage [name]"
},
"/core/enterprise/vms/storages/update": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_storages",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新虚拟机存储 [name]",
"formatEN": "update VM storage [name]"
},
"/core/enterprise/vms/sync": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步虚拟机",
"formatEN": "sync virtual machines"
},
"/core/enterprise/vms/templates": {
"bodyKeys": [
"name",
"templateName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建虚拟机 [name] 模板 [templateName]",
"formatEN": "create VM [name] template [templateName]"
},
"/core/enterprise/vms/templates/del": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "vm_templates",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "删除虚拟机模板 [name]",
"formatEN": "delete VM template [name]"
},
"/core/enterprise/vms/update": {
"bodyKeys": [
"name"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新虚拟机 [name]",
"formatEN": "update VM [name]"
},
"/core/groups": { "/core/groups": {
"bodyKeys": [ "bodyKeys": [
"name", "name",
@@ -1853,15 +2209,22 @@
}, },
"/core/xpack/exchange/file": { "/core/xpack/exchange/file": {
"bodyKeys": [ "bodyKeys": [
"source", "sourceNode",
"dest", "targetNode",
"sourcePath", "sourcePath",
"destPath" "targetPath"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "交换文件 [sourcePath] 到 [destPath]", "formatZH": "交换文件 [sourcePath] 到 [targetPath]",
"formatEN": "exchange file [sourcePath] to [destPath]" "formatEN": "exchange file [sourcePath] to [targetPath]"
},
"/core/xpack/exchange/image": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "同步节点镜像",
"formatEN": "sync images"
}, },
"/core/xpack/nodes": { "/core/xpack/nodes": {
"bodyKeys": [ "bodyKeys": [
@@ -1948,6 +2311,16 @@
"formatZH": "修复节点 [name]", "formatZH": "修复节点 [name]",
"formatEN": "fix node [name]" "formatEN": "fix node [name]"
}, },
"/core/xpack/nodes/health/setting": {
"bodyKeys": [
"interval",
"loadFailedResources"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新节点健康检查设置",
"formatEN": "update node health check setting"
},
"/core/xpack/nodes/reinstall": { "/core/xpack/nodes/reinstall": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
@@ -2166,6 +2539,15 @@
"formatZH": "更新自定义应用配置 [status]", "formatZH": "更新自定义应用配置 [status]",
"formatEN": "update custom app config [status]" "formatEN": "update custom app config [status]"
}, },
"/core/xpack/settings/app/image-prefix": {
"bodyKeys": [
"imagePrefix"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新应用镜像前缀 [imagePrefix]",
"formatEN": "update app image prefix [imagePrefix]"
},
"/core/xpack/settings/reset": { "/core/xpack/settings/reset": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
@@ -2191,27 +2573,11 @@
"formatEN": "update xpack setting [key]" "formatEN": "update xpack setting [key]"
}, },
"/core/xpack/sync/app": { "/core/xpack/sync/app": {
"bodyKeys": [ "bodyKeys": [],
"sourceNode",
"targetNode",
"appInstallID",
"appBackupID"
],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "同步应用备份 [appInstallID] 到节点 [targetNode]", "formatZH": "同步节点应用",
"formatEN": "sync app backup [appInstallID] to node [targetNode]" "formatEN": "sync app"
},
"/core/xpack/sync/app/install": {
"bodyKeys": [
"appKey",
"version",
"nodes"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "安装应用 [appKey] [version] 到节点 [nodes]",
"formatEN": "install app [appKey] [version] to nodes [nodes]"
}, },
"/core/xpack/sync/ssl": { "/core/xpack/sync/ssl": {
"bodyKeys": [ "bodyKeys": [
@@ -2352,34 +2718,14 @@
"formatZH": "更新自定义应用配置 [status]", "formatZH": "更新自定义应用配置 [status]",
"formatEN": "update custom app config [status]" "formatEN": "update custom app config [status]"
}, },
"/custom/app/install": { "/custom/app/image-prefix": {
"bodyKeys": [ "bodyKeys": [
"name", "imagePrefix"
"appKey"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "从节点安装应用 [name]", "formatZH": "更新应用镜像前缀 [imagePrefix]",
"formatEN": "install app [name] from node" "formatEN": "update app image prefix [imagePrefix]"
},
"/custom/app/install/check": {
"bodyKeys": [
"name",
"appKey"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "检查应用安装 [name]",
"formatEN": "check app install [name]"
},
"/custom/app/push": {
"bodyKeys": [
"appBackupID"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "推送应用备份 [appBackupID] 到节点",
"formatEN": "push app backup [appBackupID] to node"
}, },
"/custom/app/sync": { "/custom/app/sync": {
"bodyKeys": [], "bodyKeys": [],
@@ -2414,51 +2760,23 @@
"formatZH": "创建 mysql 数据库 [name]", "formatZH": "创建 mysql 数据库 [name]",
"formatEN": "create mysql database [name]" "formatEN": "create mysql database [name]"
}, },
"/databases/bind": { "/databases/change/access": {
"bodyKeys": [ "bodyKeys": [
"database", "database"
"username"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
"formatZH": "绑定 mysql 数据库名 [database] [username]", "formatZH": "更新数据库 [database] root 访问权限",
"formatEN": "bind mysql database [database] [username]" "formatEN": "Update database [database] root access"
},
"/databases/change/access": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "database_mysqls",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新数据库 [name] 访问权限",
"formatEN": "Update database [name] access"
}, },
"/databases/change/password": { "/databases/change/password": {
"bodyKeys": [ "bodyKeys": [
"id" "database"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [ "beforeFunctions": [],
{ "formatZH": "更新数据库 [database] root 密码",
"input_column": "id", "formatEN": "Update database [database] root password"
"input_value": "id",
"isList": false,
"db": "database_mysqls",
"output_column": "name",
"output_value": "name"
}
],
"formatZH": "更新数据库 [name] 密码",
"formatEN": "Update database [name] password"
}, },
"/databases/common/update/conf": { "/databases/common/update/conf": {
"bodyKeys": [ "bodyKeys": [
@@ -2554,6 +2872,30 @@
"formatZH": "mysql 数据库 [name] 描述信息修改 [description]", "formatZH": "mysql 数据库 [name] 描述信息修改 [description]",
"formatEN": "The description of the mysql database [name] is modified =\u003e [description]" "formatEN": "The description of the mysql database [name] is modified =\u003e [description]"
}, },
"/databases/grants": {
"bodyKeys": [
"database",
"db",
"username",
"host"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "授权 mysql 数据库 [database] 用户 [username]@[host] 访问 [db]",
"formatEN": "grant mysql database [database] user [username]@[host] access to [db]"
},
"/databases/grants/del": {
"bodyKeys": [
"database",
"db",
"username",
"host"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "取消 mysql 数据库 [database] 用户 [username]@[host] 对 [db] 的授权",
"formatEN": "revoke mysql database [database] user [username]@[host] access to [db]"
},
"/databases/mongodb": { "/databases/mongodb": {
"bodyKeys": [ "bodyKeys": [
"name" "name"
@@ -2745,6 +3087,63 @@
"formatZH": "redis 数据库持久化配置更新", "formatZH": "redis 数据库持久化配置更新",
"formatEN": "redis database persistence configuration update" "formatEN": "redis database persistence configuration update"
}, },
"/databases/users": {
"bodyKeys": [
"database",
"username",
"host"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "创建 mysql 数据库 [database] 用户 [username]@[host]",
"formatEN": "create mysql database [database] user [username]@[host]"
},
"/databases/users/del": {
"bodyKeys": [
"database",
"username",
"host"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除 mysql 数据库 [database] 用户 [username]@[host]",
"formatEN": "delete mysql database [database] user [username]@[host]"
},
"/databases/users/password": {
"bodyKeys": [
"database",
"username",
"host"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新 mysql 数据库 [database] 用户 [username]@[host] 密码",
"formatEN": "update mysql database [database] user [username]@[host] password"
},
"/databases/users/password/save": {
"bodyKeys": [
"database",
"username",
"host"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "补充 mysql 数据库 [database] 用户 [username]@[host] 密码",
"formatEN": "save mysql database [database] user [username]@[host] password locally"
},
"/databases/users/update": {
"bodyKeys": [
"database",
"username",
"host",
"newHost",
"description"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] =\u003e [newHost] 描述 [description]",
"formatEN": "update mysql database [database] user [username] access [host] =\u003e [newHost] description [description]"
},
"/databases/variables/update": { "/databases/variables/update": {
"bodyKeys": [], "bodyKeys": [],
"paramKeys": [], "paramKeys": [],
@@ -3210,6 +3609,13 @@
"formatZH": "修改 SSH 配置文件 [key]", "formatZH": "修改 SSH 配置文件 [key]",
"formatEN": "update SSH conf [key]" "formatEN": "update SSH conf [key]"
}, },
"/hosts/ssh/log/clean": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "清空 SSH 登录日志",
"formatEN": "clean SSH login logs"
},
"/hosts/ssh/operate": { "/hosts/ssh/operate": {
"bodyKeys": [ "bodyKeys": [
"operation" "operation"
@@ -4440,6 +4846,24 @@
"formatZH": "申请证书 [domain]", "formatZH": "申请证书 [domain]",
"formatEN": "apply ssl [domain]" "formatEN": "apply ssl [domain]"
}, },
"/websites/ssl/push": {
"bodyKeys": [
"id"
],
"paramKeys": [],
"beforeFunctions": [
{
"input_column": "id",
"input_value": "id",
"isList": false,
"db": "website_ssls",
"output_column": "primary_domain",
"output_value": "domain"
}
],
"formatZH": "推送证书到节点 [domain]",
"formatEN": "Push ssl to nodes [domain]"
},
"/websites/ssl/update": { "/websites/ssl/update": {
"bodyKeys": [ "bodyKeys": [
"id" "id"
@@ -4775,6 +5199,15 @@
"formatZH": "更新 CDN 配置", "formatZH": "更新 CDN 配置",
"formatEN": "update CDN config" "formatEN": "update CDN config"
}, },
"/xpack/waf/config/global/spider": {
"bodyKeys": [
"rules"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新全局蜘蛛允许列表",
"formatEN": "update global spider allowlist"
},
"/xpack/waf/config/global/state": { "/xpack/waf/config/global/state": {
"bodyKeys": [ "bodyKeys": [
"scope", "scope",
@@ -4788,7 +5221,8 @@
"/xpack/waf/config/website/state": { "/xpack/waf/config/website/state": {
"bodyKeys": [ "bodyKeys": [
"scope", "scope",
"state" "state",
"mode"
], ],
"paramKeys": [], "paramKeys": [],
"beforeFunctions": [], "beforeFunctions": [],
@@ -4935,6 +5369,15 @@
"formatZH": "更新[scope]频率访问限制", "formatZH": "更新[scope]频率访问限制",
"formatEN": "update [scope] CC config" "formatEN": "update [scope] CC config"
}, },
"/xpack/waf/rule/common/apply": {
"bodyKeys": [
"scope"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "应用规则 [scope] 到网站",
"formatEN": "apply rule [scope] to websites"
},
"/xpack/waf/rule/common/create": { "/xpack/waf/rule/common/create": {
"bodyKeys": [ "bodyKeys": [
"scope" "scope"
@@ -434,5 +434,12 @@
"file": "lz4.so", "file": "lz4.so",
"versions": ["56","70", "71", "72", "73", "74", "80", "81", "82", "83","84","85"], "versions": ["56","70", "71", "72", "73", "74", "80", "81", "82", "83","84","85"],
"installed": false "installed": false
},
{
"name": "msgpack",
"check": "msgpack",
"file": "msgpack.so",
"versions": ["56","70", "71", "72", "73", "74", "80", "81", "82", "83","84","85"],
"installed": false
} }
] ]
+21
View File
@@ -2,6 +2,7 @@ package global
import ( import (
"context" "context"
"sync"
badger_db "github.com/1Panel-dev/1Panel/agent/init/cache/db" badger_db "github.com/1Panel-dev/1Panel/agent/init/cache/db"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
@@ -39,8 +40,28 @@ var (
AlertResourceJobID cron.EntryID AlertResourceJobID cron.EntryID
TaskCtxMap = make(map[string]context.CancelFunc) TaskCtxMap = make(map[string]context.CancelFunc)
taskCtxMu sync.RWMutex
) )
func RegisterTaskCancel(taskID string, cancel context.CancelFunc) {
taskCtxMu.Lock()
defer taskCtxMu.Unlock()
TaskCtxMap[taskID] = cancel
}
func LoadTaskCancel(taskID string) (context.CancelFunc, bool) {
taskCtxMu.RLock()
defer taskCtxMu.RUnlock()
cancel, ok := TaskCtxMap[taskID]
return cancel, ok
}
func RemoveTaskCancel(taskID string) {
taskCtxMu.Lock()
defer taskCtxMu.Unlock()
delete(TaskCtxMap, taskID)
}
func RepoURL() string { func RepoURL() string {
if CONF.Base.IsEnterprise { if CONF.Base.IsEnterprise {
return "https://resource.fit2cloud.com/1panel/package/enterprise" return "https://resource.fit2cloud.com/1panel/package/enterprise"
+52 -51
View File
@@ -4,63 +4,64 @@ go 1.25.10
replace github.com/moby/go-archive => github.com/moby/go-archive v0.1.0 replace github.com/moby/go-archive => github.com/moby/go-archive v0.1.0
replace github.com/go-acme/lego/v5 => github.com/1Panel-dev/lego/v5 v5.3.1
require ( require (
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/compose-spec/compose-go/v2 v2.11.0 github.com/compose-spec/compose-go/v2 v2.13.0
github.com/creack/pty v1.1.24 github.com/creack/pty v1.1.24
github.com/docker/cli v29.5.3+incompatible github.com/docker/cli v29.6.2+incompatible
github.com/docker/docker v28.5.2+incompatible github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-connections v0.7.0 github.com/docker/go-connections v0.7.0
github.com/fsnotify/fsnotify v1.10.1 github.com/fsnotify/fsnotify v1.10.1
github.com/gin-gonic/gin v1.12.0 github.com/gin-gonic/gin v1.12.0
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-acme/lego/v5 v5.2.2 github.com/go-acme/lego/v5 v5.3.1
github.com/go-gormigrate/gormigrate/v2 v2.1.6 github.com/go-gormigrate/gormigrate/v2 v2.1.6
github.com/go-playground/validator/v10 v10.30.3 github.com/go-playground/validator/v10 v10.30.3
github.com/go-redis/redis v6.15.9+incompatible github.com/go-redis/redis v6.15.9+incompatible
github.com/go-resty/resty/v2 v2.17.2 github.com/go-resty/resty/v2 v2.17.2
github.com/go-sql-driver/mysql v1.10.0 github.com/go-sql-driver/mysql v1.10.0
github.com/goh-chunlin/go-onedrive v1.1.1 github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/jackc/pgx/v5 v5.10.0 github.com/jackc/pgx/v5 v5.10.0
github.com/jinzhu/copier v0.4.0 github.com/jinzhu/copier v0.4.0
github.com/jinzhu/gorm v1.9.16
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/klauspost/compress v1.18.6 github.com/klauspost/compress v1.19.1
github.com/mattn/go-shellwords v1.0.14
github.com/mholt/archiver/v4 v4.0.0-alpha.8 github.com/mholt/archiver/v4 v4.0.0-alpha.8
github.com/miekg/dns v1.1.72 github.com/miekg/dns v1.1.72
github.com/minio/minio-go/v7 v7.2.0 github.com/minio/minio-go/v7 v7.2.1
github.com/nicksnyder/go-i18n/v2 v2.6.1 github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/image-spec v1.1.1
github.com/oschwald/maxminddb-golang v1.13.1 github.com/oschwald/maxminddb-golang v1.13.1
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.10 github.com/pkg/sftp v1.13.11
github.com/qiniu/go-sdk/v7 v7.26.12 github.com/qiniu/go-sdk/v7 v7.26.17
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.26.5 github.com/shirou/gopsutil/v4 v4.26.6
github.com/sirupsen/logrus v1.9.4 github.com/sirupsen/logrus v1.9.4
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/afero v1.15.0 github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.21.0
github.com/subosito/gotenv v1.6.0 github.com/subosito/gotenv v1.6.0
github.com/tencentyun/cos-go-sdk-v5 v0.7.73 github.com/tencentyun/cos-go-sdk-v5 v0.7.74
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19 github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19
github.com/upyun/go-sdk v2.1.0+incompatible github.com/upyun/go-sdk v2.1.0+incompatible
go.mongodb.org/mongo-driver/v2 v2.6.0 go.mongodb.org/mongo-driver/v2 v2.8.0
golang.org/x/crypto v0.53.0 golang.org/x/crypto v0.54.0
golang.org/x/net v0.55.0 golang.org/x/net v0.57.0
golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.22.0
golang.org/x/sync v0.21.0 golang.org/x/sys v0.47.0
golang.org/x/sys v0.46.0 golang.org/x/text v0.40.0
golang.org/x/text v0.38.0
golang.org/x/time v0.15.0 golang.org/x/time v0.15.0
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478 google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478
gopkg.in/ini.v1 v1.67.3 gopkg.in/ini.v1 v1.67.3
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.31.1 gorm.io/gorm v1.31.2
) )
require ( require (
@@ -69,28 +70,28 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.1 // indirect github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.3 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/tea v1.5.0 // indirect github.com/alibabacloud-go/tea v1.5.2 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 // indirect github.com/alibabacloud-go/tea-utils/v2 v2.0.9 // indirect
github.com/aliyun/credentials-go v1.4.12 // indirect github.com/aliyun/credentials-go v1.4.12 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect github.com/andybalholm/brotli v1.2.1 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.8 // indirect github.com/aws/aws-sdk-go-v2 v1.42.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.19 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.30 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.18 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.29 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.24 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.24 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.24 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.25 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.24 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.8 // indirect github.com/aws/aws-sdk-go-v2/service/route53 v1.64.1 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.1.0 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.4.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.18 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.32.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.1 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.42.2 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.44.1 // indirect
github.com/aws/smithy-go v1.25.1 // indirect github.com/aws/smithy-go v1.27.3 // indirect
github.com/baidubce/bce-sdk-go v0.9.267 // indirect github.com/baidubce/bce-sdk-go v0.9.270 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect github.com/bodgit/windows v1.0.1 // indirect
@@ -106,7 +107,6 @@ require (
github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker-credential-helpers v0.9.5 // indirect github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
@@ -115,12 +115,10 @@ require (
github.com/ebitengine/purego v0.10.0 // indirect github.com/ebitengine/purego v0.10.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gammazero/toposort v0.1.1 // indirect
github.com/gin-contrib/sse v1.1.1 // indirect github.com/gin-contrib/sse v1.1.1 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-acme/alidns-20150109/v5 v5.4.1 // indirect github.com/go-acme/alidns-20150109/v5 v5.5.0 // indirect
github.com/go-acme/esa-20240910/v3 v3.2.2 // indirect github.com/go-acme/esa-20240910/v3 v3.4.0 // indirect
github.com/go-acme/tencentclouddnspod v1.3.24 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
@@ -133,15 +131,14 @@ require (
github.com/gofrs/flock v0.13.0 // indirect github.com/gofrs/flock v0.13.0 // indirect
github.com/golang/snappy v1.0.0 // indirect github.com/golang/snappy v1.0.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect github.com/google/go-querystring v1.2.0 // indirect
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/h2non/filetype v1.1.3 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.198 // indirect github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.205 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jarcoal/httpmock v1.4.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
@@ -152,7 +149,7 @@ require (
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-shellwords v1.0.13 // indirect github.com/maxatome/go-testdeep v1.14.0 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -175,6 +172,8 @@ require (
github.com/nrdcg/namesilo v0.5.0 // indirect github.com/nrdcg/namesilo v0.5.0 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/nwaples/rardecode/v2 v2.2.2 // indirect github.com/nwaples/rardecode/v2 v2.2.2 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/onsi/gomega v1.37.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/ovh/go-ovh v1.9.0 // indirect github.com/ovh/go-ovh v1.9.0 // indirect
github.com/pelletier/go-toml/v2 v2.3.1 // indirect github.com/pelletier/go-toml/v2 v2.3.1 // indirect
@@ -189,7 +188,8 @@ require (
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.106 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.133 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.3.131 // indirect
github.com/therootcompany/xz v1.0.1 // indirect github.com/therootcompany/xz v1.0.1 // indirect
github.com/tinylib/msgp v1.6.4 // indirect github.com/tinylib/msgp v1.6.4 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect
@@ -198,7 +198,7 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect github.com/ulikunitz/xz v0.5.15 // indirect
github.com/volcengine/volc-sdk-golang v1.0.249 // indirect github.com/volcengine/volc-sdk-golang v1.0.251 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
@@ -217,13 +217,14 @@ require (
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/arch v0.26.0 // indirect golang.org/x/arch v0.26.0 // indirect
golang.org/x/mod v0.36.0 // indirect golang.org/x/mod v0.37.0 // indirect
golang.org/x/tools v0.45.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/tools v0.47.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
modernc.org/fileutil v1.4.0 // indirect modernc.org/fileutil v1.4.0 // indirect
modernc.org/libc v1.72.0 // indirect modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.48.2 // indirect modernc.org/sqlite v1.48.2 // indirect
software.sslmate.com/src/go-pkcs12 v0.7.1 // indirect software.sslmate.com/src/go-pkcs12 v0.7.3 // indirect
) )
+107 -130
View File
@@ -33,6 +33,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/1Panel-dev/lego/v5 v5.3.1 h1:Fp8cUTooZIFG84dJlj6TDfwIyOy0HWqY/1pnqA8v8vs=
github.com/1Panel-dev/lego/v5 v5.3.1/go.mod h1:yNl7Zqt0FK1zjhE1yuGz/rjcECyBeDk2IxkKUzkfEN8=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
@@ -48,7 +50,6 @@ github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw= github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
@@ -74,9 +75,10 @@ github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8= github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc= github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc= github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.14/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.1 h1:R8b55YFS4K9x5P5IdgA+QWinVfVmulzqaJG/tr6HhxM=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.1/go.mod h1:OCFim1kMbp2m+V8WS5IBnnVrk6nXaJiDwZpg3uqw8Po= github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.1/go.mod h1:OCFim1kMbp2m+V8WS5IBnnVrk6nXaJiDwZpg3uqw8Po=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.2/go.mod h1:CsqHkHPpAKL64jl/fChR249EL0abYEIDWmlwFYgo7/E=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.3 h1:rec08F9BzxIiJ03h2u6BT2QqLVa8ktnpvJ27j5PsjLM=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.3/go.mod h1:eHjVxrT9g8uVYN/nyAwOFQEfkVA154ChiqRc2XnNKYU=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg= github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ= github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo= github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
@@ -97,12 +99,12 @@ github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/Ke
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk= github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
github.com/alibabacloud-go/tea v1.5.0 h1:8pUo8WzMChtJT+jpLIcN1vzMi6SW3rihzzBoihPGUvs=
github.com/alibabacloud-go/tea v1.5.0/go.mod h1:hgSs82CkOiehSQMoiFN79dL6zsGX7pVGvnn9SIEs8/0= github.com/alibabacloud-go/tea v1.5.0/go.mod h1:hgSs82CkOiehSQMoiFN79dL6zsGX7pVGvnn9SIEs8/0=
github.com/alibabacloud-go/tea v1.5.1/go.mod h1:hgSs82CkOiehSQMoiFN79dL6zsGX7pVGvnn9SIEs8/0=
github.com/alibabacloud-go/tea v1.5.2 h1:DUzV/cRTbJC++rmADfqVu//GuWbaD64ZJ3yW3v2ukNU=
github.com/alibabacloud-go/tea v1.5.2/go.mod h1:hgSs82CkOiehSQMoiFN79dL6zsGX7pVGvnn9SIEs8/0=
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4= github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 h1:y6pUIlhjxbZl9ObDAcmA1H3c21eaAxADHTDQmBnAIgA= github.com/alibabacloud-go/tea-utils/v2 v2.0.9 h1:y6pUIlhjxbZl9ObDAcmA1H3c21eaAxADHTDQmBnAIgA=
github.com/alibabacloud-go/tea-utils/v2 v2.0.9/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= github.com/alibabacloud-go/tea-utils/v2 v2.0.9/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
@@ -115,7 +117,6 @@ github.com/aliyun/credentials-go v1.4.12 h1:7D8eXGotNwthZuUEgAMgBoqxmIHwfaPVwW+/
github.com/aliyun/credentials-go v1.4.12/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= github.com/aliyun/credentials-go v1.4.12/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@@ -125,40 +126,40 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
github.com/aws/aws-sdk-go-v2 v1.41.8 h1:sRs7nG6/RiEBZ/K5UO2sNw0w40U02Nmz1VtARloTZXk= github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek=
github.com/aws/aws-sdk-go-v2 v1.41.8/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM=
github.com/aws/aws-sdk-go-v2/config v1.32.19 h1:qRhIJMbevHUvIE7X4TK8N8zye5+5AhapcslPrvB+qKE= github.com/aws/aws-sdk-go-v2/config v1.32.30 h1:XwsEzpTJfQYJbFicz/QMLwAZdyeNVVoOEkbF7R3gPJk=
github.com/aws/aws-sdk-go-v2/config v1.32.19/go.mod h1:RbJ24nfoya63+Mf5VI+CGCGk9vEdv28xPeii+gojRYs= github.com/aws/aws-sdk-go-v2/config v1.32.30/go.mod h1:Ud32SuMc+/9BGxfpSVld7HrE2o05JwKmXY4M3jOQNZU=
github.com/aws/aws-sdk-go-v2/credentials v1.19.18 h1:GcXQz2M/0ZvMo0v5DakUqbDBeBM1ZNaivkolEF4Esgw= github.com/aws/aws-sdk-go-v2/credentials v1.19.29 h1:WHZGssHH887cO0ox07SIQZsFx3MKD4ps6w0xUEmnKYQ=
github.com/aws/aws-sdk-go-v2/credentials v1.19.18/go.mod h1:sHJ06tMGcD3ZpmMyJqV+VBsGilhSIZPIN+ZFy5Dg0C4= github.com/aws/aws-sdk-go-v2/credentials v1.19.29/go.mod h1:Mhl0xR6zjguiuj00XRx2wMx22sAltk7oya39sT7fdg8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.24 h1:FQm5ApnyzkuJdXLGskPce83CK1CQKC4RUnIHKVe4BU4= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 h1:/hi1JADLEW9YYryEz1w4GQu0EtP23pP553Cf9KgsDV4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.24/go.mod h1:JsC7dqQc55MlZ5mvNsDMMge71u8pVcSzU3RNz2h/5yQ= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30/go.mod h1:/3AOgy4K17Dm4ucMZVC/MJkzy5kmfKUcINRHZyo0koQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.24 h1:u6kJU2i0va1AgtJsH3RdWKWqHULlTh7zHwb35Womf74= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.24/go.mod h1:7GY+xLcXOFUpCkNwDReft9qOAVg54A4/AnjHIU7sSAY= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30/go.mod h1:WueJeNDZvK1fMYEWJIkcivBfEzUkTpBhzlrUKKY8EuA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.24 h1:Xhbcf3KugX6vX7SDyUK205Oicyfg7EGuvoVNyP5L6DM= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 h1:jn46zC9LdsVR/ZpMIJqMqb8hHv31BlLx3ulVqNspUOk=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.24/go.mod h1:rwDgb2HNOGZsnTHylOUedM7Vnl+bCfnXDqUNPsFWYfk= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30/go.mod h1:1hTMsAgbdS/AtUi4bw8+gUuh1pceo+eXRLfpSuSQj3M=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.25 h1:54CTMmlJ71Rk2dYvM9qZOob+39wjlVja2zDLxCu69Ew= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 h1:3GUprIsfmGcC5SACIyB0e7E0BM1O1b3Erl5CePYIAeQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.25/go.mod h1:BZaHqxsS9vN1fvV5EfEl0OBLOk5+AajWsMu6MjqnZB4= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31/go.mod h1:7PuV1yl5e2xnUbm+RqvVg5i2iBM8EyijZNoI9wsOoOc=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.24 h1:CQW2FTrflfoslYWLf3fv7vG28Q219+v8YJS5QTQb2+Y= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 h1:/Z5jmNrKsSD7EmDjzAPsm/3L9IuOkzaynklJZ1qX7S4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.24/go.mod h1:Xfx13T+u3nH6EEzgl9fBSO6nDRmze1FvnZNYkctQ2zw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30/go.mod h1:lEzEZnOosE7zi8Z6royW1cFJTD9fpab4Ul1SBrllewk=
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.8 h1:1QC1xoZg5XSes1CXQ20Y0qTaaeDj12e7bB0/Yw9sQwI= github.com/aws/aws-sdk-go-v2/service/route53 v1.64.1 h1:qL5ELChQ1o1ZbzHvsl8gzQfAZdbQtrDbUyzJkjkMKcg=
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.8/go.mod h1:+GpgmfX7rnzQ8WMbMCMnI+VOjPZCf9yaUlBmRTvQgFE= github.com/aws/aws-sdk-go-v2/service/route53 v1.64.1/go.mod h1:0hIRXFez1bZsDFMGkLZvNJbByTSVZ4sFZWpxZ39NPuM=
github.com/aws/aws-sdk-go-v2/service/signin v1.1.0 h1:yQo3eZ5qFaL1sJWqs1nL6j3yPHA2/R7c6tQ4T+0IO10= github.com/aws/aws-sdk-go-v2/service/signin v1.4.1 h1:V7ZZ300WPXGjvkyore5DGe0ljVPOxCXie/thWdtSBXE=
github.com/aws/aws-sdk-go-v2/service/signin v1.1.0/go.mod h1:3Zzou41Qt/ueXfIzHvTEjDNuR5IjCUBVF01SNhrt1e8= github.com/aws/aws-sdk-go-v2/service/signin v1.4.1/go.mod h1:mxC0nT/C8wMMS97DemZPzvUZxvIt+2Iq+eS3JdFZGgg=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.18 h1:ApLTFdAZfDhZSiY5uskwECKHkSNNF83y2Ru2r7SezWA= github.com/aws/aws-sdk-go-v2/service/sso v1.32.1 h1:gYFYh4iLLcAOJRLNPY2aD2g9DIhKn4eof8UkIrr1rTk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.18/go.mod h1:A9K9qx2l6nK89hp+a350FdGfRkrkH5HdiEjHbiy/Q/c= github.com/aws/aws-sdk-go-v2/service/sso v1.32.1/go.mod h1:u8af9Nqkmqnr96f7v9nHqzZT9XBwbXEkTiqT4ROuJSE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.1 h1:4VD7TIZOGzehrgQ8vDE+1c6BQW4ErZPGY8ohZT5LXEE= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1 h1:arjT9Cm3/WYbGmD5TUZHk4UQn4Lle1fUNZs5FC6CtF0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.1/go.mod h1:er0SFJfdV89Rit5hIJu/EXtv+qC2XMnxoksLmcUFkqM= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1/go.mod h1:DMPWJBjYs6+3+f/qhBFEFPPlQ6NlhWjai3dJNvipJ84=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.2 h1:XKnxlM4KZH1gktcsh3zSWc7GW4KivEv/OkifmHOhCUY= github.com/aws/aws-sdk-go-v2/service/sts v1.44.1 h1:RvfHDg+xvAeZ+5741vUEjpOVtYSIm93W2zhx10Xtydw=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.2/go.mod h1:KJYmkQaFB3SUW2j3aBkPsxNmAb4ZsSOvbvCpuxzHJA0= github.com/aws/aws-sdk-go-v2/service/sts v1.44.1/go.mod h1:9gdl4RrflIdpDb2TlXshWgR1F9TeCkvqDx77Vpr4Z/Q=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY=
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/baidubce/bce-sdk-go v0.9.267 h1:0l3UipFvWVsEv+FcontxWRs6ny8xvBeRISLbdNtO/bw= github.com/baidubce/bce-sdk-go v0.9.270 h1:WAYDTBdrE2FU+XQVdKReuDuK9QVCjRdekK3lr3ByDvg=
github.com/baidubce/bce-sdk-go v0.9.267/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= github.com/baidubce/bce-sdk-go v0.9.270/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@@ -204,8 +205,8 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/compose-spec/compose-go/v2 v2.11.0 h1:xoq/ootgIL6TsHmbJHrkuh7+bzjhPV3NHftHRPPyVXM= github.com/compose-spec/compose-go/v2 v2.13.0 h1:2+2oS3v4SrtAOBdZRAZYBsBy47D571p5EXMSCppmTtE=
github.com/compose-spec/compose-go/v2 v2.11.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg= github.com/compose-spec/compose-go/v2 v2.13.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -223,15 +224,12 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 h1:OGNva6WhsKst5OZf7eZOklDztV3hwtTHovdrLHV+MsA=
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs= github.com/docker/cli v29.6.2+incompatible h1:/bjePvcbbFTnRrMfWJBY7AjfICdsiLVgHn6LwTVOcqw=
github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.6.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
@@ -260,8 +258,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
@@ -281,8 +277,6 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg=
github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko= github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s= github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
@@ -292,14 +286,10 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-acme/alidns-20150109/v5 v5.4.1 h1:Bp+EDtIGxBVKnIq1hPECBFf6eueTiESYqsHkY+T+CIQ= github.com/go-acme/alidns-20150109/v5 v5.5.0 h1:/LUHZSQoaDqagi8wCq8f9Rx4AoVr/Jd4rKEV2IQYvxs=
github.com/go-acme/alidns-20150109/v5 v5.4.1/go.mod h1:L+SrZRmoQFx2UzOqng3z3+LA/gNgFCaLXrwr/ndyW6g= github.com/go-acme/alidns-20150109/v5 v5.5.0/go.mod h1:J2B9WBwSFfZalzlCqSkeyBkqBcWPEZMDkxwhOjjF0MA=
github.com/go-acme/esa-20240910/v3 v3.2.2 h1:dZ+HKddDMUcPr46gkFYdn5FbtJwIic59+MEaCaH/cxM= github.com/go-acme/esa-20240910/v3 v3.4.0 h1:t2ih22iRV+BzLkhvRG/i5hkyfkfi4FSPTZ3ves8oQ/0=
github.com/go-acme/esa-20240910/v3 v3.2.2/go.mod h1:qn4WK9pNgXoS5ypmiZfCWSYdOtWcte9GDuhDjKP752A= github.com/go-acme/esa-20240910/v3 v3.4.0/go.mod h1:QGkL4IcH2mT4K+VoTMMJnq9tOBPNa0E6QdSmKmn0D2s=
github.com/go-acme/lego/v5 v5.2.2 h1:KqFas/Ak2QDdU+Qm7MO9OEnS35zC000kPmM+5tLdZKk=
github.com/go-acme/lego/v5 v5.2.2/go.mod h1:H/hb7OJKmpVGa8zypgZh0ys5P4VUu++ZNgAEcOwq+OI=
github.com/go-acme/tencentclouddnspod v1.3.24 h1:uCSiOW1EJttcnOON+MVVyVDJguFL/Q4NIGkq1CrT9p8=
github.com/go-acme/tencentclouddnspod v1.3.24/go.mod h1:RKcB2wSoZncjBA0OEFj59s1ko1XDy+ZsAtk+9uMxUF0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -340,7 +330,6 @@ github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGK
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk= github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -358,12 +347,8 @@ github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/goh-chunlin/go-onedrive v1.1.1 h1:HGtHk5iG0MZ92zYUtaY04czfZPBIJUr12UuFc+PW8m4=
github.com/goh-chunlin/go-onedrive v1.1.1/go.mod h1:N8qIGHD7tryO734epiBKk5oXcpGwxKET/u3LuBHciTs=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -456,9 +441,6 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
github.com/h2non/filetype v1.1.1/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -489,8 +471,8 @@ github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.198 h1:/Qcq965fQ5AjM0lg8ndczox8jqAqp+5J8r4KFrfpb3Y= github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.205 h1:YnI2+56S5oxByz8BYChRrsmqTUmCp1Ww2KIzZtF4XOU=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.198/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI= github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.205/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -504,8 +486,8 @@ github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
@@ -514,11 +496,8 @@ github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aW
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
@@ -544,8 +523,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
@@ -571,8 +550,6 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -587,14 +564,13 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4= github.com/mattn/go-shellwords v1.0.14 h1:yUKzIgsCnosndOASY6/enly1EAuaXeFSQ7cdyA3OuYg=
github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.14/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM= github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM=
github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60VhnHxb2X3T1teMyv5A= github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60VhnHxb2X3T1teMyv5A=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
@@ -608,8 +584,8 @@ github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLT
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.2.0 h1:RCJM0R1XOsRs+A3x3UCaf3ZYbByDaLjFeAi+YCQEPhs= github.com/minio/minio-go/v7 v7.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw=
github.com/minio/minio-go/v7 v7.2.0/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0= github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@@ -681,13 +657,15 @@ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.2 h1:HFB2fbVIlhIfCfOW81bZFbiC/RvnpXSdhbF2/DJr134=
github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
@@ -717,8 +695,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -746,8 +724,8 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/qiniu/go-sdk/v7 v7.26.12 h1:AnWiKjBY62XpULoB/MySKdNmlUo9S9pQB7/0s7QueCo= github.com/qiniu/go-sdk/v7 v7.26.17 h1:SMjjdKak2j1eedAHreMiS6sObDOLuUrvRqPtlV4KPQo=
github.com/qiniu/go-sdk/v7 v7.26.12/go.mod h1:ri7fGwbio0pRDFr8EK5TUpx0DbnpIMJ2bMSDxGWfCbk= github.com/qiniu/go-sdk/v7 v7.26.17/go.mod h1:pTwVR1B+8SXcPLhDzBUasiKFTD9F7jRglRDR553BW3k=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
@@ -773,8 +751,8 @@ github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9t
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM= github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -829,12 +807,14 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.24/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.131/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.106 h1:YjsxDQwoiB+C0ISjnVk22Wbks02Ir+7Z5wmujcz69Vg= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.133 h1:EKJsaGHbifEKqWCdJ4tJNLxbJ5h/Pm3CaLKzcCS/pkw=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.106/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.133/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.3.131 h1:YfzhVkH30GipMEOGIx6nkaJv2E/mWN+G6Vvg+M7SqGg=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.3.131/go.mod h1:Eejvn572HmciFhWFMg6a+eZvkegnEzJrPSnJYKYZX2s=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0=
github.com/tencentyun/cos-go-sdk-v5 v0.7.73 h1:uFfgp1A7cQaAGR6QP9DsIkoEQ67b8ewj5r1RV6XB540= github.com/tencentyun/cos-go-sdk-v5 v0.7.74 h1:YCsGhIK+Q5vMcmAiQqPuMyhSQ3zN1p7U+dhlHQD++6c=
github.com/tencentyun/cos-go-sdk-v5 v0.7.73/go.mod h1:STbTNaNKq03u+gscPEGOahKzLcGSYOj6Dzc5zNay7Pg= github.com/tencentyun/cos-go-sdk-v5 v0.7.74/go.mod h1:STbTNaNKq03u+gscPEGOahKzLcGSYOj6Dzc5zNay7Pg=
github.com/tencentyun/qcloud-cos-sts-sdk v0.0.0-20250515025012-e0eec8a5d123/go.mod h1:b18KQa4IxHbxeseW1GcZox53d7J0z39VNONTxvvlkXw= github.com/tencentyun/qcloud-cos-sts-sdk v0.0.0-20250515025012-e0eec8a5d123/go.mod h1:b18KQa4IxHbxeseW1GcZox53d7J0z39VNONTxvvlkXw=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
@@ -860,8 +840,8 @@ github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0o
github.com/upyun/go-sdk v2.1.0+incompatible h1:OdjXghQ/TVetWV16Pz3C1/SUpjhGBVPr+cLiqZLLyq0= github.com/upyun/go-sdk v2.1.0+incompatible h1:OdjXghQ/TVetWV16Pz3C1/SUpjhGBVPr+cLiqZLLyq0=
github.com/upyun/go-sdk v2.1.0+incompatible/go.mod h1:eu3F5Uz4b9ZE5bE5QsCL6mgSNWRwfj0zpJ9J626HEqs= github.com/upyun/go-sdk v2.1.0+incompatible/go.mod h1:eu3F5Uz4b9ZE5bE5QsCL6mgSNWRwfj0zpJ9J626HEqs=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/volcengine/volc-sdk-golang v1.0.249 h1:BNFGca7CI2t9DktB+FlXBg7GZFezS3NnKAPLmRirLlw= github.com/volcengine/volc-sdk-golang v1.0.251 h1:N/3jkDfY+w/U/MC8T91Pvhpv8nBK4ESsxE7ykLWntlg=
github.com/volcengine/volc-sdk-golang v1.0.249/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM= github.com/volcengine/volc-sdk-golang v1.0.251/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
@@ -898,8 +878,8 @@ go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lL
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8= go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8=
go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -947,12 +927,10 @@ golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -973,8 +951,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1016,9 +994,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1073,14 +1050,13 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
@@ -1100,8 +1076,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1177,8 +1153,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1192,8 +1168,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1210,8 +1186,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1273,8 +1249,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1341,8 +1317,8 @@ google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478 h1:aLsVTW0lZ8+IY5u
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478/go.mod h1:YJAzKjfHIUHb9T+bfu8L7mthAp7VVXQBUs1PLdBWS7M= google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478/go.mod h1:YJAzKjfHIUHb9T+bfu8L7mthAp7VVXQBUs1PLdBWS7M=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1360,8 +1336,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1403,14 +1379,15 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -1453,5 +1430,5 @@ rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
software.sslmate.com/src/go-pkcs12 v0.7.1 h1:bxkUPRsvTPNRBZa4M/aSX4PyMOEbq3V8I6hbkG4F4Q8= software.sslmate.com/src/go-pkcs12 v0.7.3 h1:JBQD3FDqYjTeyDAeZQklj2ar88ykBLtALloPJHyAauU=
software.sslmate.com/src/go-pkcs12 v0.7.1/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= software.sslmate.com/src/go-pkcs12 v0.7.3/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

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