96 comments across the two repos named records that no longer exist. Each now points at the consolidated record that holds its reasoning -- ADR 0034 (a test defends a decision) is 0017, the eight host records are 0005, the four lab records are 0016. Worth noting for next time: these are references from outside HQ, so renumbering there is not free. It cost 38 files here.
146 lines
5.4 KiB
Go
146 lines
5.4 KiB
Go
// Package system is the part of the host that differs between operating systems.
|
|
//
|
|
// novox/hq ADR 0005. A machine has apk because it is Alpine; the package manager, the service
|
|
// manager and the packaging format arrive together as one decision somebody made at install
|
|
// time. So they are not independent knobs — they are one implementation, named after the system
|
|
// it belongs to.
|
|
//
|
|
// Everything else in the host is shared: the declaration vocabulary, the store, the apply loop,
|
|
// the read-back discipline, the refusal model, the link. What lives here is two appliers' worth
|
|
// of difference and the probes that go with them.
|
|
//
|
|
// Not abstracted behind a lowest common denominator, deliberately. `systemctl show` reports a
|
|
// LoadState that separates *not installed* from *stopped*, and OpenRC has no equivalent — an
|
|
// interface spanning both would have to drop it, and dropping it is how absence gets reported
|
|
// as success. Each system says what it can say.
|
|
package system
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
)
|
|
|
|
// Runner executes a command. The real one runs a process; tests pass one that records what was
|
|
// asked for, because what is being tested is which commands each system issues.
|
|
type Runner func(ctx context.Context, name string, args ...string) (string, error)
|
|
|
|
// ErrUnsupported is what a system returns for a shape it cannot implement.
|
|
//
|
|
// Not an error in the ordinary sense — an Android host declining to install packages is
|
|
// correct, not broken. It is refused at the declaration rather than attempted and failed, so a
|
|
// control plane learns the difference from the profile instead of from a stack trace.
|
|
var ErrUnsupported = errors.New("this host does not implement that")
|
|
|
|
// System is one operating system's half of the host.
|
|
type System interface {
|
|
// Name is what this host was built for: "arch", "alpine", "android".
|
|
Name() string
|
|
|
|
// Shapes are the declaration types this host can apply. Anything else is refused whole.
|
|
Shapes() []declaration.Type
|
|
|
|
// Confirm proves this is the system the host was built for.
|
|
//
|
|
// A host installed on the wrong machine must say so, not discover it by calling a package
|
|
// manager that is not there. The failure is legible exactly once, at start.
|
|
Confirm(ctx context.Context, run Runner) error
|
|
|
|
PackageInstalled(ctx context.Context, run Runner, name string) (bool, error)
|
|
InstallPackage(ctx context.Context, run Runner, name string) error
|
|
|
|
// ServiceState is "running" or "stopped". A unit that does not exist is an error, never
|
|
// "stopped" — reporting absence as satisfaction is the fault this host exists to prevent.
|
|
ServiceState(ctx context.Context, run Runner, unit string) (string, error)
|
|
SetServiceState(ctx context.Context, run Runner, unit, state string) error
|
|
|
|
// ServiceBoot is "enabled" or "disabled" — whether the unit starts at boot.
|
|
ServiceBoot(ctx context.Context, run Runner, unit string) (string, error)
|
|
SetServiceBoot(ctx context.Context, run Runner, unit, boot string) error
|
|
}
|
|
|
|
// Supports reports whether this host can apply a shape.
|
|
func Supports(s System, t declaration.Type) bool {
|
|
for _, shape := range s.Shapes() {
|
|
if shape == t {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Check refuses a declaration naming a shape this host cannot apply.
|
|
//
|
|
// Refused whole and before anything is applied, which is the same treatment an unknown type
|
|
// gets (novox/hq ADR 0005) — a host that applied the parts it understood would leave a machine
|
|
// that looks configured and is not. The reason differs and the outcome does not.
|
|
func Check(s System, d *declaration.Declaration) error {
|
|
var problems []string
|
|
seen := map[declaration.Type]bool{}
|
|
for _, r := range d.Resources {
|
|
t := r.Kind()
|
|
if Supports(s, t) || seen[t] {
|
|
continue
|
|
}
|
|
seen[t] = true
|
|
problems = append(problems, fmt.Sprintf(
|
|
"resource %q is a %s, and the %s host does not implement that shape. This host "+
|
|
"applies %s",
|
|
r.Identity(), t, s.Name(), shapeList(s)))
|
|
}
|
|
if len(problems) > 0 {
|
|
return &declaration.RefusalError{Problems: problems}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func shapeList(s System) string {
|
|
names := make([]string, 0, len(s.Shapes()))
|
|
for _, t := range s.Shapes() {
|
|
names = append(names, string(t))
|
|
}
|
|
return strings.Join(names, ", ")
|
|
}
|
|
|
|
// everyShape is what a host on a full operating system can apply.
|
|
func everyShape() []declaration.Type {
|
|
return []declaration.Type{
|
|
declaration.TypeDirectory, declaration.TypeFile, declaration.TypeService,
|
|
declaration.TypePackage, declaration.TypeContainer, declaration.TypeAction,
|
|
}
|
|
}
|
|
|
|
// portableShapes need only a filesystem and a way to run something.
|
|
//
|
|
// The floor. A host that can do nothing else can still do these, which is what makes a partial
|
|
// host a real thing rather than a broken one (novox/hq ADR 0005).
|
|
func portableShapes() []declaration.Type {
|
|
return []declaration.Type{
|
|
declaration.TypeDirectory, declaration.TypeFile, declaration.TypeAction,
|
|
}
|
|
}
|
|
|
|
// For returns the system with this name, or an error naming the ones that exist.
|
|
func For(name string) (System, error) {
|
|
for _, s := range All() {
|
|
if s.Name() == name {
|
|
return s, nil
|
|
}
|
|
}
|
|
var names []string
|
|
for _, s := range All() {
|
|
names = append(names, s.Name())
|
|
}
|
|
return nil, fmt.Errorf(
|
|
"this host was built for %q, which is not a system it knows. Built hosts are: %s",
|
|
name, strings.Join(names, ", "))
|
|
}
|
|
|
|
// All is every system the host can be built for.
|
|
func All() []System {
|
|
return []System{arch{}, alpine{}, android{}}
|
|
}
|