From 67d3ef7d5754b39161d749833437fd4d5b6ef95a Mon Sep 17 00:00:00 2001 From: Jie Liu Date: Mon, 10 Aug 2026 17:24:59 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(sandbox):=20=E6=94=AF=E6=8C=81=20creat?= =?UTF-8?q?e=20=E5=92=8C=20connect=20=E6=8C=87=E5=AE=9A=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E7=94=A8=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/sandbox.go | 16 +++++++++++++--- cmd_test/sandbox_test.go | 8 ++++++++ docs/sandbox_connect.md | 11 +++++++++-- docs/sandbox_create.md | 11 +++++++++-- iqshell/sandbox/sandbox/operations/connect.go | 8 +++++++- iqshell/sandbox/sandbox/operations/create.go | 11 +++++++++-- iqshell/sandbox/sandbox/operations/terminal.go | 12 ++++++++---- 7 files changed, 63 insertions(+), 14 deletions(-) diff --git a/cmd/sandbox.go b/cmd/sandbox.go index 586ed9e8..49453c4a 100644 --- a/cmd/sandbox.go +++ b/cmd/sandbox.go @@ -135,7 +135,11 @@ var sandboxCreateCmdBuilder = func(cfg *iqshell.Config) *cobra.Command { # Create with a Kodo bucket resource mounted into the sandbox qshell sandbox create my-template \ - --resource 'type=kodo,bucket=my-bucket,mount-path=/mnt/kodo,prefix=datasets/,read-only=true'`, + --resource 'type=kodo,bucket=my-bucket,mount-path=/mnt/kodo,prefix=datasets/,read-only=true' + + # Create and connect to the terminal as a specific user + qshell sandbox create my-template -u root + qshell sbx cr my-template -u root`, Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { cfg.CmdCfg.CmdId = docs.SandboxCreateType @@ -154,6 +158,7 @@ var sandboxCreateCmdBuilder = func(cfg *iqshell.Config) *cobra.Command { cmd.Flags().Int32VarP(&info.Timeout, "timeout", "t", 0, "sandbox timeout in seconds") cmd.Flags().IntVar(&retryMax, "retry-max", 0, "maximum automatic retries for sandbox creation (0 disables retries; default uses SANDBOX_RETRY_MAX or 5)") cmd.Flags().BoolVar(&info.Detach, "detach", false, "create sandbox without connecting terminal (sandbox stays alive until timeout)") + cmd.Flags().StringVarP(&info.User, "user", "u", "", "user to run the terminal as (default: sandbox default user; ignored with --detach)") cmd.Flags().StringVarP(&info.Metadata, "metadata", "m", "", "metadata key=value pairs (comma-separated)") cmd.Flags().StringArrayVarP(&info.EnvVars, "env-var", "e", nil, "environment variables (KEY=VALUE, can be specified multiple times)") cmd.Flags().BoolVar(&info.AutoPause, "auto-pause", false, "automatically pause sandbox when timeout expires (instead of killing)") @@ -164,6 +169,7 @@ var sandboxCreateCmdBuilder = func(cfg *iqshell.Config) *cobra.Command { } var sandboxConnectCmdBuilder = func(cfg *iqshell.Config) *cobra.Command { + info := operations.ConnectInfo{} retryMax := 0 cmd := &cobra.Command{ Use: "connect ", @@ -171,7 +177,10 @@ var sandboxConnectCmdBuilder = func(cfg *iqshell.Config) *cobra.Command { Short: "Connect to an existing sandbox terminal (alias: cn)", Example: ` # Connect to a sandbox by ID qshell sandbox connect sb-xxxxxxxxxxxx - qshell sbx cn sb-xxxxxxxxxxxx`, + qshell sbx cn sb-xxxxxxxxxxxx + + # Connect as a specific user + qshell sandbox connect sb-xxxxxxxxxxxx -u root`, Run: func(cmd *cobra.Command, args []string) { cfg.CmdCfg.CmdId = docs.SandboxConnectType if !iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{}) { @@ -181,7 +190,7 @@ var sandboxConnectCmdBuilder = func(cfg *iqshell.Config) *cobra.Command { _ = cmd.Usage() return } - info := operations.ConnectInfo{SandboxID: args[0]} + info.SandboxID = args[0] if cmd.Flags().Changed("retry-max") { info.RetryMax = &retryMax } @@ -189,6 +198,7 @@ var sandboxConnectCmdBuilder = func(cfg *iqshell.Config) *cobra.Command { }, } cmd.Flags().IntVar(&retryMax, "retry-max", 0, "maximum automatic retries for sandbox connection (0 disables retries; default uses SANDBOX_RETRY_MAX or 5)") + cmd.Flags().StringVarP(&info.User, "user", "u", "", "user to run the terminal as (default: sandbox default user)") return cmd } diff --git a/cmd_test/sandbox_test.go b/cmd_test/sandbox_test.go index c17f6ecf..a7594a12 100644 --- a/cmd_test/sandbox_test.go +++ b/cmd_test/sandbox_test.go @@ -125,6 +125,10 @@ func TestSandboxCreateDocumentWithEnvVar(t *testing.T) { testSubcommandDocumentWithFlags(t, []string{"sandbox", "create"}, "-e", "FOO=bar", "-e", "BAZ=qux") } +func TestSandboxCreateDocumentWithUser(t *testing.T) { + testSubcommandDocumentWithFlags(t, []string{"sandbox", "create"}, "my-template", "-u", "root") +} + func TestSandboxKillDocumentWithAll(t *testing.T) { testSubcommandDocumentWithFlags(t, []string{"sandbox", "kill"}, "--all") } @@ -145,6 +149,10 @@ func TestSandboxResumeDocumentWithFlags(t *testing.T) { testSubcommandDocumentWithFlags(t, []string{"sandbox", "resume"}, "--all", "-m", "env=staging") } +func TestSandboxConnectDocumentWithFlags(t *testing.T) { + testSubcommandDocumentWithFlags(t, []string{"sandbox", "connect"}, "sb-test", "-u", "root") +} + func TestSandboxExecDocumentWithFlags(t *testing.T) { testSubcommandDocumentWithFlags(t, []string{"sandbox", "exec"}, "sb-test", "-b", "-c", "/app", "-u", "root") } diff --git a/docs/sandbox_connect.md b/docs/sandbox_connect.md index bb2be942..e2e61502 100644 --- a/docs/sandbox_connect.md +++ b/docs/sandbox_connect.md @@ -3,8 +3,8 @@ # 格式 ``` -qshell sandbox connect [--retry-max ] -qshell sbx cn [--retry-max ] +qshell sandbox connect [--user ] [--retry-max ] +qshell sbx cn [--user ] [--retry-max ] ``` # 帮助文档 @@ -18,6 +18,7 @@ $ qshell sandbox connect --doc # 参数 - `sandboxID`:沙箱 ID(必填) +- `--user`/`-u`:终端运行的用户;未指定时使用沙箱默认用户(`user`) - `--retry-max`:连接请求的最大自动重试次数;`0` 禁用重试。未传入时优先读取 `SANDBOX_RETRY_MAX`,未设置则默认重试 5 次 # 示例 @@ -26,6 +27,12 @@ $ qshell sandbox connect sb-xxxxxxxxxxxx $ qshell sbx cn sb-xxxxxxxxxxxx ``` +以 `root` 用户身份连接终端: +``` +$ qshell sandbox connect sb-xxxxxxxxxxxx -u root +$ qshell sbx cn sb-xxxxxxxxxxxx --user root +``` + 禁用连接请求自动重试: ``` $ qshell sandbox connect sb-xxxxxxxxxxxx --retry-max 0 diff --git a/docs/sandbox_create.md b/docs/sandbox_create.md index 0c43d939..328d1225 100644 --- a/docs/sandbox_create.md +++ b/docs/sandbox_create.md @@ -7,8 +7,8 @@ # 格式 ``` -qshell sandbox create [template] [-t ] [--retry-max ] [--detach] [-m ] [-e ...] [--auto-pause] [--injection-rule ...] [--inline-injection ...] [--resource ...] -qshell sbx cr [template] [-t ] [--retry-max ] [--detach] [-m ] [-e ...] [--auto-pause] [--injection-rule ...] [--inline-injection ...] [--resource ...] +qshell sandbox create [template] [-t ] [--retry-max ] [--detach] [-u ] [-m ] [-e ...] [--auto-pause] [--injection-rule ...] [--inline-injection ...] [--resource ...] +qshell sbx cr [template] [-t ] [--retry-max ] [--detach] [-u ] [-m ] [-e ...] [--auto-pause] [--injection-rule ...] [--inline-injection ...] [--resource ...] ``` # 帮助文档 @@ -27,6 +27,7 @@ $ qshell sandbox create --doc - `-t, --timeout`:沙箱超时时间(秒) - `--retry-max`:创建请求的最大自动重试次数;`0` 禁用重试。未传入时优先读取 `SANDBOX_RETRY_MAX`,未设置则默认重试 5 次 - `--detach`:创建沙箱但不连接终端,沙箱保持存活直到超时。此参数没有短参数 +- `-u, --user`:终端运行的用户;未指定时使用沙箱默认用户(`user`)。与 `--detach` 同时使用时不生效(不连接终端) - `-m, --metadata`:元数据键值对(格式:key1=value1,key2=value2) - `-e, --env-var`:环境变量(KEY=VALUE 格式,可多次指定) - `--auto-pause`:超时后自动暂停沙箱,而不是终止沙箱 @@ -127,3 +128,9 @@ $ qshell sandbox create my-template \ $ qshell sbx cr my-template \ --resource 'type=kodo,bucket=my-bucket,mount=/mnt/kodo' ``` + +13. 以 `root` 用户身份连接终端 +``` +$ qshell sandbox create my-template -u root +$ qshell sbx cr my-template --user root +``` diff --git a/iqshell/sandbox/sandbox/operations/connect.go b/iqshell/sandbox/sandbox/operations/connect.go index 9151a6ef..b33b9593 100644 --- a/iqshell/sandbox/sandbox/operations/connect.go +++ b/iqshell/sandbox/sandbox/operations/connect.go @@ -11,6 +11,7 @@ import ( // ConnectInfo holds parameters for connecting to a sandbox. type ConnectInfo struct { SandboxID string + User string RetryMax *int } @@ -36,5 +37,10 @@ func Connect(info ConnectInfo) { } sbClient.PrintSuccess("Connected to sandbox %s", sb.ID()) - runTerminalSession(ctx, sb) + var opts []sandbox.CommandOption + if info.User != "" { + opts = append(opts, sandbox.WithCommandUser(info.User)) + } + + runTerminalSession(ctx, sb, opts...) } diff --git a/iqshell/sandbox/sandbox/operations/create.go b/iqshell/sandbox/sandbox/operations/create.go index 18f102de..24c4f87e 100644 --- a/iqshell/sandbox/sandbox/operations/create.go +++ b/iqshell/sandbox/sandbox/operations/create.go @@ -14,7 +14,9 @@ import ( // CreateInfo holds parameters for creating a sandbox. type CreateInfo struct { - TemplateID string + TemplateID string + // User 终端运行的用户,仅在非 detach 模式下生效 + User string RetryMax *int Timeout int32 Metadata string @@ -109,7 +111,12 @@ func Create(info CreateInfo) { } }() - runTerminalSession(ctx, sb) + var opts []sandbox.CommandOption + if info.User != "" { + opts = append(opts, sandbox.WithCommandUser(info.User)) + } + + runTerminalSession(ctx, sb, opts...) } func buildSandboxInjections(ruleIDs, inlineSpecs []string) ([]sandbox.SandboxInjectionSpec, error) { diff --git a/iqshell/sandbox/sandbox/operations/terminal.go b/iqshell/sandbox/sandbox/operations/terminal.go index b8133cec..45a93156 100644 --- a/iqshell/sandbox/sandbox/operations/terminal.go +++ b/iqshell/sandbox/sandbox/operations/terminal.go @@ -92,7 +92,8 @@ func detectResize(previous terminalSize, width, height int, err error) (terminal } // runTerminalSession creates a PTY session and handles stdin/stdout bridging. -func runTerminalSession(ctx context.Context, sb *sandbox.Sandbox) { +// opts 会追加到 PTY 创建选项中,用于指定运行用户等。 +func runTerminalSession(ctx context.Context, sb *sandbox.Sandbox, opts ...sandbox.CommandOption) { // Get terminal size width, height, err := term.GetSize(int(os.Stdin.Fd())) if err != nil { @@ -111,12 +112,15 @@ func runTerminalSession(ctx context.Context, sb *sandbox.Sandbox) { defer ptyCancel() // Create PTY session + ptyOpts := append([]sandbox.CommandOption{ + sandbox.WithOnPtyData(func(data []byte) { + os.Stdout.Write(data) + }), + }, opts...) handle, err := sb.Pty().Create(ptyCtx, sandbox.PtySize{ Cols: uint32(width), Rows: uint32(height), - }, sandbox.WithOnPtyData(func(data []byte) { - os.Stdout.Write(data) - })) + }, ptyOpts...) if err != nil { sbClient.PrintError("create PTY failed: %v", err) return From a81b1b4b01971d5b8bff63407bd7796a277193a4 Mon Sep 17 00:00:00 2001 From: Jie Liu Date: Tue, 11 Aug 2026 09:46:28 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs(release):=20=E8=AE=B0=E5=BD=95=202.19.?= =?UTF-8?q?12=20=E5=8F=98=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fb1eff1..fc23a0bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 2.19.12 (2026-08-11) +## 新增 +1. `qshell sandbox create` 与 `qshell sandbox connect` 新增 `--user` / `-u` 参数,用于指定终端的运行用户;未指定时使用沙箱默认用户,`create` 搭配 `--detach` 时该参数不生效(不连接终端) + # 2.19.11 (2026-07-30) ## 新增 1. `qshell sandbox create` 通过 SDK 请求幂等键支持安全重试;未显式指定幂等键时 SDK 会自动生成