carlos / platform Public

instanceexec: the advisory exit record, written O_EXCL from systemd's four variables (plan 2, task 3)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZ1ZvWj7naf73GjTX8MBv5
Paul Campbell pushed by paul@keymail.dev f55b80acdce46c74bd2036cbbf0e51ec7e5ab847 parent f7cd33d
2 files changed, +165 −0
  • internal/instanceexec/exit.go +87 −0
  • internal/instanceexec/exit_test.go +78 −0
diff --git a/internal/instanceexec/exit.go b/internal/instanceexec/exit.go
new file mode 100644
index 000000000..930933c6e
--- /dev/null
+++ b/internal/instanceexec/exit.go
@@ -0,0 +1,87 @@
+package instanceexec
+
+import (
+ "encoding/json"
+ "errors"
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "regexp"
+ "time"
+
+ "github.com/carlosframework/platform/internal/cliflag"
+ "github.com/carlosframework/platform/internal/instancectl"
+ "github.com/carlosframework/platform/internal/instancefs"
+ "github.com/carlosframework/platform/internal/provision"
+)
+
+// invocationShape is systemd's 128-bit invocation id as 32 hex digits. The
+// id becomes part of a file name, so anything else is refused before it can
+// name a path.
+var invocationShape = regexp.MustCompile(`^[0-9a-f]{32}$`)
+
+// RunExit is `carlos instance-exit <role> <iid>`, the ExecStopPost= of every
+// template unit. It runs inside the namespace as carlos with systemd's
+// $INVOCATION_ID, $SERVICE_RESULT, $EXIT_CODE and $EXIT_STATUS, and leaves
+// the advisory exit record in the instance's own run directory (design
+// §6.5). It never fails the unit: a stop that could not be recorded is
+// still a stop, and the agent proves termination from unit state and the
+// cgroup, not from this file.
+func RunExit(args []string) {
+ fs := flag.NewFlagSet("instance-exit", flag.ExitOnError)
+ runDir := fs.String("run-dir", provision.SocketDir, "run directory holding instances/<id>")
+ fs.Usage = cliflag.Usage(fs, "usage: carlos instance-exit <run|seed|sidecar> <instance-id>")
+ _ = fs.Parse(args)
+ if fs.NArg() != 2 {
+ fs.Usage()
+ os.Exit(2)
+ }
+ role, ok := instancectl.ParseRole(fs.Arg(0))
+ if !ok {
+ log.Printf("instance-exit: role %q is not run, seed or sidecar", fs.Arg(0))
+ return
+ }
+ if err := writeExitRecord(*runDir, role, fs.Arg(1), os.Getenv, time.Now()); err != nil {
+ log.Printf("instance-exit: %v", err)
+ }
+}
+
+// writeExitRecord writes exit.<role>.<invocation>.json with O_EXCL into
+// <runDir>/instances/<iid>. A missing invocation id is a no-op (the
+// variables are systemd's to provide); a malformed one is an error.
+func writeExitRecord(runDir string, role instancectl.Role, iid string, env func(string) string, now time.Time) error {
+ inv := env("INVOCATION_ID")
+ if inv == "" {
+ return nil
+ }
+ if !invocationShape.MatchString(inv) {
+ return fmt.Errorf("INVOCATION_ID %q is not 32 hex digits", inv)
+ }
+ rec := instancectl.ExitRecord{
+ Invocation: inv,
+ Result: env("SERVICE_RESULT"),
+ Code: env("EXIT_CODE"),
+ Status: env("EXIT_STATUS"),
+ At: now.UTC(),
+ }
+ raw, err := json.MarshalIndent(rec, "", " ")
+ if err != nil {
+ return err
+ }
+ raw = append(raw, '\n')
+ dir := filepath.Join(runDir, "instances", iid)
+ f, err := instancefs.OpenBeneath(dir, instancectl.ExitRecordName(role, inv), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
+ if err != nil {
+ if errors.Is(err, os.ErrExist) {
+ return fmt.Errorf("record for invocation %s already exists", inv)
+ }
+ return err
+ }
+ if _, err := f.Write(raw); err != nil {
+ f.Close()
+ return err
+ }
+ return f.Close()
+}
diff --git a/internal/instanceexec/exit_test.go b/internal/instanceexec/exit_test.go
new file mode 100644
index 000000000..e3c8183d0
--- /dev/null
+++ b/internal/instanceexec/exit_test.go
@@ -0,0 +1,78 @@
+//go:build linux
+
+package instanceexec
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/instancectl"
+)
+
+func envOf(m map[string]string) func(string) string {
+ return func(k string) string { return m[k] }
+}
+
+func TestWriteExitRecordWritesTheFourVariables(t *testing.T) {
+ run := t.TempDir()
+ iid := "h.example.com"
+ if err := os.MkdirAll(filepath.Join(run, "instances", iid), 0o700); err != nil {
+ t.Fatal(err)
+ }
+ now := time.Date(2026, 9, 11, 12, 0, 0, 0, time.UTC)
+ env := envOf(map[string]string{"INVOCATION_ID": "deadbeefdeadbeefdeadbeefdeadbeef", "SERVICE_RESULT": "exit-code", "EXIT_CODE": "exited", "EXIT_STATUS": "3"})
+ if err := writeExitRecord(run, instancectl.RoleRun, iid, env, now); err != nil {
+ t.Fatalf("writeExitRecord: %v", err)
+ }
+ b, err := os.ReadFile(filepath.Join(run, "instances", iid, "exit.run.deadbeefdeadbeefdeadbeefdeadbeef.json"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var rec instancectl.ExitRecord
+ if err := json.Unmarshal(b, &rec); err != nil {
+ t.Fatal(err)
+ }
+ if rec.Invocation != "deadbeefdeadbeefdeadbeefdeadbeef" || rec.Result != "exit-code" || rec.Code != "exited" || rec.Status != "3" || !rec.At.Equal(now) {
+ t.Errorf("record = %+v", rec)
+ }
+ // O_EXCL: a second write for the same invocation is refused and the
+ // first record stands.
+ env2 := envOf(map[string]string{"INVOCATION_ID": "deadbeefdeadbeefdeadbeefdeadbeef", "SERVICE_RESULT": "success", "EXIT_CODE": "exited", "EXIT_STATUS": "0"})
+ if err := writeExitRecord(run, instancectl.RoleRun, iid, env2, now); err == nil {
+ t.Error("second write for the same invocation succeeded")
+ }
+ b2, _ := os.ReadFile(filepath.Join(run, "instances", iid, "exit.run.deadbeefdeadbeefdeadbeefdeadbeef.json"))
+ if string(b2) != string(b) {
+ t.Error("first record was overwritten")
+ }
+}
+
+func TestWriteExitRecordWithoutInvocationIsANoop(t *testing.T) {
+ run := t.TempDir()
+ iid := "h.example.com"
+ if err := os.MkdirAll(filepath.Join(run, "instances", iid), 0o700); err != nil {
+ t.Fatal(err)
+ }
+ if err := writeExitRecord(run, instancectl.RoleSeed, iid, envOf(nil), time.Now()); err != nil {
+ t.Fatalf("no INVOCATION_ID: %v (must not fail the unit's stop)", err)
+ }
+ entries, _ := os.ReadDir(filepath.Join(run, "instances", iid))
+ if len(entries) != 0 {
+ t.Errorf("wrote %v without an invocation id", entries)
+ }
+}
+
+func TestWriteExitRecordRefusesAnInvocationThatIsNotAName(t *testing.T) {
+ run := t.TempDir()
+ iid := "h.example.com"
+ if err := os.MkdirAll(filepath.Join(run, "instances", iid), 0o700); err != nil {
+ t.Fatal(err)
+ }
+ env := envOf(map[string]string{"INVOCATION_ID": "../../etc/x", "SERVICE_RESULT": "success"})
+ if err := writeExitRecord(run, instancectl.RoleRun, iid, env, time.Now()); err == nil {
+ t.Error("an invocation id with a path separator was accepted")
+ }
+}