Repository: 1Panel-dev/1Panel
Pull request #8359: feat: Sync docker proxy to nodes
Author: login: lan-yonghui
Labels: name: do-not-merge/release-note-label-needed
name: approved
name: dev-v2
Created: 2025-04-09T09:11:23Z | Merged: 2025-04-09T10:25:56Z



Diff:
diff --git a/core/constant/common.go b/core/constant/common.go
index 2a51735146b3..acbafe42d647 100644
--- a/core/constant/common.go
+++ b/core/constant/common.go
@@ -33,6 +33,8 @@ const (
 
 	OneDriveRedirectURI = "http://localhost/login/authorized"
 	GoogleRedirectURI   = "http://localhost:8080"
+
+	DaemonJsonPath = "/etc/docker/daemon.json"
 )
 
 const (
diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts
index 975b2ae9fe0e..00916299319b 100644
--- a/frontend/src/lang/modules/en.ts
+++ b/frontend/src/lang/modules/en.ts
@@ -1520,6 +1520,11 @@ const message = {
         proxyDocker: 'Docker Proxy',
         proxyDockerHelper:
             'Synchronize proxy server configuration to Docker, support offline server image pulling and other operations',
+        syncToNode: 'Sync to Node',
+        syncToNodeHelper: 'Sync settings to other nodes',
+        nodes: 'Node',
+        selectNode: 'Select Node',
+        selectNodeError: 'Please select a node',
         apiInterface: 'API Interface',
         apiInterfaceClose: 'Once closed, API interfaces cannot be accessed. Do you want to continue?',
         apiInterfaceHelper: 'Provide panel support for API interface access',
diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts
index 492dae11b3d5..1e1236513de9 100644
--- a/frontend/src/lang/modules/ja.ts
+++ b/frontend/src/lang/modules/ja.ts
@@ -1461,6 +1461,11 @@ const message = {
         proxyDocker: 'Dockerプロキシ',
         proxyDockerHelper:
             'プロキシサーバーの構成をDockerに同期し、オフラインサーバーイメージの引っ張りやその他の操作をサポートします',
+        syncToNode: '子ノードに同期',
+        syncToNodeHelper: '選択したノードへの同期設定',
+        nodes: 'ノード',
+        selectNode: 'ノードを選択',
+        selectNodeError: 'ノードを選択してください',
         apiInterface: 'APIを有効にします',
         apiInterfaceClose: '閉じたら、APIインターフェイスにアクセスできません。続けたいですか？',
         apiInterfaceHelper: 'サードパーティのアプリケーションにAPIにアクセスできるようにします。',
diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts
index 128dbe233c88..780993a2c016 100644
--- a/frontend/src/lang/modules/ko.ts
+++ b/frontend/src/lang/modules/ko.ts
@@ -1443,6 +1443,11 @@ const message = {
         proxyDocker: 'Docker 프록시',
         proxyDockerHelper:
             '프록시 서버 구성을 Docker 에 동기화하여 오프라인 서버 이미지 가져오기 등의 작업을 지원합니다.',
+        syncToNode: '자식 노드로 동기화',
+        syncToNodeHelper: '다른 노드로 설정 동기화',
+        nodes: '노드',
+        selectNode: '노드 선택',
+        selectNodeError: '노드를 선택해 주세요',
         apiInterface: 'API 활성화',
         apiInterfaceClose: '비활성화하면 API 인터페이스에 접근할 수 없습니다. 계속하시겠습니까?',
         apiInterfaceHelper: '서드파티 애플리케이션이 API 에 접근할 수 있도록 허용합니다.',
diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts
index 97a19be950e6..deba48aac4e7 100644
--- a/frontend/src/lang/modules/ms.ts
+++ b/frontend/src/lang/modules/ms.ts
@@ -1505,6 +1505,11 @@ const message = {
         proxyDocker: 'Proksi Docker',
         proxyDockerHelper:
             'Selaraskan konfigurasi pelayan proksi ke Docker, menyokong tarikan imej pelayan luar talian dan operasi lain',
+        syncToNode: 'Penyegerakan ke nod anak',
+        syncToNodeHelper: 'Penyegerakan tetapan ke nod lain',
+        nodes: 'Nod',
+        selectNode: 'Pilih nod',
+        selectNodeError: 'Sila pilih nod',
         apiInterface: 'Aktifkan API',
         apiInterfaceClose: 'Setelah ditutup, antara muka API tidak boleh diakses. Adakah anda mahu meneruskan?',
         apiInterfaceHelper: 'Benarkan aplikasi pihak ketiga mengakses API.',
diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts
index 7db0544da1fb..64e9dea803d0 100644
--- a/frontend/src/lang/modules/pt-br.ts
+++ b/frontend/src/lang/modules/pt-br.ts
@@ -1490,6 +1490,11 @@ const message = {
         proxyDocker: 'Proxy Docker',
         proxyDockerHelper:
             'Sincronize a configuração do servidor proxy com o Docker, suportando operações de puxar imagens de servidor offline e outras',
+        syncToNode: 'Sincronizar para o nó filho',
+        syncToNodeHelper: 'Sincronizar de alerta para outros nós',
+        nodes: 'Nós',
+        selectNode: 'Selecionar nó',
+        selectNodeError: 'Por favor, selecione um nó',
         apiInterface: 'Habilitar API',
         apiInterfaceClose: 'Uma vez fechado, as interfaces da API não poderão ser acessadas. Deseja continuar?',
         apiInterfaceHelper: 'Permitir que aplicativos de terceiros acessem a API.',
diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts
index 2c07658ffcbe..a4abf34e829c 100644
--- a/frontend/src/lang/modules/ru.ts
+++ b/frontend/src/lang/modules/ru.ts
@@ -1494,6 +1494,11 @@ const message = {
         proxyDocker: 'Прокси Docker',
         proxyDockerHelper:
             'Синхронизировать конфигурацию прокси-сервера с Docker, поддержка офлайн загрузки образов и других операций',
+        syncToNode: 'Синхронизация с дочерним узлом',
+        syncToNodeHelper: 'Синхронизация настроек с другими узлами',
+        nodes: 'Узлы',
+        selectNode: 'Выберите узел',
+        selectNodeError: 'Пожалуйста, выберите узел',
         apiInterface: 'Включить API',
         apiInterfaceClose: 'После закрытия API-интерфейсы будут недоступны. Хотите продолжить?',
         apiInterfaceHelper: 'Разрешить сторонним приложениям доступ к API.',
diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts
index 271dd5fed759..921ab2d75613 100644
--- a/frontend/src/lang/modules/zh-Hant.ts
+++ b/frontend/src/lang/modules/zh-Hant.ts
@@ -1439,6 +1439,11 @@ const message = {
         proxyPasswdKeep: '記住密碼',
         proxyDocker: 'Docker 代理',
         proxyDockerHelper: '將代理伺服器配寘同步至 Docker，支持離線服務器拉取鏡像等操作',
+        syncToNode: '同步至子節點',
+        syncToNodeHelper: '同步設置至其他節點',
+        nodes: '節點',
+        selectNode: '選擇節點',
+        selectNodeError: '請選擇節點',
         apiInterface: 'API 接口',
         apiInterfaceClose: '關閉後將不能使用 API 接口進行訪問，是否繼續？',
         apiInterfaceHelper: '提供面板支持 API 接口訪問',
diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts
index afd61a2c676c..22615b232f4f 100644
--- a/frontend/src/lang/modules/zh.ts
+++ b/frontend/src/lang/modules/zh.ts
@@ -1435,6 +1435,11 @@ const message = {
         proxyPasswdKeep: '记住密码',
         proxyDocker: 'Docker 代理',
         proxyDockerHelper: '将代理服务器配置同步至 Docker，支持离线服务器拉取镜像等操作',
+        syncToNode: '同步至子节点',
+        syncToNodeHelper: '同步设置至所选节点',
+        nodes: '节点',
+        selectNode: '选择节点',
+        selectNodeError: '请选择节点',
         apiInterface: 'API 接口',
         apiInterfaceClose: '关闭后将不能使用 API 接口进行访问，是否继续？',
         apiInterfaceHelper: '提供面板支持 API 接口访问',
diff --git a/frontend/src/views/setting/panel/index.vue b/frontend/src/views/setting/panel/index.vue
index 191c9058cb07..b3fac035002f 100644
--- a/frontend/src/views/setting/panel/index.vue
+++ b/frontend/src/views/setting/panel/index.vue
@@ -139,7 +139,7 @@
                                 </el-input>
                             </el-form-item>
 
-                            <el-form-item :label="$t('setting.proxy')" prop="proxyShow">
+                            <el-form-item :label="$t('setting.proxy')" prop="proxyShow" v-if="isMaster">
                                 <el-input disabled v-model="form.proxyShow">
                                     <template #append>
                                         <el-button @click="onChangeProxy" icon="Setting">
@@ -229,7 +229,7 @@ const loading = ref(false);
 const i18n = useI18n();
 const globalStore = GlobalStore();
 
-const { isMasterProductPro } = storeToRefs(globalStore);
+const { isMasterProductPro, isMaster } = storeToRefs(globalStore);
 
 const { switchTheme } = useTheme();
 const mobile = computed(() => {
@@ -266,6 +266,8 @@ const form = reactive({
     proxyPasswd: '',
     proxyPasswdKeep: '',
     proxyDocker: '',
+    proxyDockerSyncToNode: '',
+    proxyDockerSyncNodes: '',
 
     apiInterfaceStatus: 'Disable',
     apiKey: '',
@@ -339,6 +341,8 @@ const search = async () => {
                 : '{"light":"#005eeb","dark":"#F0BE96"}';
             globalStore.themeConfig.theme = form.theme;
             form.proxyDocker = xpackRes.data.proxyDocker;
+            form.proxyDockerSyncToNode = xpackRes.data.proxyDockerSyncToNode;
+            form.proxyDockerSyncNodes = xpackRes.data.proxyDockerSyncNodes;
         }
     } else {
         globalStore.themeConfig.theme = form.theme;
@@ -369,6 +373,8 @@ const onChangeProxy = () => {
         passwd: form.proxyPasswd,
         passwdKeep: form.proxyPasswdKeep,
         proxyDocker: form.proxyDocker,
+        proxyDockerSyncToNode: form.proxyDockerSyncToNode,
+        proxyDockerSyncNodes: form.proxyDockerSyncNodes,
     });
 };
 
diff --git a/frontend/src/views/setting/panel/proxy/index.vue b/frontend/src/views/setting/panel/proxy/index.vue
index 651ede7ca861..152b81c920be 100644
--- a/frontend/src/views/setting/panel/proxy/index.vue
+++ b/frontend/src/views/setting/panel/proxy/index.vue
@@ -46,9 +46,41 @@
                     <el-checkbox v-model="form.proxyPasswdKeepItem" :label="$t('setting.proxyPasswdKeep')" />
                 </el-form-item>
                 <el-form-item v-if="isMasterProductPro">
-                    <el-checkbox v-model="form.proxyDocker" :label="$t('setting.proxyDocker')" />
+                    <el-checkbox v-model="form.proxyDocker" @change="onChange" :label="$t('setting.proxyDocker')" />
                     <span class="input-help">{{ $t('setting.proxyDockerHelper') }}</span>
                 </el-form-item>
+                <div v-if="form.proxyDocker">
+                    <el-form-item prop="syncToNode">
+                        <el-switch
+                            @change="getNodes"
+                            v-model="form.syncToNode"
+                            active-value="Enable"
+                            inactive-value="Disable"
+                        ></el-switch>
+                        <span class="ml-2.5 el-checkbox">{{ $t('setting.syncToNode') }}</span>
+                        <span class="input-help">{{ $t('setting.syncToNodeHelper') }}</span>
+                    </el-form-item>
+                    <el-form-item :label="$t('setting.nodes')" prop="nodes" v-if="form.syncToNode === 'Enable'">
+                        <el-row :gutter="20">
+                            <el-col :span="12">
+                                <el-select v-model="form.selectNode" class="p-w-200">
+                                    <el-option :label="$t('commons.table.all')" value="all"></el-option>
+                                    <el-option :label="$t('setting.selectNode')" value="select"></el-option>
+                                </el-select>
+                            </el-col>
+                            <el-col :span="12" v-if="form.selectNode === 'select'">
+                                <el-select v-model="form.nodes" multiple class="p-w-200">
+                                    <el-option
+                                        v-for="item in nodeList"
+                                        :key="item.id"
+                                        :label="item.name"
+                                        :value="item.id"
+                                    ></el-option>
+                                </el-select>
+                            </el-col>
+                        </el-row>
+                    </el-form-item>
+                </div>
             </div>
         </el-form>
         <template #footer>
@@ -70,13 +102,13 @@ import i18n from '@/lang';
 import { MsgSuccess } from '@/utils/message';
 import { FormInstance } from 'element-plus';
 import { reactive, ref } from 'vue';
-import { updateProxy } from '@/api/modules/setting';
+import { listNodeOptions, updateProxy } from '@/api/modules/setting';
 import { GlobalStore } from '@/store';
 import { storeToRefs } from 'pinia';
-import { updateXpackSettingByKey } from '@/utils/xpack';
 import { updateDaemonJson } from '@/api/modules/container';
 import ConfirmDialog from '@/components/confirm-dialog/index.vue';
 import { escapeProxyURL } from '@/utils/util';
+import { updateDockerProxySetting } from '@/xpack/api/modules/setting';
 
 const globalStore = GlobalStore();
 const emit = defineEmits<{ (e: 'search'): void }>();
@@ -103,7 +135,11 @@ const form = reactive({
     proxyPasswdKeep: '',
     proxyPasswdKeepItem: false,
     proxyDocker: false,
+    syncToNode: 'Enable',
+    selectNode: 'all',
+    nodes: [],
 });
+const nodeList = ref([]);
 
 interface DialogProps {
     url: string;
@@ -113,6 +149,8 @@ interface DialogProps {
     passwd: string;
     passwdKeep: string;
     proxyDocker: string;
+    proxyDockerSyncToNode: string;
+    proxyDockerSyncNodes: string;
 }
 const acceptParams = (params: DialogProps): void => {
     if (params.url) {
@@ -129,11 +167,24 @@ const acceptParams = (params: DialogProps): void => {
     form.proxyUser = params.user;
     form.proxyPasswd = params.passwd;
     form.proxyDocker = params.proxyDocker !== '';
+    if (form.proxyDocker) {
+        getNodes();
+        form.syncToNode = params.proxyDockerSyncToNode;
+        form.nodes = params.proxyDockerSyncNodes ? JSON.parse(params.proxyDockerSyncNodes) : [];
+        form.selectNode = form.nodes.length > 0 ? 'select' : 'all';
+    }
+
     proxyDockerVisible.value = params.proxyDocker !== '';
     passwordVisible.value = true;
     form.proxyPasswdKeepItem = params.passwdKeep === 'Enable';
 };
 
+const getNodes = () => {
+    if (form.syncToNode === 'Enable' && nodeList.value.length === 0) {
+        listNodes();
+    }
+};
+
 const submitChangePassword = async (formEl: FormInstance | undefined) => {
     if (!formEl) return;
     formEl.validate(async (valid) => {
@@ -216,16 +267,27 @@ const onSubmit = async () => {
         if (isClose || params.proxyDocker === false) {
             proxyUrl = '';
         }
-        await updateXpackSettingByKey('ProxyDocker', proxyUrl);
-        await updateDaemonJson(`${form.proxyType}-proxy`, proxyUrl);
+        let param = {
+            proxyDocker: proxyUrl,
+            proxyDockerSyncToNode: form.syncToNode,
+            proxyDockerSyncNodes: JSON.stringify(form.nodes),
+        };
+        await updateDockerProxySetting(param);
         emit('search');
         handleClose();
         MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
-    } catch {
+    } finally {
         loading.value = false;
     }
 };
 
+const listNodes = async () => {
+    try {
+        const res = await listNodeOptions();
+        nodeList.value = res.data || [];
+    } catch (error) {}
+};
+
 const handleClose = () => {
     passwordVisible.value = false;
 };


Reviews:
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T09:11:46Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T09:11:51Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T09:11:54Z
user: login: wanghe-fit2cloud
body: /lgtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T10:24:27Z

Review comments:
diff_hunk: @@ -216,16 +267,27 @@ const onSubmit = async () => {
         if (isClose || params.proxyDocker === false) {
             proxyUrl = '';
         }
-        await updateXpackSettingByKey('ProxyDocker', proxyUrl);
-        await updateDaemonJson(`${form.proxyType}-proxy`, proxyUrl);
+        let param = {
+            proxyDocker: proxyUrl,
+            proxyDockerSyncToNode: form.syncToNode,
+            proxyDockerSyncNodes: JSON.stringify(form.nodes),
+        };
+        await updateDockerProxySetting(param);
         emit('search');
         handleClose();
         MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
-    } catch {
+    } finally {
         loading.value = false;
     }
 };
 
+const listNodes = async () => {
+    try {
+        const res = await listNodeOptions();
+        nodeList.value = res.data || [];
+    } catch (error) {}
+};
+
 const handleClose = () => {
     passwordVisible.value = false;
 };
path: frontend/src/views/setting/panel/proxy/index.vue
user: login: wanghe-fit2cloud
body: No significant irregularities or critical issues were found in the code snippet provided.

### Optimization Suggestions:

1. **Extracted Functions**: Extracting the `getNodes` function to handle the logic related to fetching node options has improved maintainability.

2. **Error Handling and Finally Block in Submission Code**:
   ```diff
-    await updateXpackSettingByKey('ProxyDocker', proxyUrl);
-    await updateDaemonJson(`${form.proxyType}-proxy`, proxyUrl);
+    let param = {
+        proxyDocker: proxyUrl,
+        proxyDockerSyncToNode: form.syncToNode,
+        proxyDockerSyncNodes: JSON.stringify(form.nodes),
+    };
+    await updateDockerProxySetting(param);
   ```
   
   Added a `finally` block to ensure that the loading indicator is removed regardless of whether an exception occurs during submission.

3. **Removed Unnecessary Checks for Selected Node**:
   ```diff
-    if (form.selectNode === 'all') {
-      this.form.nodes = [];
-    } else if (this.form.selectNode === 'select') {
-      // existing selection logic here
-    }
   ```
   This can be merged into a single condition statement:

```javascript
if (['all', ''].includes(form.selectNode)) {
  this.form.nodes = form.selectNode === 'all' ? [] : [];
}
```

These optimizations should result in cleaner and more efficient code while maintaining readability.
created_at: 2025-04-09T09:11:46Z
author_association: MEMBER
diff_hunk: @@ -369,6 +373,8 @@ const onChangeProxy = () => {
         passwd: form.proxyPasswd,
         passwdKeep: form.proxyPasswdKeep,
         proxyDocker: form.proxyDocker,
+        proxyDockerSyncToNode: form.proxyDockerSyncToNode,
+        proxyDockerSyncNodes: form.proxyDockerSyncNodes,
     });
 };
 
path: frontend/src/views/setting/panel/index.vue
user: login: wanghe-fit2cloud
body: Some potential issues:
1. The `v-if="isMaster"` condition should be set to `isMasterProductPro` instead of directly using `isMaster`.
2. The updated fields (e.g., `proxyDockerSyncToNode`, `proxyDockerSynNodes`) were added without being validated in the Vue.js template or form rules.
3. Ensure that there's logic handling changes for these new properties.

Optimization suggestions:
1. Consider adding validation checks for new props like `proxyDockerSyncToNode` and `proxyDockerSyncNodes`.

If you need further assistance with code review or optimization, please provide more details or context!
created_at: 2025-04-09T09:11:51Z
author_association: MEMBER
diff_hunk: @@ -1520,6 +1520,11 @@ const message = {
         proxyDocker: 'Docker Proxy',
         proxyDockerHelper:
             'Synchronize proxy server configuration to Docker, support offline server image pulling and other operations',
+        syncToNode: 'Sync to Node',
+        syncToNodeHelper: 'Sync settings to other nodes',
+        nodes: 'Node',
+        selectNode: 'Select Node',
+        selectNodeError: 'Please select a node',
         apiInterface: 'API Interface',
         apiInterfaceClose: 'Once closed, API interfaces cannot be accessed. Do you want to continue?',
         apiInterfaceHelper: 'Provide panel support for API interface access',
path: frontend/src/lang/modules/en.ts
user: login: wanghe-fit2cloud
body: There are no noticeable irregularities, potential issues, or suggestions for optimization in the provided code snippet. The changes appear to enhance user experience on the dashboard by adding features for syncing configurations and selecting nodes from different environments.
created_at: 2025-04-09T09:11:54Z
author_association: MEMBER

PR comments:
user: login: f2c-ci-robot[bot]
created_at: 2025-04-09T09:11:27Z
body: Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our [release note process](https://git.k8s.io/community/contributors/guide/release-notes.md) to remove it.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-09T09:13:09Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=1Panel-dev_1Panel&pullRequest=8359) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=1Panel-dev_1Panel&pullRequest=8359&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=1Panel-dev_1Panel&pullRequest=8359&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=1Panel-dev_1Panel&pullRequest=8359&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=1Panel-dev_1Panel&pullRequest=8359&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=1Panel-dev_1Panel&pullRequest=8359&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=1Panel-dev_1Panel&pullRequest=8359)


author_association: NONE
user: login: wanghe-fit2cloud
created_at: 2025-04-09T10:24:36Z
body: /approve
author_association: MEMBER
user: login: f2c-ci-robot[bot]
created_at: 2025-04-09T10:24:45Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/1Panel-dev/1Panel/pull/8359#issuecomment-2789188676" title="Approved">wanghe-fit2cloud</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=1Panel-dev%2F1Panel).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/1Panel-dev/1Panel/blob/dev-v2/OWNERS)~~ [wanghe-fit2cloud]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE