The witness moves a running build aside into a directory the controller's user cannot enter, then deletes it; a verb exec'd from os.Executable() in that window failed with permission denied. /proc/self/exe stays valid while the process lives. A verb that still cannot start, or arrives while the controller stops, is refused with link.ErrHandingOver and marked retry: handing-over.
178 lines
5.5 KiB
Go
178 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// The roles a copy of this test binary plays in TestAVerbRunsWhileItsBuildIsMovedOrDeleted.
|
|
const selfExecRole = "MESH_CONTROLLER_SELFEXEC_ROLE"
|
|
|
|
// TestSelfExecServer is a controller standing in, when run as one: it waits until its build has been
|
|
// moved, then runs a verb as the seat runs one, and prints what came of it as JSON.
|
|
func TestSelfExecServer(t *testing.T) {
|
|
if os.Getenv(selfExecRole) != "server" {
|
|
t.Skip("run by TestAVerbRunsWhileItsBuildIsMovedOrDeleted")
|
|
}
|
|
line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
|
if strings.TrimSpace(line) != "go" {
|
|
t.Fatalf("told %q", line)
|
|
}
|
|
if err := os.Setenv(selfExecRole, "verb"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
answer, err := runVerb(t.Context(), []string{"-test.run", "^TestSelfExecVerb$", "-test.v"})
|
|
said := map[string]any{"ok": answer.OK, "output": answer.Output}
|
|
if err != nil {
|
|
said["error"] = err.Error()
|
|
}
|
|
body, _ := json.Marshal(said)
|
|
fmt.Println("ANSWER " + string(body))
|
|
}
|
|
|
|
// TestSelfExecVerb is the verb, when run as one.
|
|
func TestSelfExecVerb(t *testing.T) {
|
|
if os.Getenv(selfExecRole) != "verb" {
|
|
t.Skip("run by TestSelfExecServer")
|
|
}
|
|
fmt.Println("the verb ran")
|
|
}
|
|
|
|
// **A verb runs while the build it was started from is moved where its user may not go, or deleted**
|
|
// (novox/hq issue 289). The node-engine's witness moves a running controller's build into a directory
|
|
// only it may enter as it places the next, and deletes it once the next is proved; the controller
|
|
// still serving in between ran its verbs from the path and answered "permission denied".
|
|
//
|
|
// A copy of this test binary is the controller: started from one place, moved into a directory closed
|
|
// to everyone (or deleted), and only then asked to run a verb.
|
|
func TestAVerbRunsWhileItsBuildIsMovedOrDeleted(t *testing.T) {
|
|
if runtime.GOOS != "linux" {
|
|
t.Skip("the image is named through /proc on Linux only")
|
|
}
|
|
self, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, how := range []string{"moved into a closed directory", "deleted"} {
|
|
t.Run(how, func(t *testing.T) {
|
|
root := t.TempDir()
|
|
placed := filepath.Join(root, "mesh-controller", "mesh-controller")
|
|
if err := os.MkdirAll(filepath.Dir(placed), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
copyFile(t, self, placed)
|
|
|
|
server := exec.Command(placed, "-test.run", "^TestSelfExecServer$", "-test.v")
|
|
server.Env = append(os.Environ(), selfExecRole+"=server")
|
|
stdin, err := server.StdinPipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stdout, err := server.StdoutPipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
server.Stderr = os.Stderr
|
|
if err := server.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = server.Process.Kill(); _ = server.Wait() })
|
|
|
|
// What the witness does to a running build, once the process runs.
|
|
switch how {
|
|
case "deleted":
|
|
if err := os.RemoveAll(filepath.Dir(placed)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
default:
|
|
kept := filepath.Join(root, ".witness", "mesh-controller", "previous")
|
|
if err := os.MkdirAll(filepath.Dir(kept), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Rename(filepath.Dir(placed), kept); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chmod(filepath.Join(root, ".witness"), 0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = os.Chmod(filepath.Join(root, ".witness"), 0o700) })
|
|
}
|
|
if _, err := io.WriteString(stdin, "go\n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, _ := io.ReadAll(stdout)
|
|
_ = server.Wait()
|
|
var said struct {
|
|
OK bool `json:"ok"`
|
|
Output string `json:"output"`
|
|
Error string `json:"error"`
|
|
}
|
|
found := false
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
if rest, ok := strings.CutPrefix(line, "ANSWER "); ok {
|
|
found = json.Unmarshal([]byte(rest), &said) == nil
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("the controller standing in answered nothing:\n%s", out)
|
|
}
|
|
if said.Error != "" || !said.OK || !strings.Contains(said.Output, "the verb ran") {
|
|
t.Fatalf("the verb did not run from the controller's own image after its build was %s: %+v", how, said)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A verb the controller cannot start from its own build is refused as a handover — marked so its
|
|
// caller asks again — never a permission error; and so is one arriving once the controller is stopping.
|
|
func TestAVerbThatCannotRunIsRefusedAsAHandover(t *testing.T) {
|
|
closed := filepath.Join(t.TempDir(), "closed")
|
|
if err := os.MkdirAll(closed, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
was := ownImage
|
|
ownImage = func() string { return filepath.Join(closed, "gone", "mesh-controller") }
|
|
t.Cleanup(func() { ownImage = was })
|
|
_, err := runVerb(t.Context(), []string{"status"})
|
|
if !errors.Is(err, link.ErrHandingOver) {
|
|
t.Fatalf("a build that is not there was answered %v, not a handover", err)
|
|
}
|
|
|
|
ownImage = was
|
|
handingOver.Store(true)
|
|
t.Cleanup(func() { handingOver.Store(false) })
|
|
if _, err := runVerb(t.Context(), []string{"-test.run", "^$"}); !errors.Is(err, link.ErrHandingOver) {
|
|
t.Fatalf("a controller stopping ran a verb: %v", err)
|
|
}
|
|
}
|
|
|
|
func copyFile(t *testing.T, from, to string) {
|
|
t.Helper()
|
|
in, err := os.Open(from)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer in.Close()
|
|
out, err := os.OpenFile(to, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := io.Copy(out, in); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := out.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|