Ctrl K
ComunidadGuías…

utils/execute_command_shellfree_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//go:build linux || darwin

package utils

import (
	"bytes"
	"context"
	"os"
	"os/exec"
	"path/filepath"
	"testing"
	"time"

	"go.uber.org/zap"
)

// TestExecuteCommand_RunsPipedStdinShapeWithoutShell is the regression guard for
// the shell dependency on the command-execution path.
//
// Every command keploy launches comes through here, including the `docker
// compose -f -` that takes a compose document keploy generated ITSELF on
// stdin. On a distroless image with no /bin/sh the old `sh -c <cmd>` wrapper
// died with `exec: "sh": executable file not found`.
//
// This test drives the *real* ExecuteCommand in an environment where `sh` is not
// resolvable (PATH points at a directory that contains only the target binary),
// with a multi-token command plus piped stdin. It asserts the command runs
// directly (no shell) and actually receives the stdin. If anyone reintroduces a
// hard `sh -c` on this path, `sh` won't be found and this test fails.
//
// `tee <file>` stands in for the app command: it is a real, shell-free binary
// that echoes its stdin to a file, letting us prove the piped compose document
// reached the launched process.
func TestExecuteCommand_RunsPipedStdinShapeWithoutShell(t *testing.T) {
	teePath, err := exec.LookPath("tee")
	if err != nil {
		t.Skipf("tee not available: %v", err)
	}

	// A bin dir holding ONLY the target binary — deliberately no sh/bash/dash,
	// so keploy's exec.LookPath("sh") fails exactly as it would on a distroless
	// runtime image.
	binDir := t.TempDir()
	if err := os.Symlink(teePath, filepath.Join(binDir, "tee")); err != nil {
		t.Fatalf("link tee into shell-less bin dir: %v", err)
	}
	t.Setenv("PATH", binDir)

	// Sanity: the environment really has no shell.
	if p, err := exec.LookPath("sh"); err == nil {
		t.Fatalf("test setup is wrong: sh is still resolvable at %s", p)
	}

	capturedPath := filepath.Join(t.TempDir(), "captured-compose.yaml")
	composeDoc := []byte("services:\n  app:\n    image: demo:latest\n")

	// Multi-token argv + a document on stdin, the shape a user command that
	// reads from stdin takes.
	cmdStr := "tee " + capturedPath
	noopCancel := func(_ *exec.Cmd) func() error { return func() error { return nil } }

	cmdErr := ExecuteCommand(context.Background(), zap.NewNop(), cmdStr, Empty, noopCancel, time.Second, composeDoc, nil)
	if cmdErr.Err != nil {
		t.Fatalf("ExecuteCommand failed in a shell-less env (a hard `sh -c` regression?): type=%s err=%v",
			cmdErr.Type, cmdErr.Err)
	}

	got, err := os.ReadFile(capturedPath)
	if err != nil {
		t.Fatalf("command did not run (nothing captured): %v", err)
	}
	if !bytes.Equal(got, composeDoc) {
		t.Fatalf("stdin was not piped to the launched command: got %q want %q", got, composeDoc)
	}
}

// TestExecuteCommand_GivesExtraEnvToThatCommandAlone: extraEnv is how the
// docker compose command that starts the agent gets the control-plane token
// its compose file names without a value. It must reach that command — and only
// that command: exporting it to keploy's own environment would hand it to every
// process keploy starts afterwards.
func TestExecuteCommand_GivesExtraEnvToThatCommandAlone(t *testing.T) {
	const name = "KEPLOY_TEST_EXTRA_ENV"
	t.Setenv(name, "") // restored afterwards
	if err := os.Unsetenv(name); err != nil {
		t.Fatal(err)
	}
	out := filepath.Join(t.TempDir(), "out")
	noopCancel := func(_ *exec.Cmd) func() error { return func() error { return nil } }

	cmdErr := ExecuteCommand(context.Background(), zap.NewNop(), `printf %s "$`+name+`" > `+out, Empty, noopCancel, time.Second, nil,
		[]string{name + "=only-for-this-command"})
	if cmdErr.Err != nil {
		t.Fatalf("ExecuteCommand: %v", cmdErr.Err)
	}
	got, err := os.ReadFile(out)
	if err != nil {
		t.Fatal(err)
	}
	if string(got) != "only-for-this-command" {
		t.Fatalf("the command saw %q, not the variable it was given", got)
	}
	if _, exported := os.LookupEnv(name); exported {
		t.Fatalf("%s was exported to keploy's own environment", name)
	}
}