package system import ( "context" "fmt" "strings" "github.com/novox/mesh-host/internal/declaration" ) // alpine is apk and OpenRC. // // The intended first node. Where it differs from systemd is not cosmetic, and each difference // below is a place where the systemd implementation's care had to be re-derived rather than // translated. type alpine struct{} func (alpine) Name() string { return "alpine" } func (alpine) Shapes() []declaration.Type { return everyShape() } func (a alpine) Confirm(ctx context.Context, run Runner) error { // `apk info -e apk-tools` asks the installed-package database about something that is // certainly there. `apk --version` would prove only that a binary exists, which is the // assumption 04-ISSUES/007 records. if _, err := run(ctx, "apk", "info", "-e", "apk-tools"); err != nil { return fmt.Errorf( "this is the alpine host and apk does not answer here. Either this machine is not "+ "Alpine, or its package database is broken: %w", err) } return nil } // PackageInstalled asks apk, having first established that apk answers. // // `apk info -e ` prints the name when installed and NOTHING when not — and exits zero // either way. So unlike pacman, the exit code cannot be used at all here: an empty answer is // the negative. Reading the exit code would report every package as installed. func (a alpine) PackageInstalled(ctx context.Context, run Runner, name string) (bool, error) { if err := a.Confirm(ctx, run); err != nil { return false, fmt.Errorf("nothing can be said about %q: %w", name, err) } out, err := run(ctx, "apk", "info", "-e", name) if err != nil { return false, nil } return strings.TrimSpace(out) != "", nil } func (alpine) InstallPackage(ctx context.Context, run Runner, name string) error { _, err := run(ctx, "apk", "add", "--no-cache", name) return err } // ServiceState reads what OpenRC says about a service. // // The same trap as systemd's, and it needs answering differently because OpenRC has no // LoadState. `rc-service status` exits 3 for a stopped service and 1 for one that does // not exist — but the exit code reaches us wrapped, so the output is read instead: OpenRC says // "does not exist" plainly, and that distinction is the whole reason this function is not a // one-liner. func (alpine) ServiceState(ctx context.Context, run Runner, unit string) (string, error) { out, err := run(ctx, "rc-service", unit, "status") text := strings.ToLower(out + " " + errText(err)) switch { case strings.Contains(text, "does not exist"): return "", fmt.Errorf( "%s does not exist on this machine. A declaration naming a service that is not "+ "installed cannot be satisfied, and reporting it stopped would be reporting "+ "absence as success", unit) case strings.Contains(text, "status: started"), strings.Contains(text, "status: starting"): return "running", nil case strings.Contains(text, "status: stopped"), strings.Contains(text, "status: stopping"): return "stopped", nil case strings.Contains(text, "status: crashed"): // Crashed is not running, and it is not the same as stopped either — but a // declaration can only ask for one of two things, and the honest mapping is that the // service is not up. Starting it is then the right next act. return "stopped", nil case strings.TrimSpace(text) == "": return "", fmt.Errorf("the service manager said nothing about %s", unit) default: return "", fmt.Errorf( "the service manager reports %s as %q, which is neither running nor stopped", unit, strings.TrimSpace(out)) } } func (alpine) SetServiceState(ctx context.Context, run Runner, unit, state string) error { verb := "start" if state == "stopped" { verb = "stop" } _, err := run(ctx, "rc-service", unit, verb) return err } // ServiceBoot reads whether a service is in a runlevel. // // OpenRC has no `is-enabled`. What it has is `rc-update show`, which lists services against the // runlevels they are added to — so "does it start at boot" becomes "does it appear here", and // there is no equivalent of systemd's `static` because OpenRC has no unit files without an // install story. func (alpine) ServiceBoot(ctx context.Context, run Runner, unit string) (string, error) { out, err := run(ctx, "rc-update", "show", "default") if err != nil { return "", fmt.Errorf("cannot read which services start at boot: %w", err) } for _, line := range strings.Split(out, "\n") { // A line looks like ` docker | default`. The name is the first field. name, _, _ := strings.Cut(strings.TrimSpace(line), "|") if strings.TrimSpace(name) == unit { return "enabled", nil } } return "disabled", nil } func (alpine) SetServiceBoot(ctx context.Context, run Runner, unit, boot string) error { verb := "add" if boot == "disabled" { verb = "del" } _, err := run(ctx, "rc-update", verb, unit, "default") return err } func errText(err error) string { if err == nil { return "" } return err.Error() }