carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3import (
4 "context"
5 "errors"
6 "os"
7 "os/exec"
8 "path/filepath"
9 "strings"
10
11 "github.com/carlosframework/platform/internal/instancectl"
12)
13
14// Control is what the exec backend needs from the root instance-control
15// channel (design §5.3). instancectl.Client is the production
16// implementation; tests use a fake.
17type Control interface {
18 Start(ctx context.Context, host string, role instancectl.Role, memoryMax int64) (string, error)
19 Seed(ctx context.Context, host string, memoryMax int64) (instancectl.SeedResponse, error)
20 // Cmd starts host's cmd unit and returns its invocation; CmdStatus
21 // long-polls it (run spec §5.4).
22 Cmd(ctx context.Context, host string, memoryMax int64) (string, error)
23 CmdStatus(ctx context.Context, host, invocation string) (instancectl.CmdStatus, error)
24 Stop(ctx context.Context, host string, role instancectl.Role) error
25 Reset(ctx context.Context, host string, role instancectl.Role) error
26 Sweep(ctx context.Context) (instancectl.SweepResponse, error)
27 Ready(ctx context.Context) error
28}
29
30var _ Control = (*instancectl.Client)(nil)
31
32// GenControl is the generation half of the instance-control channel
33// (zero-downtime spec §7.6): the carlos-instance-gen@<id>:<n> template's
34// start, stop and reset, and the host slice's parent limit (§7.3). It is a
35// separate interface so a Control without it (a fake, an older client)
36// still satisfies Control. The backend asks for it only while
37// GenContainment is on.
38type GenControl interface {
39 StartGen(ctx context.Context, host string, n uint64, memoryMax int64) (string, error)
40 StartGenExe(ctx context.Context, host string, n uint64, memoryMax int64, x instancectl.StagedExe) (string, error)
41 StartSidecarExe(ctx context.Context, host string, memoryMax int64, x instancectl.StagedExe) (string, error)
42 StopGen(ctx context.Context, host string, n uint64, kill bool) error
43 ResetGen(ctx context.Context, host string, n uint64) error
44 Parent(ctx context.Context, host string, memoryMax int64) error
45}
46
47var _ GenControl = (*instancectl.Client)(nil)
48
49// StagedExeControl is the original templates' half of naming an
50// executable (settings spec §4.2): the run unit at generation 0 and the
51// seed unit, started on a staged build root validates and binds in place
52// of the live link. A cold wake runs the build it recorded through it,
53// with generations on or off. It is separate from Control and GenControl
54// so a Control without it (a fake, an older client) still satisfies both;
55// the backend asks for it only when it has a file to name, and refuses
56// the start without it (stagedExeControl), never binding the link instead.
57type StagedExeControl interface {
58 StartExe(ctx context.Context, host string, role instancectl.Role, memoryMax int64, x instancectl.StagedExe) (string, error)
59 SeedExe(ctx context.Context, host string, memoryMax int64, x instancectl.StagedExe) (instancectl.SeedResponse, error)
60}
61
62var _ StagedExeControl = (*instancectl.Client)(nil)
63
64// UnitState answers `systemctl show -p ActiveState,InvocationID <unit>`,
65// which needs no privilege. An unloaded unit answers inactive with an
66// empty invocation.
67type UnitState func(ctx context.Context, unit string) (active, invocation string, err error)
68
69func systemctlUnitState(ctx context.Context, unit string) (string, string, error) {
70 out, err := exec.CommandContext(ctx, "systemctl", "show", "-p", "ActiveState,InvocationID", unit).Output()
71 if err != nil {
72 return "", "", err
73 }
74 var active, inv string
75 for _, line := range strings.Split(string(out), "\n") {
76 if k, v, ok := strings.Cut(line, "="); ok {
77 switch k {
78 case "ActiveState":
79 active = strings.TrimSpace(v)
80 case "InvocationID":
81 inv = strings.TrimSpace(v)
82 }
83 }
84 }
85 return active, inv, nil
86}
87
88// CgroupEvents reads the unit's cgroup.events under the host slice. cgroup
89// v2 files are world-readable, so the agent reads them as carlos, as the
90// sampler does. An absent directory is the unit's cgroup already removed.
91type CgroupEvents func(slice, unit string) (exists, populated bool, err error)
92
93func cgroupEventsUnder(root string) CgroupEvents {
94 return func(slice, unit string) (bool, bool, error) {
95 b, err := os.ReadFile(filepath.Join(root, "carlos.slice", "carlos-instances.slice", slice, unit, "cgroup.events"))
96 if errors.Is(err, os.ErrNotExist) {
97 return false, false, nil
98 }
99 if err != nil {
100 return true, true, err
101 }
102 return true, !strings.Contains(string(b), "populated 0"), nil
103 }
104}
105