diff --git a/vm/cocoon_cli.go b/vm/cocoon_cli.go index 0786176..82ccf43 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,14 @@ func buildRunArgs(opts RunOptions) []string { return args } -// buildExecArgs assembles `cocoon vm exec [-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 +// 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") + 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) }