mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-11-13 00:14:22 +08:00
133 lines
5.1 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|