mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-11-13 00:14:22 +08:00
330 lines
11 KiB
Go
330 lines
11 KiB
Go
package socket
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
func routeAgentOperation(t *testing.T, reporter *WebSocketReporter, operation string, data interface{}) CommandResponse {
|
|
t.Helper()
|
|
peer := make(chan *websocket.Conn, 1)
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
c, err := (&websocket.Upgrader{}).Upgrade(w, r, nil)
|
|
if err == nil {
|
|
peer <- c
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
c, _, err := websocket.DefaultDialer.Dial("ws"+strings.TrimPrefix(server.URL, "http"), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer c.Close()
|
|
reader := <-peer
|
|
defer reader.Close()
|
|
reporter.ctx = context.Background()
|
|
reporter.conn = c
|
|
reporter.connected = true
|
|
reporter.routeCommand(CommandMessage{Type: operation, RequestId: "restart-request", Data: data})
|
|
reader.SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
for {
|
|
var response CommandResponse
|
|
if err := reader.ReadJSON(&response); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if response.Type == operation+"Response" {
|
|
if response.RequestId != "restart-request" {
|
|
t.Fatalf("request id lost: %s", response.RequestId)
|
|
}
|
|
return response
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRollbackAgentStagesBackupBeforeRestart(t *testing.T) {
|
|
binary := filepath.Join(t.TempDir(), "flux_agent")
|
|
for path, body := range map[string]string{binary: "current", binary + ".old": "backup"} {
|
|
if err := os.WriteFile(path, []byte(body), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
called := false
|
|
reporter := &WebSocketReporter{upgradeBinaryPath: binary, startUpgradeRestart: func(staged, dest string) error {
|
|
called = true
|
|
body, err := os.ReadFile(staged)
|
|
if err != nil || string(body) != "backup" || staged == binary+".old" || dest != binary {
|
|
t.Fatalf("bad rollback staging: %s %s %q %v", staged, dest, body, err)
|
|
}
|
|
return os.Rename(staged, dest)
|
|
}}
|
|
response := routeAgentOperation(t, reporter, "RollbackAgent", nil)
|
|
if !response.Success || !called {
|
|
t.Fatalf("rollback failed: called=%v %s", called, response.Message)
|
|
}
|
|
body, err := os.ReadFile(binary + ".old")
|
|
if err != nil || string(body) != "backup" {
|
|
t.Fatalf("backup consumed: %q %v", body, err)
|
|
}
|
|
}
|
|
|
|
func TestAgentServiceManagerDetection(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
alpine, systemd bool
|
|
tools string
|
|
want string
|
|
}{
|
|
{"alpine shim", true, true, "rc-service systemctl systemd-run", "openrc"},
|
|
{"alpine missing openrc", true, true, "systemctl systemd-run", ""},
|
|
{"systemd", false, true, "rc-service systemctl systemd-run", "systemd"},
|
|
{"missing runner", false, true, "systemctl", ""},
|
|
{"openrc fallback", false, false, "rc-service", "openrc"},
|
|
{"unmanaged", false, false, "systemctl systemd-run", ""},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
manager, err := detectAgentServiceManager(tc.alpine, tc.systemd, func(name string) (string, error) {
|
|
if strings.Contains(" "+tc.tools+" ", " "+name+" ") {
|
|
return name, nil
|
|
}
|
|
return "", os.ErrNotExist
|
|
})
|
|
if manager.kind != tc.want || (err != nil) != (tc.want == "") {
|
|
t.Fatalf("manager=%+v err=%v", manager, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func writeAgentFixture(t *testing.T, path, body string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, []byte(body), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
func readAgentFixture(t *testing.T, path string) string {
|
|
t.Helper()
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(body)
|
|
}
|
|
func waitAgentFixture(t *testing.T, path, want string) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
body, _ := os.ReadFile(path)
|
|
if string(body) == want {
|
|
return
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
body, _ := os.ReadFile(path)
|
|
t.Fatalf("%s = %q, want %q", path, body, want)
|
|
}
|
|
|
|
func TestAgentCommandsManagerLaunch(t *testing.T) {
|
|
for _, operation := range []string{"UpgradeAgent", "RollbackAgent"} {
|
|
for _, mode := range []string{"systemd", "openrc", "missing", "rejected"} {
|
|
t.Run(operation+"/"+mode, func(t *testing.T) {
|
|
if mode == "openrc" && runtime.GOOS != "linux" {
|
|
t.Skip("detached launcher is Linux-only")
|
|
}
|
|
dir := t.TempDir()
|
|
binary := filepath.Join(dir, "flux 'agent")
|
|
log := filepath.Join(dir, "events")
|
|
writeAgentFixture(t, binary, "current")
|
|
writeAgentFixture(t, binary+".old", "backup")
|
|
control := filepath.Join(dir, "manager")
|
|
writeAgentFixture(t, control, "#!/bin/sh\nprintf '%s\\n' \"$*\" >> "+shellQuote(log)+"\n")
|
|
runner := filepath.Join(dir, "runner")
|
|
writeAgentFixture(t, runner, "#!/bin/sh\nshift\n\"$@\" >/dev/null 2>&1 &\n")
|
|
if mode == "rejected" {
|
|
writeAgentFixture(t, runner, "#!/bin/sh\necho rejected >&2\nexit 9\n")
|
|
}
|
|
reporter := &WebSocketReporter{upgradeBinaryPath: binary, detectAgentManager: func() (agentServiceManager, error) {
|
|
return detectAgentServiceManager(mode == "openrc", mode != "missing", func(name string) (string, error) {
|
|
if mode == "missing" {
|
|
return "", os.ErrNotExist
|
|
}
|
|
if name == "systemd-run" {
|
|
return runner, nil
|
|
}
|
|
return control, nil
|
|
})
|
|
}}
|
|
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 mode == "missing" || mode == "rejected" {
|
|
if response.Success {
|
|
t.Fatal("accepted failed manager launch")
|
|
}
|
|
if readAgentFixture(t, binary) != "current" {
|
|
t.Fatal("live binary mutated")
|
|
}
|
|
if mode == "missing" && readAgentFixture(t, binary+".old") != "backup" {
|
|
t.Fatal("backup mutated before manager validation")
|
|
}
|
|
matches, _ := filepath.Glob(filepath.Join(dir, ".flux-agent-*"))
|
|
if len(matches) != 0 {
|
|
t.Fatalf("staging residue %v", matches)
|
|
}
|
|
if _, err := os.Stat(binary + ".new"); !os.IsNotExist(err) {
|
|
t.Fatal("upgrade residue")
|
|
}
|
|
return
|
|
}
|
|
if !response.Success {
|
|
t.Fatal(response.Message)
|
|
}
|
|
want, backup := "abc", "current"
|
|
if operation == "RollbackAgent" {
|
|
want, backup = "backup", "backup"
|
|
}
|
|
waitAgentFixture(t, binary, want)
|
|
events := "stop flux_agent\nstart flux_agent\n"
|
|
if mode == "openrc" {
|
|
events = "flux_agent stop\nflux_agent start\n"
|
|
}
|
|
waitAgentFixture(t, log, events)
|
|
if readAgentFixture(t, binary+".old") != backup {
|
|
t.Fatal("wrong backup")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAgentRestartScriptFailureOrdering(t *testing.T) {
|
|
for _, mode := range []string{"stop", "replace"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
binary := filepath.Join(dir, "agent")
|
|
staged := filepath.Join(dir, "staged")
|
|
log := filepath.Join(dir, "events")
|
|
writeAgentFixture(t, binary, "current")
|
|
writeAgentFixture(t, staged, "replacement")
|
|
control := filepath.Join(dir, "rc-service")
|
|
script := "#!/bin/sh\necho \"$*\" >> " + shellQuote(log) + "\n"
|
|
if mode == "stop" {
|
|
script += "[ \"$2\" != stop ]\n"
|
|
}
|
|
writeAgentFixture(t, control, script)
|
|
if mode == "replace" {
|
|
writeAgentFixture(t, filepath.Join(dir, "mv"), "#!/bin/sh\nexit 23\n")
|
|
t.Setenv("PATH", dir+":"+os.Getenv("PATH"))
|
|
}
|
|
cmd := exec.Command("/bin/sh", "-c", buildManagedAgentRestartScript(agentServiceManager{kind: "openrc", control: control}, staged, binary, true))
|
|
if err := cmd.Run(); err == nil {
|
|
t.Fatal("expected failed stop/replacement")
|
|
}
|
|
if readAgentFixture(t, binary) != "current" {
|
|
t.Fatal("changed binary after failure")
|
|
}
|
|
want := "flux_agent stop\n"
|
|
if mode == "replace" {
|
|
want += "flux_agent start\n"
|
|
}
|
|
if readAgentFixture(t, log) != want {
|
|
t.Fatalf("wrong sequence %q", readAgentFixture(t, log))
|
|
}
|
|
if _, err := os.Stat(staged); !os.IsNotExist(err) {
|
|
t.Fatal("staging residue")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpgradeAgentBackupFailurePreservesExistingBackup(t *testing.T) {
|
|
dir := t.TempDir()
|
|
binary := filepath.Join(dir, "agent")
|
|
writeAgentFixture(t, binary+".old", "prior")
|
|
called := false
|
|
reporter := &WebSocketReporter{upgradeBinaryPath: binary, startUpgradeRestart: func(string, string) error { called = true; return nil }}
|
|
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, "UpgradeAgent", map[string]string{"downloadUrl": fixture.URL + "/binary", "checksumUrl": fixture.URL + "/checksum"})
|
|
if response.Success || called {
|
|
t.Fatal("upgrade accepted without backup")
|
|
}
|
|
if readAgentFixture(t, binary+".old") != "prior" {
|
|
t.Fatal("backup lost")
|
|
}
|
|
if _, err := os.Stat(binary + ".new"); !os.IsNotExist(err) {
|
|
t.Fatal("staging residue")
|
|
}
|
|
}
|
|
|
|
func TestRollbackAgentRejectsDirectoryDestination(t *testing.T) {
|
|
dir := t.TempDir()
|
|
binary := filepath.Join(dir, "agent")
|
|
if err := os.Mkdir(binary, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeAgentFixture(t, binary+".old", "backup")
|
|
called := false
|
|
reporter := &WebSocketReporter{upgradeBinaryPath: binary, startUpgradeRestart: func(string, string) error { called = true; return nil }}
|
|
response := routeAgentOperation(t, reporter, "RollbackAgent", nil)
|
|
if response.Success || called {
|
|
t.Fatal("accepted directory destination")
|
|
}
|
|
if readAgentFixture(t, binary+".old") != "backup" {
|
|
t.Fatal("changed backup")
|
|
}
|
|
entries, err := os.ReadDir(binary)
|
|
if err != nil || len(entries) != 0 {
|
|
t.Fatalf("destination changed: %v %v", entries, err)
|
|
}
|
|
}
|
|
|
|
func TestAgentRestartScriptRejectsDirectoryDestination(t *testing.T) {
|
|
dir := t.TempDir()
|
|
binary := filepath.Join(dir, "agent")
|
|
staged := filepath.Join(dir, "stage")
|
|
if err := os.Mkdir(binary, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeAgentFixture(t, staged, "replacement")
|
|
script := buildManagedAgentRestartScript(agentServiceManager{kind: "openrc", control: "/usr/bin/true"}, staged, binary, false)
|
|
if err := exec.Command("/bin/sh", "-c", script).Run(); err == nil {
|
|
t.Fatal("accepted directory destination")
|
|
}
|
|
entries, err := os.ReadDir(binary)
|
|
if err != nil || len(entries) != 0 {
|
|
t.Fatalf("binary moved inside directory: %v %v", entries, err)
|
|
}
|
|
}
|
|
|
|
func TestOpenRCRestartNeverCleansSystemdLegacyService(t *testing.T) {
|
|
script := buildManagedAgentRestartScript(agentServiceManager{kind: "openrc", control: "rc-service"}, "staged", "binary", true)
|
|
for _, forbidden := range []string{"systemctl", "gost", "daemon-reload", "/etc/systemd"} {
|
|
if strings.Contains(script, forbidden) {
|
|
t.Fatalf("OpenRC helper contains %s: %s", forbidden, script)
|
|
}
|
|
}
|
|
}
|