carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package instanceexec
2
3import (
4 "encoding/json"
5 "errors"
6 "flag"
7 "fmt"
8 "log"
9 "os"
10 "path/filepath"
11 "regexp"
12 "time"
13
14 "github.com/carlosframework/platform/internal/cliflag"
15 "github.com/carlosframework/platform/internal/instancectl"
16 "github.com/carlosframework/platform/internal/instancefs"
17 "github.com/carlosframework/platform/internal/provision"
18)
19
20// invocationShape is systemd's 128-bit invocation id as 32 hex digits. The
21// id becomes part of a file name, so anything else is refused before it can
22// name a path.
23var invocationShape = regexp.MustCompile(`^[0-9a-f]{32}$`)
24
25// RunExit is `carlos instance-exit <role> <iid>`, the ExecStopPost= of every
26// template unit. It runs inside the namespace as carlos with systemd's
27// $INVOCATION_ID, $SERVICE_RESULT, $EXIT_CODE and $EXIT_STATUS, and leaves
28// the advisory exit record in the instance's own run directory (design
29// §6.5). It never fails the unit: a stop that could not be recorded is
30// still a stop, and the agent proves termination from unit state and the
31// cgroup, not from this file.
32func RunExit(args []string) {
33 fs := flag.NewFlagSet("instance-exit", flag.ExitOnError)
34 runDir := fs.String("run-dir", provision.SocketDir, "run directory holding instances/<id>")
35 fs.Usage = cliflag.Usage(fs, "usage: carlos instance-exit <run|seed|sidecar|cmd> <instance-id>")
36 _ = fs.Parse(args)
37 if fs.NArg() != 2 {
38 fs.Usage()
39 os.Exit(2)
40 }
41 role, ok := instancectl.ParseRole(fs.Arg(0))
42 if !ok {
43 log.Printf("instance-exit: role %q is not run, seed, sidecar or cmd", fs.Arg(0))
44 return
45 }
46 if err := writeExitRecord(*runDir, role, fs.Arg(1), os.Getenv, time.Now()); err != nil {
47 log.Printf("instance-exit: %v", err)
48 }
49}
50
51// writeExitRecord writes exit.<role>.<invocation>.json with O_EXCL into
52// <runDir>/instances/<iid>. A missing invocation id is a no-op (the
53// variables are systemd's to provide); a malformed one is an error.
54func writeExitRecord(runDir string, role instancectl.Role, inst string, env func(string) string, now time.Time) error {
55 inv := env("INVOCATION_ID")
56 if inv == "" {
57 return nil
58 }
59 if !invocationShape.MatchString(inv) {
60 return fmt.Errorf("INVOCATION_ID %q is not 32 hex digits", inv)
61 }
62 iid, name, err := recordTarget(role, inst, inv)
63 if err != nil {
64 return err
65 }
66 rec := instancectl.ExitRecord{
67 Invocation: inv,
68 Result: env("SERVICE_RESULT"),
69 Code: env("EXIT_CODE"),
70 Status: env("EXIT_STATUS"),
71 At: now.UTC(),
72 }
73 raw, err := json.MarshalIndent(rec, "", " ")
74 if err != nil {
75 return err
76 }
77 raw = append(raw, '\n')
78 dir := filepath.Join(runDir, "instances", iid)
79 // 0600: this record carries SERVICE_RESULT/EXIT_CODE/EXIT_STATUS from
80 // systemd, which is diagnostic detail about the instance's own process,
81 // not something every reader of the instance directory needs (final
82 // review, plan 2, item 6).
83 f, err := instancefs.OpenBeneath(dir, name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
84 if err != nil {
85 if errors.Is(err, os.ErrExist) {
86 return fmt.Errorf("record for invocation %s already exists", inv)
87 }
88 return err
89 }
90 if _, err := f.Write(raw); err != nil {
91 f.Close()
92 return err
93 }
94 return f.Close()
95}
96