Skip to content

Commit d276cd5

Browse files
author
gitlayzer
committed
feat: productize dashboard workbench
1 parent 6d13ed1 commit d276cd5

19 files changed

Lines changed: 1511 additions & 64 deletions

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Sealtun 是一款功能强大、设计优雅的 CLI 工具,旨在为 **Sealos
1313
- 👤 **Profile 多账号管理**:可把不同 Sealos 账号、region、workspace 和 kubeconfig 保存为命名 profile,按需切换。
1414
- 🚀 **一键暴露服务**:执行 `sealtun expose 8080`,即可获得一个受信任的 HTTPS URL,将流量安全地路由到本地。
1515
- 🌐 **自定义域名自动化**:可用 `domain plan/add/verify/status/doctor` 生成 CNAME 指引、等待 DNS、绑定域名并检查证书状态。
16-
- 📊 **状态与诊断**`doctor <tunnel-id>``inspect --remote``logs``events``metrics` 可定位本地端口、daemon、远端 Pod、Service、Ingress 与证书问题。
16+
- 📊 **状态、诊断与工作台**`doctor <tunnel-id>``inspect --remote``logs``events``metrics``dashboard` 可定位本地端口、daemon、远端 Pod、Service、Ingress 与证书问题,也可在本地工作台中管理隧道
1717
- 🧩 **协议模板**`template https|ssh|tcp|mysql|postgres|redis|mqtt` 可生成直接命令和 `sealtun.yaml` 示例。
1818
- 🧾 **声明式配置**`apply -f sealtun.yaml` 可用 YAML 声明隧道,并以稳定名称幂等创建或更新。
1919
- 🌐 **深度适配 Sealos**:原生使用 Sealos Cloud 的 Kubernetes、Service 与 Ingress 能力,当前稳定支持 HTTPS 入口和 WebSocket 隧道。
@@ -314,15 +314,22 @@ sealtun doctor <tunnel-id> --json
314314

315315
`metrics` 会聚合本地 session 状态、远端 Deployment/Pod/Ingress 状态,并在远端 Pod 支持时读取受 Bearer secret 保护的 `/_sealtun/metrics` 请求计数。TCP/SSH 四层隧道还会暴露 TCP 连接数、活跃连接数、字节数和错误数。
316316

317-
启动本地只读控制台
317+
启动本地工作台
318318
```bash
319319
sealtun dashboard
320320

321321
# 自定义监听地址
322322
sealtun dashboard --addr 127.0.0.1 --port 19777
323323
```
324324

325-
Dashboard 仅监听本地地址,数据来自当前 CLI 进程读取到的本地 session、登录状态、远端诊断和自定义域名状态。
325+
Dashboard 默认仅监听本地地址,数据来自当前 active profile/region/namespace 的本地 session、登录状态、远端诊断和自定义域名状态。页面可以创建 HTTPS/SSH/TCP 隧道、执行 `sealtun.yaml` 的 dry-run/diff/apply、stop/start/cleanup 隧道、查看 logs/metrics/events,并执行 domain plan/add/verify/clear。
326+
327+
```bash
328+
# 允许远程访问工作台,仅建议在可信网络临时使用
329+
sealtun dashboard --addr 0.0.0.0 --allow-remote
330+
```
331+
332+
远程模式不会把 dashboard token 写进 HTML;访问者需要 URL fragment 或请求头中的 token。所有写操作都要求页面确认,并由后端再次校验 `confirm` 字段,避免误触或脚本误调用。
326333

327334
### 7. 协议模板
328335
不确定该怎么写命令或声明式配置时,可以先生成模板:

README_EN.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ It connects your local development machine straight to the internet by dynamical
1313
- 👤 **Named Profiles**: Save different Sealos accounts, regions, workspaces, and kubeconfigs as named profiles and switch between them.
1414
- 🚀 **One-Command Expose**: Execute `sealtun expose 8080`, and get a fully trusted HTTPS URL for your localhost securely routed.
1515
- 🌐 **Custom Domain Automation**: Use `domain plan/add/verify/status/doctor` to generate CNAME guidance, wait for DNS, attach domains, and inspect certificate readiness.
16-
- 📊 **Status and Diagnostics**: Use `doctor <tunnel-id>`, `inspect --remote`, `logs`, `events`, and `metrics` to diagnose local ports, daemon state, remote Pods, Services, Ingresses, and certificates.
16+
- 📊 **Status, Diagnostics, and Workbench**: Use `doctor <tunnel-id>`, `inspect --remote`, `logs`, `events`, `metrics`, and `dashboard` to diagnose local ports, daemon state, remote Pods, Services, Ingresses, and certificates, or manage tunnels from the local workbench.
1717
- 🧩 **Protocol Templates**: Use `template https|ssh|tcp|mysql|postgres|redis|mqtt` to generate commands and `sealtun.yaml` examples.
1818
- 🧾 **Declarative Config**: Use `apply -f sealtun.yaml` to declare tunnels in YAML and create or update them with stable names.
1919
- 🌐 **Optimized for Sealos**: Native support for Sealos Cloud domains, HTTPS traffic, and WebSocket tunnels.
@@ -314,15 +314,22 @@ sealtun doctor <tunnel-id> --json
314314

315315
`metrics` combines local session state, remote Deployment/Pod/Ingress readiness, and server-side request counters when the remote pod supports the Bearer-secret-protected `/_sealtun/metrics` endpoint. TCP/SSH tunnels also expose TCP connection, active connection, byte, and error counters.
316316

