utils/teardown_unix_test.go
Rama main · réplica de lectura del repositorio original.
//go:build unix
package utils
import (
"context"
"os"
"path/filepath"
"syscall"
"testing"
"time"
)
// TestEscalationHelper is not a test of its own: with KEPLOY_TEST_ESCALATION
// set it is an agent that calls DieOnEscalation, says it is ready, and waits.
func TestEscalationHelper(t *testing.T) {
dir := os.Getenv("KEPLOY_TEST_ESCALATION")
if dir == "" {
return
}
helperExitsWithParent()
stopping, stop := context.WithCancel(context.Background())
stop() // its stop has begun
DieOnEscalation(stopping)
if err := os.WriteFile(filepath.Join(dir, "ready"), nil, 0o600); err != nil {
os.Exit(3)
}
time.Sleep(time.Hour)
os.Exit(0)
}
// EscalationSignal kills an agent where it stands (SIGKILL): it is how the CLI
// forces the stop of an agent sudo runs as root, which it may not signal.
func TestDieOnEscalation(t *testing.T) {
dir := t.TempDir()
helper, exited, out := startHelper(t, "TestEscalationHelper", "KEPLOY_TEST_ESCALATION="+dir)
hung := time.After(time.Minute) // only turns a hang into a failure
for {
if _, err := os.Stat(filepath.Join(dir, "ready")); err == nil {
break
}
select {
case err := <-exited:
t.Fatalf("the helper ended before it was ready: %v\n%s", err, out.String())
case <-hung:
t.Fatalf("the helper was never ready\n%s", out.String())
case <-time.After(5 * time.Millisecond):
}
}
if err := helper.Process.Signal(EscalationSignal); err != nil {
t.Fatal(err)
}
select {
case <-exited:
case <-time.After(time.Minute):
t.Fatalf("the helper outlived the escalation signal\n%s", out.String())
}
if st := helper.ProcessState.Sys().(syscall.WaitStatus); !st.Signaled() || st.Signal() != syscall.SIGKILL {
t.Fatalf("the helper ended with %v, want it killed (SIGKILL)\n%s", helper.ProcessState, out.String())
}
}
// A path the command line gives is held to what a keploy CLI gives before the
// agent, root under sudo, writes there: the right name, in a directory that is
// not a symlink and that only its owner may write to. And every write goes
// through that directory as it was opened: a path swapped for a symlink
// afterwards does not move the writes.
func TestACommandLineProgressPathIsChecked(t *testing.T) {
ok := filepath.Join(t.TempDir(), "keploy-agent-stop-ok")
if err := os.Mkdir(ok, 0o700); err != nil {
t.Fatal(err)
}
open := filepath.Join(t.TempDir(), "open")
if err := os.Mkdir(open, 0o700); err != nil {
t.Fatal(err)
}
if err := os.Chmod(open, 0o777); err != nil {
t.Fatal(err)
}
link := filepath.Join(t.TempDir(), "link")
if err := os.Symlink(ok, link); err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name, path string
ok bool
}{
{"a relative path", filepath.Join("rel", TeardownProgressFileName), false},
{"another name", filepath.Join(ok, "shadow"), false},
{"a directory others may write to", filepath.Join(open, TeardownProgressFileName), false},
{"a directory that is a symlink", filepath.Join(link, TeardownProgressFileName), false},
{"the CLI's", filepath.Join(ok, TeardownProgressFileName), true},
} {
t.Run(tc.name, func(t *testing.T) {
sink, err := openCommandLineSink(tc.path)
if (err == nil) != tc.ok {
t.Fatalf("openCommandLineSink(%q) = %v, want ok=%v", tc.path, err, tc.ok)
}
if sink != nil {
sink.(*dirSink).close()
}
})
}
sink, err := openCommandLineSink(filepath.Join(ok, TeardownProgressFileName))
if err != nil {
t.Fatal(err)
}
defer sink.(*dirSink).close()
// The path is moved and a symlink put in its place: the writes stay
// in the directory that was opened.
moved, elsewhere := ok+".moved", t.TempDir()
if err := os.Rename(ok, moved); err != nil {
t.Fatal(err)
}
if err := os.Symlink(elsewhere, ok); err != nil {
t.Fatal(err)
}
if err := sink.write([]byte(`{"seq":1}`)); err != nil {
t.Fatal(err)
}
if b, err := os.ReadFile(filepath.Join(moved, TeardownProgressFileName)); err != nil || string(b) != `{"seq":1}` {
t.Fatalf("the report is not in the directory that was opened: %q %v", b, err)
}
if entries, _ := os.ReadDir(elsewhere); len(entries) != 0 {
t.Fatalf("a write followed the symlink put in the directory's place: %v", entries)
}
if err := sink.clear(); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(moved, TeardownProgressFileName)); !os.IsNotExist(err) {
t.Fatalf("clear left the report: %v", err)
}
}
// TestEscalationWhileBusyHelper is not a test of its own: with
// KEPLOY_TEST_ESCALATION_BUSY set it is an agent in the middle of teardown
// work, which ends when the test writes "end" in that directory.
func TestEscalationWhileBusyHelper(t *testing.T) {
dir := os.Getenv("KEPLOY_TEST_ESCALATION_BUSY")
if dir == "" {
return
}
helperExitsWithParent()
stopping, stop := context.WithCancel(context.Background())
stop() // its stop has begun
DieOnEscalation(stopping)
end := BeginTeardownWork()
if err := os.WriteFile(filepath.Join(dir, "ready"), nil, 0o600); err != nil {
os.Exit(3)
}
for {
if _, err := os.Stat(filepath.Join(dir, "end")); err == nil {
break
}
time.Sleep(5 * time.Millisecond)
}
end()
time.Sleep(time.Hour)
os.Exit(0)
}
// The escalation signal does not kill an agent in the middle of teardown work
// -- a uprobe removal, a CA tool rewriting a trust store -- but once that work
// has ended: the CLI judged from a report a moment old.
func TestTheEscalationWaitsForTeardownWork(t *testing.T) {
dir := t.TempDir()
helper, exited, out := startHelper(t, "TestEscalationWhileBusyHelper", "KEPLOY_TEST_ESCALATION_BUSY="+dir)
hung := time.After(time.Minute) // only turns a hang into a failure
for {
if _, err := os.Stat(filepath.Join(dir, "ready")); err == nil {
break
}
select {
case err := <-exited:
t.Fatalf("the helper ended before it was ready: %v\n%s", err, out.String())
case <-hung:
t.Fatalf("the helper was never ready\n%s", out.String())
case <-time.After(5 * time.Millisecond):
}
}
if err := helper.Process.Signal(EscalationSignal); err != nil {
t.Fatal(err)
}
select { // only that it has NOT died yet
case err := <-exited:
t.Fatalf("the escalation killed the agent in the middle of teardown work (%v)\n%s", err, out.String())
case <-time.After(200 * time.Millisecond):
}
if err := os.WriteFile(filepath.Join(dir, "end"), nil, 0o600); err != nil {
t.Fatal(err)
}
select {
case <-exited:
case <-time.After(time.Minute):
t.Fatalf("the helper outlived the escalation once its work ended\n%s", out.String())
}
if st := helper.ProcessState.Sys().(syscall.WaitStatus); !st.Signaled() || st.Signal() != syscall.SIGKILL {
t.Fatalf("the helper ended with %v, want it killed (SIGKILL)\n%s", helper.ProcessState, out.String())
}
}
// TestEscalationBeforeTheStopHelper is not a test of its own: with
// KEPLOY_TEST_ESCALATION_EARLY set it is an agent whose stop has not begun;
// it marks "stopping" once its root context is cancelled, and waits.
func TestEscalationBeforeTheStopHelper(t *testing.T) {
dir := os.Getenv("KEPLOY_TEST_ESCALATION_EARLY")
if dir == "" {
return
}
helperExitsWithParent()
root, cancel := context.WithCancel(context.Background())
SetCancel(cancel)
DieOnEscalation(root)
if err := os.WriteFile(filepath.Join(dir, "ready"), nil, 0o600); err != nil {
os.Exit(3)
}
<-root.Done()
if err := os.WriteFile(filepath.Join(dir, "stopping"), nil, 0o600); err != nil {
os.Exit(3)
}
time.Sleep(time.Hour)
os.Exit(0)
}
// Before an agent's stop has begun, the escalation signal begins it, as
// SIGTERM would: a stray one -- an operator who meant SIGUSR1 -- does not kill
// an agent with its CA installed. Once the stop has begun, it kills.
func TestTheEscalationBeforeTheStopBeginsIt(t *testing.T) {
dir := t.TempDir()
helper, exited, out := startHelper(t, "TestEscalationBeforeTheStopHelper", "KEPLOY_TEST_ESCALATION_EARLY="+dir)
await := func(name string) {
t.Helper()
hung := time.After(time.Minute) // only turns a hang into a failure
for {
if _, err := os.Stat(filepath.Join(dir, name)); err == nil {
return
}
select {
case err := <-exited:
t.Fatalf("the helper ended before %q: %v\n%s", name, err, out.String())
case <-hung:
t.Fatalf("the helper never got to %q\n%s", name, out.String())
case <-time.After(5 * time.Millisecond):
}
}
}
await("ready")
if err := helper.Process.Signal(EscalationSignal); err != nil {
t.Fatal(err)
}
await("stopping") // the signal began the stop, and killed nothing
if err := helper.Process.Signal(EscalationSignal); err != nil {
t.Fatal(err)
}
select {
case <-exited:
case <-time.After(time.Minute):
t.Fatalf("the helper outlived the escalation once its stop had begun\n%s", out.String())
}
if st := helper.ProcessState.Sys().(syscall.WaitStatus); !st.Signaled() || st.Signal() != syscall.SIGKILL {
t.Fatalf("the helper ended with %v, want it killed (SIGKILL)\n%s", helper.ProcessState, out.String())
}
}
// StopTeardownReporting ends a reporter the command-line entry point
// (ReportTeardownProgressTo) started too, and that reporter lets go of the
// directory it held open: no fd of this process names it any more.
func TestStopTeardownReportingReleasesACommandLineReporter(t *testing.T) {
if _, err := os.Stat("/proc/self/fd"); err != nil {
t.Skipf("no /proc/self/fd to count this process's fds by (%v)", err)
}
dir := filepath.Join(t.TempDir(), "keploy-agent-stop-fd")
if err := os.Mkdir(dir, 0o700); err != nil {
t.Fatal(err)
}
resolved, err := filepath.EvalSymlinks(dir) // as /proc/self/fd names it
if err != nil {
t.Fatal(err)
}
held := func() int {
ents, err := os.ReadDir("/proc/self/fd")
if err != nil {
t.Fatal(err)
}
n := 0
for _, e := range ents {
if target, err := os.Readlink("/proc/self/fd/" + e.Name()); err == nil && target == resolved {
n++
}
}
return n
}
before := held()
ctx, begin := context.WithCancel(context.Background())
path := filepath.Join(dir, TeardownProgressFileName)
if err := ReportTeardownProgressTo(ctx, nil, path); err != nil {
t.Fatal(err)
}
t.Cleanup(StopTeardownReporting) // if the test fails before it does
begin()
awaitReport(t, path, func(TeardownProgress) bool { return true })
if n := held(); n != before+1 {
t.Fatalf("the reporter holds the directory open %d times, want once: this test cannot see it let go", n-before)
}
StopTeardownReporting()
if n := reporting.live.Load(); n != 0 {
t.Fatalf("%d reporters still running after StopTeardownReporting returned", n)
}
if n := held(); n != before {
t.Fatalf("the stopped reporter still holds the directory open (%d fds)", n-before)
}
}