From 5e72a8302e47bb75828a97e3d97f8310c83caf87 Mon Sep 17 00:00:00 2001
From: mlhiter <3076438032@qq.com>
Date: Wed, 6 Nov 2024 18:09:36 +0800
Subject: [PATCH] feat: devboxListView refactor
---
.../ide/vscode/devbox/images/delete.svg | 1 +
extensions/ide/vscode/devbox/package.json | 22 ++--
extensions/ide/vscode/devbox/src/api/ssh.ts | 51 ++++----
.../devbox/src/commands/remoteConnector.ts | 79 ++++--------
.../ide/vscode/devbox/src/constant/file.ts | 9 ++
extensions/ide/vscode/devbox/src/extension.ts | 8 +-
.../DevboxListViewProvider.ts} | 43 +++---
.../ide/vscode/devbox/src/utils/handleUri.ts | 7 +-
.../ide/vscode/devbox/src/utils/sshConfig.ts | 122 ++++++++++++++++++
9 files changed, 225 insertions(+), 117 deletions(-)
create mode 100644 extensions/ide/vscode/devbox/images/delete.svg
create mode 100644 extensions/ide/vscode/devbox/src/constant/file.ts
rename extensions/ide/vscode/devbox/src/{commands/treeview.ts => providers/DevboxListViewProvider.ts} (86%)
diff --git a/extensions/ide/vscode/devbox/images/delete.svg b/extensions/ide/vscode/devbox/images/delete.svg
new file mode 100644
index 00000000000..fbb59e83b77
--- /dev/null
+++ b/extensions/ide/vscode/devbox/images/delete.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/extensions/ide/vscode/devbox/package.json b/extensions/ide/vscode/devbox/package.json
index 51116023733..f4c70e70eb4 100644
--- a/extensions/ide/vscode/devbox/package.json
+++ b/extensions/ide/vscode/devbox/package.json
@@ -58,22 +58,23 @@
"title": "Create Devbox",
"icon": "images/create.svg"
},
- {
- "command": "devboxDashboard.deleteDevbox",
- "title": "Delete Devbox"
- },
{
"command": "devboxDashboard.openDevbox",
"title": "Open Devbox",
"icon": "images/open.svg"
},
+ {
+ "command": "devboxDashboard.deleteDevbox",
+ "title": "Delete Devbox",
+ "icon": "images/delete.svg"
+ },
{
"command": "devbox.openExternalLink",
"title": "Devbox:Open in Browser"
}
],
"views": {
- "devboxView": [
+ "devboxListView": [
{
"id": "devboxDashboard",
"name": "My Projects"
@@ -99,7 +100,7 @@
"viewsContainers": {
"activitybar": [
{
- "id": "devboxView",
+ "id": "devboxListView",
"title": "Devbox",
"icon": "images/explorer.svg"
}
@@ -138,13 +139,14 @@
],
"view/item/context": [
{
- "command": "devboxDashboard.deleteDevbox",
- "when": "view == devboxDashboard && viewItem == devbox"
+ "command": "devboxDashboard.openDevbox",
+ "when": "view == devboxDashboard && viewItem == devbox",
+ "group": "inline@1"
},
{
- "command": "devboxDashboard.openDevbox",
+ "command": "devboxDashboard.deleteDevbox",
"when": "view == devboxDashboard && viewItem == devbox",
- "group": "inline"
+ "group": "inline@2"
}
]
}
diff --git a/extensions/ide/vscode/devbox/src/api/ssh.ts b/extensions/ide/vscode/devbox/src/api/ssh.ts
index e634bcbd09f..55e605b497a 100644
--- a/extensions/ide/vscode/devbox/src/api/ssh.ts
+++ b/extensions/ide/vscode/devbox/src/api/ssh.ts
@@ -1,49 +1,52 @@
-const fs = require('fs')
+import fs from 'fs'
-export const parseSSHConfig = (configFilePath: string) => {
+import { GlobalStateManager } from '../utils/globalStateManager'
+
+export const parseSSHConfig = (filePath: string) => {
return new Promise((resolve, reject) => {
- fs.readFile(configFilePath, 'utf-8', (err: any, data: any) => {
+ fs.readFile(filePath, 'utf-8', (err: any, data: any) => {
if (err) {
return reject(err)
}
const lines = data.split('\n')
const devboxList = [] as any[]
- let currentHost = {} as any
- let lastComment = ''
+ let currentHostObj = {} as any
lines.forEach((line: string) => {
line = line.trim()
- if (line.startsWith('#')) {
- // 保存注释,特别是 WorkingDir 注释
- lastComment = line
- if (line.startsWith('# WorkingDir:')) {
- currentHost.remotePath = line.split(':')[1].trim()
- }
- } else if (line.startsWith('Host ')) {
- // 如果当前有主机信息且是 usw.sailos.io,则保存
- if (currentHost.hostName === 'usw.sailos.io') {
- devboxList.push(currentHost)
+ if (line.startsWith('Host ')) {
+ // TODO:这里改成注入,而不是硬编码
+ if (!!currentHostObj.StrictHostKeyChecking) {
+ currentHostObj.remotePath =
+ GlobalStateManager.getWorkDir('remotePath')
+ devboxList.push(currentHostObj)
}
- // 开始新的主机信息
- currentHost = { host: line.split(' ')[1] }
+ currentHostObj = { host: line.split(' ')[1] }
} else if (line.startsWith('HostName ')) {
- currentHost.hostName = line.split(' ')[1]
+ currentHostObj.hostName = line.split(' ')[1]
} else if (line.startsWith('User ')) {
- currentHost.user = line.split(' ')[1]
+ currentHostObj.user = line.split(' ')[1]
} else if (line.startsWith('Port ')) {
- currentHost.port = line.split(' ')[1]
+ currentHostObj.port = line.split(' ')[1]
} else if (line.startsWith('IdentityFile ')) {
- currentHost.identityFile = line.split(' ')[1]
+ currentHostObj.identityFile = line.split(' ')[1]
+ } else if (line.startsWith('IdentitiesOnly ')) {
+ currentHostObj.IdentitiesOnly = line.split(' ')[1]
+ } else if (line.startsWith('StrictHostKeyChecking ')) {
+ currentHostObj.StrictHostKeyChecking = line.split(' ')[1]
}
})
- // 最后一个主机信息处理
- if (currentHost.hostName === 'usw.sailos.io') {
- devboxList.push(currentHost)
+ // the last one
+ if (!!currentHostObj.StrictHostKeyChecking) {
+ currentHostObj.remotePath = GlobalStateManager.getWorkDir('remotePath')
+ devboxList.push(currentHostObj)
}
+ console.log(devboxList)
+
resolve(devboxList)
})
})
diff --git a/extensions/ide/vscode/devbox/src/commands/remoteConnector.ts b/extensions/ide/vscode/devbox/src/commands/remoteConnector.ts
index 8548c4ce0f5..eb4e89450e3 100644
--- a/extensions/ide/vscode/devbox/src/commands/remoteConnector.ts
+++ b/extensions/ide/vscode/devbox/src/commands/remoteConnector.ts
@@ -1,4 +1,3 @@
-import path from 'path'
import * as os from 'os'
import * as fs from 'fs'
import * as vscode from 'vscode'
@@ -7,13 +6,15 @@ import { execSync } from 'child_process'
import { Disposable } from '../common/dispose'
import { modifiedRemoteSSHConfig } from '../utils/remoteSSHConfig'
-
-const defaultSSHConfigPath = path.resolve(os.homedir(), '.ssh/config')
-const defaultDevboxSSHConfigPath = path.resolve(
- os.homedir(),
- '.ssh/sealos/devbox_config'
-)
-const defaultSSHKeyPath = path.resolve(os.homedir(), '.ssh/sealos')
+import {
+ defaultSSHConfigPath,
+ defaultDevboxSSHConfigPath,
+ defaultSSHKeyPath,
+} from '../constant/file'
+import {
+ convertSSHConfigToVersion2,
+ ensureFileExists,
+} from '../utils/sshConfig'
export class RemoteSSHConnector extends Disposable {
constructor(context: vscode.ExtensionContext) {
@@ -26,6 +27,22 @@ export class RemoteSSHConnector extends Disposable {
)
}
}
+ private sshConfigPreProcess() {
+ // 1. ensure .ssh/config exists
+ ensureFileExists(defaultSSHConfigPath, '.ssh')
+ // 2. ensure .ssh/sealos/devbox_config exists
+ ensureFileExists(defaultDevboxSSHConfigPath, '.ssh/sealos')
+ // 3. ensure .ssh/config includes .ssh/sealos/devbox_config
+ const existingSSHConfig = fs.readFileSync(defaultSSHConfigPath, 'utf8')
+ if (!existingSSHConfig.includes('Include ~/.ssh/sealos/devbox_config')) {
+ let existingSSHConfig = fs.readFileSync(defaultSSHConfigPath, 'utf-8')
+ const newConfig =
+ 'Include ~/.ssh/sealos/devbox_config\n' + existingSSHConfig
+ fs.writeFileSync(defaultSSHConfigPath, newConfig)
+ }
+ // 4. ensure sshConfig from version1 to version2
+ convertSSHConfigToVersion2(defaultDevboxSSHConfigPath)
+ }
private async connectRemoteSSH(args: {
sshDomain: string
@@ -59,51 +76,9 @@ export class RemoteSSHConnector extends Disposable {
})
const sshConfigString = SSHConfig.stringify(sshConfig)
- try {
- // 1. ensure .ssh/config exists
- if (!fs.existsSync(defaultSSHConfigPath)) {
- fs.mkdirSync(path.resolve(os.homedir(), '.ssh'), {
- recursive: true,
- })
- fs.writeFileSync(defaultSSHConfigPath, '', 'utf8')
- // .ssh/config authority
- if (os.platform() === 'win32') {
- // Windows
- execSync(`icacls "${defaultSSHConfigPath}" /inheritance:r`)
- execSync(
- `icacls "${defaultSSHConfigPath}" /grant:r ${process.env.USERNAME}:F`
- )
- execSync(`icacls "${defaultSSHConfigPath}" /remove:g everyone`)
- } else {
- // Unix-like system (Mac, Linux)
- execSync(`chmod 600 "${defaultSSHConfigPath}"`)
- }
- }
- // 2. ensure .ssh/sealos/devbox_config exists and has the correct authority
- if (!fs.existsSync(defaultDevboxSSHConfigPath)) {
- fs.mkdirSync(path.resolve(os.homedir(), '.ssh/sealos'), {
- recursive: true,
- })
- fs.writeFileSync(defaultDevboxSSHConfigPath, '', 'utf8')
- if (os.platform() === 'win32') {
- execSync(`icacls "${defaultDevboxSSHConfigPath}" /inheritance:r`)
- execSync(
- `icacls "${defaultDevboxSSHConfigPath}" /grant:r ${process.env.USERNAME}:F`
- )
- execSync(`icacls "${defaultDevboxSSHConfigPath}" /remove:g everyone`)
- } else {
- execSync(`chmod 600 "${defaultDevboxSSHConfigPath}"`)
- }
- }
- // 3. ensure .ssh/config includes .ssh/sealos/devbox_config
- const existingSSHConfig = fs.readFileSync(defaultSSHConfigPath, 'utf8')
- if (!existingSSHConfig.includes('Include ~/.ssh/sealos/devbox_config')) {
- let existingSSHConfig = fs.readFileSync(defaultSSHConfigPath, 'utf-8')
- const newConfig =
- 'Include ~/.ssh/sealos/devbox_config\n' + existingSSHConfig
- fs.writeFileSync(defaultSSHConfigPath, newConfig)
- }
+ this.sshConfigPreProcess()
+ try {
// 读取现有的 devbox 配置文件
const existingDevboxConfigLines = fs
.readFileSync(defaultDevboxSSHConfigPath, 'utf8')
diff --git a/extensions/ide/vscode/devbox/src/constant/file.ts b/extensions/ide/vscode/devbox/src/constant/file.ts
new file mode 100644
index 00000000000..71d5657f405
--- /dev/null
+++ b/extensions/ide/vscode/devbox/src/constant/file.ts
@@ -0,0 +1,9 @@
+import path from 'path'
+import * as os from 'os'
+
+export const defaultSSHConfigPath = path.resolve(os.homedir(), '.ssh/config')
+export const defaultDevboxSSHConfigPath = path.resolve(
+ os.homedir(),
+ '.ssh/sealos/devbox_config'
+)
+export const defaultSSHKeyPath = path.resolve(os.homedir(), '.ssh/sealos')
diff --git a/extensions/ide/vscode/devbox/src/extension.ts b/extensions/ide/vscode/devbox/src/extension.ts
index d2be9dee5c8..144ddac0bf3 100644
--- a/extensions/ide/vscode/devbox/src/extension.ts
+++ b/extensions/ide/vscode/devbox/src/extension.ts
@@ -2,7 +2,7 @@ import * as vscode from 'vscode'
import { Webview } from './commands/webview'
import { RemoteSSHConnector } from './commands/remoteConnector'
-import { TreeView } from './commands/treeview'
+import { DevboxListViewProvider } from './providers/DevboxListViewProvider'
import { UriHandler } from './utils/handleUri'
import { NetworkViewProvider } from './providers/NetworkViewProvider'
import { DBViewProvider } from './providers/DbViewProvider'
@@ -22,9 +22,9 @@ export async function activate(context: vscode.ExtensionContext) {
const remoteConnector = new RemoteSSHConnector(context)
context.subscriptions.push(remoteConnector)
- // tree view
- const treeView = new TreeView(context)
- context.subscriptions.push(treeView)
+ // devboxList view
+ const devboxListViewProvider = new DevboxListViewProvider(context)
+ context.subscriptions.push(devboxListViewProvider)
// token manager
GlobalStateManager.init(context)
diff --git a/extensions/ide/vscode/devbox/src/commands/treeview.ts b/extensions/ide/vscode/devbox/src/providers/DevboxListViewProvider.ts
similarity index 86%
rename from extensions/ide/vscode/devbox/src/commands/treeview.ts
rename to extensions/ide/vscode/devbox/src/providers/DevboxListViewProvider.ts
index 73d318b9701..35cbb76027f 100644
--- a/extensions/ide/vscode/devbox/src/commands/treeview.ts
+++ b/extensions/ide/vscode/devbox/src/providers/DevboxListViewProvider.ts
@@ -1,18 +1,18 @@
-import path from 'path'
-import * as os from 'os'
import * as vscode from 'vscode'
import { parseSSHConfig } from '../api/ssh'
import { Disposable } from '../common/dispose'
import { DevboxListItem } from '../types/devbox'
+import { defaultDevboxSSHConfigPath } from '../constant/file'
-export class TreeView extends Disposable {
+export class DevboxListViewProvider extends Disposable {
constructor(context: vscode.ExtensionContext) {
super()
if (context.extension.extensionKind === vscode.ExtensionKind.UI) {
+ // view
const projectTreeDataProvider = new MyTreeDataProvider('devboxDashboard')
+ // TODO: 完善 feedback部分
const feedbackTreeDataProvider = new MyTreeDataProvider('devboxFeedback')
- // views
const devboxDashboardView = vscode.window.createTreeView(
'devboxDashboard',
{
@@ -20,8 +20,6 @@ export class TreeView extends Disposable {
}
)
this._register(devboxDashboardView)
-
- // 添加视图可见性变化事件监听器
this._register(
devboxDashboardView.onDidChangeVisibility(() => {
if (devboxDashboardView.visible) {
@@ -29,7 +27,6 @@ export class TreeView extends Disposable {
}
})
)
-
// commands
this._register(
vscode.commands.registerCommand('devboxDashboard.refresh', () => {
@@ -83,12 +80,7 @@ class MyTreeDataProvider implements vscode.TreeDataProvider {
private refreshData(): void {
if (this.treeName === 'devboxDashboard') {
- const defaultSSHConfigPath = path.resolve(
- os.homedir(),
- '.ssh/sealos/devbox_config'
- )
-
- parseSSHConfig(defaultSSHConfigPath).then((data) => {
+ parseSSHConfig(defaultDevboxSSHConfigPath).then((data) => {
this.treeData = data as DevboxListItem[]
this._onDidChangeTreeData.fire(undefined)
})
@@ -108,9 +100,11 @@ class MyTreeDataProvider implements vscode.TreeDataProvider {
return element
}
+ // TODO: 根据不同的代理跳转到不同的页面,而且可以进行设置里的配置
create(item: MyTreeItem) {
- vscode.commands.executeCommand('devbox.openWebview')
- vscode.window.showInformationMessage('create')
+ vscode.commands.executeCommand('devbox.openExternalLink', [
+ 'https://usw.sailos.io/?openapp=system-devbox',
+ ])
}
async open(item: MyTreeItem) {
@@ -138,7 +132,7 @@ class MyTreeDataProvider implements vscode.TreeDataProvider {
if (!element) {
// 第一级:显示所有域名
const domains = [
- ...new Set(this.treeData.map((item) => item.host.split('-')[0])),
+ ...new Set(this.treeData.map((item) => item.host.split('_')[0])),
]
return Promise.resolve(
domains.map(
@@ -157,10 +151,7 @@ class MyTreeDataProvider implements vscode.TreeDataProvider {
...new Set(
this.treeData
.filter((item) => item.host.startsWith(element.label as string))
- .map((item) => {
- const parts = item.host.split('-')
- return parts.slice(1, 3).join('-')
- })
+ .map((item) => item.host.split('_')[1])
),
]
return Promise.resolve(
@@ -178,15 +169,15 @@ class MyTreeDataProvider implements vscode.TreeDataProvider {
} else if (!element.devboxName) {
// 第三级:显示指定命名空间下的所有 devbox
const devboxes = this.treeData.filter((item) => {
- const parts = item.host.split('-')
+ const parts = item.host.split('_')
const domain = parts[0]
- const namespace = parts.slice(1, 3).join('-')
+ const namespace = parts[1]
return domain === element.domain && namespace === element.namespace
})
return Promise.resolve(
devboxes.map((devbox) => {
- const parts = devbox.host.split('-')
- const devboxName = parts.slice(3, -1).join('-')
+ const parts = devbox.host.split('_')
+ const devboxName = parts.slice(2).join('_')
const treeItem = new MyTreeItem(
devboxName,
devbox.hostName,
@@ -195,9 +186,9 @@ class MyTreeDataProvider implements vscode.TreeDataProvider {
element.namespace,
devboxName,
devbox.host,
- devbox.remotePath // 添加这个参数
+ devbox.remotePath
)
- treeItem.contextValue = 'devbox' // 确保设置了正确的 contextValue
+ treeItem.contextValue = 'devbox'
return treeItem
})
)
diff --git a/extensions/ide/vscode/devbox/src/utils/handleUri.ts b/extensions/ide/vscode/devbox/src/utils/handleUri.ts
index 898c0800ca4..57a6b231ac6 100644
--- a/extensions/ide/vscode/devbox/src/utils/handleUri.ts
+++ b/extensions/ide/vscode/devbox/src/utils/handleUri.ts
@@ -20,6 +20,10 @@ export class UriHandler {
GlobalStateManager.setToken(params.sshHostLabel, params.token)
}
+ if (params.workingDir && params.sshHostLabel) {
+ GlobalStateManager.setWorkDir(params.sshHostLabel, params.workingDir)
+ }
+
if (params.sshPort === '0') {
vscode.window.showInformationMessage(
`SSH Port is not correct,maybe your devbox's nodeport is over the limit`
@@ -44,13 +48,14 @@ export class UriHandler {
}
private validateParams(params: any): boolean {
+ console.log(params)
return !!(
params.sshDomain &&
params.sshPort &&
params.base64PrivateKey &&
params.sshHostLabel &&
params.workingDir &&
- params.authToken
+ params.token
)
}
}
diff --git a/extensions/ide/vscode/devbox/src/utils/sshConfig.ts b/extensions/ide/vscode/devbox/src/utils/sshConfig.ts
index e69de29bb2d..c25ccf1be83 100644
--- a/extensions/ide/vscode/devbox/src/utils/sshConfig.ts
+++ b/extensions/ide/vscode/devbox/src/utils/sshConfig.ts
@@ -0,0 +1,122 @@
+import * as os from 'os'
+import * as fs from 'fs'
+import path from 'path'
+import { execSync } from 'child_process'
+import { GlobalStateManager } from './globalStateManager'
+
+// 将老版本的 ssh 配置改成新版本的 ssh 配置
+// # WorkingDir: /home/sealos/project
+// Host bja.sealos.run-ns-wappehp7-test-t6unaf4bbob
+// HostName bja.sealos.run
+// User sealos
+// Port 40398
+// IdentityFile ~/.ssh/sealos/bja.sealos.run_ns-wappehp7_test
+// IdentitiesOnly yes
+// StrictHostKeyChecking no
+
+// 转换为下边的:
+// Host的转换,去掉随机串,然后-改为_
+// 去掉WorkingDir 的注释,改为全局存储
+// Host usw.sailos.io_ns-rqtny6y6_devbox
+// HostName usw.sailos.io
+// User devbox
+// Port 31328
+// IdentityFile ~/.ssh/sealos/usw.sailos.io_ns-rqtny6y6_devbox
+// IdentitiesOnly yes
+// StrictHostKeyChecking no
+export function convertSSHConfigToVersion2(filePath: string) {
+ const output: Record> = {}
+ let result = ''
+
+ fs.readFile(filePath, 'utf8', (err, data) => {
+ if (err) {
+ console.error('Error reading file:', err)
+ return
+ }
+
+ const lines = data.split('\n')
+ let currentWorkDir: any = null
+ let formattedHostName = ''
+
+ lines.forEach((line) => {
+ line = line.trim()
+
+ // 处理 WorkingDir 行
+ if (line.startsWith('# WorkingDir:')) {
+ currentWorkDir = line.split(': ')[1].trim()
+ return
+ }
+
+ // 处理 Host 行
+ const hostMatch = line.match(/^Host (.+)/)
+ if (hostMatch) {
+ let hostName = hostMatch[1]
+ if (hostName.includes('_ns-')) {
+ formattedHostName = hostName
+ } else {
+ hostName = hostName.replace(/-([^-\s]+)$/, '')
+ const namespace = hostName.match(/ns-([a-z0-9]+)(?=-)/)
+ if (namespace) {
+ formattedHostName = hostName.replace(
+ /^(.+)-ns-([a-z0-9]+)-(.+)$/,
+ '$1_ns-$2_$3'
+ )
+ } else {
+ formattedHostName = hostName
+ }
+ }
+
+ // 初始化 Host 对象
+ output[formattedHostName] = {
+ workDir: currentWorkDir,
+ }
+ return
+ }
+
+ // 处理其他配置项
+ if (currentWorkDir && line) {
+ const keyValueMatch = line.match(/(\S+)\s+(.+)/)
+ if (keyValueMatch) {
+ const [_, key, value] = keyValueMatch
+ output[formattedHostName][key] = value
+ }
+ }
+ })
+ console.log('output', output)
+
+ for (const [host, config] of Object.entries(output)) {
+ if (config.workDir) {
+ GlobalStateManager.setWorkDir(host, config.workDir)
+ }
+
+ result += `Host ${host}\n`
+ for (const [key, value] of Object.entries(config)) {
+ if (key !== 'workDir') {
+ result += ` ${key} ${value}\n`
+ }
+ }
+ result += '\n'
+ }
+ result = result.trim()
+ fs.writeFileSync(filePath, result, 'utf8')
+ })
+}
+
+export function ensureFileExists(filePath: string, parentDir: string) {
+ if (!fs.existsSync(filePath)) {
+ fs.mkdirSync(path.resolve(os.homedir(), parentDir), {
+ recursive: true,
+ })
+ fs.writeFileSync(filePath, '', 'utf8')
+ // .ssh/config authority
+ if (os.platform() === 'win32') {
+ // Windows
+ execSync(`icacls "${filePath}" /inheritance:r`)
+ execSync(`icacls "${filePath}" /grant:r ${process.env.USERNAME}:F`)
+ execSync(`icacls "${filePath}" /remove:g everyone`)
+ } else {
+ // Unix-like system (Mac, Linux)
+ execSync(`chmod 600 "${filePath}"`)
+ }
+ }
+}