| 1 | package activator |
| 2 | |
| 3 | import ( |
| 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. |
| 17 | type 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 | |
| 30 | var _ 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. |
| 38 | type 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 | |
| 47 | var _ 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. |
| 57 | type 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 | |
| 62 | var _ 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. |
| 67 | type UnitState func(ctx context.Context, unit string) (active, invocation string, err error) |
| 68 | |
| 69 | func 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. |
| 91 | type CgroupEvents func(slice, unit string) (exists, populated bool, err error) |
| 92 | |
| 93 | func 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 | |