| new file mode 100644 |
| index 000000000..73810a815 |
| --- /dev/null |
| +++ b/internal/instancectl/names.go |
| @@ -0,0 +1,185 @@ |
| +// Package instancectl is the root instance-control channel of the |
| +// isolated-exec design (§5) and the names everything on both sides of it |
| +// agrees on: unit and slice names keyed by the bounded instance identifier, |
| +// the launch file the trampoline reads (§6.2), the advisory exit record |
| +// (§6.5) and the probe's verdict (§8.5). |
| +// |
| +// The names live in one package because three programs and two services |
| +// build them: the agent (plan 3) composes launch files and requests, the |
| +// control service builds unit names from validated hosts, and the |
| +// in-namespace programs read files keyed by the same identifier. A |
| +// disagreement between any two of them would be a unit that never starts |
| +// or a record nobody finds. |
| +package instancectl |
| + |
| +import ( |
| + "fmt" |
| + "strings" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/provision" |
| +) |
| + |
| +// Role is which of the three template units a request or a file is about. |
| +type Role string |
| + |
| +const ( |
| + RoleRun Role = "run" |
| + RoleSeed Role = "seed" |
| + RoleSidecar Role = "sidecar" |
| +) |
| + |
| +// ParseRole accepts exactly the three literals. |
| +func ParseRole(s string) (Role, bool) { |
| + switch Role(s) { |
| + case RoleRun, RoleSeed, RoleSidecar: |
| + return Role(s), true |
| + } |
| + return "", false |
| +} |
| + |
| +// Escape is systemd-escape(1)'s rule for a unit instance name: "/" becomes |
| +// "-", a leading "." becomes "\x2e", and every byte outside [a-zA-Z0-9:_.] |
| +// becomes "\x" plus two lowercase hex digits. For the identifiers this |
| +// platform produces (hosts are [a-z0-9.-], a hashed identifier adds a |
| +// leading underscore) that means "-" becomes "\x2d" and nothing else |
| +// changes; the general rule is kept so an unexpected byte can never yield |
| +// a name systemd refuses to load. |
| +func Escape(s string) string { |
| + var b strings.Builder |
| + for i := 0; i < len(s); i++ { |
| + c := s[i] |
| + switch { |
| + case c == '/': |
| + b.WriteByte('-') |
| + case i == 0 && c == '.': |
| + b.WriteString(`\x2e`) |
| + case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9', c == ':', c == '_', c == '.': |
| + b.WriteByte(c) |
| + default: |
| + fmt.Fprintf(&b, `\x%02x`, c) |
| + } |
| + } |
| + return b.String() |
| +} |
| + |
| +// The three template prefixes. A unit under any of them is the platform's |
| +// to start, stop and sweep; nothing else is (design §5.3). |
| +const ( |
| + UnitPrefixRun = "carlos-instance@" |
| + UnitPrefixSeed = "carlos-instance-seed@" |
| + UnitPrefixSidecar = "carlos-instance-sidecar@" |
| +) |
| + |
| +func prefixFor(role Role) string { |
| + switch role { |
| + case RoleSeed: |
| + return UnitPrefixSeed |
| + case RoleSidecar: |
| + return UnitPrefixSidecar |
| + } |
| + return UnitPrefixRun |
| +} |
| + |
| +// UnitName is the template instance for a role and a host: |
| +// carlos-instance[-seed|-sidecar]@<Escape(InstanceID(host))>.service. |
| +func UnitName(role Role, host string) string { |
| + return prefixFor(role) + Escape(provision.InstanceID(host)) + ".service" |
| +} |
| + |
| +// SliceName is the per-host slice every role of a host runs in (design |
| +// §4.3): carlos-instances-<Escape(InstanceID(host))>.slice. |
| +func SliceName(host string) string { |
| + return "carlos-instances-" + Escape(provision.InstanceID(host)) + ".slice" |
| +} |
| + |
| +// ParseUnitName recognises the three templates' instances and returns the |
| +// role and the ESCAPED identifier (systemd reports names escaped). Anything |
| +// else, including a template with no instance, is not ours. |
| +func ParseUnitName(unit string) (Role, string, bool) { |
| + if !strings.HasSuffix(unit, ".service") { |
| + return "", "", false |
| + } |
| + body := strings.TrimSuffix(unit, ".service") |
| + for _, p := range []struct { |
| + prefix string |
| + role Role |
| + }{{UnitPrefixSeed, RoleSeed}, {UnitPrefixSidecar, RoleSidecar}, {UnitPrefixRun, RoleRun}} { |
| + if strings.HasPrefix(body, p.prefix) { |
| + esc := strings.TrimPrefix(body, p.prefix) |
| + if esc == "" { |
| + return "", "", false |
| + } |
| + return p.role, esc, true |
| + } |
| + } |
| + return "", "", false |
| +} |
| + |
| +// LaunchSchema is the only launch-file schema this release understands. |
| +const LaunchSchema = 1 |
| + |
| +// Launch is what the agent writes into the instance directory before a |
| +// start and what the trampoline execs (design §6.2). Env is the complete |
| +// environment, one entry per key, exactly as the agent composed it; the |
| +// trampoline parses nothing. |
| +type Launch struct { |
| + Schema int `json:"schema"` |
| + Host string `json:"host"` |
| + Argv []string `json:"argv"` |
| + Env []string `json:"env"` |
| +} |
| + |
| +// LaunchFileName is "launch.<role>.json" inside the instance directory. |
| +func LaunchFileName(role Role) string { return "launch." + string(role) + ".json" } |
| + |
| +// ExitRecord is the advisory record the unit's ExecStopPost leaves in the |
| +// instance's run directory (design §6.5). It decides the wording of the |
| +// member-facing event line and nothing else. |
| +type ExitRecord struct { |
| + Invocation string `json:"invocation"` |
| + Result string `json:"result"` |
| + Code string `json:"code"` |
| + Status string `json:"status"` |
| + At time.Time `json:"at"` |
| +} |
| + |
| +// ExitRecordName is "exit.<role>.<invocation>.json". |
| +func ExitRecordName(role Role, invocation string) string { |
| + return "exit." + string(role) + "." + invocation + ".json" |
| +} |
| + |
| +// ProbeDirName is the instance directory the readiness probe runs in. It |
| +// starts with an underscore, which no valid host does, so it can never |
| +// collide with a real instance's directory. |
| +const ProbeDirName = "_probe" |
| + |
| +// VerdictLine is one check of the readiness probe (design §8.5). Verdict is |
| +// "ok", "unverified" or "fail"; only "fail" blocks readiness. |
| +type VerdictLine struct { |
| + Check string `json:"check"` |
| + Verdict string `json:"verdict"` |
| + Detail string `json:"detail,omitempty"` |
| +} |
| + |
| +// ProbeReport is what the probe prints and what /ready answers with. |
| +type ProbeReport struct { |
| + Ready bool `json:"ready"` |
| + Lines []VerdictLine `json:"lines"` |
| +} |
| + |
| +// Responses of the control verbs (design §5.3). |
| +type StartResponse struct { |
| + InvocationID string `json:"invocation_id"` |
| +} |
| + |
| +type SeedResponse struct { |
| + InvocationID string `json:"invocation_id"` |
| + Result string `json:"result"` |
| + Status string `json:"status"` |
| + Code string `json:"code"` |
| +} |
| + |
| +type SweepResponse struct { |
| + Units []string `json:"units"` |
| +} |