317-
Run the local read-only dashboard:
317+
Run the local workbench:
318318
```bash
319319
sealtun dashboard
320320

321321
# Custom listen address
322322
sealtun dashboard --addr 127.0.0.1 --port 19777
323323
```
324324

325-
The dashboard listens locally and reads the same data as the CLI: local sessions, login state, remote diagnostics, and custom domain readiness.
325+
The dashboard listens locally by default and uses only the current active profile/region/namespace. It reads local sessions, login state, remote diagnostics, and custom domain readiness. The page can create HTTPS/SSH/TCP tunnels, run `sealtun.yaml` dry-run/diff/apply, stop/start/cleanup tunnels, view logs/metrics/events, and run domain plan/add/verify/clear.
326+
327+
```bash
328+
# Allow remote access to the workbench; use only on trusted networks
329+
sealtun dashboard --addr 0.0.0.0 --allow-remote
330+
```
331+
332+
Remote mode does not embed the dashboard token in HTML; callers need the URL fragment token or request header. Every mutating action requires a page confirmation and a backend-validated `confirm` field to avoid accidental clicks or scripted misuse.
326333

327334
### 7. Protocol templates
328335
When you are unsure which command or declarative config to use, generate a template first:

cmd/apply.go

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,18 @@ func runApply(ctx context.Context, path string, dryRun bool) ([]applyResult, err
169169
if err != nil {
170170
return nil, err
171171
}
172+
return runApplyConfig(ctx, config, dryRun)
173+
}
174+
175+
func runApplyContent(ctx context.Context, data []byte, dryRun bool) ([]applyResult, error) {
176+
config, err := loadApplyData("dashboard yaml", data)
177+
if err != nil {
178+
return nil, err
179+
}
180+
return runApplyConfig(ctx, config, dryRun)
181+
}
182+
183+
func runApplyConfig(ctx context.Context, config *applyFile, dryRun bool) ([]applyResult, error) {
172184
if len(config.Tunnels) == 0 {
173185
return nil, fmt.Errorf("apply file has no tunnels")
174186
}
@@ -253,18 +265,25 @@ func loadApplyFile(path string) (*applyFile, error) {
253265
if len(data) > applyFileMaxBytes {
254266
return nil, fmt.Errorf("apply file %s is too large; limit is %d bytes", path, applyFileMaxBytes)
255267
}
268+
return loadApplyData(path, data)
269+
}
270+
271+
func loadApplyData(label string, data []byte) (*applyFile, error) {
272+
if len(data) > applyFileMaxBytes {
273+
return nil, fmt.Errorf("apply file %s is too large; limit is %d bytes", label, applyFileMaxBytes)
274+
}
256275
var config applyFile
257276
decoder := yaml.NewDecoder(bytes.NewReader(data))
258277
decoder.KnownFields(true)
259278
if err := decoder.Decode(&config); err != nil {
260-
return nil, fmt.Errorf("parse %s: %w", path, err)
279+
return nil, fmt.Errorf("parse %s: %w", label, err)
261280
}
262281
var extra interface{}
263282
if err := decoder.Decode(&extra); err != io.EOF {
264283
if err != nil {
265-
return nil, fmt.Errorf("parse %s: %w", path, err)
284+
return nil, fmt.Errorf("parse %s: %w", label, err)
266285
}
267-
return nil, fmt.Errorf("parse %s: multiple YAML documents are not supported", path)
286+
return nil, fmt.Errorf("parse %s: multiple YAML documents are not supported", label)
268287
}
269288
if config.Version == "" {
270289
config.Version = "v1"
@@ -853,6 +872,22 @@ func runDiff(path string) ([]diffResult, error) {
853872
if err != nil {
854873
return nil, err
855874
}
875+
return runDiffConfig(config)
876+
}
877+
878+
func runDiffContent(data []byte) ([]diffResult, error) {
879+
config, err := loadApplyData("dashboard yaml", data)
880+
if err != nil {
881+
return nil, err
882+
}
883+
return runDiffConfig(config)
884+
}
885+
886+
func runDiffConfig(config *applyFile) ([]diffResult, error) {
887+
return runDiffConfigWithSessionLookup(config, session.Get)
888+
}
889+
890+
func runDiffConfigWithSessionLookup(config *applyFile, lookup func(string) (*session.TunnelSession, error)) ([]diffResult, error) {
856891
if len(config.Tunnels) == 0 {
857892
return nil, fmt.Errorf("apply file has no tunnels")
858893
}
@@ -874,7 +909,7 @@ func runDiff(path string) ([]diffResult, error) {
874909
AccessPolicy: normalized.AccessPolicy != nil,
875910
BasicAuth: normalized.BasicAuth != nil && normalized.BasicAuth.Enabled,
876911
}
877-
existing, err := session.Get(normalized.TunnelID)
912+
existing, err := lookup(normalized.TunnelID)
878913
if err == nil {
879914
reuseExistingExpiration(&normalized, existing)
880915
result.ExpiresAt = normalized.ExpiresAt

cmd/daemon.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ func runDaemonTunnel(ctx context.Context, sess session.TunnelSession) {
247247
err = fmt.Errorf("invalid tunnel control host: %w", hostErr)
248248
} else {
249249
wsURL := fmt.Sprintf("wss://%s/_sealtun/ws", controlHost)
250-
err = tunnel.DialServerAndServeWithOnConnected(ctx, wsURL, current.Secret, current.LocalPort, func() {
250+
err = tunnel.DialServerAndServeProtocol(ctx, wsURL, current.Secret, current.LocalPort, current.Protocol, func() {
251251
latest, getErr := session.Get(sess.TunnelID)
252252
if getErr != nil {
253253
return

0 commit comments

Comments
 (0)