Files
flvx/go-gost/x/socket/agent_restart_linux_test.go
T

133 lines
5.1 KiB
Go

package socket
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"testing"
"time"
)
// This subprocess stands in for the running agent. The rc-service double kills
// its entire process group, not merely a fake PID, while the real helper runs.
func TestAgentDetachedParent(t *testing.T) {
dir := os.Getenv("FLVX_TEST_DETACH_ROOT")
if dir == "" {
t.Skip("subprocess helper")
}
binary := filepath.Join(dir, "agent")
control := filepath.Join(dir, "rc-service")
writeAgentFixture(t, control, "#!/bin/sh\nif [ \"$2\" = stop ]; then kill -TERM -"+strconv.Itoa(os.Getpid())+"; else echo started > "+shellQuote(filepath.Join(dir, "started"))+"; fi\n")
reporter := &WebSocketReporter{upgradeBinaryPath: binary, detectAgentManager: func() (agentServiceManager, error) { return agentServiceManager{kind: "openrc", control: control}, nil }}
response := routeAgentOperation(t, reporter, "RollbackAgent", nil)
if !response.Success {
t.Fatal(response.Message)
}
time.Sleep(15 * time.Second)
t.Fatal("original agent was not stopped")
}
func TestOpenRCHelperSurvivesAgentProcessGroupStop(t *testing.T) {
dir := t.TempDir()
binary := filepath.Join(dir, "agent")
writeAgentFixture(t, binary, "current")
writeAgentFixture(t, binary+".old", "backup")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestAgentDetachedParent$", "-test.v")
cmd.Env = append(os.Environ(), "FLVX_TEST_DETACH_ROOT="+dir)
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
output, err := cmd.CombinedOutput()
if err == nil || ctx.Err() != nil {
t.Fatalf("parent termination = %v, timeout=%v, %s", err, ctx.Err(), output)
}
exit, ok := err.(*exec.ExitError)
if !ok || exit.Sys().(syscall.WaitStatus).Signal() != syscall.SIGTERM {
t.Fatalf("parent did not receive SIGTERM: %v %s", err, output)
}
waitAgentFixture(t, filepath.Join(dir, "started"), "started\n")
if readAgentFixture(t, binary) != "backup" || readAgentFixture(t, binary+".old") != "backup" {
t.Fatal("rollback failed after parent termination")
}
fmt.Println("detached OpenRC helper survived original agent process-group SIGTERM")
}
// Run only in an explicitly opted-in disposable Alpine container. The docker
// invocation mounts only the test binary, never a host /bin directory. Removing
// the container's /bin/sh symlink forces the real detached cmd.Start to fail.
func TestOpenRCCommandsCleanStageOnHelperExecFailure(t *testing.T) {
if os.Getenv("FLVX_TEST_DISPOSABLE_ALPINE_SHELL_FAILURE") != "1" {
t.Skip("requires explicit disposable Alpine container opt-in")
}
for _, path := range []string{"/etc/alpine-release", "/.dockerenv"} {
if _, err := os.Stat(path); err != nil {
t.Fatalf("refusing shell mutation outside disposable Alpine: %s: %v", path, err)
}
}
info, err := os.Lstat("/bin/sh")
if err != nil || info.Mode()&os.ModeSymlink == 0 {
t.Fatalf("refusing to rename a non-symlink /bin/sh: %v", err)
}
saved := "/bin/.flvx-test-sh-" + strconv.Itoa(os.Getpid())
if _, err := os.Lstat(saved); !os.IsNotExist(err) {
t.Fatalf("refusing to overwrite shell backup: %v", err)
}
if err := os.Rename("/bin/sh", saved); err != nil {
t.Fatal(err)
}
defer func() {
if err := os.Rename(saved, "/bin/sh"); err != nil {
t.Errorf("restore container shell: %v", err)
}
}()
for _, operation := range []string{"UpgradeAgent", "RollbackAgent"} {
t.Run(operation, func(t *testing.T) {
dir := t.TempDir()
binary := filepath.Join(dir, "agent")
writeAgentFixture(t, binary, "current")
writeAgentFixture(t, binary+".old", "prior backup")
writeAgentFixture(t, filepath.Join(dir, "rc-service"), "#!/bin/sh\nexit 0\n")
t.Setenv("PATH", dir)
// No manager or launcher injection: Alpine detection and exec.LookPath are
// production code too. Only the installation path points to an isolated dir.
reporter := &WebSocketReporter{upgradeBinaryPath: binary}
fixture := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/checksum" {
fmt.Fprint(w, abcSHA256)
} else {
fmt.Fprint(w, "abc")
}
}))
defer fixture.Close()
response := routeAgentOperation(t, reporter, operation, map[string]string{"downloadUrl": fixture.URL + "/binary", "checksumUrl": fixture.URL + "/checksum"})
if response.Success || !strings.Contains(response.Message, "/bin/sh") || !strings.Contains(response.Message, "no such file") {
t.Fatalf("expected real helper exec failure, got %+v", response)
}
if readAgentFixture(t, binary) != "current" {
t.Fatal("live binary changed")
}
backup := "prior backup"
if operation == "UpgradeAgent" {
backup = "current"
}
if readAgentFixture(t, binary+".old") != backup {
t.Fatal("unexpected backup contents")
}
matches, err := filepath.Glob(filepath.Join(dir, ".flux-agent-*"))
if err != nil || len(matches) != 0 {
t.Fatalf("rollback staging residue: %v %v", matches, err)
}
if _, err := os.Stat(binary + ".new"); !os.IsNotExist(err) {
t.Fatalf("upgrade staging residue: %v", err)
}
})
}
}