From b0bdc2c79ef136bc060c02cab0ff36f6f3ad5291 Mon Sep 17 00:00:00 2001 From: tonic Date: Sun, 12 Jul 2026 19:35:00 +0800 Subject: [PATCH 1/2] fix(exec): pass -i to cocoon vm exec only when the caller attaches stdin cocoon v0.5.0 made vm exec stdin opt-in (docker semantics): without -i the child's stdin closes immediately. vk wired kubectl's stdin stream to the cocoon process unconditionally, so on v0.5.0+ hosts 'kubectl exec -i' delivers EOF instead of the caller's input. Map attach.Stdin() presence to -i: RunInContainer gets a nil reader when kubectl exec runs without -i, so stdin-less execs (including post-clone) keep their exact argv and no longer wire an unused pipe. Requires cocoon >= v0.5.0 for interactive execs (older cocoon rejects -i as an unknown flag); non-interactive execs are unaffected either way. --- vm/cocoon_cli.go | 19 +++++++++++++------ vm/cocoon_cli_test.go | 21 +++++++++++++++------ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/vm/cocoon_cli.go b/vm/cocoon_cli.go index 0786176..1b086a0 100644 --- a/vm/cocoon_cli.go +++ b/vm/cocoon_cli.go @@ -180,8 +180,10 @@ func (c *CocoonCLI) Exec(ctx context.Context, vmID string, argv []string, env ma if len(argv) == 0 { return errors.New("cocoon vm exec: argv is empty") } - cmd := c.command(ctx, buildExecArgs(vmID, argv, env)...) - cmd.Stdin = stdin + cmd := c.command(ctx, buildExecArgs(vmID, argv, env, stdin != nil)...) + if stdin != nil { + cmd.Stdin = stdin + } cmd.Stdout = stdout cmd.Stderr = stderr if err := cmd.Run(); err != nil { @@ -464,12 +466,17 @@ func buildRunArgs(opts RunOptions) []string { return args } -// buildExecArgs assembles `cocoon vm exec [-e KEY=VAL...] -- ...`. +// buildExecArgs assembles `cocoon vm exec [-i] [-e KEY=VAL...] -- ...`. // Env keys are sorted so the resulting argv is deterministic (test-friendly, -// log-friendly); cocoon doesn't care about order. -func buildExecArgs(vmID string, argv []string, env map[string]string) []string { - args := make([]string, 0, 4+2*len(env)+len(argv)) //nolint:mnd +// log-friendly); cocoon doesn't care about order. Since cocoon v0.5.0 exec +// stdin is opt-in (docker semantics): -i goes on only when the caller has a +// stdin to attach, so stdin-less execs don't hold the vsock stream open. +func buildExecArgs(vmID string, argv []string, env map[string]string, interactive bool) []string { + args := make([]string, 0, 5+2*len(env)+len(argv)) //nolint:mnd args = append(args, "vm", "exec") + if interactive { + args = append(args, "-i") + } for _, k := range slices.Sorted(maps.Keys(env)) { args = append(args, "-e", k+"="+env[k]) } diff --git a/vm/cocoon_cli_test.go b/vm/cocoon_cli_test.go index f48e56e..392e450 100644 --- a/vm/cocoon_cli_test.go +++ b/vm/cocoon_cli_test.go @@ -214,11 +214,12 @@ func TestBuildExecArgsAssemblesEnvAndArgv(t *testing.T) { t.Parallel() cases := []struct { - name string - vmID string - argv []string - env map[string]string - want []string + name string + vmID string + argv []string + env map[string]string + interactive bool + want []string }{ { name: "no env passes through plain argv", @@ -239,10 +240,18 @@ func TestBuildExecArgsAssemblesEnvAndArgv(t *testing.T) { argv: []string{"--", "ls"}, want: []string{"vm", "exec", "vm-3", "--", "--", "ls"}, }, + { + name: "interactive adds -i before env", + vmID: "vm-4", + argv: []string{"cat"}, + env: map[string]string{"FOO": "1"}, + interactive: true, + want: []string{"vm", "exec", "-i", "-e", "FOO=1", "vm-4", "--", "cat"}, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - got := buildExecArgs(tc.vmID, tc.argv, tc.env) + got := buildExecArgs(tc.vmID, tc.argv, tc.env, tc.interactive) if !reflect.DeepEqual(got, tc.want) { t.Fatalf("buildExecArgs() = %#v, want %#v", got, tc.want) } From 37653c917c5535e93702d31032bd109eb928e287 Mon Sep 17 00:00:00 2001 From: CMGS Date: Sun, 12 Jul 2026 22:15:37 +0800 Subject: [PATCH 2/2] review: tighten buildExecArgs godoc, drop imprecise version claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The -i opt-in landed in cocoon v0.4.8, not v0.5.0, and the note that stdin-less execs skip -i does not hold on the websocket exec path (its ignore-channel hands back a non-nil stdin reader), so the argv still carries -i there — harmless on cocoon >= v0.4.8. Describe what the code does instead of overstating the version/stream guarantees. --- vm/cocoon_cli.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/vm/cocoon_cli.go b/vm/cocoon_cli.go index 1b086a0..82ccf43 100644 --- a/vm/cocoon_cli.go +++ b/vm/cocoon_cli.go @@ -466,11 +466,8 @@ func buildRunArgs(opts RunOptions) []string { return args } -// buildExecArgs assembles `cocoon vm exec [-i] [-e KEY=VAL...] -- ...`. -// Env keys are sorted so the resulting argv is deterministic (test-friendly, -// log-friendly); cocoon doesn't care about order. Since cocoon v0.5.0 exec -// stdin is opt-in (docker semantics): -i goes on only when the caller has a -// stdin to attach, so stdin-less execs don't hold the vsock stream open. +// buildExecArgs assembles `cocoon vm exec [-i] [-e KEY=VAL...] -- ...`, +// sorting env keys for a deterministic argv. -i attaches stdin (opt-in, docker semantics). func buildExecArgs(vmID string, argv []string, env map[string]string, interactive bool) []string { args := make([]string, 0, 5+2*len(env)+len(argv)) //nolint:mnd args = append(args, "vm", "exec")