mirror of
				https://gitee.com/dromara/mayfly-go
				synced 2025-11-04 08:20:25 +08:00 
			
		
		
		
	feat: 数据库、redis、mongo支持ssh隧道等
This commit is contained in:
		
							
								
								
									
										36
									
								
								README.en.md
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								README.en.md
									
									
									
									
									
								
							@@ -1,36 +0,0 @@
 | 
				
			|||||||
# mayfly-go
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Description
 | 
					 | 
				
			||||||
golang实现linux运维等
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Software Architecture
 | 
					 | 
				
			||||||
Software architecture description
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Installation
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1.  xxxx
 | 
					 | 
				
			||||||
2.  xxxx
 | 
					 | 
				
			||||||
3.  xxxx
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Instructions
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1.  xxxx
 | 
					 | 
				
			||||||
2.  xxxx
 | 
					 | 
				
			||||||
3.  xxxx
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Contribution
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1.  Fork the repository
 | 
					 | 
				
			||||||
2.  Create Feat_xxx branch
 | 
					 | 
				
			||||||
3.  Commit your code
 | 
					 | 
				
			||||||
4.  Create Pull Request
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Gitee Feature
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1.  You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
 | 
					 | 
				
			||||||
2.  Gitee blog [blog.gitee.com](https://blog.gitee.com)
 | 
					 | 
				
			||||||
3.  Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
 | 
					 | 
				
			||||||
4.  The most valuable open source project [GVP](https://gitee.com/gvp)
 | 
					 | 
				
			||||||
5.  The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
 | 
					 | 
				
			||||||
6.  The most popular members  [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
 | 
					 | 
				
			||||||
							
								
								
									
										33
									
								
								mayfly_go_web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										33
									
								
								mayfly_go_web/package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -15,6 +15,7 @@
 | 
				
			|||||||
        "cropperjs": "^1.5.11",
 | 
					        "cropperjs": "^1.5.11",
 | 
				
			||||||
        "echarts": "^5.3.3",
 | 
					        "echarts": "^5.3.3",
 | 
				
			||||||
        "element-plus": "^2.2.9",
 | 
					        "element-plus": "^2.2.9",
 | 
				
			||||||
 | 
					        "jsencrypt": "^3.2.1",
 | 
				
			||||||
        "jsoneditor": "^9.9.0",
 | 
					        "jsoneditor": "^9.9.0",
 | 
				
			||||||
        "lodash": "^4.17.21",
 | 
					        "lodash": "^4.17.21",
 | 
				
			||||||
        "mitt": "^3.0.0",
 | 
					        "mitt": "^3.0.0",
 | 
				
			||||||
@@ -24,7 +25,7 @@
 | 
				
			|||||||
        "sql-formatter": "^7.0.3",
 | 
					        "sql-formatter": "^7.0.3",
 | 
				
			||||||
        "vue": "^3.2.37",
 | 
					        "vue": "^3.2.37",
 | 
				
			||||||
        "vue-clipboard3": "^1.0.1",
 | 
					        "vue-clipboard3": "^1.0.1",
 | 
				
			||||||
        "vue-router": "^4.0.16",
 | 
					        "vue-router": "^4.1.2",
 | 
				
			||||||
        "vuex": "^4.0.2",
 | 
					        "vuex": "^4.0.2",
 | 
				
			||||||
        "xterm": "^4.19.0",
 | 
					        "xterm": "^4.19.0",
 | 
				
			||||||
        "xterm-addon-fit": "^0.5.0"
 | 
					        "xterm-addon-fit": "^0.5.0"
 | 
				
			||||||
@@ -1880,6 +1881,11 @@
 | 
				
			|||||||
        "js-yaml": "bin/js-yaml.js"
 | 
					        "js-yaml": "bin/js-yaml.js"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/jsencrypt": {
 | 
				
			||||||
 | 
					      "version": "3.2.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmmirror.com/jsencrypt/-/jsencrypt-3.2.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-k1sD5QV0KPn+D8uG9AdGzTQuamt82QZ3A3l6f7TRwMU6Oi2Vg0BsL+wZIQBONcraO1pc78ExMdvmBBJ8WhNYUA=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/json-schema-traverse": {
 | 
					    "node_modules/json-schema-traverse": {
 | 
				
			||||||
      "version": "0.4.1",
 | 
					      "version": "0.4.1",
 | 
				
			||||||
      "resolved": "https://registry.npm.taobao.org/json-schema-traverse/download/json-schema-traverse-0.4.1.tgz",
 | 
					      "resolved": "https://registry.npm.taobao.org/json-schema-traverse/download/json-schema-traverse-0.4.1.tgz",
 | 
				
			||||||
@@ -2931,15 +2937,11 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/vue-router": {
 | 
					    "node_modules/vue-router": {
 | 
				
			||||||
      "version": "4.0.16",
 | 
					      "version": "4.1.2",
 | 
				
			||||||
      "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.0.16.tgz",
 | 
					      "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.2.tgz",
 | 
				
			||||||
      "integrity": "sha512-JcO7cb8QJLBWE+DfxGUL3xUDOae/8nhM1KVdnudadTAORbuxIC/xAydC5Zr/VLHUDQi1ppuTF5/rjBGzgzrJNA==",
 | 
					      "integrity": "sha512-5BP1qXFncVRwgV/XnqzsKApdMjQPqWIpoUBdL1ynz8HyLxIX/UDAx7Ql2BjmA5CXT/p61JfZvkpiFWFpaqcfag==",
 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@vue/devtools-api": "^6.0.0"
 | 
					        "@vue/devtools-api": "^6.1.4"
 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "url": "https://github.com/sponsors/posva"
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "peerDependencies": {
 | 
					      "peerDependencies": {
 | 
				
			||||||
        "vue": "^3.2.0"
 | 
					        "vue": "^3.2.0"
 | 
				
			||||||
@@ -4358,6 +4360,11 @@
 | 
				
			|||||||
        "argparse": "^2.0.1"
 | 
					        "argparse": "^2.0.1"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "jsencrypt": {
 | 
				
			||||||
 | 
					      "version": "3.2.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmmirror.com/jsencrypt/-/jsencrypt-3.2.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-k1sD5QV0KPn+D8uG9AdGzTQuamt82QZ3A3l6f7TRwMU6Oi2Vg0BsL+wZIQBONcraO1pc78ExMdvmBBJ8WhNYUA=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "json-schema-traverse": {
 | 
					    "json-schema-traverse": {
 | 
				
			||||||
      "version": "0.4.1",
 | 
					      "version": "0.4.1",
 | 
				
			||||||
      "resolved": "https://registry.npm.taobao.org/json-schema-traverse/download/json-schema-traverse-0.4.1.tgz",
 | 
					      "resolved": "https://registry.npm.taobao.org/json-schema-traverse/download/json-schema-traverse-0.4.1.tgz",
 | 
				
			||||||
@@ -5044,11 +5051,11 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "vue-router": {
 | 
					    "vue-router": {
 | 
				
			||||||
      "version": "4.0.16",
 | 
					      "version": "4.1.2",
 | 
				
			||||||
      "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.0.16.tgz",
 | 
					      "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.2.tgz",
 | 
				
			||||||
      "integrity": "sha512-JcO7cb8QJLBWE+DfxGUL3xUDOae/8nhM1KVdnudadTAORbuxIC/xAydC5Zr/VLHUDQi1ppuTF5/rjBGzgzrJNA==",
 | 
					      "integrity": "sha512-5BP1qXFncVRwgV/XnqzsKApdMjQPqWIpoUBdL1ynz8HyLxIX/UDAx7Ql2BjmA5CXT/p61JfZvkpiFWFpaqcfag==",
 | 
				
			||||||
      "requires": {
 | 
					      "requires": {
 | 
				
			||||||
        "@vue/devtools-api": "^6.0.0"
 | 
					        "@vue/devtools-api": "^6.1.4"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "vuex": {
 | 
					    "vuex": {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,10 +23,13 @@
 | 
				
			|||||||
                    </el-select>
 | 
					                    </el-select>
 | 
				
			||||||
                </el-form-item>
 | 
					                </el-form-item>
 | 
				
			||||||
                <el-form-item prop="host" label="host:" required>
 | 
					                <el-form-item prop="host" label="host:" required>
 | 
				
			||||||
 | 
					                    <el-col :span="18">
 | 
				
			||||||
                        <el-input v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
 | 
					                        <el-input v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
 | 
				
			||||||
                </el-form-item>
 | 
					                    </el-col>
 | 
				
			||||||
                <el-form-item prop="port" label="port:" required>
 | 
					                    <el-col style="text-align: center" :span="1">:</el-col>
 | 
				
			||||||
 | 
					                    <el-col :span="5">
 | 
				
			||||||
                        <el-input type="number" v-model.number="form.port" placeholder="请输入端口"></el-input>
 | 
					                        <el-input type="number" v-model.number="form.port" placeholder="请输入端口"></el-input>
 | 
				
			||||||
 | 
					                    </el-col>
 | 
				
			||||||
                </el-form-item>
 | 
					                </el-form-item>
 | 
				
			||||||
                <el-form-item prop="username" label="用户名:" required>
 | 
					                <el-form-item prop="username" label="用户名:" required>
 | 
				
			||||||
                    <el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
 | 
					                    <el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
 | 
				
			||||||
@@ -68,26 +71,22 @@
 | 
				
			|||||||
                    <el-button v-else class="ml5 mt5" size="small" @click="showInputDb"> + 添加数据库 </el-button>
 | 
					                    <el-button v-else class="ml5 mt5" size="small" @click="showInputDb"> + 添加数据库 </el-button>
 | 
				
			||||||
                </el-form-item>
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <el-form-item prop="enable_ssh" label="SSH:" v-if="form.type === 'mysql'">
 | 
					                <el-form-item prop="enableSshTunnel" label="SSH隧道:">
 | 
				
			||||||
                    <el-checkbox v-model="form.enable_ssh" :true-label=1 :false-label=0></el-checkbox>
 | 
					                    <el-col :span="3">
 | 
				
			||||||
                </el-form-item>
 | 
					                        <el-checkbox @change="getSshTunnelMachines" v-model="form.enableSshTunnel" :true-label="1" :false-label="-1"></el-checkbox>
 | 
				
			||||||
                <el-form-item prop="ssh_host" label="SSH Host:" v-if="form.enable_ssh === 1 && form.type === 'mysql'">
 | 
					                    </el-col>
 | 
				
			||||||
                    <el-input v-model.trim="form.ssh_host" placeholder="请输入主机ip" auto-complete="off"></el-input>
 | 
					                    <el-col :span="2" v-if="form.enableSshTunnel == 1"> 机器: </el-col>
 | 
				
			||||||
                </el-form-item>
 | 
					                    <el-col :span="19" v-if="form.enableSshTunnel == 1">
 | 
				
			||||||
                <el-form-item prop="ssh_user" label="SSH User:" v-if="form.enable_ssh === 1 && form.type === 'mysql'">
 | 
					                        <el-select style="width: 100%" v-model="form.sshTunnelMachineId" placeholder="请选择SSH隧道机器">
 | 
				
			||||||
                    <el-input v-model.trim="form.ssh_user" placeholder="请输入用户名"></el-input>
 | 
					                            <el-option
 | 
				
			||||||
                </el-form-item>
 | 
					                                v-for="item in sshTunnelMachineList"
 | 
				
			||||||
                <el-form-item prop="ssh_pass" label="SSH Pass:" v-if="form.enable_ssh === 1 && form.type === 'mysql'">
 | 
					                                :key="item.id"
 | 
				
			||||||
                    <el-input
 | 
					                                :label="`${item.ip}:${item.port} [${item.name}]`"
 | 
				
			||||||
                        type="password"
 | 
					                                :value="item.id"
 | 
				
			||||||
                        show-password
 | 
					                            >
 | 
				
			||||||
                        v-model.trim="form.ssh_pass"
 | 
					                            </el-option>
 | 
				
			||||||
                        placeholder="请输入密码,修改操作可不填"
 | 
					                        </el-select>
 | 
				
			||||||
                        autocomplete="new-password"
 | 
					                    </el-col>
 | 
				
			||||||
                    ></el-input>
 | 
					 | 
				
			||||||
                </el-form-item>
 | 
					 | 
				
			||||||
                <el-form-item prop="ssh_port" label="SSH Port:" v-if="form.enable_ssh === 1 && form.type === 'mysql'">
 | 
					 | 
				
			||||||
                    <el-input type="number" v-model.number="form.ssh_port" placeholder="请输入端口"></el-input>
 | 
					 | 
				
			||||||
                </el-form-item>
 | 
					                </el-form-item>
 | 
				
			||||||
            </el-form>
 | 
					            </el-form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -105,6 +104,7 @@
 | 
				
			|||||||
import { toRefs, reactive, nextTick, watch, defineComponent, ref } from 'vue';
 | 
					import { toRefs, reactive, nextTick, watch, defineComponent, ref } from 'vue';
 | 
				
			||||||
import { dbApi } from './api';
 | 
					import { dbApi } from './api';
 | 
				
			||||||
import { projectApi } from '../project/api.ts';
 | 
					import { projectApi } from '../project/api.ts';
 | 
				
			||||||
 | 
					import { machineApi } from '../machine/api.ts';
 | 
				
			||||||
import { ElMessage } from 'element-plus';
 | 
					import { ElMessage } from 'element-plus';
 | 
				
			||||||
import type { ElInput } from 'element-plus';
 | 
					import type { ElInput } from 'element-plus';
 | 
				
			||||||
import { notBlank } from '@/common/assert';
 | 
					import { notBlank } from '@/common/assert';
 | 
				
			||||||
@@ -135,6 +135,7 @@ export default defineComponent({
 | 
				
			|||||||
            projects: [],
 | 
					            projects: [],
 | 
				
			||||||
            envs: [],
 | 
					            envs: [],
 | 
				
			||||||
            databaseList: [] as any,
 | 
					            databaseList: [] as any,
 | 
				
			||||||
 | 
					            sshTunnelMachineList: [],
 | 
				
			||||||
            inputDbVisible: false,
 | 
					            inputDbVisible: false,
 | 
				
			||||||
            inputDbValue: '',
 | 
					            inputDbValue: '',
 | 
				
			||||||
            form: {
 | 
					            form: {
 | 
				
			||||||
@@ -149,11 +150,8 @@ export default defineComponent({
 | 
				
			|||||||
                projectId: null,
 | 
					                projectId: null,
 | 
				
			||||||
                envId: null,
 | 
					                envId: null,
 | 
				
			||||||
                env: null,
 | 
					                env: null,
 | 
				
			||||||
                enable_ssh: null,
 | 
					                enableSshTunnel: null,
 | 
				
			||||||
                ssh_host: null,
 | 
					                sshTunnelMachineId: null,
 | 
				
			||||||
                ssh_user: null,
 | 
					 | 
				
			||||||
                ssh_pass: null,
 | 
					 | 
				
			||||||
                ssh_port: 22,
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            btnLoading: false,
 | 
					            btnLoading: false,
 | 
				
			||||||
            rules: {
 | 
					            rules: {
 | 
				
			||||||
@@ -188,14 +186,7 @@ export default defineComponent({
 | 
				
			|||||||
                host: [
 | 
					                host: [
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        required: true,
 | 
					                        required: true,
 | 
				
			||||||
                        message: '请输入主机ip',
 | 
					                        message: '请输入主机ip和port',
 | 
				
			||||||
                        trigger: ['change', 'blur'],
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
                port: [
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        required: true,
 | 
					 | 
				
			||||||
                        message: '请输入端口',
 | 
					 | 
				
			||||||
                        trigger: ['change', 'blur'],
 | 
					                        trigger: ['change', 'blur'],
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
@@ -217,6 +208,10 @@ export default defineComponent({
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        watch(props, (newValue) => {
 | 
					        watch(props, (newValue) => {
 | 
				
			||||||
 | 
					            state.dialogVisible = newValue.visible;
 | 
				
			||||||
 | 
					            if (!state.dialogVisible) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            state.projects = newValue.projects;
 | 
					            state.projects = newValue.projects;
 | 
				
			||||||
            if (newValue.db) {
 | 
					            if (newValue.db) {
 | 
				
			||||||
                getEnvs(newValue.db.projectId);
 | 
					                getEnvs(newValue.db.projectId);
 | 
				
			||||||
@@ -225,10 +220,10 @@ export default defineComponent({
 | 
				
			|||||||
                state.databaseList = newValue.db.database.split(' ');
 | 
					                state.databaseList = newValue.db.database.split(' ');
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                state.envs = [];
 | 
					                state.envs = [];
 | 
				
			||||||
                state.form = { port: 3306 } as any;
 | 
					                state.form = { port: 3306, enableSshTunnel: -1 } as any;
 | 
				
			||||||
                state.databaseList = [];
 | 
					                state.databaseList = [];
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            state.dialogVisible = newValue.visible;
 | 
					            getSshTunnelMachines();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const handleClose = (db: string) => {
 | 
					        const handleClose = (db: string) => {
 | 
				
			||||||
@@ -259,6 +254,13 @@ export default defineComponent({
 | 
				
			|||||||
            state.form.database = state.databaseList.length == 0 ? '' : state.databaseList.join(' ');
 | 
					            state.form.database = state.databaseList.length == 0 ? '' : state.databaseList.join(' ');
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const getSshTunnelMachines = async () => {
 | 
				
			||||||
 | 
					            if (state.form.enableSshTunnel == 1 && state.sshTunnelMachineList.length == 0) {
 | 
				
			||||||
 | 
					                const res = await machineApi.list.request({ pageNum: 1, pageSize: 100 });
 | 
				
			||||||
 | 
					                state.sshTunnelMachineList = res.list;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const getEnvs = async (projectId: any) => {
 | 
					        const getEnvs = async (projectId: any) => {
 | 
				
			||||||
            state.envs = await projectApi.projectEnvs.request({ projectId });
 | 
					            state.envs = await projectApi.projectEnvs.request({ projectId });
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
@@ -291,7 +293,7 @@ export default defineComponent({
 | 
				
			|||||||
                if (valid) {
 | 
					                if (valid) {
 | 
				
			||||||
                    const reqForm = { ...state.form };
 | 
					                    const reqForm = { ...state.form };
 | 
				
			||||||
                    reqForm.password = await RsaEncrypt(reqForm.password);
 | 
					                    reqForm.password = await RsaEncrypt(reqForm.password);
 | 
				
			||||||
                    reqForm.ssh_pass = await RsaEncrypt(reqForm.ssh_pass);
 | 
					                    // reqForm.ssh_pass = await RsaEncrypt(reqForm.ssh_pass);
 | 
				
			||||||
                    dbApi.saveDb.request(reqForm).then(() => {
 | 
					                    dbApi.saveDb.request(reqForm).then(() => {
 | 
				
			||||||
                        ElMessage.success('保存成功');
 | 
					                        ElMessage.success('保存成功');
 | 
				
			||||||
                        emit('val-change', state.form);
 | 
					                        emit('val-change', state.form);
 | 
				
			||||||
@@ -330,6 +332,7 @@ export default defineComponent({
 | 
				
			|||||||
            handleClose,
 | 
					            handleClose,
 | 
				
			||||||
            showInputDb,
 | 
					            showInputDb,
 | 
				
			||||||
            handleInputDbConfirm,
 | 
					            handleInputDbConfirm,
 | 
				
			||||||
 | 
					            getSshTunnelMachines,
 | 
				
			||||||
            changeProject,
 | 
					            changeProject,
 | 
				
			||||||
            changeEnv,
 | 
					            changeEnv,
 | 
				
			||||||
            btnOk,
 | 
					            btnOk,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,15 +11,24 @@
 | 
				
			|||||||
                    <el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
 | 
					                    <el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
 | 
				
			||||||
                </el-form-item>
 | 
					                </el-form-item>
 | 
				
			||||||
                <el-form-item prop="ip" label="ip:" required>
 | 
					                <el-form-item prop="ip" label="ip:" required>
 | 
				
			||||||
                    <el-input v-model.trim="form.ip" placeholder="请输入主机ip" auto-complete="off"></el-input>
 | 
					                    <el-col :span="18">
 | 
				
			||||||
                </el-form-item>
 | 
					                        <el-input v-model.trim="form.ip" placeholder="主机ip" auto-complete="off"></el-input>
 | 
				
			||||||
                <el-form-item prop="port" label="port:" required>
 | 
					                    </el-col>
 | 
				
			||||||
                    <el-input type="number" v-model.number="form.port" placeholder="请输入端口"></el-input>
 | 
					                    <el-col style="text-align: center" :span="1">:</el-col>
 | 
				
			||||||
 | 
					                    <el-col :span="5">
 | 
				
			||||||
 | 
					                        <el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
 | 
				
			||||||
 | 
					                    </el-col>
 | 
				
			||||||
                </el-form-item>
 | 
					                </el-form-item>
 | 
				
			||||||
                <el-form-item prop="username" label="用户名:" required>
 | 
					                <el-form-item prop="username" label="用户名:" required>
 | 
				
			||||||
                    <el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
 | 
					                    <el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
 | 
				
			||||||
                </el-form-item>
 | 
					                </el-form-item>
 | 
				
			||||||
                <el-form-item prop="password" label="密码:">
 | 
					                <el-form-item prop="authMethod" label="认证方式:" required>
 | 
				
			||||||
 | 
					                    <el-select style="width: 100%" v-model="form.authMethod" placeholder="请选择认证方式">
 | 
				
			||||||
 | 
					                        <el-option key="1" label="Password" :value="1"> </el-option>
 | 
				
			||||||
 | 
					                        <el-option key="2" label="PublicKey" :value="2"> </el-option>
 | 
				
			||||||
 | 
					                    </el-select>
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					                <el-form-item v-if="form.authMethod == 1" prop="password" label="密码:">
 | 
				
			||||||
                    <el-input
 | 
					                    <el-input
 | 
				
			||||||
                        type="password"
 | 
					                        type="password"
 | 
				
			||||||
                        show-password
 | 
					                        show-password
 | 
				
			||||||
@@ -28,6 +37,9 @@
 | 
				
			|||||||
                        autocomplete="new-password"
 | 
					                        autocomplete="new-password"
 | 
				
			||||||
                    ></el-input>
 | 
					                    ></el-input>
 | 
				
			||||||
                </el-form-item>
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					                <el-form-item v-if="form.authMethod == 2" prop="password" label="秘钥:">
 | 
				
			||||||
 | 
					                    <el-input type="textarea" :rows="3" v-model="form.password" placeholder="请将私钥文件内容拷贝至此,修改操作可不填"></el-input>
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
                <el-form-item prop="remark" label="备注:">
 | 
					                <el-form-item prop="remark" label="备注:">
 | 
				
			||||||
                    <el-input type="textarea" v-model="form.remark"></el-input>
 | 
					                    <el-input type="textarea" v-model="form.remark"></el-input>
 | 
				
			||||||
                </el-form-item>
 | 
					                </el-form-item>
 | 
				
			||||||
@@ -76,9 +88,10 @@ export default defineComponent({
 | 
				
			|||||||
                projectId: null,
 | 
					                projectId: null,
 | 
				
			||||||
                projectName: null,
 | 
					                projectName: null,
 | 
				
			||||||
                name: null,
 | 
					                name: null,
 | 
				
			||||||
 | 
					                authMethod: 1,
 | 
				
			||||||
                port: 22,
 | 
					                port: 22,
 | 
				
			||||||
                username: "",
 | 
					                username: '',
 | 
				
			||||||
                password: "",
 | 
					                password: '',
 | 
				
			||||||
                remark: '',
 | 
					                remark: '',
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            btnLoading: false,
 | 
					            btnLoading: false,
 | 
				
			||||||
@@ -107,14 +120,7 @@ export default defineComponent({
 | 
				
			|||||||
                ip: [
 | 
					                ip: [
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        required: true,
 | 
					                        required: true,
 | 
				
			||||||
                        message: '请输入主机ip',
 | 
					                        message: '请输入主机ip和端口',
 | 
				
			||||||
                        trigger: ['change', 'blur'],
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
                port: [
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        required: true,
 | 
					 | 
				
			||||||
                        message: '请输入端口',
 | 
					 | 
				
			||||||
                        trigger: ['change', 'blur'],
 | 
					                        trigger: ['change', 'blur'],
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
@@ -125,16 +131,26 @@ export default defineComponent({
 | 
				
			|||||||
                        trigger: ['change', 'blur'],
 | 
					                        trigger: ['change', 'blur'],
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
 | 
					                authMethod: [
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        required: true,
 | 
				
			||||||
 | 
					                        message: '请选择认证方式',
 | 
				
			||||||
 | 
					                        trigger: ['change', 'blur'],
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        watch(props, async (newValue) => {
 | 
					        watch(props, async (newValue) => {
 | 
				
			||||||
            state.dialogVisible = newValue.visible;
 | 
					            state.dialogVisible = newValue.visible;
 | 
				
			||||||
 | 
					            if (!state.dialogVisible) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            state.projects = newValue.projects;
 | 
					            state.projects = newValue.projects;
 | 
				
			||||||
            if (newValue.machine) {
 | 
					            if (newValue.machine) {
 | 
				
			||||||
                state.form = { ...newValue.machine };
 | 
					                state.form = { ...newValue.machine };
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                state.form = { port: 22 } as any;
 | 
					                state.form = { port: 22, authMethod: 1 } as any;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -153,17 +169,18 @@ export default defineComponent({
 | 
				
			|||||||
            machineForm.value.validate(async (valid: boolean) => {
 | 
					            machineForm.value.validate(async (valid: boolean) => {
 | 
				
			||||||
                if (valid) {
 | 
					                if (valid) {
 | 
				
			||||||
                    const reqForm = { ...state.form };
 | 
					                    const reqForm = { ...state.form };
 | 
				
			||||||
 | 
					                    if (reqForm.authMethod == 1) {
 | 
				
			||||||
                        reqForm.password = await RsaEncrypt(state.form.password);
 | 
					                        reqForm.password = await RsaEncrypt(state.form.password);
 | 
				
			||||||
                    machineApi.saveMachine.request(reqForm).then(() => {
 | 
					                    }
 | 
				
			||||||
 | 
					                    state.btnLoading = true;
 | 
				
			||||||
 | 
					                    try {
 | 
				
			||||||
 | 
					                        await machineApi.saveMachine.request(reqForm);
 | 
				
			||||||
                        ElMessage.success('保存成功');
 | 
					                        ElMessage.success('保存成功');
 | 
				
			||||||
                        emit('val-change', state.form);
 | 
					                        emit('val-change', state.form);
 | 
				
			||||||
                        state.btnLoading = true;
 | 
					 | 
				
			||||||
                        setTimeout(() => {
 | 
					 | 
				
			||||||
                            state.btnLoading = false;
 | 
					 | 
				
			||||||
                        }, 1000);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        cancel();
 | 
					                        cancel();
 | 
				
			||||||
                    });
 | 
					                    } finally {
 | 
				
			||||||
 | 
					                        state.btnLoading = false;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    ElMessage.error('请正确填写信息');
 | 
					                    ElMessage.error('请正确填写信息');
 | 
				
			||||||
                    return false;
 | 
					                    return false;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -260,13 +260,13 @@ export default defineComponent({
 | 
				
			|||||||
            search();
 | 
					            search();
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const openFormDialog = (redis: any) => {
 | 
					        const openFormDialog = (machine: any) => {
 | 
				
			||||||
            let dialogTitle;
 | 
					            let dialogTitle;
 | 
				
			||||||
            if (redis) {
 | 
					            if (machine) {
 | 
				
			||||||
                state.machineEditDialog.data = state.currentData as any;
 | 
					                state.machineEditDialog.data = state.currentData as any;
 | 
				
			||||||
                dialogTitle = '编辑机器';
 | 
					                dialogTitle = '编辑机器';
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                state.machineEditDialog.data = { port: 22 } as any;
 | 
					                state.machineEditDialog.data = null;
 | 
				
			||||||
                dialogTitle = '添加机器';
 | 
					                dialogTitle = '添加机器';
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
        <el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" width="35%" :destroy-on-close="true">
 | 
					        <el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" width="38%" :destroy-on-close="true">
 | 
				
			||||||
            <el-form :model="form" ref="mongoForm" :rules="rules" label-width="65px">
 | 
					            <el-form :model="form" ref="mongoForm" :rules="rules" label-width="85px">
 | 
				
			||||||
                <el-form-item prop="projectId" label="项目" required>
 | 
					                <el-form-item prop="projectId" label="项目" required>
 | 
				
			||||||
                    <el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable>
 | 
					                    <el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable>
 | 
				
			||||||
                        <el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
 | 
					                        <el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
 | 
				
			||||||
@@ -25,6 +25,24 @@
 | 
				
			|||||||
                        auto-complete="off"
 | 
					                        auto-complete="off"
 | 
				
			||||||
                    ></el-input>
 | 
					                    ></el-input>
 | 
				
			||||||
                </el-form-item>
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-form-item prop="enableSshTunnel" label="SSH隧道:">
 | 
				
			||||||
 | 
					                    <el-col :span="3">
 | 
				
			||||||
 | 
					                        <el-checkbox @change="getSshTunnelMachines" v-model="form.enableSshTunnel" :true-label="1" :false-label="-1"></el-checkbox>
 | 
				
			||||||
 | 
					                    </el-col>
 | 
				
			||||||
 | 
					                    <el-col :span="2" v-if="form.enableSshTunnel == 1"> 机器: </el-col>
 | 
				
			||||||
 | 
					                    <el-col :span="19" v-if="form.enableSshTunnel == 1">
 | 
				
			||||||
 | 
					                        <el-select style="width: 100%" v-model="form.sshTunnelMachineId" placeholder="请选择SSH隧道机器">
 | 
				
			||||||
 | 
					                            <el-option
 | 
				
			||||||
 | 
					                                v-for="item in sshTunnelMachineList"
 | 
				
			||||||
 | 
					                                :key="item.id"
 | 
				
			||||||
 | 
					                                :label="`${item.ip}:${item.port} [${item.name}]`"
 | 
				
			||||||
 | 
					                                :value="item.id"
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
 | 
					                            </el-option>
 | 
				
			||||||
 | 
					                        </el-select>
 | 
				
			||||||
 | 
					                    </el-col>
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
            </el-form>
 | 
					            </el-form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <template #footer>
 | 
					            <template #footer>
 | 
				
			||||||
@@ -41,6 +59,7 @@
 | 
				
			|||||||
import { toRefs, reactive, watch, defineComponent, ref } from 'vue';
 | 
					import { toRefs, reactive, watch, defineComponent, ref } from 'vue';
 | 
				
			||||||
import { mongoApi } from './api';
 | 
					import { mongoApi } from './api';
 | 
				
			||||||
import { projectApi } from '../project/api.ts';
 | 
					import { projectApi } from '../project/api.ts';
 | 
				
			||||||
 | 
					import { machineApi } from '../machine/api.ts';
 | 
				
			||||||
import { ElMessage } from 'element-plus';
 | 
					import { ElMessage } from 'element-plus';
 | 
				
			||||||
import { RsaEncrypt } from '@/common/rsa';
 | 
					import { RsaEncrypt } from '@/common/rsa';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -66,10 +85,13 @@ export default defineComponent({
 | 
				
			|||||||
            dialogVisible: false,
 | 
					            dialogVisible: false,
 | 
				
			||||||
            projects: [],
 | 
					            projects: [],
 | 
				
			||||||
            envs: [],
 | 
					            envs: [],
 | 
				
			||||||
 | 
					            sshTunnelMachineList: [],
 | 
				
			||||||
            form: {
 | 
					            form: {
 | 
				
			||||||
                id: null,
 | 
					                id: null,
 | 
				
			||||||
                name: null,
 | 
					                name: null,
 | 
				
			||||||
                uri: null,
 | 
					                uri: null,
 | 
				
			||||||
 | 
					                enableSshTunnel: -1,
 | 
				
			||||||
 | 
					                sshTunnelMachineId: null,
 | 
				
			||||||
                project: null,
 | 
					                project: null,
 | 
				
			||||||
                projectId: null,
 | 
					                projectId: null,
 | 
				
			||||||
                envId: null,
 | 
					                envId: null,
 | 
				
			||||||
@@ -110,6 +132,9 @@ export default defineComponent({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        watch(props, async (newValue) => {
 | 
					        watch(props, async (newValue) => {
 | 
				
			||||||
            state.dialogVisible = newValue.visible;
 | 
					            state.dialogVisible = newValue.visible;
 | 
				
			||||||
 | 
					            if (!state.dialogVisible) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            state.projects = newValue.projects;
 | 
					            state.projects = newValue.projects;
 | 
				
			||||||
            if (newValue.mongo) {
 | 
					            if (newValue.mongo) {
 | 
				
			||||||
                getEnvs(newValue.mongo.projectId);
 | 
					                getEnvs(newValue.mongo.projectId);
 | 
				
			||||||
@@ -118,8 +143,16 @@ export default defineComponent({
 | 
				
			|||||||
                state.envs = [];
 | 
					                state.envs = [];
 | 
				
			||||||
                state.form = { db: 0 } as any;
 | 
					                state.form = { db: 0 } as any;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            getSshTunnelMachines();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const getSshTunnelMachines = async () => {
 | 
				
			||||||
 | 
					            if (state.form.enableSshTunnel == 1 && state.sshTunnelMachineList.length == 0) {
 | 
				
			||||||
 | 
					                const res = await machineApi.list.request({ pageNum: 1, pageSize: 100 });
 | 
				
			||||||
 | 
					                state.sshTunnelMachineList = res.list;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const getEnvs = async (projectId: any) => {
 | 
					        const getEnvs = async (projectId: any) => {
 | 
				
			||||||
            state.envs = await projectApi.projectEnvs.request({ projectId });
 | 
					            state.envs = await projectApi.projectEnvs.request({ projectId });
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
@@ -175,6 +208,7 @@ export default defineComponent({
 | 
				
			|||||||
            ...toRefs(state),
 | 
					            ...toRefs(state),
 | 
				
			||||||
            mongoForm,
 | 
					            mongoForm,
 | 
				
			||||||
            changeProject,
 | 
					            changeProject,
 | 
				
			||||||
 | 
					            getSshTunnelMachines,
 | 
				
			||||||
            changeEnv,
 | 
					            changeEnv,
 | 
				
			||||||
            btnOk,
 | 
					            btnOk,
 | 
				
			||||||
            cancel,
 | 
					            cancel,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
        <el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" :destroy-on-close="true" width="35%">
 | 
					        <el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" :destroy-on-close="true" width="38%">
 | 
				
			||||||
            <el-form :model="form" ref="redisForm" :rules="rules" label-width="85px">
 | 
					            <el-form :model="form" ref="redisForm" :rules="rules" label-width="85px">
 | 
				
			||||||
                <el-form-item prop="projectId" label="项目:" required>
 | 
					                <el-form-item prop="projectId" label="项目:" required>
 | 
				
			||||||
                    <el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable>
 | 
					                    <el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable>
 | 
				
			||||||
@@ -42,6 +42,23 @@
 | 
				
			|||||||
                <el-form-item prop="remark" label="备注:">
 | 
					                <el-form-item prop="remark" label="备注:">
 | 
				
			||||||
                    <el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
 | 
					                    <el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
 | 
				
			||||||
                </el-form-item>
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					                <el-form-item prop="enableSshTunnel" label="SSH隧道:">
 | 
				
			||||||
 | 
					                    <el-col :span="3">
 | 
				
			||||||
 | 
					                        <el-checkbox @change="getSshTunnelMachines" v-model="form.enableSshTunnel" :true-label="1" :false-label="-1"></el-checkbox>
 | 
				
			||||||
 | 
					                    </el-col>
 | 
				
			||||||
 | 
					                    <el-col :span="2" v-if="form.enableSshTunnel == 1"> 机器: </el-col>
 | 
				
			||||||
 | 
					                    <el-col :span="19" v-if="form.enableSshTunnel == 1">
 | 
				
			||||||
 | 
					                        <el-select style="width: 100%" v-model="form.sshTunnelMachineId" placeholder="请选择SSH隧道机器">
 | 
				
			||||||
 | 
					                            <el-option
 | 
				
			||||||
 | 
					                                v-for="item in sshTunnelMachineList"
 | 
				
			||||||
 | 
					                                :key="item.id"
 | 
				
			||||||
 | 
					                                :label="`${item.ip}:${item.port} [${item.name}]`"
 | 
				
			||||||
 | 
					                                :value="item.id"
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
 | 
					                            </el-option>
 | 
				
			||||||
 | 
					                        </el-select>
 | 
				
			||||||
 | 
					                    </el-col>
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
            </el-form>
 | 
					            </el-form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <template #footer>
 | 
					            <template #footer>
 | 
				
			||||||
@@ -58,6 +75,7 @@
 | 
				
			|||||||
import { toRefs, reactive, watch, defineComponent, ref } from 'vue';
 | 
					import { toRefs, reactive, watch, defineComponent, ref } from 'vue';
 | 
				
			||||||
import { redisApi } from './api';
 | 
					import { redisApi } from './api';
 | 
				
			||||||
import { projectApi } from '../project/api.ts';
 | 
					import { projectApi } from '../project/api.ts';
 | 
				
			||||||
 | 
					import { machineApi } from '../machine/api.ts';
 | 
				
			||||||
import { ElMessage } from 'element-plus';
 | 
					import { ElMessage } from 'element-plus';
 | 
				
			||||||
import { RsaEncrypt } from '@/common/rsa';
 | 
					import { RsaEncrypt } from '@/common/rsa';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -83,6 +101,7 @@ export default defineComponent({
 | 
				
			|||||||
            dialogVisible: false,
 | 
					            dialogVisible: false,
 | 
				
			||||||
            projects: [],
 | 
					            projects: [],
 | 
				
			||||||
            envs: [],
 | 
					            envs: [],
 | 
				
			||||||
 | 
					            sshTunnelMachineList: [],
 | 
				
			||||||
            form: {
 | 
					            form: {
 | 
				
			||||||
                id: null,
 | 
					                id: null,
 | 
				
			||||||
                name: null,
 | 
					                name: null,
 | 
				
			||||||
@@ -94,6 +113,8 @@ export default defineComponent({
 | 
				
			|||||||
                envId: null,
 | 
					                envId: null,
 | 
				
			||||||
                env: null,
 | 
					                env: null,
 | 
				
			||||||
                remark: '',
 | 
					                remark: '',
 | 
				
			||||||
 | 
					                enableSshTunnel: null,
 | 
				
			||||||
 | 
					                sshTunnelMachineId: null,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            btnLoading: false,
 | 
					            btnLoading: false,
 | 
				
			||||||
            rules: {
 | 
					            rules: {
 | 
				
			||||||
@@ -137,16 +158,27 @@ export default defineComponent({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        watch(props, async (newValue) => {
 | 
					        watch(props, async (newValue) => {
 | 
				
			||||||
            state.dialogVisible = newValue.visible;
 | 
					            state.dialogVisible = newValue.visible;
 | 
				
			||||||
 | 
					            if (!state.dialogVisible) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            state.projects = newValue.projects;
 | 
					            state.projects = newValue.projects;
 | 
				
			||||||
            if (newValue.redis) {
 | 
					            if (newValue.redis) {
 | 
				
			||||||
                getEnvs(newValue.redis.projectId);
 | 
					                getEnvs(newValue.redis.projectId);
 | 
				
			||||||
                state.form = { ...newValue.redis };
 | 
					                state.form = { ...newValue.redis };
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                state.envs = [];
 | 
					                state.envs = [];
 | 
				
			||||||
                state.form = { db: 0 } as any;
 | 
					                state.form = { db: 0, enableSshTunnel: -1 } as any;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            getSshTunnelMachines();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const getSshTunnelMachines = async () => {
 | 
				
			||||||
 | 
					            if (state.form.enableSshTunnel == 1 && state.sshTunnelMachineList.length == 0) {
 | 
				
			||||||
 | 
					                const res = await machineApi.list.request({ pageNum: 1, pageSize: 100 });
 | 
				
			||||||
 | 
					                state.sshTunnelMachineList = res.list;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const getEnvs = async (projectId: any) => {
 | 
					        const getEnvs = async (projectId: any) => {
 | 
				
			||||||
            state.envs = await projectApi.projectEnvs.request({ projectId });
 | 
					            state.envs = await projectApi.projectEnvs.request({ projectId });
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
@@ -201,6 +233,7 @@ export default defineComponent({
 | 
				
			|||||||
        return {
 | 
					        return {
 | 
				
			||||||
            ...toRefs(state),
 | 
					            ...toRefs(state),
 | 
				
			||||||
            redisForm,
 | 
					            redisForm,
 | 
				
			||||||
 | 
					            getSshTunnelMachines,
 | 
				
			||||||
            changeProject,
 | 
					            changeProject,
 | 
				
			||||||
            changeEnv,
 | 
					            changeEnv,
 | 
				
			||||||
            btnOk,
 | 
					            btnOk,
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,6 +1,6 @@
 | 
				
			|||||||
app:
 | 
					app:
 | 
				
			||||||
  name: mayfly-go
 | 
					  name: mayfly-go
 | 
				
			||||||
  version: 1.2.0
 | 
					  version: 1.2.3
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
server:
 | 
					server:
 | 
				
			||||||
  # debug release test
 | 
					  # debug release test
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ require (
 | 
				
			|||||||
	github.com/dgrijalva/jwt-go v3.2.0+incompatible // jwt
 | 
						github.com/dgrijalva/jwt-go v3.2.0+incompatible // jwt
 | 
				
			||||||
	github.com/gin-gonic/gin v1.8.1
 | 
						github.com/gin-gonic/gin v1.8.1
 | 
				
			||||||
	github.com/go-redis/redis/v8 v8.11.5
 | 
						github.com/go-redis/redis/v8 v8.11.5
 | 
				
			||||||
 | 
						github.com/go-sql-driver/mysql v1.6.0
 | 
				
			||||||
	github.com/gorilla/websocket v1.5.0
 | 
						github.com/gorilla/websocket v1.5.0
 | 
				
			||||||
	github.com/lib/pq v1.10.6
 | 
						github.com/lib/pq v1.10.6
 | 
				
			||||||
	github.com/mojocn/base64Captcha v1.3.5 // 验证码
 | 
						github.com/mojocn/base64Captcha v1.3.5 // 验证码
 | 
				
			||||||
@@ -28,7 +29,6 @@ require (
 | 
				
			|||||||
	github.com/go-playground/locales v0.14.0 // indirect
 | 
						github.com/go-playground/locales v0.14.0 // indirect
 | 
				
			||||||
	github.com/go-playground/universal-translator v0.18.0 // indirect
 | 
						github.com/go-playground/universal-translator v0.18.0 // indirect
 | 
				
			||||||
	github.com/go-playground/validator/v10 v10.10.1 // indirect
 | 
						github.com/go-playground/validator/v10 v10.10.1 // indirect
 | 
				
			||||||
	github.com/go-sql-driver/mysql v1.6.0 // indirect
 | 
					 | 
				
			||||||
	github.com/go-stack/stack v1.8.0 // indirect
 | 
						github.com/go-stack/stack v1.8.0 // indirect
 | 
				
			||||||
	github.com/goccy/go-json v0.9.7 // indirect
 | 
						github.com/goccy/go-json v0.9.7 // indirect
 | 
				
			||||||
	github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
 | 
						github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -56,13 +56,13 @@ func (d *Db) Save(rc *ctx.ReqCtx) {
 | 
				
			|||||||
	// 密码脱敏记录日志
 | 
						// 密码脱敏记录日志
 | 
				
			||||||
	form.Password = "****"
 | 
						form.Password = "****"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if form.Type == "mysql" && form.EnableSSH == 1 {
 | 
						// if form.Type == "mysql" && form.EnableSSH == 1 {
 | 
				
			||||||
		originSSHPwd, err := utils.DefaultRsaDecrypt(form.SSHPass, true)
 | 
						// 	// originSSHPwd, err := utils.DefaultRsaDecrypt(form.SSHPass, true)
 | 
				
			||||||
		biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
 | 
						// 	biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
 | 
				
			||||||
		db.SSHPass = originSSHPwd
 | 
						// 	// db.SSHPass = originSSHPwd
 | 
				
			||||||
		// 密码脱敏记录日志
 | 
						// 	// 密码脱敏记录日志
 | 
				
			||||||
		form.SSHPass = "****"
 | 
						// 	form.SSHPass = "****"
 | 
				
			||||||
	}
 | 
						// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	rc.ReqParam = form
 | 
						rc.ReqParam = form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,11 +15,8 @@ type DbForm struct {
 | 
				
			|||||||
	Env       string `json:"env"`
 | 
						Env       string `json:"env"`
 | 
				
			||||||
	EnvId     uint64 `binding:"required" json:"envId"`
 | 
						EnvId     uint64 `binding:"required" json:"envId"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	EnableSSH int    `json:"enable_ssh"`
 | 
						EnableSshTunnel    int8   `json:"enableSshTunnel"`
 | 
				
			||||||
	SSHHost   string `json:"ssh_host"`
 | 
						SshTunnelMachineId uint64 `json:"sshTunnelMachineId"`
 | 
				
			||||||
	SSHPort   int    `json:"ssh_port"`
 | 
					 | 
				
			||||||
	SSHUser   string `json:"ssh_user"`
 | 
					 | 
				
			||||||
	SSHPass   string `json:"ssh_pass"`
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type DbSqlSaveForm struct {
 | 
					type DbSqlSaveForm struct {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,13 +5,11 @@ type MachineForm struct {
 | 
				
			|||||||
	ProjectId   uint64 `json:"projectId"`
 | 
						ProjectId   uint64 `json:"projectId"`
 | 
				
			||||||
	ProjectName string `json:"projectName"`
 | 
						ProjectName string `json:"projectName"`
 | 
				
			||||||
	Name        string `json:"name" binding:"required"`
 | 
						Name        string `json:"name" binding:"required"`
 | 
				
			||||||
	// IP地址
 | 
						Ip          string `json:"ip" binding:"required"`       // IP地址
 | 
				
			||||||
	Ip string `json:"ip" binding:"required"`
 | 
						Username    string `json:"username" binding:"required"` // 用户名
 | 
				
			||||||
	// 用户名
 | 
						AuthMethod  int8   `json:"authMethod" binding:"required"`
 | 
				
			||||||
	Username string `json:"username" binding:"required"`
 | 
					 | 
				
			||||||
	Password    string `json:"password"`
 | 
						Password    string `json:"password"`
 | 
				
			||||||
	// 端口号
 | 
						Port        int    `json:"port" binding:"required"` // 端口号
 | 
				
			||||||
	Port   int    `json:"port" binding:"required"`
 | 
					 | 
				
			||||||
	Remark      string `json:"remark"`
 | 
						Remark      string `json:"remark"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,8 @@ package form
 | 
				
			|||||||
type Mongo struct {
 | 
					type Mongo struct {
 | 
				
			||||||
	Id                 uint64
 | 
						Id                 uint64
 | 
				
			||||||
	Uri                string `binding:"required" json:"uri"`
 | 
						Uri                string `binding:"required" json:"uri"`
 | 
				
			||||||
 | 
						EnableSshTunnel    int8   `json:"enableSshTunnel"`    // 是否启用ssh隧道
 | 
				
			||||||
 | 
						SshTunnelMachineId uint64 `json:"sshTunnelMachineId"` // ssh隧道机器id
 | 
				
			||||||
	Name               string `binding:"required" json:"name"`
 | 
						Name               string `binding:"required" json:"name"`
 | 
				
			||||||
	ProjectId          uint64 `binding:"required" json:"projectId"`
 | 
						ProjectId          uint64 `binding:"required" json:"projectId"`
 | 
				
			||||||
	Project            string `json:"project"`
 | 
						Project            string `json:"project"`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,8 @@ type Redis struct {
 | 
				
			|||||||
	Password           string `json:"password"`
 | 
						Password           string `json:"password"`
 | 
				
			||||||
	Mode               string `json:"mode"`
 | 
						Mode               string `json:"mode"`
 | 
				
			||||||
	Db                 int    `json:"db"`
 | 
						Db                 int    `json:"db"`
 | 
				
			||||||
 | 
						EnableSshTunnel    int8   `json:"enableSshTunnel"`    // 是否启用ssh隧道
 | 
				
			||||||
 | 
						SshTunnelMachineId uint64 `json:"sshTunnelMachineId"` // ssh隧道机器id
 | 
				
			||||||
	ProjectId          uint64 `binding:"required" json:"projectId"`
 | 
						ProjectId          uint64 `binding:"required" json:"projectId"`
 | 
				
			||||||
	Project            string `json:"project"`
 | 
						Project            string `json:"project"`
 | 
				
			||||||
	Env                string `json:"env"`
 | 
						Env                string `json:"env"`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -54,20 +54,22 @@ func (m *Machine) SaveMachine(rc *ctx.ReqCtx) {
 | 
				
			|||||||
	machineForm := new(form.MachineForm)
 | 
						machineForm := new(form.MachineForm)
 | 
				
			||||||
	ginx.BindJsonAndValid(g, machineForm)
 | 
						ginx.BindJsonAndValid(g, machineForm)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	entity := new(entity.Machine)
 | 
						me := new(entity.Machine)
 | 
				
			||||||
	utils.Copy(entity, machineForm)
 | 
						utils.Copy(me, machineForm)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if me.AuthMethod == entity.MachineAuthMethodPassword {
 | 
				
			||||||
		// 密码解密,并使用解密后的赋值
 | 
							// 密码解密,并使用解密后的赋值
 | 
				
			||||||
		originPwd, err := utils.DefaultRsaDecrypt(machineForm.Password, true)
 | 
							originPwd, err := utils.DefaultRsaDecrypt(machineForm.Password, true)
 | 
				
			||||||
		biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
 | 
							biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
 | 
				
			||||||
	entity.Password = originPwd
 | 
							me.Password = originPwd
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 密码脱敏记录日志
 | 
						// 密码脱敏记录日志
 | 
				
			||||||
	machineForm.Password = "****"
 | 
						machineForm.Password = "****"
 | 
				
			||||||
	rc.ReqParam = machineForm
 | 
						rc.ReqParam = machineForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	entity.SetBaseInfo(rc.LoginAccount)
 | 
						me.SetBaseInfo(rc.LoginAccount)
 | 
				
			||||||
	m.MachineApp.Save(entity)
 | 
						m.MachineApp.Save(me)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (m *Machine) ChangeStatus(rc *ctx.ReqCtx) {
 | 
					func (m *Machine) ChangeStatus(rc *ctx.ReqCtx) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,8 +20,6 @@ type SelectDataDbVO struct {
 | 
				
			|||||||
	Creator    *string    `json:"creator"`
 | 
						Creator    *string    `json:"creator"`
 | 
				
			||||||
	CreatorId  *int64     `json:"creatorId"`
 | 
						CreatorId  *int64     `json:"creatorId"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	EnableSSH *int    `json:"enable_ssh"`
 | 
						EnableSshTunnel    *int8   `json:"enableSshTunnel"`
 | 
				
			||||||
	SSHHost   *string `json:"ssh_host"`
 | 
						SshTunnelMachineId *uint64 `json:"sshTunnelMachineId"`
 | 
				
			||||||
	SSHPort   *int    `json:"ssh_port"`
 | 
					 | 
				
			||||||
	SSHUser   *string `json:"ssh_user"`
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,8 @@ type Redis struct {
 | 
				
			|||||||
	ProjectId          *int64     `json:"projectId"`
 | 
						ProjectId          *int64     `json:"projectId"`
 | 
				
			||||||
	Project            *string    `json:"project"`
 | 
						Project            *string    `json:"project"`
 | 
				
			||||||
	Mode               *string    `json:"mode"`
 | 
						Mode               *string    `json:"mode"`
 | 
				
			||||||
 | 
						EnableSshTunnel    *int8      `orm:"column(enable_ssh_tunnel)" json:"enableSshTunnel"`        // 是否启用ssh隧道
 | 
				
			||||||
 | 
						SshTunnelMachineId *uint64    `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
 | 
				
			||||||
	Remark             *string    `json:"remark"`
 | 
						Remark             *string    `json:"remark"`
 | 
				
			||||||
	Env                *string    `json:"env"`
 | 
						Env                *string    `json:"env"`
 | 
				
			||||||
	EnvId              *int64     `json:"envId"`
 | 
						EnvId              *int64     `json:"envId"`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,6 +22,7 @@ type MachineVO struct {
 | 
				
			|||||||
	Username    *string    `json:"username"`
 | 
						Username    *string    `json:"username"`
 | 
				
			||||||
	Ip          *string    `json:"ip"`
 | 
						Ip          *string    `json:"ip"`
 | 
				
			||||||
	Port        *int       `json:"port"`
 | 
						Port        *int       `json:"port"`
 | 
				
			||||||
 | 
						AuthMethod  *int8      `json:"authMethod"`
 | 
				
			||||||
	Status      *int8      `json:"status"`
 | 
						Status      *int8      `json:"status"`
 | 
				
			||||||
	CreateTime  *time.Time `json:"createTime"`
 | 
						CreateTime  *time.Time `json:"createTime"`
 | 
				
			||||||
	Creator     *string    `json:"creator"`
 | 
						Creator     *string    `json:"creator"`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,13 @@
 | 
				
			|||||||
package application
 | 
					package application
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
	"database/sql"
 | 
						"database/sql"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"mayfly-go/internal/devops/domain/entity"
 | 
						"mayfly-go/internal/devops/domain/entity"
 | 
				
			||||||
	"mayfly-go/internal/devops/domain/repository"
 | 
						"mayfly-go/internal/devops/domain/repository"
 | 
				
			||||||
 | 
						"mayfly-go/internal/devops/infrastructure/machine"
 | 
				
			||||||
	"mayfly-go/internal/devops/infrastructure/persistence"
 | 
						"mayfly-go/internal/devops/infrastructure/persistence"
 | 
				
			||||||
	"mayfly-go/pkg/biz"
 | 
						"mayfly-go/pkg/biz"
 | 
				
			||||||
	"mayfly-go/pkg/cache"
 | 
						"mayfly-go/pkg/cache"
 | 
				
			||||||
@@ -20,7 +22,8 @@ import (
 | 
				
			|||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/go-sql-driver/mysql"
 | 
						"github.com/go-sql-driver/mysql"
 | 
				
			||||||
	_ "github.com/lib/pq"
 | 
						"github.com/lib/pq"
 | 
				
			||||||
 | 
						"golang.org/x/crypto/ssh"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Db interface {
 | 
					type Db interface {
 | 
				
			||||||
@@ -77,15 +80,11 @@ func (d *dbAppImpl) GetById(id uint64, cols ...string) *entity.Db {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (d *dbAppImpl) Save(dbEntity *entity.Db) {
 | 
					func (d *dbAppImpl) Save(dbEntity *entity.Db) {
 | 
				
			||||||
	// 默认tcp连接
 | 
						// 默认tcp连接
 | 
				
			||||||
	if dbEntity.Type == "mysql" && dbEntity.EnableSSH == 1 {
 | 
						dbEntity.Network = dbEntity.GetNetwork()
 | 
				
			||||||
		dbEntity.Network = "mysql+ssh"
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		dbEntity.Network = "tcp"
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 测试连接
 | 
						// 测试连接
 | 
				
			||||||
	if dbEntity.Password != "" {
 | 
						if dbEntity.Password != "" {
 | 
				
			||||||
		TestConnection(*dbEntity)
 | 
							TestConnection(dbEntity)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 查找是否存在该库
 | 
						// 查找是否存在该库
 | 
				
			||||||
@@ -109,6 +108,8 @@ func (d *dbAppImpl) Save(dbEntity *entity.Db) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	var oldDbs []interface{}
 | 
						var oldDbs []interface{}
 | 
				
			||||||
	for _, v := range strings.Split(old.Database, " ") {
 | 
						for _, v := range strings.Split(old.Database, " ") {
 | 
				
			||||||
 | 
							// 关闭数据库连接
 | 
				
			||||||
 | 
							CloseDb(dbEntity.Id, v)
 | 
				
			||||||
		oldDbs = append(oldDbs, v)
 | 
							oldDbs = append(oldDbs, v)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -121,14 +122,11 @@ func (d *dbAppImpl) Save(dbEntity *entity.Db) {
 | 
				
			|||||||
		return i1.(string) == i2.(string)
 | 
							return i1.(string) == i2.(string)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	for _, v := range delDb {
 | 
						for _, v := range delDb {
 | 
				
			||||||
		// 先关闭数据库连接
 | 
					 | 
				
			||||||
		CloseDb(dbEntity.Id, v.(string))
 | 
					 | 
				
			||||||
		// 删除该库关联的所有sql记录
 | 
							// 删除该库关联的所有sql记录
 | 
				
			||||||
		d.dbSqlRepo.DeleteBy(&entity.DbSql{DbId: dbId, Db: v.(string)})
 | 
							d.dbSqlRepo.DeleteBy(&entity.DbSql{DbId: dbId, Db: v.(string)})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	d.dbRepo.Update(dbEntity)
 | 
						d.dbRepo.Update(dbEntity)
 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (d *dbAppImpl) Delete(id uint64) {
 | 
					func (d *dbAppImpl) Delete(id uint64) {
 | 
				
			||||||
@@ -160,28 +158,43 @@ func (da *dbAppImpl) GetDbInstance(id uint64, db string) *DbInstance {
 | 
				
			|||||||
	d := da.GetById(id)
 | 
						d := da.GetById(id)
 | 
				
			||||||
	biz.NotNil(d, "数据库信息不存在")
 | 
						biz.NotNil(d, "数据库信息不存在")
 | 
				
			||||||
	biz.IsTrue(strings.Contains(d.Database, db), "未配置该库的操作权限")
 | 
						biz.IsTrue(strings.Contains(d.Database, db), "未配置该库的操作权限")
 | 
				
			||||||
	global.Log.Infof("连接db: %s:%d/%s", d.Host, d.Port, db)
 | 
					
 | 
				
			||||||
 | 
						cacheKey := GetDbCacheKey(id, db)
 | 
				
			||||||
 | 
						dbi := &DbInstance{Id: cacheKey, Type: d.Type, ProjectId: d.ProjectId}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	//SSH Conect
 | 
						//SSH Conect
 | 
				
			||||||
	if d.Type == "mysql" && d.EnableSSH == 1 {
 | 
						if d.EnableSshTunnel == 1 && d.SshTunnelMachineId != 0 {
 | 
				
			||||||
		sshClient, err := utils.SSHConnect(d.SSHUser, d.SSHPass, d.SSHHost, "", d.SSHPort)
 | 
							me := MachineApp.GetById(d.SshTunnelMachineId)
 | 
				
			||||||
		if err != nil {
 | 
							biz.NotNil(me, "隧道机器信息不存在")
 | 
				
			||||||
			global.Log.Errorf("ssh连接失败: %s@%s:%d", d.SSHUser, d.SSHHost, d.SSHPort)
 | 
							sshClient, err := machine.GetSshClient(me)
 | 
				
			||||||
			panic(biz.NewBizErr(fmt.Sprintf("ssh连接失败: %s", err.Error())))
 | 
							biz.ErrIsNilAppendErr(err, "ssh隧道连接失败: %s")
 | 
				
			||||||
		}
 | 
							dbi.sshTunnel = sshClient
 | 
				
			||||||
		mysql.RegisterDial("mysql+ssh", func(addr string) (net.Conn, error) {
 | 
					
 | 
				
			||||||
 | 
							if d.Type == entity.DbTypeMysql {
 | 
				
			||||||
 | 
								mysql.RegisterDialContext(d.Network, func(ctx context.Context, addr string) (net.Conn, error) {
 | 
				
			||||||
				return sshClient.Dial("tcp", addr)
 | 
									return sshClient.Dial("tcp", addr)
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
 | 
							} else if d.Type == entity.DbTypePostgres {
 | 
				
			||||||
 | 
								_, err := pq.DialOpen(&PqSqlDialer{sshTunnel: sshClient}, getDsn(d))
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									dbi.Close()
 | 
				
			||||||
 | 
									panic(biz.NewBizErr(fmt.Sprintf("postgres隧道连接失败: %s", err.Error())))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 将数据库替换为要访问的数据库,原本数据库为空格拼接的所有库
 | 
						// 将数据库替换为要访问的数据库,原本数据库为空格拼接的所有库
 | 
				
			||||||
	d.Database = db
 | 
						d.Database = db
 | 
				
			||||||
	DB, err := sql.Open(d.Type, getDsn(d))
 | 
						DB, err := sql.Open(d.Type, getDsn(d))
 | 
				
			||||||
	biz.ErrIsNil(err, fmt.Sprintf("Open %s failed, err:%v\n", d.Type, err))
 | 
						if err != nil {
 | 
				
			||||||
	perr := DB.Ping()
 | 
							dbi.Close()
 | 
				
			||||||
	if perr != nil {
 | 
							panic(biz.NewBizErr(fmt.Sprintf("Open %s failed, err:%v\n", d.Type, err)))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = DB.Ping()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							dbi.Close()
 | 
				
			||||||
		global.Log.Errorf("连接db失败: %s:%d/%s", d.Host, d.Port, db)
 | 
							global.Log.Errorf("连接db失败: %s:%d/%s", d.Host, d.Port, db)
 | 
				
			||||||
		panic(biz.NewBizErr(fmt.Sprintf("数据库连接失败: %s", perr.Error())))
 | 
							panic(biz.NewBizErr(fmt.Sprintf("数据库连接失败: %s", err.Error())))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 最大连接周期,超过时间的连接就close
 | 
						// 最大连接周期,超过时间的连接就close
 | 
				
			||||||
@@ -191,14 +204,30 @@ func (da *dbAppImpl) GetDbInstance(id uint64, db string) *DbInstance {
 | 
				
			|||||||
	// 设置闲置连接数
 | 
						// 设置闲置连接数
 | 
				
			||||||
	DB.SetMaxIdleConns(1)
 | 
						DB.SetMaxIdleConns(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	cacheKey := GetDbCacheKey(id, db)
 | 
						dbi.db = DB
 | 
				
			||||||
	dbi := &DbInstance{Id: cacheKey, Type: d.Type, ProjectId: d.ProjectId, db: DB}
 | 
						global.Log.Infof("连接db: %s:%d/%s", d.Host, d.Port, db)
 | 
				
			||||||
	if needCache {
 | 
						if needCache {
 | 
				
			||||||
		dbCache.Put(cacheKey, dbi)
 | 
							dbCache.Put(cacheKey, dbi)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return dbi
 | 
						return dbi
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PqSqlDialer struct {
 | 
				
			||||||
 | 
						sshTunnel *ssh.Client
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (pd *PqSqlDialer) Dial(network, address string) (net.Conn, error) {
 | 
				
			||||||
 | 
						if sshConn, err := pd.sshTunnel.Dial(network, address); err == nil {
 | 
				
			||||||
 | 
							// 将ssh conn包装,否则redis内部设置超时会报错,ssh conn不支持设置超时会返回错误: ssh: tcpChan: deadline not supported
 | 
				
			||||||
 | 
							return &utils.WrapSshConn{Conn: sshConn}, nil
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (pd *PqSqlDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
 | 
				
			||||||
 | 
						return pd.Dial(network, address)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//------------------------------------------------------------------------------
 | 
					//------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 客户端连接缓存,30分钟内没有访问则会被关闭, key为数据库实例id:数据库
 | 
					// 客户端连接缓存,30分钟内没有访问则会被关闭, key为数据库实例id:数据库
 | 
				
			||||||
@@ -220,22 +249,28 @@ func GetDbInstanceByCache(id string) *DbInstance {
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestConnection(d entity.Db) {
 | 
					func TestConnection(d *entity.Db) {
 | 
				
			||||||
	//SSH Conect
 | 
						//SSH Conect
 | 
				
			||||||
	if d.Type == "mysql" && d.EnableSSH == 1 {
 | 
						if d.EnableSshTunnel == 1 && d.SshTunnelMachineId != 0 {
 | 
				
			||||||
		sshClient, err := utils.SSHConnect(d.SSHUser, d.SSHPass, d.SSHHost, "", d.SSHPort)
 | 
							me := MachineApp.GetById(d.SshTunnelMachineId)
 | 
				
			||||||
		if err != nil {
 | 
							sshClient, err := machine.GetSshClient(me)
 | 
				
			||||||
			global.Log.Errorf("ssh连接失败: %s@%s:%d", d.SSHUser, d.SSHHost, d.SSHPort)
 | 
							biz.ErrIsNilAppendErr(err, "ssh隧道连接失败: %s")
 | 
				
			||||||
			panic(biz.NewBizErr(fmt.Sprintf("ssh连接失败: %s", err.Error())))
 | 
							defer sshClient.Close()
 | 
				
			||||||
		}
 | 
							if d.Type == entity.DbTypeMysql {
 | 
				
			||||||
		mysql.RegisterDial("mysql+ssh", func(addr string) (net.Conn, error) {
 | 
								mysql.RegisterDialContext(d.Network, func(ctx context.Context, addr string) (net.Conn, error) {
 | 
				
			||||||
				return sshClient.Dial("tcp", addr)
 | 
									return sshClient.Dial("tcp", addr)
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
 | 
							} else if d.Type == entity.DbTypePostgres {
 | 
				
			||||||
 | 
								_, err := pq.DialOpen(&PqSqlDialer{sshTunnel: sshClient}, getDsn(d))
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									panic(biz.NewBizErr(fmt.Sprintf("postgres隧道连接失败: %s", err.Error())))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 验证第一个库是否可以连接即可
 | 
						// 验证第一个库是否可以连接即可
 | 
				
			||||||
	d.Database = strings.Split(d.Database, " ")[0]
 | 
						d.Database = strings.Split(d.Database, " ")[0]
 | 
				
			||||||
	DB, err := sql.Open(d.Type, getDsn(&d))
 | 
						DB, err := sql.Open(d.Type, getDsn(d))
 | 
				
			||||||
	biz.ErrIsNil(err, "Open %s failed, err:%v\n", d.Type, err)
 | 
						biz.ErrIsNil(err, "Open %s failed, err:%v\n", d.Type, err)
 | 
				
			||||||
	defer DB.Close()
 | 
						defer DB.Close()
 | 
				
			||||||
	perr := DB.Ping()
 | 
						perr := DB.Ping()
 | 
				
			||||||
@@ -248,6 +283,7 @@ type DbInstance struct {
 | 
				
			|||||||
	Type      string
 | 
						Type      string
 | 
				
			||||||
	ProjectId uint64
 | 
						ProjectId uint64
 | 
				
			||||||
	db        *sql.DB
 | 
						db        *sql.DB
 | 
				
			||||||
 | 
						sshTunnel *ssh.Client
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 执行查询语句
 | 
					// 执行查询语句
 | 
				
			||||||
@@ -359,13 +395,22 @@ func (d *DbInstance) Exec(sql string) (int64, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// 关闭连接
 | 
					// 关闭连接
 | 
				
			||||||
func (d *DbInstance) Close() {
 | 
					func (d *DbInstance) Close() {
 | 
				
			||||||
	d.db.Close()
 | 
						if d.db != nil {
 | 
				
			||||||
 | 
							if err := d.db.Close(); err != nil {
 | 
				
			||||||
 | 
								global.Log.Errorf("关闭数据库实例[%s]连接失败: %s", d.Id, err.Error())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if d.sshTunnel != nil {
 | 
				
			||||||
 | 
							if err := d.sshTunnel.Close(); err != nil {
 | 
				
			||||||
 | 
								global.Log.Errorf("关闭数据库实例[%s]的ssh隧道失败: %s", d.Id, err.Error())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 获取dataSourceName
 | 
					// 获取dataSourceName
 | 
				
			||||||
func getDsn(d *entity.Db) string {
 | 
					func getDsn(d *entity.Db) string {
 | 
				
			||||||
	var dsn string
 | 
						var dsn string
 | 
				
			||||||
	if d.Type == "mysql" {
 | 
						if d.Type == entity.DbTypeMysql {
 | 
				
			||||||
		dsn = fmt.Sprintf("%s:%s@%s(%s:%d)/%s?timeout=8s", d.Username, d.Password, d.Network, d.Host, d.Port, d.Database)
 | 
							dsn = fmt.Sprintf("%s:%s@%s(%s:%d)/%s?timeout=8s", d.Username, d.Password, d.Network, d.Host, d.Port, d.Database)
 | 
				
			||||||
		if d.Params != "" {
 | 
							if d.Params != "" {
 | 
				
			||||||
			dsn = fmt.Sprintf("%s&%s", dsn, d.Params)
 | 
								dsn = fmt.Sprintf("%s&%s", dsn, d.Params)
 | 
				
			||||||
@@ -373,7 +418,7 @@ func getDsn(d *entity.Db) string {
 | 
				
			|||||||
		return dsn
 | 
							return dsn
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if d.Type == "postgres" {
 | 
						if d.Type == entity.DbTypePostgres {
 | 
				
			||||||
		dsn = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", d.Host, d.Port, d.Username, d.Password, d.Database)
 | 
							dsn = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", d.Host, d.Port, d.Username, d.Password, d.Database)
 | 
				
			||||||
		if d.Params != "" {
 | 
							if d.Params != "" {
 | 
				
			||||||
			dsn = fmt.Sprintf("%s %s", dsn, strings.Join(strings.Split(d.Params, "&"), " "))
 | 
								dsn = fmt.Sprintf("%s %s", dsn, strings.Join(strings.Split(d.Params, "&"), " "))
 | 
				
			||||||
@@ -469,7 +514,7 @@ const (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (d *DbInstance) GetTableMetedatas() []map[string]interface{} {
 | 
					func (d *DbInstance) GetTableMetedatas() []map[string]interface{} {
 | 
				
			||||||
	var sql string
 | 
						var sql string
 | 
				
			||||||
	if d.Type == "mysql" {
 | 
						if d.Type == entity.DbTypeMysql {
 | 
				
			||||||
		sql = MYSQL_TABLE_MA
 | 
							sql = MYSQL_TABLE_MA
 | 
				
			||||||
	} else if d.Type == "postgres" {
 | 
						} else if d.Type == "postgres" {
 | 
				
			||||||
		sql = PGSQL_TABLE_MA
 | 
							sql = PGSQL_TABLE_MA
 | 
				
			||||||
@@ -489,10 +534,10 @@ func (d *DbInstance) GetColumnMetadatas(tableNames ...string) []map[string]inter
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	var countSqlTmp string
 | 
						var countSqlTmp string
 | 
				
			||||||
	var sqlTmp string
 | 
						var sqlTmp string
 | 
				
			||||||
	if d.Type == "mysql" {
 | 
						if d.Type == entity.DbTypeMysql {
 | 
				
			||||||
		countSqlTmp = MYSQL_COLOUMN_MA_COUNT
 | 
							countSqlTmp = MYSQL_COLOUMN_MA_COUNT
 | 
				
			||||||
		sqlTmp = MYSQL_COLUMN_MA
 | 
							sqlTmp = MYSQL_COLUMN_MA
 | 
				
			||||||
	} else if d.Type == "postgres" {
 | 
						} else if d.Type == entity.DbTypePostgres {
 | 
				
			||||||
		countSqlTmp = PGSQL_COLUMN_MA_COUNT
 | 
							countSqlTmp = PGSQL_COLUMN_MA_COUNT
 | 
				
			||||||
		sqlTmp = PGSQL_COLUMN_MA
 | 
							sqlTmp = PGSQL_COLUMN_MA
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -524,9 +569,9 @@ func (d *DbInstance) GetPrimaryKey(tablename string) string {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (d *DbInstance) GetTableInfos() []map[string]interface{} {
 | 
					func (d *DbInstance) GetTableInfos() []map[string]interface{} {
 | 
				
			||||||
	var sql string
 | 
						var sql string
 | 
				
			||||||
	if d.Type == "mysql" {
 | 
						if d.Type == entity.DbTypeMysql {
 | 
				
			||||||
		sql = MYSQL_TABLE_INFO
 | 
							sql = MYSQL_TABLE_INFO
 | 
				
			||||||
	} else if d.Type == "postgres" {
 | 
						} else if d.Type == entity.DbTypePostgres {
 | 
				
			||||||
		sql = PGSQL_TABLE_INFO
 | 
							sql = PGSQL_TABLE_INFO
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	_, res, _ := d.SelectData(sql)
 | 
						_, res, _ := d.SelectData(sql)
 | 
				
			||||||
@@ -535,9 +580,9 @@ func (d *DbInstance) GetTableInfos() []map[string]interface{} {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (d *DbInstance) GetTableIndex(tableName string) []map[string]interface{} {
 | 
					func (d *DbInstance) GetTableIndex(tableName string) []map[string]interface{} {
 | 
				
			||||||
	var sql string
 | 
						var sql string
 | 
				
			||||||
	if d.Type == "mysql" {
 | 
						if d.Type == entity.DbTypeMysql {
 | 
				
			||||||
		sql = fmt.Sprintf(MYSQL_INDEX_INFO, tableName)
 | 
							sql = fmt.Sprintf(MYSQL_INDEX_INFO, tableName)
 | 
				
			||||||
	} else if d.Type == "postgres" {
 | 
						} else if d.Type == entity.DbTypePostgres {
 | 
				
			||||||
		sql = fmt.Sprintf(PGSQL_INDEX_INFO, tableName)
 | 
							sql = fmt.Sprintf(PGSQL_INDEX_INFO, tableName)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	_, res, _ := d.SelectData(sql)
 | 
						_, res, _ := d.SelectData(sql)
 | 
				
			||||||
@@ -546,7 +591,7 @@ func (d *DbInstance) GetTableIndex(tableName string) []map[string]interface{} {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (d *DbInstance) GetCreateTableDdl(tableName string) []map[string]interface{} {
 | 
					func (d *DbInstance) GetCreateTableDdl(tableName string) []map[string]interface{} {
 | 
				
			||||||
	var sql string
 | 
						var sql string
 | 
				
			||||||
	if d.Type == "mysql" {
 | 
						if d.Type == entity.DbTypeMysql {
 | 
				
			||||||
		sql = fmt.Sprintf("show create table %s ", tableName)
 | 
							sql = fmt.Sprintf("show create table %s ", tableName)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	_, res, _ := d.SelectData(sql)
 | 
						_, res, _ := d.SelectData(sql)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,15 +4,19 @@ import (
 | 
				
			|||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"mayfly-go/internal/devops/domain/entity"
 | 
						"mayfly-go/internal/devops/domain/entity"
 | 
				
			||||||
	"mayfly-go/internal/devops/domain/repository"
 | 
						"mayfly-go/internal/devops/domain/repository"
 | 
				
			||||||
 | 
						"mayfly-go/internal/devops/infrastructure/machine"
 | 
				
			||||||
	"mayfly-go/internal/devops/infrastructure/persistence"
 | 
						"mayfly-go/internal/devops/infrastructure/persistence"
 | 
				
			||||||
	"mayfly-go/pkg/biz"
 | 
						"mayfly-go/pkg/biz"
 | 
				
			||||||
	"mayfly-go/pkg/cache"
 | 
						"mayfly-go/pkg/cache"
 | 
				
			||||||
	"mayfly-go/pkg/global"
 | 
						"mayfly-go/pkg/global"
 | 
				
			||||||
	"mayfly-go/pkg/model"
 | 
						"mayfly-go/pkg/model"
 | 
				
			||||||
 | 
						"mayfly-go/pkg/utils"
 | 
				
			||||||
 | 
						"net"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"go.mongodb.org/mongo-driver/mongo"
 | 
						"go.mongodb.org/mongo-driver/mongo"
 | 
				
			||||||
	"go.mongodb.org/mongo-driver/mongo/options"
 | 
						"go.mongodb.org/mongo-driver/mongo/options"
 | 
				
			||||||
 | 
						"golang.org/x/crypto/ssh"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Mongo interface {
 | 
					type Mongo interface {
 | 
				
			||||||
@@ -80,13 +84,13 @@ func (d *mongoAppImpl) Save(m *entity.Mongo) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (d *mongoAppImpl) GetMongoCli(id uint64) *mongo.Client {
 | 
					func (d *mongoAppImpl) GetMongoCli(id uint64) *mongo.Client {
 | 
				
			||||||
	cli, err := GetMongoCli(id, func(u uint64) string {
 | 
						mongoInstance, err := GetMongoInstance(id, func(u uint64) *entity.Mongo {
 | 
				
			||||||
		mongo := d.GetById(id)
 | 
							mongo := d.GetById(u)
 | 
				
			||||||
		biz.NotNil(mongo, "mongo信息不存在")
 | 
							biz.NotNil(mongo, "mongo信息不存在")
 | 
				
			||||||
		return mongo.Uri
 | 
							return mongo
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	biz.ErrIsNilAppendErr(err, "连接mongo失败: %s")
 | 
						biz.ErrIsNilAppendErr(err, "连接mongo失败: %s")
 | 
				
			||||||
	return cli
 | 
						return mongoInstance.Cli
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// -----------------------------------------------------------
 | 
					// -----------------------------------------------------------
 | 
				
			||||||
@@ -95,21 +99,22 @@ func (d *mongoAppImpl) GetMongoCli(id uint64) *mongo.Client {
 | 
				
			|||||||
var mongoCliCache = cache.NewTimedCache(30*time.Minute, 5*time.Second).
 | 
					var mongoCliCache = cache.NewTimedCache(30*time.Minute, 5*time.Second).
 | 
				
			||||||
	WithUpdateAccessTime(true).
 | 
						WithUpdateAccessTime(true).
 | 
				
			||||||
	OnEvicted(func(key interface{}, value interface{}) {
 | 
						OnEvicted(func(key interface{}, value interface{}) {
 | 
				
			||||||
		global.Log.Info("关闭mongo连接: id = ", key)
 | 
							global.Log.Info("删除mongo连接缓存: id = ", key)
 | 
				
			||||||
		value.(*mongo.Client).Disconnect(context.TODO())
 | 
							value.(*MongoInstance).Close()
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GetMongoCli(mongoId uint64, getMongoUri func(uint64) string) (*mongo.Client, error) {
 | 
					// 获取mongo的连接实例
 | 
				
			||||||
	cli, err := mongoCliCache.ComputeIfAbsent(mongoId, func(key interface{}) (interface{}, error) {
 | 
					func GetMongoInstance(mongoId uint64, getMongoEntity func(uint64) *entity.Mongo) (*MongoInstance, error) {
 | 
				
			||||||
		c, err := connect(getMongoUri(mongoId))
 | 
						mi, err := mongoCliCache.ComputeIfAbsent(mongoId, func(_ interface{}) (interface{}, error) {
 | 
				
			||||||
 | 
							c, err := connect(getMongoEntity(mongoId))
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, err
 | 
								return nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return c, nil
 | 
							return c, nil
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if cli != nil {
 | 
						if mi != nil {
 | 
				
			||||||
		return cli.(*mongo.Client), err
 | 
							return mi.(*MongoInstance), err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return nil, err
 | 
						return nil, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -118,16 +123,67 @@ func DeleteMongoCache(mongoId uint64) {
 | 
				
			|||||||
	mongoCliCache.Delete(mongoId)
 | 
						mongoCliCache.Delete(mongoId)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type MongoInstance struct {
 | 
				
			||||||
 | 
						Id        uint64
 | 
				
			||||||
 | 
						ProjectId uint64
 | 
				
			||||||
 | 
						Cli       *mongo.Client
 | 
				
			||||||
 | 
						sshTunnel *ssh.Client
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (mi *MongoInstance) Close() {
 | 
				
			||||||
 | 
						if mi.Cli != nil {
 | 
				
			||||||
 | 
							if err := mi.Cli.Disconnect(context.Background()); err != nil {
 | 
				
			||||||
 | 
								global.Log.Errorf("关闭mongo实例[%d]连接失败: %s", mi.Id, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if mi.sshTunnel != nil {
 | 
				
			||||||
 | 
							if err := mi.sshTunnel.Close(); err != nil {
 | 
				
			||||||
 | 
								global.Log.Errorf("关闭mongo实例[%d]的ssh隧道失败: %s", mi.Id, err.Error())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 连接mongo,并返回client
 | 
					// 连接mongo,并返回client
 | 
				
			||||||
func connect(uri string) (*mongo.Client, error) {
 | 
					func connect(me *entity.Mongo) (*MongoInstance, error) {
 | 
				
			||||||
	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
 | 
						ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 | 
				
			||||||
	defer cancel()
 | 
						defer cancel()
 | 
				
			||||||
	client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri).SetMaxPoolSize(2))
 | 
					
 | 
				
			||||||
 | 
						mongoInstance := &MongoInstance{Id: me.Id, ProjectId: me.ProjectId}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mongoOptions := options.Client().ApplyURI(me.Uri).
 | 
				
			||||||
 | 
							SetMaxPoolSize(1)
 | 
				
			||||||
 | 
						// 启用ssh隧道则连接隧道机器
 | 
				
			||||||
 | 
						if me.EnableSshTunnel == 1 {
 | 
				
			||||||
 | 
							machineEntity := MachineApp.GetById(4)
 | 
				
			||||||
 | 
							sshClient, err := machine.GetSshClient(machineEntity)
 | 
				
			||||||
 | 
							biz.ErrIsNilAppendErr(err, "ssh隧道连接失败: %s")
 | 
				
			||||||
 | 
							mongoInstance.sshTunnel = sshClient
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							mongoOptions.SetDialer(&MongoSshDialer{sshTunnel: sshClient})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						client, err := mongo.Connect(ctx, mongoOptions)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err = client.Ping(context.TODO(), nil); err != nil {
 | 
						if err = client.Ping(context.TODO(), nil); err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return client, err
 | 
					
 | 
				
			||||||
 | 
						global.Log.Infof("连接mongo: %s", me.Uri)
 | 
				
			||||||
 | 
						mongoInstance.Cli = client
 | 
				
			||||||
 | 
						return mongoInstance, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type MongoSshDialer struct {
 | 
				
			||||||
 | 
						sshTunnel *ssh.Client
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (sd *MongoSshDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
 | 
				
			||||||
 | 
						if sshConn, err := sd.sshTunnel.Dial(network, address); err == nil {
 | 
				
			||||||
 | 
							// 将ssh conn包装,否则内部部设置超时会报错,ssh conn不支持设置超时会返回错误: ssh: tcpChan: deadline not supported
 | 
				
			||||||
 | 
							return &utils.WrapSshConn{Conn: sshConn}, nil
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,15 +5,19 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"mayfly-go/internal/devops/domain/entity"
 | 
						"mayfly-go/internal/devops/domain/entity"
 | 
				
			||||||
	"mayfly-go/internal/devops/domain/repository"
 | 
						"mayfly-go/internal/devops/domain/repository"
 | 
				
			||||||
 | 
						"mayfly-go/internal/devops/infrastructure/machine"
 | 
				
			||||||
	"mayfly-go/internal/devops/infrastructure/persistence"
 | 
						"mayfly-go/internal/devops/infrastructure/persistence"
 | 
				
			||||||
	"mayfly-go/pkg/biz"
 | 
						"mayfly-go/pkg/biz"
 | 
				
			||||||
	"mayfly-go/pkg/cache"
 | 
						"mayfly-go/pkg/cache"
 | 
				
			||||||
	"mayfly-go/pkg/global"
 | 
						"mayfly-go/pkg/global"
 | 
				
			||||||
	"mayfly-go/pkg/model"
 | 
						"mayfly-go/pkg/model"
 | 
				
			||||||
 | 
						"mayfly-go/pkg/utils"
 | 
				
			||||||
 | 
						"net"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/go-redis/redis/v8"
 | 
						"github.com/go-redis/redis/v8"
 | 
				
			||||||
 | 
						"golang.org/x/crypto/ssh"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Redis interface {
 | 
					type Redis interface {
 | 
				
			||||||
@@ -109,25 +113,23 @@ func (r *redisAppImpl) GetRedisInstance(id uint64) *RedisInstance {
 | 
				
			|||||||
	biz.NotNil(re, "redis信息不存在")
 | 
						biz.NotNil(re, "redis信息不存在")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	redisMode := re.Mode
 | 
						redisMode := re.Mode
 | 
				
			||||||
	ri := &RedisInstance{Id: id, ProjectId: re.ProjectId, Mode: redisMode}
 | 
						var ri *RedisInstance
 | 
				
			||||||
	if redisMode == "" || redisMode == entity.RedisModeStandalone {
 | 
						if redisMode == "" || redisMode == entity.RedisModeStandalone {
 | 
				
			||||||
		rcli := getRedisCient(re)
 | 
							ri = getRedisCient(re)
 | 
				
			||||||
		// 测试连接
 | 
							// 测试连接
 | 
				
			||||||
		_, e := rcli.Ping(context.Background()).Result()
 | 
							_, e := ri.Cli.Ping(context.Background()).Result()
 | 
				
			||||||
		if e != nil {
 | 
							if e != nil {
 | 
				
			||||||
			rcli.Close()
 | 
								ri.Close()
 | 
				
			||||||
			panic(biz.NewBizErr(fmt.Sprintf("redis连接失败: %s", e.Error())))
 | 
								panic(biz.NewBizErr(fmt.Sprintf("redis连接失败: %s", e.Error())))
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		ri.Cli = rcli
 | 
					 | 
				
			||||||
	} else if redisMode == entity.RedisModeCluster {
 | 
						} else if redisMode == entity.RedisModeCluster {
 | 
				
			||||||
		ccli := getRedisClusterClient(re)
 | 
							ri = getRedisClusterClient(re)
 | 
				
			||||||
		// 测试连接
 | 
							// 测试连接
 | 
				
			||||||
		_, e := ccli.Ping(context.Background()).Result()
 | 
							_, e := ri.ClusterCli.Ping(context.Background()).Result()
 | 
				
			||||||
		if e != nil {
 | 
							if e != nil {
 | 
				
			||||||
			ccli.Close()
 | 
								ri.Close()
 | 
				
			||||||
			panic(biz.NewBizErr(fmt.Sprintf("redis集群连接失败: %s", e.Error())))
 | 
								panic(biz.NewBizErr(fmt.Sprintf("redis集群连接失败: %s", e.Error())))
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		ri.ClusterCli = ccli
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	global.Log.Infof("连接redis: %s", re.Host)
 | 
						global.Log.Infof("连接redis: %s", re.Host)
 | 
				
			||||||
@@ -137,21 +139,56 @@ func (r *redisAppImpl) GetRedisInstance(id uint64) *RedisInstance {
 | 
				
			|||||||
	return ri
 | 
						return ri
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func getRedisCient(re *entity.Redis) *redis.Client {
 | 
					func getRedisCient(re *entity.Redis) *RedisInstance {
 | 
				
			||||||
	return redis.NewClient(&redis.Options{
 | 
						ri := &RedisInstance{Id: re.Id, ProjectId: re.ProjectId, Mode: re.Mode}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						redisOptions := &redis.Options{
 | 
				
			||||||
		Addr:         re.Host,
 | 
							Addr:         re.Host,
 | 
				
			||||||
		Password:     re.Password, // no password set
 | 
							Password:     re.Password, // no password set
 | 
				
			||||||
		DB:           re.Db,       // use default DB
 | 
							DB:           re.Db,       // use default DB
 | 
				
			||||||
		DialTimeout:  8 * time.Second,
 | 
							DialTimeout:  8 * time.Second,
 | 
				
			||||||
	})
 | 
							ReadTimeout:  -1, // Disable timeouts, because SSH does not support deadlines.
 | 
				
			||||||
 | 
							WriteTimeout: -1,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if re.EnableSshTunnel == 1 {
 | 
				
			||||||
 | 
							sshClient, dialerFunc := getRedisDialer(re.SshTunnelMachineId)
 | 
				
			||||||
 | 
							ri.sshTunnel = sshClient
 | 
				
			||||||
 | 
							redisOptions.Dialer = dialerFunc
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ri.Cli = redis.NewClient(redisOptions)
 | 
				
			||||||
 | 
						return ri
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func getRedisClusterClient(re *entity.Redis) *redis.ClusterClient {
 | 
					func getRedisClusterClient(re *entity.Redis) *RedisInstance {
 | 
				
			||||||
	return redis.NewClusterClient(&redis.ClusterOptions{
 | 
						ri := &RedisInstance{Id: re.Id, ProjectId: re.ProjectId, Mode: re.Mode}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						redisClusterOptions := &redis.ClusterOptions{
 | 
				
			||||||
		Addrs:       strings.Split(re.Host, ","),
 | 
							Addrs:       strings.Split(re.Host, ","),
 | 
				
			||||||
		Password:    re.Password,
 | 
							Password:    re.Password,
 | 
				
			||||||
		DialTimeout: 8 * time.Second,
 | 
							DialTimeout: 8 * time.Second,
 | 
				
			||||||
	})
 | 
						}
 | 
				
			||||||
 | 
						if re.EnableSshTunnel == 1 {
 | 
				
			||||||
 | 
							sshClient, dialerFunc := getRedisDialer(re.SshTunnelMachineId)
 | 
				
			||||||
 | 
							ri.sshTunnel = sshClient
 | 
				
			||||||
 | 
							redisClusterOptions.Dialer = dialerFunc
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ri.ClusterCli = redis.NewClusterClient(redisClusterOptions)
 | 
				
			||||||
 | 
						return ri
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getRedisDialer(machineId uint64) (*ssh.Client, func(ctx context.Context, network, addr string) (net.Conn, error)) {
 | 
				
			||||||
 | 
						me := MachineApp.GetById(machineId)
 | 
				
			||||||
 | 
						sshClient, err := machine.GetSshClient(me)
 | 
				
			||||||
 | 
						biz.ErrIsNilAppendErr(err, "ssh隧道连接失败: %s")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return sshClient, func(_ context.Context, network, addr string) (net.Conn, error) {
 | 
				
			||||||
 | 
							if sshConn, err := sshClient.Dial(network, addr); err == nil {
 | 
				
			||||||
 | 
								// 将ssh conn包装,否则redis内部设置超时会报错,ssh conn不支持设置超时会返回错误: ssh: tcpChan: deadline not supported
 | 
				
			||||||
 | 
								return &utils.WrapSshConn{Conn: sshConn}, nil
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//------------------------------------------------------------------------------
 | 
					//------------------------------------------------------------------------------
 | 
				
			||||||
@@ -174,11 +211,11 @@ func TestRedisConnection(re *entity.Redis) {
 | 
				
			|||||||
	if re.Mode == "" || re.Mode == entity.RedisModeStandalone {
 | 
						if re.Mode == "" || re.Mode == entity.RedisModeStandalone {
 | 
				
			||||||
		rcli := getRedisCient(re)
 | 
							rcli := getRedisCient(re)
 | 
				
			||||||
		defer rcli.Close()
 | 
							defer rcli.Close()
 | 
				
			||||||
		cmd = rcli
 | 
							cmd = rcli.Cli
 | 
				
			||||||
	} else if re.Mode == entity.RedisModeCluster {
 | 
						} else if re.Mode == entity.RedisModeCluster {
 | 
				
			||||||
		ccli := getRedisClusterClient(re)
 | 
							ccli := getRedisClusterClient(re)
 | 
				
			||||||
		defer ccli.Close()
 | 
							defer ccli.Close()
 | 
				
			||||||
		cmd = ccli
 | 
							cmd = ccli.ClusterCli
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 测试连接
 | 
						// 测试连接
 | 
				
			||||||
@@ -193,6 +230,7 @@ type RedisInstance struct {
 | 
				
			|||||||
	Mode       string
 | 
						Mode       string
 | 
				
			||||||
	Cli        *redis.Client
 | 
						Cli        *redis.Client
 | 
				
			||||||
	ClusterCli *redis.ClusterClient
 | 
						ClusterCli *redis.ClusterClient
 | 
				
			||||||
 | 
						sshTunnel  *ssh.Client
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 获取命令执行接口的具体实现
 | 
					// 获取命令执行接口的具体实现
 | 
				
			||||||
@@ -215,10 +253,18 @@ func (r *RedisInstance) Scan(cursor uint64, match string, count int64) ([]string
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (r *RedisInstance) Close() {
 | 
					func (r *RedisInstance) Close() {
 | 
				
			||||||
	if r.Mode == entity.RedisModeStandalone {
 | 
						if r.Mode == entity.RedisModeStandalone {
 | 
				
			||||||
		r.Cli.Close()
 | 
							if err := r.Cli.Close(); err != nil {
 | 
				
			||||||
		return
 | 
								global.Log.Errorf("关闭redis单机实例[%d]连接失败: %s", r.Id, err.Error())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if r.Mode == entity.RedisModeCluster {
 | 
						if r.Mode == entity.RedisModeCluster {
 | 
				
			||||||
		r.ClusterCli.Close()
 | 
							if err := r.ClusterCli.Close(); err != nil {
 | 
				
			||||||
 | 
								global.Log.Errorf("关闭redis集群实例[%d]连接失败: %s", r.Id, err.Error())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if r.sshTunnel != nil {
 | 
				
			||||||
 | 
							if err := r.sshTunnel.Close(); err != nil {
 | 
				
			||||||
 | 
								global.Log.Errorf("关闭redis实例[%d]的ssh隧道失败: %s", r.Id, err.Error())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
package entity
 | 
					package entity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"mayfly-go/pkg/model"
 | 
						"mayfly-go/pkg/model"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -21,9 +22,24 @@ type Db struct {
 | 
				
			|||||||
	EnvId     uint64
 | 
						EnvId     uint64
 | 
				
			||||||
	Env       string
 | 
						Env       string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	EnableSSH int    `orm:"column(enable_ssh)" json:"enable_ssh"`
 | 
						EnableSshTunnel    int8   `orm:"column(enable_ssh_tunnel)" json:"enableSshTunnel"`        // 是否启用ssh隧道
 | 
				
			||||||
	SSHHost   string `orm:"column(ssh_host)" json:"ssh_host"`
 | 
						SshTunnelMachineId uint64 `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
 | 
				
			||||||
	SSHPort   int    `orm:"column(ssh_port)" json:"ssh_port"`
 | 
					 | 
				
			||||||
	SSHUser   string `orm:"column(ssh_user)" json:"ssh_user"`
 | 
					 | 
				
			||||||
	SSHPass   string `orm:"column(ssh_pass)" json:"-"`
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 获取数据库连接网络, 若没有使用ssh隧道,则直接返回。否则返回拼接的网络需要注册至指定dial
 | 
				
			||||||
 | 
					func (d Db) GetNetwork() string {
 | 
				
			||||||
 | 
						network := d.Network
 | 
				
			||||||
 | 
						if d.EnableSshTunnel == -1 {
 | 
				
			||||||
 | 
							if network == "" {
 | 
				
			||||||
 | 
								return "tcp"
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return network
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return fmt.Sprintf("%s+ssh:%d", d.Type, d.SshTunnelMachineId)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						DbTypeMysql    = "mysql"
 | 
				
			||||||
 | 
						DbTypePostgres = "postgres"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@ type Machine struct {
 | 
				
			|||||||
	Name        string `json:"name"`
 | 
						Name        string `json:"name"`
 | 
				
			||||||
	Ip          string `json:"ip"`         // IP地址
 | 
						Ip          string `json:"ip"`         // IP地址
 | 
				
			||||||
	Username    string `json:"username"`   // 用户名
 | 
						Username    string `json:"username"`   // 用户名
 | 
				
			||||||
 | 
						AuthMethod  int8   `json:"authMethod"` // 授权认证方式
 | 
				
			||||||
	Password    string `json:"-"`
 | 
						Password    string `json:"-"`
 | 
				
			||||||
	Port        int    `json:"port"`   // 端口号
 | 
						Port        int    `json:"port"`   // 端口号
 | 
				
			||||||
	Status      int8   `json:"status"` // 状态 1:启用;2:停用
 | 
						Status      int8   `json:"status"` // 状态 1:启用;2:停用
 | 
				
			||||||
@@ -20,4 +21,6 @@ type Machine struct {
 | 
				
			|||||||
const (
 | 
					const (
 | 
				
			||||||
	MachineStatusEnable        int8 = 1  // 启用状态
 | 
						MachineStatusEnable        int8 = 1  // 启用状态
 | 
				
			||||||
	MachineStatusDisable       int8 = -1 // 禁用状态
 | 
						MachineStatusDisable       int8 = -1 // 禁用状态
 | 
				
			||||||
 | 
						MachineAuthMethodPassword  int8 = 1  // 密码登录
 | 
				
			||||||
 | 
						MachineAuthMethodPublicKey int8 = 2  // 公钥免密登录
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,8 @@ type Mongo struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	Name               string `orm:"column(name)" json:"name"`
 | 
						Name               string `orm:"column(name)" json:"name"`
 | 
				
			||||||
	Uri                string `orm:"column(uri)" json:"uri"`
 | 
						Uri                string `orm:"column(uri)" json:"uri"`
 | 
				
			||||||
 | 
						EnableSshTunnel    int8   `orm:"column(enable_ssh_tunnel)" json:"enableSshTunnel"`        // 是否启用ssh隧道
 | 
				
			||||||
 | 
						SshTunnelMachineId uint64 `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
 | 
				
			||||||
	ProjectId          uint64 `json:"projectId"`
 | 
						ProjectId          uint64 `json:"projectId"`
 | 
				
			||||||
	Project            string `json:"project"`
 | 
						Project            string `json:"project"`
 | 
				
			||||||
	EnvId              uint64 `json:"envId"`
 | 
						EnvId              uint64 `json:"envId"`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,8 @@ type Redis struct {
 | 
				
			|||||||
	Mode               string `json:"mode"`
 | 
						Mode               string `json:"mode"`
 | 
				
			||||||
	Password           string `orm:"column(password)" json:"-"`
 | 
						Password           string `orm:"column(password)" json:"-"`
 | 
				
			||||||
	Db                 int    `orm:"column(database)" json:"db"`
 | 
						Db                 int    `orm:"column(database)" json:"db"`
 | 
				
			||||||
 | 
						EnableSshTunnel    int8   `orm:"column(enable_ssh_tunnel)" json:"enableSshTunnel"`        // 是否启用ssh隧道
 | 
				
			||||||
 | 
						SshTunnelMachineId uint64 `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
 | 
				
			||||||
	Remark             string
 | 
						Remark             string
 | 
				
			||||||
	ProjectId          uint64
 | 
						ProjectId          uint64
 | 
				
			||||||
	Project            string
 | 
						Project            string
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,58 +24,6 @@ type Cli struct {
 | 
				
			|||||||
	sftpClient *sftp.Client
 | 
						sftpClient *sftp.Client
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 机器客户端连接缓存,45分钟内没有访问则会被关闭
 | 
					 | 
				
			||||||
var cliCache = cache.NewTimedCache(45*time.Minute, 5*time.Second).
 | 
					 | 
				
			||||||
	WithUpdateAccessTime(true).
 | 
					 | 
				
			||||||
	OnEvicted(func(key interface{}, value interface{}) {
 | 
					 | 
				
			||||||
		value.(*Cli).Close()
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 是否存在指定id的客户端连接
 | 
					 | 
				
			||||||
func HasCli(machineId uint64) bool {
 | 
					 | 
				
			||||||
	if _, ok := cliCache.Get(machineId); ok {
 | 
					 | 
				
			||||||
		return true
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return false
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 删除指定机器客户端,并关闭客户端连接
 | 
					 | 
				
			||||||
func DeleteCli(id uint64) {
 | 
					 | 
				
			||||||
	cliCache.Delete(id)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 从缓存中获取客户端信息,不存在则回调获取机器信息函数,并新建
 | 
					 | 
				
			||||||
func GetCli(machineId uint64, getMachine func(uint64) *entity.Machine) (*Cli, error) {
 | 
					 | 
				
			||||||
	cli, err := cliCache.ComputeIfAbsent(machineId, func(key interface{}) (interface{}, error) {
 | 
					 | 
				
			||||||
		c, err := newClient(getMachine(machineId))
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return c, nil
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if cli != nil {
 | 
					 | 
				
			||||||
		return cli.(*Cli), err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return nil, err
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//根据机器信息创建客户端对象
 | 
					 | 
				
			||||||
func newClient(machine *entity.Machine) (*Cli, error) {
 | 
					 | 
				
			||||||
	if machine == nil {
 | 
					 | 
				
			||||||
		return nil, errors.New("机器不存在")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	global.Log.Infof("[%s]机器连接:%s:%d", machine.Name, machine.Ip, machine.Port)
 | 
					 | 
				
			||||||
	cli := new(Cli)
 | 
					 | 
				
			||||||
	cli.machine = machine
 | 
					 | 
				
			||||||
	err := cli.connect()
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return cli, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//连接
 | 
					//连接
 | 
				
			||||||
func (c *Cli) connect() error {
 | 
					func (c *Cli) connect() error {
 | 
				
			||||||
	// 如果已经有client则直接返回
 | 
						// 如果已经有client则直接返回
 | 
				
			||||||
@@ -83,16 +31,7 @@ func (c *Cli) connect() error {
 | 
				
			|||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	m := c.machine
 | 
						m := c.machine
 | 
				
			||||||
	config := ssh.ClientConfig{
 | 
						sshClient, err := GetSshClient(m)
 | 
				
			||||||
		User: m.Username,
 | 
					 | 
				
			||||||
		Auth: []ssh.AuthMethod{ssh.Password(m.Password)},
 | 
					 | 
				
			||||||
		HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
 | 
					 | 
				
			||||||
			return nil
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		Timeout: 5 * time.Second,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	addr := fmt.Sprintf("%s:%d", m.Ip, m.Port)
 | 
					 | 
				
			||||||
	sshClient, err := ssh.Dial("tcp", addr, &config)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -100,25 +39,6 @@ func (c *Cli) connect() error {
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 测试连接
 | 
					 | 
				
			||||||
func TestConn(m *entity.Machine) error {
 | 
					 | 
				
			||||||
	config := ssh.ClientConfig{
 | 
					 | 
				
			||||||
		User: m.Username,
 | 
					 | 
				
			||||||
		Auth: []ssh.AuthMethod{ssh.Password(m.Password)},
 | 
					 | 
				
			||||||
		HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
 | 
					 | 
				
			||||||
			return nil
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		Timeout: 5 * time.Second,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	addr := fmt.Sprintf("%s:%d", m.Ip, m.Port)
 | 
					 | 
				
			||||||
	sshClient, err := ssh.Dial("tcp", addr, &config)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	defer sshClient.Close()
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 关闭client和并从缓存中移除
 | 
					// 关闭client和并从缓存中移除
 | 
				
			||||||
func (c *Cli) Close() {
 | 
					func (c *Cli) Close() {
 | 
				
			||||||
	m := c.machine
 | 
						m := c.machine
 | 
				
			||||||
@@ -184,3 +104,91 @@ func (c *Cli) Run(shell string) (*string, error) {
 | 
				
			|||||||
func (c *Cli) GetMachine() *entity.Machine {
 | 
					func (c *Cli) GetMachine() *entity.Machine {
 | 
				
			||||||
	return c.machine
 | 
						return c.machine
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 机器客户端连接缓存,45分钟内没有访问则会被关闭
 | 
				
			||||||
 | 
					var cliCache = cache.NewTimedCache(45*time.Minute, 5*time.Second).
 | 
				
			||||||
 | 
						WithUpdateAccessTime(true).
 | 
				
			||||||
 | 
						OnEvicted(func(_, value interface{}) {
 | 
				
			||||||
 | 
							value.(*Cli).Close()
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 是否存在指定id的客户端连接
 | 
				
			||||||
 | 
					func HasCli(machineId uint64) bool {
 | 
				
			||||||
 | 
						if _, ok := cliCache.Get(machineId); ok {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 删除指定机器客户端,并关闭客户端连接
 | 
				
			||||||
 | 
					func DeleteCli(id uint64) {
 | 
				
			||||||
 | 
						cliCache.Delete(id)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 从缓存中获取客户端信息,不存在则回调获取机器信息函数,并新建
 | 
				
			||||||
 | 
					func GetCli(machineId uint64, getMachine func(uint64) *entity.Machine) (*Cli, error) {
 | 
				
			||||||
 | 
						cli, err := cliCache.ComputeIfAbsent(machineId, func(_ interface{}) (interface{}, error) {
 | 
				
			||||||
 | 
							c, err := newClient(getMachine(machineId))
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return c, nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if cli != nil {
 | 
				
			||||||
 | 
							return cli.(*Cli), err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 测试连接
 | 
				
			||||||
 | 
					func TestConn(m *entity.Machine) error {
 | 
				
			||||||
 | 
						sshClient, err := GetSshClient(m)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer sshClient.Close()
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetSshClient(m *entity.Machine) (*ssh.Client, error) {
 | 
				
			||||||
 | 
						config := ssh.ClientConfig{
 | 
				
			||||||
 | 
							User: m.Username,
 | 
				
			||||||
 | 
							HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Timeout: 5 * time.Second,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if m.AuthMethod == entity.MachineAuthMethodPassword {
 | 
				
			||||||
 | 
							config.Auth = []ssh.AuthMethod{ssh.Password(m.Password)}
 | 
				
			||||||
 | 
						} else if m.AuthMethod == entity.MachineAuthMethodPublicKey {
 | 
				
			||||||
 | 
							if signer, err := ssh.ParsePrivateKey([]byte(m.Password)); err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								config.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						addr := fmt.Sprintf("%s:%d", m.Ip, m.Port)
 | 
				
			||||||
 | 
						sshClient, err := ssh.Dial("tcp", addr, &config)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return sshClient, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//根据机器信息创建客户端对象
 | 
				
			||||||
 | 
					func newClient(machine *entity.Machine) (*Cli, error) {
 | 
				
			||||||
 | 
						if machine == nil {
 | 
				
			||||||
 | 
							return nil, errors.New("机器不存在")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						global.Log.Infof("[%s]机器连接:%s:%d", machine.Name, machine.Ip, machine.Port)
 | 
				
			||||||
 | 
						cli := new(Cli)
 | 
				
			||||||
 | 
						cli.machine = machine
 | 
				
			||||||
 | 
						err := cli.connect()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return cli, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,6 +30,8 @@ CREATE TABLE `t_db` (
 | 
				
			|||||||
  `database` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '数据库,空格分割多个数据库',
 | 
					  `database` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '数据库,空格分割多个数据库',
 | 
				
			||||||
  `params` varchar(125) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '其他连接参数',
 | 
					  `params` varchar(125) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '其他连接参数',
 | 
				
			||||||
  `network` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
					  `network` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
				
			||||||
 | 
					  `enableSshTunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
 | 
				
			||||||
 | 
					  `sshTunnelMachineId` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
 | 
				
			||||||
  `project_id` bigint(20) DEFAULT NULL,
 | 
					  `project_id` bigint(20) DEFAULT NULL,
 | 
				
			||||||
  `project` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
					  `project` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
				
			||||||
  `env_id` bigint(20) DEFAULT NULL COMMENT '环境id',
 | 
					  `env_id` bigint(20) DEFAULT NULL COMMENT '环境id',
 | 
				
			||||||
@@ -41,11 +43,6 @@ CREATE TABLE `t_db` (
 | 
				
			|||||||
  `update_time` datetime DEFAULT NULL,
 | 
					  `update_time` datetime DEFAULT NULL,
 | 
				
			||||||
  `modifier_id` bigint(20) DEFAULT NULL,
 | 
					  `modifier_id` bigint(20) DEFAULT NULL,
 | 
				
			||||||
  `modifier` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
					  `modifier` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
				
			||||||
  `enable_ssh` tinyint(1) unsigned NOT NULL DEFAULT '0',
 | 
					 | 
				
			||||||
  `ssh_host` varchar(50) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
 | 
					 | 
				
			||||||
  `ssh_port` int(8) NOT NULL,
 | 
					 | 
				
			||||||
  `ssh_user` varchar(255) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
 | 
					 | 
				
			||||||
  `ssh_pass` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
					 | 
				
			||||||
  PRIMARY KEY (`id`)
 | 
					  PRIMARY KEY (`id`)
 | 
				
			||||||
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据库资源信息表';
 | 
					) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据库资源信息表';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -112,7 +109,8 @@ CREATE TABLE `t_machine` (
 | 
				
			|||||||
  `ip` varchar(36) COLLATE utf8mb4_bin NOT NULL,
 | 
					  `ip` varchar(36) COLLATE utf8mb4_bin NOT NULL,
 | 
				
			||||||
  `port` int(12) NOT NULL,
 | 
					  `port` int(12) NOT NULL,
 | 
				
			||||||
  `username` varchar(12) COLLATE utf8mb4_bin NOT NULL,
 | 
					  `username` varchar(12) COLLATE utf8mb4_bin NOT NULL,
 | 
				
			||||||
  `password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
					  `auth_method` tinyint(2) NULL DEFAULT NULL COMMENT '1.密码登录2.publickey登录',
 | 
				
			||||||
 | 
					  `password` varchar(3200) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
				
			||||||
  `status` tinyint(2) NOT NULL COMMENT '状态: 1:启用; -1:禁用',
 | 
					  `status` tinyint(2) NOT NULL COMMENT '状态: 1:启用; -1:禁用',
 | 
				
			||||||
  `remark` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
					  `remark` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
				
			||||||
  `need_monitor` tinyint(2) DEFAULT NULL,
 | 
					  `need_monitor` tinyint(2) DEFAULT NULL,
 | 
				
			||||||
@@ -263,6 +261,8 @@ CREATE TABLE `t_redis` (
 | 
				
			|||||||
  `password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
					  `password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
				
			||||||
  `db` int(32) DEFAULT NULL,
 | 
					  `db` int(32) DEFAULT NULL,
 | 
				
			||||||
  `mode` varchar(32) DEFAULT NULL,
 | 
					  `mode` varchar(32) DEFAULT NULL,
 | 
				
			||||||
 | 
					  `enableSshTunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
 | 
				
			||||||
 | 
					  `sshTunnelMachineId` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
 | 
				
			||||||
  `remark` varchar(125) DEFAULT NULL,
 | 
					  `remark` varchar(125) DEFAULT NULL,
 | 
				
			||||||
  `project_id` bigint(20) DEFAULT NULL,
 | 
					  `project_id` bigint(20) DEFAULT NULL,
 | 
				
			||||||
  `project` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
					  `project` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
				
			||||||
@@ -668,6 +668,8 @@ CREATE TABLE `t_mongo` (
 | 
				
			|||||||
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
 | 
					  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
  `name` varchar(36) COLLATE utf8mb4_bin NOT NULL COMMENT '名称',
 | 
					  `name` varchar(36) COLLATE utf8mb4_bin NOT NULL COMMENT '名称',
 | 
				
			||||||
  `uri` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '连接uri',
 | 
					  `uri` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '连接uri',
 | 
				
			||||||
 | 
					  `enableSshTunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
 | 
				
			||||||
 | 
					  `sshTunnelMachineId` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
 | 
				
			||||||
  `project_id` bigint(20) NOT NULL,
 | 
					  `project_id` bigint(20) NOT NULL,
 | 
				
			||||||
  `project` varchar(36) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
					  `project` varchar(36) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
				
			||||||
  `env_id` bigint(20) DEFAULT NULL,
 | 
					  `env_id` bigint(20) DEFAULT NULL,
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										36
									
								
								server/pkg/utils/ssh_conn_wrap.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								server/pkg/utils/ssh_conn_wrap.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					package utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type WrapSshConn struct {
 | 
				
			||||||
 | 
						Conn net.Conn
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *WrapSshConn) Read(b []byte) (n int, err error) {
 | 
				
			||||||
 | 
						return c.Conn.Read(b)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *WrapSshConn) Write(b []byte) (n int, err error) {
 | 
				
			||||||
 | 
						return c.Conn.Write(b)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (c *WrapSshConn) Close() error {
 | 
				
			||||||
 | 
						return c.Conn.Close()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (c *WrapSshConn) LocalAddr() net.Addr {
 | 
				
			||||||
 | 
						return c.Conn.LocalAddr()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (c *WrapSshConn) RemoteAddr() net.Addr {
 | 
				
			||||||
 | 
						return c.Conn.RemoteAddr()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (c *WrapSshConn) SetDeadline(t time.Time) error {
 | 
				
			||||||
 | 
						return c.Conn.SetDeadline(t)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (c *WrapSshConn) SetReadDeadline(t time.Time) error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (c *WrapSshConn) SetWriteDeadline(t time.Time) error {
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user